From fc243e944ec00b59b75f41d07494116f925d58b4 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Sat, 16 Feb 2019 12:59:23 +0000 Subject: [PATCH] GnuTLS: Fix client detection of server reject of client cert under TLS1.3 --- doc/doc-txt/ChangeLog | 9 +++- src/src/deliver.c | 2 +- src/src/smtp_out.c | 10 +++-- src/src/tls-gnu.c | 23 +++------- src/src/transports/lmtp.c | 3 +- src/src/transports/smtp.c | 81 +++++++++++++++++++++++++++-------- test/confs/2027 | 8 ++-- test/confs/5652 | 1 + test/confs/5821 | 2 +- test/log/2027 | 2 +- test/runtest | 16 ++++++- test/scripts/2000-GnuTLS/2027 | 2 + 12 files changed, 113 insertions(+), 46 deletions(-) diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index 18db733aa..6a9aae365 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -11,7 +11,7 @@ Exim version 4.93 JH/01 OpenSSL: With debug enabled output keying information sufficient, server side, to decode a TLS 1.3 packet capture. -JH/02 OpenSSL: suppress the sending of (stateful) TLS1.3 session tickets. +JH/02 OpenSSL: Suppress the sending of (stateful) TLS1.3 session tickets. Previously the default library behaviour applied, sending two, each in its own TCP segment. @@ -25,6 +25,13 @@ JH/05 DKIM: ensure that dkim_domain elements are lowercased before use. JH/06 Fix buggy handling of autoreply bounce_return_size_limit, and a possible buffer overrun for (non-chunking) other transports. +JH/07 GnuTLS: Our use of late (post-handshake) certificate verification, under + TLS1.3, means that a server rejecting a client certificate is not visible + to the client until the first read of encrypted data (typically the + response to EHLO). Add detection for that case and treat it as a failed + TLS connection attempt, so that the normal retry-in-clear can work (if + suitably configured). + Exim version 4.92 ----------------- diff --git a/src/src/deliver.c b/src/src/deliver.c index ebb64778c..506470070 100644 --- a/src/src/deliver.c +++ b/src/src/deliver.c @@ -7401,7 +7401,7 @@ if (addr_senddsn) tctx.u.fd = fd; tctx.options = topt_add_return_path | topt_no_body; - /*XXX hmm, retval ignored. + /*XXX hmm, FALSE(fail) retval ignored. Could error for any number of reasons, and they are not handled. */ transport_write_message(&tctx, 0); fflush(f); diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c index 8a4dace7e..b7a835479 100644 --- a/src/src/smtp_out.c +++ b/src/src/smtp_out.c @@ -690,20 +690,22 @@ Returns: TRUE if a valid, non-error response was received; else FALSE /*XXX could move to smtp transport; no other users */ BOOL -smtp_read_response(void * sx0, uschar *buffer, int size, int okdigit, +smtp_read_response(void * sx0, uschar * buffer, int size, int okdigit, int timeout) { smtp_context * sx = sx0; -uschar *ptr = buffer; -int count = 0; +uschar * ptr = buffer; +int count = 0, rc; errno = 0; /* Ensure errno starts out zero */ #ifdef EXPERIMENTAL_PIPE_CONNECT if (sx->pending_BANNER || sx->pending_EHLO) - if (smtp_reap_early_pipe(sx, &count) != OK) + if ((rc = smtp_reap_early_pipe(sx, &count)) != OK) { DEBUG(D_transport) debug_printf("failed reaping pipelined cmd responsess\n"); + buffer[0] = '\0'; + if (rc == DEFER) errno = ERRNO_TLSFAILURE; return FALSE; } #endif diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c index faad38f76..1752bf3d8 100644 --- a/src/src/tls-gnu.c +++ b/src/src/tls-gnu.c @@ -238,7 +238,7 @@ static gnutls_dh_params_t dh_server_params = NULL; static const int ssl_session_timeout = 200; -static const char * const exim_default_gnutls_priority = "NORMAL"; +static const uschar * const exim_default_gnutls_priority = US"NORMAL"; /* Guard library core initialisation */ @@ -1287,7 +1287,6 @@ int rc; size_t sz; const char *errpos; uschar *p; -BOOL want_default_priorities; if (!exim_gnutls_base_init_done) { @@ -1396,32 +1395,24 @@ and replaces gnutls_require_kx, gnutls_require_mac & gnutls_require_protocols. This was backwards incompatible, but means Exim no longer needs to track all algorithms and provide string forms for them. */ -want_default_priorities = TRUE; - +p = NULL; if (state->tls_require_ciphers && *state->tls_require_ciphers) { if (!expand_check_tlsvar(tls_require_ciphers, errstr)) return DEFER; if (state->exp_tls_require_ciphers && *state->exp_tls_require_ciphers) { - DEBUG(D_tls) debug_printf("GnuTLS session cipher/priority \"%s\"\n", - state->exp_tls_require_ciphers); - - rc = gnutls_priority_init(&state->priority_cache, - CS state->exp_tls_require_ciphers, &errpos); - want_default_priorities = FALSE; p = state->exp_tls_require_ciphers; + DEBUG(D_tls) debug_printf("GnuTLS session cipher/priority \"%s\"\n", p); } } -if (want_default_priorities) +if (!p) { + p = exim_default_gnutls_priority; 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; + debug_printf("GnuTLS using default session cipher/priority \"%s\"\n", p); } +rc = gnutls_priority_init(&state->priority_cache, CCS p, &errpos); exim_gnutls_err_check(rc, string_sprintf( "gnutls_priority_init(%s) failed at offset %ld, \"%.6s..\"", diff --git a/src/src/transports/lmtp.c b/src/src/transports/lmtp.c index f007f4a97..b2bf5f0da 100644 --- a/src/src/transports/lmtp.c +++ b/src/src/transports/lmtp.c @@ -122,7 +122,8 @@ Arguments: Returns: TRUE if a "QUIT" command should be sent, else FALSE */ -static BOOL check_response(int *errno_value, int more_errno, uschar *buffer, +static BOOL +check_response(int *errno_value, int more_errno, uschar *buffer, int *yield, uschar **message) { *yield = '4'; /* Default setting is to give a temporary error */ diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c index 9ee1304bc..6ff993d57 100644 --- a/src/src/transports/smtp.c +++ b/src/src/transports/smtp.c @@ -593,6 +593,11 @@ switch(*errno_value) pl, smtp_command, s); return FALSE; + case ERRNO_TLSFAILURE: /* Handle bad first read; can happen with + GnuTLS and TLS1.3 */ + *message = US"bad first read from TLS conn"; + return TRUE; + case ERRNO_FILTER_FAIL: /* Handle a failed filter process error; can't send QUIT as we mustn't end the DATA. */ *message = string_sprintf("transport filter process failed (%d)%s", @@ -941,6 +946,7 @@ Arguments: Return: OK all well + DEFER error on first read of TLS'd conn FAIL SMTP error in response */ int @@ -948,6 +954,7 @@ smtp_reap_early_pipe(smtp_context * sx, int * countp) { BOOL pending_BANNER = sx->pending_BANNER; BOOL pending_EHLO = sx->pending_EHLO; +int rc = FAIL; sx->pending_BANNER = FALSE; /* clear early to avoid recursion */ sx->pending_EHLO = FALSE; @@ -959,6 +966,7 @@ if (pending_BANNER) if (!smtp_reap_banner(sx)) { DEBUG(D_transport) debug_printf("bad banner\n"); + if (tls_out.active.sock >= 0) rc = DEFER; goto fail; } } @@ -973,6 +981,7 @@ if (pending_EHLO) if (!smtp_reap_ehlo(sx)) { DEBUG(D_transport) debug_printf("bad response for EHLO\n"); + if (tls_out.active.sock >= 0) rc = DEFER; goto fail; } @@ -1010,7 +1019,7 @@ return OK; fail: invalidate_ehlo_cache_entry(sx); (void) smtp_discard_responses(sx, sx->conn_args.ob, *countp); - return FAIL; + return rc; } #endif @@ -1055,6 +1064,7 @@ Returns: 3 if at least one address had 2xx and one had 5xx -2 I/O or other non-response error for RCPT -3 DATA or MAIL failed - errno and buffer set -4 banner or EHLO failed (early-pipelining) + -5 banner or EHLO failed (early-pipelining, TLS) */ static int @@ -1063,10 +1073,11 @@ sync_responses(smtp_context * sx, int count, int pending_DATA) address_item * addr = sx->sync_addr; smtp_transport_options_block * ob = sx->conn_args.ob; int yield = 0; +int rc; #ifdef EXPERIMENTAL_PIPE_CONNECT -if (smtp_reap_early_pipe(sx, &count) != OK) - return -4; +if ((rc = smtp_reap_early_pipe(sx, &count)) != OK) + return rc == FAIL ? -4 : -5; #endif /* Handle the response for a MAIL command. On error, reinstate the original @@ -1082,6 +1093,8 @@ if (sx->pending_MAIL) { DEBUG(D_transport) debug_printf("bad response for MAIL\n"); Ustrcpy(big_buffer, mail_command); /* Fits, because it came from there! */ + if (errno == ERRNO_TLSFAILURE) + return -5; if (errno == 0 && sx->buffer[0] != 0) { int save_errno = 0; @@ -1140,6 +1153,11 @@ while (count-- > 0) } } + /* Error on first TLS read */ + + else if (errno == ERRNO_TLSFAILURE) + return -5; + /* Timeout while reading the response */ else if (errno == ETIMEDOUT) @@ -1252,6 +1270,10 @@ if (pending_DATA != 0) int code; uschar *msg; BOOL pass_message; + + if (errno == ERRNO_TLSFAILURE) /* Error on first TLS read */ + return -5; + if (pending_DATA > 0 || (yield & 1) != 0) { if (errno == 0 && sx->buffer[0] == '4') @@ -1800,7 +1822,9 @@ Args: tc_chunk_last add LAST option to SMTP BDAT command tc_reap_prev reap response to previous SMTP commands -Returns: OK or ERROR +Returns: + OK or ERROR + DEFER TLS error on first read (EHLO-resp); errno set */ static int @@ -1857,10 +1881,12 @@ if (flags & tc_reap_prev && prev_cmd_count > 0) case 2: sx->completed_addr = TRUE; /* 5xx (only) => progress made */ case 0: break; /* No 2xx or 5xx, but no probs */ - case -1: /* Timeout on RCPT */ + case -5: errno = ERRNO_TLSFAILURE; + return DEFER; #ifdef EXPERIMENTAL_PIPE_CONNECT case -4: /* non-2xx for pipelined banner or EHLO */ #endif + case -1: /* Timeout on RCPT */ default: return ERROR; /* I/O error, or any MAIL/DATA error */ } cmd_count = 1; @@ -1931,6 +1957,9 @@ BOOL pass_message = FALSE; uschar * message = NULL; int yield = OK; int rc; +#ifdef SUPPORT_TLS +uschar * tls_errstr; +#endif sx->conn_args.ob = ob; @@ -2471,27 +2500,27 @@ if ( smtp_peer_options & OPTION_TLS else TLS_NEGOTIATE: { - uschar * errstr; sx->cctx.tls_ctx = tls_client_start(sx->cctx.sock, sx->conn_args.host, sx->addrlist, sx->conn_args.tblock, # ifdef SUPPORT_DANE sx->dane ? &tlsa_dnsa : NULL, # endif - &tls_out, &errstr); + &tls_out, &tls_errstr); if (!sx->cctx.tls_ctx) { /* TLS negotiation failed; give an error. From outside, this function may be called again to try in clear on a new connection, if the options permit it for this host. */ - DEBUG(D_tls) debug_printf("TLS session fail: %s\n", errstr); +GNUTLS_CONN_FAILED: + DEBUG(D_tls) debug_printf("TLS session fail: %s\n", tls_errstr); # ifdef SUPPORT_DANE if (sx->dane) { log_write(0, LOG_MAIN, "DANE attempt failed; TLS connection to %s [%s]: %s", - sx->conn_args.host->name, sx->conn_args.host->address, errstr); + sx->conn_args.host->name, sx->conn_args.host->address, tls_errstr); # ifndef DISABLE_EVENT (void) event_raise(sx->conn_args.tblock->event_action, US"dane:fail", US"validation-failure"); /* could do with better detail */ @@ -2500,7 +2529,7 @@ if ( smtp_peer_options & OPTION_TLS # endif errno = ERRNO_TLSFAILURE; - message = string_sprintf("TLS session: %s", errstr); + message = string_sprintf("TLS session: %s", tls_errstr); sx->send_quit = FALSE; goto TLS_FAILED; } @@ -2598,7 +2627,22 @@ if (tls_out.active.sock >= 0) #endif { if (!smtp_reap_ehlo(sx)) +#ifdef USE_GNUTLS + { + /* The GnuTLS layer in Exim only spots a server-rejection of a client + cert late, under TLS1.3 - which means here; the first time we try to + receive crypted data. Treat it as if it was a connect-time failure. + See also the early-pipe equivalent... which will be hard; every call + to sync_responses will need to check the result. + It would be nicer to have GnuTLS check the cert during the handshake. + Can it do that, with all the flexibility we need? */ + + tls_errstr = US"error on first read"; + goto GNUTLS_CONN_FAILED; + } +#else goto RESPONSE_FAILED; +#endif smtp_peer_options = 0; } } @@ -3257,6 +3301,7 @@ for (addr = sx->first_addr, address_count = 0; #ifdef EXPERIMENTAL_PIPE_CONNECT case -4: return -1; /* non-2xx for pipelined banner or EHLO */ + case -5: return -1; /* TLS first-read error */ #endif } sx->pending_MAIL = FALSE; /* Dealt with MAIL */ @@ -3582,11 +3627,12 @@ if ( !(sx.peer_offered & OPTION_CHUNKING) case 1: sx.ok = TRUE; /* 2xx (only) => OK, but if LMTP, */ if (!sx.lmtp) sx.completed_addr = TRUE; /* can't tell about progress yet */ - case 0: break; /* No 2xx or 5xx, but no probs */ + case 0: break; /* No 2xx or 5xx, but no probs */ - case -1: goto END_OFF; /* Timeout on RCPT */ + case -1: goto END_OFF; /* Timeout on RCPT */ #ifdef EXPERIMENTAL_PIPE_CONNECT + case -5: /* TLS first-read error */ case -4: HDEBUG(D_transport) debug_printf("failed reaping pipelined cmd responses\n"); #endif @@ -3723,19 +3769,20 @@ else { case 3: sx.ok = TRUE; /* 2xx & 5xx => OK & progress made */ case 2: sx.completed_addr = TRUE; /* 5xx (only) => progress made */ - break; + break; - case 1: sx.ok = TRUE; /* 2xx (only) => OK, but if LMTP, */ + case 1: sx.ok = TRUE; /* 2xx (only) => OK, but if LMTP, */ if (!sx.lmtp) sx.completed_addr = TRUE; /* can't tell about progress yet */ - case 0: break; /* No 2xx or 5xx, but no probs */ + case 0: break; /* No 2xx or 5xx, but no probs */ - case -1: goto END_OFF; /* Timeout on RCPT */ + case -1: goto END_OFF; /* Timeout on RCPT */ #ifdef EXPERIMENTAL_PIPE_CONNECT + case -5: /* TLS first-read error */ case -4: HDEBUG(D_transport) debug_printf("failed reaping pipelined cmd responses\n"); #endif - default: goto RESPONSE_FAILED; /* I/O error, or any MAIL/DATA error */ + default: goto RESPONSE_FAILED; /* I/O error, or any MAIL/DATA error */ } } diff --git a/test/confs/2027 b/test/confs/2027 index cc47218fb..c1b93a2ce 100644 --- a/test/confs/2027 +++ b/test/confs/2027 @@ -47,9 +47,11 @@ local_delivery: user = CALLER send_to_server: - driver = smtp + driver = smtp allow_localhost - hosts = ${if eq{$local_part}{userx}{127.0.0.1}{HOSTIPV4}} - port = PORT_D + hosts = ${if eq{$local_part}{userx}{127.0.0.1}{HOSTIPV4}} + port = PORT_D + tls_verify_certificates = DIR/aux-fixed/cert1 + tls_verify_cert_hostnames = : # End diff --git a/test/confs/5652 b/test/confs/5652 index 13c8d8617..28d3a95bb 100644 --- a/test/confs/5652 +++ b/test/confs/5652 @@ -29,6 +29,7 @@ tls_ocsp_file = DRSA/server1.example.com/server1.example.com.ocsp.good.resp \ : DECDSA/server1.example_ec.com/server1.example_ec.com.ocsp.good.resp +tls_require_ciphers = NORMAL:!VERS-TLS1.3 # ------ ACL ------ diff --git a/test/confs/5821 b/test/confs/5821 index 86ddbdedd..8a2d6459e 100644 --- a/test/confs/5821 +++ b/test/confs/5821 @@ -23,7 +23,7 @@ tls_certificate = ${if eq {SERVER}{server} {CDIR2/fullchain.pem}fail} tls_privatekey = ${if eq {SERVER}{server} {CDIR2/server1.example.com.unlocked.key}fail} # Permit two specific ciphers -tls_require_ciphers = NORMAL:-KX-ALL:+RSA:-CIPHER-ALL:+AES-128-CBC:+CAMELLIA-256-GCM +tls_require_ciphers = NORMAL:-VERS-TLS1.3:-KX-ALL:+RSA:-CIPHER-ALL:+AES-128-CBC:+CAMELLIA-256-GCM # ----- Routers ----- begin routers diff --git a/test/log/2027 b/test/log/2027 index 0bdd86a57..499351cd0 100644 --- a/test/log/2027 +++ b/test/log/2027 @@ -1,7 +1,7 @@ 1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@myhost.test.ex U=CALLER P=local S=sss 1999-03-02 09:44:33 10HmaY-0005vi-00 <= CALLER@myhost.test.ex U=CALLER P=local S=sss 1999-03-02 09:44:33 Start queue run: pid=pppp -qf -1999-03-02 09:44:33 10HmaX-0005vi-00 => userx@test.ex R=client T=send_to_server H=127.0.0.1 [127.0.0.1] X=TLS1.x:ke-RSA-AES256-SHAnnn:xxx CV=no C="250 OK id=10HmaZ-0005vi-00" +1999-03-02 09:44:33 10HmaX-0005vi-00 => userx@test.ex R=client T=send_to_server H=127.0.0.1 [127.0.0.1] X=TLS1.x:ke-RSA-AES256-SHAnnn:xxx CV=yes C="250 OK id=10HmaZ-0005vi-00" 1999-03-02 09:44:33 10HmaX-0005vi-00 Completed 1999-03-02 09:44:33 10HmaY-0005vi-00 TLS session: (gnutls_handshake): A TLS fatal alert has been received.: delivering unencrypted to H=ip4.ip4.ip4.ip4 [ip4.ip4.ip4.ip4] (not in hosts_require_tls) 1999-03-02 09:44:33 10HmaY-0005vi-00 => usery@test.ex R=client T=send_to_server H=ip4.ip4.ip4.ip4 [ip4.ip4.ip4.ip4] C="250 OK id=10HmbA-0005vi-00" diff --git a/test/runtest b/test/runtest index 77b701c0b..7aaf1032d 100755 --- a/test/runtest +++ b/test/runtest @@ -553,7 +553,7 @@ RESET_AFTER_EXTRA_LINE_READ: # (discarding kex, cipher, mac). For TLS 1.3 there is no kex # element (and no _WITH); insert a spurious "RSA". - s/^\s+by .+ with .+ \K tls TLS_.*?([^_]+)_WITH.+$/(TLS1.x:ke-\1-AES256-SHAnnn:xxx)/; + s/^\s+by .+ with .+ \K tls TLS_.*?([^_]+)_WITH.+$/(TLS1.x:ke-$1-AES256-SHAnnn:xxx)/; s/^\s+by .+ with .+ \K tls TLS_.+$/(TLS1.x:ke-RSA-AES256-SHAnnn:xxx)/; # Test machines might have various different TLS library versions supporting @@ -1263,6 +1263,20 @@ RESET_AFTER_EXTRA_LINE_READ: s/(DKIM: validation error: )error:[0-9A-F]{8}:rsa routines:(?:(?i)int_rsa_verify|CRYPTO_internal):(?:bad signature|algorithm mismatch)$/$1Public key signature verification has failed./; s/ARC: AMS signing: privkey PEM-block import: error:\K[0-9A-F]{8}:(PEM routines):get_name:(no start line)/0906D06C:$1:PEM_read_bio:$2/; + # gnutls version variances + if (/TLS error on connection \(recv\): .* Decode error/) + { + my $prev = $_; + $_ = ; + if (/error on first read/) + { + s/TLS session: \Kerror on first read:/(gnutls_handshake): A TLS fatal alert has been received.:/; + goto RESET_AFTER_EXTRA_LINE_READ; + } + else + { $_ = $prev; } + } + # DKIM timestamps if ( /(DKIM: d=.*) t=([0-9]*) x=([0-9]*) / ) { diff --git a/test/scripts/2000-GnuTLS/2027 b/test/scripts/2000-GnuTLS/2027 index 0d94ac4cd..3e071b665 100644 --- a/test/scripts/2000-GnuTLS/2027 +++ b/test/scripts/2000-GnuTLS/2027 @@ -3,9 +3,11 @@ gnutls munge gnutls_handshake exim -DSERVER=server -bd -oX PORT_D **** +# will send to 127.0.0.1 and the server requests a client-cert exim userx@test.ex Test message **** +# will send to HOSTIPV4 and the server requests&requires exim usery@test.ex Test message **** -- 2.25.1