SPDX: license tags (mostly by guesswork)
[exim.git] / src / src / auths / gsasl_exim.c
index 12713705b4c1a776547e0c8773d305a471234568..aac9c84e6b3d137350afa8e87bd2f1be79677915 100644 (file)
@@ -2,9 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2019-2020 */
+/* Copyright (c) The Exim Maintainers 2019 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Copyright (c) Twitter Inc 2012
    Author: Phil Pennock <pdp@exim.org> */
@@ -39,24 +40,40 @@ static void dummy(int x) { dummy2(x-1); }
 #include "gsasl_exim.h"
 
 
-#if GSASL_VERSION_MINOR >= 10
-# define EXIM_GSASL_HAVE_SCRAM_SHA_256
-# define EXIM_GSASL_SCRAM_S_KEY
+#if GSASL_VERSION_MAJOR == 2
 
-#elif GSASL_VERSION_MINOR == 9
 # define EXIM_GSASL_HAVE_SCRAM_SHA_256
+# define EXIM_GSASL_SCRAM_S_KEY
+# if GSASL_VERSION_MINOR >= 1
+#  define EXIM_GSASL_HAVE_EXPORTER
+# elif GSASL_VERSION_PATCH >= 1
+#  define EXIM_GSASL_HAVE_EXPORTER
+# endif
 
-# if GSASL_VERSION_PATCH >= 1
+#elif GSASL_VERSION_MAJOR == 1
+# if GSASL_VERSION_MINOR >= 10
+#  define EXIM_GSASL_HAVE_SCRAM_SHA_256
 #  define EXIM_GSASL_SCRAM_S_KEY
-# endif
-# if GSASL_VERSION_PATCH < 2
+
+# elif GSASL_VERSION_MINOR == 9
+#  define EXIM_GSASL_HAVE_SCRAM_SHA_256
+
+#  if GSASL_VERSION_PATCH >= 1
+#   define EXIM_GSASL_SCRAM_S_KEY
+#  endif
+#  if GSASL_VERSION_PATCH < 2
+#   define CHANNELBIND_HACK
+#  endif
+
+# else
 #  define CHANNELBIND_HACK
 # endif
-
-#else
-# define CHANNELBIND_HACK
 #endif
 
