For DH, use standard primes from RFCs
[exim.git] / src / src / tls-gnu.c
index 328466cc35466dd74539a56901ffd1aa89be77be..7aab309e6bc74f219d772284a03f669c698291cf 100644 (file)
@@ -79,6 +79,7 @@ typedef struct exim_gnutls_state {
   BOOL have_set_peerdn;
   const struct host_item *host;
   uschar *peerdn;
+  uschar *ciphersuite;
   uschar *received_sni;
 
   const uschar *tls_certificate;
@@ -99,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, FALSE,
-  NULL, NULL, NULL,
+  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
@@ -313,7 +311,7 @@ cipher = gnutls_cipher_get(state->session);
 /* returns size in "bytes" */
 tls_bits = gnutls_cipher_get_key_size(cipher) * 8;
 
-tls_cipher = state->cipherbuf;
+tls_cipher = state->ciphersuite;
 
 DEBUG(D_tls) debug_printf("cipher: %s\n", tls_cipher);
 
@@ -360,9 +358,6 @@ 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
 */
 
@@ -372,8 +367,12 @@ init_server_dh(void)
 int fd, rc;
 unsigned int dh_bits;
 gnutls_datum m;
-uschar filename[PATH_MAX];
+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 */
 
 DEBUG(D_tls) debug_printf("Initialising GnuTLS server params.\n");
@@ -381,6 +380,46 @@ DEBUG(D_tls) debug_printf("Initialising GnuTLS server params.\n");
 rc = gnutls_dh_params_init(&dh_server_params);
 exim_gnutls_err_check(US"gnutls_dh_params_init");
 
+m.data = NULL;
+m.size = 0;
+
+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. */
@@ -397,9 +436,22 @@ DEBUG(D_tls)
       dh_bits);
 #endif
 
-if (!string_format(filename, sizeof(filename),
-      "%s/gnutls-params-%d", spool_directory, dh_bits))
-  return tls_error(US"overlong filename", NULL, NULL);
+/* 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. */
@@ -476,6 +528,7 @@ case. */
 if (rc < 0)
   {
   uschar *temp_fn;
+  unsigned int dh_bits_gen = dh_bits;
 
   if ((PATH_MAX - Ustrlen(filename)) < 10)
     return tls_error(US"Filename too long to generate replacement",
@@ -487,8 +540,26 @@ if (rc < 0)
     return tls_error(US"Unable to open temp file", strerror(errno), NULL);
   (void)fchown(fd, exim_uid, exim_gid);   /* Probably not necessary */
 
-  DEBUG(D_tls) debug_printf("generating %d bits Diffie-Hellman key ...\n", dh_bits);
-  rc = gnutls_dh_params_generate2(dh_server_params, dh_bits);
+  /* 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);
+    }
+
+  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,
@@ -505,6 +576,7 @@ 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 */
   rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM,
       m.data, &sz);
   if (rc != GNUTLS_E_SUCCESS)
@@ -512,6 +584,7 @@ if (rc < 0)
     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)
@@ -580,7 +653,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;
@@ -697,16 +770,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;
   }
@@ -730,15 +805,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;
@@ -867,6 +945,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;
@@ -975,6 +1054,20 @@ return OK;
 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 *
 
@@ -984,8 +1077,9 @@ 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;
@@ -1008,7 +1102,7 @@ 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),
+string_format(cipherbuf, sizeof(cipherbuf),
     "%s:%s:%d",
     gnutls_protocol_get_name(protocol),
     gnutls_cipher_suite_get_name(kx, cipher, mac),
@@ -1017,9 +1111,14 @@ string_format(state->cipherbuf, sizeof(state->cipherbuf),
 /* 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)
+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);
@@ -1160,7 +1259,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
 
@@ -1344,7 +1450,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)
@@ -1479,7 +1586,8 @@ 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)
@@ -1796,6 +1904,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.           *
 *************************************************/