X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/17c761988f30054827a9951761d93ffeeaad0cb7..bba74fc65f77dc6678b3d33eef0acf43efe8f653:/src/src/tls-gnu.c diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c index f0e391f97..c8bf634bc 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 @@ -140,12 +139,6 @@ static const char * const exim_default_gnutls_priority = "NORMAL"; static BOOL exim_gnutls_base_init_done = FALSE; -/* ------------------------------------------------------------------------ */ -/* Callback declarations */ - -static void exim_gnutls_logger_cb(int level, const char *message); -static int exim_sni_handling_cb(gnutls_session_t session); - /* ------------------------------------------------------------------------ */ /* macros */ @@ -154,24 +147,47 @@ static int exim_sni_handling_cb(gnutls_session_t session); /* Set this to control gnutls_global_set_log_level(); values 0 to 9 will setup the library logging; a value less than 0 disables the calls to set up logging callbacks. */ +#ifndef EXIM_GNUTLS_LIBRARY_LOG_LEVEL #define EXIM_GNUTLS_LIBRARY_LOG_LEVEL -1 +#endif +#ifndef EXIM_CLIENT_DH_MIN_BITS #define EXIM_CLIENT_DH_MIN_BITS 1024 +#endif + +/* With GnuTLS 2.12.x+ we have gnutls_sec_param_to_pk_bits() with which we +can ask for a bit-strength. Without that, we stick to the constant we had +before, for now. */ +#ifndef EXIM_SERVER_DH_BITS_PRE2_12 +#define EXIM_SERVER_DH_BITS_PRE2_12 1024 +#endif #define exim_gnutls_err_check(Label) do { \ if (rc != GNUTLS_E_SUCCESS) { return tls_error((Label), gnutls_strerror(rc), host); } } while (0) -#define exim_gnutls_err_debugreturn0(Label) do { \ - if (rc != GNUTLS_E_SUCCESS) { \ - DEBUG(D_tls) debug_printf("TLS failure: %s: %s", (Label), gnutls_strerror(rc)); \ - return 0; } } while (0) - #define expand_check_tlsvar(Varname) expand_check(state->Varname, US #Varname, &state->exp_##Varname) #if GNUTLS_VERSION_NUMBER >= 0x020c00 #define HAVE_GNUTLS_SESSION_CHANNEL_BINDING +#define HAVE_GNUTLS_SEC_PARAM_CONSTANTS +#define HAVE_GNUTLS_RND #endif + + + +/* ------------------------------------------------------------------------ */ +/* Callback declarations */ + +#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0 +static void exim_gnutls_logger_cb(int level, const char *message); +#endif + +static int exim_sni_handling_cb(gnutls_session_t session); + + + + /* ------------------------------------------------------------------------ */ /* Static functions */ @@ -280,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; @@ -299,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); @@ -365,9 +358,6 @@ file is never present. If two processes both compute some new parameters, you waste a bit of effort, but it doesn't seem worth messing around with locking to prevent this. -Argument: - host NULL for server, server for client (for error handling) - Returns: OK/DEFER/FAIL */ @@ -377,25 +367,91 @@ init_server_dh(void) int fd, rc; unsigned int dh_bits; gnutls_datum m; -uschar filename[PATH_MAX]; +uschar filename_buf[PATH_MAX]; +uschar *filename = NULL; size_t sz; +uschar *exp_tls_dhparam; +BOOL use_file_in_spool = FALSE; +BOOL use_fixed_file = FALSE; host_item *host = NULL; /* dummy for macros */ -const char * const dh_param_fn_ext = "normal"; /* change as dh_bits changes */ DEBUG(D_tls) debug_printf("Initialising GnuTLS server params.\n"); rc = gnutls_dh_params_init(&dh_server_params); exim_gnutls_err_check(US"gnutls_dh_params_init"); -/* If you change this, also change dh_param_fn_ext so that we can use a +m.data = NULL; +m.size = 0; + +if (!expand_check(tls_dhparam, US"tls_dhparam", &exp_tls_dhparam)) + return DEFER; + +if (!exp_tls_dhparam) + { + DEBUG(D_tls) debug_printf("Loading default hard-coded DH params\n"); + m.data = US std_dh_prime_default(); + m.size = Ustrlen(m.data); + } +else if (Ustrcmp(exp_tls_dhparam, "historic") == 0) + use_file_in_spool = TRUE; +else if (Ustrcmp(exp_tls_dhparam, "none") == 0) + { + DEBUG(D_tls) debug_printf("Requested no DH parameters.\n"); + return OK; + } +else if (exp_tls_dhparam[0] != '/') + { + m.data = US std_dh_prime_named(exp_tls_dhparam); + if (m.data == NULL) + return tls_error(US"No standard prime named", CS exp_tls_dhparam, NULL); + m.size = Ustrlen(m.data); + } +else + { + use_fixed_file = TRUE; + filename = exp_tls_dhparam; + } + +if (m.data) + { + rc = gnutls_dh_params_import_pkcs3(dh_server_params, &m, GNUTLS_X509_FMT_PEM); + exim_gnutls_err_check(US"gnutls_dh_params_import_pkcs3"); + DEBUG(D_tls) debug_printf("Loaded fixed standard D-H parameters\n"); + return OK; + } + +#ifdef HAVE_GNUTLS_SEC_PARAM_CONSTANTS +/* If you change this constant, also change dh_param_fn_ext so that we can use a different filename and ensure we have sufficient bits. */ dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_NORMAL); if (!dh_bits) return tls_error(US"gnutls_sec_param_to_pk_bits() failed", NULL, NULL); +DEBUG(D_tls) + debug_printf("GnuTLS tells us that for D-H PK, NORMAL is %d bits.\n", + dh_bits); +#else +dh_bits = EXIM_SERVER_DH_BITS_PRE2_12; +DEBUG(D_tls) + debug_printf("GnuTLS lacks gnutls_sec_param_to_pk_bits(), using %d bits.\n", + dh_bits); +#endif -if (!string_format(filename, sizeof(filename), - "%s/gnutls-params-%s", spool_directory, dh_param_fn_ext)) - return tls_error(US"overlong filename", NULL, NULL); +/* 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 (use_file_in_spool) + { + if (!string_format(filename_buf, sizeof(filename_buf), + "%s/gnutls-params-%d", spool_directory, dh_bits)) + return tls_error(US"overlong filename", NULL, NULL); + filename = filename_buf; + } /* Open the cache file for reading and if successful, read it and set up the parameters. */ @@ -472,6 +528,7 @@ case. */ if (rc < 0) { uschar *temp_fn; + unsigned int dh_bits_gen = dh_bits; if ((PATH_MAX - Ustrlen(filename)) < 10) return tls_error(US"Filename too long to generate replacement", @@ -483,8 +540,26 @@ if (rc < 0) return tls_error(US"Unable to open temp file", strerror(errno), NULL); (void)fchown(fd, exim_uid, exim_gid); /* Probably not necessary */ - DEBUG(D_tls) debug_printf("generating %d bits Diffie-Hellman key ...\n", dh_bits); - rc = gnutls_dh_params_generate2(dh_server_params, dh_bits); + /* GnuTLS overshoots! + * If we ask for 2236, we might get 2237 or more. + * But there's no way to ask GnuTLS how many bits there really are. + * We can ask how many bits were used in a TLS session, but that's it! + * The prime itself is hidden behind too much abstraction. + * So we ask for less, and proceed on a wing and a prayer. + * First attempt, subtracted 3 for 2233 and got 2240. + */ + if (dh_bits >= EXIM_CLIENT_DH_MIN_BITS + 10) + { + dh_bits_gen = dh_bits - 10; + DEBUG(D_tls) + debug_printf("being paranoid about DH generation, make it '%d' bits'\n", + dh_bits_gen); + } + + DEBUG(D_tls) + debug_printf("requesting generation of %d bit Diffie-Hellman prime ...\n", + dh_bits_gen); + rc = gnutls_dh_params_generate2(dh_server_params, dh_bits_gen); exim_gnutls_err_check(US"gnutls_dh_params_generate2"); /* gnutls_dh_params_export_pkcs3() will tell us the exact size, every time, @@ -501,6 +576,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) @@ -508,6 +584,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) @@ -562,6 +639,7 @@ Returns: OK/DEFER/FAIL static int tls_expand_session_files(exim_gnutls_state_st *state) { +struct stat statbuf; int rc; const host_item *host = state->host; /* macro should be reconsidered? */ uschar *saved_tls_certificate = NULL; @@ -575,7 +653,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; @@ -583,6 +661,7 @@ if (!state->host) } else { + /* useful for debugging */ saved_tls_certificate = state->exp_tls_certificate; saved_tls_privatekey = state->exp_tls_privatekey; saved_tls_verify_certificates = state->exp_tls_verify_certificates; @@ -590,6 +669,9 @@ if (!state->host) } } +rc = gnutls_certificate_allocate_credentials(&state->x509_cred); +exim_gnutls_err_check(US"gnutls_certificate_allocate_credentials"); + /* remember: expand_check_tlsvar() is expand_check() but fiddling with state members, assuming consistent naming; and expand_check() returns false if expansion failed, unless expansion was forced to fail. */ @@ -625,7 +707,6 @@ if (state->tls_privatekey == NULL || *state->tls_privatekey == '\0') if (state->exp_tls_certificate && *state->exp_tls_certificate) { - BOOL setit = TRUE; DEBUG(D_tls) debug_printf("certificate file = %s\nkey file = %s\n", state->exp_tls_certificate, state->exp_tls_privatekey); @@ -634,25 +715,22 @@ if (state->exp_tls_certificate && *state->exp_tls_certificate) if ((Ustrcmp(state->exp_tls_certificate, saved_tls_certificate) == 0) && (Ustrcmp(state->exp_tls_privatekey, saved_tls_privatekey) == 0)) { - DEBUG(D_tls) debug_printf("cert and key unchanged with SNI.\n"); - setit = FALSE; + DEBUG(D_tls) debug_printf("TLS SNI: cert and key unchanged\n"); } else { - DEBUG(D_tls) debug_printf("SNI changed cert/key pair.\n"); + DEBUG(D_tls) debug_printf("TLS SNI: have a changed cert/key pair.\n"); } } - if (setit) - { - rc = gnutls_certificate_set_x509_key_file(state->x509_cred, - CS state->exp_tls_certificate, CS state->exp_tls_privatekey, - GNUTLS_X509_FMT_PEM); - exim_gnutls_err_check( - string_sprintf("cert/key setup: cert=%s key=%s", - state->exp_tls_certificate, state->exp_tls_privatekey)); - } - } + rc = gnutls_certificate_set_x509_key_file(state->x509_cred, + CS state->exp_tls_certificate, CS state->exp_tls_privatekey, + GNUTLS_X509_FMT_PEM); + exim_gnutls_err_check( + string_sprintf("cert/key setup: cert=%s key=%s", + state->exp_tls_certificate, state->exp_tls_privatekey)); + DEBUG(D_tls) debug_printf("TLS: cert/key registered\n"); + } /* tls_certificate */ /* Set the trusted CAs file if one is provided, and then add the CRL if one is provided. Experiment shows that, if the certificate file is empty, an unhelpful @@ -662,89 +740,135 @@ behaviour. */ if (state->tls_verify_certificates && *state->tls_verify_certificates) { - struct stat statbuf; - BOOL setit_vc = TRUE, setit_crl = TRUE; - if (!expand_check_tlsvar(tls_verify_certificates)) return DEFER; if (state->tls_crl && *state->tls_crl) if (!expand_check_tlsvar(tls_crl)) return DEFER; - if (state->received_sni) + if (!(state->exp_tls_verify_certificates && + *state->exp_tls_verify_certificates)) { - if (Ustrcmp(state->exp_tls_verify_certificates, saved_tls_verify_certificates) == 0) - setit_vc = FALSE; - if (Ustrcmp(state->exp_tls_crl, saved_tls_crl) == 0) - setit_crl = FALSE; - } - - /* nb: early exit; change if add more expansions to this function */ - if (!(setit_vc || setit_crl)) + DEBUG(D_tls) + debug_printf("TLS: tls_verify_certificates expanded empty, ignoring\n"); + /* With no tls_verify_certificates, we ignore tls_crl too */ return OK; - - if (Ustat(state->exp_tls_verify_certificates, &statbuf) < 0) - { - log_write(0, LOG_MAIN|LOG_PANIC, "could not stat %s " - "(tls_verify_certificates): %s", state->exp_tls_verify_certificates, - strerror(errno)); - return DEFER; } + } +else + { + DEBUG(D_tls) + debug_printf("TLS: tls_verify_certificates not set or empty, ignoring\n"); + return OK; + } - if (!S_ISREG(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)"); - log_write(0, LOG_MAIN|LOG_PANIC, - "tls_verify_certificates \"%s\" is not a file", +if (Ustat(state->exp_tls_verify_certificates, &statbuf) < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "could not stat %s " + "(tls_verify_certificates): %s", state->exp_tls_verify_certificates, + strerror(errno)); + return DEFER; + } + +/* 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 a dir: \"%s\"\n", state->exp_tls_verify_certificates); - return DEFER; - } + log_write(0, LOG_MAIN|LOG_PANIC, + "tls_verify_certificates \"%s\" is a directory", + state->exp_tls_verify_certificates); + return DEFER; + } - DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n", - state->exp_tls_verify_certificates, statbuf.st_size); +DEBUG(D_tls) debug_printf("verify certificates = %s size=" OFF_T_FMT "\n", + state->exp_tls_verify_certificates, statbuf.st_size); - /* If the CA cert file is empty, there's no point in loading the CRL file, - as we aren't verifying, so checking for revocation is pointless. */ +if (statbuf.st_size == 0) + { + DEBUG(D_tls) + debug_printf("cert file empty, no certs, no verification, ignoring any CRL\n"); + return OK; + } - if (statbuf.st_size > 0) - { - if (setit_vc) - { - cert_count = gnutls_certificate_set_x509_trust_file(state->x509_cred, - CS state->exp_tls_verify_certificates, GNUTLS_X509_FMT_PEM); - if (cert_count < 0) - { - rc = cert_count; - exim_gnutls_err_check(US"gnutls_certificate_set_x509_trust_file"); - } - DEBUG(D_tls) debug_printf("Added %d certificate authorities.\n", cert_count); - } +cert_count = gnutls_certificate_set_x509_trust_file(state->x509_cred, + CS state->exp_tls_verify_certificates, GNUTLS_X509_FMT_PEM); +if (cert_count < 0) + { + rc = cert_count; + exim_gnutls_err_check(US"gnutls_certificate_set_x509_trust_file"); + } +DEBUG(D_tls) debug_printf("Added %d certificate authorities.\n", cert_count); - if (setit_crl && state->tls_crl && *state->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); - rc = gnutls_certificate_set_x509_crl_file(state->x509_cred, - CS state->exp_tls_crl, GNUTLS_X509_FMT_PEM); - exim_gnutls_err_check(US"gnutls_certificate_set_x509_crl_file"); - } - } - } /* statbuf.st_size */ - } /* tls_verify_certificates */ +if (state->tls_crl && *state->tls_crl && + 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) + { + 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; -/* also above, during verify_certificates/crl, during SNI, if unchanged */ } +/************************************************* +* Set X.509 state variables * +*************************************************/ + +/* In GnuTLS, the registered cert/key are not replaced by a later +set of a cert/key, so for SNI support we need a whole new x509_cred +structure. Which means various other non-re-expanded pieces of state +need to be re-set in the new struct, so the setting logic is pulled +out to this. + +Arguments: + state exim_gnutls_state_st * + +Returns: OK/DEFER/FAIL +*/ + +static int +tls_set_remaining_x509(exim_gnutls_state_st *state) +{ +int rc; +const host_item *host = state->host; /* macro should be reconsidered? */ + +/* Create D-H parameters, or read them from the cache file. This function does +its own SMTP error messaging. This only happens for the server, TLS D-H ignores +client-side params. */ + +if (!state->host) + { + if (!dh_server_params) + { + rc = init_server_dh(); + if (rc != OK) return rc; + } + gnutls_certificate_set_dh_params(state->x509_cred, dh_server_params); + } + +/* Link the credentials to the session. */ + +rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->x509_cred); +exim_gnutls_err_check(US"gnutls_credentials_set"); + +return OK; +} + /************************************************* * Initialize for GnuTLS * *************************************************/ @@ -821,13 +945,11 @@ 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; -rc = gnutls_certificate_allocate_credentials(&state->x509_cred); -exim_gnutls_err_check(US"gnutls_certificate_allocate_credentials"); - /* This handles the variables that might get re-expanded after TLS SNI; that's tls_certificate, tls_privatekey, tls_verify_certificates, tls_crl */ @@ -836,21 +958,11 @@ DEBUG(D_tls) rc = tls_expand_session_files(state); if (rc != OK) return rc; -/* Create D-H parameters, or read them from the cache file. This function does -its own SMTP error messaging. This only happens for the server, TLS D-H ignores -client-side params. */ - -if (!host) - { - rc = init_server_dh(); - if (rc != OK) return rc; - gnutls_certificate_set_dh_params(state->x509_cred, dh_server_params); - } +/* These are all other parts of the x509_cred handling, since SNI in GnuTLS +requires a new structure afterwards. */ -/* Link the credentials to the session. */ - -rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->x509_cred); -exim_gnutls_err_check(US"gnutls_credentials_set"); +rc = tls_set_remaining_x509(state); +if (rc != OK) return rc; /* set SNI in client, only */ if (host) @@ -896,6 +1008,9 @@ if (state->tls_require_ciphers && *state->tls_require_ciphers) } if (want_default_priorities) { + DEBUG(D_tls) + debug_printf("GnuTLS using default session cipher/priority \"%s\"\n", + exim_default_gnutls_priority); rc = gnutls_priority_init(&state->priority_cache, exim_default_gnutls_priority, &errpos); p = US exim_default_gnutls_priority; @@ -936,7 +1051,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 * @@ -947,24 +1077,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 = US"unknown"; +state->peerdn = NULL; +/* 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) @@ -977,7 +1137,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) @@ -1064,7 +1223,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) { @@ -1072,12 +1231,13 @@ if ((rc < 0) || (verify & (GNUTLS_CERT_INVALID|GNUTLS_CERT_REVOKED)) != 0) return FALSE; } DEBUG(D_tls) - debug_printf("TLS verify failure overriden (host in tls_try_verify_hosts)\n"); + debug_printf("TLS verify failure overridden (host in tls_try_verify_hosts)\n"); } 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; @@ -1095,11 +1255,20 @@ return TRUE; * gnutls_global_set_log_function() * gnutls_global_set_log_level() 0..9 */ +#if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0 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 /* Called after client hello, should handle SNI work. @@ -1127,7 +1296,18 @@ unsigned int sni_type; int rc, old_pool; rc = gnutls_server_name_get(session, sni_name, &data_len, &sni_type, 0); -exim_gnutls_err_debugreturn0("gnutls_server_name_get()"); +if (rc != GNUTLS_E_SUCCESS) + { + DEBUG(D_tls) { + if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) + debug_printf("TLS: no SNI presented in handshake.\n"); + else + debug_printf("TLS failure: gnutls_server_name_get(): %s [%d]\n", + gnutls_strerror(rc), rc); + }; + return 0; + } + if (sni_type != GNUTLS_NAME_DNS) { DEBUG(D_tls) debug_printf("TLS: ignoring SNI of unhandled type %u\n", sni_type); @@ -1157,8 +1337,10 @@ if (rc != OK) return GNUTLS_E_APPLICATION_ERROR_MIN; } -rc = gnutls_credentials_set(state->session, GNUTLS_CRD_CERTIFICATE, state->x509_cred); -return (rc == GNUTLS_E_SUCCESS) ? 0 : rc; +rc = tls_set_remaining_x509(state); +if (rc != OK) return GNUTLS_E_APPLICATION_ERROR_MIN; + +return 0; } @@ -1268,7 +1450,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) @@ -1403,9 +1586,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 */ @@ -1419,7 +1607,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); @@ -1667,6 +1855,7 @@ Arguments: Returns a random number in range [0, max-1] */ +#ifdef HAVE_GNUTLS_RND int vaguely_random_number(int max) { @@ -1704,6 +1893,71 @@ for (p = smallbuf; needed_len; --needed_len, ++p) * smooth distribution and cares enough then they should submit a patch then. */ return r % max; } +#else /* HAVE_GNUTLS_RND */ +int +vaguely_random_number(int max) +{ + return vaguely_random_number_fallback(max); +} +#endif /* HAVE_GNUTLS_RND */ + + + + +/************************************************* +* 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; +}