+/* Convenience for testing strings */
+
+#define STREQIC(Foo, Bar) (strcmpic((Foo), (Bar)) == 0)
+
 
 /* Authenticator-specific options. */
 /* I did have server_*_condition options for various mechanisms, but since
@@ -108,7 +125,7 @@ void auth_gsasl_init(auth_instance *ablock) {}
 int auth_gsasl_server(auth_instance *ablock, uschar *data) {return 0;}
 int auth_gsasl_client(auth_instance *ablock, void * sx,
   int timeout, uschar *buffer, int buffsize) {return 0;}
-void auth_gsasl_version_report(FILE *f) {}
+gstring * auth_gsasl_version_report(gstring * g) {return NULL;}
 
 void
 auth_gsasl_macros(void)
@@ -200,15 +217,21 @@ if (!gsasl_client_support_p(gsasl_ctx, CCS ob->server_mech))
            "GNU SASL does not support mechanism \"%s\"",
            ablock->name, ob->server_mech);
 
-ablock->server = TRUE;
-
-if (  !ablock->server_condition
-   && (  streqic(ob->server_mech, US"EXTERNAL")
-      || streqic(ob->server_mech, US"ANONYMOUS")
-      || streqic(ob->server_mech, US"PLAIN")
-      || streqic(ob->server_mech, US"LOGIN")
-   )  )
+if (ablock->server_condition)
+  ablock->server = TRUE;
+else if(  ob->server_mech
+       && !STREQIC(ob->server_mech, US"EXTERNAL")
+       && !STREQIC(ob->server_mech, US"ANONYMOUS")
+       && !STREQIC(ob->server_mech, US"PLAIN")
+       && !STREQIC(ob->server_mech, US"LOGIN")
+       )
   {
+  /* At present, for mechanisms we don't panic on absence of server_condition;
+  need to figure out the most generically correct approach to deciding when
+  it's critical and when it isn't.  Eg, for simple validation (PLAIN mechanism,
+  etc) it clearly is critical.
+  */
+
   ablock->server = FALSE;
   HDEBUG(D_auth) debug_printf("%s authenticator:  "
            "Need server_condition for %s mechanism\n",
@@ -219,7 +242,7 @@ if (  !ablock->server_condition
 which properties will be needed. */
 
 if (  !ob->server_realm
-   && streqic(ob->server_mech, US"DIGEST-MD5"))
+   && STREQIC(ob->server_mech, US"DIGEST-MD5"))
   {
   ablock->server = FALSE;
   HDEBUG(D_auth) debug_printf("%s authenticator:  "
@@ -227,12 +250,6 @@ if (  !ob->server_realm
            ablock->name, ob->server_mech);
   }
 
-/* At present, for mechanisms we don't panic on absence of server_condition;
-need to figure out the most generically correct approach to deciding when
-it's critical and when it isn't.  Eg, for simple validation (PLAIN mechanism,
-etc) it clearly is critical.
-*/
-
 ablock->client = ob->client_username && ob->client_password;
 }
 
@@ -254,7 +271,7 @@ if (!cb_state)
   if (prop == GSASL_CB_TLS_UNIQUE)
     {
     uschar * s;
-    if ((s = gsasl_callback_hook_get(ctx)))
+    if ((s = gsasl_callback_hook_get(ctx)))    /* Gross hack for early lib vers */
       {
       HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE from ctx hook\n");
       gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CS s);
@@ -308,48 +325,56 @@ gsasl_prop_code_to_name(Gsasl_property prop)
 {
 switch (prop)
   {
-  case GSASL_AUTHID: return US"AUTHID";
-  case GSASL_AUTHZID: return US"AUTHZID";
-  case GSASL_PASSWORD: return US"PASSWORD";
-  case GSASL_ANONYMOUS_TOKEN: return US"ANONYMOUS_TOKEN";
-  case GSASL_SERVICE: return US"SERVICE";
-  case GSASL_HOSTNAME: return US"HOSTNAME";
-  case GSASL_GSSAPI_DISPLAY_NAME: return US"GSSAPI_DISPLAY_NAME";
-  case GSASL_PASSCODE: return US"PASSCODE";
-  case GSASL_SUGGESTED_PIN: return US"SUGGESTED_PIN";
-  case GSASL_PIN: return US"PIN";
-  case GSASL_REALM: return US"REALM";
-  case GSASL_DIGEST_MD5_HASHED_PASSWORD: return US"DIGEST_MD5_HASHED_PASSWORD";
-  case GSASL_QOPS: return US"QOPS";
-  case GSASL_QOP: return US"QOP";
-  case GSASL_SCRAM_ITER: return US"SCRAM_ITER";
-  case GSASL_SCRAM_SALT: return US"SCRAM_SALT";
-  case GSASL_SCRAM_SALTED_PASSWORD: return US"SCRAM_SALTED_PASSWORD";
+  case GSASL_AUTHID:                   return US"AUTHID";
+  case GSASL_AUTHZID:                  return US"AUTHZID";
+  case GSASL_PASSWORD:                 return US"PASSWORD";
+  case GSASL_ANONYMOUS_TOKEN:          return US"ANONYMOUS_TOKEN";
+  case GSASL_SERVICE:                  return US"SERVICE";
+  case GSASL_HOSTNAME:                 return US"HOSTNAME";
+  case GSASL_GSSAPI_DISPLAY_NAME:      return US"GSSAPI_DISPLAY_NAME";
+  case GSASL_PASSCODE:                 return US"PASSCODE";
+  case GSASL_SUGGESTED_PIN:            return US"SUGGESTED_PIN";
+  case GSASL_PIN:                      return US"PIN";
+  case GSASL_REALM:                    return US"REALM";
+  case GSASL_DIGEST_MD5_HASHED_PASSWORD:       return US"DIGEST_MD5_HASHED_PASSWORD";
+  case GSASL_QOPS:                     return US"QOPS";
+  case GSASL_QOP:                      return US"QOP";
+  case GSASL_SCRAM_ITER:               return US"SCRAM_ITER";
+  case GSASL_SCRAM_SALT:               return US"SCRAM_SALT";
+  case GSASL_SCRAM_SALTED_PASSWORD:    return US"SCRAM_SALTED_PASSWORD";
 #ifdef EXIM_GSASL_SCRAM_S_KEY
-  case GSASL_SCRAM_STOREDKEY: return US"SCRAM_STOREDKEY";
-  case GSASL_SCRAM_SERVERKEY: return US"SCRAM_SERVERKEY";
+  case GSASL_SCRAM_STOREDKEY:          return US"SCRAM_STOREDKEY";
+  case GSASL_SCRAM_SERVERKEY:          return US"SCRAM_SERVERKEY";
 #endif
-  case GSASL_CB_TLS_UNIQUE: return US"CB_TLS_UNIQUE";
-  case GSASL_SAML20_IDP_IDENTIFIER: return US"SAML20_IDP_IDENTIFIER";
-  case GSASL_SAML20_REDIRECT_URL: return US"SAML20_REDIRECT_URL";
-  case GSASL_OPENID20_REDIRECT_URL: return US"OPENID20_REDIRECT_URL";
-  case GSASL_OPENID20_OUTCOME_DATA: return US"OPENID20_OUTCOME_DATA";
-  case GSASL_SAML20_AUTHENTICATE_IN_BROWSER: return US"SAML20_AUTHENTICATE_IN_BROWSER";
-  case GSASL_OPENID20_AUTHENTICATE_IN_BROWSER: return US"OPENID20_AUTHENTICATE_IN_BROWSER";
-#ifdef EXIM_GSASL_SCRAM_S_KEY
-  case GSASL_SCRAM_CLIENTKEY: return US"SCRAM_CLIENTKEY";
+#ifdef EXIM_GSASL_HAVE_EXPORTER                /* v. 2.1.0 */
+  case GSASL_CB_TLS_EXPORTER:          return US"CB_TLS_EXPORTER";
 #endif
-  case GSASL_VALIDATE_SIMPLE: return US"VALIDATE_SIMPLE";
-  case GSASL_VALIDATE_EXTERNAL: return US"VALIDATE_EXTERNAL";
-  case GSASL_VALIDATE_ANONYMOUS: return US"VALIDATE_ANONYMOUS";
-  case GSASL_VALIDATE_GSSAPI: return US"VALIDATE_GSSAPI";
-  case GSASL_VALIDATE_SECURID: return US"VALIDATE_SECURID";
-  case GSASL_VALIDATE_SAML20: return US"VALIDATE_SAML20";
-  case GSASL_VALIDATE_OPENID20: return US"VALIDATE_OPENID20";
+  case GSASL_CB_TLS_UNIQUE:            return US"CB_TLS_UNIQUE";
+  case GSASL_SAML20_IDP_IDENTIFIER:    return US"SAML20_IDP_IDENTIFIER";
+  case GSASL_SAML20_REDIRECT_URL:      return US"SAML20_REDIRECT_URL";
+  case GSASL_OPENID20_REDIRECT_URL:    return US"OPENID20_REDIRECT_URL";
+  case GSASL_OPENID20_OUTCOME_DATA:    return US"OPENID20_OUTCOME_DATA";
+  case GSASL_SAML20_AUTHENTICATE_IN_BROWSER:   return US"SAML20_AUTHENTICATE_IN_BROWSER";
+  case GSASL_OPENID20_AUTHENTICATE_IN_BROWSER: return US"OPENID20_AUTHENTICATE_IN_BROWSER";
+  case GSASL_VALIDATE_SIMPLE:          return US"VALIDATE_SIMPLE";
+  case GSASL_VALIDATE_EXTERNAL:                return US"VALIDATE_EXTERNAL";
+  case GSASL_VALIDATE_ANONYMOUS:       return US"VALIDATE_ANONYMOUS";
+  case GSASL_VALIDATE_GSSAPI:          return US"VALIDATE_GSSAPI";
+  case GSASL_VALIDATE_SECURID:         return US"VALIDATE_SECURID";
+  case GSASL_VALIDATE_SAML20:          return US"VALIDATE_SAML20";
+  case GSASL_VALIDATE_OPENID20:                return US"VALIDATE_OPENID20";
   }
 return CUS string_sprintf("(unknown prop: %d)", (int)prop);
 }
 
+static void
+preload_prop(Gsasl_session * sctx, Gsasl_property propcode, const uschar * val)
+{
+DEBUG(D_auth) debug_printf("preloading prop %s val %s\n",
+  gsasl_prop_code_to_name(propcode), val);
+gsasl_property_set(sctx, propcode, CCS val);
+}
+
 /*************************************************
 *             Server entry point                 *
 *************************************************/
@@ -357,12 +382,12 @@ return CUS string_sprintf("(unknown prop: %d)", (int)prop);
 /* For interface, see auths/README */
 
 int
-auth_gsasl_server(auth_instance *ablock, uschar *initial_data)
+auth_gsasl_server(auth_instance * ablock, uschar * initial_data)
 {
-char *tmps;
-char *to_send, *received;
-Gsasl_session *sctx = NULL;
-auth_gsasl_options_block *ob =
+uschar * tmps;
+char * to_send, * received;
+Gsasl_session * sctx = NULL;
+auth_gsasl_options_block * ob =
   (auth_gsasl_options_block *)(ablock->options_block);
 struct callback_exim_state cb_state;
 int rc, auth_result, exim_error, exim_error_override;
@@ -405,18 +430,18 @@ cb_state.ablock = ablock;
 cb_state.currently = CURRENTLY_SERVER;
 gsasl_session_hook_set(sctx, &cb_state);
 
-tmps = CS expand_string(ob->server_service);
-gsasl_property_set(sctx, GSASL_SERVICE, tmps);
-tmps = CS expand_string(ob->server_hostname);
-gsasl_property_set(sctx, GSASL_HOSTNAME, tmps);
+tmps = expand_string(ob->server_service);
+preload_prop(sctx, GSASL_SERVICE, tmps);
+tmps = expand_string(ob->server_hostname);
+preload_prop(sctx, GSASL_HOSTNAME, tmps);
 if (ob->server_realm)
   {
-  tmps = CS expand_string(ob->server_realm);
+  tmps = expand_string(ob->server_realm);
   if (tmps && *tmps)
-    gsasl_property_set(sctx, GSASL_REALM, tmps);
+    preload_prop(sctx, GSASL_REALM, tmps);
   }
 /* We don't support protection layers. */
-gsasl_property_set(sctx, GSASL_QOPS, "qop-auth");
+preload_prop(sctx, GSASL_QOPS, US "qop-auth");
 
 #ifndef DISABLE_TLS
 if (tls_in.channelbinding)
@@ -450,7 +475,12 @@ if (tls_in.channelbinding)
     HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
        ablock->name);
 # ifndef CHANNELBIND_HACK
-    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_in.channelbinding);
+    preload_prop(sctx,
+#  ifdef EXIM_GSASL_HAVE_EXPORTER
+      tls_in.channelbind_exporter ? GSASL_CB_TLS_EXPORTER :
+#  endif
+                                   GSASL_CB_TLS_UNIQUE,
+      tls_in.channelbinding);
 # endif
     }
   else
