Support optional server certificate name checking. Bug 1479
[exim.git] / src / src / tls-gnu.c
index 880aaeb14831565a9ae4b752bfe2506f687c2c72..af43686e42fde592d7711404586436264808705e 100644 (file)
@@ -60,7 +60,12 @@ Changes:
 
 /* Values for verify_requirement */
 
-enum peer_verify_requirement { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED };
+enum peer_verify_requirement
+  { VERIFY_NONE, VERIFY_OPTIONAL, VERIFY_REQUIRED
+#ifdef EXPERIMENTAL_CERTNAMES
+    ,VERIFY_WITHHOST
+#endif
+  };
 
 /* 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
@@ -95,12 +100,17 @@ typedef struct exim_gnutls_state {
   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;
+#ifdef EXPERIMENTAL_CERTNAMES
+  uschar *exp_tls_verify_cert_hostnames;
+#endif
 
   tls_support *tlsp;   /* set in tls_init() */
 
@@ -115,7 +125,10 @@ 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, NULL, NULL, NULL,
+#ifdef EXPERIMENTAL_CERTNAMES
+                                            NULL,
+#endif
   NULL,
   NULL, 0, 0, 0, 0,
 };
@@ -177,18 +190,18 @@ before, for now. */
 #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
+# 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 */
+# ifndef AVOID_GNUTLS_PKCS11
+#  define HAVE_GNUTLS_PKCS11
+# endif /* AVOID_GNUTLS_PKCS11 */
 #endif
 
 
@@ -203,6 +216,10 @@ static void exim_gnutls_logger_cb(int level, const char *message);
 
 static int exim_sni_handling_cb(gnutls_session_t session);
 
+#ifdef EXPERIMENTAL_OCSP
+static int server_ocsp_stapling_cb(gnutls_session_t session, void * ptr,
+  gnutls_datum_t * ocsp_response);
+#endif
 
 
 
@@ -289,10 +306,16 @@ tls_error(when, msg, state->host);
 *        Set various Exim expansion vars         *
 *************************************************/
 
-#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)
+#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)
 
 static int
 import_cert(const gnutls_datum * cert, gnutls_x509_crt_t * crtp)
@@ -791,18 +814,18 @@ if (  !host       /* server */
    && tls_ocsp_file
    )
   {
-  uschar * expanded;
-  int rc;
-
-  if (!expand_check(tls_ocsp_file, US"tls_ocsp_file", &expanded))
+  if (!expand_check(tls_ocsp_file, US"tls_ocsp_file",
+       &state->exp_tls_ocsp_file))
     return DEFER;
 
-  /* Lazy way; would like callback to emit debug on actual response */
+  /* 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);
 
-  rc = gnutls_certificate_set_ocsp_status_request_file(state->x509_cred,
-      expanded, 0);
-  exim_gnutls_err_check(US"gnutls_certificate_set_ocsp_status_request_file");
-  DEBUG(D_tls) debug_printf("Set OCSP response file %s\n", expanded);
+  DEBUG(D_tls) debug_printf("Set OCSP response file %s\n", &state->exp_tls_ocsp_file);
   }
 #endif
 
@@ -1215,7 +1238,7 @@ if (cert_list == NULL || cert_list_size == 0)
   {
   DEBUG(D_tls) debug_printf("TLS: no certificate from peer (%p & %d)\n",
       cert_list, cert_list_size);
-  if (state->verify_requirement == VERIFY_REQUIRED)
+  if (state->verify_requirement >= VERIFY_REQUIRED)
     return tls_error(US"certificate verification failed",
         "no certificate received from peer", state->host);
   return OK;
@@ -1227,17 +1250,23 @@ 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)
+  if (state->verify_requirement >= VERIFY_REQUIRED)
     return tls_error(US"certificate verification not possible, unhandled type",
         ctn, state->host);
   return OK;
   }
 
-#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)
+#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)
 
 rc = import_cert(&cert_list[0], &crt);
 exim_gnutls_peer_err(US"cert 0");
@@ -1301,7 +1330,9 @@ else
 /* 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))
+if (rc < 0 ||
+    verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)
+   )
   {
   state->peer_cert_verified = FALSE;
   if (!*error)
@@ -1309,21 +1340,42 @@ if (rc < 0 || verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED))
       ? "certificate revoked" : "certificate invalid";
 
   DEBUG(D_tls)
-    debug_printf("TLS certificate verification failed (%s): peerdn=%s\n",
+    debug_printf("TLS certificate verification failed (%s): peerdn=\"%s\"\n",
         *error, state->peerdn ? state->peerdn : US"<unset>");
 
-  if (state->verify_requirement == VERIFY_REQUIRED)
+  if (state->verify_requirement >= VERIFY_REQUIRED)
     {
-    gnutls_alert_send(state->session, GNUTLS_AL_FATAL, GNUTLS_A_BAD_CERTIFICATE);
+    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");
   }
+
 else
   {
+#ifdef EXPERIMENTAL_CERTNAMES
+  if (state->verify_requirement == VERIFY_WITHHOST)
+    {
+    int sep = 0;
+    uschar * list = state->exp_tls_verify_cert_hostnames;
+    uschar * name;
+    while (name = string_nextinlist(&list, &sep, NULL, 0))
+      if (gnutls_x509_crt_check_hostname(state->tlsp->peercert, CS name))
+       break;
+    if (!name)
+      {
+      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;
+      }
+    }
+#endif
   state->peer_cert_verified = TRUE;
-  DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n",
+  DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=\"%s\"\n",
       state->peerdn ? state->peerdn : US"<unset>");
   }
 
@@ -1433,6 +1485,31 @@ return 0;
 
 
 
+#ifdef EXPERIMENTAL_OCSP
+
+static int
+server_ocsp_stapling_cb(gnutls_session_t session, void * ptr,
+  gnutls_datum_t * ocsp_response)
+{
+int ret;
+
+if ((ret = gnutls_load_file(ptr, ocsp_response)) < 0)
+  {
+  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;
+  }
+
+tls_in.ocsp = OCSP_VFY_NOT_TRIED;
+return 0;
+}
+
+#endif
+
+
+
+
 
 /* ------------------------------------------------------------------------ */
 /* Exported functions */
@@ -1487,19 +1564,22 @@ optional, set up appropriately. */
 
 if (verify_check_host(&tls_verify_hosts) == OK)
   {
-  DEBUG(D_tls) debug_printf("TLS: a client certificate will be required.\n");
+  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)
   {
-  DEBUG(D_tls) debug_printf("TLS: a client certificate will be requested but not required.\n");
+  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");
+  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);
   }
