New expansion operator sha256 for certificates. Bug 1170
[exim.git] / src / src / tls-gnu.c
index 8fcdd109dd7ecec7a4d99690e079b30d598e74a1..3c926c0d4ab3f515a1aa13ffb647d5838a7439ce 100644 (file)
-/* $Cambridge: exim/src/src/tls-gnu.c,v 1.24 2009/11/16 19:50:37 nm4 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2009 */
+/* Copyright (c) University of Cambridge 1995 - 2014 */
 /* See the file NOTICE for conditions of use and distribution. */
 
-/* This module provides TLS (aka SSL) support for Exim using the GnuTLS
-library. It is #included into tls.c when that library is used. The code herein
-is based on a patch that was contributed by Nikos Mavroyanopoulos.
+/* Copyright (c) Phil Pennock 2012 */
+
+/* This file provides TLS/SSL support for Exim using the GnuTLS library,
+one of the available supported implementations.  This file is #included into
+tls.c when USE_GNUTLS has been set.
+
+The code herein is a revamp of GnuTLS integration using the current APIs; the
+original tls-gnu.c was based on a patch which was contributed by Nikos
+Mavroyanopoulos.  The revamp is partially a rewrite, partially cut&paste as
+appropriate.
 
-No cryptographic code is included in Exim. All this module does is to call
-functions from the GnuTLS library. */
+APIs current as of GnuTLS 2.12.18; note that the GnuTLS manual is for GnuTLS 3,
+which is not widely deployed by OS vendors.  Will note issues below, which may
+assist in updating the code in the future.  Another sources of hints is
+mod_gnutls for Apache (SNI callback registration and handling).
 
+Keeping client and server variables more split than before and is currently
+the norm, in anticipation of TLS in ACL callouts.
 
-/* Heading stuff for GnuTLS */
+I wanted to switch to gnutls_certificate_set_verify_function() so that
+certificate rejection could happen during handshake where it belongs, rather
+than being dropped afterwards, but that was introduced in 2.10.0 and Debian
+(6.0.5) is still on 2.8.6.  So for now we have to stick with sub-par behaviour.
+
+(I wasn't looking for libraries quite that old, when updating to get rid of
+compiler warnings of deprecated APIs.  If it turns out that a lot of the rest
+require current GnuTLS, then we'll drop support for the ancient libraries).
+*/
 
 #include <gnutls/gnutls.h>
+/* needed for cert checks in verification and DN extraction: */
 #include <gnutls/x509.h>
+/* man-page is incorrect, gnutls_rnd() is not in gnutls.h: */
+#include <gnutls/crypto.h>
+/* needed to disable PKCS11 autoload unless requested */
+#if GNUTLS_VERSION_NUMBER >= 0x020c00
+# include <gnutls/pkcs11.h>
+#endif
+#ifdef EXPERIMENTAL_OCSP
+# include <gnutls/ocsp.h>
+#endif
 
+/* GnuTLS 2 vs 3
 
-#define UNKNOWN_NAME "unknown"
-#define DH_BITS      1024
-#define PARAM_SIZE 2*1024
-
+GnuTLS 3 only:
+  gnutls_global_set_audit_log_function()
 
-/* Values for verify_requirment */
+Changes:
+  gnutls_certificate_verify_peers2(): is new, drop the 2 for old version
+*/
 
-enum { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED };
+/* Local static variables for GnuTLS */
 
-/* Local static variables for GNUTLS */
+/* Values for verify_requirement */
 
-static host_item *client_host;
+enum peer_verify_requirement { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED };
 
-static gnutls_dh_params dh_params = NULL;
+/* This holds most state for server or client; with this, we can set up an
+outbound TLS-enabled connection in an ACL callout, while not stomping all
+over the TLS variables available for expansion.
 
-static gnutls_certificate_server_credentials x509_cred = NULL;
-static gnutls_session tls_session = NULL;
+Some of these correspond to variables in globals.c; those variables will
+be set to point to content in one of these instances, as appropriate for
+the stage of the process lifetime.
 
-static char ssl_errstring[256];
+Not handled here: global tls_channelbinding_b64.
+*/
 
-static int  ssl_session_timeout = 200;
-static int  verify_requirement;
+typedef struct exim_gnutls_state {
+  gnutls_session_t     session;
+  gnutls_certificate_credentials_t x509_cred;
+  gnutls_priority_t    priority_cache;
+  enum peer_verify_requirement verify_requirement;
+  int                  fd_in;
+  int                  fd_out;
+  BOOL                 peer_cert_verified;
+  BOOL                 trigger_sni_changes;
+  BOOL                 have_set_peerdn;
+  const struct host_item *host;
+  gnutls_x509_crt_t    peercert;
+  uschar               *peerdn;
+  uschar               *ciphersuite;
+  uschar               *received_sni;
+
+  const uschar *tls_certificate;
+  const uschar *tls_privatekey;
+  const uschar *tls_sni; /* client send only, not received */
+  const uschar *tls_verify_certificates;
+  const uschar *tls_crl;
+  const uschar *tls_require_ciphers;
+  uschar *exp_tls_certificate;
+  uschar *exp_tls_privatekey;
+  uschar *exp_tls_sni;
+  uschar *exp_tls_verify_certificates;
+  uschar *exp_tls_crl;
+  uschar *exp_tls_require_ciphers;
+  uschar *exp_tls_ocsp_file;
+
+  tls_support *tlsp;   /* set in tls_init() */
+
+  uschar *xfer_buffer;
+  int xfer_buffer_lwm;
+  int xfer_buffer_hwm;
+  int xfer_eof;
+  int xfer_error;
+} exim_gnutls_state_st;
+
+static const exim_gnutls_state_st exim_gnutls_state_init = {
+  NULL, NULL, NULL, VERIFY_NONE, -1, -1, FALSE, FALSE, FALSE,
+  NULL, NULL, NULL, NULL,
+  NULL, NULL, NULL, NULL, NULL, NULL,
+  NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+  NULL,
+  NULL, 0, 0, 0, 0,
+};
 
-/* Priorities for TLS algorithms to use. In each case there's a default table,
-and space into which it can be copied and altered. */
+/* Not only do we have our own APIs which don't pass around state, assuming
+it's held in globals, GnuTLS doesn't appear to let us register callback data
+for callbacks, or as part of the session, so we have to keep a "this is the
+context we're currently dealing with" pointer and rely upon being
+single-threaded to keep from processing data on an inbound TLS connection while
+talking to another TLS connection for an outbound check.  This does mean that
+there's no way for heart-beats to be responded to, for the duration of the
+second connection. */
 
-static const int default_proto_priority[16] = {
-  GNUTLS_TLS1,
-  GNUTLS_SSL3,
-  0 };
+static exim_gnutls_state_st state_server, state_client;
 
-static int proto_priority[16];
+/* dh_params are initialised once within the lifetime of a process using TLS;
+if we used TLS in a long-lived daemon, we'd have to reconsider this.  But we
+don't want to repeat this. */
 
-static const int default_kx_priority[16] = {
-  GNUTLS_KX_RSA,
-  GNUTLS_KX_DHE_DSS,
-  GNUTLS_KX_DHE_RSA,
-  0 };
+static gnutls_dh_params_t dh_server_params = NULL;
 
-static int kx_priority[16];
+/* No idea how this value was chosen; preserving it.  Default is 3600. */
 
-static int default_cipher_priority[16] = {
-  GNUTLS_CIPHER_AES_256_CBC,
-  GNUTLS_CIPHER_AES_128_CBC,
-  GNUTLS_CIPHER_3DES_CBC,
-  GNUTLS_CIPHER_ARCFOUR_128,
-  0 };
+static const int ssl_session_timeout = 200;
 
-static int cipher_priority[16];
+static const char * const exim_default_gnutls_priority = "NORMAL";
 
-static const int default_mac_priority[16] = {
-  GNUTLS_MAC_SHA,
-  GNUTLS_MAC_MD5,
-  0 };
+/* Guard library core initialisation */
 
-static int mac_priority[16];
+static BOOL exim_gnutls_base_init_done = FALSE;
 
-/* These two are currently not changeable. */
 
-static const int comp_priority[16] = { GNUTLS_COMP_NULL, 0 };
-static const int cert_type_priority[16] = { GNUTLS_CRT_X509, 0 };
+/* ------------------------------------------------------------------------ */
+/* macros */
 
-/* Tables of priority names and equivalent numbers */
+#define MAX_HOST_LEN 255
 
-typedef struct pri_item {
-  uschar *name;
-  int *values;
-} pri_item;
+/* Set this to control gnutls_global_set_log_level(); values 0 to 9 will setup
+the library logging; a value less than 0 disables the calls to set up logging
+callbacks. */
+#ifndef EXIM_GNUTLS_LIBRARY_LOG_LEVEL
+#define EXIM_GNUTLS_LIBRARY_LOG_LEVEL -1
+#endif
 
+#ifndef EXIM_CLIENT_DH_MIN_BITS
+#define EXIM_CLIENT_DH_MIN_BITS 1024
+#endif
 
-static int tls1_codes[] = { GNUTLS_TLS1, 0 };
-static int ssl3_codes[] = { GNUTLS_SSL3, 0 };
+/* With GnuTLS 2.12.x+ we have gnutls_sec_param_to_pk_bits() with which we
+can ask for a bit-strength.  Without that, we stick to the constant we had
+before, for now. */
+#ifndef EXIM_SERVER_DH_BITS_PRE2_12
+#define EXIM_SERVER_DH_BITS_PRE2_12 1024
+#endif
 
-static pri_item proto_index[] = {
-  { US"TLS1", tls1_codes },
-  { US"SSL3", ssl3_codes }
-};
+#define exim_gnutls_err_check(Label) do { \
+  if (rc != GNUTLS_E_SUCCESS) { return tls_error((Label), gnutls_strerror(rc), host); } } while (0)
+
+#define expand_check_tlsvar(Varname) expand_check(state->Varname, US #Varname, &state->exp_##Varname)
+
+#if GNUTLS_VERSION_NUMBER >= 0x020c00
+#define HAVE_GNUTLS_SESSION_CHANNEL_BINDING
+#define HAVE_GNUTLS_SEC_PARAM_CONSTANTS
+#define HAVE_GNUTLS_RND
+/* The security fix we provide with the gnutls_allow_auto_pkcs11 option
+ * (4.82 PP/09) introduces a compatibility regression. The symbol simply
+ * isn't available sometimes, so this needs to become a conditional
+ * compilation; the sanest way to deal with this being a problem on
+ * older OSes is to block it in the Local/Makefile with this compiler
+ * definition  */
+#ifndef AVOID_GNUTLS_PKCS11
+#define HAVE_GNUTLS_PKCS11
+#endif /* AVOID_GNUTLS_PKCS11 */
+#endif
 
 
-static int kx_rsa_codes[]      = { GNUTLS_KX_RSA,
-                                   GNUTLS_KX_DHE_RSA, 0 };
-static int kx_dhe_codes[]      = { GNUTLS_KX_DHE_DSS,
-                                   GNUTLS_KX_DHE_RSA, 0 };
-static int kx_dhe_dss_codes[]  = { GNUTLS_KX_DHE_DSS, 0 };
-static int kx_dhe_rsa_codes[]  = { GNUTLS_KX_DHE_RSA, 0 };
 
-static pri_item kx_index[] = {
-  { US"DHE_DSS", kx_dhe_dss_codes },
-  { US"DHE_RSA", kx_dhe_rsa_codes },
-  { US"RSA", kx_rsa_codes },
-  { US"DHE", kx_dhe_codes }
-};
 
+/* ------------------------------------------------------------------------ */
+/* Callback declarations */
 
-static int arcfour_128_codes[] = { GNUTLS_CIPHER_ARCFOUR_128, 0 };
-static int arcfour_40_codes[]  = { GNUTLS_CIPHER_ARCFOUR_40, 0 };
-static int arcfour_codes[]     = { GNUTLS_CIPHER_ARCFOUR_128,
-                                   GNUTLS_CIPHER_ARCFOUR_40, 0 };
-static int aes_256_codes[]     = { GNUTLS_CIPHER_AES_256_CBC, 0 };
-static int aes_128_codes[]     = { GNUTLS_CIPHER_AES_128_CBC, 0 };
-static int aes_codes[]         = { GNUTLS_CIPHER_AES_256_CBC,
-                                   GNUTLS_CIPHER_AES_128_CBC, 0 };
-static int des3_codes[]        = { GNUTLS_CIPHER_3DES_CBC, 0 };
-
-static pri_item cipher_index[] = {
-  { US"ARCFOUR_128", arcfour_128_codes },
-  { US"ARCFOUR_40", arcfour_40_codes },
-  { US"ARCFOUR", arcfour_codes },
-  { US"AES_256", aes_256_codes },
-  { US"AES_128", aes_128_codes },
-  { US"AES", aes_codes },
-  { US"3DES", des3_codes }
-};
+#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0
+static void exim_gnutls_logger_cb(int level, const char *message);
+#endif
 
+static int exim_sni_handling_cb(gnutls_session_t session);
 
-static int mac_sha_codes[]     = { GNUTLS_MAC_SHA, 0 };
-static int mac_md5_codes[]     = { GNUTLS_MAC_MD5, 0 };
+#ifdef EXPERIMENTAL_OCSP
+static int server_ocsp_stapling_cb(gnutls_session_t session, void * ptr,
+  gnutls_datum_t * ocsp_response);
+#endif
 
