[exim-cvs] Authenticator gsasl: client support. Bug 2349

Startseite
Nachricht löschen
Nachricht beantworten
Autor: Exim Git Commits Mailing List
Datum:  
To: exim-cvs
Betreff: [exim-cvs] Authenticator gsasl: client support. Bug 2349
Gitweb: https://git.exim.org/exim.git/commitdiff/14a806d6c13afdfb2f44dce64e50bffa6cb6869c
Commit:     14a806d6c13afdfb2f44dce64e50bffa6cb6869c
Parent:     4533e306fc21e0dc3cce32db0e2bfa146a5dd78c
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Fri Dec 27 18:37:19 2019 +0000
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Fri Dec 27 18:37:19 2019 +0000


    Authenticator gsasl: client support.  Bug 2349
---
 doc/doc-docbook/spec.xfpt                   |  28 ++-
 doc/doc-txt/NewStuff                        |   4 +
 doc/doc-txt/OptionLists.txt                 |   5 +-
 src/src/EDITME                              |   3 +
 src/src/auths/get_data.c                    |   2 +-
 src/src/auths/gsasl_exim.c                  | 349 +++++++++++++++++++++++-----
 src/src/auths/gsasl_exim.h                  |   7 +
 src/src/drtables.c                          |   2 +-
 src/src/tls-openssl.c                       |   4 +-
 test/confs/3820                             |  52 ++++-
 test/confs/3821                             |   1 +
 test/confs/3828                             |  66 ++++++
 test/confs/3829                             |   1 +
 test/log/3828                               |  12 +
 test/scripts/3820-Gnu-SASL/3821             |  10 +
 test/scripts/3828-gsasl-plaintext/3828      |  16 ++
 test/scripts/3828-gsasl-plaintext/REQUIRES  |   2 +
 test/scripts/3829-gsasl-scram-plus/3829     |   8 +
 test/scripts/3829-gsasl-scram-plus/REQUIRES |   2 +
 19 files changed, 497 insertions(+), 77 deletions(-)


diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index 42a3935..eea304d 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -27435,19 +27435,37 @@ auth_mechanisms = plain login ntlm
.cindex "authentication" "DIGEST-MD5"
.cindex "authentication" "CRAM-MD5"
.cindex "authentication" "SCRAM-SHA-1"
-The &(gsasl)& authenticator provides server integration for the GNU SASL
+The &(gsasl)& authenticator provides integration for the GNU SASL
library and the mechanisms it provides. This is new as of the 4.80 release
and there are a few areas where the library does not let Exim smoothly
scale to handle future authentication mechanisms, so no guarantee can be
made that any particular new authentication mechanism will be supported
without code changes in Exim.

-Exim's &(gsasl)& authenticator does not have client-side support at this
-time; only the server-side support is implemented. Patches welcome.

+.new
+.option client_authz gsasl string&!! unset
+This option can be used to supply an &'authorization id'&
+which is different to the &'authentication_id'& provided
+by $%client_username%& option.
+If unset or (after expansion) empty it is not used,
+which is the common case.
+
+.option client_channelbinding gsasl boolean false
+See $%server_channelbinding%& below.
+
+.option client_password gsasl string&!! unset
+This option is exapanded before use, and should result in
+the password to be used, in clear.
+
+.option client_username gsasl string&!! unset
+This option is exapanded before use, and should result in
+the account name to be used.
+.wen

.option server_channelbinding gsasl boolean false
-Do not set this true without consulting a cryptographic engineer.
+Do not set this true and rely on the properties
+without consulting a cryptographic engineer.

Some authentication mechanisms are able to use external context at both ends
of the session to bind the authentication to that context, and fail the
@@ -27469,7 +27487,7 @@ This defaults off to ensure smooth upgrade across Exim releases, in case
this option causes some clients to start failing. Some future release
of Exim might have switched the default to be true.

-However, Channel Binding in TLS has proven to be broken in current versions.
+However, Channel Binding in TLS has proven to be vulnerable in current versions.
Do not plan to rely upon this feature for security, ever, without consulting
with a subject matter expert (a cryptographic engineer).

diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index cd380a3..6b163b8 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -17,6 +17,10 @@ Version 4.94

3. A msg:defer event.

+ 4. Client-side support in the gsasl authenticator.  Tested against the plaintext
+    driver for PLAIN; only against itself for SCRAM-SHA-1 and SCRAM-SHA-1-PLUS
+    methods.
+


 Version 4.93
 ------------
