expanded comment, noting size types and API issue
[exim.git] / src / src / tls-gnu.c
index 4e1e5104ba034eb379662f0204be25799886814f..b2659d7a794c208f795ab7da15404c6a8978e57f 100644 (file)
@@ -76,8 +76,10 @@ typedef struct exim_gnutls_state {
   int fd_out;
   BOOL peer_cert_verified;
   BOOL trigger_sni_changes;
+  BOOL have_set_peerdn;
   const struct host_item *host;
   uschar *peerdn;
+  uschar *ciphersuite;
   uschar *received_sni;
 
   const uschar *tls_certificate;
@@ -98,17 +100,14 @@ typedef struct exim_gnutls_state {
   int xfer_buffer_hwm;
   int xfer_eof;
   int xfer_error;
-
-  uschar cipherbuf[256];
 } exim_gnutls_state_st;
 
 static const exim_gnutls_state_st exim_gnutls_state_init = {
-  NULL, NULL, NULL, VERIFY_NONE, -1, -1, FALSE, FALSE,
-  NULL, NULL, NULL,
+  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, 0, 0, 0, 0,
-  ""
 };
 
 /* Not only do we have our own APIs which don't pass around state, assuming
@@ -297,11 +296,7 @@ Argument:
 static void
 extract_exim_vars_from_tls_state(exim_gnutls_state_st *state)
 {
-gnutls_protocol_t protocol;
 gnutls_cipher_algorithm_t cipher;
-gnutls_kx_algorithm_t kx;
-gnutls_mac_algorithm_t mac;
-uschar *p;
 #ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
 int old_pool;
 int rc;
@@ -316,26 +311,7 @@ cipher = gnutls_cipher_get(state->session);
 /* returns size in "bytes" */
 tls_bits = gnutls_cipher_get_key_size(cipher) * 8;
 
-if (!*state->cipherbuf)
-  {
-  protocol = gnutls_protocol_get_version(state->session);
-  mac = gnutls_mac_get(state->session);
-  kx = gnutls_kx_get(state->session);
-
-  string_format(state->cipherbuf, sizeof(state->cipherbuf),
-      "%s:%s:%u",
-      gnutls_protocol_get_name(protocol),
-      gnutls_cipher_suite_get_name(kx, cipher, mac),
-      tls_bits);
-
-  /* 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 = state->cipherbuf; *p != '\0'; ++p)
-    if (isspace(*p))
-      *p = '-';
-  }
-tls_cipher = state->cipherbuf;
+tls_cipher = state->ciphersuite;
 
 DEBUG(D_tls) debug_printf("cipher: %s\n", tls_cipher);
 
@@ -419,6 +395,15 @@ DEBUG(D_tls)
       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 (!string_format(filename, sizeof(filename),
       "%s/gnutls-params-%d", spool_directory, dh_bits))
   return tls_error(US"overlong filename", NULL, NULL);
@@ -527,8 +512,13 @@ if (rc < 0)
   m.data = malloc(m.size);
   if (m.data == NULL)
     return tls_error(US"memory allocation failed", strerror(errno), NULL);
+  /* this will return a size 1 less than the allocation size above; I
+  originally used sz so as to avoid type compatibility errors, as gnutls_datum
+  uses "unsigned int" for the size field, but this call takes separate data
+  and size fields, with the latter being a size_t*.  For now, we live with
+  the error as being safer than throwing away type information. */
   rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM,
-      m.data, &sz);
+      m.data, &m.size);
   if (rc != GNUTLS_E_SUCCESS)
     {
     free(m.data);
@@ -602,7 +592,7 @@ if (!state->host)
   {
   if (!state->received_sni)
     {
-    if (Ustrstr(state->tls_certificate, US"tls_sni"))
+    if (state->tls_certificate && Ustrstr(state->tls_certificate, US"tls_sni"))
       {
       DEBUG(D_tls) debug_printf("We will re-expand TLS session files if we receive SNI.\n");
       state->trigger_sni_changes = TRUE;
@@ -719,16 +709,18 @@ if (Ustat(state->exp_tls_verify_certificates, &statbuf) < 0)
   return DEFER;
   }
 
-if (!S_ISREG(statbuf.st_mode))
+/* 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 not a file: \"%s\"\n%s\n",
-        state->exp_tls_verify_certificates,
-        S_ISDIR(statbuf.st_mode)
-          ? " it's a directory, that's OpenSSL, this is GnuTLS"
-          : " (not a directory either)");
+    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 not a file",
+      "tls_verify_certificates \"%s\" is a directory",
       state->exp_tls_verify_certificates);
   return DEFER;
   }
@@ -752,15 +744,18 @@ if (cert_count < 0)
   }
 DEBUG(D_tls) debug_printf("Added %d certificate authorities.\n", cert_count);
 
-if (state->tls_crl && *state->tls_crl)
+if (state->tls_crl && *state->tls_crl &&
+    state->exp_tls_crl && *state->exp_tls_crl)
   {
-  if (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)
     {
-    DEBUG(D_tls) debug_printf("loading CRL file = %s\n", state->exp_tls_crl);
-    rc = gnutls_certificate_set_x509_crl_file(state->x509_cred,
-        CS state->exp_tls_crl, GNUTLS_X509_FMT_PEM);
+    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;
@@ -889,6 +884,7 @@ 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;
@@ -994,7 +990,22 @@ return OK;
 *************************************************/
 
 /* Called from both server and client code.
-Only this is allowed to set state->peerdn and we use that to detect double-calls.
+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:
   state           exim_gnutls_state_st *
@@ -1005,24 +1016,54 @@ Returns:          OK/DEFER/FAIL
 static int
 peer_status(exim_gnutls_state_st *state)
 {
+uschar cipherbuf[256];
 const gnutls_datum *cert_list;
-int rc;
+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 *dn_buf;
+uschar *p, *dn_buf;
 size_t sz;
 
-if (state->peerdn)
+if (state->have_set_peerdn)
   return OK;
+state->have_set_peerdn = TRUE;
+
+state->peerdn = NULL;
 
-state->peerdn = US"unknown";
+/* 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;
+tls_cipher = state->ciphersuite;
 
+/* tls_peerdn */
 cert_list = gnutls_certificate_get_peers(state->session, &cert_list_size);
 
 if (cert_list == NULL || cert_list_size == 0)
   {
-  state->peerdn = US"unknown (no certificate)";
   DEBUG(D_tls) debug_printf("TLS: no certificate from peer (%p & %d)\n",
       cert_list, cert_list_size);
   if (state->verify_requirement == VERIFY_REQUIRED)
@@ -1035,7 +1076,6 @@ ct = gnutls_certificate_type_get(state->session);
 if (ct != GNUTLS_CRT_X509)
   {
   const char *ctn = gnutls_certificate_type_get_name(ct);
-  state->peerdn = string_sprintf("unknown (type %s)", ctn);
   DEBUG(D_tls)
     debug_printf("TLS: peer cert not X.509 but instead \"%s\"\n", ctn);
   if (state->verify_requirement == VERIFY_REQUIRED)
@@ -1122,7 +1162,7 @@ if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0)
 
   DEBUG(D_tls)
     debug_printf("TLS certificate verification failed (%s): peerdn=%s\n",
-        *error, state->peerdn);
+        *error, state->peerdn ? state->peerdn : US"<unset>");
 
   if (state->verify_requirement == VERIFY_REQUIRED)
     {
@@ -1135,7 +1175,8 @@ if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0)
 else
   {
   state->peer_cert_verified = TRUE;
-  DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n", state->peerdn);
+  DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n",
+      state->peerdn ? state->peerdn : US"<unset>");
   }
 
 tls_peerdn = state->peerdn;
@@ -1157,7 +1198,14 @@ return TRUE;
 static void
 exim_gnutls_logger_cb(int level, const char *message)
 {
-  DEBUG(D_tls) debug_printf("GnuTLS<%d>: %s\n", level, 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
 
@@ -1341,7 +1389,8 @@ if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout);
 do
   {
   rc = gnutls_handshake(state->session);
-  } while ((rc == GNUTLS_E_AGAIN) || (rc == GNUTLS_E_INTERRUPTED));
+  } while ((rc == GNUTLS_E_AGAIN) ||
+      (rc == GNUTLS_E_INTERRUPTED && !sigalrm_seen));
 alarm(0);
 
 if (rc != GNUTLS_E_SUCCESS)
@@ -1476,9 +1525,14 @@ alarm(timeout);
 do
   {
   rc = gnutls_handshake(state->session);
-  } while ((rc == GNUTLS_E_AGAIN) || (rc == GNUTLS_E_INTERRUPTED));
+  } while ((rc == GNUTLS_E_AGAIN) ||
+      (rc == GNUTLS_E_INTERRUPTED && !sigalrm_seen));
 alarm(0);
 
+if (rc != GNUTLS_E_SUCCESS)
+  return tls_error(US"gnutls_handshake",
+      sigalrm_seen ? "timed out" : gnutls_strerror(rc), state->host);
+
 DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n");
 
 /* Verify late */
@@ -1492,7 +1546,7 @@ if (state->verify_requirement != VERIFY_NONE &&
 rc = peer_status(state);
 if (rc != OK) return rc;
 
-/* Sets various Exim expansion variables; always safe within server */
+/* Sets various Exim expansion variables; may need to adjust for ACL callouts */
 
 extract_exim_vars_from_tls_state(state);
 
@@ -1789,6 +1843,64 @@ vaguely_random_number(int max)
 
 
 
+/*************************************************
+*  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");
+
+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);
+
+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();
+
+return NULL;
+}
+
+
+
+
 /*************************************************
 *         Report the library versions.           *
 *************************************************/