Pull Andreas Metzler's fix for gnutls_certificate_verify_peers (bug 1095)
[exim.git] / src / src / tls-gnu.c
index 4a8355003b04d774b986a15b6f381570103b83d2..6b80637e910973bff232222ac3c0400e97027f53 100644 (file)
@@ -1,10 +1,8 @@
-/* $Cambridge: exim/src/src/tls-gnu.c,v 1.4 2005/01/04 10:00:42 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* This module provides TLS (aka SSL) support for Exim using the GnuTLS
@@ -14,6 +12,13 @@ is based on a patch that was contributed by Nikos Mavroyanopoulos.
 No cryptographic code is included in Exim. All this module does is to call
 functions from the GnuTLS library. */
 
+/* Note: This appears to be using an old API from compat.h; it is likely that
+someone familiary with GnuTLS programming could rework a lot of this to a
+modern API and perhaps remove the explicit knowledge of crypto algorithms from
+Exim.  Such a re-work would be most welcome and we'd sacrifice support for
+older GnuTLS releases without too many qualms -- maturity and experience
+in crypto libraries tends to improve their robustness against attack.
+Frankly, if you maintain it, you decide what's supported and what isn't. */
 
 /* Heading stuff for GnuTLS */
 
@@ -22,20 +27,18 @@ functions from the GnuTLS library. */
 
 
 #define UNKNOWN_NAME "unknown"
-#define DH_BITS      768
-#define RSA_BITS     512
+#define DH_BITS      1024
+#define PARAM_SIZE 2*1024
+
 
-/* Values for verify_requirment and initialized */
+/* Values for verify_requirment */
 
 enum { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED };
-enum { INITIALIZED_NOT, INITIALIZED_SERVER, INITIALIZED_CLIENT };
 
 /* Local static variables for GNUTLS */
 
-static BOOL initialized = INITIALIZED_NOT;
 static host_item *client_host;
 
-static gnutls_rsa_params rsa_params = NULL;
 static gnutls_dh_params dh_params = NULL;
 
 static gnutls_certificate_server_credentials x509_cred = NULL;
@@ -46,18 +49,31 @@ static char ssl_errstring[256];
 static int  ssl_session_timeout = 200;
 static int  verify_requirement;
 
-/* Priorities for TLS algorithms to use. At present, only the cipher priority
-vector can be altered. */
+/* Priorities for TLS algorithms to use. In each case there's a default table,
+and space into which it can be copied and altered. */
+
+static const int default_proto_priority[16] = {
+  /* These are gnutls_protocol_t enum values */
+#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 7
+  GNUTLS_TLS1_2,
+#endif
+#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 2
+  GNUTLS_TLS1_1,
+#endif
+  GNUTLS_TLS1,
+  GNUTLS_SSL3,
+  0 };
 
-static const int protocol_priority[16] = { GNUTLS_TLS1, GNUTLS_SSL3, 0 };
+static int proto_priority[16];
 
