SPDX: license tags (mostly by guesswork)
[exim.git] / src / src / auths / gsasl_exim.c
index 80ae354f9ef9dd3a3ab99ec8e07bef1d37cc4d96..aac9c84e6b3d137350afa8e87bd2f1be79677915 100644 (file)
@@ -2,9 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2019 */
+/* 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> */
@@ -27,7 +28,6 @@ sense in all contexts.  For some, we can do checks at init time.
 */
 
 #include "../exim.h"
-#define CHANNELBIND_HACK
 
 #ifndef AUTH_GSASL
 /* dummy function to satisfy compilers when we link in an "empty" file. */
@@ -40,45 +40,70 @@ static void dummy(int x) { dummy2(x-1); }
 #include "gsasl_exim.h"
 
 
-#if GSASL_VERSION_MINOR >= 9
+#if GSASL_VERSION_MAJOR == 2
+
 # 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
+
+#elif GSASL_VERSION_MAJOR == 1
+# if GSASL_VERSION_MINOR >= 10
+#  define EXIM_GSASL_HAVE_SCRAM_SHA_256
+#  define EXIM_GSASL_SCRAM_S_KEY
+
+# 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
 #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
 we only ever handle one mechanism at a time, I didn't see the point in keeping
 that.  In case someone sees a point, I've left the condition_check() API
 alone. */
+#define LOFF(field) OPT_OFF(auth_gsasl_options_block, field)
+
 optionlist auth_gsasl_options[] = {
-  { "client_authz",          opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, client_authz)) },
-  { "client_channelbinding", opt_bool,
-      (void *)(offsetof(auth_gsasl_options_block, client_channelbinding)) },
-  { "client_password",      opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, client_password)) },
-  { "client_username",      opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, client_username)) },
-
-  { "server_channelbinding", opt_bool,
-      (void *)(offsetof(auth_gsasl_options_block, server_channelbinding)) },
-  { "server_hostname",      opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, server_hostname)) },
-  { "server_mech",          opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, server_mech)) },
-  { "server_password",      opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, server_password)) },
-  { "server_realm",         opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, server_realm)) },
-  { "server_scram_iter",    opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, server_scram_iter)) },
-  { "server_scram_salt",    opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, server_scram_salt)) },
-  { "server_service",       opt_stringptr,
-      (void *)(offsetof(auth_gsasl_options_block, server_service)) }
+  { "client_authz",            opt_stringptr,  LOFF(client_authz) },
+  { "client_channelbinding",   opt_bool,       LOFF(client_channelbinding) },
+  { "client_password",         opt_stringptr,  LOFF(client_password) },
+  { "client_spassword",                opt_stringptr,  LOFF(client_spassword) },
+  { "client_username",         opt_stringptr,  LOFF(client_username) },
+
+  { "server_channelbinding",   opt_bool,       LOFF(server_channelbinding) },
+  { "server_hostname",         opt_stringptr,  LOFF(server_hostname) },
+#ifdef EXIM_GSASL_SCRAM_S_KEY
+  { "server_key",              opt_stringptr,  LOFF(server_key) },
+#endif
+  { "server_mech",             opt_stringptr,  LOFF(server_mech) },
+  { "server_password",         opt_stringptr,  LOFF(server_password) },
+  { "server_realm",            opt_stringptr,  LOFF(server_realm) },
+  { "server_scram_iter",       opt_stringptr,  LOFF(server_scram_iter) },
+  { "server_scram_salt",       opt_stringptr,  LOFF(server_scram_salt) },
+#ifdef EXIM_GSASL_SCRAM_S_KEY
+  { "server_skey",             opt_stringptr,  LOFF(server_s_key) },
+#endif
+  { "server_service",          opt_stringptr,  LOFF(server_service) }
 };
-/* GSASL_SCRAM_SALTED_PASSWORD documented only for client, so not implementing
-hooks to avoid cleartext passwords in the Exim server. */
 
 int auth_gsasl_options_count =
   sizeof(auth_gsasl_options)/sizeof(optionlist);
@@ -93,13 +118,14 @@ auth_gsasl_options_block auth_gsasl_option_defaults = {
 
 
 #ifdef MACRO_PREDEF
+# include "../macro_predef.h"
 
 /* Dummy values */
 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)
@@ -107,6 +133,9 @@ auth_gsasl_macros(void)
 # ifdef EXIM_GSASL_HAVE_SCRAM_SHA_256
   builtin_macro_create(US"_HAVE_AUTH_GSASL_SCRAM_SHA_256");
 # endif
