Move certificate name checking to mainline, default enabled
[exim.git] / src / src / tls-gnu.c
index 5bdb21e6ee3447d6b9dc8198aa688bc1acbd3b12..b520ebfd84edcad4c49f2e8bca1831c7398aae09 100644 (file)
@@ -47,6 +47,15 @@ require current GnuTLS, then we'll drop support for the ancient libraries).
 # warning "GnuTLS library version too old; define DISABLE_OCSP in Makefile"
 # define DISABLE_OCSP
 #endif
+#if GNUTLS_VERSION_NUMBER < 0x020a00 && defined(EXPERIMENTAL_EVENT)
+# warning "GnuTLS library version too old; tls:cert event unsupported"
+# undef EXPERIMENTAL_EVENT
+#endif
+#if GNUTLS_VERSION_NUMBER >= 0x030306
+# define SUPPORT_CA_DIR
+#else
+# undef  SUPPORT_CA_DIR
+#endif
 
 #ifndef DISABLE_OCSP
 # include <gnutls/ocsp.h>
@@ -66,11 +75,7 @@ Changes:
 /* Values for verify_requirement */
 
 enum peer_verify_requirement
-  { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED
-#ifdef EXPERIMENTAL_CERTNAMES
-    ,VERIFY_WITHHOST
-#endif
-  };
+  { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED };
 
 /* 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
@@ -108,13 +113,13 @@ typedef struct exim_gnutls_state {
 
   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;
-#ifdef EXPERIMENTAL_CERTNAMES
   uschar *exp_tls_verify_cert_hostnames;
+#ifdef EXPERIMENTAL_EVENT
+  uschar *event_action;
 #endif
 
   tls_support *tlsp;   /* set in tls_init() */
@@ -131,7 +136,8 @@ static const exim_gnutls_state_st exim_gnutls_state_init = {
   NULL, NULL, NULL, NULL,
   NULL, NULL, NULL, NULL, NULL, NULL,
   NULL, NULL, NULL, NULL, NULL, NULL, NULL,
-#ifdef EXPERIMENTAL_CERTNAMES
+  NULL,
+#ifdef EXPERIMENTAL_EVENT
                                             NULL,
 #endif
   NULL,
@@ -145,7 +151,9 @@ 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. */
+second connection.
+XXX But see gnutls_session_get_ptr()
+*/
 
 static exim_gnutls_state_st state_server, state_client;
 
@@ -175,18 +183,18 @@ static BOOL exim_gnutls_base_init_done = FALSE;
 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
+# define EXIM_GNUTLS_LIBRARY_LOG_LEVEL -1
 #endif
 
 #ifndef EXIM_CLIENT_DH_MIN_BITS
-#define EXIM_CLIENT_DH_MIN_BITS 1024
+# define EXIM_CLIENT_DH_MIN_BITS 1024
 #endif
 
 /* 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
+# define EXIM_SERVER_DH_BITS_PRE2_12 1024
 #endif
 
 #define exim_gnutls_err_check(Label) do { \
@@ -257,7 +265,7 @@ tls_error(const uschar *prefix, const char *msg, const host_item *host)
 {
 if (host)
   {
-  log_write(0, LOG_MAIN, "TLS error on connection to %s [%s] (%s)%s%s",
+  log_write(0, LOG_MAIN, "H=%s [%s] TLS error on connection (%s)%s%s",
       host->name, host->address, prefix, msg ? ": " : "", msg ? msg : "");
   return FAIL;
   }
@@ -266,6 +274,7 @@ else
   uschar *conn_info = smtp_get_connection_info();
   if (Ustrncmp(conn_info, US"SMTP ", 5) == 0)
     conn_info += 5;
+  /* I'd like to get separated H= here, but too hard for now */
   log_write(0, LOG_MAIN, "TLS error on %s (%s)%s%s",
       conn_info, prefix, msg ? ": " : "", msg ? msg : "");
   return DEFER;
@@ -873,6 +882,7 @@ if (Ustat(state->exp_tls_verify_certificates, &statbuf) < 0)
   return DEFER;
   }
 
