diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/src/globals.c | 2 | ||||
-rw-r--r-- | src/src/globals.h | 2 | ||||
-rw-r--r-- | src/src/readconf.c | 2 | ||||
-rw-r--r-- | src/src/tls-gnu.c | 249 | ||||
-rw-r--r-- | src/src/tls-openssl.c | 156 | ||||
-rw-r--r-- | src/src/tls.c | 1 | ||||
-rw-r--r-- | src/src/transports/smtp.c | 2 | ||||
-rw-r--r-- | src/src/transports/smtp.h | 2 |
8 files changed, 330 insertions, 86 deletions
diff --git a/src/src/globals.c b/src/src/globals.c index 9e68aaca8..1e12bcb92 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -124,9 +124,11 @@ uschar *dsn_advertise_hosts = NULL; #ifndef DISABLE_TLS BOOL gnutls_compat_mode = FALSE; BOOL gnutls_allow_auto_pkcs11 = FALSE; +uschar *hosts_require_alpn = NULL; uschar *openssl_options = NULL; const pcre *regex_STARTTLS = NULL; uschar *tls_advertise_hosts = US"*"; +uschar *tls_alpn = US"smtp:esmtp"; uschar *tls_certificate = NULL; uschar *tls_crl = NULL; /* This default matches NSS DH_MAX_P_BITS value at current time (2012), because diff --git a/src/src/globals.h b/src/src/globals.h index 657e6c706..d5d93148f 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -125,8 +125,10 @@ extern tls_support tls_out; #ifndef DISABLE_TLS extern BOOL gnutls_compat_mode; /* Less security, more compatibility */ extern BOOL gnutls_allow_auto_pkcs11; /* Let GnuTLS autoload PKCS11 modules */ +extern uschar *hosts_require_alpn; /* Mandatory ALPN successful nogitiation */ extern uschar *openssl_options; /* OpenSSL compatibility options */ extern const pcre *regex_STARTTLS; /* For recognizing STARTTLS settings */ +extern uschar *tls_alpn; /* ALPN names acceptable */ extern uschar *tls_certificate; /* Certificate file */ extern uschar *tls_crl; /* CRL File */ extern int tls_dh_max_bits; /* don't accept higher lib suggestions */ diff --git a/src/src/readconf.c b/src/src/readconf.c index 01e85a329..a1eafb2ec 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -184,6 +184,7 @@ static optionlist optionlist_config[] = { #ifdef SUPPORT_PROXY { "hosts_proxy", opt_stringptr, {&hosts_proxy} }, #endif + { "hosts_require_alpn", opt_stringptr, {&hosts_require_alpn} }, { "hosts_require_helo", opt_stringptr, {&hosts_require_helo} }, { "hosts_treat_as_local", opt_stringptr, {&hosts_treat_as_local} }, #ifdef LOOKUP_IBASE @@ -378,6 +379,7 @@ static optionlist optionlist_config[] = { { "timezone", opt_stringptr, {&timezone_string} }, { "tls_advertise_hosts", opt_stringptr, {&tls_advertise_hosts} }, #ifndef DISABLE_TLS + { "tls_alpn", opt_stringptr, {&tls_alpn} }, { "tls_certificate", opt_stringptr, {&tls_certificate} }, { "tls_crl", opt_stringptr, {&tls_crl} }, { "tls_dh_max_bits", opt_int, {&tls_dh_max_bits} }, diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c index 2d7041f3e..bc16ec03e 100644 --- a/src/src/tls-gnu.c +++ b/src/src/tls-gnu.c @@ -285,7 +285,7 @@ static BOOL exim_testharness_disable_ocsp_validity_check = FALSE; #endif #ifdef EXIM_HAVE_ALPN -static BOOL server_seen_alpn = FALSE; +static int server_seen_alpn = -1; /* count of names */ #endif #ifdef EXIM_HAVE_TLS_RESUME static gnutls_datum_t server_sessticket_key; @@ -388,10 +388,15 @@ return host ? FAIL : DEFER; static int -tls_error_gnu(const uschar *prefix, int err, const host_item *host, +tls_error_gnu(exim_gnutls_state_st * state, const uschar *prefix, int err, uschar ** errstr) { -return tls_error(prefix, US gnutls_strerror(err), host, errstr); +return tls_error(prefix, + state && err == GNUTLS_E_FATAL_ALERT_RECEIVED + ? US gnutls_alert_get_name(gnutls_alert_get(state->session)) + : US gnutls_strerror(err), + state ? state->host : NULL, + errstr); } static int @@ -451,12 +456,12 @@ To prevent this, we init PKCS11 first, which is the documented approach. */ if (!gnutls_allow_auto_pkcs11) if ((rc = gnutls_pkcs11_init(GNUTLS_PKCS11_FLAG_MANUAL, NULL))) - return tls_error_gnu(US"gnutls_pkcs11_init", rc, NULL, errstr); + return tls_error_gnu(NULL, US"gnutls_pkcs11_init", rc, errstr); #endif #ifndef GNUTLS_AUTO_GLOBAL_INIT if ((rc = gnutls_global_init())) - return tls_error_gnu(US"gnutls_global_init", rc, NULL, errstr); + return tls_error_gnu(UNULL, S"gnutls_global_init", rc, errstr); #endif #if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0 @@ -715,7 +720,7 @@ host_item *host = NULL; /* dummy for macros */ DEBUG(D_tls) debug_printf("Initialising GnuTLS server params\n"); if ((rc = gnutls_dh_params_init(&dh_server_params))) - return tls_error_gnu(US"gnutls_dh_params_init", rc, host, errstr); + return tls_error_gnu(NULL, US"gnutls_dh_params_init", rc, errstr); if (!expand_check(tls_dhparam, US"tls_dhparam", &exp_tls_dhparam, errstr)) return DEFER; @@ -745,7 +750,7 @@ else if (m.data) { if ((rc = gnutls_dh_params_import_pkcs3(dh_server_params, &m, GNUTLS_X509_FMT_PEM))) - return tls_error_gnu(US"gnutls_dh_params_import_pkcs3", rc, host, errstr); + return tls_error_gnu(NULL, US"gnutls_dh_params_import_pkcs3", rc, errstr); DEBUG(D_tls) debug_printf("Loaded fixed standard D-H parameters\n"); return OK; } @@ -829,7 +834,7 @@ if ((fd = Uopen(filename, O_RDONLY, 0)) >= 0) rc = gnutls_dh_params_import_pkcs3(dh_server_params, &m, GNUTLS_X509_FMT_PEM); store_free(m.data); if (rc) - return tls_error_gnu(US"gnutls_dh_params_import_pkcs3", rc, host, errstr); + return tls_error_gnu(NULL, US"gnutls_dh_params_import_pkcs3", rc, errstr); DEBUG(D_tls) debug_printf("read D-H parameters from file \"%s\"\n", filename); } @@ -884,7 +889,7 @@ if (rc < 0) debug_printf("requesting generation of %d bit Diffie-Hellman prime ...\n", dh_bits_gen); if ((rc = gnutls_dh_params_generate2(dh_server_params, dh_bits_gen))) - return tls_error_gnu(US"gnutls_dh_params_generate2", rc, host, errstr); + return tls_error_gnu(NULL, US"gnutls_dh_params_generate2", rc, errstr); /* 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 @@ -895,8 +900,8 @@ if (rc < 0) if ( (rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM, m.data, &sz)) && rc != GNUTLS_E_SHORT_MEMORY_BUFFER) - return tls_error_gnu(US"gnutls_dh_params_export_pkcs3(NULL) sizing", - rc, host, errstr); + return tls_error_gnu(NULL, US"gnutls_dh_params_export_pkcs3(NULL) sizing", + rc, errstr); m.size = sz; if (!(m.data = store_malloc(m.size))) return tls_error_sys(US"memory allocation failed", errno, NULL, errstr); @@ -906,7 +911,7 @@ if (rc < 0) m.data, &sz))) { store_free(m.data); - return tls_error_gnu(US"gnutls_dh_params_export_pkcs3() real", rc, host, errstr); + return tls_error_gnu(NULL, US"gnutls_dh_params_export_pkcs3() real", rc, errstr); } m.size = sz; /* shrink by 1, probably */ @@ -1011,7 +1016,7 @@ out: return rc; err: - rc = tls_error_gnu(where, rc, NULL, errstr); + rc = tls_error_gnu(state, where, rc, errstr); goto out; } @@ -1032,9 +1037,9 @@ tls_add_certfile(exim_gnutls_state_st * state, const host_item * host, int rc = gnutls_certificate_set_x509_key_file(state->lib_state.x509_cred, CCS certfile, CCS keyfile, GNUTLS_X509_FMT_PEM); if (rc < 0) - return tls_error_gnu( + return tls_error_gnu(state, string_sprintf("cert/key setup: cert=%s key=%s", certfile, keyfile), - rc, host, errstr); + rc, errstr); return -rc; } @@ -1069,19 +1074,34 @@ return 0; /* Make a note that we saw a status-request */ static int tls_server_clienthello_ext(void * ctx, unsigned tls_id, - const unsigned char *data, unsigned size) + const uschar * data, unsigned size) { /* https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml */ switch (tls_id) { - case 5: /* status_request */ + case 5: /* Status Request */ DEBUG(D_tls) debug_printf("Seen status_request extension from client\n"); tls_in.ocsp = OCSP_NOT_RESP; break; #ifdef EXIM_HAVE_ALPN case 16: /* Application Layer Protocol Notification */ - DEBUG(D_tls) debug_printf("Seen ALPN extension from client\n"); - server_seen_alpn = TRUE; + /* The format of "data" here doesn't seem to be documented, but appears + to be a 2-byte field with a (redundant, given the "size" arg) total length + then a sequence of one-byte size then string (not nul-term) names. The + latter is as described in OpenSSL documentation. */ + + DEBUG(D_tls) debug_printf("Seen ALPN extension from client (s=%u):", size); + for (const uschar * s = data+2; s-data < size-1; s += *s + 1) + { + server_seen_alpn++; + DEBUG(D_tls) debug_printf(" '%.*s'", (int)*s, s+1); + } + DEBUG(D_tls) debug_printf("\n"); + if (server_seen_alpn > 1) + { + DEBUG(D_tls) debug_printf("TLS: too many ALPNs presented in handshake\n"); + return GNUTLS_E_NO_APPLICATION_PROTOCOL; + } break; #endif } @@ -1279,9 +1299,9 @@ while (cfile = string_nextinlist(&clist, &csep, NULL, 0)) if ((rc = gnutls_certificate_set_ocsp_status_request_file2( state->lib_state.x509_cred, CCS ofile, gnutls_cert_index, ocsp_fmt)) < 0) - return tls_error_gnu( + return tls_error_gnu(state, US"gnutls_certificate_set_ocsp_status_request_file2", - rc, NULL, errstr); + rc, errstr); DEBUG(D_tls) debug_printf(" %d response%s loaded\n", rc, rc>1 ? "s":""); @@ -1299,9 +1319,9 @@ while (cfile = string_nextinlist(&clist, &csep, NULL, 0)) if ((rc = gnutls_certificate_set_ocsp_status_request_function2( state->lib_state.x509_cred, gnutls_cert_index, server_ocsp_stapling_cb, ofile))) - return tls_error_gnu( + return tls_error_gnu(state, US"gnutls_certificate_set_ocsp_status_request_function2", - rc, NULL, errstr); + rc, errstr); else # endif { @@ -1403,7 +1423,7 @@ else } if (cert_count < 0) - return tls_error_gnu(US"setting certificate trust", cert_count, host, errstr); + return tls_error_gnu(state, US"setting certificate trust", cert_count, errstr); DEBUG(D_tls) debug_printf("Added %d certificate authorities\n", cert_count); @@ -1418,8 +1438,8 @@ int cert_count; DEBUG(D_tls) debug_printf("loading CRL file = %s\n", crl); if ((cert_count = gnutls_certificate_set_x509_crl_file(state->lib_state.x509_cred, CS crl, GNUTLS_X509_FMT_PEM)) < 0) - return tls_error_gnu(US"gnutls_certificate_set_x509_crl_file", - cert_count, state->host, errstr); + return tls_error_gnu(state, US"gnutls_certificate_set_x509_crl_file", + cert_count, errstr); DEBUG(D_tls) debug_printf("Processed %d CRLs\n", cert_count); return OK; @@ -1730,8 +1750,8 @@ if (!state->lib_state.x509_cred) { if ((rc = gnutls_certificate_allocate_credentials( (gnutls_certificate_credentials_t *) &state->lib_state.x509_cred))) - return tls_error_gnu(US"gnutls_certificate_allocate_credentials", - rc, host, errstr); + return tls_error_gnu(state, US"gnutls_certificate_allocate_credentials", + rc, errstr); creds_basic_init(state->lib_state.x509_cred, !host); } @@ -1935,7 +1955,7 @@ if (!state->host) if ((rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->lib_state.x509_cred))) - return tls_error_gnu(US"gnutls_credentials_set", rc, host, errstr); + return tls_error_gnu(state, US"gnutls_credentials_set", rc, errstr); return OK; } @@ -2015,7 +2035,7 @@ else state->tls_crl = tls_crl; } if (rc) - return tls_error_gnu(US"gnutls_init", rc, host, errstr); + return tls_error_gnu(state, US"gnutls_init", rc, errstr); state->tls_require_ciphers = require_ciphers; state->host = host; @@ -2044,7 +2064,7 @@ if (host) sz = Ustrlen(state->tlsp->sni); if ((rc = gnutls_server_name_set(state->session, GNUTLS_NAME_DNS, state->tlsp->sni, sz))) - return tls_error_gnu(US"gnutls_server_name_set", rc, host, errstr); + return tls_error_gnu(state, US"gnutls_server_name_set", rc, errstr); } } else if (state->tls_sni) @@ -2074,10 +2094,10 @@ if (!state->lib_state.pri_string) } if ((rc = creds_load_pristring(state, p, &errpos))) - return tls_error_gnu(string_sprintf( + return tls_error_gnu(state, string_sprintf( "gnutls_priority_init(%s) failed at offset %ld, \"%.6s..\"", p, errpos - CS p, errpos), - rc, host, errstr); + rc, errstr); } else { @@ -2087,7 +2107,7 @@ else if ((rc = gnutls_priority_set(state->session, state->lib_state.pri_cache))) - return tls_error_gnu(US"gnutls_priority_set", rc, host, errstr); + return tls_error_gnu(state, US"gnutls_priority_set", rc, errstr); /* This also sets the server ticket expiration time to the same, and the STEK rotation time to 3x. */ @@ -2285,7 +2305,7 @@ if ((ct = gnutls_certificate_type_get(session)) != GNUTLS_CRT_X509) 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_gnu((Label), rc, state->host, errstr); \ + return tls_error_gnu(state, (Label), rc, errstr); \ return OK; \ } \ } while (0) @@ -2808,26 +2828,70 @@ if (gnutls_session_is_resumed(state->session)) DEBUG(D_tls) debug_printf("Session resumed\n"); } } -#endif +#endif /* EXIM_HAVE_TLS_RESUME */ #ifdef EXIM_HAVE_ALPN +/* Expand and convert an Exim list to a gnutls_datum list. False return for fail. +NULL plist return for silent no-ALPN. +*/ + +static BOOL +tls_alpn_plist(const uschar * tls_alpn, const gnutls_datum_t ** plist, unsigned * plen, + uschar ** errstr) +{ +uschar * exp_alpn; + +if (!expand_check(tls_alpn, US"tls_alpn", &exp_alpn, errstr)) + return FALSE; + +if (!exp_alpn) + { + DEBUG(D_tls) debug_printf("Setting TLS ALPN forced to fail, not sending\n"); + *plist = NULL; + } +else + { + const uschar * list = exp_alpn; + int sep = 0; + unsigned cnt = 0; + gnutls_datum_t * p; + uschar * s; + + while (string_nextinlist(&list, &sep, NULL, 0)) cnt++; + + p = store_get(sizeof(gnutls_datum_t) * cnt, is_tainted(exp_alpn)); + list = exp_alpn; + for (int i = 0; s = string_nextinlist(&list, &sep, NULL, 0); i++) + { p[i].data = s; p[i].size = Ustrlen(s); } + *plist = (*plen = cnt) ? p : NULL; + } +return TRUE; +} + static void -tls_server_set_acceptable_alpns(exim_gnutls_state_st * state) +tls_server_set_acceptable_alpns(exim_gnutls_state_st * state, uschar ** errstr) { int rc; -gnutls_datum_t protocols[2] = {[0] = {.data = US"smtp", .size = 4}, - [1] = {.data = US"esmtp", .size = 5}}; +const gnutls_datum_t * plist; +unsigned plen; -/* Set non-mandatory set of protocol names */ -if (!(rc = gnutls_alpn_set_protocols(state->session, protocols, 2, 0))) - gnutls_handshake_set_hook_function(state->session, - GNUTLS_HANDSHAKE_ANY, GNUTLS_HOOK_POST, tls_server_hook_cb); -else - DEBUG(D_tls) - debug_printf("setting alpn protocols: %s\n", US gnutls_strerror(rc)); +if (tls_alpn_plist(tls_alpn, &plist, &plen, errstr) && plist) + { + /* This seems to be only mandatory if the client sends an ALPN extension; + not trying ALPN is ok. Need to decide how to support server-side must-alpn. */ + + server_seen_alpn = 0; + if (!(rc = gnutls_alpn_set_protocols(state->session, plist, plen, + GNUTLS_ALPN_MANDATORY))) + gnutls_handshake_set_hook_function(state->session, + GNUTLS_HANDSHAKE_ANY, GNUTLS_HOOK_POST, tls_server_hook_cb); + else + DEBUG(D_tls) + debug_printf("setting alpn protocols: %s\n", US gnutls_strerror(rc)); + } } -#endif +#endif /* EXIM_HAVE_ALPN */ /* ------------------------------------------------------------------------ */ /* Exported functions */ @@ -2886,7 +2950,7 @@ DEBUG(D_tls) debug_printf("initialising GnuTLS as a server\n"); } #ifdef EXIM_HAVE_ALPN -tls_server_set_acceptable_alpns(state); +tls_server_set_acceptable_alpns(state, errstr); #endif #ifdef EXIM_HAVE_TLS_RESUME @@ -2977,7 +3041,7 @@ if (rc != GNUTLS_E_SUCCESS) } else { - tls_error_gnu(US"gnutls_handshake", rc, NULL, errstr); + tls_error_gnu(state, US"gnutls_handshake", rc, errstr); (void) gnutls_alert_send_appropriate(state->session, rc); gnutls_deinit(state->session); gnutls_certificate_free_credentials(state->lib_state.x509_cred); @@ -3005,29 +3069,30 @@ tls_server_resume_posthandshake(state); DEBUG(D_tls) post_handshake_debug(state); #ifdef EXIM_HAVE_ALPN -if (server_seen_alpn) +if (server_seen_alpn > 0) { - /* The client offered ALPN. We were set up with a nonmandatory list; - see what was negotiated. We require a match now, given that something - was offered. */ - gnutls_datum_t p = {.size = 0}; - int rc = gnutls_alpn_get_selected_protocol(state->session, &p); - if (!rc || rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) - { - if (p.size == 0) - { - *errstr = US"ALPN rejected"; - return FAIL; - } - else - DEBUG(D_tls) + DEBUG(D_tls) + { /* The client offered ALPN. See what was negotiated. */ + gnutls_datum_t p = {.size = 0}; + int rc = gnutls_alpn_get_selected_protocol(state->session, &p); + if (!rc) debug_printf("ALPN negotiated: %.*s\n", (int)p.size, p.data); - } - else - DEBUG(D_tls) - debug_printf("getting alpn protocol: %s\n", US gnutls_strerror(rc)); + else + debug_printf("getting alpn protocol: %s\n", US gnutls_strerror(rc)); + } } +else if (server_seen_alpn == 0) + if (verify_check_host(&hosts_require_alpn) == OK) + { + gnutls_alert_send(state->session, GNUTLS_AL_FATAL, GNUTLS_A_NO_APPLICATION_PROTOCOL); + tls_error(US"handshake", US"ALPN required but not negotiated", NULL, errstr); + return FAIL; + } + else + DEBUG(D_tls) debug_printf("TLS: no ALPN presented in handshake\n"); +else + DEBUG(D_tls) debug_printf("TLS: was not watching for ALPN\n"); #endif /* Verify after the fact */ @@ -3367,6 +3432,28 @@ if (!cipher_list) #endif } +if (ob->tls_alpn) +#ifdef EXIM_HAVE_ALPN + { + const gnutls_datum_t * plist; + unsigned plen; + + if (!tls_alpn_plist(ob->tls_alpn, &plist, &plen, errstr)) + return FALSE; + if (plist) + if (gnutls_alpn_set_protocols(state->session, plist, plen, 0) != 0) + { + tls_error(US"alpn init", NULL, state->host, errstr); + return FALSE; + } + else + DEBUG(D_tls) debug_printf("Setting TLS ALPN '%s'\n", ob->tls_alpn); + } +#else + log_write(0, LOG_MAIN, "ALPN unusable with this GnuTLS library version; ignoring \"%s\"\n", + ob->tls_alpn); +#endif + { int dh_min_bits = ob->tls_dh_min_bits; if (dh_min_bits < EXIM_CLIENT_DH_MIN_MIN_BITS) @@ -3435,7 +3522,7 @@ if (request_ocsp) if ((rc = gnutls_ocsp_status_request_enable_client(state->session, NULL, 0, NULL)) != OK) { - tls_error_gnu(US"cert-status-req", rc, state->host, errstr); + tls_error_gnu(state, US"cert-status-req", rc, errstr); return FALSE; } tlsp->ocsp = OCSP_NOT_RESP; @@ -3477,7 +3564,7 @@ if (rc != GNUTLS_E_SUCCESS) tls_error(US"gnutls_handshake", US"timed out", state->host, errstr); } else - tls_error_gnu(US"gnutls_handshake", rc, state->host, errstr); + tls_error_gnu(state, US"gnutls_handshake", rc, errstr); return FALSE; } @@ -3522,9 +3609,9 @@ if (request_ocsp) gnutls_free(printed.data); } else - (void) tls_error_gnu(US"ocsp decode", rc, state->host, errstr); + (void) tls_error_gnu(state, US"ocsp decode", rc, errstr); if (idx == 0 && rc) - (void) tls_error_gnu(US"ocsp decode", rc, state->host, errstr); + (void) tls_error_gnu(state, US"ocsp decode", rc, errstr); } if (gnutls_ocsp_status_request_is_checked(state->session, 0) == 0) @@ -3546,6 +3633,24 @@ if (request_ocsp) tls_client_resume_posthandshake(state, tlsp, host); #endif +#ifdef EXIM_HAVE_ALPN +if (ob->tls_alpn) /* We requested. See what was negotiated. */ + { + gnutls_datum_t p = {.size = 0}; + + if (gnutls_alpn_get_selected_protocol(state->session, &p) == 0) + { DEBUG(D_tls) debug_printf("ALPN negotiated: '%.*s'\n", (int)p.size, p.data); } + else if (verify_check_given_host(CUSS &ob->hosts_require_alpn, host) == OK) + { + gnutls_alert_send(state->session, GNUTLS_AL_FATAL, GNUTLS_A_NO_APPLICATION_PROTOCOL); + tls_error(US"handshake", US"ALPN required but not negotiated", state->host, errstr); + return FALSE; + } + else + DEBUG(D_tls) debug_printf("No ALPN negotiated"); + } +#endif + /* Sets various Exim expansion variables; may need to adjust for ACL callouts */ extract_exim_vars_from_tls_state(state); diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c index d99de53d6..678288ca3 100644 --- a/src/src/tls-openssl.c +++ b/src/src/tls-openssl.c @@ -359,6 +359,9 @@ typedef struct { #ifdef EXIM_HAVE_OPENSSL_TLSEXT static SSL_CTX *server_sni = NULL; #endif +#ifdef EXIM_HAVE_ALPN +static BOOL server_seen_alpn = FALSE; +#endif static char ssl_errstring[256]; @@ -2140,9 +2143,9 @@ bad: return SSL_TLSEXT_ERR_ALERT_FATAL; * Callback to handle ALPN * *************************************************/ -/* SSL_CTX_set_alpn_select_cb() */ -/* Called on server when client offers ALPN, after the SNI callback. -If set and not e?smtp then we dump the connection */ +/* Called on server if tls_alpn nonblank after expansion, +when client offers ALPN, after the SNI callback. +If set and not matching the list then we dump the connection */ static int tls_server_alpn_cb(SSL *ssl, const uschar ** out, uschar * outlen, @@ -2150,6 +2153,7 @@ tls_server_alpn_cb(SSL *ssl, const uschar ** out, uschar * outlen, { const exim_openssl_state_st * state = arg; +server_seen_alpn = TRUE; DEBUG(D_tls) { debug_printf("Received TLS ALPN offer:"); @@ -2159,23 +2163,32 @@ DEBUG(D_tls) if (pos + 1 + siz > inlen) siz = inlen - pos - 1; debug_printf(" '%.*s'", siz, in + pos + 1); } - debug_printf("\n"); + debug_printf(". Our list: '%s'\n", tls_alpn); } /* Look for an acceptable ALPN */ + if ( inlen > 1 /* at least one name */ && in[0]+1 == inlen /* filling the vector, so exactly one name */ - && ( Ustrncmp(in+1, "smtp", in[0]) == 0 - || Ustrncmp(in+1, "esmtp", in[0]) == 0 - ) ) + ) { - *out = in; /* we checked for exactly one, so can just point to it */ - *outlen = inlen; - return SSL_TLSEXT_ERR_OK; /* use ALPN */ + const uschar * list = tls_alpn; + int sep = 0; + for (uschar * name; name = string_nextinlist(&list, &sep, NULL, 0); ) + if (Ustrncmp(in+1, name, in[0]) == 0) + { + *out = in; /* we checked for exactly one, so can just point to it */ + *outlen = inlen; + return SSL_TLSEXT_ERR_OK; /* use ALPN */ + } } -/* Reject unacceptable ALPN */ -/* This will be fatal to the TLS conn; would be nice to kill TCP also */ +/* More than one name from clilent, or name did not match our list. */ + +/* This will be fatal to the TLS conn; would be nice to kill TCP also. +Maybe as an option in future; for now leave control to the config (must-tls). */ + +DEBUG(D_tls) debug_printf("TLS ALPN rejected\n"); return SSL_TLSEXT_ERR_ALERT_FATAL; } #endif /* EXIM_HAVE_ALPN */ @@ -2649,8 +2662,20 @@ if (!host) /* server */ tls_certificate */ SSL_CTX_set_tlsext_servername_callback(ctx, tls_servername_cb); SSL_CTX_set_tlsext_servername_arg(ctx, state); + # ifdef EXIM_HAVE_ALPN - SSL_CTX_set_alpn_select_cb(ctx, tls_server_alpn_cb, state); + if (tls_alpn && *tls_alpn) + { + uschar * exp_alpn; + if ( expand_check(tls_alpn, US"tls_alpn", &exp_alpn, errstr) + && *exp_alpn && !isblank(*exp_alpn)) + { + tls_alpn = exp_alpn; /* subprocess so ok to overwrite */ + SSL_CTX_set_alpn_select_cb(ctx, tls_server_alpn_cb, state); + } + else + tls_alpn = NULL; + } # endif } # ifndef DISABLE_OCSP @@ -3226,6 +3251,30 @@ if (SSL_session_reused(ssl)) } #endif +#ifdef EXIM_HAVE_ALPN +/* If require-alpn, check server_seen_alpn here. Else abort TLS */ +if (!tls_alpn || !*tls_alpn) + { DEBUG(D_tls) debug_printf("TLS: was not watching for ALPN\n"); } +else if (!server_seen_alpn) + if (verify_check_host(&hosts_require_alpn) == OK) + { + /* We'd like to send a definitive Alert but OpenSSL provides no facility */ + SSL_shutdown(ssl); + tls_error(US"handshake", NULL, US"ALPN required but not negotiated", errstr); + return FAIL; + } + else + { DEBUG(D_tls) debug_printf("TLS: no ALPN presented in handshake\n"); } +else DEBUG(D_tls) + { + const uschar * name; + unsigned len; + SSL_get0_alpn_selected(ssl, &name, &len); + debug_printf("ALPN negotiated: '%.*s'\n", (int)*name, name+1); + } +#endif + + /* TLS has been set up. Record data for the connection, adjust the input functions to read via TLS, and initialize things. */ @@ -3593,6 +3642,47 @@ if (SSL_session_reused(exim_client_ctx->ssl)) #endif /* !DISABLE_TLS_RESUME */ +#ifdef EXIM_HAVE_ALPN +/* Expand and convert an Exim list to an ALPN list. False return for fail. +NULL plist return for silent no-ALPN. +*/ + +static BOOL +tls_alpn_plist(const uschar * tls_alpn, const uschar ** plist, unsigned * plen, + uschar ** errstr) +{ +uschar * exp_alpn; + +if (!expand_check(tls_alpn, US"tls_alpn", &exp_alpn, errstr)) + return FALSE; + +if (!exp_alpn) + { + DEBUG(D_tls) debug_printf("Setting TLS ALPN forced to fail, not sending\n"); + *plist = NULL; + } +else + { + /* The server implementation only accepts exactly one protocol name + but it's little extra code complexity in the client. */ + + const uschar * list = exp_alpn; + uschar * p = store_get(Ustrlen(exp_alpn), is_tainted(exp_alpn)), * s, * t; + int sep = 0; + uschar len; + + for (t = p; s = string_nextinlist(&list, &sep, NULL, 0); t += len) + { + *t++ = len = (uschar) Ustrlen(s); + memcpy(t, s, len); + } + *plist = (*plen = t - p) ? p : NULL; + } +return TRUE; +} +#endif /* EXIM_HAVE_ALPN */ + + /************************************************* * Start a TLS session in a client * *************************************************/ @@ -3778,6 +3868,28 @@ if (ob->tls_sni) } } +if (ob->tls_alpn) +#ifdef EXIM_HAVE_ALPN + { + const uschar * plist; + unsigned plen; + + if (!tls_alpn_plist(ob->tls_alpn, &plist, &plen, errstr)) + return FALSE; + if (plist) + if (SSL_set_alpn_protos(exim_client_ctx->ssl, plist, plen) != 0) + { + tls_error(US"alpn init", host, NULL, errstr); + return FALSE; + } + else + DEBUG(D_tls) debug_printf("Setting TLS ALPN '%s'\n", ob->tls_alpn); + } +#else + log_write(0, LOG_MAIN, "ALPN unusable with this OpenSSL library version; ignoring \"%s\"\n", + ob->alpn); +#endif + #ifdef SUPPORT_DANE if (conn_args->dane) if (dane_tlsa_load(exim_client_ctx->ssl, host, &conn_args->tlsa_dnsa, errstr) != OK) @@ -3857,6 +3969,24 @@ DEBUG(D_tls) tls_client_resume_posthandshake(exim_client_ctx, tlsp); #endif +#ifdef EXIM_HAVE_ALPN +if (ob->tls_alpn) /* We requested. See what was negotiated. */ + { + const uschar * name; + unsigned len; + + SSL_get0_alpn_selected(exim_client_ctx->ssl, &name, &len); + if (len > 0) + { DEBUG(D_tls) debug_printf("ALPN negotiated %u: '%.*s'\n", len, (int)*name, name+1); } + else if (verify_check_given_host(CUSS &ob->hosts_require_alpn, host) == OK) + { + /* Would like to send a relevant fatal Alert, but OpenSSL has no API */ + tls_error(US"handshake", host, US"ALPN required but not negotiated", errstr); + return FALSE; + } + } +#endif + #ifdef SSL_get_extms_support tlsp->ext_master_secret = SSL_get_extms_support(exim_client_ctx->ssl) == 1; #endif diff --git a/src/src/tls.c b/src/src/tls.c index 0df99845c..c497ac307 100644 --- a/src/src/tls.c +++ b/src/src/tls.c @@ -685,7 +685,6 @@ else if ((subjdn = tls_cert_subject(cert, NULL))) return FALSE; } - /* Environment cleanup: The GnuTLS library uses SSLKEYLOGFILE in the environment and writes a file by that name. Our OpenSSL code does the same, using keying info from the library API. diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c index 3210e596c..c62de724d 100644 --- a/src/src/transports/smtp.c +++ b/src/src/transports/smtp.c @@ -84,6 +84,7 @@ optionlist smtp_transport_options[] = { #if !defined(DISABLE_TLS) && !defined(DISABLE_OCSP) { "hosts_request_ocsp", opt_stringptr, LOFF(hosts_request_ocsp) }, #endif + { "hosts_require_alpn", opt_stringptr, LOFF(hosts_require_alpn) }, { "hosts_require_auth", opt_stringptr, LOFF(hosts_require_auth) }, #ifndef DISABLE_TLS # ifdef SUPPORT_DANE @@ -123,6 +124,7 @@ optionlist smtp_transport_options[] = { { "socks_proxy", opt_stringptr, LOFF(socks_proxy) }, #endif #ifndef DISABLE_TLS + { "tls_alpn", opt_stringptr, LOFF(tls_alpn) }, { "tls_certificate", opt_stringptr, LOFF(tls_certificate) }, { "tls_crl", opt_stringptr, LOFF(tls_crl) }, { "tls_dh_min_bits", opt_int, LOFF(tls_dh_min_bits) }, diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h index aff3f5452..2ce8b11c5 100644 --- a/src/src/transports/smtp.h +++ b/src/src/transports/smtp.h @@ -48,6 +48,7 @@ typedef struct { uschar *dscp; uschar *serialize_hosts; uschar *hosts_try_auth; + uschar *hosts_require_alpn; uschar *hosts_require_auth; uschar *hosts_try_chunking; #ifdef SUPPORT_DANE @@ -101,6 +102,7 @@ typedef struct { uschar *socks_proxy; #endif #ifndef DISABLE_TLS + uschar *tls_alpn; uschar *tls_certificate; uschar *tls_crl; uschar *tls_privatekey; |