diff --git a/doc/doc-txt/OptionLists.txt b/doc/doc-txt/OptionLists.txt
index 1618e42..2978aed 100644
--- a/doc/doc-txt/OptionLists.txt
+++ b/doc/doc-txt/OptionLists.txt
@@ -127,12 +127,15 @@ check_spool_space                    integer         0             main
 check_string                         string          "From "       appendfile        3.03
                                                      unset         pipe              3.03
 check_srv                            string*         unset         dnslookup         4.31
+client_authz                         string*         unset         gsasl             4.94
 client_condition                     string*         unset         authenticators    4.68
 client_ignore_invalid_base64         boolean         false         plaintext         4.61
 client_name                          string*         +             cram_md5          3.10
+client_password                      string*         unset         gsasl             4.94
 client_secret                        string*         unset         cram_md5          3.10
 client_send                          string*         unset         plaintext         3.10
-client_send                          string*         unset         external (auth)   4.93
+                                                     unset         external (auth)   4.93
+client_username                      string*         unset         gsasl             4.94
 command                              string*         unset         lmtp              3.20
                                                      unset         pipe
                                                      unset         queryprogram      4.00
diff --git a/src/src/EDITME b/src/src/EDITME
index 9024b6f..352bc7d 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -784,6 +784,9 @@ FIXED_NEVER_USERS=root
 # AUTH_LIBS=-lgsasl
 # AUTH_LIBS=-lgssapi -lheimntlm -lkrb5 -lhx509 -lcom_err -lhcrypto -lasn1 -lwind -lroken -lcrypt


+# If using AUTH_GSASL with SCRAM methods, you should also be defining
+# SUPPORT_I18N to get standards-conformant support of utf8 normalization.
+

#------------------------------------------------------------------------------
# When Exim is decoding MIME "words" in header lines, most commonly for use
diff --git a/src/src/auths/get_data.c b/src/src/auths/get_data.c
index efb4d6d..8a05a82 100644
--- a/src/src/auths/get_data.c
+++ b/src/src/auths/get_data.c
@@ -193,7 +193,7 @@ else
has succeeded. There may be more data to send, but is there any point
in provoking an error here? */

-if (smtp_read_response(sx, US buffer, buffsize, '2', timeout))
+if (smtp_read_response(sx, buffer, buffsize, '2', timeout))
   {
   *inout = NULL;
   return OK;
diff --git a/src/src/auths/gsasl_exim.c b/src/src/auths/gsasl_exim.c
index 614c179..db14a40 100644
--- a/src/src/auths/gsasl_exim.c
+++ b/src/src/auths/gsasl_exim.c
@@ -2,6 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/


+/* Copyright (c) The Exim Maintainers 2019 */
/* Copyright (c) University of Cambridge 1995 - 2018 */
/* See the file NOTICE for conditions of use and distribution. */

@@ -26,6 +27,7 @@ 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. */
@@ -37,12 +39,26 @@ static void dummy(int x) { dummy2(x-1); }
#include <gsasl.h>
#include "gsasl_exim.h"

+#ifdef SUPPORT_I18N
+# include <stringprep.h>
+#endif
+
+
 /* 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. */
 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,
@@ -68,14 +84,10 @@ int auth_gsasl_options_count =


 /* Defaults for the authenticator-specific options. */
 auth_gsasl_options_block auth_gsasl_option_defaults = {
-  US"smtp",                 /* server_service */
-  US"$primary_hostname",    /* server_hostname */
-  NULL,                     /* server_realm */
-  NULL,                     /* server_mech */
-  NULL,                     /* server_password */
-  NULL,                     /* server_scram_iter */
-  NULL,                     /* server_scram_salt */
-  FALSE                     /* server_channelbinding */
+  .server_service = US"smtp",
+  .server_hostname = US"$primary_hostname",
+  .server_scram_iter = US"4096",
+  /* all others zero/null */
 };



@@ -125,8 +137,8 @@ to be set up. */
void
auth_gsasl_init(auth_instance *ablock)
{
-char *p;
-int rc, supported;
+static char * once = NULL;
+int rc;
auth_gsasl_options_block *ob =
(auth_gsasl_options_block *)(ablock->options_block);

@@ -152,48 +164,55 @@ if (!gsasl_ctx)

/* We don't need this except to log it for debugging. */

-if ((rc = gsasl_server_mechlist(gsasl_ctx, &p)) != GSASL_OK)
-  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator:  "
-        "failed to retrieve list of mechanisms: %s (%s)",
-        ablock->name,  gsasl_strerror_name(rc), gsasl_strerror(rc));
+HDEBUG(D_auth) if (!once)
+  {
+  if ((rc = gsasl_server_mechlist(gsasl_ctx, &once)) != GSASL_OK)
+    log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator:  "
+          "failed to retrieve list of mechanisms: %s (%s)",
+          ablock->name,  gsasl_strerror_name(rc), gsasl_strerror(rc));


-HDEBUG(D_auth) debug_printf("GNU SASL supports: %s\n", p);
+ debug_printf("GNU SASL supports: %s\n", once);
+ }

