GSASL: use tls-exporter for SCRAM*PLUS methods under TLSv1.3
authorJeremy Harris <jgh146exb@wizmail.org>
Thu, 18 Aug 2022 19:47:01 +0000 (20:47 +0100)
committerJeremy Harris <jgh146exb@wizmail.org>
Thu, 18 Aug 2022 19:47:01 +0000 (20:47 +0100)
src/src/auths/gsasl_exim.c
src/src/globals.h
src/src/tls-gnu.c
src/src/tls-openssl.c
src/src/transports/smtp.c

index bae5f081b0a53a37afb788710687c0bd3acfb377..e49e83b81e11133e9ab9b36f81f8678b9cd89df7 100644 (file)
@@ -39,22 +39,34 @@ static void dummy(int x) { dummy2(x-1); }
 #include "gsasl_exim.h"
 
 
 #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_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
 #  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
 #  define CHANNELBIND_HACK
 # endif
-
-#else
-# define CHANNELBIND_HACK
 #endif
 
 /* Convenience for testing strings */
 #endif
 
 /* Convenience for testing strings */
@@ -258,7 +270,7 @@ if (!cb_state)
   if (prop == GSASL_CB_TLS_UNIQUE)
     {
     uschar * s;
   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);
       {
       HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE from ctx hook\n");
       gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CS s);
@@ -332,6 +344,9 @@ switch (prop)
 #ifdef EXIM_GSASL_SCRAM_S_KEY
   case GSASL_SCRAM_STOREDKEY:          return US"SCRAM_STOREDKEY";
   case GSASL_SCRAM_SERVERKEY:          return US"SCRAM_SERVERKEY";
 #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";
 #endif
   case GSASL_CB_TLS_UNIQUE:            return US"CB_TLS_UNIQUE";
   case GSASL_SAML20_IDP_IDENTIFIER:    return US"SAML20_IDP_IDENTIFIER";
@@ -351,6 +366,14 @@ switch (prop)
 return CUS string_sprintf("(unknown prop: %d)", (int)prop);
 }
 
 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                 *
 *************************************************/
 /*************************************************
 *             Server entry point                 *
 *************************************************/
@@ -358,12 +381,12 @@ return CUS string_sprintf("(unknown prop: %d)", (int)prop);
 /* For interface, see auths/README */
 
 int
 /* 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;
   (auth_gsasl_options_block *)(ablock->options_block);
 struct callback_exim_state cb_state;
 int rc, auth_result, exim_error, exim_error_override;
@@ -406,18 +429,18 @@ cb_state.ablock = ablock;
 cb_state.currently = CURRENTLY_SERVER;
 gsasl_session_hook_set(sctx, &cb_state);
 
 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)
   {
 if (ob->server_realm)
   {
-  tmps = CS expand_string(ob->server_realm);
+  tmps = expand_string(ob->server_realm);
   if (tmps && *tmps)
   if (tmps && *tmps)
-    gsasl_property_set(sctx, GSASL_REALM, tmps);
+    preload_prop(sctx, GSASL_REALM, tmps);
   }
 /* We don't support protection layers. */
   }
 /* 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)
 
 #ifndef DISABLE_TLS
 if (tls_in.channelbinding)
@@ -451,7 +474,12 @@ if (tls_in.channelbinding)
     HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
        ablock->name);
 # ifndef CHANNELBIND_HACK
     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
 # endif
     }
   else
@@ -811,13 +839,13 @@ return TRUE;
 
 int
 auth_gsasl_client(
 
 int
 auth_gsasl_client(
-  auth_instance *ablock,               /* authenticator block */
+  auth_instance * ablock,              /* authenticator block */
   void * sx,                           /* connection */
   int timeout,                         /* command timeout */
   void * sx,                           /* connection */
   int timeout,                         /* command timeout */
-  uschar *buffer,                      /* buffer for reading response */
+  uschar * buffer,                     /* buffer for reading response */
   int buffsize)                                /* size of buffer */
 {
   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;
   (auth_gsasl_options_block *)(ablock->options_block);
 Gsasl_session * sctx = NULL;
 struct callback_exim_state cb_state;
@@ -883,7 +911,12 @@ if (tls_out.channelbinding)
     HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
        ablock->name);
 # ifndef CHANNELBIND_HACK
     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
 # endif
     }
   else