-static pri_item mac_index[] = {
-  { US"SHA",  mac_sha_codes },
-  { US"SHA1", mac_sha_codes },
-  { US"MD5",  mac_md5_codes }
-};
 
 
+/* ------------------------------------------------------------------------ */
+/* Static functions */
 
 /*************************************************
 *               Handle TLS error                 *
@@ -160,120 +227,175 @@ some shared functions.
 
 Argument:
   prefix    text to include in the logged error
-  host      NULL if setting up a server;
-            the connected host if setting up a client
   msg       additional error string (may be NULL)
             usually obtained from gnutls_strerror()
+  host      NULL if setting up a server;
+            the connected host if setting up a client
 
 Returns:    OK/DEFER/FAIL
 */
 
 static int
-tls_error(uschar *prefix, host_item *host, const char *msg)
+tls_error(const uschar *prefix, const char *msg, const host_item *host)
 {
-if (host == NULL)
+if (host)
+  {
+  log_write(0, LOG_MAIN, "TLS error on connection to %s [%s] (%s)%s%s",
+      host->name, host->address, prefix, msg ? ": " : "", msg ? msg : "");
+  return FAIL;
+  }
+else
   {
   uschar *conn_info = smtp_get_connection_info();
-  if (strncmp(conn_info, "SMTP ", 5) == 0)
+  if (Ustrncmp(conn_info, US"SMTP ", 5) == 0)
     conn_info += 5;
   log_write(0, LOG_MAIN, "TLS error on %s (%s)%s%s",
-    conn_info, prefix, msg ? ": " : "", msg ? msg : "");
+      conn_info, prefix, msg ? ": " : "", msg ? msg : "");
   return DEFER;
   }
+}
+
+
+
+
+/*************************************************
+*    Deal with logging errors during I/O         *
+*************************************************/
+
+/* We have to get the identity of the peer from saved data.
+
+Argument:
+  state    the current GnuTLS exim state container
+  rc       the GnuTLS error code, or 0 if it's a local error
+  when     text identifying read or write
+  text     local error text when ec is 0
+
+Returns:   nothing
+*/
+
+static void
+record_io_error(exim_gnutls_state_st *state, int rc, uschar *when, uschar *text)
+{
+const char *msg;
+
+if (rc == GNUTLS_E_FATAL_ALERT_RECEIVED)
+  msg = CS string_sprintf("%s: %s", US gnutls_strerror(rc),
+    US gnutls_alert_get_name(gnutls_alert_get(state->session)));
 else
-  {
-  log_write(0, LOG_MAIN, "TLS error on connection to %s [%s] (%s)%s%s",
-    host->name, host->address, prefix, msg ? ": " : "", msg ? msg : "");
-  return FAIL;
-  }
+  msg = gnutls_strerror(rc);
+
+tls_error(when, msg, state->host);
 }
 
 
 
+
 /*************************************************
-*             Verify certificate                 *
+*        Set various Exim expansion vars         *
 *************************************************/
 
-/* Called after a successful handshake, when certificate verification is
-required or optional, for both server and client.
+#define exim_gnutls_cert_err(Label) do { \
+  if (rc != GNUTLS_E_SUCCESS) { \
+    DEBUG(D_tls) debug_printf("TLS: cert problem: %s: %s\n", (Label), gnutls_strerror(rc)); \
+    return rc; } } while (0)
 
-Arguments:
-  session    GNUTLS session
-  error      where to put text giving a reason for failure
+static int
+import_cert(const gnutls_datum * cert, gnutls_x509_crt_t * crtp)
+{
+int rc;
+
+rc = gnutls_x509_crt_init(crtp);
+exim_gnutls_cert_err(US"gnutls_x509_crt_init (crt)");
+
+rc = gnutls_x509_crt_import(*crtp, cert, GNUTLS_X509_FMT_DER);
+exim_gnutls_cert_err(US"failed to import certificate [gnutls_x509_crt_import(cert)]");
+
+return rc;
+}
+
+#undef exim_gnutls_cert_err
+
+
+/* We set various Exim global variables from the state, once a session has
+been established.  With TLS callouts, may need to change this to stack
+variables, or just re-call it with the server state after client callout
+has finished.
+
+Make sure anything set here is unset in tls_getc().
+
+Sets:
+  tls_active                fd
+  tls_bits                  strength indicator
+  tls_certificate_verified  bool indicator
+  tls_channelbinding_b64    for some SASL mechanisms
+  tls_cipher                a string
+  tls_peercert              pointer to library internal
+  tls_peerdn                a string
+  tls_sni                   a (UTF-8) string
+  tls_ourcert               pointer to library internal
 
-Returns:     TRUE/FALSE
+Argument:
+  state      the relevant exim_gnutls_state_st *
 */
 
-static BOOL
-verify_certificate(gnutls_session session, const char **error)
+static void
+extract_exim_vars_from_tls_state(exim_gnutls_state_st * state)
 {
-int verify;
-uschar *dn_string = US"";
-const gnutls_datum *cert;
-unsigned int cert_size = 0;
+gnutls_cipher_algorithm_t cipher;
+#ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
+int old_pool;
+int rc;
+gnutls_datum_t channel;
+#endif
+tls_support * tlsp = state->tlsp;
 
-*error = NULL;
+tlsp->active = state->fd_out;
 
-/* Get the peer's certificate. If it sent one, extract it's DN, and then
-attempt to verify the certificate. If no certificate is supplied, verification
-is forced to fail. */
+cipher = gnutls_cipher_get(state->session);
+/* returns size in "bytes" */
+tlsp->bits = gnutls_cipher_get_key_size(cipher) * 8;
 
-cert = gnutls_certificate_get_peers(session, &cert_size);
-if (cert != NULL)
-  {
-  uschar buff[1024];
-  gnutls_x509_crt gcert;
+tlsp->cipher = state->ciphersuite;
 
-  gnutls_x509_crt_init(&gcert);
-  dn_string = US"unknown";
+DEBUG(D_tls) debug_printf("cipher: %s\n", state->ciphersuite);
 
-  if (gnutls_x509_crt_import(gcert, cert, GNUTLS_X509_FMT_DER) == 0)
-    {
-    size_t bufsize = sizeof(buff);
-    if (gnutls_x509_crt_get_dn(gcert, CS buff, &bufsize) >= 0)
-      dn_string = string_copy_malloc(buff);
-    }
+tlsp->certificate_verified = state->peer_cert_verified;
 
-  verify = gnutls_certificate_verify_peers(session);
-  }
-else
-  {
-  DEBUG(D_tls) debug_printf("no peer certificate supplied\n");
-  verify = GNUTLS_CERT_INVALID;
-  *error = "not supplied";
-  }
+/* note that tls_channelbinding_b64 is not saved to the spool file, since it's
+only available for use for authenticators while this TLS session is running. */
 
-/* Handle the result of verification. INVALID seems to be set as well
-as REVOKED, but leave the test for both. */
+tls_channelbinding_b64 = NULL;
+#ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
+channel.data = NULL;
+channel.size = 0;
+rc = gnutls_session_channel_binding(state->session, GNUTLS_CB_TLS_UNIQUE, &channel);
+if (rc) {
+  DEBUG(D_tls) debug_printf("Channel binding error: %s\n", gnutls_strerror(rc));
+} else {
+  old_pool = store_pool;
+  store_pool = POOL_PERM;
+  tls_channelbinding_b64 = auth_b64encode(channel.data, (int)channel.size);
+  store_pool = old_pool;
+  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage.\n");
+}
+#endif
 
-if ((verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0)
-  {
-  tls_certificate_verified = FALSE;
-  if (*error == NULL) *error = ((verify & GNUTLS_CERT_REVOKED) != 0)?
-    "revoked" : "invalid";
-  if (verify_requirement == VERIFY_REQUIRED)
-    {
-    DEBUG(D_tls) debug_printf("TLS certificate verification failed (%s): "
-      "peerdn=%s\n", *error, dn_string);
-    gnutls_alert_send(session, GNUTLS_AL_FATAL, GNUTLS_A_BAD_CERTIFICATE);
-    return FALSE;                       /* reject */
-    }
-  DEBUG(D_tls) debug_printf("TLS certificate verify failure (%s) overridden "
-      "(host in tls_try_verify_hosts): peerdn=%s\n", *error, dn_string);
-  }
-else
+/* peercert is set in peer_status() */
+tlsp->peerdn = state->peerdn;
+tlsp->sni =    state->received_sni;
+
+/* record our certificate */
   {
-  tls_certificate_verified = TRUE;
-  DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n",
-    dn_string);
-  }
+  const gnutls_datum * cert = gnutls_certificate_get_ours(state->session);
+  gnutls_x509_crt_t crt;
 
-tls_peerdn = dn_string;
-return TRUE;                            /* accept */
+  tlsp->ourcert = cert && import_cert(cert, &crt)==0 ? crt : NULL;
+  }
 }
 
 
 
+
 /*************************************************
 *            Setup up DH parameters              *
 *************************************************/
@@ -288,30 +410,100 @@ file is never present. If two processes both compute some new parameters, you
 waste a bit of effort, but it doesn't seem worth messing around with locking to
 prevent this.
 
-Argument:
-  host       NULL for server, server for client (for error handling)
-
 Returns:     OK/DEFER/FAIL
 */
 
 static int
-init_dh(host_item *host)
+init_server_dh(void)
 {
-int fd;
-int ret;
+int fd, rc;
+unsigned int dh_bits;
 gnutls_datum m;
-uschar filename[200];
+uschar filename_buf[PATH_MAX];
+uschar *filename = NULL;
+size_t sz;
+uschar *exp_tls_dhparam;
+BOOL use_file_in_spool = FALSE;
+BOOL use_fixed_file = FALSE;
+host_item *host = NULL; /* dummy for macros */
 
-/* Initialize the data structures for holding the parameters */
+DEBUG(D_tls) debug_printf("Initialising GnuTLS server params.\n");
 
-ret = gnutls_dh_params_init(&dh_params);
-if (ret < 0) return tls_error(US"init dh_params", host, gnutls_strerror(ret));
+rc = gnutls_dh_params_init(&dh_server_params);
+exim_gnutls_err_check(US"gnutls_dh_params_init");
 
-/* Set up the name of the cache file */
+m.data = NULL;
+m.size = 0;
 
-if (!string_format(filename, sizeof(filename), "%s/gnutls-params",
-      spool_directory))
-  return tls_error(US"overlong filename", host, NULL);
+if (!expand_check(tls_dhparam, US"tls_dhparam", &exp_tls_dhparam))
+  return DEFER;
+
+if (!exp_tls_dhparam)
+  {
+  DEBUG(D_tls) debug_printf("Loading default hard-coded DH params\n");
+  m.data = US std_dh_prime_default();
+  m.size = Ustrlen(m.data);
+  }
+else if (Ustrcmp(exp_tls_dhparam, "historic") == 0)
+  use_file_in_spool = TRUE;
+else if (Ustrcmp(exp_tls_dhparam, "none") == 0)
+  {
+  DEBUG(D_tls) debug_printf("Requested no DH parameters.\n");
+  return OK;
+  }
+else if (exp_tls_dhparam[0] != '/')
+  {
+  m.data = US std_dh_prime_named(exp_tls_dhparam);
+  if (m.data == NULL)
+    return tls_error(US"No standard prime named", CS exp_tls_dhparam, NULL);
+  m.size = Ustrlen(m.data);
+  }
+else
+  {
+  use_fixed_file = TRUE;
+  filename = exp_tls_dhparam;
+  }
+
+if (m.data)
+  {
+  rc = gnutls_dh_params_import_pkcs3(dh_server_params, &m, GNUTLS_X509_FMT_PEM);
+  exim_gnutls_err_check(US"gnutls_dh_params_import_pkcs3");
+  DEBUG(D_tls) debug_printf("Loaded fixed standard D-H parameters\n");
+  return OK;
+  }
+
+#ifdef HAVE_GNUTLS_SEC_PARAM_CONSTANTS
+/* If you change this constant, also change dh_param_fn_ext so that we can use a
+different filename and ensure we have sufficient bits. */
+dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_NORMAL);
+if (!dh_bits)
+  return tls_error(US"gnutls_sec_param_to_pk_bits() failed", NULL, NULL);
+DEBUG(D_tls)
+  debug_printf("GnuTLS tells us that for D-H PK, NORMAL is %d bits.\n",
+      dh_bits);
+#else
+dh_bits = EXIM_SERVER_DH_BITS_PRE2_12;
+DEBUG(D_tls)
+  debug_printf("GnuTLS lacks gnutls_sec_param_to_pk_bits(), using %d bits.\n",
+      dh_bits);
+#endif
+
+/* Some clients have hard-coded limits. */
+if (dh_bits > tls_dh_max_bits)
+  {
+  DEBUG(D_tls)
+    debug_printf("tls_dh_max_bits clamping override, using %d bits instead.\n",
+        tls_dh_max_bits);
+  dh_bits = tls_dh_max_bits;
+  }
+
+if (use_file_in_spool)
+  {
+  if (!string_format(filename_buf, sizeof(filename_buf),
+        "%s/gnutls-params-%d", spool_directory, dh_bits))
+    return tls_error(US"overlong filename", NULL, NULL);
+  filename = filename_buf;
+  }
 
 /* Open the cache file for reading and if successful, read it and set up the
 parameters. */
