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 {