GnuTLS: multiple server certs, OCSP stapling. Bug 2092
[users/jgh/exim.git] / src / src / tls-gnu.c
index d54c560f708ee9d4d6fd0458a9ee10c2c71a52a3..e69fc8bee83d358669f653c32431497cf0144384 100644 (file)
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* Copyright (c) University of Cambridge 1995 - 2017 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Copyright (c) Phil Pennock 2012 */
@@ -63,6 +63,9 @@ require current GnuTLS, then we'll drop support for the ancient libraries).
 #if GNUTLS_VERSION_NUMBER >= 0x030109
 # define SUPPORT_CORK
 #endif
+#if GNUTLS_VERSION_NUMBER >= 0x030506 && !defined(DISABLE_OCSP)
+# define SUPPORT_SRV_OCSP_STACK
+#endif
 
 #ifndef DISABLE_OCSP
 # include <gnutls/ocsp.h>
@@ -123,7 +126,6 @@ typedef struct exim_gnutls_state {
   uschar *exp_tls_verify_certificates;
   uschar *exp_tls_crl;
   uschar *exp_tls_require_ciphers;
-  uschar *exp_tls_ocsp_file;
   const uschar *exp_tls_verify_cert_hostnames;
 #ifndef DISABLE_EVENT
   uschar *event_action;
@@ -166,7 +168,6 @@ static const exim_gnutls_state_st exim_gnutls_state_init = {
   .exp_tls_verify_certificates = NULL,
   .exp_tls_crl =       NULL,
   .exp_tls_require_ciphers = NULL,
-  .exp_tls_ocsp_file = NULL,
   .exp_tls_verify_cert_hostnames = NULL,
 #ifndef DISABLE_EVENT
   .event_action =      NULL,
@@ -221,7 +222,8 @@ static BOOL gnutls_buggy_ocsp = FALSE;
 
 /* Set this to control gnutls_global_set_log_level(); values 0 to 9 will setup
 the library logging; a value less than 0 disables the calls to set up logging
-callbacks. */
+callbacks.  Possibly GNuTLS also looks for an environment variable
+"GNUTLS_DEBUG_LEVEL". */
 #ifndef EXIM_GNUTLS_LIBRARY_LOG_LEVEL
 # define EXIM_GNUTLS_LIBRARY_LOG_LEVEL -1
 #endif
@@ -237,8 +239,8 @@ before, for now. */
 # define EXIM_SERVER_DH_BITS_PRE2_12 1024
 #endif
 
-#define exim_gnutls_err_check(Label) do { \
-  if (rc != GNUTLS_E_SUCCESS) \
+#define exim_gnutls_err_check(rc, Label) do { \
+  if ((rc) != GNUTLS_E_SUCCESS) \
     return tls_error((Label), gnutls_strerror(rc), host, errstr); \
   } while (0)
 
@@ -506,7 +508,7 @@ host_item *host = NULL; /* dummy for macros */
 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");
+exim_gnutls_err_check(rc, US"gnutls_dh_params_init");
 
 m.data = NULL;
 m.size = 0;
@@ -542,7 +544,7 @@ else
 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");
+  exim_gnutls_err_check(rc, US"gnutls_dh_params_import_pkcs3");
   DEBUG(D_tls) debug_printf("Loaded fixed standard D-H parameters\n");
   return OK;
   }
@@ -625,7 +627,7 @@ if ((fd = Uopen(filename, O_RDONLY, 0)) >= 0)
 
   rc = gnutls_dh_params_import_pkcs3(dh_server_params, &m, GNUTLS_X509_FMT_PEM);
   free(m.data);
-  exim_gnutls_err_check(US"gnutls_dh_params_import_pkcs3");
+  exim_gnutls_err_check(rc, US"gnutls_dh_params_import_pkcs3");
   DEBUG(D_tls) debug_printf("read D-H parameters from file \"%s\"\n", filename);
   }
 
@@ -682,7 +684,7 @@ if (rc < 0)
     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");