@@ -320,27 +512,50 @@ fd = Uopen(filename, O_RDONLY, 0);
 if (fd >= 0)
   {
   struct stat statbuf;
-  if (fstat(fd, &statbuf) < 0)
+  FILE *fp;
+  int saved_errno;
+
+  if (fstat(fd, &statbuf) < 0)  /* EIO */
     {
+    saved_errno = errno;
     (void)close(fd);
-    return tls_error(US"TLS cache stat failed", host, strerror(errno));
+    return tls_error(US"TLS cache stat failed", strerror(saved_errno), NULL);
+    }
+  if (!S_ISREG(statbuf.st_mode))
+    {
+    (void)close(fd);
+    return tls_error(US"TLS cache not a file", NULL, NULL);
+    }
+  fp = fdopen(fd, "rb");
+  if (!fp)
+    {
+    saved_errno = errno;
+    (void)close(fd);
+    return tls_error(US"fdopen(TLS cache stat fd) failed",
+        strerror(saved_errno), NULL);
     }
 
   m.size = statbuf.st_size;
   m.data = malloc(m.size);
   if (m.data == NULL)
-    return tls_error(US"memory allocation failed", host, strerror(errno));
-  errno = 0;
-  if (read(fd, m.data, m.size) != m.size)
-    return tls_error(US"TLS cache read failed", host, strerror(errno));
-  (void)close(fd);
-
-  ret = gnutls_dh_params_import_pkcs3(dh_params, &m, GNUTLS_X509_FMT_PEM);
-  if (ret < 0)
-    return tls_error(US"DH params import", host, gnutls_strerror(ret));
-  DEBUG(D_tls) debug_printf("read D-H parameters from file\n");
+    {
+    fclose(fp);
+    return tls_error(US"malloc failed", strerror(errno), NULL);
+    }
+  sz = fread(m.data, m.size, 1, fp);
+  if (!sz)
+    {
+    saved_errno = errno;
+    fclose(fp);
+    free(m.data);
+    return tls_error(US"fread failed", strerror(saved_errno), NULL);
+    }
+  fclose(fp);
 
+  rc = gnutls_dh_params_import_pkcs3(dh_server_params, &m, GNUTLS_X509_FMT_PEM);
   free(m.data);
+  exim_gnutls_err_check(US"gnutls_dh_params_import_pkcs3");
+  DEBUG(D_tls) debug_printf("read D-H parameters from file \"%s\"\n", filename);
   }
 
 /* If the file does not exist, fall through to compute new data and cache it.
@@ -348,13 +563,13 @@ If there was any other opening error, it is serious. */
 
 else if (errno == ENOENT)
   {
-  ret = -1;
+  rc = -1;
   DEBUG(D_tls)
-    debug_printf("parameter cache file %s does not exist\n", filename);
+    debug_printf("D-H parameter cache file \"%s\" does not exist\n", filename);
   }
 else
-  return tls_error(string_open_failed(errno, "%s for reading", filename),
-    host, NULL);
+  return tls_error(string_open_failed(errno, "\"%s\" for reading", filename),
+      NULL, NULL);
 
 /* If ret < 0, either the cache file does not exist, or the data it contains
 is not useful. One particular case of this is when upgrading from an older
@@ -362,59 +577,93 @@ release of Exim in which the data was stored in a different format. We don't
 try to be clever and support both formats; we just regenerate new data in this
 case. */
 
-if (ret < 0)
+if (rc < 0)
   {
-  uschar tempfilename[sizeof(filename) + 10];
-
-  DEBUG(D_tls) debug_printf("generating %d bit Diffie-Hellman key...\n",
-    DH_BITS);
-  ret = gnutls_dh_params_generate2(dh_params, DH_BITS);
-  if (ret < 0) return tls_error(US"D-H key generation", host, gnutls_strerror(ret));
+  uschar *temp_fn;
+  unsigned int dh_bits_gen = dh_bits;
 
-  /* Write the parameters to a file in the spool directory so that we
-  can use them from other Exim processes. */
+  if ((PATH_MAX - Ustrlen(filename)) < 10)
+    return tls_error(US"Filename too long to generate replacement",
+        CS filename, NULL);
 
-  sprintf(CS tempfilename, "%s-%d", filename, (int)getpid());
-  fd = Uopen(tempfilename, O_WRONLY|O_CREAT, 0400);
+  temp_fn = string_copy(US "%s.XXXXXXX");
+  fd = mkstemp(CS temp_fn); /* modifies temp_fn */
   if (fd < 0)
-    return tls_error(string_open_failed(errno, "%s for writing", filename),
-      host, NULL);
+    return tls_error(US"Unable to open temp file", strerror(errno), NULL);
   (void)fchown(fd, exim_uid, exim_gid);   /* Probably not necessary */
 
-  /* export the parameters in a format that can be generated using GNUTLS'
-   * certtool or other programs.
-   *
-   * The commands for certtool are:
-   * $ certtool --generate-dh-params --bits 1024 > params
+  /* GnuTLS overshoots!
+   * If we ask for 2236, we might get 2237 or more.
+   * But there's no way to ask GnuTLS how many bits there really are.
+   * We can ask how many bits were used in a TLS session, but that's it!
+   * The prime itself is hidden behind too much abstraction.
+   * So we ask for less, and proceed on a wing and a prayer.
+   * First attempt, subtracted 3 for 2233 and got 2240.
    */
+  if (dh_bits >= EXIM_CLIENT_DH_MIN_BITS + 10)
+    {
+    dh_bits_gen = dh_bits - 10;
+    DEBUG(D_tls)
+      debug_printf("being paranoid about DH generation, make it '%d' bits'\n",
+          dh_bits_gen);
+    }
 
-  m.size = PARAM_SIZE;
+  DEBUG(D_tls)
+    debug_printf("requesting generation of %d bit Diffie-Hellman prime ...\n",
+        dh_bits_gen);
+  rc = gnutls_dh_params_generate2(dh_server_params, dh_bits_gen);
+  exim_gnutls_err_check(US"gnutls_dh_params_generate2");
+
+  /* gnutls_dh_params_export_pkcs3() will tell us the exact size, every time,
+  and I confirmed that a NULL call to get the size first is how the GnuTLS
+  sample apps handle this. */
+
+  sz = 0;
+  m.data = NULL;
+  rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM,
+      m.data, &sz);
+  if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
+    exim_gnutls_err_check(US"gnutls_dh_params_export_pkcs3(NULL) sizing");
+  m.size = sz;
   m.data = malloc(m.size);
   if (m.data == NULL)
-    return tls_error(US"memory allocation failed", host, strerror(errno));
-
-  m.size = PARAM_SIZE;
-  ret = gnutls_dh_params_export_pkcs3(dh_params, GNUTLS_X509_FMT_PEM, m.data,
-    &m.size);
-  if (ret < 0)
-    return tls_error(US"DH params export", host, gnutls_strerror(ret));
-
-  m.size = Ustrlen(m.data);
-  errno = 0;
-  if (write(fd, m.data, m.size) != m.size || write(fd, "\n", 1) != 1)
-    return tls_error(US"TLS cache write failed", host, strerror(errno));
+    return tls_error(US"memory allocation failed", strerror(errno), NULL);
+  /* this will return a size 1 less than the allocation size above */
+  rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM,
+      m.data, &sz);
+  if (rc != GNUTLS_E_SUCCESS)
+    {
+    free(m.data);
+    exim_gnutls_err_check(US"gnutls_dh_params_export_pkcs3() real");
+    }
+  m.size = sz; /* shrink by 1, probably */
 
+  sz = write_to_fd_buf(fd, m.data, (size_t) m.size);
+  if (sz != m.size)
+    {
+    free(m.data);
+    return tls_error(US"TLS cache write D-H params failed",
+        strerror(errno), NULL);
+    }
   free(m.data);
-  (void)close(fd);
+  sz = write_to_fd_buf(fd, US"\n", 1);
+  if (sz != 1)
+    return tls_error(US"TLS cache write D-H params final newline failed",
+        strerror(errno), NULL);
 
-  if (rename(CS tempfilename, CS filename) < 0)
-    return tls_error(string_sprintf("failed to rename %s as %s",
-      tempfilename, filename), host, strerror(errno));
+  rc = close(fd);
+  if (rc)
+    return tls_error(US"TLS cache write close() failed",
+        strerror(errno), NULL);
 
-  DEBUG(D_tls) debug_printf("wrote D-H parameters to file %s\n", filename);
+  if (Urename(temp_fn, filename) < 0)
+    return tls_error(string_sprintf("failed to rename \"%s\" as \"%s\"",
+          temp_fn, filename), strerror(errno), NULL);
+
+  DEBUG(D_tls) debug_printf("wrote D-H parameters to file \"%s\"\n", filename);
   }
 
-DEBUG(D_tls) debug_printf("initialized D-H parameters\n");
+DEBUG(D_tls) debug_printf("initialized server D-H parameters\n");
 return OK;
 }
 
@@ -422,91 +671,146 @@ return OK;
 
 
 /*************************************************
-*            Initialize for GnuTLS               *
+*       Variables re-expanded post-SNI           *
 *************************************************/
 
-/* Called from both server and client code. In the case of a server, errors
-before actual TLS negotiation return DEFER.
+/* Called from both server and client code, via tls_init(), and also from
+the SNI callback after receiving an SNI, if tls_certificate includes "tls_sni".
+
+We can tell the two apart by state->received_sni being non-NULL in callback.
+
+The callback should not call us unless state->trigger_sni_changes is true,
+which we are responsible for setting on the first pass through.
 
 Arguments:
-  host            connected host, if client; NULL if server
-  certificate     certificate file
-  privatekey      private key file
-  cas             CA certs file
-  crl             CRL file
+  state           exim_gnutls_state_st *
 
 Returns:          OK/DEFER/FAIL
 */
 
 static int
-tls_init(host_item *host, uschar *certificate, uschar *privatekey, uschar *cas,
-  uschar *crl)
+tls_expand_session_files(exim_gnutls_state_st *state)
 {
+struct stat statbuf;
 int rc;
-uschar *cert_expanded, *key_expanded, *cas_expanded, *crl_expanded;
-
-client_host = host;
+const host_item *host = state->host;  /* macro should be reconsidered? */
+uschar *saved_tls_certificate = NULL;
+uschar *saved_tls_privatekey = NULL;
+uschar *saved_tls_verify_certificates = NULL;
+uschar *saved_tls_crl = NULL;
+int cert_count;
+
+/* We check for tls_sni *before* expansion. */
+if (!host)     /* server */
+  {
+  if (!state->received_sni)
+    {
+    if (state->tls_certificate &&
+        (Ustrstr(state->tls_certificate, US"tls_sni") ||
+         Ustrstr(state->tls_certificate, US"tls_in_sni") ||
+         Ustrstr(state->tls_certificate, US"tls_out_sni")
+       ))
+      {
+      DEBUG(D_tls) debug_printf("We will re-expand TLS session files if we receive SNI.\n");
+      state->trigger_sni_changes = TRUE;
+      }
+    }
+  else
+    {
+    /* useful for debugging */
+    saved_tls_certificate = state->exp_tls_certificate;
+    saved_tls_privatekey = state->exp_tls_privatekey;
+    saved_tls_verify_certificates = state->exp_tls_verify_certificates;
+    saved_tls_crl = state->exp_tls_crl;
+    }
+  }
 
-rc = gnutls_global_init();
-if (rc < 0) return tls_error(US"tls-init", host, gnutls_strerror(rc));
+rc = gnutls_certificate_allocate_credentials(&state->x509_cred);
+exim_gnutls_err_check(US"gnutls_certificate_allocate_credentials");
 
-/* Create D-H parameters, or read them from the cache file. This function does
-its own SMTP error messaging. */
+/* remember: expand_check_tlsvar() is expand_check() but fiddling with
+state members, assuming consistent naming; and expand_check() returns
+false if expansion failed, unless expansion was forced to fail. */
 
-rc = init_dh(host);
-if (rc != OK) return rc;
+/* check if we at least have a certificate, before doing expensive
+D-H generation. */
 
-/* Create the credentials structure */
+if (!expand_check_tlsvar(tls_certificate))
+  return DEFER;
 
-rc = gnutls_certificate_allocate_credentials(&x509_cred);
-if (rc < 0)
-  return tls_error(US"certificate_allocate_credentials",
-    host, gnutls_strerror(rc));
+/* certificate is mandatory in server, optional in client */
 
-/* This stuff must be done for each session, because different certificates
-may be required for different sessions. */
+if ((state->exp_tls_certificate == NULL) ||
+    (*state->exp_tls_certificate == '\0'))
+  {
+  if (!host)
+    return tls_error(US"no TLS server certificate is specified", NULL, NULL);
+  else
+    DEBUG(D_tls) debug_printf("TLS: no client certificate specified; okay\n");
+  }
 
-if (!expand_check(certificate, US"tls_certificate", &cert_expanded))
+if (state->tls_privatekey && !expand_check_tlsvar(tls_privatekey))
   return DEFER;
 
-key_expanded = NULL;
-if (privatekey != NULL)
+/* tls_privatekey is optional, defaulting to same file as certificate */
+
+if (state->tls_privatekey == NULL || *state->tls_privatekey == '\0')
   {
-  if (!expand_check(privatekey, US"tls_privatekey", &key_expanded))
-    return DEFER;
+  state->tls_privatekey = state->tls_certificate;
+  state->exp_tls_privatekey = state->exp_tls_certificate;
   }
 
-/* If expansion was forced to fail, key_expanded will be NULL. If the result of
-the expansion is an empty string, ignore it also, and assume that the private
-key is in the same file as the certificate. */
-
-if (key_expanded == NULL || *key_expanded == 0)
-  key_expanded = cert_expanded;
 
-/* Set the certificate and private keys */
-
-if (cert_expanded != NULL)
+if (state->exp_tls_certificate && *state->exp_tls_certificate)
   {
   DEBUG(D_tls) debug_printf("certificate file = %s\nkey file = %s\n",
-    cert_expanded, key_expanded);
-  rc = gnutls_certificate_set_x509_key_file(x509_cred, CS cert_expanded,
-    CS key_expanded, GNUTLS_X509_FMT_PEM);
-  if (rc < 0)
+      state->exp_tls_certificate, state->exp_tls_privatekey);
+
+  if (state->received_sni)
     {
-    uschar *msg = string_sprintf("cert/key setup: cert=%s key=%s",
-      cert_expanded, key_expanded);
-    return tls_error(msg, host, gnutls_strerror(rc));
+    if ((Ustrcmp(state->exp_tls_certificate, saved_tls_certificate) == 0) &&
+        (Ustrcmp(state->exp_tls_privatekey, saved_tls_privatekey) == 0))
+      {
+      DEBUG(D_tls) debug_printf("TLS SNI: cert and key unchanged\n");
+      }
+    else
+      {
+      DEBUG(D_tls) debug_printf("TLS SNI: have a changed cert/key pair.\n");
+      }
     }
-  }
 