-supported = gsasl_client_support_p(gsasl_ctx, CCS ob->server_mech);
-if (!supported)
+if (!gsasl_client_support_p(gsasl_ctx, CCS ob->server_mech))
   log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator:  "
         "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")
    )  )
-  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator:  "
-        "Need server_condition for %s mechanism",
+  {
+  ablock->server = FALSE;
+  HDEBUG(D_auth) debug_printf("%s authenticator:  "
+        "Need server_condition for %s mechanism\n",
         ablock->name, ob->server_mech);
+  }


/* This does *not* scale to new SASL mechanisms. Need a better way to ask
which properties will be needed. */

 if (  !ob->server_realm
    && streqic(ob->server_mech, US"DIGEST-MD5"))
-  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator:  "
-        "Need server_realm for %s mechanism",
+  {
+  ablock->server = FALSE;
+  HDEBUG(D_auth) debug_printf("%s authenticator:  "
+        "Need server_realm for %s mechanism\n",
         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.
-
-So don't activate without server_condition, this might be relaxed in the future.
*/

-if (ablock->server_condition) ablock->server = TRUE;
-ablock->client = FALSE;
+ablock->client = ob->client_username && ob->client_password;
}


@@ -207,21 +226,38 @@ int rc = 0;
struct callback_exim_state *cb_state =
(struct callback_exim_state *)gsasl_session_hook_get(sctx);

-HDEBUG(D_auth)
-  debug_printf("GNU SASL Callback entered, prop=%d (loop prop=%d)\n",
-      prop, callback_loop);
-
 if (!cb_state)
   {
-  HDEBUG(D_auth) debug_printf("  not from our server/client processing.\n");
+  HDEBUG(D_auth) debug_printf("gsasl callback (%d) not from our server/client processing\n", prop);
+#ifdef CHANNELBIND_HACK
+  if (prop == GSASL_CB_TLS_UNIQUE)
+    {
+    uschar * s;
+    if ((s = gsasl_callback_hook_get(ctx)))
+      {
+      HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE from ctx hook\n");
+      gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CS s);
+      }
+    else
+      {
+      HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE!  dummy for now\n");
+      gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, "");
+      }
+    return GSASL_OK;
+    }
+#endif
   return GSASL_NO_CALLBACK;
   }