@@ -968,9 +1001,18 @@ 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)
   {
            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:
     gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding);
     return GSASL_OK;
   case GSASL_SCRAM_SALTED_PASSWORD:
index 3f3c798b71b5a40d5fd1258e62667b6876abb13e..c40ae4bebee9c074f3f4e9dfdaec0f7691a226df 100644 (file)
@@ -117,6 +117,7 @@ typedef struct {
 #endif
   BOOL   verify_override:1;    /* certificate_verified only due to tls_try_verify_hosts */
   BOOL   ext_master_secret:1;  /* extended-master-secret was used */
 #endif
   BOOL   verify_override:1;    /* certificate_verified only due to tls_try_verify_hosts */
   BOOL   ext_master_secret:1;  /* extended-master-secret was used */
+  BOOL   channelbind_exporter:1; /* channelbinding is EXPORTER not UNIQUE */
 } tls_support;
 extern tls_support tls_in;
 extern tls_support tls_out;
 } tls_support;
 extern tls_support tls_in;
 extern tls_support tls_out;
index fcb8f7ac464a98806477bca2e9a5a20d44f3f7f1..7a6db94e1fbc13d527cce0037a8e2d4c922697a3 100644 (file)
@@ -121,6 +121,10 @@ require current GnuTLS, then we'll drop support for the ancient libraries).
 # endif
 #endif
 
 # endif
 #endif
 
+#if GNUTLS_VERSION_NUMBER >= 0x030702
+# define HAVE_GNUTLS_EXPORTER
+#endif
+
 #ifndef DISABLE_OCSP
 # include <gnutls/ocsp.h>
 #endif
 #ifndef DISABLE_OCSP
 # include <gnutls/ocsp.h>
 #endif