+# ifdef EXIM_GSASL_SCRAM_S_KEY
+  builtin_macro_create(US"_HAVE_AUTH_GSASL_SCRAM_S_KEY");
+# endif
 }
 
 #else   /*!MACRO_PREDEF*/
@@ -188,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",
@@ -207,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:  "
@@ -215,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;
 }
 
@@ -242,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);
@@ -288,6 +317,64 @@ return rc;
 }
 
 
+/*************************************************
+*             Debug service function             *
+*************************************************/
+static const uschar * 
+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";
+#ifdef EXIM_GSASL_SCRAM_S_KEY
+  case GSASL_SCRAM_STOREDKEY:          return US"SCRAM_STOREDKEY";
+  case GSASL_SCRAM_SERVERKEY:          return US"SCRAM_SERVERKEY";
+#endif
+#ifdef EXIM_GSASL_HAVE_EXPORTER                /* v. 2.1.0 */
+  case GSASL_CB_TLS_EXPORTER:          return US"CB_TLS_EXPORTER";
+#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";
+  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                 *
 *************************************************/
@@ -295,12 +382,12 @@ return rc;
 /* 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;
@@ -312,7 +399,7 @@ HDEBUG(D_auth)
 #ifndef DISABLE_TLS
 if (tls_in.channelbinding && ob->server_channelbinding)
   {
-# ifdef EXPERIMENTAL_TLS_RESUME
+# ifndef DISABLE_TLS_RESUME
   if (!tls_in.ext_master_secret && tls_in.resumption == RESUME_USED)
     {          /* per RFC 7677 section 4 */
     HDEBUG(D_auth) debug_printf(
@@ -321,9 +408,9 @@ if (tls_in.channelbinding && ob->server_channelbinding)
     }
 # endif
 # ifdef CHANNELBIND_HACK
-/* This is a gross hack to get around the library a) requiring that
-c-b was already set, at the _start() call, and b) caching a b64'd
-version of the binding then which it never updates. */
+/* This is a gross hack to get around the library before 1.9.2
+a) requiring that c-b was already set, at the _start() call, and
+b) caching a b64'd version of the binding then which it never updates. */
 
   gsasl_callback_hook_set(gsasl_ctx, tls_in.channelbinding);
 # endif
@@ -343,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)
@@ -376,13 +463,24 @@ if (tls_in.channelbinding)
   would then result in mechanism name changes on a library update, we
   have little choice but to default it off and let the admin choose to
   enable it.  *sigh*
+
+  Earlier library versions need this set early, during the _start() call,
+  so we had to misuse gsasl_callback_hook_set/get() as a data transfer
+  mech for the callback done at that time to get the bind-data.  More recently
+  the callback is done (if needed) during the first gsasl_stop().  We know
+  the bind-data here so can set it (and should not get a callback).
   */
   if (ob->server_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
@@ -440,6 +538,7 @@ do {
       goto STOP_INTERACTION;
     }
 
+  /*XXX having our caller send the final smtp "235" is unfortunate; wastes a roundtrip */
   if ((rc == GSASL_NEEDS_MORE) || (to_send && *to_send))
     exim_error = auth_get_no64_data(USS &received, US to_send);
 
@@ -457,6 +556,21 @@ do {
 STOP_INTERACTION:
 auth_result = rc;
 
+HDEBUG(D_auth)
+  {
+  const uschar * s;
+  if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_ITER)))
+    debug_printf(" - itercnt:   '%s'\n", s);
+  if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_SALT)))
+    debug_printf(" - salt:      '%s'\n", s);
+#ifdef EXIM_GSASL_SCRAM_S_KEY
+  if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_SERVERKEY)))
+    debug_printf(" - ServerKey: '%s'\n", s);
+  if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_STOREDKEY)))
+    debug_printf(" - StoredKey: '%s'\n", s);
+#endif
+  }
+
 gsasl_finish(sctx);
 
 /* Can return: OK DEFER FAIL CANCELLED BAD64 UNEXPECTED */
@@ -501,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;
 }
 
@@ -530,19 +649,36 @@ set_exim_authvar_from_prop(sctx, GSASL_REALM);
 }
 
 