+HDEBUG(D_auth)
+  debug_printf("GNU SASL Callback entered, prop=%d (loop prop=%d)\n",
+      prop, callback_loop);
+
 if (callback_loop > 0)
   {
-  /* Most likely is that we were asked for property foo, and to
-  expand the string we asked for property bar to put into an auth
-  variable, but property bar is not supplied for this mechanism. */
+  /* Most likely is that we were asked for property FOO, and to
+  expand the string we asked for property BAR to put into an auth
+  variable, but property BAR is not supplied for this mechanism. */
   HDEBUG(D_auth)
     debug_printf("Loop, asked for property %d while handling property %d\n",
     prop, callback_loop);
@@ -261,9 +297,20 @@ struct callback_exim_state cb_state;
 int rc, auth_result, exim_error, exim_error_override;


 HDEBUG(D_auth)
-  debug_printf("GNU SASL: initialising session for %s, mechanism %s.\n",
+  debug_printf("GNU SASL: initialising session for %s, mechanism %s\n",
       ablock->name, ob->server_mech);


+#ifndef DISABLE_TLS
+# 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. */
+
+if (tls_in.channelbinding && ob->server_channelbinding)
+ gsasl_callback_hook_set(gsasl_ctx, tls_in.channelbinding);
+# endif
+#endif
+
if ((rc = gsasl_server_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK)
{
auth_defer_msg = string_sprintf("GNU SASL: session start failure: %s (%s)",
@@ -273,10 +320,9 @@ if ((rc = gsasl_server_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK
}
/* Hereafter: gsasl_finish(sctx) please */

-gsasl_session_hook_set(sctx, (void *)ablock);
cb_state.ablock = ablock;
cb_state.currently = CURRENTLY_SERVER;
-gsasl_session_hook_set(sctx, (void *)&cb_state);
+gsasl_session_hook_set(sctx, &cb_state);

 tmps = CS expand_string(ob->server_service);
 gsasl_property_set(sctx, GSASL_SERVICE, tmps);
@@ -316,8 +362,9 @@ if (tls_in.channelbinding)
     {
     HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
     ablock->name);
-    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE,
-    CCS  tls_in.channelbinding);
+# ifdef CHANNELBIND_HACK
+    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_in.channelbinding);
+# endif
     }
   else
     HDEBUG(D_auth)
@@ -375,7 +422,7 @@ do {
     }


   if ((rc == GSASL_NEEDS_MORE) || (to_send && *to_send))
-    exim_error = auth_get_no64_data((uschar **)&received, US to_send);
+    exim_error = auth_get_no64_data(USS &received, US to_send);


   if (to_send)
     {
@@ -454,12 +501,13 @@ expand_nmax = 0;
 switch (prop)
   {
   case GSASL_VALIDATE_SIMPLE:
+    HDEBUG(D_auth) debug_printf(" VALIDATE_SIMPLE\n");
     /* GSASL_AUTHID, GSASL_AUTHZID, and GSASL_PASSWORD */
-    propval = US  gsasl_property_fast(sctx, GSASL_AUTHID);
+    propval = US gsasl_property_fast(sctx, GSASL_AUTHID);
     auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US"";
-    propval = US  gsasl_property_fast(sctx, GSASL_AUTHZID);
+    propval = US gsasl_property_fast(sctx, GSASL_AUTHZID);
     auth_vars[1] = expand_nstring[2] = propval ? string_copy(propval) : US"";
-    propval = US  gsasl_property_fast(sctx, GSASL_PASSWORD);
+    propval = US gsasl_property_fast(sctx, GSASL_PASSWORD);
     auth_vars[2] = expand_nstring[3] = propval ? string_copy(propval) : US"";
     expand_nmax = 3;
     for (int i = 1; i <= 3; ++i)
@@ -470,13 +518,14 @@ 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");
+      HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate EXTERNAL\n");
       cbrc = GSASL_AUTHENTICATION_ERROR;
       break;
       }
-    propval = US  gsasl_property_fast(sctx, GSASL_AUTHZID);
+    propval = US gsasl_property_fast(sctx, GSASL_AUTHZID);


     /* We always set $auth1, even if only to empty string. */
     auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US"";
@@ -489,13 +538,14 @@ 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");
+      HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate ANONYMOUS\n");
       cbrc = GSASL_AUTHENTICATION_ERROR;
       break;
       }
-    propval = US  gsasl_property_fast(sctx, GSASL_ANONYMOUS_TOKEN);
+    propval = US gsasl_property_fast(sctx, GSASL_ANONYMOUS_TOKEN);


     /* We always set $auth1, even if only to empty string. */


@@ -509,6 +559,7 @@ 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
@@ -518,9 +569,9 @@ switch (prop)
     to the first release of Exim with this authenticator, they've been
     switched to match the ordering of GSASL_VALIDATE_SIMPLE. */


-    propval = US  gsasl_property_fast(sctx, GSASL_GSSAPI_DISPLAY_NAME);
+    propval = US gsasl_property_fast(sctx, GSASL_GSSAPI_DISPLAY_NAME);
     auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US"";
-    propval = US  gsasl_property_fast(sctx, GSASL_AUTHZID);
+    propval = US gsasl_property_fast(sctx, GSASL_AUTHZID);
     auth_vars[1] = expand_nstring[2] = propval ? string_copy(propval) : US"";
     expand_nmax = 2;
     for (int i = 1; i <= 2; ++i)
@@ -535,6 +586,7 @@ switch (prop)
     break;


   case GSASL_SCRAM_ITER:
+    HDEBUG(D_auth) debug_printf(" SCRAM_ITER\n");
     if (ob->server_scram_iter)
       {
       tmps = CS expand_string(ob->server_scram_iter);
@@ -544,6 +596,7 @@ switch (prop)
     break;


   case GSASL_SCRAM_SALT:
+    HDEBUG(D_auth) debug_printf(" SCRAM_SALT\n");
     if (ob->server_scram_iter)
       {
       tmps = CS expand_string(ob->server_scram_salt);
@@ -553,6 +606,7 @@ switch (prop)
     break;


   case GSASL_PASSWORD:
+    HDEBUG(D_auth) debug_printf(" PASSWORD\n");
     /* DIGEST-MD5: GSASL_AUTHID, GSASL_AUTHZID and GSASL_REALM
        CRAM-MD5: GSASL_AUTHID
        PLAIN: GSASL_AUTHID and GSASL_AUTHZID
@@ -576,11 +630,11 @@ switch (prop)
     needing to add more glue, since avoiding that is a large part of the
     point of SASL. */


-    propval = US  gsasl_property_fast(sctx, GSASL_AUTHID);
+    propval = US gsasl_property_fast(sctx, GSASL_AUTHID);
     auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US"";
-    propval = US  gsasl_property_fast(sctx, GSASL_AUTHZID);
+    propval = US gsasl_property_fast(sctx, GSASL_AUTHZID);
     auth_vars[1] = expand_nstring[2] = propval ? string_copy(propval) : US"";
-    propval = US  gsasl_property_fast(sctx, GSASL_REALM);
+    propval = US gsasl_property_fast(sctx, GSASL_REALM);
     auth_vars[2] = expand_nstring[3] = propval ? string_copy(propval) : US"";
     expand_nmax = 3;
     for (int i = 1; i <= 3; ++i)
@@ -604,7 +658,7 @@ switch (prop)
     break;


   default:
-    HDEBUG(D_auth) debug_printf("Unrecognised callback: %d\n", prop);
+    HDEBUG(D_auth) debug_printf(" Unrecognised callback: %d\n", prop);
     cbrc = GSASL_NO_CALLBACK;
   }


@@ -615,6 +669,48 @@ return cbrc;
}


+/******************************************************************************/
+
+#define PROP_OPTIONAL    BIT(0)
+#define PROP_STRINGPREP    BIT(1)
+
+
+static BOOL
+client_prop(Gsasl_session * sctx, Gsasl_property propnum, uschar * val,
+  const uschar * why, unsigned flags, uschar * buffer, int buffsize)
+{
+uschar * s, * t;
+int rc;
+
+if (flags & PROP_OPTIONAL && !val) return TRUE;
+if (!(s = expand_string(val)) || !(flags & PROP_OPTIONAL) && !*s)
+  {
+  string_format(buffer, buffsize, "%s", expand_string_message);
+  return FALSE;
+  }
+if (!*s) return TRUE;
+
+#ifdef SUPPORT_I18N
+if (flags & PROP_STRINGPREP)
+  {
+  if (gsasl_saslprep(CCS s, 0, CSS &t, &rc) != GSASL_OK)
+    {
+    string_format(buffer, buffsize, "Bad result from saslprep(%s): %s\n",
+          why, stringprep_strerror(rc));
+    HDEBUG(D_auth) debug_printf("%s\n", buffer);
+    return FALSE;
+    }
+  gsasl_property_set(sctx, propnum, CS t);
+
+  free(t);
+  }
+else
+#endif
+  gsasl_property_set(sctx, propnum, CS s);
+
+return TRUE;
+}
+
 /*************************************************
 *              Client entry point                *
 *************************************************/
@@ -629,24 +725,149 @@ auth_gsasl_client(
   uschar *buffer,            /* buffer for reading response */
   int buffsize)                /* size of buffer */
 {
+auth_gsasl_options_block *ob =
+  (auth_gsasl_options_block *)(ablock->options_block);
+Gsasl_session * sctx = NULL;
+struct callback_exim_state cb_state;
+uschar * s;
+BOOL initial = TRUE, do_stringprep;
+int rc, yield = FAIL, flags;
+
 HDEBUG(D_auth)
-  debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
-/* NOT IMPLEMENTED */
-return FAIL;
+  debug_printf("GNU SASL: initialising session for %s, mechanism %s\n",
+      ablock->name, ob->server_mech);
+
+*buffer = 0;
+
+#ifndef DISABLE_TLS
+/* 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. */
+
+if (tls_out.channelbinding)
+  if (ob->client_channelbinding)
+    gsasl_callback_hook_set(gsasl_ctx, tls_out.channelbinding);
+#endif
+
+if ((rc = gsasl_client_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK)
+  {
+  string_format(buffer, buffsize, "GNU SASL: session start failure: %s (%s)",
+      gsasl_strerror_name(rc), gsasl_strerror(rc));
+  HDEBUG(D_auth) debug_printf("%s\n", buffer);
+  return ERROR;
+  }
+
+cb_state.ablock = ablock;
+cb_state.currently = CURRENTLY_CLIENT;
+gsasl_session_hook_set(sctx, &cb_state);
+
+/* Set properties */
+
+flags = Ustrncmp(ob->server_mech, "SCRAM-", 5) == 0 ? PROP_STRINGPREP : 0;
+
+if (  !client_prop(sctx, GSASL_PASSWORD, ob->client_password, US"password",
+          flags, buffer, buffsize)
+   || !client_prop(sctx, GSASL_AUTHID, ob->client_username, US"username",
+          flags, buffer, buffsize)
+   || !client_prop(sctx, GSASL_AUTHZID, ob->client_authz, US"authz",
+          flags | PROP_OPTIONAL, buffer, buffsize)
+   )
+  return ERROR;
+
+#ifndef DISABLE_TLS
+if (tls_out.channelbinding)
+  if (ob->client_channelbinding)
+    {
+    HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
+    ablock->name);
+# ifdef CHANNELBIND_HACK
+    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding);
+# endif
+    }
+  else
+    HDEBUG(D_auth)
+      debug_printf("Auth %s: Not enabling channel-binding (data available)\n",
+      ablock->name);
+#endif
+
+/* Run the SASL conversation with the server */
+
+for(s = NULL; ;)
+  {
+  uschar * outstr;
+  BOOL fail;
+
+  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)
+    {
+    yield = FAIL_SEND;
+    goto done;
+    }
+  initial = FALSE;
+
+  if (rc != GSASL_NEEDS_MORE)
+    {
+    if (rc != GSASL_OK)
+      {
+      string_format(buffer, buffsize, "gsasl: %s", gsasl_strerror(rc));
+      break;
+      }
+
+    /* expecting a final 2xx from the server, accepting the AUTH */
+
+    if (smtp_read_response(sx, buffer, buffsize, '2', timeout))
+      yield = OK;
+    break;    /* from SASL sequence loop */
+    }
+
+  /* 2xx or 3xx response is acceptable.  If 2xx, no further input */
+
+  if (!smtp_read_response(sx, buffer, buffsize, '3', timeout))
+    if (errno == 0 && buffer[0] == '2')
+      buffer[4] = '\0';
+    else
+      {
+      yield = FAIL;
+      goto done;
+      }
+  s = buffer + 4;
+  }
+
+done:
+gsasl_finish(sctx);
+return yield;
 }


 static int
 client_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, auth_instance *ablock)
 {
-int cbrc = GSASL_NO_CALLBACK;
-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("Client side NOT IMPLEMENTED: you should not see this!\n");
-
-return cbrc;
+HDEBUG(D_auth) debug_printf("GNU SASL callback %d for %s/%s as client\n",
+        prop, ablock->name, ablock->public_name);
+switch (prop)
+  {
+  case GSASL_AUTHZID:
+    HDEBUG(D_auth) debug_printf(" inquired for AUTHZID; not providing one\n");
+    break;
+  case GSASL_SCRAM_SALTED_PASSWORD:
+    HDEBUG(D_auth)
+      debug_printf(" inquired for SCRAM_SALTED_PASSWORD; not providing one\n");
+    break;
+  case GSASL_CB_TLS_UNIQUE:
+    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);
+    break;
+  }
+return GSASL_NO_CALLBACK;
 }


