Replace open_close_notify with open_app_data.

While a fairly small hook, open_close_notify is pretty weird. It
processes things at the record level and not above. Notably, this will
break if it skips past a TLS 1.3 KeyUpdate.

Instead, it can share the core part of SSL_read/SSL_peek, with slight
tweaks to post-handshake processing. Note this does require some tweaks
to that code. Notably, to retain the current semantics that SSL_shutdown
does not call funny callbacks, we suppress tickets.

Change-Id: Ia0cbd0b9f4527f1b091dd2083a5d8c7efb2bac65
Reviewed-on: https://boringssl-review.googlesource.com/21885
Commit-Queue: Steven Valdez <svaldez@google.com>
CQ-Verified: CQ bot account: commit-bot@chromium.org <commit-bot@chromium.org>
Reviewed-by: Steven Valdez <svaldez@google.com>
diff --git a/ssl/d1_pkt.cc b/ssl/d1_pkt.cc
index e2a8e6a..c6be93d 100644
--- a/ssl/d1_pkt.cc
+++ b/ssl/d1_pkt.cc
@@ -187,27 +187,6 @@
   return ssl_open_record_success;
 }
 
-ssl_open_record_t dtls1_open_close_notify(SSL *ssl, size_t *out_consumed,
-                                          uint8_t *out_alert,
-                                          bssl::Span<uint8_t> in) {
-  switch (ssl->s3->read_shutdown) {
-    // Bidirectional shutdown doesn't make sense for an unordered transport.
-    // DTLS alerts also aren't delivered reliably, so we may even time out
-    // because the peer never received our close_notify. Report to the caller
-    // that the channel has fully shut down.
-    case ssl_shutdown_none:
-    case ssl_shutdown_close_notify:
-      ssl->s3->read_shutdown = ssl_shutdown_close_notify;
-      return ssl_open_record_close_notify;
-    case ssl_shutdown_error:
-      ERR_restore_state(ssl->s3->read_error);
-      *out_alert = 0;
-      return ssl_open_record_error;
-  }
-  assert(0);
-  return ssl_open_record_error;
-}
-
 int dtls1_write_app_data(SSL *ssl, bool *out_needs_handshake,
                          const uint8_t *buf, int len) {
   assert(!SSL_in_init(ssl));
diff --git a/ssl/dtls_method.cc b/ssl/dtls_method.cc
index ac06842..fab19be 100644
--- a/ssl/dtls_method.cc
+++ b/ssl/dtls_method.cc
@@ -121,7 +121,6 @@
     dtls1_open_handshake,
     dtls1_open_change_cipher_spec,
     dtls1_open_app_data,
-    dtls1_open_close_notify,
     dtls1_write_app_data,
     dtls1_dispatch_alert,
     dtls1_supports_cipher,
diff --git a/ssl/internal.h b/ssl/internal.h
index 6bbde76..7801cc5 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1754,11 +1754,6 @@
   ssl_open_record_t (*open_app_data)(SSL *ssl, Span<uint8_t> *out,
                                      size_t *out_consumed, uint8_t *out_alert,
                                      Span<uint8_t> in);
-  // open_close_notify processes a record from |in| for reading close_notify.
-  // It discards all records and returns |ssl_open_record_close_notify| when it
-  // receives one.
-  ssl_open_record_t (*open_close_notify)(SSL *ssl, size_t *out_consumed,
-                                         uint8_t *out_alert, Span<uint8_t> in);
   int (*write_app_data)(SSL *ssl, bool *out_needs_handshake, const uint8_t *buf,
                         int len);
   int (*dispatch_alert)(SSL *ssl);
@@ -2711,8 +2706,6 @@
 ssl_open_record_t ssl3_open_change_cipher_spec(SSL *ssl, size_t *out_consumed,
                                                uint8_t *out_alert,
                                                Span<uint8_t> in);
-ssl_open_record_t ssl3_open_close_notify(SSL *ssl, size_t *out_consumed,
-                                         uint8_t *out_alert, Span<uint8_t> in);
 int ssl3_write_app_data(SSL *ssl, bool *out_needs_handshake, const uint8_t *buf,
                         int len);
 
@@ -2747,8 +2740,6 @@
 ssl_open_record_t dtls1_open_change_cipher_spec(SSL *ssl, size_t *out_consumed,
                                                 uint8_t *out_alert,
                                                 Span<uint8_t> in);
-ssl_open_record_t dtls1_open_close_notify(SSL *ssl, size_t *out_consumed,
-                                          uint8_t *out_alert, Span<uint8_t> in);
 
 int dtls1_write_app_data(SSL *ssl, bool *out_needs_handshake,
                          const uint8_t *buf, int len);
diff --git a/ssl/s3_pkt.cc b/ssl/s3_pkt.cc
index e647d06..7966e6f 100644
--- a/ssl/s3_pkt.cc
+++ b/ssl/s3_pkt.cc
@@ -390,19 +390,6 @@
   return ssl_open_record_success;
 }
 