+static int
+prop_from_option(Gsasl_session * sctx, Gsasl_property prop,
+  const uschar * option)
+{
+HDEBUG(D_auth) debug_printf(" %s\n", gsasl_prop_code_to_name(prop));
+if (option)
+  {
+  set_exim_authvars_from_a_az_r_props(sctx);
+  option = expand_cstring(option);
+  HDEBUG(D_auth) debug_printf("  '%s'\n", option);
+  if (*option)
+    gsasl_property_set(sctx, prop, CCS option);
+  return GSASL_OK;
+  }
+HDEBUG(D_auth) debug_printf("  option not set\n");
+return GSASL_NO_CALLBACK;
+}
+
 static int
 server_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop,
   auth_instance *ablock)
 {
-char *tmps;
-uschar *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 %d for %s/%s as server\n",
-      prop, ablock->name, ablock->public_name);
+HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as server\n",
+           gsasl_prop_code_to_name(prop), ablock->name, ablock->public_name);
 
 for (int i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
 expand_nmax = 0;
@@ -550,7 +686,6 @@ expand_nmax = 0;
 switch (prop)
   {
   case GSASL_VALIDATE_SIMPLE:
-    HDEBUG(D_auth) debug_printf(" VALIDATE_SIMPLE\n");
     /* GSASL_AUTHID, GSASL_AUTHZID, and GSASL_PASSWORD */
     set_exim_authvar_from_prop(sctx, GSASL_AUTHID);
     set_exim_authvar_from_prop(sctx, GSASL_AUTHZID);
@@ -561,7 +696,6 @@ switch (prop)
     break;
 
   case GSASL_VALIDATE_EXTERNAL:
-    HDEBUG(D_auth) debug_printf(" VALIDATE_EXTERNAL\n");
     if (!ablock->server_condition)
       {
       HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate EXTERNAL\n");
@@ -576,7 +710,6 @@ switch (prop)
     break;
 
   case GSASL_VALIDATE_ANONYMOUS:
-    HDEBUG(D_auth) debug_printf(" VALIDATE_ANONYMOUS\n");
     if (!ablock->server_condition)
       {
       HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate ANONYMOUS\n");
@@ -591,7 +724,6 @@ switch (prop)
     break;
 
   case GSASL_VALIDATE_GSSAPI:
-    HDEBUG(D_auth) debug_printf(" VALIDATE_GSSAPI\n");
     /* GSASL_AUTHZID and GSASL_GSSAPI_DISPLAY_NAME
     The display-name is authenticated as part of GSS, the authzid is claimed
     by the SASL integration after authentication; protected against tampering
@@ -613,37 +745,25 @@ switch (prop)
     break;
 
   case GSASL_SCRAM_ITER:
-    HDEBUG(D_auth) debug_printf(" SCRAM_ITER\n");
-    if (ob->server_scram_iter)
-      {
-      set_exim_authvars_from_a_az_r_props(sctx);
-      tmps = CS expand_string(ob->server_scram_iter);
-      HDEBUG(D_auth) debug_printf("  '%s'\n", tmps);
-      gsasl_property_set(sctx, GSASL_SCRAM_ITER, tmps);
-      cbrc = GSASL_OK;
-      }
-    else
-      HDEBUG(D_auth) debug_printf("  option not set\n");
+    cbrc = prop_from_option(sctx, prop, ob->server_scram_iter);
     break;
 
   case GSASL_SCRAM_SALT:
-    HDEBUG(D_auth) debug_printf(" SCRAM_SALT\n");
-    if (ob->server_scram_salt)
-      {
-      set_exim_authvars_from_a_az_r_props(sctx);
-      tmps = CS expand_string(ob->server_scram_salt);
-      HDEBUG(D_auth) debug_printf("  '%s'\n", tmps);
-      if (*tmps)
-       gsasl_property_set(sctx, GSASL_SCRAM_SALT, tmps);
-      cbrc = GSASL_OK;
-      }
-    else
-      HDEBUG(D_auth) debug_printf("  option not set\n");
+    cbrc = prop_from_option(sctx, prop, ob->server_scram_salt);
+    break;
+
+#ifdef EXIM_GSASL_SCRAM_S_KEY
+  case GSASL_SCRAM_STOREDKEY:
+    cbrc = prop_from_option(sctx, prop, ob->server_s_key);
     break;
 
+  case GSASL_SCRAM_SERVERKEY:
+    cbrc = prop_from_option(sctx, prop, ob->server_key);
+    break;
+#endif
+
   case GSASL_PASSWORD:
-    HDEBUG(D_auth) debug_printf(" PASSWORD\n");
-    /* SCRAM-SHA-1: GSASL_AUTHID, GSASL_AUTHZID and GSASL_REALM
+    /* SCRAM-*: GSASL_AUTHID, GSASL_AUTHZID and GSASL_REALM
        DIGEST-MD5: GSASL_AUTHID, GSASL_AUTHZID and GSASL_REALM
        CRAM-MD5: GSASL_AUTHID
        PLAIN: GSASL_AUTHID and GSASL_AUTHZID
@@ -651,12 +771,12 @@ switch (prop)
      */
     set_exim_authvars_from_a_az_r_props(sctx);
 
-    if (!ob->server_password)
+    if (!(s = ob->server_password))
       {
       HDEBUG(D_auth) debug_printf("option not set\n");
       break;
       }
-    if (!(tmps = CS expand_string(ob->server_password)))
+    if (!(tmps = CS expand_string(s)))
       {
       sasl_error_should_defer = !f.expand_string_forcedfail;
       HDEBUG(D_auth) debug_printf("server_password expansion failed, so "
@@ -670,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;
 
@@ -691,19 +811,24 @@ return cbrc;
 #define PROP_OPTIONAL  BIT(0)
 
 static BOOL
-client_prop(Gsasl_session * sctx, Gsasl_property propnum, uschar * val,
-  const uschar * why, unsigned flags, uschar * buffer, int buffsize)
+set_client_prop(Gsasl_session * sctx, Gsasl_property prop, uschar * val,
+  unsigned flags, uschar * buffer, int buffsize)
 {
 uschar * s;
-int rc;
 
-if (flags & PROP_OPTIONAL && !val) return TRUE;
+if (!val) return !!(flags & PROP_OPTIONAL);
 if (!(s = expand_string(val)) || !(flags & PROP_OPTIONAL) && !*s)
   {
   string_format(buffer, buffsize, "%s", expand_string_message);
   return FALSE;
   }
-if (*s) gsasl_property_set(sctx, propnum, CS s);
+if (*s)
+  {
+  HDEBUG(D_auth) debug_printf("%s: set %s = '%s'\n", __FUNCTION__,
+    gsasl_prop_code_to_name(prop), s);
+  gsasl_property_set(sctx, prop, CS s);
+  }
+
 return TRUE;
 }
 
@@ -715,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;
@@ -738,18 +863,19 @@ HDEBUG(D_auth)
 #ifndef DISABLE_TLS
 if (tls_out.channelbinding && ob->client_channelbinding)
   {
-# ifdef EXPERIMENTAL_TLS_RESUME
+# 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;
     }
 # endif
 # ifdef CHANNELBIND_HACK
-  /* This is a gross hack to get around the library a) requiring that
-  c-b was already set, at the _start() call, and b) caching a b64'd
-  version of the binding then which it never updates. */
+  /* This is a gross hack to get around the library before 1.9.2
+  a) requiring that c-b was already set, at the _start() call, and
+  b) caching a b64'd version of the binding then which it never updates. */
 
   gsasl_callback_hook_set(gsasl_ctx, tls_out.channelbinding);
 # endif
@@ -770,11 +896,11 @@ gsasl_session_hook_set(sctx, &cb_state);
 
 /* Set properties */
 
-if (  !client_prop(sctx, GSASL_PASSWORD, ob->client_password, US"password",
+if (  !set_client_prop(sctx, GSASL_PASSWORD, ob->client_password,
                  0, buffer, buffsize)
-   || !client_prop(sctx, GSASL_AUTHID, ob->client_username, US"username",
+   || !set_client_prop(sctx, GSASL_AUTHID, ob->client_username,
                  0, buffer, buffsize)
-   || !client_prop(sctx, GSASL_AUTHZID, ob->client_authz, US"authz",
+   || !set_client_prop(sctx, GSASL_AUTHZID, ob->client_authz,
                  PROP_OPTIONAL, buffer, buffsize)
    )
   return ERROR;
@@ -786,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
@@ -800,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)
     {
@@ -848,6 +982,15 @@ for(s = NULL; ;)
   }
 
 done:
+if (yield == OK)
+  {
+  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);
 return yield;
 }
@@ -855,21 +998,47 @@ return yield;
 static int
 client_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, auth_instance *ablock)
 {
-HDEBUG(D_auth) debug_printf("GNU SASL callback %d for %s/%s as client\n",
-               prop, ablock->name, ablock->public_name);
+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_AUTHZID:
-    HDEBUG(D_auth) debug_printf(" inquired for AUTHZID; not providing one\n");
-    break;
+#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:
-    HDEBUG(D_auth)
-      debug_printf(" inquired for SCRAM_SALTED_PASSWORD; not providing one\n");
+    {
+    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;
-  case GSASL_CB_TLS_UNIQUE:
+    }
+  default:
     HDEBUG(D_auth)
-      debug_printf(" inquired for CB_TLS_UNIQUE, filling in\n");
-    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding);
+      debug_printf(" not providing one\n");
     break;
   }
 return GSASL_NO_CALLBACK;
@@ -879,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));
 }