X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/2c17bb02e213012d5d98ebac506a67b23b2cf693..1f00591e43de58718045ab86916011cc564e5201:/src/src/tls-gnu.c diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c index 4e1e5104b..aa2f92514 100644 --- a/src/src/tls-gnu.c +++ b/src/src/tls-gnu.c @@ -76,8 +76,10 @@ typedef struct exim_gnutls_state { int fd_out; BOOL peer_cert_verified; BOOL trigger_sni_changes; + BOOL have_set_peerdn; const struct host_item *host; uschar *peerdn; + uschar *ciphersuite; uschar *received_sni; const uschar *tls_certificate; @@ -98,17 +100,14 @@ typedef struct exim_gnutls_state { int xfer_buffer_hwm; int xfer_eof; int xfer_error; - - uschar cipherbuf[256]; } exim_gnutls_state_st; static const exim_gnutls_state_st exim_gnutls_state_init = { - NULL, NULL, NULL, VERIFY_NONE, -1, -1, FALSE, FALSE, - NULL, NULL, NULL, + NULL, NULL, NULL, VERIFY_NONE, -1, -1, FALSE, FALSE, FALSE, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0, 0, 0, - "" }; /* Not only do we have our own APIs which don't pass around state, assuming @@ -297,11 +296,7 @@ Argument: static void extract_exim_vars_from_tls_state(exim_gnutls_state_st *state) { -gnutls_protocol_t protocol; gnutls_cipher_algorithm_t cipher; -gnutls_kx_algorithm_t kx; -gnutls_mac_algorithm_t mac; -uschar *p; #ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING int old_pool; int rc; @@ -316,26 +311,7 @@ cipher = gnutls_cipher_get(state->session); /* returns size in "bytes" */ tls_bits = gnutls_cipher_get_key_size(cipher) * 8; -if (!*state->cipherbuf) - { - protocol = gnutls_protocol_get_version(state->session); - mac = gnutls_mac_get(state->session); - kx = gnutls_kx_get(state->session); - - string_format(state->cipherbuf, sizeof(state->cipherbuf), - "%s:%s:%u", - gnutls_protocol_get_name(protocol), - gnutls_cipher_suite_get_name(kx, cipher, mac), - tls_bits); - - /* I don't see a way that spaces could occur, in the current GnuTLS - code base, but it was a concern in the old code and perhaps older GnuTLS - releases did return "TLS 1.0"; play it safe, just in case. */ - for (p = state->cipherbuf; *p != '\0'; ++p) - if (isspace(*p)) - *p = '-'; - } -tls_cipher = state->cipherbuf; +tls_cipher = state->ciphersuite; DEBUG(D_tls) debug_printf("cipher: %s\n", tls_cipher); @@ -419,6 +395,15 @@ DEBUG(D_tls) dh_bits); #endif +/* Some clients have hard-coded limits. */ +if (dh_bits > tls_dh_max_bits) + { + DEBUG(D_tls) + debug_printf("tls_dh_max_bits clamping override, using %d bits instead.\n", + tls_dh_max_bits); + dh_bits = tls_dh_max_bits; + } + if (!string_format(filename, sizeof(filename), "%s/gnutls-params-%d", spool_directory, dh_bits)) return tls_error(US"overlong filename", NULL, NULL); @@ -527,6 +512,7 @@ if (rc < 0) m.data = malloc(m.size); if (m.data == NULL) return tls_error(US"memory allocation failed", strerror(errno), NULL); + /* this will return a size 1 less than the allocation size above */ rc = gnutls_dh_params_export_pkcs3(dh_server_params, GNUTLS_X509_FMT_PEM, m.data, &sz); if (rc != GNUTLS_E_SUCCESS) @@ -534,6 +520,7 @@ if (rc < 0) free(m.data); exim_gnutls_err_check(US"gnutls_dh_params_export_pkcs3() real"); } + m.size = sz; /* shrink by 1, probably */ sz = write_to_fd_buf(fd, m.data, (size_t) m.size); if (sz != m.size) @@ -602,7 +589,7 @@ if (!state->host) { if (!state->received_sni) { - if (Ustrstr(state->tls_certificate, US"tls_sni")) + if (state->tls_certificate && Ustrstr(state->tls_certificate, US"tls_sni")) { DEBUG(D_tls) debug_printf("We will re-expand TLS session files if we receive SNI.\n"); state->trigger_sni_changes = TRUE; @@ -719,16 +706,18 @@ if (Ustat(state->exp_tls_verify_certificates, &statbuf) < 0) return DEFER; } -if (!S_ISREG(statbuf.st_mode)) +/* The test suite passes in /dev/null; we could check for that path explicitly, +but who knows if someone has some weird FIFO which always dumps some certs, or +other weirdness. The thing we really want to check is that it's not a +directory, since while OpenSSL supports that, GnuTLS does not. +So s/!S_ISREG/S_ISDIR/ and change some messsaging ... */ +if (S_ISDIR(statbuf.st_mode)) { DEBUG(D_tls) - debug_printf("verify certificates path is not a file: \"%s\"\n%s\n", - state->exp_tls_verify_certificates, - S_ISDIR(statbuf.st_mode) - ? " it's a directory, that's OpenSSL, this is GnuTLS" - : " (not a directory either)"); + debug_printf("verify certificates path is a dir: \"%s\"\n", + state->exp_tls_verify_certificates); log_write(0, LOG_MAIN|LOG_PANIC, - "tls_verify_certificates \"%s\" is not a file", + "tls_verify_certificates \"%s\" is a directory", state->exp_tls_verify_certificates); return DEFER; } @@ -752,15 +741,18 @@ if (cert_count < 0) } DEBUG(D_tls) debug_printf("Added %d certificate authorities.\n", cert_count); -if (state->tls_crl && *state->tls_crl) +if (state->tls_crl && *state->tls_crl && + state->exp_tls_crl && *state->exp_tls_crl) { - if (state->exp_tls_crl && *state->exp_tls_crl) + DEBUG(D_tls) debug_printf("loading CRL file = %s\n", state->exp_tls_crl); + cert_count = gnutls_certificate_set_x509_crl_file(state->x509_cred, + CS state->exp_tls_crl, GNUTLS_X509_FMT_PEM); + if (cert_count < 0) { - DEBUG(D_tls) debug_printf("loading CRL file = %s\n", state->exp_tls_crl); - rc = gnutls_certificate_set_x509_crl_file(state->x509_cred, - CS state->exp_tls_crl, GNUTLS_X509_FMT_PEM); + rc = cert_count; exim_gnutls_err_check(US"gnutls_certificate_set_x509_crl_file"); } + DEBUG(D_tls) debug_printf("Processed %d CRLs.\n", cert_count); } return OK; @@ -889,6 +881,7 @@ state->host = host; state->tls_certificate = certificate; state->tls_privatekey = privatekey; +state->tls_require_ciphers = require_ciphers; state->tls_sni = sni; state->tls_verify_certificates = cas; state->tls_crl = crl; @@ -994,7 +987,22 @@ return OK; *************************************************/ /* Called from both server and client code. -Only this is allowed to set state->peerdn and we use that to detect double-calls. +Only this is allowed to set state->peerdn and state->have_set_peerdn +and we use that to detect double-calls. + +NOTE: the state blocks last while the TLS connection is up, which is fine +for logging in the server side, but for the client side, we log after teardown +in src/deliver.c. While the session is up, we can twist about states and +repoint tls_* globals, but those variables used for logging or other variable +expansion that happens _after_ delivery need to have a longer life-time. + +So for those, we get the data from POOL_PERM; the re-invoke guard keeps us from +doing this more than once per generation of a state context. We set them in +the state context, and repoint tls_* to them. After the state goes away, the +tls_* copies of the pointers remain valid and client delivery logging is happy. + +tls_certificate_verified is a BOOL, so the tls_peerdn and tls_cipher issues +don't apply. Arguments: state exim_gnutls_state_st * @@ -1005,24 +1013,54 @@ Returns: OK/DEFER/FAIL static int peer_status(exim_gnutls_state_st *state) { +uschar cipherbuf[256]; const gnutls_datum *cert_list; -int rc; +int old_pool, rc; unsigned int cert_list_size = 0; +gnutls_protocol_t protocol; +gnutls_cipher_algorithm_t cipher; +gnutls_kx_algorithm_t kx; +gnutls_mac_algorithm_t mac; gnutls_certificate_type_t ct; gnutls_x509_crt_t crt; -uschar *dn_buf; +uschar *p, *dn_buf; size_t sz; -if (state->peerdn) +if (state->have_set_peerdn) return OK; +state->have_set_peerdn = TRUE; + +state->peerdn = NULL; -state->peerdn = US"unknown"; +/* tls_cipher */ +cipher = gnutls_cipher_get(state->session); +protocol = gnutls_protocol_get_version(state->session); +mac = gnutls_mac_get(state->session); +kx = gnutls_kx_get(state->session); + +string_format(cipherbuf, sizeof(cipherbuf), + "%s:%s:%d", + gnutls_protocol_get_name(protocol), + gnutls_cipher_suite_get_name(kx, cipher, mac), + (int) gnutls_cipher_get_key_size(cipher) * 8); + +/* I don't see a way that spaces could occur, in the current GnuTLS +code base, but it was a concern in the old code and perhaps older GnuTLS +releases did return "TLS 1.0"; play it safe, just in case. */ +for (p = cipherbuf; *p != '\0'; ++p) + if (isspace(*p)) + *p = '-'; +old_pool = store_pool; +store_pool = POOL_PERM; +state->ciphersuite = string_copy(cipherbuf); +store_pool = old_pool; +tls_cipher = state->ciphersuite; +/* tls_peerdn */ cert_list = gnutls_certificate_get_peers(state->session, &cert_list_size); if (cert_list == NULL || cert_list_size == 0) { - state->peerdn = US"unknown (no certificate)"; DEBUG(D_tls) debug_printf("TLS: no certificate from peer (%p & %d)\n", cert_list, cert_list_size); if (state->verify_requirement == VERIFY_REQUIRED) @@ -1035,7 +1073,6 @@ ct = gnutls_certificate_type_get(state->session); if (ct != GNUTLS_CRT_X509) { const char *ctn = gnutls_certificate_type_get_name(ct); - state->peerdn = string_sprintf("unknown (type %s)", ctn); DEBUG(D_tls) debug_printf("TLS: peer cert not X.509 but instead \"%s\"\n", ctn); if (state->verify_requirement == VERIFY_REQUIRED) @@ -1122,7 +1159,7 @@ if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0) DEBUG(D_tls) debug_printf("TLS certificate verification failed (%s): peerdn=%s\n", - *error, state->peerdn); + *error, state->peerdn ? state->peerdn : US""); if (state->verify_requirement == VERIFY_REQUIRED) { @@ -1135,7 +1172,8 @@ if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0) else { state->peer_cert_verified = TRUE; - DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n", state->peerdn); + DEBUG(D_tls) debug_printf("TLS certificate verified: peerdn=%s\n", + state->peerdn ? state->peerdn : US""); } tls_peerdn = state->peerdn; @@ -1157,7 +1195,14 @@ return TRUE; static void exim_gnutls_logger_cb(int level, const char *message) { - DEBUG(D_tls) debug_printf("GnuTLS<%d>: %s\n", level, message); + size_t len = strlen(message); + if (len < 1) + { + DEBUG(D_tls) debug_printf("GnuTLS<%d> empty debug message\n", level); + return; + } + DEBUG(D_tls) debug_printf("GnuTLS<%d>: %s%s", level, message, + message[len-1] == '\n' ? "" : "\n"); } #endif @@ -1341,7 +1386,8 @@ if (smtp_receive_timeout > 0) alarm(smtp_receive_timeout); do { rc = gnutls_handshake(state->session); - } while ((rc == GNUTLS_E_AGAIN) || (rc == GNUTLS_E_INTERRUPTED)); + } while ((rc == GNUTLS_E_AGAIN) || + (rc == GNUTLS_E_INTERRUPTED && !sigalrm_seen)); alarm(0); if (rc != GNUTLS_E_SUCCESS) @@ -1476,9 +1522,14 @@ alarm(timeout); do { rc = gnutls_handshake(state->session); - } while ((rc == GNUTLS_E_AGAIN) || (rc == GNUTLS_E_INTERRUPTED)); + } while ((rc == GNUTLS_E_AGAIN) || + (rc == GNUTLS_E_INTERRUPTED && !sigalrm_seen)); alarm(0); +if (rc != GNUTLS_E_SUCCESS) + return tls_error(US"gnutls_handshake", + sigalrm_seen ? "timed out" : gnutls_strerror(rc), state->host); + DEBUG(D_tls) debug_printf("gnutls_handshake was successful\n"); /* Verify late */ @@ -1492,7 +1543,7 @@ if (state->verify_requirement != VERIFY_NONE && rc = peer_status(state); if (rc != OK) return rc; -/* Sets various Exim expansion variables; always safe within server */ +/* Sets various Exim expansion variables; may need to adjust for ACL callouts */ extract_exim_vars_from_tls_state(state); @@ -1789,6 +1840,64 @@ vaguely_random_number(int max) +/************************************************* +* Let tls_require_ciphers be checked at startup * +*************************************************/ + +/* The tls_require_ciphers option, if set, must be something which the +library can parse. + +Returns: NULL on success, or error message +*/ + +uschar * +tls_validate_require_cipher(void) +{ +int rc; +uschar *expciphers = NULL; +gnutls_priority_t priority_cache; +const char *errpos; + +#define validate_check_rc(Label) do { \ + if (rc != GNUTLS_E_SUCCESS) { if (exim_gnutls_base_init_done) gnutls_global_deinit(); \ + return string_sprintf("%s failed: %s", (Label), gnutls_strerror(rc)); } } while (0) +#define return_deinit(Label) do { gnutls_global_deinit(); return (Label); } while (0) + +if (exim_gnutls_base_init_done) + log_write(0, LOG_MAIN|LOG_PANIC, + "already initialised GnuTLS, Exim developer bug"); + +rc = gnutls_global_init(); +validate_check_rc(US"gnutls_global_init()"); +exim_gnutls_base_init_done = TRUE; + +if (!(tls_require_ciphers && *tls_require_ciphers)) + return_deinit(NULL); + +if (!expand_check(tls_require_ciphers, US"tls_require_ciphers", &expciphers)) + return_deinit(US"failed to expand tls_require_ciphers"); + +if (!(expciphers && *expciphers)) + return_deinit(NULL); + +DEBUG(D_tls) + debug_printf("tls_require_ciphers expands to \"%s\"\n", expciphers); + +rc = gnutls_priority_init(&priority_cache, CS expciphers, &errpos); +validate_check_rc(string_sprintf( + "gnutls_priority_init(%s) failed at offset %ld, \"%.8s..\"", + expciphers, errpos - CS expciphers, errpos)); + +#undef return_deinit +#undef validate_check_rc +gnutls_global_deinit(); + +return NULL; +} + + + + /************************************************* * Report the library versions. * *************************************************/