+#ifndef SUPPORT_CA_DIR
 /* 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
@@ -888,6 +898,7 @@ if (S_ISDIR(statbuf.st_mode))
       state->exp_tls_verify_certificates);
   return DEFER;
   }
+#endif
 
 DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n",
         state->exp_tls_verify_certificates, statbuf.st_size);
@@ -899,8 +910,18 @@ if (statbuf.st_size == 0)
   return OK;
   }
 
-cert_count = gnutls_certificate_set_x509_trust_file(state->x509_cred,
+cert_count =
+
+#ifdef SUPPORT_CA_DIR
+  (statbuf.st_mode & S_IFMT) == S_IFDIR
+  ?
+  gnutls_certificate_set_x509_trust_dir(state->x509_cred,
+    CS state->exp_tls_verify_certificates, GNUTLS_X509_FMT_PEM)
+  :
+#endif
+  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;
@@ -1086,15 +1107,15 @@ 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))
+  if (!expand_check(sni, US"tls_out_sni", &state->tlsp->sni))
     return DEFER;
-  if (state->exp_tls_sni && *state->exp_tls_sni)
+  if (state->tlsp->sni && *state->tlsp->sni)
     {
     DEBUG(D_tls)
-      debug_printf("Setting TLS client SNI to \"%s\"\n", state->exp_tls_sni);
-    sz = Ustrlen(state->exp_tls_sni);
+      debug_printf("Setting TLS client SNI to \"%s\"\n", state->tlsp->sni);
+    sz = Ustrlen(state->tlsp->sni);
     rc = gnutls_server_name_set(state->session,
-        GNUTLS_NAME_DNS, state->exp_tls_sni, sz);
+        GNUTLS_NAME_DNS, state->tlsp->sni, sz);
     exim_gnutls_err_check(US"gnutls_server_name_set");
     }
   }
@@ -1360,8 +1381,7 @@ if (rc < 0 ||
 
 else
   {
-#ifdef EXPERIMENTAL_CERTNAMES
-  if (state->verify_requirement == VERIFY_WITHHOST)
+  if (state->exp_tls_verify_cert_hostnames)
     {
     int sep = 0;
     uschar * list = state->exp_tls_verify_cert_hostnames;
@@ -1373,12 +1393,15 @@ else
       {
       DEBUG(D_tls)
        debug_printf("TLS certificate verification failed: cert name mismatch\n");
-      gnutls_alert_send(state->session,
-       GNUTLS_AL_FATAL, GNUTLS_A_BAD_CERTIFICATE);
-      return FALSE;
+      if (state->verify_requirement >= VERIFY_REQUIRED)
+       {
+       gnutls_alert_send(state->session,
+         GNUTLS_AL_FATAL, GNUTLS_A_BAD_CERTIFICATE);
+       return FALSE;
+       }
+      return TRUE;
       }
     }
-#endif
   state->peer_cert_verified = TRUE;
   DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=\"%s\"\n",
       state->peerdn ? state->peerdn : US"<unset>");
@@ -1513,6 +1536,54 @@ return 0;
 #endif
 
 
+#ifdef EXPERIMENTAL_EVENT
+/*
+We use this callback to get observability and detail-level control
+for an exim TLS connection (either direction), raising a tls:cert event
+for each cert in the chain presented by the peer.  Any event
+can deny verification.
+
+Return 0 for the handshake to continue or non-zero to terminate.
+*/
+
+static int
+verify_cb(gnutls_session_t session)
+{
+const gnutls_datum * cert_list;
+unsigned int cert_list_size = 0;
+gnutls_x509_crt_t crt;
+int rc;
+uschar * yield;
+exim_gnutls_state_st * state = gnutls_session_get_ptr(session);
+
+cert_list = gnutls_certificate_get_peers(session, &cert_list_size);
+if (cert_list)
+  while (cert_list_size--)
+  {
+  rc = import_cert(&cert_list[cert_list_size], &crt);
+  if (rc != GNUTLS_E_SUCCESS)
+    {
+    DEBUG(D_tls) debug_printf("TLS: peer cert problem: depth %d: %s\n",
+      cert_list_size, gnutls_strerror(rc));
+    break;
+    }
+
+  state->tlsp->peercert = crt;
+  if ((yield = event_raise(state->event_action,
+             US"tls:cert", string_sprintf("%d", cert_list_size))))
+    {
+    log_write(0, LOG_MAIN,
+             "SSL verify denied by event-action: depth=%d: %s",
+             cert_list_size, yield);
+    return 1;                     /* reject */
+    }
+  state->tlsp->peercert = NULL;
+  }
+
+return 0;
+}
+
+#endif
 
 
 
@@ -1589,6 +1660,15 @@ else
   gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_IGNORE);
   }
 
+#ifdef EXPERIMENTAL_EVENT
+if (event_action)
+  {
+  state->event_action = event_action;
+  gnutls_session_set_ptr(state->session, state);
+  gnutls_certificate_set_verify_function(state->x509_cred, verify_cb);
+  }
+#endif
+
 /* Register SNI handling; always, even if not in tls_certificate, so that the
 expansion variable $tls_sni is always available. */
 
