From 7be682ca5ebd9571a01b762195b11c34cd231830 Mon Sep 17 00:00:00 2001 From: Phil Pennock Date: Fri, 4 May 2012 04:39:01 -0700 Subject: [PATCH] TLS SNI support for OpenSSL ($tls_sni) --- doc/doc-docbook/spec.xfpt | 18 +++ doc/doc-txt/ChangeLog | 3 + doc/doc-txt/NewStuff | 7 ++ src/src/daemon.c | 13 ++- src/src/expand.c | 3 + src/src/globals.c | 3 + src/src/globals.h | 3 + src/src/mytypes.h | 2 + src/src/spool_in.c | 7 ++ src/src/spool_out.c | 3 + src/src/tls-openssl.c | 233 +++++++++++++++++++++++++++++++------- 11 files changed, 253 insertions(+), 42 deletions(-) diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 016f3f075..32e24ca80 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -11888,6 +11888,24 @@ the value of the Distinguished Name of the certificate is made available in the value is retained during message delivery, except during outbound SMTP deliveries. +.new +.vitem &$tls_sni$& +.vindex "&$tls_sni$&" +.cindex "TLS" "Server Name Indication" +When a TLS session is being established, if the client sends the Server +Name Indication extension, the value will be placed in this variable. +If the variable appears in &%tls_certificate%& then this option and +&%tls_privatekey%& will be re-expanded early in the TLS session, to permit +a different certificate to be presented (and optionally a different key to be +used) to the client, based upon the value of the SNI extension. + +The value will be retained for the lifetime of the message, and not changed +during outbound SMTP. + +This is currently only available when using OpenSSL, built with support for +SNI. +.wen + .vitem &$tod_bsdinbox$& .vindex "&$tod_bsdinbox$&" The time of day and the date, in the format required for BSD-style mailbox diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index a491cf973..4ad79c28e 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -73,6 +73,9 @@ PP/16 Removed "dont_insert_empty_fragments" fron "openssl_options". Removed SSL_clear() after SSL_new() which led to protocol negotiation failures. We appear to now support TLS1.1+ with Exim. +PP/17 OpenSSL: new expansion var $tls_sni, which if used in tls_certificate + lets Exim select keys and certificates based upon TLS SNI from client. + Exim version 4.77 ----------------- diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index 0aee33cec..b788b45dc 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -42,6 +42,13 @@ Version 4.78 administrators can choose to make the trade-off themselves and restore compatibility at the cost of session security. + 7. Use of the new expansion variable $tls_sni in the main configuration option + tls_certificate will cause Exim to re-expand the option, if the client + sends the TLS Server Name Indication extension, to permit choosing a + different certificate; tls_privatekey will also be re-expanded. You must + still set these options to expand to valid files when $tls_sni is not set. + Currently OpenSSL only. + Version 4.77 ------------ diff --git a/src/src/daemon.c b/src/src/daemon.c index 4ac34332b..27b4cb265 100644 --- a/src/src/daemon.c +++ b/src/src/daemon.c @@ -828,8 +828,17 @@ pid_t pid; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { int i; - DEBUG(D_any) debug_printf("child %d ended: status=0x%x\n", (int)pid, - status); + DEBUG(D_any) + { + debug_printf("child %d ended: status=0x%x\n", (int)pid, status); +#ifdef WCOREDUMP + if (WIFEXITED(status)) + debug_printf(" normal exit, %d\n", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + debug_printf(" signal exit, signal %d%s\n", WTERMSIG(status), + WCOREDUMP(status) ? " (core dumped)" : ""); +#endif + } /* If it's a listening daemon for which we are keeping track of individual subprocesses, deal with an accepting process that has terminated. */ diff --git a/src/src/expand.c b/src/src/expand.c index 54501de0b..22f7d9a66 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -615,6 +615,9 @@ static var_entry var_table[] = { { "tls_certificate_verified", vtype_int, &tls_certificate_verified }, { "tls_cipher", vtype_stringptr, &tls_cipher }, { "tls_peerdn", vtype_stringptr, &tls_peerdn }, +#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS) + { "tls_sni", vtype_stringptr, &tls_sni }, +#endif { "tod_bsdinbox", vtype_todbsdin, NULL }, { "tod_epoch", vtype_tode, NULL }, { "tod_full", vtype_todf, NULL }, diff --git a/src/src/globals.c b/src/src/globals.c index 6124cb585..7985cd3f0 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -116,6 +116,9 @@ BOOL tls_offered = FALSE; uschar *tls_privatekey = NULL; BOOL tls_remember_esmtp = FALSE; uschar *tls_require_ciphers = NULL; +#ifndef USE_GNUTLS +uschar *tls_sni = NULL; +#endif uschar *tls_try_verify_hosts = NULL; uschar *tls_verify_certificates= NULL; uschar *tls_verify_hosts = NULL; diff --git a/src/src/globals.h b/src/src/globals.h index a51e3bc50..f9540785c 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -98,6 +98,9 @@ extern BOOL tls_offered; /* Server offered TLS */ extern uschar *tls_privatekey; /* Private key file */ extern BOOL tls_remember_esmtp; /* For YAEB */ extern uschar *tls_require_ciphers; /* So some can be avoided */ +#ifndef USE_GNUTLS +extern uschar *tls_sni; /* Server Name Indication */ +#endif extern uschar *tls_try_verify_hosts; /* Optional client verification */ extern uschar *tls_verify_certificates;/* Path for certificates to check */ extern uschar *tls_verify_hosts; /* Mandatory client verification */ diff --git a/src/src/mytypes.h b/src/src/mytypes.h index 5215777f8..ade294e5d 100644 --- a/src/src/mytypes.h +++ b/src/src/mytypes.h @@ -31,8 +31,10 @@ the arguments of printf-like functions. This is done by a macro. */ #if defined(__GNUC__) || defined(__clang__) #define PRINTF_FUNCTION(A,B) __attribute__((format(printf,A,B))) +#define ARG_UNUSED __attribute__((__unused__)) #else #define PRINTF_FUNCTION(A,B) +#define ARG_UNUSED /**/ #endif diff --git a/src/src/spool_in.c b/src/src/spool_in.c index e0d7fcffe..bdc3903c0 100644 --- a/src/src/spool_in.c +++ b/src/src/spool_in.c @@ -286,6 +286,9 @@ dkim_collect_input = FALSE; tls_certificate_verified = FALSE; tls_cipher = NULL; tls_peerdn = NULL; +#ifndef USE_GNUTLS +tls_sni = NULL; +#endif #endif #ifdef WITH_CONTENT_SCAN @@ -549,6 +552,10 @@ for (;;) tls_cipher = string_copy(big_buffer + 12); else if (Ustrncmp(p, "ls_peerdn", 9) == 0) tls_peerdn = string_unprinting(string_copy(big_buffer + 12)); + #ifndef USE_GNUTLS + else if (Ustrncmp(p, "ls_sni", 6) == 0) + tls_sni = string_unprinting(string_copy(big_buffer + 9)); + #endif break; #endif diff --git a/src/src/spool_out.c b/src/src/spool_out.c index 7b8229934..fa4f1b6e2 100644 --- a/src/src/spool_out.c +++ b/src/src/spool_out.c @@ -229,6 +229,9 @@ if (bmi_verdicts != NULL) fprintf(f, "-bmi_verdicts %s\n", bmi_verdicts); if (tls_certificate_verified) fprintf(f, "-tls_certificate_verified\n"); if (tls_cipher != NULL) fprintf(f, "-tls_cipher %s\n", tls_cipher); if (tls_peerdn != NULL) fprintf(f, "-tls_peerdn %s\n", string_printing(tls_peerdn)); +#ifndef USE_GNUTLS +if (tls_sni != NULL) fprintf(f, "-tls_sni %s\n", string_printing(tls_sni)); +#endif #endif /* To complete the envelope, write out the tree of non-recipients, followed by diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c index 5e8c804e5..8cc2457e5 100644 --- a/src/src/tls-openssl.c +++ b/src/src/tls-openssl.c @@ -34,6 +34,7 @@ static BOOL verify_callback_called = FALSE; static const uschar *sid_ctx = US"exim"; static SSL_CTX *ctx = NULL; +static SSL_CTX *ctx_sni = NULL; static SSL *ssl = NULL; static char ssl_errstring[256]; @@ -41,8 +42,26 @@ static char ssl_errstring[256]; static int ssl_session_timeout = 200; static BOOL verify_optional = FALSE; +static BOOL reexpand_tls_files_for_sni = FALSE; +typedef struct tls_ext_ctx_cb { + uschar *certificate; + uschar *privatekey; + uschar *dhparam; + /* these are cached from first expand */ + uschar *server_cipher_list; + /* only passed down to tls_error: */ + host_item *host; +} tls_ext_ctx_cb; + +/* should figure out a cleanup of API to handle state preserved per +implementation, for various reasons, which can be void * in the APIs. +For now, we hack around it. */ +tls_ext_ctx_cb *static_cbinfo = NULL; + +static int +setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host, BOOL optional); /************************************************* @@ -201,8 +220,8 @@ return 1; /* accept */ *************************************************/ /* The SSL library functions call this from time to time to indicate what they -are doing. We copy the string to the debugging output when the level is high -enough. +are doing. We copy the string to the debugging output when TLS debugging has +been requested. Arguments: s the SSL connection @@ -279,6 +298,145 @@ return yield; +/************************************************* +* Expand key and cert file specs * +*************************************************/ + +/* Called once during tls_init and possibly againt during TLS setup, for a +new context, if Server Name Indication was used and tls_sni was seen in +the certificate string. + +Arguments: + sctx the SSL_CTX* to update + cbinfo various parts of session state + +Returns: OK/DEFER/FAIL +*/ + +static int +tls_expand_session_files(SSL_CTX *sctx, const tls_ext_ctx_cb *cbinfo) +{ +uschar *expanded; + +if (cbinfo->certificate == NULL) + return OK; + +if (Ustrstr(cbinfo->certificate, US"tls_sni")) + reexpand_tls_files_for_sni = TRUE; + +if (!expand_check(cbinfo->certificate, US"tls_certificate", &expanded)) + return DEFER; + +if (expanded != NULL) + { + DEBUG(D_tls) debug_printf("tls_certificate file %s\n", expanded); + if (!SSL_CTX_use_certificate_chain_file(sctx, CS expanded)) + return tls_error(string_sprintf( + "SSL_CTX_use_certificate_chain_file file=%s", expanded), + cbinfo->host, NULL); + } + +if (cbinfo->privatekey != NULL && + !expand_check(cbinfo->privatekey, US"tls_privatekey", &expanded)) + return DEFER; + +/* If expansion was forced to fail, key_expanded will be NULL. If the result +of the expansion is an empty string, ignore it also, and assume the private +key is in the same file as the certificate. */ + +if (expanded != NULL && *expanded != 0) + { + DEBUG(D_tls) debug_printf("tls_privatekey file %s\n", expanded); + if (!SSL_CTX_use_PrivateKey_file(sctx, CS expanded, SSL_FILETYPE_PEM)) + return tls_error(string_sprintf( + "SSL_CTX_use_PrivateKey_file file=%s", expanded), cbinfo->host, NULL); + } + +return OK; +} + + + + +/************************************************* +* Callback to handle SNI * +*************************************************/ + +/* Called when acting as server during the TLS session setup if a Server Name +Indication extension was sent by the client. + +API documentation is OpenSSL s_server.c implementation. + +Arguments: + s SSL* of the current session + ad unknown (part of OpenSSL API) (unused) + arg Callback of "our" registered data + +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; +int rc; + +if (!servername) + return SSL_TLSEXT_ERR_OK; + +DEBUG(D_tls) debug_printf("TLS SNI: %s%s\n", servername, + reexpand_tls_files_for_sni ? "" : " (unused for certificate selection)"); + +/* Make the extension value available for expansion */ +tls_sni = servername; + +if (!reexpand_tls_files_for_sni) + return SSL_TLSEXT_ERR_OK; + +/* Can't find an SSL_CTX_clone() or equivalent, so we do it manually; +not confident that memcpy wouldn't break some internal reference counting. +Especially since there's a references struct member, which would be off. */ + +ctx_sni = SSL_CTX_new(SSLv23_server_method()); +if (!ctx_sni) + { + ERR_error_string(ERR_get_error(), ssl_errstring); + DEBUG(D_tls) debug_printf("SSL_CTX_new() failed: %s\n", ssl_errstring); + return SSL_TLSEXT_ERR_NOACK; + } + +/* Not sure how many of these are actually needed, since SSL object +already exists. Might even need this selfsame callback, for reneg? */ + +SSL_CTX_set_info_callback(ctx_sni, SSL_CTX_get_info_callback(ctx)); +SSL_CTX_set_mode(ctx_sni, SSL_CTX_get_mode(ctx)); +SSL_CTX_set_options(ctx_sni, SSL_CTX_get_options(ctx)); +SSL_CTX_set_timeout(ctx_sni, SSL_CTX_get_timeout(ctx)); +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); + +rc = tls_expand_session_files(ctx_sni, cbinfo); +if (rc != OK) return SSL_TLSEXT_ERR_NOACK; + +rc = setup_certs(ctx_sni, tls_verify_certificates, tls_crl, NULL, FALSE); +if (rc != OK) return SSL_TLSEXT_ERR_NOACK; + +DEBUG(D_tls) debug_printf("Switching SSL context.\n"); +SSL_set_SSL_CTX(s, ctx_sni); + +return SSL_TLSEXT_ERR_OK; +} + + + + /************************************************* * Initialize for TLS * *************************************************/ @@ -301,7 +459,15 @@ tls_init(host_item *host, uschar *dhparam, uschar *certificate, uschar *privatekey, address_item *addr) { long init_options; +int rc; BOOL okay; +tls_ext_ctx_cb *cbinfo; + +cbinfo = store_malloc(sizeof(tls_ext_ctx_cb)); +cbinfo->certificate = certificate; +cbinfo->privatekey = privatekey; +cbinfo->dhparam = dhparam; +cbinfo->host = host; SSL_load_error_strings(); /* basic set up */ OpenSSL_add_ssl_algorithms(); @@ -379,36 +545,16 @@ if (!init_dh(dhparam, host)) return DEFER; /* Set up certificate and key */ -if (certificate != NULL) - { - uschar *expanded; - if (!expand_check(certificate, US"tls_certificate", &expanded)) - return DEFER; - - if (expanded != NULL) - { - DEBUG(D_tls) debug_printf("tls_certificate file %s\n", expanded); - if (!SSL_CTX_use_certificate_chain_file(ctx, CS expanded)) - return tls_error(string_sprintf( - "SSL_CTX_use_certificate_chain_file file=%s", expanded), host, NULL); - } - - if (privatekey != NULL && - !expand_check(privatekey, US"tls_privatekey", &expanded)) - return DEFER; - - /* If expansion was forced to fail, key_expanded will be NULL. If the result - of the expansion is an empty string, ignore it also, and assume the private - key is in the same file as the certificate. */ +rc = tls_expand_session_files(ctx, cbinfo); +if (rc != OK) return rc; - if (expanded != NULL && *expanded != 0) - { - DEBUG(D_tls) debug_printf("tls_privatekey file %s\n", expanded); - if (!SSL_CTX_use_PrivateKey_file(ctx, CS expanded, SSL_FILETYPE_PEM)) - return tls_error(string_sprintf( - "SSL_CTX_use_PrivateKey_file file=%s", expanded), host, NULL); - } - } +/* If we need to handle SNI, do so */ +#if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT) +/* 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); +SSL_CTX_set_tlsext_servername_arg(ctx, cbinfo); +#endif /* Set up the RSA callback */ @@ -418,6 +564,9 @@ SSL_CTX_set_tmp_rsa_callback(ctx, rsa_callback); SSL_CTX_set_timeout(ctx, ssl_session_timeout); DEBUG(D_tls) debug_printf("Initialized TLS\n"); + +static_cbinfo = cbinfo; + return OK; } @@ -496,6 +645,7 @@ DEBUG(D_tls) debug_printf("Cipher: %s\n", cipherbuf); /* Called by both client and server startup Arguments: + sctx SSL_CTX* to initialise certs certs file or NULL crl CRL file or NULL host NULL in a server; the remote host in a client @@ -506,7 +656,7 @@ Returns: OK/DEFER/FAIL */ static int -setup_certs(uschar *certs, uschar *crl, host_item *host, BOOL optional) +setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host, BOOL optional) { uschar *expcerts, *expcrl; @@ -516,7 +666,7 @@ if (!expand_check(certs, US"tls_verify_certificates", &expcerts)) if (expcerts != NULL) { struct stat statbuf; - if (!SSL_CTX_set_default_verify_paths(ctx)) + if (!SSL_CTX_set_default_verify_paths(sctx)) return tls_error(US"SSL_CTX_set_default_verify_paths", host, NULL); if (Ustat(expcerts, &statbuf) < 0) @@ -539,12 +689,12 @@ if (expcerts != NULL) says no certificate was supplied.) But this is better. */ if ((file == NULL || statbuf.st_size > 0) && - !SSL_CTX_load_verify_locations(ctx, CS file, CS dir)) + !SSL_CTX_load_verify_locations(sctx, CS file, CS dir)) return tls_error(US"SSL_CTX_load_verify_locations", host, NULL); if (file != NULL) { - SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file(CS file)); + SSL_CTX_set_client_CA_list(sctx, SSL_load_client_CA_file(CS file)); } } @@ -576,7 +726,7 @@ if (expcerts != NULL) { /* is it a file or directory? */ uschar *file, *dir; - X509_STORE *cvstore = SSL_CTX_get_cert_store(ctx); + X509_STORE *cvstore = SSL_CTX_get_cert_store(sctx); if ((statbufcrl.st_mode & S_IFMT) == S_IFDIR) { file = NULL; @@ -603,7 +753,7 @@ if (expcerts != NULL) /* If verification is optional, don't fail if no certificate */ - SSL_CTX_set_verify(ctx, + SSL_CTX_set_verify(sctx, SSL_VERIFY_PEER | (optional? 0 : SSL_VERIFY_FAIL_IF_NO_PEER_CERT), verify_callback); } @@ -641,6 +791,7 @@ tls_server_start(uschar *require_ciphers, uschar *require_mac, { int rc; uschar *expciphers; +tls_ext_ctx_cb *cbinfo; /* Check for previous activation */ @@ -656,6 +807,7 @@ the error. */ rc = tls_init(NULL, tls_dhparam, tls_certificate, tls_privatekey, NULL); if (rc != OK) return rc; +cbinfo = static_cbinfo; if (!expand_check(require_ciphers, US"tls_require_ciphers", &expciphers)) return FAIL; @@ -671,6 +823,7 @@ if (expciphers != NULL) DEBUG(D_tls) debug_printf("required ciphers: %s\n", expciphers); if (!SSL_CTX_set_cipher_list(ctx, CS expciphers)) return tls_error(US"SSL_CTX_set_cipher_list", NULL, NULL); + cbinfo->server_cipher_list = expciphers; } /* If this is a host for which certificate verification is mandatory or @@ -681,13 +834,13 @@ verify_callback_called = FALSE; if (verify_check_host(&tls_verify_hosts) == OK) { - rc = setup_certs(tls_verify_certificates, tls_crl, NULL, FALSE); + rc = setup_certs(ctx, tls_verify_certificates, tls_crl, NULL, FALSE); if (rc != OK) return rc; verify_optional = FALSE; } else if (verify_check_host(&tls_try_verify_hosts) == OK) { - rc = setup_certs(tls_verify_certificates, tls_crl, NULL, TRUE); + rc = setup_certs(ctx, tls_verify_certificates, tls_crl, NULL, TRUE); if (rc != OK) return rc; verify_optional = TRUE; } @@ -839,7 +992,7 @@ if (expciphers != NULL) return tls_error(US"SSL_CTX_set_cipher_list", host, NULL); } -rc = setup_certs(verify_certs, crl, host, FALSE); +rc = setup_certs(ctx, verify_certs, crl, host, FALSE); if (rc != OK) return rc; if ((ssl = SSL_new(ctx)) == NULL) return tls_error(US"SSL_new", host, NULL); -- 2.25.1