From 3f7eeb86e15557a030b86e90d62708e96d68c023 Mon Sep 17 00:00:00 2001 From: Phil Pennock Date: Tue, 8 May 2012 08:20:33 -0700 Subject: [PATCH] OCSP Stapling support, under EXPERIMENTAL_OCSP. OpenSSL only. --- doc/doc-txt/ChangeLog | 2 + doc/doc-txt/NewStuff | 8 + doc/doc-txt/experimental-spec.txt | 59 +++++++ src/src/EDITME | 5 + src/src/config.h.defaults | 5 +- src/src/globals.c | 3 + src/src/globals.h | 3 + src/src/readconf.c | 3 + src/src/tls-openssl.c | 259 ++++++++++++++++++++++++++++-- 9 files changed, 333 insertions(+), 14 deletions(-) diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index d202cf16b..7c6ce246f 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -91,6 +91,8 @@ PP/20 Revert part of NM/04, it broke log_path containing %D expansions. PP/21 Defaulting "accept_8bitmime" to true, not false. +PP/22 Added EXPERIMENTAL_OCSP for OpenSSL. + Exim version 4.77 ----------------- diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index 1c8190597..96839cde6 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -62,6 +62,14 @@ Version 4.78 Those who disagree, or know that they are talking to mail servers that, even today, are not 8-bit clean, need to turn off this option. + 9. With OpenSSL, if built with EXPERIMENTAL_OCSP, a new option tls_ocsp_file + is now available. If the contents of the file are valid, then Exim will + send that back in response to a TLS status request; this is OCSP Stapling. + Exim will not maintain the contents of the file in any way: administrators + are responsible for ensuring that it is up-to-date. + + See "experimental-spec.txt" for more details. + Version 4.77 ------------ diff --git a/doc/doc-txt/experimental-spec.txt b/doc/doc-txt/experimental-spec.txt index 1d290c26b..0073b07af 100644 --- a/doc/doc-txt/experimental-spec.txt +++ b/doc/doc-txt/experimental-spec.txt @@ -6,6 +6,65 @@ about experimenatal features, all of which are unstable and liable to incompatibile change. +OCSP Stapling support +-------------------------------------------------------------- + +X509 PKI certificates expire and can be revoked; to handle this, the +clients need some way to determine if a particular certificate, from a +particular Certificate Authority (CA), is still valid. There are three +main ways to do so. + +The simplest way is to serve up a Certificate Revocation List (CRL) with +an ordinary web-server, regenerating the CRL before it expires. The +downside is that clients have to periodically re-download a potentially +huge file from every certificate authority it knows of. + +The way with most moving parts at query time is Online Certificate +Status Protocol (OCSP), where the client verifies the certificate +against an OCSP server run by the CA. This lets the CA track all +usage of the certs. This requires running software with access to the +private key of the CA, to sign the responses to the OCSP queries. OCSP +is based on HTTP and can be proxied accordingly. + +The only widespread OCSP server implementation (known to this writer) +comes as part of OpenSSL and aborts on an invalid request, such as +connecting to the port and then disconnecting. This requires +re-entering the passphrase each time some random client does this. + +The third way is OCSP Stapling; in this, the server using a certificate +issued by the CA periodically requests an OCSP proof of validity from +the OCSP server, then serves it up inline as part of the TLS +negotiation. This approach adds no extra round trips, does not let the +CA track users, scales well with number of certs issued by the CA and is +resilient to temporary OCSP server failures, as long as the server +starts retrying to fetch an OCSP proof some time before its current +proof expires. The downside is that it requires server support. + +If Exim is built with EXPERIMENTAL_OCSP and it was built with OpenSSL, +then it gains one new option: "tls_ocsp_file". + +The file specified therein is expected to be in DER format, and contain +an OCSP proof. Exim will serve it as part of the TLS handshake. This +option will be re-expanded for SNI, if the tls_certificate option +contains $tls_sni, as per other TLS options. + +Exim does not at this time implement any support for fetching a new OCSP +proof. The burden is on the administrator to handle this, outside of +Exim. The file specified should be replaced atomically, so that the +contents are always valid. Exim will expand the "tls_ocsp_file" option +on each connection, so a new file will be handled transparently on the +next connection. + +Exim will check for a validity next update timestamp in the OCSP proof; +if not present, or if the proof has expired, it will be ignored. + +At this point in time, we're gathering feedback on use, to determine if +it's worth adding complexity to the Exim daemon to periodically re-fetch +OCSP files and somehow handling multiple files. + + + + Brightmail AntiSpam (BMI) suppport -------------------------------------------------------------- diff --git a/src/src/EDITME b/src/src/EDITME index f247f44a9..f4e788ae5 100644 --- a/src/src/EDITME +++ b/src/src/EDITME @@ -439,6 +439,11 @@ EXIM_MONITOR=eximon.bin # CFLAGS += -I/opt/brightmail/bsdk-6.0/include # LDFLAGS += -lxml2_single -lbmiclient_single -L/opt/brightmail/bsdk-6.0/lib +# Uncomment the following line to add OCSP stapling support in TLS, if Exim +# was built using OpenSSL. + +# EXPERIMENTAL_OCSP=yes + ############################################################################### diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults index c082b9269..a5e12d2ab 100644 --- a/src/src/config.h.defaults +++ b/src/src/config.h.defaults @@ -158,10 +158,11 @@ it's a default value. */ #define WITH_OLD_CLAMAV_STREAM /* EXPERIMENTAL features */ -#define EXPERIMENTAL_SPF -#define EXPERIMENTAL_SRS #define EXPERIMENTAL_BRIGHTMAIL #define EXPERIMENTAL_DCC +#define EXPERIMENTAL_OCSP +#define EXPERIMENTAL_SPF +#define EXPERIMENTAL_SRS /* Things that are not routinely changed but are nevertheless configurable just in case. */ diff --git a/src/src/globals.c b/src/src/globals.c index af0c14b02..5ea432912 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -112,6 +112,9 @@ uschar *tls_advertise_hosts = NULL; /* This is deliberate */ uschar *tls_certificate = NULL; uschar *tls_crl = NULL; uschar *tls_dhparam = NULL; +#if defined(EXPERIMENTAL_OCSP) && !defined(USE_GNUTLS) +uschar *tls_ocsp_file = NULL; +#endif BOOL tls_offered = FALSE; uschar *tls_privatekey = NULL; BOOL tls_remember_esmtp = FALSE; diff --git a/src/src/globals.h b/src/src/globals.h index f9540785c..ec19d0a23 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -94,6 +94,9 @@ extern uschar *tls_certificate; /* Certificate file */ extern uschar *tls_channelbinding_b64; /* string of base64 channel binding */ extern uschar *tls_crl; /* CRL File */ extern uschar *tls_dhparam; /* DH param file */ +#if defined(EXPERIMENTAL_OCSP) && !defined(USE_GNUTLS) +extern uschar *tls_ocsp_file; /* OCSP stapling proof file */ +#endif extern BOOL tls_offered; /* Server offered TLS */ extern uschar *tls_privatekey; /* Private key file */ extern BOOL tls_remember_esmtp; /* For YAEB */ diff --git a/src/src/readconf.c b/src/src/readconf.c index b35811e48..badb6a276 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -415,6 +415,9 @@ static optionlist optionlist_config[] = { { "tls_certificate", opt_stringptr, &tls_certificate }, { "tls_crl", opt_stringptr, &tls_crl }, { "tls_dhparam", opt_stringptr, &tls_dhparam }, +#if defined(EXPERIMENTAL_OCSP) && !defined(USE_GNUTLS) + { "tls_ocsp_file", opt_stringptr, &tls_ocsp_file }, +#endif { "tls_on_connect_ports", opt_stringptr, &tls_on_connect_ports }, { "tls_privatekey", opt_stringptr, &tls_privatekey }, { "tls_remember_esmtp", opt_bool, &tls_remember_esmtp }, diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c index e609670ee..9ead7945d 100644 --- a/src/src/tls-openssl.c +++ b/src/src/tls-openssl.c @@ -20,6 +20,14 @@ functions from the OpenSSL library. */ #include #include #include +#ifdef EXPERIMENTAL_OCSP +#include +#endif + +#ifdef EXPERIMENTAL_OCSP +#define EXIM_OCSP_SKEW_SECONDS (300L) +#define EXIM_OCSP_MAX_AGE (-1L) +#endif /* Structure for collecting random data for seeding. */ @@ -48,6 +56,11 @@ static BOOL reexpand_tls_files_for_sni = FALSE; typedef struct tls_ext_ctx_cb { uschar *certificate; uschar *privatekey; +#ifdef EXPERIMENTAL_OCSP + uschar *ocsp_file; + uschar *ocsp_file_expanded; + OCSP_RESPONSE *ocsp_response; +#endif uschar *dhparam; /* these are cached from first expand */ uschar *server_cipher_list; @@ -63,6 +76,12 @@ tls_ext_ctx_cb *static_cbinfo = NULL; static int setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host, BOOL optional); +/* Callbacks */ +static int tls_servername_cb(SSL *s, int *ad ARG_UNUSED, void *arg); +#ifdef EXPERIMENTAL_OCSP +static int tls_stapling_cb(SSL *s, void *arg); +#endif + /************************************************* * Handle TLS error * @@ -298,6 +317,131 @@ return yield; +#ifdef EXPERIMENTAL_OCSP +/************************************************* +* Load OCSP information into state * +*************************************************/ + +/* Called to load the OCSP response from the given file into memory, once +caller has determined this is needed. Checks validity. Debugs a message +if invalid. + +ASSUMES: single response, for single cert. + +Arguments: + sctx the SSL_CTX* to update + cbinfo various parts of session state + expanded the filename putatively holding an OCSP response + +*/ + +static void +ocsp_load_response(SSL_CTX *sctx, + tls_ext_ctx_cb *cbinfo, + const uschar *expanded) +{ +BIO *bio; +OCSP_RESPONSE *resp; +OCSP_BASICRESP *basic_response; +OCSP_SINGLERESP *single_response; +ASN1_GENERALIZEDTIME *rev, *thisupd, *nextupd; +X509_STORE *store; +unsigned long verify_flags; +int status, reason, i; + +cbinfo->ocsp_file_expanded = string_copy(expanded); +if (cbinfo->ocsp_response) + { + OCSP_RESPONSE_free(cbinfo->ocsp_response); + cbinfo->ocsp_response = NULL; + } + +bio = BIO_new_file(CS cbinfo->ocsp_file_expanded, "rb"); +if (!bio) + { + DEBUG(D_tls) debug_printf("Failed to open OCSP response file \"%s\"\n", + cbinfo->ocsp_file_expanded); + return; + } + +resp = d2i_OCSP_RESPONSE_bio(bio, NULL); +BIO_free(bio); +if (!resp) + { + DEBUG(D_tls) debug_printf("Error reading OCSP response.\n"); + return; + } + +status = OCSP_response_status(resp); +if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) + { + DEBUG(D_tls) debug_printf("OCSP response not valid: %s (%d)\n", + OCSP_response_status_str(status), status); + return; + } + +basic_response = OCSP_response_get1_basic(resp); +if (!basic_response) + { + DEBUG(D_tls) + debug_printf("OCSP response parse error: unable to extract basic response.\n"); + return; + } + +store = SSL_CTX_get_cert_store(sctx); +verify_flags = OCSP_NOVERIFY; /* check sigs, but not purpose */ + +/* May need to expose ability to adjust those flags? +OCSP_NOSIGS OCSP_NOVERIFY OCSP_NOCHAIN OCSP_NOCHECKS OCSP_NOEXPLICIT +OCSP_TRUSTOTHER OCSP_NOINTERN */ + +i = OCSP_basic_verify(basic_response, NULL, store, verify_flags); +if (i <= 0) + { + DEBUG(D_tls) { + ERR_error_string(ERR_get_error(), ssl_errstring); + debug_printf("OCSP response verify failure: %s\n", US ssl_errstring); + } + return; + } + +/* Here's the simplifying assumption: there's only one response, for the +one certificate we use, and nothing for anything else in a chain. If this +proves false, we need to extract a cert id from our issued cert +(tls_certificate) and use that for OCSP_resp_find_status() (which finds the +right cert in the stack and then calls OCSP_single_get0_status()). + +I'm hoping to avoid reworking a bunch more of how we handle state here. */ +single_response = OCSP_resp_get0(basic_response, 0); +if (!single_response) + { + DEBUG(D_tls) + debug_printf("Unable to get first response from OCSP basic response.\n"); + return; + } + +status = OCSP_single_get0_status(single_response, &reason, &rev, &thisupd, &nextupd); +/* how does this status differ from the one above? */ +if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) + { + DEBUG(D_tls) debug_printf("OCSP response not valid (take 2): %s (%d)\n", + OCSP_response_status_str(status), status); + return; + } + +if (!OCSP_check_validity(thisupd, nextupd, EXIM_OCSP_SKEW_SECONDS, EXIM_OCSP_MAX_AGE)) + { + DEBUG(D_tls) debug_printf("OCSP status invalid times.\n"); + return; + } + +cbinfo->ocsp_response = resp; +} +#endif + + + + /************************************************* * Expand key and cert file specs * *************************************************/ @@ -314,7 +458,7 @@ Returns: OK/DEFER/FAIL */ static int -tls_expand_session_files(SSL_CTX *sctx, const tls_ext_ctx_cb *cbinfo) +tls_expand_session_files(SSL_CTX *sctx, tls_ext_ctx_cb *cbinfo) { uschar *expanded; @@ -352,6 +496,27 @@ if (expanded != NULL && *expanded != 0) "SSL_CTX_use_PrivateKey_file file=%s", expanded), cbinfo->host, NULL); } +#ifdef EXPERIMENTAL_OCSP +if (cbinfo->ocsp_file != NULL) + { + if (!expand_check(cbinfo->ocsp_file, US"tls_ocsp_file", &expanded)) + return DEFER; + + if (expanded != NULL && *expanded != 0) + { + DEBUG(D_tls) debug_printf("tls_ocsp_file %s\n", expanded); + if (cbinfo->ocsp_file_expanded && + (Ustrcmp(expanded, cbinfo->ocsp_file_expanded) == 0)) + { + DEBUG(D_tls) + debug_printf("tls_ocsp_file value unchanged, using existing values.\n"); + } else { + ocsp_load_response(sctx, cbinfo, expanded); + } + } + } +#endif + return OK; } @@ -375,15 +540,11 @@ Arguments: Returns: SSL_TLSEXT_ERR_{OK,ALERT_WARNING,ALERT_FATAL,NOACK} */ -static int -tls_servername_cb(SSL *s, int *ad ARG_UNUSED, void *arg); -/* pre-declared for SSL_CTX_set_tlsext_servername_callback call within func */ - static int tls_servername_cb(SSL *s, int *ad ARG_UNUSED, void *arg) { const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); -const tls_ext_ctx_cb *cbinfo = (tls_ext_ctx_cb *) arg; +tls_ext_ctx_cb *cbinfo = (tls_ext_ctx_cb *) arg; int rc; int old_pool = store_pool; @@ -424,11 +585,20 @@ SSL_CTX_set_tlsext_servername_callback(ctx_sni, tls_servername_cb); SSL_CTX_set_tlsext_servername_arg(ctx_sni, cbinfo); if (cbinfo->server_cipher_list) SSL_CTX_set_cipher_list(ctx_sni, CS cbinfo->server_cipher_list); +#ifdef EXPERIMENTAL_OCSP +if (cbinfo->ocsp_file) + { + SSL_CTX_set_tlsext_status_cb(ctx_sni, tls_stapling_cb); + SSL_CTX_set_tlsext_status_arg(ctx, cbinfo); + } +#endif -rc = tls_expand_session_files(ctx_sni, cbinfo); +rc = setup_certs(ctx_sni, tls_verify_certificates, tls_crl, NULL, FALSE); if (rc != OK) return SSL_TLSEXT_ERR_NOACK; -rc = setup_certs(ctx_sni, tls_verify_certificates, tls_crl, NULL, FALSE); +/* do this after setup_certs, because this can require the certs for verifying +OCSP information. */ +rc = tls_expand_session_files(ctx_sni, cbinfo); if (rc != OK) return SSL_TLSEXT_ERR_NOACK; DEBUG(D_tls) debug_printf("Switching SSL context.\n"); @@ -440,6 +610,45 @@ return SSL_TLSEXT_ERR_OK; +#ifdef EXPERIMENTAL_OCSP +/************************************************* +* Callback to handle OCSP Stapling * +*************************************************/ + +/* Called when acting as server during the TLS session setup if the client +requests OCSP information with a Certificate Status Request. + +Documentation via openssl s_server.c and the Apache patch from the OpenSSL +project. + +*/ + +static int +tls_stapling_cb(SSL *s, void *arg) +{ +const tls_ext_ctx_cb *cbinfo = (tls_ext_ctx_cb *) arg; +uschar *response_der; +int response_der_len; + +DEBUG(D_tls) debug_printf("Received TLS status request (OCSP stapling); %s response.\n", + cbinfo->ocsp_response ? "have" : "lack"); +if (!cbinfo->ocsp_response) + return SSL_TLSEXT_ERR_NOACK; + +response_der = NULL; +response_der_len = i2d_OCSP_RESPONSE(cbinfo->ocsp_response, &response_der); +if (response_der_len <= 0) + return SSL_TLSEXT_ERR_NOACK; + +SSL_set_tlsext_status_ocsp_resp(ssl, response_der, response_der_len); +return SSL_TLSEXT_ERR_OK; +} + +#endif /* EXPERIMENTAL_OCSP */ + + + + /************************************************* * Initialize for TLS * *************************************************/ @@ -459,7 +668,11 @@ Returns: OK/DEFER/FAIL static int tls_init(host_item *host, uschar *dhparam, uschar *certificate, - uschar *privatekey, address_item *addr) + uschar *privatekey, +#ifdef EXPERIMENTAL_OCSP + uschar *ocsp_file, +#endif + address_item *addr) { long init_options; int rc; @@ -469,6 +682,9 @@ tls_ext_ctx_cb *cbinfo; cbinfo = store_malloc(sizeof(tls_ext_ctx_cb)); cbinfo->certificate = certificate; cbinfo->privatekey = privatekey; +#ifdef EXPERIMENTAL_OCSP +cbinfo->ocsp_file = ocsp_file; +#endif cbinfo->dhparam = dhparam; cbinfo->host = host; @@ -546,7 +762,7 @@ else if (!init_dh(dhparam, host)) return DEFER; -/* Set up certificate and key */ +/* Set up certificate and key (and perhaps OCSP info) */ rc = tls_expand_session_files(ctx, cbinfo); if (rc != OK) return rc; @@ -555,6 +771,17 @@ if (rc != OK) return rc; #if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT) if (host == NULL) { +#ifdef EXPERIMENTAL_OCSP + /* We check ocsp_file, not ocsp_response, because we care about if + the option exists, not what the current expansion might be, as SNI might + change the certificate and OCSP file in use between now and the time the + callback is invoked. */ + if (cbinfo->ocsp_file) + { + SSL_CTX_set_tlsext_status_cb(ctx, tls_stapling_cb); + SSL_CTX_set_tlsext_status_arg(ctx, cbinfo); + } +#endif /* We always do this, so that $tls_sni is available even if not used in tls_certificate */ SSL_CTX_set_tlsext_servername_callback(ctx, tls_servername_cb); @@ -811,7 +1038,11 @@ if (tls_active >= 0) /* Initialize the SSL library. If it fails, it will already have logged the error. */ -rc = tls_init(NULL, tls_dhparam, tls_certificate, tls_privatekey, NULL); +rc = tls_init(NULL, tls_dhparam, tls_certificate, tls_privatekey, +#ifdef EXPERIMENTAL_OCSP + tls_ocsp_file, +#endif + NULL); if (rc != OK) return rc; cbinfo = static_cbinfo; @@ -978,7 +1209,11 @@ uschar *expciphers; X509* server_cert; int rc; -rc = tls_init(host, dhparam, certificate, privatekey, addr); +rc = tls_init(host, dhparam, certificate, privatekey, +#ifdef EXPERIMENTAL_OCSP + NULL, +#endif + addr); if (rc != OK) return rc; tls_certificate_verified = FALSE; -- 2.30.2