diff --git a/m4/ax_python_devel.m4 b/m4/ax_python_devel.m4 index 44dbd83e0053f9e23cfd782a2c016d246178de79..780584eaeb5d27b79d8a248c4dae78c3997d2164 100644 --- a/m4/ax_python_devel.m4 +++ b/m4/ax_python_devel.m4 @@ -67,7 +67,7 @@ # modified version of the Autoconf Macro, you may extend this special # exception to the GPL to apply to your modified version as well. -#serial 21 +#serial 32 AU_ALIAS([AC_PYTHON_DEVEL], [AX_PYTHON_DEVEL]) AC_DEFUN([AX_PYTHON_DEVEL],[ @@ -112,15 +112,39 @@ to something else than an empty string. fi # - # if the macro parameter ``version'' is set, honour it + # If the macro parameter ``version'' is set, honour it. + # A Python shim class, VPy, is used to implement correct version comparisons via + # string expressions, since e.g. a naive textual ">= 2.7.3" won't work for + # Python 2.7.10 (the ".1" being evaluated as less than ".3"). # if test -n "$1"; then AC_MSG_CHECKING([for a version of Python $1]) - ac_supports_python_ver=`$PYTHON -c "import sys; \ - ver = sys.version.split ()[[0]]; \ + cat << EOF > ax_python_devel_vpy.py +class VPy: + def vtup(self, s): + return tuple(map(int, s.strip().replace("rc", ".").split("."))) + def __init__(self): + import sys + self.vpy = tuple(sys.version_info) + def __eq__(self, s): + return self.vpy == self.vtup(s) + def __ne__(self, s): + return self.vpy != self.vtup(s) + def __lt__(self, s): + return self.vpy < self.vtup(s) + def __gt__(self, s): + return self.vpy > self.vtup(s) + def __le__(self, s): + return self.vpy <= self.vtup(s) + def __ge__(self, s): + return self.vpy >= self.vtup(s) +EOF + ac_supports_python_ver=`$PYTHON -c "import ax_python_devel_vpy; \ + ver = ax_python_devel_vpy.VPy(); \ print (ver $1)"` + rm -rf ax_python_devel_vpy*.py* __pycache__/ax_python_devel_vpy*.py* if test "$ac_supports_python_ver" = "True"; then - AC_MSG_RESULT([yes]) + AC_MSG_RESULT([yes]) else AC_MSG_RESULT([no]) AC_MSG_ERROR([this package requires Python $1. @@ -135,16 +159,25 @@ variable to configure. See ``configure --help'' for reference. # # Check if you have distutils, else fail # - AC_MSG_CHECKING([for the distutils Python package]) - ac_distutils_result=`$PYTHON -c "import distutils" 2>&1` + AC_MSG_CHECKING([for the sysconfig Python package]) + ac_sysconfig_result=`$PYTHON -c "import sysconfig" 2>&1` if test $? -eq 0; then AC_MSG_RESULT([yes]) + IMPORT_SYSCONFIG="import sysconfig" else AC_MSG_RESULT([no]) - AC_MSG_ERROR([cannot import Python module "distutils". + + AC_MSG_CHECKING([for the distutils Python package]) + ac_sysconfig_result=`$PYTHON -c "from distutils import sysconfig" 2>&1` + if test $? -eq 0; then + AC_MSG_RESULT([yes]) + IMPORT_SYSCONFIG="from distutils import sysconfig" + else + AC_MSG_ERROR([cannot import Python module "distutils". Please check your Python installation. The error was: -$ac_distutils_result]) - PYTHON_VERSION="" +$ac_sysconfig_result]) + PYTHON_VERSION="" + fi fi # @@ -152,10 +185,19 @@ $ac_distutils_result]) # AC_MSG_CHECKING([for Python include path]) if test -z "$PYTHON_CPPFLAGS"; then - python_path=`$PYTHON -c "import distutils.sysconfig; \ - print (distutils.sysconfig.get_python_inc ());"` - plat_python_path=`$PYTHON -c "import distutils.sysconfig; \ - print (distutils.sysconfig.get_python_inc (plat_specific=1));"` + if test "$IMPORT_SYSCONFIG" = "import sysconfig"; then + # sysconfig module has different functions + python_path=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + print (sysconfig.get_path ('include'));"` + plat_python_path=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + print (sysconfig.get_path ('platinclude'));"` + else + # old distutils way + python_path=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + print (sysconfig.get_python_inc ());"` + plat_python_path=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + print (sysconfig.get_python_inc (plat_specific=1));"` + fi if test -n "${python_path}"; then if test "${plat_python_path}" != "${python_path}"; then python_path="-I$python_path -I$plat_python_path" @@ -179,7 +221,7 @@ $ac_distutils_result]) # join all versioning strings, on some systems # major/minor numbers could be in different list elements -from distutils.sysconfig import * +from sysconfig import * e = get_config_var('VERSION') if e is not None: print(e) @@ -190,7 +232,7 @@ EOD` ac_python_version=$PYTHON_VERSION else ac_python_version=`$PYTHON -c "import sys; \ - print (sys.version[[:3]])"` + print ("%d.%d" % sys.version_info[[:2]])"` fi fi @@ -202,8 +244,8 @@ EOD` ac_python_libdir=`cat<<EOD | $PYTHON - # There should be only one -import distutils.sysconfig -e = distutils.sysconfig.get_config_var('LIBDIR') +$IMPORT_SYSCONFIG +e = sysconfig.get_config_var('LIBDIR') if e is not None: print (e) EOD` @@ -211,8 +253,8 @@ EOD` # Now, for the library: ac_python_library=`cat<<EOD | $PYTHON - -import distutils.sysconfig -c = distutils.sysconfig.get_config_vars() +$IMPORT_SYSCONFIG +c = sysconfig.get_config_vars() if 'LDVERSION' in c: print ('python'+c[['LDVERSION']]) else: @@ -231,7 +273,7 @@ EOD` else # old way: use libpython from python_configdir ac_python_libdir=`$PYTHON -c \ - "from distutils.sysconfig import get_python_lib as f; \ + "from sysconfig import get_python_lib as f; \ import os; \ print (os.path.join(f(plat_specific=1, standard_lib=1), 'config'));"` PYTHON_LIBS="-L$ac_python_libdir -lpython$ac_python_version" @@ -252,19 +294,66 @@ EOD` # AC_MSG_CHECKING([for Python site-packages path]) if test -z "$PYTHON_SITE_PKG"; then - PYTHON_SITE_PKG=`$PYTHON -c "import distutils.sysconfig; \ - print (distutils.sysconfig.get_python_lib(0,0));"` + if test "$IMPORT_SYSCONFIG" = "import sysconfig"; then + PYTHON_SITE_PKG=`$PYTHON -c " +$IMPORT_SYSCONFIG; +if hasattr(sysconfig, 'get_default_scheme'): + scheme = sysconfig.get_default_scheme() +else: + scheme = sysconfig._get_default_scheme() +if scheme == 'posix_local': + # Debian's default scheme installs to /usr/local/ but we want to find headers in /usr/ + scheme = 'posix_prefix' +prefix = '$prefix' +if prefix == 'NONE': + prefix = '$ac_default_prefix' +sitedir = sysconfig.get_path('purelib', scheme, vars={'base': prefix}) +print(sitedir)"` + else + # distutils.sysconfig way + PYTHON_SITE_PKG=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + print (sysconfig.get_python_lib(0,0));"` + fi fi AC_MSG_RESULT([$PYTHON_SITE_PKG]) AC_SUBST([PYTHON_SITE_PKG]) + # + # Check for platform-specific site packages + # + AC_MSG_CHECKING([for Python platform specific site-packages path]) + if test -z "$PYTHON_PLATFORM_SITE_PKG"; then + if test "$IMPORT_SYSCONFIG" = "import sysconfig"; then + PYTHON_PLATFORM_SITE_PKG=`$PYTHON -c " +$IMPORT_SYSCONFIG; +if hasattr(sysconfig, 'get_default_scheme'): + scheme = sysconfig.get_default_scheme() +else: + scheme = sysconfig._get_default_scheme() +if scheme == 'posix_local': + # Debian's default scheme installs to /usr/local/ but we want to find headers in /usr/ + scheme = 'posix_prefix' +prefix = '$prefix' +if prefix == 'NONE': + prefix = '$ac_default_prefix' +sitedir = sysconfig.get_path('platlib', scheme, vars={'platbase': prefix}) +print(sitedir)"` + else + # distutils.sysconfig way + PYTHON_PLATFORM_SITE_PKG=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + print (sysconfig.get_python_lib(1,0));"` + fi + fi + AC_MSG_RESULT([$PYTHON_PLATFORM_SITE_PKG]) + AC_SUBST([PYTHON_PLATFORM_SITE_PKG]) + # # libraries which must be linked in when embedding # AC_MSG_CHECKING(python extra libraries) if test -z "$PYTHON_EXTRA_LIBS"; then - PYTHON_EXTRA_LIBS=`$PYTHON -c "import distutils.sysconfig; \ - conf = distutils.sysconfig.get_config_var; \ + PYTHON_EXTRA_LIBS=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + conf = sysconfig.get_config_var; \ print (conf('LIBS') + ' ' + conf('SYSLIBS'))"` fi AC_MSG_RESULT([$PYTHON_EXTRA_LIBS]) @@ -275,8 +364,8 @@ EOD` # AC_MSG_CHECKING(python extra linking flags) if test -z "$PYTHON_EXTRA_LDFLAGS"; then - PYTHON_EXTRA_LDFLAGS=`$PYTHON -c "import distutils.sysconfig; \ - conf = distutils.sysconfig.get_config_var; \ + PYTHON_EXTRA_LDFLAGS=`$PYTHON -c "$IMPORT_SYSCONFIG; \ + conf = sysconfig.get_config_var; \ print (conf('LINKFORSHARED'))"` fi AC_MSG_RESULT([$PYTHON_EXTRA_LDFLAGS]) @@ -290,7 +379,7 @@ EOD` ac_save_LIBS="$LIBS" ac_save_LDFLAGS="$LDFLAGS" ac_save_CPPFLAGS="$CPPFLAGS" - LIBS="$ac_save_LIBS $PYTHON_LIBS $PYTHON_EXTRA_LIBS $PYTHON_EXTRA_LIBS" + LIBS="$ac_save_LIBS $PYTHON_LIBS $PYTHON_EXTRA_LIBS" LDFLAGS="$ac_save_LDFLAGS $PYTHON_EXTRA_LDFLAGS" CPPFLAGS="$ac_save_CPPFLAGS $PYTHON_CPPFLAGS" AC_LANG_PUSH([C]) @@ -306,7 +395,7 @@ EOD` AC_MSG_RESULT([$pythonexists]) - if test ! "x$pythonexists" = "xyes"; then + if test ! "x$pythonexists" = "xyes"; then AC_MSG_FAILURE([ Could not link test program to Python. Maybe the main Python library has been installed in some non-standard library path. If so, pass it to configure, diff --git a/src/mod_sso/session.c b/src/mod_sso/session.c index a3f1fc30705d070289badba0a6581c7d962085dc..4eeec5b6bea7c6bfcdd520d18f48620b95f9e8e6 100644 --- a/src/mod_sso/session.c +++ b/src/mod_sso/session.c @@ -33,6 +33,7 @@ #include "apr_strings.h" #include "apr_file_io.h" +#include "base64.h" #include "mod_sso.h" static const char *session_cookie_name = "_sso_local_session"; diff --git a/src/sso/Makefile.am b/src/sso/Makefile.am index deb9bcf47d1be17e9b4b5a0ce24f68dbbb88da61..21aed38c4fcec8b3d965831372b0b9e31cc3b0b3 100644 --- a/src/sso/Makefile.am +++ b/src/sso/Makefile.am @@ -8,7 +8,7 @@ ssotool_LDADD = libsso.la libsso_la_SOURCES = \ sso.c sso.h \ - base64.c \ + base64.c base64.h \ randombytes.c \ tweetnacl.c tweetnacl.h libsso_la_includedir = $(includedir)/sso diff --git a/src/sso/base64.h b/src/sso/base64.h new file mode 100644 index 0000000000000000000000000000000000000000..6b4a95e81addc9299b553a771a6e4de417de1703 --- /dev/null +++ b/src/sso/base64.h @@ -0,0 +1,13 @@ +#ifndef __sso_base64_h +#define __sso_base64_h 1 + +#include <sys/types.h> + +/* Base64 encoding utilities. */ +int sso_base64_encode(unsigned char *dst, size_t *dlen, + const unsigned char *src, size_t slen); +int sso_base64_decode(unsigned char *dst, size_t *dlen, + const unsigned char *src, size_t slen); +size_t sso_base64_encode_size(size_t slen); + +#endif diff --git a/src/sso/sso.c b/src/sso/sso.c index be03ba1ff668f550a8cc4f92ee2751ad9e741a85..905e5d9d974ba7e422d58eaddd4d6fc23af42774 100644 --- a/src/sso/sso.c +++ b/src/sso/sso.c @@ -3,6 +3,7 @@ #include <string.h> #include <time.h> +#include "base64.h" #include "sso.h" #include "tweetnacl.h" @@ -31,6 +32,14 @@ static char **group_list_dup(const char **groups) { return result; } +static void group_list_free(char **groups) { + char **p = groups; + for (; *p; p++) { + free((char *)*p); + } + free(groups); +} + static int group_list_overlap(char **a, const char **b) { char **ga; const char **gb; @@ -78,11 +87,7 @@ void sso_ticket_free(sso_ticket_t t) { free(t->nonce); } if (t->groups != NULL) { - char **p; - for (p = t->groups; *p; p++) { - free(*p); - } - free(t->groups); + group_list_free(t->groups); } free(t); } @@ -198,61 +203,94 @@ static char **group_list_parse(char *s) { return groups; } -static int sso_ticket_deserialize(sso_ticket_t *t, char *s) { - char *token, *ss; - sso_ticket_t tmp = sso_ticket_new(NULL, NULL, NULL, NULL, NULL, 0); - int i; - - /* - * Split s into fields, which may be empty. - */ - for (i = 0, ss = s; ss; i++) { - token = ss; - ss = strchr(ss, FIELD_SEP_CH); - if (ss != NULL) { - *ss++ = '\0'; - } - if (*token == 0) { +static int sso_ticket_deserialize(sso_ticket_t *t, const char *s, int sz) { + char *version = NULL, + *user = NULL, + *service = NULL, + *domain = NULL, + *nonce = NULL; + char **groups = NULL; + time_t expires = 0; + int err = SSO_OK; + int field_size; + int i = 0, last = 0, field = 0; + char *token; + + // Split s into fields, which may be empty. We can't use strtok() + // because it merges away empty fields, and we need to know about + // those. The loop extends one further than the end of the input. + for (; i <= sz; i++) { + if (i < sz && s[i] != FIELD_SEP_CH) { continue; } - switch (i) { - case 0: - if (strcmp(token, SSO_TICKET_VERSION) != 0) { - sso_ticket_free(tmp); - return SSO_ERR_UNSUPPORTED_VERSION; + + field_size = i - last; + if (field_size > 0) { + // Make a copy of s[last:i]. + token = (char *)malloc(field_size + 1); + memcpy(token, s + last, field_size); + token[field_size] = 0; + + switch (field) { + case 0: + version = token; + break; + case 1: + user = token; + break; + case 2: + service = token; + break; + case 3: + domain = token; + break; + case 4: + nonce = token; + break; + case 5: + expires = (time_t)strtol(token, NULL, 10); + free(token); + break; + case 6: + groups = group_list_parse(token); + free(token); + break; + default: + err = SSO_ERR_DESERIALIZATION; + free(token); + goto fail; } - break; - case 1: - tmp->user = strdup(token); - break; - case 2: - tmp->service = strdup(token); - break; - case 3: - tmp->domain = strdup(token); - break; - case 4: - tmp->nonce = strdup(token); - break; - case 5: - tmp->expires = (time_t)strtol(token, NULL, 10); - break; - case 6: - tmp->groups = group_list_parse(token); - break; - default: - sso_ticket_free(tmp); - return SSO_ERR_DESERIALIZATION; } + last = i + 1; + field++; } - if (i < 6 || i > 7) { - sso_ticket_free(tmp); - return SSO_ERR_DESERIALIZATION; + if (version == NULL || strcmp(version, SSO_TICKET_VERSION) != 0) { + err = SSO_ERR_UNSUPPORTED_VERSION; + goto fail; } - *t = tmp; - return SSO_OK; + if (field < 6 || field > 7) { + err = SSO_ERR_DESERIALIZATION; + goto fail; + } + + *t = sso_ticket_new(user, service, domain, nonce, (const char **)groups, expires); + + fail: + if (version != NULL) + free(version); + if (user != NULL) + free(user); + if (service != NULL) + free(service); + if (domain != NULL) + free(domain); + if (nonce != NULL) + free(nonce); + if (groups != NULL) + group_list_free(groups); + return err; } int sso_generate_keys(unsigned char *publicp, unsigned char *secretp) { @@ -351,8 +389,7 @@ int sso_ticket_open(sso_ticket_t *t, const char *str, return SSO_ERR_BAD_SIGNATURE; } - serialized[serialized_size] = '\0'; - r = sso_ticket_deserialize(t, (char *)serialized); + r = sso_ticket_deserialize(t, (char *)serialized, serialized_size); free(serialized); free(signed_data); return r; diff --git a/src/sso/sso.h b/src/sso/sso.h index 9f0beb3c084fb3de2388e6f0ebb21c47ee42df14..2794556d98cb43b6a03fd30322712b94952cc88d 100644 --- a/src/sso/sso.h +++ b/src/sso/sso.h @@ -141,13 +141,6 @@ int sso_validate(sso_ticket_t t, const char *service, const char *domain, */ const char *sso_strerror(int err); -/* Base64 encoding utilities. */ -int sso_base64_encode(unsigned char *dst, size_t *dlen, - const unsigned char *src, size_t slen); -int sso_base64_decode(unsigned char *dst, size_t *dlen, - const unsigned char *src, size_t slen); -size_t sso_base64_encode_size(size_t slen); - #ifdef __cplusplus } #endif diff --git a/src/sso/test/fuzz.sh b/src/sso/test/fuzz.sh new file mode 100755 index 0000000000000000000000000000000000000000..779896dfe694b050d65dcbda64a3142a4e6f75be --- /dev/null +++ b/src/sso/test/fuzz.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# +# Compile libsso with libfuzz, using a specific fuzz entry point. +# Useful libfuzz args include -jobs=N -workers=N for parallelization. +# +# Note: on Debian this probably requires CC=clang-11. +# + +dir=$(dirname "$0") +dir=${dir:-.} +top_srcdir=${dir}/.. + +fuzz_src="$1" +if [ -z "$fuzz_src" ]; then + echo "Usage: $0 <target_source_file>" >&2 + exit 2 +fi +shift + +set -e +make -C ${top_srcdir} clean +make -C ${top_srcdir} CC=${CC:-clang} CFLAGS='-g -fsanitize=address,fuzzer-no-link' +${CC:-clang} -I${top_srcdir} -g -fsanitize=address,fuzzer -o fuzz "${fuzz_src}" -L${top_srcdir}/.libs -lsso +LD_LIBRARY_PATH=${top_srcdir}/.libs ./fuzz "$@" diff --git a/src/sso/test/fuzz_serialized.c b/src/sso/test/fuzz_serialized.c new file mode 100644 index 0000000000000000000000000000000000000000..415d52e4696c38a58b205566966bacf20e71965b --- /dev/null +++ b/src/sso/test/fuzz_serialized.c @@ -0,0 +1,36 @@ +// Fuzz the serialized version of a SSO ticket. + +#include <stddef.h> +#include <stdint.h> +#include <string.h> +#include <stdlib.h> + +#include "sso.h" + +static unsigned char public_key[SSO_PUBLIC_KEY_SIZE], secret_key[SSO_SECRET_KEY_SIZE]; +static int public_key_init = 0; +static const unsigned char *get_public_key() { + if (!public_key_init) { + sso_generate_keys(public_key, secret_key); + public_key_init = 1; + } + return public_key; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + char *buf; + sso_ticket_t tkt = NULL; + int r; + + buf = (char *)malloc(size + 1); + memcpy(buf, data, size); + buf[size] = 0; + + r = sso_ticket_open(&tkt, (const char *)buf, get_public_key()); + if (r == SSO_OK) { + sso_ticket_free(tkt); + } + + free(buf); + return 0; +} diff --git a/src/sso/test/fuzz_signed.c b/src/sso/test/fuzz_signed.c new file mode 100644 index 0000000000000000000000000000000000000000..b5e025b7abf1376fe6c78cc8dcfbefea7277cc54 --- /dev/null +++ b/src/sso/test/fuzz_signed.c @@ -0,0 +1,61 @@ +// Fuzz the contents of a signed, encoded ticket. +// (Encrypts the data coming from the fuzzer). + +#include <stdio.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> +#include <stdlib.h> + +#include "base64.h" +#include "sso.h" +#include "tweetnacl.h" + +static unsigned char public_key[SSO_PUBLIC_KEY_SIZE], + secret_key[SSO_SECRET_KEY_SIZE]; +static int key_init = 0; + +static inline void do_key_init() { + if (!key_init) { + sso_generate_keys(public_key, secret_key); + key_init = 1; + } +} + +static inline const unsigned char *get_public_key() { + do_key_init(); + return public_key; +} + +static inline const unsigned char *get_secret_key() { + do_key_init(); + return secret_key; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + unsigned char *buf; + unsigned char *b64buf; + sso_ticket_t tkt = NULL; + int r; + size_t b64bufsz; + unsigned long long signed_size; + + signed_size = crypto_sign_BYTES + size; + buf = (unsigned char *)malloc(signed_size); + crypto_sign(buf, &signed_size, data, size, get_secret_key()); + + b64bufsz = sso_base64_encode_size(signed_size) * 2 + 1; + b64buf = (unsigned char *)malloc(b64bufsz); + + r = sso_base64_encode(b64buf, &b64bufsz, buf, + (size_t)signed_size); + + r = sso_ticket_open(&tkt, (const char *)b64buf, get_public_key()); + if (r == SSO_OK) { + sso_ticket_free(tkt); + } + + free(b64buf); + free(buf); + return 0; +} diff --git a/src/sso/test/sso_unittest.cc b/src/sso/test/sso_unittest.cc index cc6639aa1d29700a7223889fc993d18e9ecd5353..4c668574fe31c5c41e6d8051f63495c6c4a6edda 100644 --- a/src/sso/test/sso_unittest.cc +++ b/src/sso/test/sso_unittest.cc @@ -3,6 +3,7 @@ #include "sso.h" extern "C" { +#include "base64.h" #include "tweetnacl.h" } @@ -201,11 +202,12 @@ TEST_F(SSO, Open) { {NULL, 0}, }; - for (struct open_testdata *tdp = td; tdp->ticket_str; tdp++) { + int i = 0; + for (struct open_testdata *tdp = td; tdp->ticket_str; i++, tdp++) { sso_ticket_t t2 = NULL; EXPECT_EQ(tdp->expected_result, sso_ticket_open(&t2, tdp->ticket_str, public_key)) - << "Failed decoding ticket: " << tdp->ticket_str; + << "Test #" << i << " - Failed decoding ticket: " << tdp->ticket_str; if (tdp->expected_result == 0) { EXPECT_TRUE(t2); } else {