+  exim_gnutls_err_check(rc, US"gnutls_dh_params_generate2");
 
   /* gnutls_dh_params_export_pkcs3() will tell us the exact size, every time,
   and I confirmed that a NULL call to get the size first is how the GnuTLS
@@ -693,7 +695,7 @@ if (rc < 0)
   rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM,
       m.data, &sz);
   if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
-    exim_gnutls_err_check(US"gnutls_dh_params_export_pkcs3(NULL) sizing");
+    exim_gnutls_err_check(rc, US"gnutls_dh_params_export_pkcs3(NULL) sizing");
   m.size = sz;
   if (!(m.data = malloc(m.size)))
     return tls_error(US"memory allocation failed", strerror(errno), NULL, errstr);
@@ -704,7 +706,7 @@ if (rc < 0)
   if (rc != GNUTLS_E_SUCCESS)
     {
     free(m.data);
-    exim_gnutls_err_check(US"gnutls_dh_params_export_pkcs3() real");
+    exim_gnutls_err_check(rc, US"gnutls_dh_params_export_pkcs3() real");
     }
   m.size = sz; /* shrink by 1, probably */
 
@@ -804,6 +806,27 @@ err:
 
 
 
+/* Add certificate and key, from files.
+
+Return:
+  Zero or negative: good.  Negate value for certificate index if < 0.
+  Greater than zero: FAIL or DEFER code.
+*/
+
+static int
+tls_add_certfile(exim_gnutls_state_st * state, const host_item * host,
+  uschar * certfile, uschar * keyfile, uschar ** errstr)
+{
+int rc = gnutls_certificate_set_x509_key_file(state->x509_cred,
+    CS certfile, CS keyfile, GNUTLS_X509_FMT_PEM);
+if (rc < 0)
+  return tls_error(
+    string_sprintf("cert/key setup: cert=%s key=%s", certfile, keyfile),
+    gnutls_strerror(rc), host, errstr);
+return -rc;
+}
+
+
 /*************************************************
 *       Variables re-expanded post-SNI           *
 *************************************************/
@@ -824,7 +847,7 @@ Returns:          OK/DEFER/FAIL
 */
 
 static int