-ssl_open_record_t ssl3_open_close_notify(SSL *ssl, size_t *out_consumed,
-                                         uint8_t *out_alert, Span<uint8_t> in) {
-  // TODO(davidben): Replace this with open_app_data so we actually process
-  // various bad behaviors.
-  uint8_t type;
-  Span<uint8_t> body;
-  auto ret = tls_open_record(ssl, &type, &body, out_consumed, out_alert, in);
-  if (ret == ssl_open_record_success) {
-    return ssl_open_record_discard;
-  }
-  return ret;
-}
-
 int ssl_send_alert(SSL *ssl, int level, int desc) {
   // It is illegal to send an alert when we've already sent a closing one.
   if (ssl->s3->write_shutdown != ssl_shutdown_none) {
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 8e06c49..ee7406d 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -884,7 +884,8 @@
   // protocol, namely in HTTPS, just before reading the HTTP response. Require
   // the record-layer be idle and avoid complexities of sending a handshake
   // record while an application_data record is being written.
-  if (ssl_write_buffer_is_pending(ssl)) {
+  if (ssl_write_buffer_is_pending(ssl) ||
+      ssl->s3->write_shutdown != ssl_shutdown_none) {
     goto no_renegotiation;
   }
 
@@ -902,12 +903,12 @@
   return 1;
 
 no_renegotiation:
-  ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_NO_RENEGOTIATION);
   OPENSSL_PUT_ERROR(SSL, SSL_R_NO_RENEGOTIATION);
+  ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_NO_RENEGOTIATION);
   return 0;
 }
 