@@ -1685,6 +1765,20 @@ return OK;
 
 
 
+static void
+tls_client_setup_hostname_checks(host_item * host, exim_gnutls_state_st * state,
+  smtp_transport_options_block * ob)
+{
+if (verify_check_given_host(&ob->tls_verify_cert_hostnames, host) == OK)
+  {
+  state->exp_tls_verify_cert_hostnames = host->name;
+  DEBUG(D_tls)
+    debug_printf("TLS: server cert verification includes hostname: \"%s\".\n",
+                   state->exp_tls_verify_cert_hostnames);
+  }
+}
+
+
 /*************************************************
 *    Start a TLS session in a client             *
 *************************************************/
@@ -1695,7 +1789,7 @@ Arguments:
   fd                the fd of the connection
   host              connected host (for messages)
   addr              the first address (not used)
-  ob                smtp transport options
+  tb                transport (always smtp)
 
 Returns:            OK/DEFER/FAIL (because using common functions),
                     but for a client, DEFER and FAIL have the same meaning
@@ -1704,18 +1798,22 @@ Returns:            OK/DEFER/FAIL (because using common functions),
 int
 tls_client_start(int fd, host_item *host,
     address_item *addr ARG_UNUSED,
-    void *v_ob)
+    transport_instance *tb
+#ifdef EXPERIMENTAL_DANE
+    , dne_answer * unused_tlsa_dnsa
+#endif
+    )
 {
-smtp_transport_options_block *ob = v_ob;
+smtp_transport_options_block *ob =
+  (smtp_transport_options_block *)tb->options_block;
 int rc;
 const char *error;
 exim_gnutls_state_st *state = NULL;
 #ifndef DISABLE_OCSP
-BOOL require_ocsp = verify_check_this_host(&ob->hosts_require_ocsp,
-  NULL, host->name, host->address, NULL) == OK;
+BOOL require_ocsp =
+  verify_check_given_host(&ob->hosts_require_ocsp, host) == OK;
 BOOL request_ocsp = require_ocsp ? TRUE
-  : verify_check_this_host(&ob->hosts_request_ocsp,
-      NULL, host->name, host->address, NULL) == OK;
+  : verify_check_given_host(&ob->hosts_request_ocsp, host) == OK;
 #endif
 
 DEBUG(D_tls) debug_printf("initialising GnuTLS as a client on fd %d\n", fd);
@@ -1746,39 +1844,22 @@ if ((rc = tls_init(host, ob->tls_certificate, ob->tls_privatekey,
 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
+if (  (  state->exp_tls_verify_certificates
+      && !ob->tls_verify_hosts
+      && !ob->tls_try_verify_hosts
+      )
+    || verify_check_given_host(&ob->tls_verify_hosts, host) == OK
    )
   {
-#ifdef EXPERIMENTAL_CERTNAMES
-  if (ob->tls_verify_cert_hostnames)
-    {
-    DEBUG(D_tls)
-      debug_printf("TLS: server cert incl. hostname verification required.\n");
-    state->verify_requirement = VERIFY_WITHHOST;
-    if (!expand_check(ob->tls_verify_cert_hostnames,
-                     US"tls_verify_cert_hostnames",
-                     &state->exp_tls_verify_cert_hostnames))
-      return FAIL;
-    if (state->exp_tls_verify_cert_hostnames)
-      DEBUG(D_tls) debug_printf("Cert hostname to check: \"%s\"\n",
-                     state->exp_tls_verify_cert_hostnames);
-    }
-  else
-#endif
-    {
-    DEBUG(D_tls)
-      debug_printf("TLS: server certificate verification required.\n");
-    state->verify_requirement = VERIFY_REQUIRED;
-    }
+  tls_client_setup_hostname_checks(host, state, ob);
+  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)
+else if (verify_check_given_host(&ob->tls_try_verify_hosts, host) == OK)
   {
+  tls_client_setup_hostname_checks(host, state, ob);
   DEBUG(D_tls)
     debug_printf("TLS: server certificate verification optional.\n");
   state->verify_requirement = VERIFY_OPTIONAL;
@@ -1805,6 +1886,15 @@ if (request_ocsp)
   }
 #endif
 
+#ifdef EXPERIMENTAL_EVENT
+if (tb->event_action)
+  {
+  state->event_action = tb->event_action;
+  gnutls_session_set_ptr(state->session, state);
+  gnutls_certificate_set_verify_function(state->x509_cred, verify_cb);
+  }
+#endif
+
 gnutls_transport_set_ptr(state->session, (gnutls_transport_ptr)(long) fd);
 state->fd_in = fd;
 state->fd_out = fd;