-/* A certificate is mandatory in a server, but not in a client */
+  rc = gnutls_certificate_set_x509_key_file(state->x509_cred,
+      CS state->exp_tls_certificate, CS state->exp_tls_privatekey,
+      GNUTLS_X509_FMT_PEM);
+  exim_gnutls_err_check(
+      string_sprintf("cert/key setup: cert=%s key=%s",
+        state->exp_tls_certificate, state->exp_tls_privatekey));
+  DEBUG(D_tls) debug_printf("TLS: cert/key registered\n");
+  } /* tls_certificate */
 
-else
+
+/* Set the OCSP stapling server info */
+
+#ifdef EXPERIMENTAL_OCSP
+if (  !host    /* server */
+   && tls_ocsp_file
+   )
   {
-  if (host == NULL)
-    return tls_error(US"no TLS server certificate is specified", NULL, NULL);
-  DEBUG(D_tls) debug_printf("no TLS client certificate is specified\n");
+  if (!expand_check(tls_ocsp_file, US"tls_ocsp_file",
+       &state->exp_tls_ocsp_file))
+    return DEFER;
+
+  /* Use the full callback method for stapling just to get observability.
+  More efficient would be to read the file once only, if it never changed
+  (due to SNI). Would need restart on file update, or watch datestamp.  */
+
+  gnutls_certificate_set_ocsp_status_request_function(state->x509_cred,
+    server_ocsp_stapling_cb, state->exp_tls_ocsp_file);
+
+  DEBUG(D_tls) debug_printf("Set OCSP response file %s\n", &state->exp_tls_ocsp_file);
   }
+#endif
+
 
 /* Set the trusted CAs file if one is provided, and then add the CRL if one is
 provided. Experiment shows that, if the certificate file is empty, an unhelpful
@@ -514,336 +818,655 @@ error message is provided. However, if we just refrain from setting anything up
 in that case, certificate verification fails, which seems to be the correct
 behaviour. */
 
-if (cas != NULL)
+if (state->tls_verify_certificates && *state->tls_verify_certificates)
   {
-  struct stat statbuf;
-
-  if (!expand_check(cas, US"tls_verify_certificates", &cas_expanded))
+  if (!expand_check_tlsvar(tls_verify_certificates))
     return DEFER;
+  if (state->tls_crl && *state->tls_crl)
+    if (!expand_check_tlsvar(tls_crl))
+      return DEFER;
+
+  if (!(state->exp_tls_verify_certificates &&
+        *state->exp_tls_verify_certificates))
+    {
+    DEBUG(D_tls)
+      debug_printf("TLS: tls_verify_certificates expanded empty, ignoring\n");
+    /* With no tls_verify_certificates, we ignore tls_crl too */
+    return OK;
+    }
+  }
+else
+  {
+  DEBUG(D_tls)
+    debug_printf("TLS: tls_verify_certificates not set or empty, ignoring\n");
+  return OK;
+  }
+
+if (Ustat(state->exp_tls_verify_certificates, &statbuf) < 0)
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC, "could not stat %s "
+      "(tls_verify_certificates): %s", state->exp_tls_verify_certificates,
+      strerror(errno));
+  return DEFER;
+  }
+
+/* The test suite passes in /dev/null; we could check for that path explicitly,
+but who knows if someone has some weird FIFO which always dumps some certs, or
+other weirdness.  The thing we really want to check is that it's not a
+directory, since while OpenSSL supports that, GnuTLS does not.
+So s/!S_ISREG/S_ISDIR/ and change some messsaging ... */
+if (S_ISDIR(statbuf.st_mode))
+  {
+  DEBUG(D_tls)
+    debug_printf("verify certificates path is a dir: \"%s\"\n",
+        state->exp_tls_verify_certificates);
+  log_write(0, LOG_MAIN|LOG_PANIC,
+      "tls_verify_certificates \"%s\" is a directory",
+      state->exp_tls_verify_certificates);
+  return DEFER;
+  }
 
-  if (stat(CS cas_expanded, &statbuf) < 0)
+DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n",
+        state->exp_tls_verify_certificates, statbuf.st_size);
+
+if (statbuf.st_size == 0)
+  {
+  DEBUG(D_tls)
+    debug_printf("cert file empty, no certs, no verification, ignoring any CRL\n");
+  return OK;
+  }
+
+cert_count = gnutls_certificate_set_x509_trust_file(state->x509_cred,
+    CS state->exp_tls_verify_certificates, GNUTLS_X509_FMT_PEM);
+if (cert_count < 0)
+  {
+  rc = cert_count;
+  exim_gnutls_err_check(US"gnutls_certificate_set_x509_trust_file");
+  }
+DEBUG(D_tls) debug_printf("Added %d certificate authorities.\n", cert_count);
+
+if (state->tls_crl && *state->tls_crl &&
+    state->exp_tls_crl && *state->exp_tls_crl)
+  {
+  DEBUG(D_tls) debug_printf("loading CRL file = %s\n", state->exp_tls_crl);
+  cert_count = gnutls_certificate_set_x509_crl_file(state->x509_cred,
+      CS state->exp_tls_crl, GNUTLS_X509_FMT_PEM);
+  if (cert_count < 0)
     {
-    log_write(0, LOG_MAIN|LOG_PANIC, "could not stat %s "
-      "(tls_verify_certificates): %s", cas_expanded, strerror(errno));
+    rc = cert_count;
+    exim_gnutls_err_check(US"gnutls_certificate_set_x509_crl_file");
+    }
+  DEBUG(D_tls) debug_printf("Processed %d CRLs.\n", cert_count);
+  }
+
+return OK;
+}
+
+
+
+
+/*************************************************
+*          Set X.509 state variables             *
+*************************************************/
+
+/* In GnuTLS, the registered cert/key are not replaced by a later
+set of a cert/key, so for SNI support we need a whole new x509_cred
+structure.  Which means various other non-re-expanded pieces of state
+need to be re-set in the new struct, so the setting logic is pulled
+out to this.
+
+Arguments:
+  state           exim_gnutls_state_st *
+
+Returns:          OK/DEFER/FAIL
+*/
+
+static int
+tls_set_remaining_x509(exim_gnutls_state_st *state)
+{
+int rc;
+const host_item *host = state->host;  /* macro should be reconsidered? */
+
+/* Create D-H parameters, or read them from the cache file. This function does
+its own SMTP error messaging. This only happens for the server, TLS D-H ignores
+client-side params. */
+
+if (!state->host)
+  {
+  if (!dh_server_params)
+    {
+    rc = init_server_dh();
+    if (rc != OK) return rc;
+    }
+  gnutls_certificate_set_dh_params(state->x509_cred, dh_server_params);
+  }
+
+/* Link the credentials to the session. */
+
+rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->x509_cred);
+exim_gnutls_err_check(US"gnutls_credentials_set");
+
+return OK;
+}
+
+/*************************************************
+*            Initialize for GnuTLS               *
+*************************************************/
+
+/* Called from both server and client code. In the case of a server, errors
+before actual TLS negotiation return DEFER.
+
+Arguments:
+  host            connected host, if client; NULL if server
+  certificate     certificate file
+  privatekey      private key file
+  sni             TLS SNI to send, sometimes when client; else NULL
+  cas             CA certs file
+  crl             CRL file
+  require_ciphers tls_require_ciphers setting
+  caller_state    returned state-info structure
+
+Returns:          OK/DEFER/FAIL
+*/
+
+static int
+tls_init(
+    const host_item *host,
+    const uschar *certificate,
+    const uschar *privatekey,
+    const uschar *sni,
+    const uschar *cas,
+    const uschar *crl,
+    const uschar *require_ciphers,
+    exim_gnutls_state_st **caller_state)
+{
+exim_gnutls_state_st *state;
+int rc;
+size_t sz;
+const char *errpos;
+uschar *p;
+BOOL want_default_priorities;
+
+if (!exim_gnutls_base_init_done)
+  {
+  DEBUG(D_tls) debug_printf("GnuTLS global init required.\n");
+
+#ifdef HAVE_GNUTLS_PKCS11
+  /* By default, gnutls_global_init will init PKCS11 support in auto mode,
+  which loads modules from a config file, which sounds good and may be wanted
+  by some sysadmin, but also means in common configurations that GNOME keyring
+  environment variables are used and so breaks for users calling mailq.
+  To prevent this, we init PKCS11 first, which is the documented approach. */
+  if (!gnutls_allow_auto_pkcs11)
+    {
+    rc = gnutls_pkcs11_init(GNUTLS_PKCS11_FLAG_MANUAL, NULL);
+    exim_gnutls_err_check(US"gnutls_pkcs11_init");
+    }
+#endif
+
+  rc = gnutls_global_init();
+  exim_gnutls_err_check(US"gnutls_global_init");
+
+#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0
+  DEBUG(D_tls)
+    {
+    gnutls_global_set_log_function(exim_gnutls_logger_cb);
+    /* arbitrarily chosen level; bump upto 9 for more */
+    gnutls_global_set_log_level(EXIM_GNUTLS_LIBRARY_LOG_LEVEL);
+    }
+#endif
+
+  exim_gnutls_base_init_done = TRUE;
+  }
+
+if (host)
+  {
+  state = &state_client;
+  memcpy(state, &exim_gnutls_state_init, sizeof(exim_gnutls_state_init));
+  state->tlsp = &tls_out;
+  DEBUG(D_tls) debug_printf("initialising GnuTLS client session\n");
+  rc = gnutls_init(&state->session, GNUTLS_CLIENT);
+  }
+else
+  {
+  state = &state_server;
+  memcpy(state, &exim_gnutls_state_init, sizeof(exim_gnutls_state_init));
+  state->tlsp = &tls_in;
+  DEBUG(D_tls) debug_printf("initialising GnuTLS server session\n");
+  rc = gnutls_init(&state->session, GNUTLS_SERVER);
+  }
+exim_gnutls_err_check(US"gnutls_init");
+
+state->host = host;
+
+state->tls_certificate = certificate;
+state->tls_privatekey = privatekey;
+state->tls_require_ciphers = require_ciphers;
+state->tls_sni = sni;
+state->tls_verify_certificates = cas;
+state->tls_crl = crl;
+
+/* This handles the variables that might get re-expanded after TLS SNI;
+that's tls_certificate, tls_privatekey, tls_verify_certificates, tls_crl */
+
+DEBUG(D_tls)
+  debug_printf("Expanding various TLS configuration options for session credentials.\n");
+rc = tls_expand_session_files(state);
+if (rc != OK) return rc;
+
+/* These are all other parts of the x509_cred handling, since SNI in GnuTLS
+requires a new structure afterwards. */
+
+rc = tls_set_remaining_x509(state);
+if (rc != OK) return rc;
+
+/* set SNI in client, only */
+if (host)
+  {
+  if (!expand_check(state->tlsp->sni, US"tls_out_sni", &state->exp_tls_sni))
     return DEFER;
+  if (state->exp_tls_sni && *state->exp_tls_sni)
+    {
+    DEBUG(D_tls)
+      debug_printf("Setting TLS client SNI to \"%s\"\n", state->exp_tls_sni);
+    sz = Ustrlen(state->exp_tls_sni);
+    rc = gnutls_server_name_set(state->session,
+        GNUTLS_NAME_DNS, state->exp_tls_sni, sz);
+    exim_gnutls_err_check(US"gnutls_server_name_set");
     }
+  }
+else if (state->tls_sni)
+  DEBUG(D_tls) debug_printf("*** PROBABLY A BUG *** " \
+      "have an SNI set for a client [%s]\n", state->tls_sni);
 
-  DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n",
-    cas_expanded, statbuf.st_size);
+/* This is the priority string support,
+http://www.gnutls.org/manual/html_node/Priority-Strings.html
+and replaces gnutls_require_kx, gnutls_require_mac & gnutls_require_protocols.
+This was backwards incompatible, but means Exim no longer needs to track
+all algorithms and provide string forms for them. */
 
-  /* If the cert file is empty, there's no point in loading the CRL file. */
+want_default_priorities = TRUE;
 