@@ -1526,8 +1606,8 @@ if (!state->tlsp->on_connect)
 that the GnuTLS library doesn't. */
 
 gnutls_transport_set_ptr2(state->session,
-    (gnutls_transport_ptr)fileno(smtp_in),
-    (gnutls_transport_ptr)fileno(smtp_out));
+    (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);
 
@@ -1610,17 +1690,7 @@ Arguments:
   fd                the fd of the connection
   host              connected host (for messages)
   addr              the first address (not used)
-  certificate       certificate file
-  privatekey        private key file
-  sni               TLS SNI to send to remote host
-  verify_certs      file for certificate verify
-  verify_crl        CRL for verify
-  require_ciphers   list of allowed ciphers or NULL
-  hosts_require_ocsp hosts for which to request certificate-status (OCSP)
-  dh_min_bits       minimum number of bits acceptable in server's DH prime
-  timeout           startup timeout
-  verify_hosts      mandatory client verification 
-  try_verify_hosts  optional client verification
+  ob                smtp transport options
 
 Returns:            OK/DEFER/FAIL (because using common functions),
                     but for a client, DEFER and FAIL have the same meaning
@@ -1629,83 +1699,107 @@ Returns:            OK/DEFER/FAIL (because using common functions),
 int
 tls_client_start(int fd, host_item *host,
     address_item *addr ARG_UNUSED,
-    uschar *certificate, uschar *privatekey, uschar *sni,
-    uschar *verify_certs, uschar *verify_crl,
-    uschar *require_ciphers,
-#ifdef EXPERIMENTAL_OCSP
-    uschar *hosts_require_ocsp,
-#endif
-    int dh_min_bits, int timeout,
-    uschar *verify_hosts, uschar *try_verify_hosts)
+    void *v_ob)
 {
+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(&hosts_require_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("initialising GnuTLS as a client on fd %d\n", fd);
 
-if ((rc = tls_init(host, certificate, privatekey,
-    sni, verify_certs, verify_crl, require_ciphers, &state)) != OK)
+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 (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;
-  }
+  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);
+  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);
+  }
 
 /* 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
-    && !verify_hosts
-    && !try_verify_hosts
+    && !ob->tls_verify_hosts
+    && !ob->tls_try_verify_hosts
     )
     ||
-    verify_check_host(&verify_hosts) == OK
+    verify_check_host(&ob->tls_verify_hosts) == OK
    )
   {
-  DEBUG(D_tls) debug_printf("TLS: server certificate verification required.\n");
-  state->verify_requirement = VERIFY_REQUIRED;
+#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;
+    }
   gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUIRE);
   }
-else if (verify_check_host(&try_verify_hosts) == OK)
+else if (verify_check_host(&ob->tls_try_verify_hosts) == OK)
   {
-  DEBUG(D_tls) debug_printf("TLS: server certificate verification optional.\n");
+  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");
+  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);
   }
 
 #ifdef EXPERIMENTAL_OCSP       /* since GnuTLS 3.1.3 */
-if (require_ocsp)
+if (request_ocsp)
   {
   DEBUG(D_tls) debug_printf("TLS: will request OCSP stapling\n");
-  rc = gnutls_ocsp_status_request_enable_client(state->session,
-                   NULL, 0, NULL);
-  if (rc != OK)
+  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(state->session, (gnutls_transport_ptr)fd);
+gnutls_transport_set_ptr(state->session, (gnutls_transport_ptr)(long) fd);
 state->fd_in = fd;
 state->fd_out = fd;
 
@@ -1713,7 +1807,7 @@ 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);
+alarm(ob->command_timeout);
 do
   {
   rc = gnutls_handshake(state->session);
@@ -1747,17 +1841,20 @@ if (require_ocsp)
        && (rc= gnutls_ocsp_resp_print(resp, GNUTLS_OCSP_PRINT_FULL, &printed)) == 0
        )
       {
-      fprintf(stderr, "%.4096s", printed.data);
+      debug_printf("%.4096s", printed.data);
       gnutls_free(printed.data);
       }
     else
       (void) tls_error(US"ocsp decode", gnutls_strerror(rc), state->host);
     }
 
-  fprintf(stderr, "%s: checking ocsp\n", __FUNCTION__);
   if (gnutls_ocsp_status_request_is_checked(state->session, 0) == 0)
+    {
+    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