-static const int kx_priority[16] = {
+static const int default_kx_priority[16] = {
   GNUTLS_KX_RSA,
   GNUTLS_KX_DHE_DSS,
   GNUTLS_KX_DHE_RSA,
-  GNUTLS_KX_RSA_EXPORT,
   0 };
 
+static int kx_priority[16];
+
 static int default_cipher_priority[16] = {
   GNUTLS_CIPHER_AES_256_CBC,
   GNUTLS_CIPHER_AES_128_CBC,
@@ -67,21 +83,66 @@ static int default_cipher_priority[16] = {
 
 static int cipher_priority[16];
 
-static const int mac_priority[16] = {
+static const int default_mac_priority[16] = {
   GNUTLS_MAC_SHA,
   GNUTLS_MAC_MD5,
   0 };
 
+static int mac_priority[16];
+
+/* 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 };
 
-/* Tables of cipher names and equivalent numbers */
+/* Tables of priority names and equivalent numbers */
 
 typedef struct pri_item {
   uschar *name;
   int *values;
 } pri_item;
 
+
+#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 7
+static int tls1_2_codes[] = { GNUTLS_TLS1_2, 0 };
+#endif
+#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 2
+static int tls1_1_codes[] = { GNUTLS_TLS1_1, 0 };
+#endif
+/* more recent libraries define this as an equivalent value to the
+canonical GNUTLS_TLS1_0; since they're the same, we stick to the
+older name. */
+static int tls1_0_codes[] = { GNUTLS_TLS1, 0 };
+static int ssl3_codes[] = { GNUTLS_SSL3, 0 };
+
+static pri_item proto_index[] = {
+#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 7
+  { US"TLS1.2", tls1_2_codes },
+#endif
+#if GNUTLS_VERSION_MAJOR > 1 || GNUTLS_VERSION_MINOR >= 2
+  { US"TLS1.1", tls1_1_codes },
+#endif
+  { US"TLS1.0", tls1_0_codes },
+  { US"TLS1", tls1_0_codes },
+  { US"SSL3", ssl3_codes }
+};
+
+
+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 }
+};
+
+
 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,
@@ -103,6 +164,16 @@ static pri_item cipher_index[] = {
 };
 
 
+static int mac_sha_codes[]     = { GNUTLS_MAC_SHA, 0 };
+static int mac_md5_codes[]     = { GNUTLS_MAC_MD5, 0 };
+
+static pri_item mac_index[] = {
+  { US"SHA",  mac_sha_codes },
+  { US"SHA1", mac_sha_codes },
+  { US"MD5",  mac_md5_codes }
+};
+
+
 
 /*************************************************
 *               Handle TLS error                 *
@@ -119,27 +190,28 @@ Argument:
   prefix    text to include in the logged error
   host      NULL if setting up a server;
             the connected host if setting up a client
-  err       a GnuTLS error number, or 0 if local error
+  msg       additional error string (may be NULL)
+            usually obtained from gnutls_strerror()
 
 Returns:    OK/DEFER/FAIL
 */
 
 static int
-tls_error(uschar *prefix, host_item *host, int err)
+tls_error(uschar *prefix, host_item *host, const char *msg)
 {
-uschar *errtext = US"";
-if (err != 0) errtext = string_sprintf(": %s", gnutls_strerror(err));
 if (host == NULL)
   {
-  log_write(0, LOG_MAIN, "TLS error on connection from %s (%s)%s",
-    (sender_fullhost != NULL)? sender_fullhost : US "local process",
-    prefix, errtext);
+  uschar *conn_info = smtp_get_connection_info();
+  if (strncmp(conn_info, "SMTP ", 5) == 0)
+    conn_info += 5;
+  log_write(0, LOG_MAIN, "TLS error on %s (%s)%s%s",
+    conn_info, prefix, msg ? ": " : "", msg ? msg : "");
   return DEFER;
   }
 else
   {
-  log_write(0, LOG_MAIN, "TLS error on connection to %s [%s] (%s)%s",
-    host->name, host->address, prefix, errtext);
+  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;
   }
 }
@@ -161,12 +233,12 @@ Returns:     TRUE/FALSE
 */
 
 static BOOL
-verify_certificate(gnutls_session session, uschar **error)
+verify_certificate(gnutls_session session, const char **error)
 {
-int verify;
+int rc;
 uschar *dn_string = US"";
 const gnutls_datum *cert;
-unsigned int cert_size = 0;
+unsigned int verify, cert_size = 0;
 
 *error = NULL;
 
@@ -190,23 +262,23 @@ if (cert != NULL)
       dn_string = string_copy_malloc(buff);
     }
 
-  verify = gnutls_certificate_verify_peers(session);
+  rc = gnutls_certificate_verify_peers2(session, &verify);
   }
 else
   {
   DEBUG(D_tls) debug_printf("no peer certificate supplied\n");
   verify = GNUTLS_CERT_INVALID;
-  *error = US"not supplied";
+  *error = "not supplied";
   }
 
 /* Handle the result of verification. INVALID seems to be set as well
 as REVOKED, but leave the test for both. */
 
-if ((verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0)
+if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0)
   {
   tls_certificate_verified = FALSE;
   if (*error == NULL) *error = ((verify & GNUTLS_CERT_REVOKED) != 0)?
-    US"revoked" : US"invalid";
+    "revoked" : "invalid";
   if (verify_requirement == VERIFY_REQUIRED)
     {
     DEBUG(D_tls) debug_printf("TLS certificate verification failed (%s): "
@@ -230,48 +302,11 @@ return TRUE;                            /* accept */
 
 
 
-
-/*************************************************
-*        Write/read datum to/from file           *
-*************************************************/
-
-/* These functions are used for saving and restoring the RSA and D-H parameters
-for use by all Exim processes. Data that is read is placed in malloc'd store
-because that's what happens for newly generated data.
-
-Arguments:
-  fd          the file descriptor
-  d           points to the datum
-
-returns:      FALSE on error (errno set)
-*/
-
-static BOOL
-write_datum(int fd, gnutls_datum *d)
-{
-if (write(fd, &(d->size), sizeof(d->size)) != sizeof(d->size)) return FALSE;
-if (write(fd, d->data, d->size) != d->size) return FALSE;
-return TRUE;
-}
-
-
-static BOOL
-read_datum(int fd, gnutls_datum *d)
-{
-if (read(fd, &(d->size), sizeof(d->size)) != sizeof(d->size)) return FALSE;
-d->data = malloc(d->size);
-if (d->data == NULL) return FALSE;
-if (read(fd, d->data, d->size) != d->size) return FALSE;
-return TRUE;
-}
-
-
-
 /*************************************************
-*          Setup up RSA and DH parameters        *
+*            Setup up DH parameters              *
 *************************************************/
 
-/* Generating the RSA and D-H parameters takes a long time. They only need to
+/* Generating the D-H parameters may take a long time. They only need to
 be re-generated every so often, depending on security policy. What we do is to
 keep these parameters in a file in the spool directory. If the file does not
 exist, we generate them. This means that it is easy to cause a regeneration.
@@ -288,49 +323,81 @@ Returns:     OK/DEFER/FAIL
 */
 
 static int
-init_rsa_dh(host_item *host)
+init_dh(host_item *host)
 {
-int fd, ret;
-gnutls_datum m, e, d, p, q, u, prime, generator;
+int fd;
+int ret;
+gnutls_datum m;
 uschar filename[200];
 
 /* Initialize the data structures for holding the parameters */
 
-ret = gnutls_rsa_params_init(&rsa_params);
-if (ret < 0) return tls_error(US"init rsa_params", host, ret);
-
 ret = gnutls_dh_params_init(&dh_params);
-if (ret < 0) return tls_error(US"init dh_params", host, ret);
+if (ret < 0) return tls_error(US"init dh_params", host, gnutls_strerror(ret));
 
 /* Set up the name of the cache file */
 
 if (!string_format(filename, sizeof(filename), "%s/gnutls-params",
       spool_directory))
-  return tls_error(US"overlong filename", host, 0);
+  return tls_error(US"overlong filename", host, NULL);
 
-/* Open the cache file for reading. If this fails because of a non-existent
-file, compute a new set of parameters, write them to a temporary file, and then
-rename that file as the cache file. Other opening errors are bad. */
+/* Open the cache file for reading and if successful, read it and set up the
+parameters. */
 
 fd = Uopen(filename, O_RDONLY, 0);
-if (fd < 0)
+if (fd >= 0)
   {
-  unsigned int rsa_bits = RSA_BITS;
-  unsigned int dh_bits = DH_BITS;
-  uschar tempfilename[sizeof(filename) + 10];
+  struct stat statbuf;
+  if (fstat(fd, &statbuf) < 0)
+    {
+    (void)close(fd);
+    return tls_error(US"TLS cache stat failed", host, strerror(errno));
+    }
+
+  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");
+
+  free(m.data);
+  }
 
-  if (errno != ENOENT)
-    return tls_error(string_open_failed(errno, "%s for reading", filename),
-      host, 0);
+/* If the file does not exist, fall through to compute new data and cache it.
+If there was any other opening error, it is serious. */
 
-  DEBUG(D_tls) debug_printf("generating %d bit RSA key...\n", RSA_BITS);
-  ret = gnutls_rsa_params_generate2(rsa_params, RSA_BITS);
-  if (ret < 0) return tls_error(US"RSA key generation", host, ret);
+else if (errno == ENOENT)
+  {
+  ret = -1;
+  DEBUG(D_tls)
+    debug_printf("parameter cache file %s does not exist\n", filename);
+  }
+else
+  return tls_error(string_open_failed(errno, "%s for reading", filename),
+    host, 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
+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)
+  {
+  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, ret);
+  if (ret < 0) return tls_error(US"D-H key generation", host, gnutls_strerror(ret));
 
   /* Write the parameters to a file in the spool directory so that we
   can use them from other Exim processes. */
@@ -339,61 +406,43 @@ if (fd < 0)
   fd = Uopen(tempfilename, O_WRONLY|O_CREAT, 0400);
   if (fd < 0)
     return tls_error(string_open_failed(errno, "%s for writing", filename),
-      host, 0);
+      host, NULL);
   (void)fchown(fd, exim_uid, exim_gid);   /* Probably not necessary */
 
-  ret = gnutls_rsa_params_export_raw(rsa_params, &m, &e, &d, &p, &q, &u,
-    &rsa_bits);
-  if (ret < 0) return tls_error(US"RSA params export", host, ret);
-
-  ret = gnutls_dh_params_export_raw(dh_params, &prime, &generator, &dh_bits);
-  if (ret < 0) return tls_error(US"DH params export", host, ret);
-
-  if (!write_datum(fd, &m) ||
-      !write_datum(fd, &e) ||
-      !write_datum(fd, &d) ||
-      !write_datum(fd, &p) ||
-      !write_datum(fd, &q) ||
-      !write_datum(fd, &u) ||
-      !write_datum(fd, &prime) ||
-      !write_datum(fd, &generator))
-    return tls_error(US"TLS cache write failed", host, 0);
-
+  /* 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
+   */
+
+  m.size = PARAM_SIZE;
+  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));
+
+  free(m.data);
   (void)close(fd);
 
   if (rename(CS tempfilename, CS filename) < 0)
-    return tls_error(string_sprintf("failed to rename %s as %s: %s",
-      tempfilename, filename, strerror(errno)), host, 0);
-
-  DEBUG(D_tls) debug_printf("wrote RSA and D-H parameters to file\n");
-  }
-
-/* File opened for reading; get the data */
-
-else
-  {
-  if (!read_datum(fd, &m) ||
-      !read_datum(fd, &e) ||
-      !read_datum(fd, &d) ||
-      !read_datum(fd, &p) ||
-      !read_datum(fd, &q) ||
-      !read_datum(fd, &u) ||
-      !read_datum(fd, &prime) ||
-      !read_datum(fd, &generator))
-    return tls_error(US"TLS cache read failed", host, 0);
-
-  (void)close(fd);
-
-  ret = gnutls_rsa_params_import_raw(rsa_params, &m, &e, &d, &p, &q, &u);
-  if (ret < 0) return tls_error(US"RSA params import", host, ret);
+    return tls_error(string_sprintf("failed to rename %s as %s",
+      tempfilename, filename), host, strerror(errno));
 
-  ret = gnutls_dh_params_import_raw(dh_params, &prime, &generator);
-  if (ret < 0) return tls_error(US"DH params import", host, ret);
-
-  DEBUG(D_tls) debug_printf("read RSA and D-H parameters from file\n");
+  DEBUG(D_tls) debug_printf("wrote D-H parameters to file %s\n", filename);
   }
 
-DEBUG(D_tls) debug_printf("initialized RSA and D-H parameters\n");
+DEBUG(D_tls) debug_printf("initialized D-H parameters\n");
 return OK;
 }
 