-  if (statbuf.st_size > 0)
+if (state->tls_require_ciphers && *state->tls_require_ciphers)
+  {
+  if (!expand_check_tlsvar(tls_require_ciphers))
+    return DEFER;
+  if (state->exp_tls_require_ciphers && *state->exp_tls_require_ciphers)
     {
-    rc = gnutls_certificate_set_x509_trust_file(x509_cred, CS cas_expanded,
-      GNUTLS_X509_FMT_PEM);
-    if (rc < 0) return tls_error(US"setup_certs", host, gnutls_strerror(rc));
+    DEBUG(D_tls) debug_printf("GnuTLS session cipher/priority \"%s\"\n",
+        state->exp_tls_require_ciphers);
 
-    if (crl != NULL && *crl != 0)
-      {
-      if (!expand_check(crl, US"tls_crl", &crl_expanded))
-        return DEFER;
-      DEBUG(D_tls) debug_printf("loading CRL file = %s\n", crl_expanded);
-      rc = gnutls_certificate_set_x509_crl_file(x509_cred, CS crl_expanded,
-        GNUTLS_X509_FMT_PEM);
-      if (rc < 0) return tls_error(US"CRL setup", host, gnutls_strerror(rc));
-      }
+    rc = gnutls_priority_init(&state->priority_cache,
+        CS state->exp_tls_require_ciphers, &errpos);
+    want_default_priorities = FALSE;
+    p = state->exp_tls_require_ciphers;
     }
   }
+if (want_default_priorities)
+  {
+  DEBUG(D_tls)
+    debug_printf("GnuTLS using default session cipher/priority \"%s\"\n",
+        exim_default_gnutls_priority);
+  rc = gnutls_priority_init(&state->priority_cache,
+      exim_default_gnutls_priority, &errpos);
+  p = US exim_default_gnutls_priority;
+  }
+
+exim_gnutls_err_check(string_sprintf(
+      "gnutls_priority_init(%s) failed at offset %ld, \"%.6s..\"",
+      p, errpos - CS p, errpos));
 
-/* Associate the parameters with the x509 credentials structure. */
+rc = gnutls_priority_set(state->session, state->priority_cache);
+exim_gnutls_err_check(US"gnutls_priority_set");
 
-gnutls_certificate_set_dh_params(x509_cred, dh_params);
+gnutls_db_set_cache_expiration(state->session, ssl_session_timeout);
+
+/* Reduce security in favour of increased compatibility, if the admin
+decides to make that trade-off. */
+if (gnutls_compat_mode)
+  {
+#if LIBGNUTLS_VERSION_NUMBER >= 0x020104
+  DEBUG(D_tls) debug_printf("lowering GnuTLS security, compatibility mode\n");
+  gnutls_session_enable_compatibility_mode(state->session);
+#else
+  DEBUG(D_tls) debug_printf("Unable to set gnutls_compat_mode - GnuTLS version too old\n");
+#endif
+  }
 
-DEBUG(D_tls) debug_printf("initialized certificate stuff\n");
+*caller_state = state;
 return OK;
 }
 
 
 
-
 /*************************************************
-*           Remove from a priority list          *
+*            Extract peer information            *
 *************************************************/
 
-/* Cautiously written so that it will remove duplicates if present.
+/* Called from both server and client code.
+Only this is allowed to set state->peerdn and state->have_set_peerdn
+and we use that to detect double-calls.
+
+NOTE: the state blocks last while the TLS connection is up, which is fine
+for logging in the server side, but for the client side, we log after teardown
+in src/deliver.c.  While the session is up, we can twist about states and
+repoint tls_* globals, but those variables used for logging or other variable
+expansion that happens _after_ delivery need to have a longer life-time.
+
+So for those, we get the data from POOL_PERM; the re-invoke guard keeps us from
+doing this more than once per generation of a state context.  We set them in
+the state context, and repoint tls_* to them.  After the state goes away, the
+tls_* copies of the pointers remain valid and client delivery logging is happy.
+
+tls_certificate_verified is a BOOL, so the tls_peerdn and tls_cipher issues
+don't apply.
 
 Arguments:
-  list         a zero-terminated list
-  remove_list  a zero-terminated list to be removed
+  state           exim_gnutls_state_st *
 
-Returns:       nothing
+Returns:          OK/DEFER/FAIL
 */
 
-static void
-remove_priority(int *list, int *remove_list)
+static int
+peer_status(exim_gnutls_state_st *state)
 {
-for (; *remove_list != 0; remove_list++)
+uschar cipherbuf[256];
+const gnutls_datum *cert_list;
+int old_pool, rc;
+unsigned int cert_list_size = 0;
+gnutls_protocol_t protocol;
+gnutls_cipher_algorithm_t cipher;
+gnutls_kx_algorithm_t kx;
+gnutls_mac_algorithm_t mac;
+gnutls_certificate_type_t ct;
+gnutls_x509_crt_t crt;
+uschar *p, *dn_buf;
+size_t sz;
+
+if (state->have_set_peerdn)
+  return OK;
+state->have_set_peerdn = TRUE;
+
+state->peerdn = NULL;
+
+/* tls_cipher */
+cipher = gnutls_cipher_get(state->session);
+protocol = gnutls_protocol_get_version(state->session);
+mac = gnutls_mac_get(state->session);
+kx = gnutls_kx_get(state->session);
+
+string_format(cipherbuf, sizeof(cipherbuf),
+    "%s:%s:%d",
+    gnutls_protocol_get_name(protocol),
+    gnutls_cipher_suite_get_name(kx, cipher, mac),
+    (int) gnutls_cipher_get_key_size(cipher) * 8);
+
+/* I don't see a way that spaces could occur, in the current GnuTLS
+code base, but it was a concern in the old code and perhaps older GnuTLS
+releases did return "TLS 1.0"; play it safe, just in case. */
+for (p = cipherbuf; *p != '\0'; ++p)
+  if (isspace(*p))
+    *p = '-';
+old_pool = store_pool;
+store_pool = POOL_PERM;
+state->ciphersuite = string_copy(cipherbuf);
+store_pool = old_pool;
+state->tlsp->cipher = state->ciphersuite;
+
+/* tls_peerdn */
+cert_list = gnutls_certificate_get_peers(state->session, &cert_list_size);
+
+if (cert_list == NULL || cert_list_size == 0)
   {
-  int *p = list;
-  while (*p != 0)
-    {
-    if (*p == *remove_list)
-      {
-      int *pp = p;
-      do { pp[0] = pp[1]; pp++; } while (*pp != 0);
-      }
-    else p++;
-    }
+  DEBUG(D_tls) debug_printf("TLS: no certificate from peer (%p & %d)\n",
+      cert_list, cert_list_size);
+  if (state->verify_requirement == VERIFY_REQUIRED)
+    return tls_error(US"certificate verification failed",
+        "no certificate received from peer", state->host);
+  return OK;
   }
-}
-
 
+ct = gnutls_certificate_type_get(state->session);
+if (ct != GNUTLS_CRT_X509)
+  {
+  const char *ctn = gnutls_certificate_type_get_name(ct);
+  DEBUG(D_tls)
+    debug_printf("TLS: peer cert not X.509 but instead \"%s\"\n", ctn);
+  if (state->verify_requirement == VERIFY_REQUIRED)
+    return tls_error(US"certificate verification not possible, unhandled type",
+        ctn, state->host);
+  return OK;
+  }
 
-/*************************************************
-*            Add to a priority list              *
-*************************************************/
-
-/* Cautiously written to check the list size
+#define exim_gnutls_peer_err(Label) do { \
+  if (rc != GNUTLS_E_SUCCESS) { \
+    DEBUG(D_tls) debug_printf("TLS: peer cert problem: %s: %s\n", (Label), gnutls_strerror(rc)); \
+    if (state->verify_requirement == VERIFY_REQUIRED) { return tls_error((Label), gnutls_strerror(rc), state->host); } \
+    return OK; } } while (0)
 
-Arguments:
-  list         a zero-terminated list
-  list_max     maximum offset in the list
-  add_list     a zero-terminated list to be added
+rc = import_cert(&cert_list[0], &crt);
+exim_gnutls_peer_err(US"cert 0");
 
-Returns:       TRUE if OK; FALSE if list overflows
-*/
+state->tlsp->peercert = state->peercert = crt;
 
-static BOOL
-add_priority(int *list, int list_max, int *add_list)
-{
-int next = 0;
-while (list[next] != 0) next++;
-while (*add_list != 0)
+sz = 0;
+rc = gnutls_x509_crt_get_dn(crt, NULL, &sz);
+if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
   {
-  if (next >= list_max) return FALSE;
-  list[next++] = *add_list++;
+  exim_gnutls_peer_err(US"getting size for cert DN failed");
+  return FAIL; /* should not happen */
   }
-list[next] = 0;
-return TRUE;
+dn_buf = store_get_perm(sz);
+rc = gnutls_x509_crt_get_dn(crt, CS dn_buf, &sz);
+exim_gnutls_peer_err(US"failed to extract certificate DN [gnutls_x509_crt_get_dn(cert 0)]");
+
+state->peerdn = dn_buf;
+
+return OK;
+#undef exim_gnutls_peer_err
 }
 
 
 
+
 /*************************************************
-*          Adjust a priority list                *
+*            Verify peer certificate             *
 *************************************************/
 
-/* This function is called to adjust the lists of cipher algorithms, MAC
-algorithms, key-exchange methods, and protocols.
+/* Called from both server and client code.
+*Should* be using a callback registered with
+gnutls_certificate_set_verify_function() to fail the handshake if we dislike
+the peer information, but that's too new for some OSes.
 
 Arguments:
-  plist       the appropriate priority list
-  psize       the length of the list
-  s           the configuation string
-  index       the index of recognized strings
-  isize       the length of the index
-
+  state           exim_gnutls_state_st *
+  error           where to put an error message
 
-  which       text for an error message
-
-Returns:      FALSE if the table overflows, else TRUE
+Returns:
+  FALSE     if the session should be rejected
+  TRUE      if the cert is okay or we just don't care
 */
 
 static BOOL
-set_priority(int *plist, int psize, uschar *s, pri_item *index, int isize,
-   uschar *which)
+verify_certificate(exim_gnutls_state_st *state, const char **error)
 {
-int sep = 0;
-BOOL first = TRUE;
-uschar *t;
+int rc;
+unsigned int verify;
+
+*error = NULL;
 
-while ((t = string_nextinlist(&s, &sep, big_buffer, big_buffer_size)) != NULL)
+if ((rc = peer_status(state)) != OK)
   {
-  int i;
-  BOOL exclude = t[0] == '!';
-  if (first && !exclude) plist[0] = 0;
-  first = FALSE;
-  for (i = 0; i < isize; i++)
+  verify = GNUTLS_CERT_INVALID;
+  *error = "certificate not supplied";
+  }
+else
+  rc = gnutls_certificate_verify_peers2(state->session, &verify);
+
+/* Handle the result of verification. INVALID seems to be set as well
+as REVOKED, but leave the test for both. */
+
+if (rc < 0 || verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED))
+  {
+  state->peer_cert_verified = FALSE;
+  if (!*error)
+    *error = verify & GNUTLS_CERT_REVOKED
+      ? "certificate revoked" : "certificate invalid";
+
+  DEBUG(D_tls)
+    debug_printf("TLS certificate verification failed (%s): peerdn=%s\n",
+        *error, state->peerdn ? state->peerdn : US"<unset>");
+
+  if (state->verify_requirement == VERIFY_REQUIRED)
     {
-    uschar *ss = strstric(t, index[i].name, FALSE);
-    if (ss != NULL)
-      {
-      uschar *endss = ss + Ustrlen(index[i].name);
-      if ((ss == t || !isalnum(ss[-1])) && !isalnum(*endss))
-        {
-        if (exclude)
-          remove_priority(plist, index[i].values);
-        else
-          {
-          if (!add_priority(plist, psize, index[i].values))
-            {
-            log_write(0, LOG_MAIN|LOG_PANIC, "GnuTLS init failed: %s "
-              "priority table overflow", which);
-            return FALSE;
-            }
-          }
-        }
-      }
+    gnutls_alert_send(state->session, GNUTLS_AL_FATAL, GNUTLS_A_BAD_CERTIFICATE);
+    return FALSE;
     }
+  DEBUG(D_tls)
+    debug_printf("TLS verify failure overridden (host in tls_try_verify_hosts)\n");
   }
-
-DEBUG(D_tls)
+else
   {
-  int *ptr = plist;
-  debug_printf("adjusted %s priorities:", which);
-  while (*ptr != 0) debug_printf(" %d", *ptr++);
-  debug_printf("\n");
+  state->peer_cert_verified = TRUE;
+  DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n",
+      state->peerdn ? state->peerdn : US"<unset>");
   }
 
+state->tlsp->peerdn = state->peerdn;
+
 return TRUE;
 }
 
 
 
 
-/*************************************************
-*        Initialize a single GNUTLS session      *
-*************************************************/
+/* ------------------------------------------------------------------------ */
+/* Callbacks */
+
+/* Logging function which can be registered with
+ *   gnutls_global_set_log_function()
+ *   gnutls_global_set_log_level() 0..9
+ */
+#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0
+static void
+exim_gnutls_logger_cb(int level, const char *message)
+{
+  size_t len = strlen(message);
+  if (len < 1)
+    {
+    DEBUG(D_tls) debug_printf("GnuTLS<%d> empty debug message\n", level);
+    return;
+    }
+  DEBUG(D_tls) debug_printf("GnuTLS<%d>: %s%s", level, message,
+      message[len-1] == '\n' ? "" : "\n");
+}
+#endif
 