-static int ssl_read_impl(SSL *ssl, void *buf, int num, bool peek) {
+static int ssl_read_impl(SSL *ssl) {
   ssl_reset_error_state(ssl);
 
   if (ssl->do_handshake == NULL) {
@@ -915,7 +916,7 @@
     return -1;
   }
 
-  for (;;) {
+  while (ssl->s3->pending_app_data.empty()) {
     // Complete the current handshake, if any. False Start will cause
     // |SSL_do_handshake| to return mid-handshake, so this may require multiple
     // iterations.
@@ -941,48 +942,52 @@
       continue;  // Loop again. We may have begun a new handshake.
     }
 
-    if (ssl->s3->pending_app_data.empty()) {
-      uint8_t alert = SSL_AD_DECODE_ERROR;
-      size_t consumed = 0;
-      auto ret =
-          ssl->method->open_app_data(ssl, &ssl->s3->pending_app_data, &consumed,
-                                     &alert, ssl_read_buffer(ssl));
-      bool retry;
-      int bio_ret = ssl_handle_open_record(ssl, &retry, ret, consumed, alert);
-      if (bio_ret <= 0) {
-        return bio_ret;
-      }
-      if (retry) {
-        continue;
-      }
+    uint8_t alert = SSL_AD_DECODE_ERROR;
+    size_t consumed = 0;
+    auto ret =
+        ssl->method->open_app_data(ssl, &ssl->s3->pending_app_data, &consumed,
+                                   &alert, ssl_read_buffer(ssl));
+    bool retry;
+    int bio_ret = ssl_handle_open_record(ssl, &retry, ret, consumed, alert);
+    if (bio_ret <= 0) {
+      return bio_ret;
+    }
+    if (!retry) {
+      assert(!ssl->s3->pending_app_data.empty());
       ssl->s3->key_update_count = 0;
     }
-
-    if (num <= 0) {
-      return num;
-    }
-
-    size_t todo =
-        std::min(ssl->s3->pending_app_data.size(), static_cast<size_t>(num));
-    OPENSSL_memcpy(buf, ssl->s3->pending_app_data.data(), todo);
-    if (!peek) {
-      // TODO(davidben): In DTLS, should the rest of the record be discarded?
-      // DTLS is not a stream. See https://crbug.com/boringssl/65.
-      ssl->s3->pending_app_data = ssl->s3->pending_app_data.subspan(todo);
-      if (ssl->s3->pending_app_data.empty()) {
-        ssl_read_buffer_discard(ssl);
-      }
-    }
-    return static_cast<int>(todo);
   }
+
+  return 1;
 }
 
 int SSL_read(SSL *ssl, void *buf, int num) {
-  return ssl_read_impl(ssl, buf, num, false /* consume bytes */);
+  int ret = SSL_peek(ssl, buf, num);
+  if (ret <= 0) {
+    return ret;
+  }
+  // TODO(davidben): In DTLS, should the rest of the record be discarded?  DTLS
+  // is not a stream. See https://crbug.com/boringssl/65.
+  ssl->s3->pending_app_data =
+      ssl->s3->pending_app_data.subspan(static_cast<size_t>(ret));
+  if (ssl->s3->pending_app_data.empty()) {
+    ssl_read_buffer_discard(ssl);
+  }
+  return ret;
 }
 
 int SSL_peek(SSL *ssl, void *buf, int num) {
-  return ssl_read_impl(ssl, buf, num, true /* peek */);
+  int ret = ssl_read_impl(ssl);
+  if (ret <= 0) {
+    return ret;
+  }
+  if (num <= 0) {
+    return num;
+  }
+  size_t todo =
+      std::min(ssl->s3->pending_app_data.size(), static_cast<size_t>(num));
+  OPENSSL_memcpy(buf, ssl->s3->pending_app_data.data(), todo);
+  return static_cast<int>(todo);
 }
 
 int SSL_write(SSL *ssl, const void *buf, int num) {
@@ -1056,21 +1061,28 @@
       return -1;
     }
   } else if (ssl->s3->read_shutdown != ssl_shutdown_close_notify) {
-    ssl->s3->pending_app_data = Span<uint8_t>();
-    for (;;) {
-      uint8_t alert = SSL_AD_DECODE_ERROR;
-      size_t consumed = 0;
-      auto ret = ssl->method->open_close_notify(ssl, &consumed, &alert,
-                                                ssl_read_buffer(ssl));
-      bool retry;
-      int bio_ret = ssl_handle_open_record(ssl, &retry, ret, consumed, alert);
-      if (bio_ret <= 0) {
-        break;
+    if (SSL_is_dtls(ssl)) {
+      // Bidirectional shutdown doesn't make sense for an unordered
+      // transport. DTLS alerts also aren't delivered reliably, so we may even
+      // time out because the peer never received our close_notify. Report to
+      // the caller that the channel has fully shut down.
+      if (ssl->s3->read_shutdown == ssl_shutdown_error) {
+        ERR_restore_state(ssl->s3->read_error);
+        return -1;
       }
-      assert(retry);  // open_close_notify never reports success.
-    }
-    if (ssl->s3->read_shutdown != ssl_shutdown_close_notify) {
-      return -1;
+      ssl->s3->read_shutdown = ssl_shutdown_close_notify;
+    } else {
+      // Keep discarding data until we see a close_notify.
+      for (;;) {
+        ssl->s3->pending_app_data = Span<uint8_t>();
+        int ret = ssl_read_impl(ssl);
+        if (ret <= 0) {
+          break;
+        }
+      }
+      if (ssl->s3->read_shutdown != ssl_shutdown_close_notify) {
+        return -1;
+      }
     }
   }
 
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index f475b73..434dfdd 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -3930,6 +3930,39 @@
   EXPECT_EQ(3, SSL_pending(client_.get()));
 }
 
+// Test that post-handshake tickets consumed by |SSL_shutdown| are ignored.
+TEST(SSLTest, ShutdownIgnoresTickets) {
+  bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(ctx);
+  ASSERT_TRUE(SSL_CTX_set_min_proto_version(ctx.get(), TLS1_3_VERSION));
+  ASSERT_TRUE(SSL_CTX_set_max_proto_version(ctx.get(), TLS1_3_VERSION));
+
+  bssl::UniquePtr<X509> cert = GetTestCertificate();
+  bssl::UniquePtr<EVP_PKEY> key = GetTestKey();
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(key);
+  ASSERT_TRUE(SSL_CTX_use_certificate(ctx.get(), cert.get()));
+  ASSERT_TRUE(SSL_CTX_use_PrivateKey(ctx.get(), key.get()));
+
+  SSL_CTX_set_session_cache_mode(ctx.get(), SSL_SESS_CACHE_BOTH);
+
+  bssl::UniquePtr<SSL> client, server;
+  ASSERT_TRUE(ConnectClientAndServer(&client, &server, ctx.get(), ctx.get()));
+
+  SSL_CTX_sess_set_new_cb(ctx.get(), [](SSL *ssl, SSL_SESSION *session) -> int {
+    ADD_FAILURE() << "New session callback called during SSL_shutdown";
+    return 0;
+  });
+
+  // Send close_notify.
+  EXPECT_EQ(0, SSL_shutdown(server.get()));
+  EXPECT_EQ(0, SSL_shutdown(client.get()));
+
+  // Receive close_notify.
+  EXPECT_EQ(1, SSL_shutdown(server.get()));
+  EXPECT_EQ(1, SSL_shutdown(client.get()));
+}
+
 // TODO(davidben): Convert this file to GTest properly.
 TEST(SSLTest, AllTests) {
   if (!TestSSL_SESSIONEncoding(kOpenSSLSession) ||
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index a57362d..065dee8 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -4894,6 +4894,92 @@
 				sendWarningAlerts: 1,
 				flags:             []string{"-check-close-notify"},
 			})
+
+			// Test that SSL_shutdown still processes KeyUpdate.
+			tests = append(tests, testCase{
+				name: "Shutdown-Shim-KeyUpdate",
+				config: Config{
+					MinVersion: VersionTLS13,
+					MaxVersion: VersionTLS13,
+					Bugs: ProtocolBugs{
+						ExpectCloseNotify: true,
+					},
+				},
+				shimShutsDown:    true,
+				sendKeyUpdates:   1,
+				keyUpdateRequest: keyUpdateRequested,
+				flags:            []string{"-check-close-notify"},
+			})
+
+			// Test that SSL_shutdown processes HelloRequest
+			// correctly.
+			tests = append(tests, testCase{
+				name: "Shutdown-Shim-HelloRequest-Ignore",
+				config: Config{
+					MinVersion: VersionTLS12,
+					MaxVersion: VersionTLS12,
+					Bugs: ProtocolBugs{
+						SendHelloRequestBeforeEveryAppDataRecord: true,
+						ExpectCloseNotify:                        true,
+					},
+				},
+				shimShutsDown: true,
+				flags: []string{
+					"-renegotiate-ignore",
+					"-check-close-notify",
+				},
+			})
+			tests = append(tests, testCase{
+				name: "Shutdown-Shim-HelloRequest-Reject",
+				config: Config{
+					MinVersion: VersionTLS12,
+					MaxVersion: VersionTLS12,
+					Bugs: ProtocolBugs{
+						SendHelloRequestBeforeEveryAppDataRecord: true,
+						ExpectCloseNotify:                        true,
+					},
+				},
+				shimShutsDown: true,
+				shouldFail:    true,
+				expectedError: ":NO_RENEGOTIATION:",
+				flags:         []string{"-check-close-notify"},
+			})
+			tests = append(tests, testCase{
+				name: "Shutdown-Shim-HelloRequest-CannotHandshake",
+				config: Config{
+					MinVersion: VersionTLS12,
+					MaxVersion: VersionTLS12,
+					Bugs: ProtocolBugs{
+						SendHelloRequestBeforeEveryAppDataRecord: true,
+						ExpectCloseNotify:                        true,
+					},
+				},
+				shimShutsDown: true,
+				shouldFail:    true,
+				expectedError: ":NO_RENEGOTIATION:",
+				flags: []string{
+					"-check-close-notify",
+					"-renegotiate-freely",
+				},
+			})
+
+			tests = append(tests, testCase{
+				testType: serverTest,
+				name:     "Shutdown-Shim-Renegotiate-Server-Forbidden",
+				config: Config{
+					MaxVersion: VersionTLS12,
+					Bugs: ProtocolBugs{
+						ExpectCloseNotify: true,
+					},
+				},
+				shimShutsDown: true,
+				renegotiate:   1,
+				shouldFail:    true,
+				expectedError: ":NO_RENEGOTIATION:",
+				flags: []string{
+					"-check-close-notify",
+				},
+			})
 		}
 	} else {
 		// TODO(davidben): DTLS 1.3 will want a similar thing for
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index e75d976..a03c581 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -774,6 +774,13 @@
 }
 
 int tls13_process_new_session_ticket(SSL *ssl, const SSLMessage &msg) {
+  if (ssl->s3->write_shutdown != ssl_shutdown_none) {
+    // Ignore tickets on shutdown. Callers tend to indiscriminately call
+    // |SSL_shutdown| before destroying an |SSL|, at which point calling the new
+    // session callback may be confusing.
+    return 1;
+  }
+
   UniquePtr<SSL_SESSION> session(SSL_SESSION_dup(ssl->s3->established_session,
                                                  SSL_SESSION_INCLUDE_NONAUTH));
   if (!session) {
diff --git a/ssl/tls_method.cc b/ssl/tls_method.cc
index c7352ce..94b9e20 100644
--- a/ssl/tls_method.cc
+++ b/ssl/tls_method.cc
@@ -116,7 +116,6 @@
     ssl3_open_handshake,
     ssl3_open_change_cipher_spec,
     ssl3_open_app_data,
-    ssl3_open_close_notify,
     ssl3_write_app_data,
     ssl3_dispatch_alert,
     ssl3_supports_cipher,