@@ -424,21 +473,23 @@ tls_init(host_item *host, uschar *certificate, uschar *privatekey, uschar *cas,
 int rc;
 uschar *cert_expanded, *key_expanded, *cas_expanded, *crl_expanded;
 
-initialized = (host == NULL)? INITIALIZED_SERVER : INITIALIZED_CLIENT;
+client_host = host;
 
 rc = gnutls_global_init();
-if (rc < 0) return tls_error(US"tls-init", host, rc);
+if (rc < 0) return tls_error(US"tls-init", host, gnutls_strerror(rc));
 
-/* Create RSA and D-H parameters, or read them from the cache file. This
-function does its own SMTP error messaging. */
+/* Create D-H parameters, or read them from the cache file. This function does
+its own SMTP error messaging. */
 
-rc = init_rsa_dh(host);
+rc = init_dh(host);
 if (rc != OK) return rc;
 
 /* Create the credentials structure */
 
 rc = gnutls_certificate_allocate_credentials(&x509_cred);
-if (rc < 0) return tls_error(US"certificate_allocate_credentials", host, rc);
+if (rc < 0)
+  return tls_error(US"certificate_allocate_credentials",
+    host, gnutls_strerror(rc));
 
 /* This stuff must be done for each session, because different certificates
 may be required for different sessions. */
@@ -446,12 +497,19 @@ may be required for different sessions. */
 if (!expand_check(certificate, US"tls_certificate", &cert_expanded))
   return DEFER;
 
+key_expanded = NULL;
 if (privatekey != NULL)
   {
   if (!expand_check(privatekey, US"tls_privatekey", &key_expanded))
     return DEFER;
   }
-else key_expanded = cert_expanded;
+
+/* 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 */
 
@@ -461,12 +519,12 @@ if (cert_expanded != NULL)
     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) 