-/* Set the algorithm, the db backend, whether to request certificates etc.
 
-TLS in Exim was first implemented using OpenSSL. This has a function to which
-you pass a list of cipher suites that are permitted/not permitted. GnuTLS works
-differently. It operates using priority lists for the different components of
-cipher suites.
+/* Called after client hello, should handle SNI work.
+This will always set tls_sni (state->received_sni) if available,
+and may trigger presenting different certificates,
+if state->trigger_sni_changes is TRUE.
 
-For compatibility of configuration, we scan a list of cipher suites and set
-priorities therefrom. However, at the moment, we pay attention only to the bulk
-cipher.
+Should be registered with
+  gnutls_handshake_set_post_client_hello_function()
 
-Arguments:
-  side         one of GNUTLS_SERVER, GNUTLS_CLIENT
-  expciphers   expanded ciphers list or NULL
-  expmac       expanded MAC list or NULL
-  expkx        expanded key-exchange list or NULL
-  expproto     expanded protocol list or NULL
+"This callback must return 0 on success or a gnutls error code to terminate the
+handshake.".
 
-Returns:  a gnutls_session, or NULL if there is a problem
+For inability to get SNI information, we return 0.
+We only return non-zero if re-setup failed.
+Only used for server-side TLS.
 */
 
-static gnutls_session
-tls_session_init(int side, uschar *expciphers, uschar *expmac, uschar *expkx,
-  uschar *expproto)
+static int
+exim_sni_handling_cb(gnutls_session_t session)
 {
-gnutls_session session;
+char sni_name[MAX_HOST_LEN];
+size_t data_len = MAX_HOST_LEN;
+exim_gnutls_state_st *state = &state_server;
+unsigned int sni_type;
+int rc, old_pool;
+
+rc = gnutls_server_name_get(session, sni_name, &data_len, &sni_type, 0);
+if (rc != GNUTLS_E_SUCCESS)
+  {
+  DEBUG(D_tls) {
+    if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE)
+      debug_printf("TLS: no SNI presented in handshake.\n");
+    else
+      debug_printf("TLS failure: gnutls_server_name_get(): %s [%d]\n",
+        gnutls_strerror(rc), rc);
+  };
+  return 0;
+  }
 
-gnutls_init(&session, side);
+if (sni_type != GNUTLS_NAME_DNS)
+  {
+  DEBUG(D_tls) debug_printf("TLS: ignoring SNI of unhandled type %u\n", sni_type);
+  return 0;
+  }
 
-/* Initialize the lists of permitted protocols, key-exchange methods, ciphers,
-and MACs. */
+/* We now have a UTF-8 string in sni_name */
+old_pool = store_pool;
+store_pool = POOL_PERM;
+state->received_sni = string_copyn(US sni_name, data_len);
+store_pool = old_pool;
 
-memcpy(cipher_priority, default_cipher_priority, sizeof(cipher_priority));
-memcpy(mac_priority, default_mac_priority, sizeof(mac_priority));
-memcpy(kx_priority, default_kx_priority, sizeof(kx_priority));
-memcpy(proto_priority, default_proto_priority, sizeof(proto_priority));
+/* We set this one now so that variable expansions below will work */
+state->tlsp->sni = state->received_sni;
 
-/* The names OpenSSL uses in tls_require_ciphers are of the form DES-CBC3-SHA,
-using hyphen separators. GnuTLS uses underscore separators. So that I can use
-either form for tls_require_ciphers in my tests, and also for general
-convenience, we turn hyphens into underscores before scanning the list. */
+DEBUG(D_tls) debug_printf("Received TLS SNI \"%s\"%s\n", sni_name,
+    state->trigger_sni_changes ? "" : " (unused for certificate selection)");
 
-if (expciphers != NULL)
-  {
-  uschar *s = expciphers;
-  while (*s != 0) { if (*s == '-') *s = '_'; s++; }
-  }
+if (!state->trigger_sni_changes)
+  return 0;
 
-if ((expciphers != NULL &&
-      !set_priority(cipher_priority, sizeof(cipher_priority)/sizeof(int),
-        expciphers, cipher_index, sizeof(cipher_index)/sizeof(pri_item),
-        US"cipher")) ||
-    (expmac != NULL &&
-      !set_priority(mac_priority, sizeof(mac_priority)/sizeof(int),
-        expmac, mac_index, sizeof(mac_index)/sizeof(pri_item),
-        US"MAC")) ||
-    (expkx != NULL &&
-      !set_priority(kx_priority, sizeof(kx_priority)/sizeof(int),
-        expkx, kx_index, sizeof(kx_index)/sizeof(pri_item),
-        US"key-exchange")) ||
-    (expproto != NULL &&
-      !set_priority(proto_priority, sizeof(proto_priority)/sizeof(int),
-        expproto, proto_index, sizeof(proto_index)/sizeof(pri_item),
-        US"protocol")))
+rc = tls_expand_session_files(state);
+if (rc != OK)
   {
-  gnutls_deinit(session);
-  return NULL;
+  /* If the setup of certs/etc failed before handshake, TLS would not have
+  been offered.  The best we can do now is abort. */
+  return GNUTLS_E_APPLICATION_ERROR_MIN;
   }
 
-/* Define the various priorities */
+rc = tls_set_remaining_x509(state);
+if (rc != OK) return GNUTLS_E_APPLICATION_ERROR_MIN;
 
-gnutls_cipher_set_priority(session, cipher_priority);
-gnutls_compression_set_priority(session, comp_priority);
-gnutls_kx_set_priority(session, kx_priority);
-gnutls_protocol_set_priority(session, proto_priority);
-gnutls_mac_set_priority(session, mac_priority);
-
-gnutls_cred_set(session, GNUTLS_CRD_CERTIFICATE, x509_cred);
+return 0;
+}
 
-gnutls_dh_set_prime_bits(session, DH_BITS);
 
-/* Request or demand a certificate of the peer, as configured. This will
-happen only in a server. */
 
-if (verify_requirement != VERIFY_NONE)
-  gnutls_certificate_server_set_request(session,
-    (verify_requirement == VERIFY_OPTIONAL)?
-      GNUTLS_CERT_REQUEST : GNUTLS_CERT_REQUIRE);
+#ifdef EXPERIMENTAL_OCSP
 
-gnutls_db_set_cache_expiration(session, ssl_session_timeout);
+static int
+server_ocsp_stapling_cb(gnutls_session_t session, void * ptr,
+  gnutls_datum_t * ocsp_response)
+{
+int ret;
 
-/* Reduce security in favour of increased compatibility, if the admin
-decides to make that trade-off. */
-if (gnutls_compat_mode)
+if ((ret = gnutls_load_file(ptr, ocsp_response)) < 0)
   {
-#if LIBGNUTLS_VERSION_NUMBER >= 0x020104
-  DEBUG(D_tls) debug_printf("lowering GnuTLS security, compatibility mode\n");
-  gnutls_session_enable_compatibility_mode(session);
-#else
-  DEBUG(D_tls) debug_printf("Unable to set gnutls_compat_mode - GnuTLS version too old\n");
-#endif
+  DEBUG(D_tls) debug_printf("Failed to load ocsp stapling file %s\n",
+                             (char *)ptr);
+  tls_in.ocsp = OCSP_NOT_RESP;
+  return GNUTLS_E_NO_CERTIFICATE_STATUS;
   }
 
-DEBUG(D_tls) debug_printf("initialized GnuTLS session\n");
-return session;
+tls_in.ocsp = OCSP_VFY_NOT_TRIED;
+return 0;
 }
 
+#endif
 
 
-/*************************************************
-*           Get name of cipher in use            *
-*************************************************/
-
-/* The answer is left in a static buffer, and tls_cipher is set to point
-to it.
-
-Argument:   pointer to a GnuTLS session
-Returns:    nothing
-*/
-
-static void
-construct_cipher_name(gnutls_session session)
-{
-static uschar cipherbuf[256];
-uschar *ver;
-int bits, c, kx, mac;
-
-ver = string_copy(
-  US gnutls_protocol_get_name(gnutls_protocol_get_version(session)));
-if (Ustrncmp(ver, "TLS ", 4) == 0) ver[3] = '-';   /* Don't want space */
 
-c = gnutls_cipher_get(session);
-bits = gnutls_cipher_get_key_size(c);
 
-mac = gnutls_mac_get(session);
-kx = gnutls_kx_get(session);
 
-string_format(cipherbuf, sizeof(cipherbuf), "%s:%s:%u", ver,
-  gnutls_cipher_suite_get_name(kx, c, mac), bits);
-tls_cipher = cipherbuf;
+/* ------------------------------------------------------------------------ */
+/* Exported functions */
 
-DEBUG(D_tls) debug_printf("cipher: %s\n", cipherbuf);
-}
 
 
 
@@ -857,9 +1480,6 @@ a TLS session.
 
 Arguments:
   require_ciphers  list of allowed ciphers or NULL
-  require_mac      list of allowed MACs or NULL
-  require_kx       list of allowed key_exchange methods or NULL
-  require_proto    list of allowed protocols or NULL
 
 Returns:           OK on success
                    DEFER for errors before the start of the negotiation
@@ -868,21 +1488,16 @@ Returns:           OK on success
 */
 
 int
-tls_server_start(uschar *require_ciphers, uschar *require_mac,
-  uschar *require_kx, uschar *require_proto)
+tls_server_start(const uschar *require_ciphers)
 {
 int rc;
 const char *error;
-uschar *expciphers = NULL;
-uschar *expmac = NULL;
-uschar *expkx = NULL;
-uschar *expproto = NULL;
+exim_gnutls_state_st *state = NULL;
 
 /* Check for previous activation */
-
-if (tls_active >= 0)
+if (tls_in.active >= 0)
   {
-  tls_error("STARTTLS received after TLS started", NULL, "");
+  tls_error(US"STARTTLS received after TLS started", "", NULL);
   smtp_printf("554 Already in TLS\r\n");
   return FAIL;
   }
@@ -890,36 +1505,40 @@ if (tls_active >= 0)
 /* Initialize the library. If it fails, it will already have logged the error
 and sent an SMTP response. */
 
-DEBUG(D_tls) debug_printf("initializing GnuTLS as a server\n");
+DEBUG(D_tls) debug_printf("initialising GnuTLS as a server\n");
 
-rc = tls_init(NULL, tls_certificate, tls_privatekey, tls_verify_certificates,
-  tls_crl);
+rc = tls_init(NULL, tls_certificate, tls_privatekey,
+    NULL, tls_verify_certificates, tls_crl,
+    require_ciphers, &state);
 if (rc != OK) return rc;
 
-if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers) ||
-    !expand_check(require_mac, US"gnutls_require_mac", &expmac) ||
-    !expand_check(require_kx, US"gnutls_require_kx", &expkx) ||
-    !expand_check(require_proto, US"gnutls_require_proto", &expproto))
-  return FAIL;
-
 /* If this is a host for which certificate verification is mandatory or
 optional, set up appropriately. */
 
-tls_certificate_verified = FALSE;
-verify_requirement = VERIFY_NONE;
-
 if (verify_check_host(&tls_verify_hosts) == OK)
-  verify_requirement = VERIFY_REQUIRED;
+  {
+  DEBUG(D_tls) debug_printf("TLS: a client certificate will be required.\n");
+  state->verify_requirement = VERIFY_REQUIRED;
+  gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUIRE);
+  }
 else if (verify_check_host(&tls_try_verify_hosts) == OK)
-  verify_requirement = VERIFY_OPTIONAL;
+  {
+  DEBUG(D_tls) debug_printf("TLS: a client certificate will be requested but not required.\n");
+  state->verify_requirement = VERIFY_OPTIONAL;
+  gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUEST);
+  }
+else
+  {
+  DEBUG(D_tls) debug_printf("TLS: a client certificate will not be requested.\n");
+  state->verify_requirement = VERIFY_NONE;
+  gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_IGNORE);
+  }
 
-/* Prepare for new connection */
+/* Register SNI handling; always, even if not in tls_certificate, so that the
+expansion variable $tls_sni is always available. */
 
-tls_session = tls_session_init(GNUTLS_SERVER, expciphers, expmac, expkx,
-  expproto);
-if (tls_session == NULL)
-  return tls_error(US"tls_session_init", NULL,
-    gnutls_strerror(GNUTLS_E_MEMORY_ERROR));
+gnutls_handshake_set_post_client_hello_function(state->session,
+    exim_sni_handling_cb);
 
 /* Set context and tell client to go ahead, except in the case of TLS startup
 on connection, where outputting anything now upsets the clients and tends to
@@ -927,7 +1546,7 @@ make them disconnect. We need to have an explicit fflush() here, to force out
 the response. Other smtp_printf() calls do not need it, because in non-TLS
 mode, the fflush() happens when smtp_getc() is called. */
 