/*************************************************
diff --git a/src/src/auths/gsasl_exim.h b/src/src/auths/gsasl_exim.h
index 8842165..93d2078 100644
--- a/src/src/auths/gsasl_exim.h
+++ b/src/src/auths/gsasl_exim.h
@@ -3,6 +3,7 @@
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 2012 */
+/* Copyright (c) The Exim Maintainers 2019 */
/* See the file NOTICE for conditions of use and distribution. */

 /* Copyright (c) Twitter Inc 2012 */
@@ -19,7 +20,13 @@ typedef struct {
   uschar *server_password;
   uschar *server_scram_iter;
   uschar *server_scram_salt;
+
+  uschar *client_username;
+  uschar *client_password;
+  uschar *client_authz;
+
   BOOL    server_channelbinding;
+  BOOL      client_channelbinding;
 } auth_gsasl_options_block;


 /* Data for reading the authenticator-specific options. */
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 0597562..f202288 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -128,7 +128,7 @@ auth_info auths_available[] = {
   .options_len =    sizeof(auth_gsasl_options_block),
   .init =        auth_gsasl_init,
   .servercode =        auth_gsasl_server,
-  .clientcode =        NULL,
+  .clientcode =        auth_gsasl_client,
   .version_report =    auth_gsasl_version_report
   },
 #endif
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index a236bc0..bee5a42 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -2831,7 +2831,7 @@ See description in https://paquier.xyz/postgresql-2/channel-binding-openssl/ */
   store_pool = POOL_PERM;
     tls_in.channelbinding = b64encode_taint(CUS s, (int)len, FALSE);
   store_pool = old_pool;