+  if (rc < 0)
     {
     uschar *msg = string_sprintf("cert/key setup: cert=%s key=%s",
-      cert_expanded, key_expanded); 
-    return tls_error(msg, host, rc);
-    } 
+      cert_expanded, key_expanded);
+    return tls_error(msg, host, gnutls_strerror(rc));
+    }
   }
 
 /* A certificate is mandatory in a server, but not in a client */
@@ -474,7 +532,7 @@ if (cert_expanded != NULL)
 else
   {
   if (host == NULL)
-    return tls_error(US"no TLS server certificate is specified", host, 0);
+    return tls_error(US"no TLS server certificate is specified", NULL, NULL);
   DEBUG(D_tls) debug_printf("no TLS client certificate is specified\n");
   }
 
@@ -498,8 +556,8 @@ if (cas != NULL)
     return DEFER;
     }
 
-  DEBUG(D_tls) debug_printf("verify certificates = %s size=%d\n",
-    cas_expanded, (int)statbuf.st_size);
+  DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n",
+    cas_expanded, statbuf.st_size);
 
   /* If the cert file is empty, there's no point in loading the CRL file. */
 
@@ -507,7 +565,7 @@ if (cas != NULL)
     {
     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, rc);
+    if (rc < 0) return tls_error(US"setup_certs", host, gnutls_strerror(rc));
 
     if (crl != NULL && *crl != 0)
       {
@@ -516,7 +574,7 @@ if (cas != NULL)
       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, rc);
