From 495047ba12a834e3b15f3cae6f3ea1ad0dcf0992 Mon Sep 17 00:00:00 2001
From: Fedor Indutny <fedor@indutny.com>
Date: Sat, 30 Jan 2016 16:58:34 -0500
Subject: [PATCH] Allow downgrading when reusing sessions on client

When connecting to pool of diverse servers (both TLS1.0 and TLS1.2), a
following scenario may happen:

  1. Connect to TLS1.2 server, receive new session
  2. Store this session
  3. Attempt to reuse it later when connecting to server
  4. Connect to different server from the pool, which speaks only TLS1.0
  5. Get `SSL_R_WRONG_VERSION_NUMBER` error

Expected behavior would be scrapping off the session, and allowing
server to downgrade to supported protocol version the way it would do it
if no client session would be supplied.

This issue was discovered while working on following node.js bug:

https://github.com/nodejs/node/issues/3692
---
 ssl/s3_pkt.c  | 39 +++++++++++++++++++++++++++++++++++++++
 ssl/ssltest.c | 22 +++++++++++++++++++++-
 test/testssl  |  6 ++++++
 3 files changed, 66 insertions(+), 1 deletion(-)

diff --git a/ssl/s3_pkt.c b/ssl/s3_pkt.c
index 3798902..044bc09 100644
--- a/ssl/s3_pkt.c
+++ b/ssl/s3_pkt.c
@@ -358,6 +358,45 @@ static int ssl3_get_record(SSL *s)
 
         /* Lets check version */
         if (!s->first_packet) {
+            /* Client may attempt to reuse session with higher protocol version
+             * number than what is currently supported by the server
+             */
+            if (!s->server && !s->new_session && version != s->version) {
+#ifndef OPENSSL_NO_SSL3
+                if (version == SSL3_VERSION &&
+                    !(s->options & SSL_OP_NO_SSLv3)) {
+# ifdef OPENSSL_FIPS
+                    if (FIPS_mode()) {
+                        SSLerr(SSL_F_SSL23_GET_SERVER_HELLO,
+                               SSL_R_ONLY_TLS_ALLOWED_IN_FIPS_MODE);
+                        goto err;
+                    }
+# endif
+                    s->version = SSL3_VERSION;
+                    s->method = SSLv3_client_method();
+                } else
+#endif
+                if (version == TLS1_VERSION &&
+                    !(s->options & SSL_OP_NO_TLSv1)) {
+                    s->version = TLS1_VERSION;
+                    s->method = TLSv1_client_method();
+                } else if (version == TLS1_1_VERSION &&
+                           !(s->options & SSL_OP_NO_TLSv1_1)) {
+                    s->version = TLS1_1_VERSION;
+                    s->method = TLSv1_1_client_method();
+                } else if (version == TLS1_2_VERSION &&
+                           !(s->options & SSL_OP_NO_TLSv1_2)) {
+                    s->version = TLS1_2_VERSION;
+                    s->method = TLSv1_2_client_method();
+                }
+                /* Otherwise fallthrough to the next if block and error */
+
+                /* Make sure that the session won't be reused */
+                if (version == s->version) {
+                    s->session->session_id_length = 0;
+                }
+            }
+
             if (version != s->version) {
                 SSLerr(SSL_F_SSL3_GET_RECORD, SSL_R_WRONG_VERSION_NUMBER);
                 if ((s->version & 0xFF00) == (version & 0xFF00)
diff --git a/ssl/ssltest.c b/ssl/ssltest.c
index aaf6c6b..f5494f1 100644
--- a/ssl/ssltest.c
+++ b/ssl/ssltest.c
@@ -688,6 +688,8 @@ static void sv_usage(void)
     fprintf(stderr, " -v            - more output\n");
     fprintf(stderr, " -d            - debug output\n");
     fprintf(stderr, " -reuse        - use session-id reuse\n");
+    fprintf(stderr, " -tls1_reuse   - downgrade to TLSv1 on session reuse\n");
+    fprintf(stderr, " -no_ticket    - do not issue TLS session ticket");
     fprintf(stderr, " -num <val>    - number of connections to perform\n");
     fprintf(stderr,
             " -bytes <val>  - number of bytes to swap between client/server\n");
@@ -900,7 +902,7 @@ int main(int argc, char *argv[])
     SSL_CTX *c_ctx = NULL;
     const SSL_METHOD *meth = NULL;
     SSL *c_ssl, *s_ssl;
-    int number = 1, reuse = 0;
+    int number = 1, reuse = 0, tls1_reuse = 0, no_ticket = 0;
     long bytes = 256L;
 #ifndef OPENSSL_NO_DH
     DH *dh;
@@ -984,6 +986,10 @@ int main(int argc, char *argv[])
             debug = 1;
         else if (strcmp(*argv, "-reuse") == 0)
             reuse = 1;
+        else if (strcmp(*argv, "-tls1_reuse") == 0)
+            tls1_reuse = 1;
+        else if (strcmp(*argv, "-no_ticket") == 0)
+            no_ticket = 1;
         else if (strcmp(*argv, "-dhe512") == 0) {
 #ifndef OPENSSL_NO_DH
             dhe512 = 1;
@@ -1214,6 +1220,13 @@ int main(int argc, char *argv[])
                 "to avoid protocol mismatch.\n");
         EXIT(1);
     }
+    if ((ssl2 || ssl3 || tls1 || dtls1 || dtls12 || !reuse || number <= 1) &&
+        tls1_reuse) {
+        fprintf(stderr, "Can\'t downgrade to TLSv1 on reuse without reuse, or\n"
+                        "from already low protocol version, or when number of\n"
+                        "tests is equal to 1\n");
+        EXIT(1);
+    }
 #ifdef OPENSSL_FIPS
     if (fips_mode) {
         if (!FIPS_mode_set(1)) {
@@ -1420,6 +1433,11 @@ int main(int argc, char *argv[])
                                        sizeof session_id_context);
     }
 
+    if (no_ticket) {
+        SSL_CTX_set_options(c_ctx, SSL_OP_NO_TICKET);
+        SSL_CTX_set_options(s_ctx, SSL_OP_NO_TICKET);
+    }
+
     /* Use PSK only if PSK key is given */
     if (psk_key != NULL) {
         /*
@@ -1553,6 +1571,8 @@ int main(int argc, char *argv[])
     for (i = 0; i < number; i++) {
         if (!reuse)
             SSL_set_session(c_ssl, NULL);
+        if ((i != 0) && tls1_reuse)
+            SSL_set_ssl_method(s_ssl, TLSv1_method());
         if (bio_pair)
             ret = doit_biopair(s_ssl, c_ssl, bytes, &s_time, &c_time);
         else
diff --git a/test/testssl b/test/testssl
index 747e4ba..b5c6680 100644
--- a/test/testssl
+++ b/test/testssl
@@ -274,3 +274,9 @@ if [ -z "$extra" -a `uname -m` = "x86_64" ]; then
 fi
 
 exit 0
+
+#############################################################################
+# Downgrade on reuse
+
+echo test downgrading to tls1 on session reuse
+$ssltest -reuse -tls1_reuse -num 2 || exit 1
-- 
2.7.0