-if (!tls_on_connect)
+if (!state->tlsp->on_connect)
   {
   smtp_printf("220 TLS go ahead\r\n");
   fflush(smtp_out);
@@ -936,19 +1555,25 @@ if (!tls_on_connect)
 /* Now negotiate the TLS session. We put our own timer on it, since it seems
 that the GnuTLS library doesn't. */
 
-gnutls_transport_set_ptr2(tls_session, (gnutls_transport_ptr)fileno(smtp_in),
-                                       (gnutls_transport_ptr)fileno(smtp_out));
+gnutls_transport_set_ptr2(state->session,
+    (gnutls_transport_ptr)(long) fileno(smtp_in),
+    (gnutls_transport_ptr)(long) fileno(smtp_out));
+state->fd_in = fileno(smtp_in);
+state->fd_out = fileno(smtp_out);
 
 sigalrm_seen = FALSE;
 if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout);
-rc = gnutls_handshake(tls_session);
+do
+  {
+  rc = gnutls_handshake(state->session);
+  } while ((rc == GNUTLS_E_AGAIN) ||
+      (rc == GNUTLS_E_INTERRUPTED && !sigalrm_seen));
 alarm(0);
 
-if (rc < 0)
+if (rc != GNUTLS_E_SUCCESS)
   {
-  tls_error(US"gnutls_handshake", NULL,
-    sigalrm_seen ? "timed out" : gnutls_strerror(rc));
-
+  tls_error(US"gnutls_handshake",
+      sigalrm_seen ? "timed out" : gnutls_strerror(rc), NULL);
   /* It seems that, except in the case of a timeout, we have to close the
   connection right here; otherwise if the other end is running OpenSSL it hangs
   until the server times out. */
@@ -964,21 +1589,34 @@ if (rc < 0)
 
 DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n");
 
-if (verify_requirement != VERIFY_NONE &&
-     !verify_certificate(tls_session, &error))
+/* Verify after the fact */
+
+if (  state->verify_requirement != VERIFY_NONE
+   && !verify_certificate(state, &error))
   {
-  tls_error(US"certificate verification failed", NULL, error);
-  return FAIL;
+  if (state->verify_requirement != VERIFY_OPTIONAL)
+    {
+    tls_error(US"certificate verification failed", error, NULL);
+    return FAIL;
+    }
+  DEBUG(D_tls)
+    debug_printf("TLS: continuing on only because verification was optional, after: %s\n",
+       error);
   }
 
-construct_cipher_name(tls_session);
+/* Figure out peer DN, and if authenticated, etc. */
+
+rc = peer_status(state);
+if (rc != OK) return rc;
+
+/* Sets various Exim expansion variables; always safe within server */
+
+extract_exim_vars_from_tls_state(state);
 
 /* TLS has been set up. Adjust the input functions to read via TLS,
 and initialize appropriately. */
 
-ssl_xfer_buffer = store_malloc(ssl_xfer_buffer_size);
-ssl_xfer_buffer_lwm = ssl_xfer_buffer_hwm = 0;
-ssl_xfer_eof = ssl_xfer_error = 0;
+state->xfer_buffer = store_malloc(ssl_xfer_buffer_size);
 
 receive_getc = tls_getc;
 receive_ungetc = tls_ungetc;
@@ -986,8 +1624,6 @@ receive_feof = tls_feof;
 receive_ferror = tls_ferror;
 receive_smtp_buffered = tls_smtp_buffered;
 
-tls_active = fileno(smtp_out);
-
 return OK;
 }
 
@@ -1004,135 +1640,218 @@ Arguments:
   fd                the fd of the connection
   host              connected host (for messages)
   addr              the first address (not used)
-  dhparam           DH parameter file
-  certificate       certificate file
-  privatekey        private key file
-  verify_certs      file for certificate verify
-  verify_crl        CRL for verify
-  require_ciphers   list of allowed ciphers or NULL
-  require_mac       list of allowed MACs or NULL
-  require_kx        list of allowed key_exchange methods or NULL
-  require_proto     list of allowed protocols or NULL
-  timeout           startup timeout
+  ob                smtp transport options
 
 Returns:            OK/DEFER/FAIL (because using common functions),
                     but for a client, DEFER and FAIL have the same meaning
 */
 
 int
-tls_client_start(int fd, host_item *host, address_item *addr, uschar *dhparam,
-  uschar *certificate, uschar *privatekey, uschar *verify_certs,
-  uschar *verify_crl, uschar *require_ciphers, uschar *require_mac,
-  uschar *require_kx, uschar *require_proto, int timeout)
+tls_client_start(int fd, host_item *host,
+    address_item *addr ARG_UNUSED,
+    void *v_ob)
 {
-const gnutls_datum *server_certs;
-uschar *expciphers = NULL;
-uschar *expmac = NULL;
-uschar *expkx = NULL;
-uschar *expproto = NULL;
-const char *error;
-unsigned int server_certs_size;
+smtp_transport_options_block *ob = v_ob;
 int rc;
+const char *error;
+exim_gnutls_state_st *state = NULL;
+#ifdef EXPERIMENTAL_OCSP
+BOOL require_ocsp = verify_check_this_host(&ob->hosts_require_ocsp,
+  NULL, host->name, host->address, NULL) == OK;
+BOOL request_ocsp = require_ocsp ? TRUE
+  : verify_check_this_host(&ob->hosts_request_ocsp,
+      NULL, host->name, host->address, NULL) == OK;
+#endif
 
-DEBUG(D_tls) debug_printf("initializing GnuTLS as a client\n");
+DEBUG(D_tls) debug_printf("initialising GnuTLS as a client on fd %d\n", fd);
 
-verify_requirement = (verify_certs == NULL)? VERIFY_NONE : VERIFY_REQUIRED;
-rc = tls_init(host, certificate, privatekey, verify_certs, verify_crl);
-if (rc != OK) return rc;
+if ((rc = tls_init(host, ob->tls_certificate, ob->tls_privatekey,
+    ob->tls_sni, ob->tls_verify_certificates, ob->tls_crl,
+    ob->tls_require_ciphers, &state)) != OK)
+  return rc;
 
-if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers) ||
-    !expand_check(require_mac, US"gnutls_require_mac", &expmac) ||
-    !expand_check(require_kx, US"gnutls_require_kx", &expkx) ||
-    !expand_check(require_proto, US"gnutls_require_proto", &expproto))
-  return FAIL;
+  {
+  int dh_min_bits = ob->tls_dh_min_bits;
+  if (dh_min_bits < EXIM_CLIENT_DH_MIN_MIN_BITS)
+    {
+    DEBUG(D_tls)
+      debug_printf("WARNING: tls_dh_min_bits far too low,"
+                   " clamping %d up to %d\n",
+         dh_min_bits, EXIM_CLIENT_DH_MIN_MIN_BITS);
+    dh_min_bits = EXIM_CLIENT_DH_MIN_MIN_BITS;
+    }
+
+  DEBUG(D_tls) debug_printf("Setting D-H prime minimum"
+                   " acceptable bits to %d\n",
+      dh_min_bits);
+  gnutls_dh_set_prime_bits(state->session, dh_min_bits);
+  }
 
-tls_session = tls_session_init(GNUTLS_CLIENT, expciphers, expmac, expkx,
-  expproto);
+/* Stick to the old behaviour for compatibility if tls_verify_certificates is 
+set but both tls_verify_hosts and tls_try_verify_hosts are unset. Check only
+the specified host patterns if one of them is defined */
+
+if ((  state->exp_tls_verify_certificates
+    && !ob->tls_verify_hosts
+    && !ob->tls_try_verify_hosts
+    )
+    ||
+    verify_check_host(&ob->tls_verify_hosts) == OK
+   )
+  {
+  DEBUG(D_tls) debug_printf("TLS: server certificate verification required.\n");
+  state->verify_requirement = VERIFY_REQUIRED;
+  gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUIRE);
+  }
+else if (verify_check_host(&ob->tls_try_verify_hosts) == OK)
+  {
+  DEBUG(D_tls) debug_printf("TLS: server certificate verification optional.\n");
+  state->verify_requirement = VERIFY_OPTIONAL;
+  gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUEST);
+  }
+else
+  {
+  DEBUG(D_tls) debug_printf("TLS: server certificate verification not required.\n");
+  state->verify_requirement = VERIFY_NONE;
+  gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_IGNORE);
+  }
 
-if (tls_session == NULL)
-  return tls_error(US "tls_session_init", host,
-    gnutls_strerror(GNUTLS_E_MEMORY_ERROR));
+#ifdef EXPERIMENTAL_OCSP       /* since GnuTLS 3.1.3 */
+if (request_ocsp)
+  {
+  DEBUG(D_tls) debug_printf("TLS: will request OCSP stapling\n");
+  if ((rc = gnutls_ocsp_status_request_enable_client(state->session,
+                   NULL, 0, NULL)) != OK)
+    return tls_error(US"cert-status-req",
+                   gnutls_strerror(rc), state->host);
+  tls_out.ocsp = OCSP_NOT_RESP;
+  }
+#endif
 
-gnutls_transport_set_ptr(tls_session, (gnutls_transport_ptr)fd);
+gnutls_transport_set_ptr(state->session, (gnutls_transport_ptr)(long) fd);
+state->fd_in = fd;
+state->fd_out = fd;
 
+DEBUG(D_tls) debug_printf("about to gnutls_handshake\n");
 /* There doesn't seem to be a built-in timeout on connection. */
 
 sigalrm_seen = FALSE;
-alarm(timeout);
-rc = gnutls_handshake(tls_session);
+alarm(ob->command_timeout);
+do
+  {
+  rc = gnutls_handshake(state->session);
+  } while ((rc == GNUTLS_E_AGAIN) ||
+      (rc == GNUTLS_E_INTERRUPTED && !sigalrm_seen));
 alarm(0);
 
-if (rc < 0)
-  return tls_error(US "gnutls_handshake", host,
-    sigalrm_seen ? "timed out" : gnutls_strerror(rc));
+if (rc != GNUTLS_E_SUCCESS)
+  return tls_error(US"gnutls_handshake",
+      sigalrm_seen ? "timed out" : gnutls_strerror(rc), state->host);
 
-server_certs = gnutls_certificate_get_peers(tls_session, &server_certs_size);
+DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n");
 
-if (server_certs != NULL)
-  {
-  uschar buff[1024];
-  gnutls_x509_crt gcert;
+/* Verify late */
 
-  gnutls_x509_crt_init(&gcert);
-  tls_peerdn = US"unknown";
+if (state->verify_requirement != VERIFY_NONE &&
+    !verify_certificate(state, &error))
+  return tls_error(US"certificate verification failed", error, state->host);
+
+#ifdef EXPERIMENTAL_OCSP
+if (require_ocsp)
+  {
+  DEBUG(D_tls)
+    {
+    gnutls_datum_t stapling;
+    gnutls_ocsp_resp_t resp;
+    gnutls_datum_t printed;
+    if (  (rc= gnutls_ocsp_status_request_get(state->session, &stapling)) == 0
+       && (rc= gnutls_ocsp_resp_init(&resp)) == 0
+       && (rc= gnutls_ocsp_resp_import(resp, &stapling)) == 0
+       && (rc= gnutls_ocsp_resp_print(resp, GNUTLS_OCSP_PRINT_FULL, &printed)) == 0
+       )
+      {
+      debug_printf("%.4096s", printed.data);
+      gnutls_free(printed.data);
+      }
+    else
+      (void) tls_error(US"ocsp decode", gnutls_strerror(rc), state->host);
+    }
 
-  if (gnutls_x509_crt_import(gcert, server_certs, GNUTLS_X509_FMT_DER) == 0)
+  if (gnutls_ocsp_status_request_is_checked(state->session, 0) == 0)
     {
-    size_t bufsize = sizeof(buff);
-    if (gnutls_x509_crt_get_dn(gcert, CS buff, &bufsize) >= 0)
-      tls_peerdn = string_copy_malloc(buff);
+    tls_out.ocsp = OCSP_FAILED;
+    return tls_error(US"certificate status check failed", NULL, state->host);
     }
+  DEBUG(D_tls) debug_printf("Passed OCSP checking\n");
+  tls_out.ocsp = OCSP_VFIED;
   }
+#endif
+
+/* Figure out peer DN, and if authenticated, etc. */
+
+if ((rc = peer_status(state)) != OK)
+  return rc;
 
-/* Should we also verify the hostname here? */
+/* Sets various Exim expansion variables; may need to adjust for ACL callouts */
 
-if (verify_requirement != VERIFY_NONE &&
-      !verify_certificate(tls_session, &error))
-  return tls_error(US"certificate verification failed", host, error);
+extract_exim_vars_from_tls_state(state);
 
-construct_cipher_name(tls_session);    /* Sets tls_cipher */
-tls_active = fd;
 return OK;
 }
 
 
 
+
 /*************************************************
-*    Deal with logging errors during I/O         *
+*         Close down a TLS session               *
 *************************************************/
 
-/* We have to get the identity of the peer from saved data.
-
-Argument:
-  ec       the GnuTLS error code, or 0 if it's a local error
-  when     text identifying read or write
-  text     local error text when ec is 0
+/* This is also called from within a delivery subprocess forked from the
+daemon, to shut down the TLS library, without actually doing a shutdown (which
+would tamper with the TLS session in the parent process).
 
-Returns:   nothing
+Arguments:   TRUE if gnutls_bye is to be called
+Returns:     nothing
 */
 