+      if (rc < 0) return tls_error(US"CRL setup", host, gnutls_strerror(rc));
       }
     }
   }
@@ -524,7 +582,6 @@ if (cas != NULL)
 /* Associate the parameters with the x509 credentials structure. */
 
 gnutls_certificate_set_dh_params(x509_cred, dh_params);
-gnutls_certificate_set_rsa_params(x509_cred, rsa_params);
 
 DEBUG(D_tls) debug_printf("initialized certificate stuff\n");
 return OK;
@@ -534,7 +591,7 @@ return OK;
 
 
 /*************************************************
-*        Remove ciphers from priority list       *
+*           Remove from a priority list          *
 *************************************************/
 
 /* Cautiously written so that it will remove duplicates if present.
@@ -547,7 +604,7 @@ Returns:       nothing
 */
 
 static void
-remove_ciphers(int *list, int *remove_list)
+remove_priority(int *list, int *remove_list)
 {
 for (; *remove_list != 0; remove_list++)
   {
@@ -567,7 +624,7 @@ for (; *remove_list != 0; remove_list++)
 
 
 /*************************************************
-*        Add ciphers to priority list            *
+*            Add to a priority list              *
 *************************************************/
 
 /* Cautiously written to check the list size
@@ -581,7 +638,7 @@ Returns:       TRUE if OK; FALSE if list overflows
 */
 
 static BOOL
-add_ciphers(int *list, int list_max, int *add_list)
+add_priority(int *list, int list_max, int *add_list)
 {
 int next = 0;
 while (list[next] != 0) next++;
@@ -596,6 +653,78 @@ return TRUE;
 
 
 
+/*************************************************
+*          Adjust a priority list                *
+*************************************************/
+
+/* This function is called to adjust the lists of cipher algorithms, MAC
+algorithms, key-exchange methods, and protocols.
+
+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
+
+
+  which       text for an error message
+
+Returns:      FALSE if the table overflows, else TRUE
+*/
+
+static BOOL
+set_priority(int *plist, int psize, uschar *s, pri_item *index, int isize,
+   uschar *which)
+{
+int sep = 0;
+BOOL first = TRUE;
+uschar *t;
+
+while ((t = string_nextinlist(&s, &sep, big_buffer, big_buffer_size)) != NULL)
+  {
+  int i;
+  BOOL exclude = t[0] == '!';
+  if (first && !exclude) plist[0] = 0;
+  first = FALSE;
+  for (i = 0; i < isize; i++)
+    {
+    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;
+            }
+          }
+        }
+      }
+    }
+  }
+
+DEBUG(D_tls)
+  {
+  int *ptr = plist;
+  debug_printf("adjusted %s priorities:", which);
+  while (*ptr != 0) debug_printf(" %d", *ptr++);
+  debug_printf("\n");
+  }
+
+return TRUE;
+}
+
+
+
+
 /*************************************************
 *        Initialize a single GNUTLS session      *
 *************************************************/