@@ -646,14 +650,20 @@ tlsp->channelbinding = NULL;
 #ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
   {
   gnutls_datum_t channel = {.data = NULL, .size = 0};
 #ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
   {
   gnutls_datum_t channel = {.data = NULL, .size = 0};
-  uschar * buf;
   int rc;
 
   int rc;
 
-# ifdef HAVE_GNUTLS_PRF_RFC5705
+# ifdef HAVE_GNUTLS_EXPORTER
+  if (gnutls_protocol_get_version(state->session) >= GNUTLS_TLS1_3)
+    {
+    rc = gnutls_session_channel_binding(state->session, GNUTLS_CB_TLS_EXPORTER, &channel);
+    tlsp->channelbind_exporter = TRUE;
+    }
+  else
+# elif defined(HAVE_GNUTLS_PRF_RFC5705)
   /* Older libraries may not have GNUTLS_TLS1_3 defined! */
   if (gnutls_protocol_get_version(state->session) > GNUTLS_TLS1_2)
     {
   /* Older libraries may not have GNUTLS_TLS1_3 defined! */
   if (gnutls_protocol_get_version(state->session) > GNUTLS_TLS1_2)
     {
-    buf = store_get(32, state->host ? GET_TAINTED : GET_UNTAINTED);
+    uschar * buf = store_get(32, state->host ? GET_TAINTED : GET_UNTAINTED);
     rc = gnutls_prf_rfc5705(state->session,
                                (size_t)24,  "EXPORTER-Channel-Binding", (size_t)0, "",
                                32, CS buf);
     rc = gnutls_prf_rfc5705(state->session,
                                (size_t)24,  "EXPORTER-Channel-Binding", (size_t)0, "",
                                32, CS buf);
@@ -670,11 +680,11 @@ tlsp->channelbinding = NULL;
     {
     int old_pool = store_pool;
     /* Declare the taintedness of the binding info.  On server, untainted; on
     {
     int old_pool = store_pool;
     /* Declare the taintedness of the binding info.  On server, untainted; on
-    client, tainted - being the Finish msg from the server. */
+    client, tainted if we used the Finish msg from the server. */
 
     store_pool = POOL_PERM;
     tlsp->channelbinding = b64encode_taint(CUS channel.data, (int)channel.size,
 
     store_pool = POOL_PERM;
     tlsp->channelbinding = b64encode_taint(CUS channel.data, (int)channel.size,
-                                           state->host ? GET_TAINTED : GET_UNTAINTED);
+               !tlsp->channelbind_exporter && state->host ? GET_TAINTED : GET_UNTAINTED);
     store_pool = old_pool;
     DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage\n");
     }
     store_pool = old_pool;
     DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage\n");
     }
index 4c61fc0e6b2270fbc78f3466d88fcf99b69ce46c..22750d273fbde01d4c4e0b9a2c2c6b0816dfcb74 100644 (file)
@@ -94,6 +94,10 @@ change this guard and punt the issue for a while longer. */
 # define EXIM_HAVE_OPENSSL_CIPHER_GET_ID
 #endif
 
 # define EXIM_HAVE_OPENSSL_CIPHER_GET_ID
 #endif
 
+#if !defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER >= 0x030000000L)
+# define EXIM_HAVE_EXPORT_CHNL_BNGNG
+#endif
+
 #if !defined(LIBRESSL_VERSION_NUMBER) \
     || LIBRESSL_VERSION_NUMBER >= 0x20010000L
 # if !defined(OPENSSL_NO_ECDH)
 #if !defined(LIBRESSL_VERSION_NUMBER) \
     || LIBRESSL_VERSION_NUMBER >= 0x20010000L
 # if !defined(OPENSSL_NO_ECDH)
@@ -111,11 +115,16 @@ change this guard and punt the issue for a while longer. */
 #  define OPENSSL_HAVE_KEYLOG_CB
 #  define OPENSSL_HAVE_NUM_TICKETS
 #  define EXIM_HAVE_OPENSSL_CIPHER_STD_NAME
 #  define OPENSSL_HAVE_KEYLOG_CB
 #  define OPENSSL_HAVE_NUM_TICKETS
 #  define EXIM_HAVE_OPENSSL_CIPHER_STD_NAME
+#  define EXIM_HAVE_EXP_CHNL_BNGNG
 # else
 #  define OPENSSL_BAD_SRVR_OURCERT
 # endif
 #endif
 
 # else
 #  define OPENSSL_BAD_SRVR_OURCERT
 # endif
 #endif
 
+#if !defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER >= 0x010002000L)
+# define EXIM_HAVE_EXPORT_CHNL_BNGNG
+#endif
+
 #if !defined(EXIM_HAVE_OPENSSL_TLSEXT) && !defined(DISABLE_OCSP)
 # warning "OpenSSL library version too old; define DISABLE_OCSP in Makefile"
 # define DISABLE_OCSP
 #if !defined(EXIM_HAVE_OPENSSL_TLSEXT) && !defined(DISABLE_OCSP)
 # warning "OpenSSL library version too old; define DISABLE_OCSP in Makefile"
 # define DISABLE_OCSP
@@ -3170,6 +3179,52 @@ tls_dump_keylog(SSL * ssl)
 }
 
 
 }
 
 
+/* Channel-binding info for authenticators
+See description in https://paquier.xyz/postgresql-2/channel-binding-openssl/
+for pre-TLS1.3
+*/
+
+static void
+tls_get_channel_binding(SSL * ssl, tls_support * tlsp, const void * taintval)
+{
+uschar c, * s;
+size_t len;
+
+#ifdef EXIM_HAVE_EXPORT_CHNL_BNGNG
+if (SSL_version(ssl) >= TLS1_3_VERSION)
+  {
+  /* It's not documented by OpenSSL how big the output buffer must be.
+  The OpenSSL testcases use 80 bytes but don't say why. The GnuTLS impl only
+  serves out 32B.  RFC 9266 says it is 32B.
+  Interop fails unless we use the same each end. */
+  len = 32;
+
+  tlsp->channelbind_exporter = TRUE;
+  taintval = GET_UNTAINTED;
+  if (SSL_export_keying_material(ssl,
+       s = store_get((int)len, taintval), len,
+       "EXPORTER-Channel-Binding", (size_t) 24,
+       NULL, 0, 0) != 1)
+    len = 0;
+  }
+else
+#endif
+  {
+  len = SSL_get_peer_finished(ssl, &c, 0);
+  len = SSL_get_peer_finished(ssl, s = store_get((int)len, taintval), len);
+  }
+
+if (len > 0)
+  {
+  int old_pool = store_pool;
+  store_pool = POOL_PERM;
+    tlsp->channelbinding = b64encode_taint(CUS s, (int)len, taintval);
+  store_pool = old_pool;
+  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p %p\n", tlsp->channelbinding, tlsp);
+  }
+}
+
+
 /*************************************************
 *       Start a TLS session in a server          *
 *************************************************/
 /*************************************************
 *       Start a TLS session in a server          *
 *************************************************/
@@ -3446,6 +3501,7 @@ else DEBUG(D_tls)
 adjust the input functions to read via TLS, and initialize things. */
 
 #ifdef SSL_get_extms_support
 adjust the input functions to read via TLS, and initialize things. */
 
 #ifdef SSL_get_extms_support
+/*XXX what does this return for tls1.3 ? */
 tls_in.ext_master_secret = SSL_get_extms_support(ssl) == 1;
 #endif
 peer_cert(ssl, &tls_in, peerdn, sizeof(peerdn));
 tls_in.ext_master_secret = SSL_get_extms_support(ssl) == 1;
 #endif
 peer_cert(ssl, &tls_in, peerdn, sizeof(peerdn));
@@ -3478,19 +3534,7 @@ DEBUG(D_tls)
   tls_in.ourcert = crt ? X509_dup(crt) : NULL;
   }
 
   tls_in.ourcert = crt ? X509_dup(crt) : NULL;
   }
 