-static void
-record_io_error(int ec, uschar *when, uschar *text)
+void
+tls_close(BOOL is_server, BOOL shutdown)
 {
-const char *msg;
+exim_gnutls_state_st *state = is_server ? &state_server : &state_client;
 
-if (ec == GNUTLS_E_FATAL_ALERT_RECEIVED)
-  msg = string_sprintf("%s: %s", gnutls_strerror(ec),
-    gnutls_alert_get_name(gnutls_alert_get(tls_session)));
-else
-  msg = gnutls_strerror(ec);
+if (!state->tlsp || state->tlsp->active < 0) return;  /* TLS was not active */
+
+if (shutdown)
+  {
+  DEBUG(D_tls) debug_printf("tls_close(): shutting down TLS\n");
+  gnutls_bye(state->session, GNUTLS_SHUT_WR);
+  }
+
+gnutls_deinit(state->session);
+
+state->tlsp->active = -1;
+memcpy(state, &exim_gnutls_state_init, sizeof(exim_gnutls_state_init));
+
+if ((state_server.session == NULL) && (state_client.session == NULL))
+  {
+  gnutls_global_deinit();
+  exim_gnutls_base_init_done = FALSE;
+  }
 
-tls_error(when, client_host, msg);
 }
 
 
 
+
 /*************************************************
 *            TLS version of getc                 *
 *************************************************/
 
 /* This gets the next byte from the TLS input buffer. If the buffer is empty,
 it refills the buffer via the GnuTLS reading function.
+Only used by the server-side TLS.
+
+This feeds DKIM and should be used for all message-body reads.
 
 Arguments:  none
 Returns:    the next character or EOF
@@ -1141,15 +1860,16 @@ Returns:    the next character or EOF
 int
 tls_getc(void)
 {
-if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
+exim_gnutls_state_st *state = &state_server;
+if (state->xfer_buffer_lwm >= state->xfer_buffer_hwm)
   {
-  int inbytes;
+  ssize_t inbytes;
 
-  DEBUG(D_tls) debug_printf("Calling gnutls_record_recv(%lx, %lx, %u)\n",
-    (long) tls_session, (long) ssl_xfer_buffer, ssl_xfer_buffer_size);
+  DEBUG(D_tls) debug_printf("Calling gnutls_record_recv(%p, %p, %u)\n",
+    state->session, state->xfer_buffer, ssl_xfer_buffer_size);
 
   if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout);
-  inbytes = gnutls_record_recv(tls_session, CS ssl_xfer_buffer,
+  inbytes = gnutls_record_recv(state->session, state->xfer_buffer,
     ssl_xfer_buffer_size);
   alarm(0);
 
@@ -1167,11 +1887,15 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
     receive_ferror = smtp_ferror;
     receive_smtp_buffered = smtp_buffered;
 
-    gnutls_deinit(tls_session);
-    tls_session = NULL;
-    tls_active = -1;
-    tls_cipher = NULL;
-    tls_peerdn = NULL;
+    gnutls_deinit(state->session);
+    state->session = NULL;
+    state->tlsp->active = -1;
+    state->tlsp->bits = 0;
+    state->tlsp->certificate_verified = FALSE;
+    tls_channelbinding_b64 = NULL;
+    state->tlsp->cipher = NULL;
+    state->tlsp->peercert = NULL;
+    state->tlsp->peerdn = NULL;
 
     return smtp_getc();
     }
@@ -1180,30 +1904,32 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
 
   else if (inbytes < 0)
     {
-    record_io_error(inbytes, US"recv", NULL);
-    ssl_xfer_error = 1;
+    record_io_error(state, (int) inbytes, US"recv", NULL);
+    state->xfer_error = 1;
     return EOF;
     }
 #ifndef DISABLE_DKIM
-  dkim_exim_verify_feed(ssl_xfer_buffer, inbytes);
+  dkim_exim_verify_feed(state->xfer_buffer, inbytes);
 #endif
-  ssl_xfer_buffer_hwm = inbytes;
-  ssl_xfer_buffer_lwm = 0;
+  state->xfer_buffer_hwm = (int) inbytes;
+  state->xfer_buffer_lwm = 0;
   }
 
-
 /* Something in the buffer; return next uschar */
 
-return ssl_xfer_buffer[ssl_xfer_buffer_lwm++];
+return state->xfer_buffer[state->xfer_buffer_lwm++];
 }
 
 
 
+
 /*************************************************
 *          Read bytes from TLS channel           *
 *************************************************/
 
-/*
+/* This does not feed DKIM, so if the caller uses this for reading message body,
+then the caller must feed DKIM.
+
 Arguments:
   buff      buffer of data
   len       size of buffer
@@ -1213,32 +1939,45 @@ Returns:    the number of bytes read
 */
 
 int
-tls_read(uschar *buff, size_t len)
+tls_read(BOOL is_server, uschar *buff, size_t len)
 {
-int inbytes;
+exim_gnutls_state_st *state = is_server ? &state_server : &state_client;
+ssize_t inbytes;
+
+if (len > INT_MAX)
+  len = INT_MAX;
+
+if (state->xfer_buffer_lwm < state->xfer_buffer_hwm)
+  DEBUG(D_tls)
+    debug_printf("*** PROBABLY A BUG *** " \
+        "tls_read() called with data in the tls_getc() buffer, %d ignored\n",
+        state->xfer_buffer_hwm - state->xfer_buffer_lwm);
 
-DEBUG(D_tls) debug_printf("Calling gnutls_record_recv(%lx, %lx, %u)\n",
-  (long) tls_session, (long) buff, len);
+DEBUG(D_tls)
+  debug_printf("Calling gnutls_record_recv(%p, %p, " SIZE_T_FMT ")\n",
+      state->session, buff, len);
 
-inbytes = gnutls_record_recv(tls_session, CS buff, len);
+inbytes = gnutls_record_recv(state->session, buff, len);
 if (inbytes > 0) return inbytes;
 if (inbytes == 0)
   {
   DEBUG(D_tls) debug_printf("Got TLS_EOF\n");
   }
-else record_io_error(inbytes, US"recv", NULL);
+else record_io_error(state, (int)inbytes, US"recv", NULL);
 
 return -1;
 }
 
 
 
+
 /*************************************************
 *         Write bytes down TLS channel           *
 *************************************************/
 
 /*
 Arguments:
+  is_server channel specifier
   buff      buffer of data
   len       number of bytes
 
@@ -1247,27 +1986,28 @@ Returns:    the number of bytes after a successful write,
 */
 
 int
-tls_write(const uschar *buff, size_t len)
+tls_write(BOOL is_server, const uschar *buff, size_t len)
 {
-int outbytes;
-int left = len;
+ssize_t outbytes;
+size_t left = len;
+exim_gnutls_state_st *state = is_server ? &state_server : &state_client;
 
-DEBUG(D_tls) debug_printf("tls_do_write(%lx, %d)\n", (long) buff, left);
+DEBUG(D_tls) debug_printf("tls_do_write(%p, " SIZE_T_FMT ")\n", buff, left);
 while (left > 0)
   {
-  DEBUG(D_tls) debug_printf("gnutls_record_send(SSL, %lx, %d)\n", (long)buff,
-    left);
-  outbytes = gnutls_record_send(tls_session, CS buff, left);
+  DEBUG(D_tls) debug_printf("gnutls_record_send(SSL, %p, " SIZE_T_FMT ")\n",
+      buff, left);
+  outbytes = gnutls_record_send(state->session, buff, left);
 
-  DEBUG(D_tls) debug_printf("outbytes=%d\n", outbytes);
+  DEBUG(D_tls) debug_printf("outbytes=" SSIZE_T_FMT "\n", outbytes);
   if (outbytes < 0)
     {
-    record_io_error(outbytes, US"send", NULL);
+    record_io_error(state, outbytes, US"send", NULL);
     return -1;
     }
   if (outbytes == 0)
     {
-    record_io_error(0, US"send", US"TLS channel closed on write");
+    record_io_error(state, 0, US"send", US"TLS channel closed on write");
     return -1;
     }
 
@@ -1275,39 +2015,144 @@ while (left > 0)
   buff += outbytes;
   }
 
-return len;
+if (len > INT_MAX)
+  {
+  DEBUG(D_tls)
+    debug_printf("Whoops!  Wrote more bytes (" SIZE_T_FMT ") than INT_MAX\n",
+        len);
+  len = INT_MAX;
+  }
+
+return (int) len;
 }
 
 
 
+
 /*************************************************
-*         Close down a TLS session               *
+*            Random number generation            *
 *************************************************/
 
-/* This is also called from within a delivery subprocess forked from the
-daemon, to shut down the TLS library, without actually doing a shutdown (which
-would tamper with the TLS session in the parent process).
+/* Pseudo-random number generation.  The result is not expected to be
+cryptographically strong but not so weak that someone will shoot themselves
+in the foot using it as a nonce in input in some email header scheme or
+whatever weirdness they'll twist this into.  The result should handle fork()
+and avoid repeating sequences.  OpenSSL handles that for us.
 
-Arguments:   TRUE if gnutls_bye is to be called
-Returns:     nothing
+Arguments:
+  max       range maximum
+Returns     a random number in range [0, max-1]
 */
 
-void
-tls_close(BOOL shutdown)
+#ifdef HAVE_GNUTLS_RND
+int
+vaguely_random_number(int max)
+{
+unsigned int r;
+int i, needed_len;
+uschar *p;
+uschar smallbuf[sizeof(r)];
+
+if (max <= 1)
+  return 0;
+
+needed_len = sizeof(r);
+/* Don't take 8 times more entropy than needed if int is 8 octets and we were
+ * asked for a number less than 10. */
+for (r = max, i = 0; r; ++i)
+  r >>= 1;
+i = (i + 7) / 8;
+if (i < needed_len)
+  needed_len = i;
+
+i = gnutls_rnd(GNUTLS_RND_NONCE, smallbuf, needed_len);
+if (i < 0)
+  {
+  DEBUG(D_all) debug_printf("gnutls_rnd() failed, using fallback.\n");
+  return vaguely_random_number_fallback(max);
+  }
+r = 0;
+for (p = smallbuf; needed_len; --needed_len, ++p)
+  {
+  r *= 256;
+  r += *p;
+  }
+
+/* We don't particularly care about weighted results; if someone wants
+ * smooth distribution and cares enough then they should submit a patch then. */
+return r % max;
+}
+#else /* HAVE_GNUTLS_RND */
+int
+vaguely_random_number(int max)
 {
-if (tls_active < 0) return;  /* TLS was not active */
+  return vaguely_random_number_fallback(max);
+}
+#endif /* HAVE_GNUTLS_RND */
 
-if (shutdown)
+
+
+
+/*************************************************
+*  Let tls_require_ciphers be checked at startup *
+*************************************************/
+
+/* The tls_require_ciphers option, if set, must be something which the
+library can parse.
+
+Returns:     NULL on success, or error message
+*/
+
+uschar *
+tls_validate_require_cipher(void)
+{
+int rc;
+uschar *expciphers = NULL;
+gnutls_priority_t priority_cache;
+const char *errpos;
+
+#define validate_check_rc(Label) do { \
+  if (rc != GNUTLS_E_SUCCESS) { if (exim_gnutls_base_init_done) gnutls_global_deinit(); \
+  return string_sprintf("%s failed: %s", (Label), gnutls_strerror(rc)); } } while (0)
+#define return_deinit(Label) do { gnutls_global_deinit(); return (Label); } while (0)
+
+if (exim_gnutls_base_init_done)
+  log_write(0, LOG_MAIN|LOG_PANIC,
+      "already initialised GnuTLS, Exim developer bug");
+
+#ifdef HAVE_GNUTLS_PKCS11
+if (!gnutls_allow_auto_pkcs11)
   {
-  DEBUG(D_tls) debug_printf("tls_close(): shutting down TLS\n");
-  gnutls_bye(tls_session, GNUTLS_SHUT_WR);
+  rc = gnutls_pkcs11_init(GNUTLS_PKCS11_FLAG_MANUAL, NULL);
+  validate_check_rc(US"gnutls_pkcs11_init");
   }
+#endif
+rc = gnutls_global_init();
+validate_check_rc(US"gnutls_global_init()");
+exim_gnutls_base_init_done = TRUE;
+
+if (!(tls_require_ciphers && *tls_require_ciphers))
+  return_deinit(NULL);
+
+if (!expand_check(tls_require_ciphers, US"tls_require_ciphers", &expciphers))
+  return_deinit(US"failed to expand tls_require_ciphers");
+
+if (!(expciphers && *expciphers))
+  return_deinit(NULL);
 
-gnutls_deinit(tls_session);
-tls_session = NULL;
+DEBUG(D_tls)
+  debug_printf("tls_require_ciphers expands to \"%s\"\n", expciphers);
+
+rc = gnutls_priority_init(&priority_cache, CS expciphers, &errpos);
+validate_check_rc(string_sprintf(
+      "gnutls_priority_init(%s) failed at offset %ld, \"%.8s..\"",
+      expciphers, errpos - CS expciphers, errpos));
+
+#undef return_deinit
+#undef validate_check_rc
 gnutls_global_deinit();
 
-tls_active = -1;
+return NULL;
 }
 
 
@@ -1326,8 +2171,12 @@ Returns:     nothing
 void
 tls_version_report(FILE *f)
 {
-fprintf(f, "GnuTLS compile-time version: %s\n", LIBGNUTLS_VERSION);
-fprintf(f, "GnuTLS runtime version: %s\n", gnutls_check_version(NULL));
+fprintf(f, "Library version: GnuTLS: Compile: %s\n"
+           "                         Runtime: %s\n",
+           LIBGNUTLS_VERSION,
+           gnutls_check_version(NULL));
 }
 
+/* vi: aw ai sw=2
+*/
 /* End of tls-gnu.c */