@@ -613,78 +742,60 @@ cipher.
 
 Arguments:
   side         one of GNUTLS_SERVER, GNUTLS_CLIENT
-  expciphers   expanded ciphers list
+  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
 
 Returns:  a gnutls_session, or NULL if there is a problem
 */
 
 static gnutls_session
-tls_session_init(int side, uschar *expciphers)
+tls_session_init(int side, uschar *expciphers, uschar *expmac, uschar *expkx,
+  uschar *expproto)
 {
 gnutls_session session;
 
 gnutls_init(&session, side);
 
-/* Handle the list of permitted ciphers */
+/* Initialize the lists of permitted protocols, key-exchange methods, ciphers,
+and MACs. */
 
 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));
+
+/* 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. */
 
 if (expciphers != NULL)
   {
-  int sep = 0;
-  BOOL first = TRUE;
-  uschar *cipher;
-
-  /* The names OpenSSL uses are of the form DES-CBC3-SHA, using hyphen
-  separators. GnuTLS uses underscore separators. So that I can use either form
-  in my tests, and also for general convenience, we turn hyphens into
-  underscores before scanning the list. */
-
   uschar *s = expciphers;
   while (*s != 0) { if (*s == '-') *s = '_'; s++; }
+  }
 
-  while ((cipher = string_nextinlist(&expciphers, &sep, big_buffer,
-             big_buffer_size)) != NULL)
-    {
-    int i;
-    BOOL exclude = cipher[0] == '!';
-    if (first && !exclude) cipher_priority[0] = 0;
-    first = FALSE;
-
-    for (i = 0; i < sizeof(cipher_index)/sizeof(pri_item); i++)
-      {
-      uschar *ss = strstric(cipher, cipher_index[i].name, FALSE);
-      if (ss != NULL)
-        {
-        uschar *endss = ss + Ustrlen(cipher_index[i].name);
-        if ((ss == cipher || !isalnum(ss[-1])) && !isalnum(*endss))
-          {
-          if (exclude)
-            remove_ciphers(cipher_priority, cipher_index[i].values);
-          else
-            {
-            if (!add_ciphers(cipher_priority,
-                             sizeof(cipher_priority)/sizeof(pri_item),
-                             cipher_index[i].values))
-              {
-              log_write(0, LOG_MAIN|LOG_PANIC, "GnuTLS init failed: cipher "
-                "priority table overflow");
-              gnutls_deinit(session);
-              return NULL;
-              }
-            }
-          }
-        }
-      }
-    }
-
-  DEBUG(D_tls)
-    {
-    int *ptr = cipher_priority;
-    debug_printf("adjusted cipher priorities:");
-    while (*ptr != 0) debug_printf(" %d", *ptr++);
-    debug_printf("\n");
-    }
+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")))
+  {
+  gnutls_deinit(session);
+  return NULL;
   }
 
 /* Define the various priorities */
@@ -692,7 +803,7 @@ if (expciphers != NULL)
 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, protocol_priority);
+gnutls_protocol_set_priority(session, proto_priority);
 gnutls_mac_set_priority(session, mac_priority);
 
 gnutls_cred_set(session, GNUTLS_CRD_CERTIFICATE, x509_cred);