-  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage\n");
+  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p\n", tls_in.channelbinding);
   }


 /* Only used by the server-side tls (tls_in), including tls_getc.
@@ -3407,7 +3407,7 @@ tlsp->cipher_stdname = cipher_stdname_ssl(exim_client_ctx->ssl);
   store_pool = POOL_PERM;
     tlsp->channelbinding = b64encode_taint(CUS s, (int)len, TRUE);
   store_pool = old_pool;
-  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage\n");
+  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p %p\n", tlsp->channelbinding, tlsp);
   }


tlsp->active.sock = cctx->sock;
diff --git a/test/confs/3820 b/test/confs/3820
index a0206f3..023ed75 100644
--- a/test/confs/3820
+++ b/test/confs/3820
@@ -2,17 +2,47 @@

SERVER=

+.ifdef TRUSTED
+.include DIR/aux-var/tls_conf_prefix
+.else
.include DIR/aux-var/std_conf_prefix
+.endif

primary_hostname = myhost.test.ex
+tls_certificate = ${if eq {SERVER}{server}{DIR/aux-fixed/cert1}fail}

# ----- Main settings -----

+acl_smtp_rcpt = accept
+queue_only
+
+
+begin routers
+
+client_r:
+  driver =    accept
+  condition =    ${if !eq {SERVER}{server}}
+  transport =    smtp
+
+begin transports
+
+smtp:
+  driver =    smtp
+  hosts =    127.0.0.1
+  allow_localhost
+  port =    PORT_D
+.ifdef TRUSTED
+  hosts_require_tls = *
+  tls_verify_certificates = DIR/aux-fixed/cert1
+  tls_verify_cert_hostnames = :
+.endif
+  hosts_require_auth = *


# ----- Authentication -----

begin authenticators

+.ifndef TRUSTED
 sasl1:
   driver = gsasl
   public_name = ANONYMOUS
@@ -23,11 +53,22 @@ sasl2:
   driver = gsasl
   public_name = PLAIN
   server_set_id =    $auth1
-  server_condition =    false
+  server_condition =    ${if eq {$auth3}{pencil}}
+
+  client_condition =    ${if eq {plain}{$local_part}}
+  client_username =    ph10
+  client_password =    pencil
+.endif


 sasl3:
   driver = gsasl
+.ifdef TRUSTED
+  public_name = SCRAM-SHA-1-PLUS
+  server_advertise_condition =    ${if def:tls_in_cipher}
+  server_channelbinding =    true
+.else
   public_name = SCRAM-SHA-1
+.endif


# will need to give library salt, stored-key, server-key, itercount
#
@@ -35,13 +76,18 @@ sasl3:
# gsasl takes props: GSASL_SCRAM_ITER, GSASL_SCRAM_SALT. It _might_ take
# a GSASL_SCRAM_SALTED_PASSWORD - but that is only documented for client mode.

-  server_scram_iter =    4096
   # unclear if the salt is given in binary or base64 to the library
   server_scram_salt =    QSXCR+Q6sek8bf92
   server_password =    pencil
-
   server_condition =    true
   server_set_id =    $auth1


+  client_condition =    ${if eq {scram_sha_1}{$local_part}}
+  client_username =    ph10
+  client_password =    pencil
+.ifdef TRUSTED
+  client_channelbinding = true
+.endif
+


 # End
diff --git a/test/confs/3821 b/test/confs/3821
new file mode 120000
index 0000000..d8f3286
--- /dev/null
+++ b/test/confs/3821
@@ -0,0 +1 @@
+3820
\ No newline at end of file
diff --git a/test/confs/3828 b/test/confs/3828
new file mode 100644
index 0000000..aa9db94
--- /dev/null
+++ b/test/confs/3828
@@ -0,0 +1,66 @@
+# Exim test configuration 3828
+
+SERVER=
+
+.include DIR/aux-var/std_conf_prefix
+
+primary_hostname = myhost.test.ex
+
+# ----- Main settings -----
+
+acl_smtp_rcpt = accept
+queue_only
+
+
+begin routers
+
+client_r:
+  driver =    accept
+  condition =    ${if !eq {SERVER}{server}}
+  transport =    smtp
+
+begin transports
+
+smtp:
+  driver =    smtp
+  hosts =    127.0.0.1
+  allow_localhost
+  port =    PORT_D
+  hosts_require_auth = *
+
+# ----- Authentication -----
+
+begin authenticators
+
+.ifndef OPT
+sasl1:
+  driver =        plaintext
+  public_name =        PLAIN
+  server_prompts =    :
+  server_condition =    ${if and {{eq{$auth2}{ph10}}{eq{$auth3}{mysecret}}}}
+  server_set_id =    $auth2
+
+sasl2:
+  driver =        gsasl
+  public_name =        PLAIN
+  client_condition =    ${if eq {plain}{$local_part}}
+  client_username =    ph10
+  client_password =    mysecret
+
+.else
+sasl3:
+  driver =        gsasl
+  public_name =        PLAIN
+  server_condition =    ${if and {{eq{$auth1}{ph10}}{eq{$auth3}{mysecret}}}}
+  server_set_id =    $auth1
+
+sasl4:
+  driver =        plaintext
+  public_name =        PLAIN
+  client_condition =    ${if eq {plain}{$local_part}}
+  client_send =        ^ph10^mysecret
+
+.endif
+
+
+# End
diff --git a/test/confs/3829 b/test/confs/3829
new file mode 120000
index 0000000..d8f3286
--- /dev/null
+++ b/test/confs/3829
@@ -0,0 +1 @@
+3820
\ No newline at end of file
diff --git a/test/log/3828 b/test/log/3828
new file mode 100644
index 0000000..038a795
--- /dev/null
+++ b/test/log/3828
@@ -0,0 +1,12 @@
+1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@??? U=CALLER P=local S=sss
+1999-03-02 09:44:33 10HmaX-0005vi-00 => plain@??? R=client_r T=smtp H=127.0.0.1 [127.0.0.1] A=sasl2 C="250 OK id=10HmaY-0005vi-00"
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
+1999-03-02 09:44:33 10HmaZ-0005vi-00 <= CALLER@??? U=CALLER P=local S=sss
+1999-03-02 09:44:33 10HmaZ-0005vi-00 => plain@??? R=client_r T=smtp H=127.0.0.1 [127.0.0.1] A=sasl4 C="250 OK id=10HmbA-0005vi-00"
+1999-03-02 09:44:33 10HmaZ-0005vi-00 Completed
+
+******** SERVER ********
+1999-03-02 09:44:33 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on port PORT_D
+1999-03-02 09:44:33 10HmaY-0005vi-00 <= CALLER@??? H=localhost (myhost.test.ex) [127.0.0.1] P=esmtpa A=sasl1:ph10 S=sss id=E10HmaX-0005vi-00@???
+1999-03-02 09:44:33 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on port PORT_D
+1999-03-02 09:44:33 10HmbA-0005vi-00 <= CALLER@??? H=localhost (myhost.test.ex) [127.0.0.1] P=esmtpa A=sasl3:ph10 S=sss id=E10HmaZ-0005vi-00@???
diff --git a/test/scripts/3820-Gnu-SASL/3821 b/test/scripts/3820-Gnu-SASL/3821
new file mode 100644
index 0000000..e43f476
--- /dev/null
+++ b/test/scripts/3820-Gnu-SASL/3821
@@ -0,0 +1,10 @@
+# GSASL PLAIN & SCRAM authentication - gsasl client versus gsasl server
+#
+exim -DSERVER=server -bd -oX PORT_D
+****
+exim -odi plain@???
+****
+exim -odi scram_sha_1@???
+****
+killdaemon
+no_msglog_check
diff --git a/test/scripts/3828-gsasl-plaintext/3828 b/test/scripts/3828-gsasl-plaintext/3828
new file mode 100644
index 0000000..a30888f
--- /dev/null
+++ b/test/scripts/3828-gsasl-plaintext/3828
@@ -0,0 +1,16 @@
+# GSASL PLAIN authentication: gsasl driver vs. plaintext driver
+#
+# gsasl client against plaintext server
+exim -DSERVER=server -bd -oX PORT_D
+****
+exim -odi plain@???
+****
+killdaemon
+#
+# plaintext client against gsasl server
+exim -DSERVER=server -DOPT=y -bd -oX PORT_D
+****
+exim -odi -DOPT=y plain@???
+****
+killdaemon
+no_msglog_check
diff --git a/test/scripts/3828-gsasl-plaintext/REQUIRES b/test/scripts/3828-gsasl-plaintext/REQUIRES
new file mode 100644
index 0000000..905a622
--- /dev/null
+++ b/test/scripts/3828-gsasl-plaintext/REQUIRES
@@ -0,0 +1,2 @@
+authenticator gsasl
+authenticator plaintext
diff --git a/test/scripts/3829-gsasl-scram-plus/3829 b/test/scripts/3829-gsasl-scram-plus/3829
new file mode 100644
index 0000000..8938b1f
--- /dev/null
+++ b/test/scripts/3829-gsasl-scram-plus/3829
@@ -0,0 +1,8 @@
+# GSASL SCRAM-SHA-1-PLUS
+#
+exim -DSERVER=server -DTRUSTED -bd -oX PORT_D
+****
+exim -odi -DTRUSTED scram_sha_1@???
+****
+killdaemon
+no_msglog_check
diff --git a/test/scripts/3829-gsasl-scram-plus/REQUIRES b/test/scripts/3829-gsasl-scram-plus/REQUIRES
new file mode 100644
index 0000000..9c2ca05
--- /dev/null
+++ b/test/scripts/3829-gsasl-scram-plus/REQUIRES
@@ -0,0 +1,2 @@
+authenticator gsasl
+feature _HAVE_TLS