-tls_expand_session_files(exim_gnutls_state_st *state, uschar ** errstr)
+tls_expand_session_files(exim_gnutls_state_st * state, uschar ** errstr)
 {
 struct stat statbuf;
 int rc;
@@ -839,11 +862,11 @@ int cert_count;
 if (!host)     /* server */
   if (!state->received_sni)
     {
-    if (state->tls_certificate &&
-        (Ustrstr(state->tls_certificate, US"tls_sni") ||
-         Ustrstr(state->tls_certificate, US"tls_in_sni") ||
-         Ustrstr(state->tls_certificate, US"tls_out_sni")
-       ))
+    if (  state->tls_certificate
+       && (  Ustrstr(state->tls_certificate, US"tls_sni")
+         || Ustrstr(state->tls_certificate, US"tls_in_sni")
+         || Ustrstr(state->tls_certificate, US"tls_out_sni")
+       )  )
       {
       DEBUG(D_tls) debug_printf("We will re-expand TLS session files if we receive SNI.\n");
       state->trigger_sni_changes = TRUE;
@@ -859,7 +882,11 @@ if (!host) /* server */
     }
 
 rc = gnutls_certificate_allocate_credentials(&state->x509_cred);
-exim_gnutls_err_check(US"gnutls_certificate_allocate_credentials");
+exim_gnutls_err_check(rc, US"gnutls_certificate_allocate_credentials");
+
+#ifdef SUPPORT_SRV_OCSP_STACK
+gnutls_certificate_set_flags(state->x509_cred, GNUTLS_CERTIFICATE_API_V2);
+#endif
 
 /* remember: expand_check_tlsvar() is expand_check() but fiddling with
 state members, assuming consistent naming; and expand_check() returns
@@ -910,44 +937,81 @@ if (state->exp_tls_certificate && *state->exp_tls_certificate)
       DEBUG(D_tls) debug_printf("TLS SNI: have a changed cert/key pair.\n");
       }
 
-  rc = gnutls_certificate_set_x509_key_file(state->x509_cred,
-      CS state->exp_tls_certificate, CS state->exp_tls_privatekey,
-      GNUTLS_X509_FMT_PEM);
-  exim_gnutls_err_check(
-      string_sprintf("cert/key setup: cert=%s key=%s",
-        state->exp_tls_certificate, state->exp_tls_privatekey));
-  DEBUG(D_tls) debug_printf("TLS: cert/key registered\n");
-  } /* tls_certificate */
+  if (!host)   /* server */
+    {
+    const uschar * clist = state->exp_tls_certificate;
+    const uschar * klist = state->exp_tls_privatekey;
+    const uschar * olist;
+    int csep = 0, ksep = 0, osep = 0, cnt = 0;
+    uschar * cfile, * kfile, * ofile;
 
+#ifndef DISABLE_OCSP
+    if (!expand_check(tls_ocsp_file, US"tls_ocsp_file", &ofile, errstr))
+      return DEFER;
+    olist = ofile;
+#endif
 
-/* Set the OCSP stapling server info */
+    while (cfile = string_nextinlist(&clist, &csep, NULL, 0))
+
+      if (!(kfile = string_nextinlist(&klist, &ksep, NULL, 0)))
+       return tls_error(US"cert/key setup: out of keys", NULL, host, errstr);
+      else if (0 < (rc = tls_add_certfile(state, host, cfile, kfile, errstr)))
+       return rc;
+      else
+       {
+       int gnutls_cert_index = -rc;
+       DEBUG(D_tls) debug_printf("TLS: cert/key %s registered\n", cfile);
+
+       /* Set the OCSP stapling server info */
 
 #ifndef DISABLE_OCSP
-if (  !host    /* server */
-   && tls_ocsp_file
-   )
-  {
-  if (gnutls_buggy_ocsp)
-    {
-    DEBUG(D_tls) debug_printf("GnuTLS library is buggy for OCSP; avoiding\n");
+       if (tls_ocsp_file)
+         if (gnutls_buggy_ocsp)
+           {
+           DEBUG(D_tls)
+             debug_printf("GnuTLS library is buggy for OCSP; avoiding\n");
+           }
+         else if ((ofile = string_nextinlist(&olist, &osep, NULL, 0)))
+           {
+           /* 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.  */
+
+# ifdef SUPPORT_SRV_OCSP_STACK
+           rc = gnutls_certificate_set_ocsp_status_request_function2(
+             state->x509_cred, gnutls_cert_index,
+             server_ocsp_stapling_cb, ofile);
+
+           exim_gnutls_err_check(rc,
+             US"gnutls_certificate_set_ocsp_status_request_function2");
+# else
+           if (cnt++ > 0)
+             {
+             DEBUG(D_tls)
+               debug_printf("oops; multiple OCSP files not supported\n");
+             break;
+             }
+             gnutls_certificate_set_ocsp_status_request_function(
+               state->x509_cred, server_ocsp_stapling_cb, ofile);
+# endif
+
+           DEBUG(D_tls) debug_printf("OCSP response file = %s\n", ofile);
+           }
+         else
+           DEBUG(D_tls) debug_printf("ran out of OCSP response files in list\n");
+#endif
+       }
     }
   else
     {
-    if (!expand_check(tls_ocsp_file, US"tls_ocsp_file",
-         &state->exp_tls_ocsp_file, errstr))
-      return DEFER;
-
-    /* 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);
-
-    DEBUG(D_tls) debug_printf("OCSP response file = %s\n", state->exp_tls_ocsp_file);
+    if (0 < (rc = tls_add_certfile(state, host,
+               state->exp_tls_certificate, state->exp_tls_privatekey, errstr)))
+      return rc;
+    DEBUG(D_tls) debug_printf("TLS: cert/key registered\n");
     }
-  }
-#endif
+
+  } /* tls_certificate */
 
 
 /* Set the trusted CAs file if one is provided, and then add the CRL if one is
@@ -1042,7 +1106,7 @@ else
 if (cert_count < 0)
   {
   rc = cert_count;
-  exim_gnutls_err_check(US"setting certificate trust");
+  exim_gnutls_err_check(rc, US"setting certificate trust");
   }
 DEBUG(D_tls) debug_printf("Added %d certificate authorities.\n", cert_count);
 
@@ -1055,7 +1119,7 @@ if (state->tls_crl && *state->tls_crl &&
   if (cert_count < 0)
     {
     rc = cert_count;
-    exim_gnutls_err_check(US"gnutls_certificate_set_x509_crl_file");
+    exim_gnutls_err_check(rc, US"gnutls_certificate_set_x509_crl_file");
     }
   DEBUG(D_tls) debug_printf("Processed %d CRLs.\n", cert_count);
   }
@@ -1106,7 +1170,7 @@ if (!state->host)
 /* Link the credentials to the session. */
 
 rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->x509_cred);
-exim_gnutls_err_check(US"gnutls_credentials_set");
+exim_gnutls_err_check(rc, US"gnutls_credentials_set");
 
 return OK;
 }
@@ -1196,12 +1260,12 @@ if (!exim_gnutls_base_init_done)
   if (!gnutls_allow_auto_pkcs11)
     {
     rc = gnutls_pkcs11_init(GNUTLS_PKCS11_FLAG_MANUAL, NULL);
-    exim_gnutls_err_check(US"gnutls_pkcs11_init");
+    exim_gnutls_err_check(rc, US"gnutls_pkcs11_init");
     }
 #endif
 
   rc = gnutls_global_init();
-  exim_gnutls_err_check(US"gnutls_global_init");
+  exim_gnutls_err_check(rc, US"gnutls_global_init");
 
 #if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0
   DEBUG(D_tls)
@@ -1236,7 +1300,7 @@ else
   DEBUG(D_tls) debug_printf("initialising GnuTLS server session\n");
   rc = gnutls_init(&state->session, GNUTLS_SERVER);
   }
-exim_gnutls_err_check(US"gnutls_init");
+exim_gnutls_err_check(rc, US"gnutls_init");
 
 state->host = host;
 
@@ -1271,12 +1335,12 @@ if (host)
     sz = Ustrlen(state->tlsp->sni);
     rc = gnutls_server_name_set(state->session,
         GNUTLS_NAME_DNS, state->tlsp->sni, sz);
-    exim_gnutls_err_check(US"gnutls_server_name_set");
+    exim_gnutls_err_check(rc, US"gnutls_server_name_set");
     }
   }
 else if (state->tls_sni)
   DEBUG(D_tls) debug_printf("*** PROBABLY A BUG *** " \
-      "have an SNI set for a client [%s]\n", state->tls_sni);
+      "have an SNI set for a server [%s]\n", state->tls_sni);
 
 /* This is the priority string support,
 http://www.gnutls.org/manual/html_node/Priority-Strings.html
@@ -1311,12 +1375,12 @@ if (want_default_priorities)
   p = US exim_default_gnutls_priority;
   }
 
-exim_gnutls_err_check(string_sprintf(
+exim_gnutls_err_check(rc, string_sprintf(
       "gnutls_priority_init(%s) failed at offset %ld, \"%.6s..\"",
       p, errpos - CS p, errpos));
 
 rc = gnutls_priority_set(state->session, state->priority_cache);
-exim_gnutls_err_check(US"gnutls_priority_set");
+exim_gnutls_err_check(rc, US"gnutls_priority_set");
 
 gnutls_db_set_cache_expiration(state->session, ssl_session_timeout);
 
@@ -1676,6 +1740,7 @@ server_ocsp_stapling_cb(gnutls_session_t session, void * ptr,
   gnutls_datum_t * ocsp_response)
 {
 int ret;
+DEBUG(D_tls) debug_printf("OCSP stapling callback: %s\n", US ptr);
 
 if ((ret = gnutls_load_file(ptr, ocsp_response)) < 0)
   {