@@ -709,6 +820,18 @@ if (verify_requirement != VERIFY_NONE)
 
 gnutls_db_set_cache_expiration(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(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 GnuTLS session\n");
 return session;
 }
@@ -761,7 +884,10 @@ the STARTTLS command. It must respond to that command, and then negotiate
 a TLS session.
 
 Arguments:
-  require_ciphers  list of allowed ciphers
+  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
@@ -770,19 +896,21 @@ Returns:           OK on success
 */
 
 int
-tls_server_start(uschar *require_ciphers)
+tls_server_start(uschar *require_ciphers, uschar *require_mac,
+  uschar *require_kx, uschar *require_proto)
 {
 int rc;
-uschar *error;
+const char *error;
 uschar *expciphers = NULL;
+uschar *expmac = NULL;
+uschar *expkx = NULL;
+uschar *expproto = NULL;
 
 /* Check for previous activation */
 
 if (tls_active >= 0)
   {
-  log_write(0, LOG_MAIN, "STARTTLS received in already encrypted "
-    "connection from %s",
-    (sender_fullhost != NULL)? sender_fullhost : US"local process");
+  tls_error("STARTTLS received after TLS started", NULL, "");
   smtp_printf("554 Already in TLS\r\n");
   return FAIL;
   }
@@ -796,7 +924,10 @@ rc = tls_init(NULL, tls_certificate, tls_privatekey, tls_verify_certificates,
   tls_crl);
 if (rc != OK) return rc;
 
-if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers))
+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
@@ -812,9 +943,11 @@ else if (verify_check_host(&tls_try_verify_hosts) == OK)
 
 /* Prepare for new connection */
 
-tls_session = tls_session_init(GNUTLS_SERVER, expciphers);
+tls_session = tls_session_init(GNUTLS_SERVER, expciphers, expmac, expkx,
+  expproto);
 if (tls_session == NULL)
-  return tls_error(US"tls_session_init", NULL, GNUTLS_E_MEMORY_ERROR);
+  return tls_error(US"tls_session_init", NULL,
+    gnutls_strerror(GNUTLS_E_MEMORY_ERROR));
 
 /* 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
@@ -831,7 +964,8 @@ 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_ptr(tls_session, (gnutls_transport_ptr)fileno(smtp_out));
+gnutls_transport_set_ptr2(tls_session, (gnutls_transport_ptr)fileno(smtp_in),
+                                       (gnutls_transport_ptr)fileno(smtp_out));
 
 sigalrm_seen = FALSE;
 if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout);
@@ -840,14 +974,8 @@ alarm(0);
 
 if (rc < 0)
   {
-  if (sigalrm_seen)
-    Ustrcpy(ssl_errstring, "timed out");
-  else
-    Ustrcpy(ssl_errstring, gnutls_strerror(rc));
-  log_write(0, LOG_MAIN,
-    "TLS error on connection from %s (gnutls_handshake): %s",
-    (sender_fullhost != NULL)? sender_fullhost : US"local process",
-    ssl_errstring);
+  tls_error(US"gnutls_handshake", NULL,
+    sigalrm_seen ? "timed out" : gnutls_strerror(rc));
 
   /* 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
@@ -855,8 +983,8 @@ if (rc < 0)
 
   if (!sigalrm_seen)
     {
-    fclose(smtp_out);
-    fclose(smtp_in);
+    (void)fclose(smtp_out);
+    (void)fclose(smtp_in);
     }
 
   return FAIL;
@@ -867,9 +995,7 @@ DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n");
 if (verify_requirement != VERIFY_NONE &&
      !verify_certificate(tls_session, &error))
   {
-  log_write(0, LOG_MAIN,
-    "TLS error on connection from %s: certificate verification failed (%s)",
-    (sender_fullhost != NULL)? sender_fullhost : US"local process", error);
+  tls_error(US"certificate verification failed", NULL, error);
   return FAIL;
   }
 
@@ -886,6 +1012,7 @@ receive_getc = tls_getc;
 receive_ungetc = tls_ungetc;
 receive_feof = tls_feof;
 receive_ferror = tls_ferror;
+receive_smtp_buffered = tls_smtp_buffered;
 
 tls_active = fileno(smtp_out);
 
@@ -904,13 +1031,16 @@ return OK;
 Arguments:
   fd                the fd of the connection
   host              connected host (for messages)
-  addr
+  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
+  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
 
 Returns:            OK/DEFER/FAIL (because using common functions),
@@ -920,27 +1050,36 @@ Returns:            OK/DEFER/FAIL (because using common functions),
 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, int timeout)
+  uschar *verify_crl, uschar *require_ciphers, uschar *require_mac,
+  uschar *require_kx, uschar *require_proto, int timeout)
 {
 const gnutls_datum *server_certs;
 uschar *expciphers = NULL;
-uschar *error;
+uschar *expmac = NULL;
+uschar *expkx = NULL;
+uschar *expproto = NULL;
+const char *error;
 unsigned int server_certs_size;
 int rc;
 
 DEBUG(D_tls) debug_printf("initializing GnuTLS as a client\n");
 
-client_host = host;
 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 (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers))
+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;
 
-tls_session = tls_session_init(GNUTLS_CLIENT, expciphers);
+tls_session = tls_session_init(GNUTLS_CLIENT, expciphers, expmac, expkx,
+  expproto);
+
 if (tls_session == NULL)
-  return tls_error(US "tls_session_init", host, GNUTLS_E_MEMORY_ERROR);
+  return tls_error(US "tls_session_init", host,
+    gnutls_strerror(GNUTLS_E_MEMORY_ERROR));
 
 gnutls_transport_set_ptr(tls_session, (gnutls_transport_ptr)fd);
 
@@ -952,15 +1091,8 @@ rc = gnutls_handshake(tls_session);
 alarm(0);
 
 if (rc < 0)
-  {
-  if (sigalrm_seen)
-    {
-    log_write(0, LOG_MAIN, "TLS error on connection to %s [%s]: "
-      "gnutls_handshake timed out", host->name, host->address);
-    return FAIL;
-    }
-  else return tls_error(US "gnutls_handshake", host, rc);
-  }
+  return tls_error(US "gnutls_handshake", host,
+    sigalrm_seen ? "timed out" : gnutls_strerror(rc));
 
 server_certs = gnutls_certificate_get_peers(tls_session, &server_certs_size);
 
@@ -984,12 +1116,7 @@ if (server_certs != NULL)
 
 if (verify_requirement != VERIFY_NONE &&
       !verify_certificate(tls_session, &error))
-  {
-  log_write(0, LOG_MAIN,
-    "TLS error on connection to %s [%s]: certificate verification failed (%s)",
-    host->name, host->address, error);
-  return FAIL;
-  }
+  return tls_error(US"certificate verification failed", host, error);
 
 construct_cipher_name(tls_session);    /* Sets tls_cipher */
 tls_active = fd;