@@ -585,14 +615,19 @@ return GSASL_AUTHENTICATION_ERROR;
 }
 
 
+/* Set the "next" $auth[n] and increment expand_nmax */
+
 static void
 set_exim_authvar_from_prop(Gsasl_session * sctx, Gsasl_property prop)
 {
 uschar * propval = US gsasl_property_fast(sctx, prop);
 int i = expand_nmax, j = i + 1;
 propval = propval ? string_copy(propval) : US"";
-auth_vars[i] = expand_nstring[j] = propval;
+HDEBUG(D_auth) debug_printf("auth[%d] <=  %s'%s'\n",
+                           j, gsasl_prop_code_to_name(prop), propval);
+expand_nstring[j] = propval;
 expand_nlength[j] = Ustrlen(propval);
+if (i < AUTH_VARS) auth_vars[i] = propval;
 expand_nmax = j;
 }
 
@@ -636,10 +671,10 @@ static int
 server_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop,
   auth_instance *ablock)
 {
-char *tmps;
-uschar *s, *propval;
+char * tmps;
+uschar * s;
 int cbrc = GSASL_NO_CALLBACK;
-auth_gsasl_options_block *ob =
+auth_gsasl_options_block * ob =
   (auth_gsasl_options_block *)(ablock->options_block);
 
 HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as server\n",
@@ -755,7 +790,7 @@ switch (prop)
     for memory wiping, so expanding strings will leave stuff laying around.
     But no need to compound the problem, so get rid of the one we can. */
 
-    memset(tmps, '\0', strlen(tmps));
+    if (US tmps != s) memset(tmps, '\0', strlen(tmps));
     cbrc = GSASL_OK;
     break;
 
@@ -780,7 +815,6 @@ set_client_prop(Gsasl_session * sctx, Gsasl_property prop, uschar * val,
   unsigned flags, uschar * buffer, int buffsize)
 {
 uschar * s;
-int rc;
 
 if (!val) return !!(flags & PROP_OPTIONAL);
 if (!(s = expand_string(val)) || !(flags & PROP_OPTIONAL) && !*s)
@@ -806,13 +840,13 @@ return TRUE;
 
 int
 auth_gsasl_client(
-  auth_instance *ablock,               /* authenticator block */
+  auth_instance * ablock,              /* authenticator block */
   void * sx,                           /* connection */
   int timeout,                         /* command timeout */
-  uschar *buffer,                      /* buffer for reading response */
+  uschar * buffer,                     /* buffer for reading response */
   int buffsize)                                /* size of buffer */
 {
-auth_gsasl_options_block *ob =
+auth_gsasl_options_block * ob =
   (auth_gsasl_options_block *)(ablock->options_block);
 Gsasl_session * sctx = NULL;
 struct callback_exim_state cb_state;
@@ -831,7 +865,8 @@ if (tls_out.channelbinding && ob->client_channelbinding)
   {
 # ifndef DISABLE_TLS_RESUME
   if (!tls_out.ext_master_secret && tls_out.resumption == RESUME_USED)
-    {          /* per RFC 7677 section 4 */
+    {  /* Per RFC 7677 section 4.  See also RFC 7627, "Triple Handshake"
+       vulnerability, and https://www.mitls.org/pages/attacks/3SHAKE */
     string_format(buffer, buffsize, "%s",
       "channel binding not usable on resumed TLS without extended-master-secret");
     return FAIL;
@@ -861,10 +896,7 @@ gsasl_session_hook_set(sctx, &cb_state);
 
 /* Set properties */
 
-if (  !set_client_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD, ob->client_spassword,
-                 0, buffer, buffsize)
-      &&
-      !set_client_prop(sctx, GSASL_PASSWORD, ob->client_password,
+if (  !set_client_prop(sctx, GSASL_PASSWORD, ob->client_password,
                  0, buffer, buffsize)
    || !set_client_prop(sctx, GSASL_AUTHID, ob->client_username,
                  0, buffer, buffsize)
@@ -880,7 +912,12 @@ if (tls_out.channelbinding)
     HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
        ablock->name);
 # ifndef CHANNELBIND_HACK
-    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding);
+    preload_prop(sctx,
+#  ifdef EXIM_GSASL_HAVE_EXPORTER
+      tls_out.channelbind_exporter ? GSASL_CB_TLS_EXPORTER :
+#  endif
+                                    GSASL_CB_TLS_UNIQUE,
+      tls_out.channelbinding);
 # endif
     }
   else
@@ -894,24 +931,27 @@ if (tls_out.channelbinding)
 for(s = NULL; ;)
   {
   uschar * outstr;
-  BOOL fail;
+  BOOL fail = TRUE;
 
   rc = gsasl_step64(sctx, CS s, CSS &outstr);
 
-  fail = initial
-    ? smtp_write_command(sx, SCMD_FLUSH,
-                       outstr ? "AUTH %s %s\r\n" : "AUTH %s\r\n",
-                       ablock->public_name, outstr) <= 0
-    : outstr
-    ? smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", outstr) <= 0
-    : FALSE;
-  if (outstr && *outstr) free(outstr);
-  if (fail)
+  if (rc == GSASL_NEEDS_MORE || rc == GSASL_OK)
     {
-    yield = FAIL_SEND;
-    goto done;
+    fail = initial
+      ? smtp_write_command(sx, SCMD_FLUSH,
+                         outstr ? "AUTH %s %s\r\n" : "AUTH %s\r\n",
+                         ablock->public_name, outstr) <= 0
+      : outstr
+      ? smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", outstr) <= 0
+      : FALSE;
+    free(outstr);
+    if (fail)
+      {
+      yield = FAIL_SEND;
+      goto done;
+      }
+    initial = FALSE;
     }
-  initial = FALSE;
 
   if (rc != GSASL_NEEDS_MORE)
     {
@@ -942,10 +982,13 @@ for(s = NULL; ;)
   }
 
 done:
-HDEBUG(D_auth)
+if (yield == OK)
   {
-  const uschar * s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_SALTED_PASSWORD);
-  if (s) debug_printf(" - SaltedPassword: '%s'\n", s);
+  expand_nmax = 0;
+  set_exim_authvar_from_prop(sctx, GSASL_AUTHID);
+  set_exim_authvar_from_prop(sctx, GSASL_SCRAM_ITER);
+  set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALT);
+  set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD);
   }
 
 gsasl_finish(sctx);
@@ -959,11 +1002,40 @@ HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as client\n",
            gsasl_prop_code_to_name(prop), ablock->name, ablock->public_name);
 switch (prop)
   {
-  case GSASL_CB_TLS_UNIQUE:    /*XXX should never get called for this */
-    HDEBUG(D_auth)
-      debug_printf(" filling in\n");
+#ifdef EXIM_GSASL_HAVE_EXPORTER
+  case GSASL_CB_TLS_EXPORTER:  /* Should never get called for this, as pre-set */
+    if (!tls_out.channelbind_exporter) break;
+    HDEBUG(D_auth) debug_printf(" filling in\n");
+    gsasl_property_set(sctx, GSASL_CB_TLS_EXPORTER, CCS tls_out.channelbinding);
+    return GSASL_OK;
+#endif
+  case GSASL_CB_TLS_UNIQUE:    /* Should never get called for this, as pre-set */
+#ifdef EXIM_GSASL_HAVE_EXPORTER
+    if (tls_out.channelbind_exporter) break;
+#endif
+    HDEBUG(D_auth) debug_printf(" filling in\n");
     gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding);
+    return GSASL_OK;
+  case GSASL_SCRAM_SALTED_PASSWORD:
+    {
+    uschar * client_spassword =
+      ((auth_gsasl_options_block *) ablock->options_block)->client_spassword;
+    uschar dummy[4];
+    HDEBUG(D_auth) if (!client_spassword)
+      debug_printf(" client_spassword option unset\n");
+    if (client_spassword)
+      {
+      expand_nmax = 0;
+      set_exim_authvar_from_prop(sctx, GSASL_AUTHID);
+      set_exim_authvar_from_prop(sctx, GSASL_SCRAM_ITER);
+      set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALT);
+      set_client_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD, client_spassword,
+                 0, dummy, sizeof(dummy));
+      for (int i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
+      expand_nmax = 0;
+      }
     break;
+    }
   default:
     HDEBUG(D_auth)
       debug_printf(" not providing one\n");
@@ -976,14 +1048,12 @@ return GSASL_NO_CALLBACK;
 *                Diagnostic API                  *
 *************************************************/
 
-void
-auth_gsasl_version_report(FILE *f)
+gstring *
+auth_gsasl_version_report(gstring * g)
 {
-const char *runtime;
-runtime = gsasl_check_version(NULL);
-fprintf(f, "Library version: GNU SASL: Compile: %s\n"
-          "                           Runtime: %s\n",
-       GSASL_VERSION, runtime);
+return string_fmt_append(g, "Library version: GNU SASL: Compile: %s\n"
+                           "                           Runtime: %s\n",
+       GSASL_VERSION, gsasl_check_version(NULL));
 }