[exim-cvs] GnuTLS: Fix client detection of server reject of …

Inizio della pagina
Delete this message
Reply to this message
Autore: Exim Git Commits Mailing List
Data:  
To: exim-cvs
Oggetto: [exim-cvs] GnuTLS: Fix client detection of server reject of client cert under TLS1.3
Gitweb: https://git.exim.org/exim.git/commitdiff/fc243e944ec00b59b75f41d07494116f925d58b4
Commit:     fc243e944ec00b59b75f41d07494116f925d58b4
Parent:     882bc1704d33aa34873e3a0f72e657b0cc2985e5
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Sat Feb 16 12:59:23 2019 +0000
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Sat Feb 16 12:59:23 2019 +0000


    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 18db733..6a9aae3 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 ebb6477..5064700 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 8a4dace..b7a8354 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 faad38f..1752bf3 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 f007f4a..b2bf5f0 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 9ee1304..6ff993d 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 cc47218..c1b93a2 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 13c8d86..28d3a95 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 86ddbde..8a2d645 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 0bdd86a..499351c 100644
--- a/test/log/2027
+++ b/test/log/2027
@@ -1,7 +1,7 @@
1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@??? U=CALLER P=local S=sss
1999-03-02 09:44:33 10HmaY-0005vi-00 <= CALLER@??? 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@??? 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@??? 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@??? 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 77b701c..7aaf103 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 = $_;
+      $_ = <IN>;
+      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 0d94ac4..3e071b6 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 message
 ****
+# will send to HOSTIPV4 and the server requests&requires
 exim usery@???
 Test message
 ****