@@ -1015,21 +1142,15 @@ Returns:   nothing
 static void
 record_io_error(int ec, uschar *when, uschar *text)
 {
-uschar *additional = US"";
+const char *msg;
 
 if (ec == GNUTLS_E_FATAL_ALERT_RECEIVED)
-  additional = string_sprintf(": %s",
+  msg = string_sprintf("%s: %s", gnutls_strerror(ec),
     gnutls_alert_get_name(gnutls_alert_get(tls_session)));
-
-if (initialized == INITIALIZED_SERVER)
-  log_write(0, LOG_MAIN, "TLS %s error on connection from %s: %s%s", when,
-    (sender_fullhost != NULL)? sender_fullhost : US "local process",
-    (ec == 0)? text : US gnutls_strerror(ec), additional);
-
 else
-  log_write(0, LOG_MAIN, "TLS %s error on connection to %s [%s]: %s%s", when,
-    client_host->name, client_host->address,
-    (ec == 0)? text : US gnutls_strerror(ec), additional);
+  msg = gnutls_strerror(ec);
+
+tls_error(when, client_host, msg);
 }
 
 
@@ -1072,6 +1193,7 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
     receive_ungetc = smtp_ungetc;
     receive_feof = smtp_feof;
     receive_ferror = smtp_ferror;
+    receive_smtp_buffered = smtp_buffered;
 
     gnutls_deinit(tls_session);
     tls_session = NULL;
@@ -1090,7 +1212,9 @@ if (ssl_xfer_buffer_lwm >= ssl_xfer_buffer_hwm)
     ssl_xfer_error = 1;
     return EOF;
     }
-
+#ifndef DISABLE_DKIM
+  dkim_exim_verify_feed(ssl_xfer_buffer, inbytes);
+#endif
   ssl_xfer_buffer_hwm = inbytes;
   ssl_xfer_buffer_lwm = 0;
   }
@@ -1214,4 +1338,26 @@ gnutls_global_deinit();
 tls_active = -1;
 }
 
+
+
+
+/*************************************************
+*         Report the library versions.           *
+*************************************************/
+
+/* See a description in tls-openssl.c for an explanation of why this exists.
+
+Arguments:   a FILE* to print the results to
+Returns:     nothing
+*/
+
+void
+tls_version_report(FILE *f)
+{
+fprintf(f, "Library version: GnuTLS: Compile: %s\n"
+           "                         Runtime: %s\n",
+           LIBGNUTLS_VERSION,
+           gnutls_check_version(NULL));
+}
+
 /* End of tls-gnu.c */