summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Pennock <pdp@exim.org>2012-05-04 04:39:01 -0700
committerPhil Pennock <pdp@exim.org>2012-05-04 04:39:01 -0700
commit7be682ca5ebd9571a01b762195b11c34cd231830 (patch)
treea7c2ddde30095369d705a32131be71d6e6cc5091
parentfb2bba55d3916ab1d515f1a060f19009daf447ed (diff)
TLS SNI support for OpenSSL ($tls_sni)
-rw-r--r--doc/doc-docbook/spec.xfpt18
-rw-r--r--doc/doc-txt/ChangeLog3
-rw-r--r--doc/doc-txt/NewStuff7
-rw-r--r--src/src/daemon.c13
-rw-r--r--src/src/expand.c3
-rw-r--r--src/src/globals.c3
-rw-r--r--src/src/globals.h3
-rw-r--r--src/src/mytypes.h2
-rw-r--r--src/src/spool_in.c7
-rw-r--r--src/src/spool_out.c3
-rw-r--r--src/src/tls-openssl.c233
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
@@ -280,6 +299,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);