-/* Channel-binding info for authenticators
-See description in https://paquier.xyz/postgresql-2/channel-binding-openssl/ */
-  {
-  uschar c, * s;
-  size_t len = SSL_get_peer_finished(ssl, &c, 0);
-  int old_pool = store_pool;
-
-  SSL_get_peer_finished(ssl, s = store_get((int)len, GET_UNTAINTED), len);
-  store_pool = POOL_PERM;
-    tls_in.channelbinding = b64encode_taint(CUS s, (int)len, GET_UNTAINTED);
-  store_pool = old_pool;
-  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p\n", tls_in.channelbinding);
-  }
+tls_get_channel_binding(ssl, &tls_in, GET_UNTAINTED);
 
 /* Only used by the server-side tls (tls_in), including tls_getc.
    Client-side (tls_out) reads (seem to?) go via
 
 /* Only used by the server-side tls (tls_in), including tls_getc.
    Client-side (tls_out) reads (seem to?) go via
@@ -4168,18 +4212,7 @@ tlsp->cipher_stdname = cipher_stdname_ssl(exim_client_ctx->ssl);
   }
 
 /*XXX will this work with continued-TLS? */
   }
 
 /*XXX will this work with continued-TLS? */
-/* Channel-binding info for authenticators */
-  {
-  uschar c, * s;
-  size_t len = SSL_get_finished(exim_client_ctx->ssl, &c, 0);
-  int old_pool = store_pool;
-
-  SSL_get_finished(exim_client_ctx->ssl, s = store_get((int)len, GET_TAINTED), len);
-  store_pool = POOL_PERM;
-    tlsp->channelbinding = b64encode_taint(CUS s, (int)len, GET_TAINTED);
-  store_pool = old_pool;
-  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p %p\n", tlsp->channelbinding, tlsp);
-  }
+tls_get_channel_binding(exim_client_ctx->ssl, tlsp, GET_TAINTED);
 
 tlsp->active.sock = cctx->sock;
 tlsp->active.tls_ctx = exim_client_ctx;
 
 tlsp->active.sock = cctx->sock;
 tlsp->active.tls_ctx = exim_client_ctx;
index bbff1cad8b9c39d6cd7877eecfb6c0c056f61464..0fca4584d993e3e18625df5ef8c7bff17d6f3cfe 100644 (file)
@@ -4695,7 +4695,10 @@ if (sx->completed_addr && sx->ok && sx->send_quit)
            open, we must shut down TLS.  Not all MTAs allow for the continuation
            of the SMTP session when TLS is shut down. We test for this by sending
            a new EHLO. If we don't get a good response, we don't attempt to pass
            open, we must shut down TLS.  Not all MTAs allow for the continuation
            of the SMTP session when TLS is shut down. We test for this by sending
            a new EHLO. If we don't get a good response, we don't attempt to pass
-           the socket on. */
+           the socket on.
+           NB: TLS close is *required* per RFC 9266 when tls-exporter info has
+           been used, which we do under TLSv1.3 for the gsasl SCRAM*PLUS methods.
+           But we were always doing it anyway. */
 
          tls_close(sx->cctx.tls_ctx,
            sx->send_tlsclose ? TLS_SHUTDOWN_WAIT : TLS_SHUTDOWN_WONLY);
 
          tls_close(sx->cctx.tls_ctx,
            sx->send_tlsclose ? TLS_SHUTDOWN_WAIT : TLS_SHUTDOWN_WONLY);