Add initial support for 0-RTT with QUIC.

This adapts our existing API for QUIC, although I'm not entirely
convinced the shape of it fits as it does with TCP. Things that needed
to be changed:

- There is a slight ordering issue on the server with HRR and releasing
  the 0-RTT keys to QUIC.

- Remove EndOfEarlyData.

- At the early return point for the server, QUIC needs to have installed
  the client traffic secrets earlier.

- The maximum early data value is a constant in QUIC.

- QUIC never installs early secrets at the TLS level. (In particular,
  this avoids nuisances with do_send_second_client_hello's null cipher
  not updating the encryption level.)

- The read/write secrets for 0-RTT keys were mixed up.

As the QUIC tests are getting a bit unwieldy, I tidied them up a bit.
This CL does *not* handle the QUIC transport parameters or HTTP/3
server SETTINGS frame interactions with 0-RTT. That will be done in a
separate CL.

I suspect if we ever implement DTLS 1.3, we'll find ourselves wanting to
align some of the QUIC bits here with DTLS and perhaps refine the
handshake/transport abstractions a bit.

Bug: 221
Change-Id: I61f701d7241dbc99e5dbf57ae6c283e10b85b049
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/37145
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Steven Valdez <svaldez@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 1ef9f84..d3ca63c 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3140,6 +3140,13 @@
 // |SSL_process_quic_post_handshake| to process it. It is an error to call
 // |SSL_read| and |SSL_write| in QUIC.
 //
+// 0-RTT behaves similarly to |TLS_method|'s usual behavior. |SSL_do_handshake|
+// returns early as soon as the client (respectively, server) is allowed to send
+// 0-RTT (respectively, half-RTT) data. The caller should then call
+// |SSL_do_handshake| again to consume the remaining handshake messages and
+// confirm the handshake. As a client, |SSL_ERROR_EARLY_DATA_REJECTED| and
+// |SSL_reset_early_data_reject| behave as usual.
+//
 // Note that secrets for an encryption level may be available to QUIC before the
 // level is active in TLS. Callers should use |SSL_quic_read_level| to determine
 // the active read level for |SSL_provide_quic_data|. |SSL_do_handshake| will
@@ -3155,7 +3162,8 @@
 // |SSL_quic_max_handshake_flight_len| to get the maximum buffer length at each
 // encryption level.
 //
-// Note: 0-RTT is not currently supported via this API.
+// Note: 0-RTT support is incomplete and does not currently handle QUIC
+// transport parameters and server SETTINGS frame.
 
 // ssl_encryption_level_t represents a specific QUIC encryption level used to
 // transmit handshake messages.
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index 4e8ca6a..8be9f6b 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -459,7 +459,11 @@
   if (!tls13_init_early_key_schedule(
           hs, MakeConstSpan(ssl->session->master_key,
                             ssl->session->master_key_length)) ||
-      !tls13_derive_early_secrets(hs) ||
+      !tls13_derive_early_secret(hs) ||
+      !tls13_set_early_secret_for_quic(hs)) {
+    return ssl_hs_error;
+  }
+  if (ssl->quic_method == nullptr &&
       !tls13_set_traffic_key(ssl, ssl_encryption_early_data, evp_aead_seal,
                              hs->early_traffic_secret())) {
     return ssl_hs_error;
diff --git a/ssl/internal.h b/ssl/internal.h
index f55e047..ec3594c 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1265,9 +1265,17 @@
                            enum evp_aead_direction_t direction,
                            Span<const uint8_t> traffic_secret);
 
-// tls13_derive_early_secrets derives the early traffic secret. It returns true
-// on success and false on error.
-bool tls13_derive_early_secrets(SSL_HANDSHAKE *hs);
+// tls13_derive_early_secret derives the early traffic secret. It returns true
+// on success and false on error. Unlike with other traffic secrets, this
+// function does not pass the keys to QUIC. Call
+// |tls13_set_early_secret_for_quic| to do so. This is done to due to an
+// ordering complication around resolving HelloRetryRequest on the server.
+bool tls13_derive_early_secret(SSL_HANDSHAKE *hs);
+
+// tls13_set_early_secret_for_quic passes the early traffic secrets, as
+// derived by |tls13_derive_early_secret|, to QUIC. It returns true on success
+// and false on error.
+bool tls13_set_early_secret_for_quic(SSL_HANDSHAKE *hs);
 
 // tls13_derive_handshake_secrets derives the handshake traffic secret. It
 // returns true on success and false on error.
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 03760b6..4b92acd 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -4698,7 +4698,9 @@
 
 class MockQUICTransport {
  public:
-  MockQUICTransport() {
+  enum class Role { kClient, kServer };
+
+  explicit MockQUICTransport(Role role) : role_(role) {
     // The caller is expected to configure initial secrets.
     levels_[ssl_encryption_initial].write_secret = {1};
     levels_[ssl_encryption_initial].read_secret = {1};
@@ -4735,18 +4737,38 @@
       return false;
     }
 
-    if (level != ssl_encryption_early_data &&
-        (read_secret == nullptr || write_secret == nullptr)) {
-      ADD_FAILURE() << "key was unexpectedly null";
+    bool expect_read_secret = true, expect_write_secret = true;
+    if (level == ssl_encryption_early_data) {
+      if (role_ == Role::kClient) {
+        expect_read_secret = false;
+      } else {
+        expect_write_secret = false;
+      }
+    }
+
+    if (expect_read_secret) {
+      if (read_secret == nullptr) {
+        ADD_FAILURE() << "read secret was unexpectedly null";
+        return false;
+      }
+      levels_[level].read_secret.assign(read_secret, read_secret + secret_len);
+    } else if (read_secret != nullptr) {
+      ADD_FAILURE() << "unexpected read secret";
       return false;
     }
-    if (read_secret != nullptr) {
-      levels_[level].read_secret.assign(read_secret, read_secret + secret_len);
-    }
-    if (write_secret != nullptr) {
+
+    if (expect_write_secret) {
+      if (write_secret == nullptr) {
+        ADD_FAILURE() << "write secret was unexpectedly null";
+        return false;
+      }
       levels_[level].write_secret.assign(write_secret,
                                          write_secret + secret_len);
+    } else if (write_secret != nullptr) {
+      ADD_FAILURE() << "unexpected write secret";
+      return false;
     }
+
     levels_[level].cipher = SSL_CIPHER_get_id(cipher);
     return true;
   }
@@ -4783,7 +4805,7 @@
                          ssl_encryption_level_t level,
                          size_t num = std::numeric_limits<size_t>::max()) {
     if (levels_[level].read_secret.empty()) {
-      ADD_FAILURE() << "data read before keys configured";
+      ADD_FAILURE() << "data read before keys configured in level " << level;
       return false;
     }
     // The peer may not have configured any keys yet.
@@ -4792,11 +4814,12 @@
     }
     // Check the peer computed the same key.
     if (peer_->levels_[level].write_secret != levels_[level].read_secret) {
-      ADD_FAILURE() << "peer write key does not match read key";
+      ADD_FAILURE() << "peer write key does not match read key in level "
+                    << level;
       return false;
     }
     if (peer_->levels_[level].cipher != levels_[level].cipher) {
-      ADD_FAILURE() << "peer cipher does not match";
+      ADD_FAILURE() << "peer cipher does not match in level " << level;
       return false;
     }
     std::vector<uint8_t> *peer_data = &peer_->levels_[level].write_data;
@@ -4807,6 +4830,7 @@
   }
 
  private:
+  Role role_;
   MockQUICTransport *peer_ = nullptr;
 
   bool has_alert_ = false;
@@ -4824,21 +4848,24 @@
 
 class MockQUICTransportPair {
  public:
-  MockQUICTransportPair() {
-    server_.set_peer(&client_);
+  MockQUICTransportPair()
+      : client_(MockQUICTransport::Role::kClient),
+        server_(MockQUICTransport::Role::kServer) {
     client_.set_peer(&server_);
+    server_.set_peer(&client_);
   }
 
   ~MockQUICTransportPair() {
-    server_.set_peer(nullptr);
     client_.set_peer(nullptr);
+    server_.set_peer(nullptr);
   }
 
   MockQUICTransport *client() { return &client_; }
   MockQUICTransport *server() { return &server_; }
 
   bool SecretsMatch(ssl_encryption_level_t level) const {
-    return client_.PeerSecretsMatch(level);
+    return client_.HasSecrets(level) && server_.HasSecrets(level) &&
+           client_.PeerSecretsMatch(level);
   }
 
  private:
@@ -4890,24 +4917,80 @@
     SSL_set_connect_state(client_.get());
     SSL_set_accept_state(server_.get());
 
-    ex_data_.Set(client_.get(), transport_.client());
-    ex_data_.Set(server_.get(), transport_.server());
+    transport_.reset(new MockQUICTransportPair);
+    ex_data_.Set(client_.get(), transport_->client());
+    ex_data_.Set(server_.get(), transport_->server());
     return true;
   }
 
-  bool CreateSecondClientAndServer() {
-    client_.reset(SSL_new(client_ctx_.get()));
-    server_.reset(SSL_new(server_ctx_.get()));
-    if (!client_ || !server_) {
-      return false;
+  // CompleteHandshakesForQUIC runs |SSL_do_handshake| on |client_| and
+  // |server_| until each completes once. It returns true on success and false
+  // on failure.
+  bool CompleteHandshakesForQUIC() {
+    bool client_done = false, server_done = false;
+    while (!client_done || !server_done) {
+      if (!client_done) {
+        if (!ProvideHandshakeData(client_.get())) {
+          ADD_FAILURE() << "ProvideHandshakeData(client_) failed";
+          return false;
+        }
+        int client_ret = SSL_do_handshake(client_.get());
+        if (client_ret == 1) {
+          client_done = true;
+        } else {
+          EXPECT_EQ(client_ret, -1);
+          EXPECT_EQ(SSL_get_error(client_.get(), client_ret),
+                    SSL_ERROR_WANT_READ);
+        }
+      }
+
+      if (!server_done) {
+        if (!ProvideHandshakeData(server_.get())) {
+          ADD_FAILURE() << "ProvideHandshakeData(server_) failed";
+          return false;
+        }
+        int server_ret = SSL_do_handshake(server_.get());
+        if (server_ret == 1) {
+          server_done = true;
+        } else {
+          EXPECT_EQ(server_ret, -1);
+          EXPECT_EQ(SSL_get_error(server_.get(), server_ret),
+                    SSL_ERROR_WANT_READ);
+        }
+      }
+    }
+    return true;
+  }
+
+  bssl::UniquePtr<SSL_SESSION> CreateClientSessionForQUIC() {
+    g_last_session = nullptr;
+    SSL_CTX_sess_set_new_cb(client_ctx_.get(), SaveLastSession);
+    if (!CreateClientAndServer() ||
+        !CompleteHandshakesForQUIC()) {
+      return nullptr;
     }
 
-    SSL_set_connect_state(client_.get());
-    SSL_set_accept_state(server_.get());
+    // The server sent NewSessionTicket messages in the handshake.
+    if (!ProvideHandshakeData(client_.get()) ||
+        !SSL_process_quic_post_handshake(client_.get())) {
+      return nullptr;
+    }
 
-    ex_data_.Set(client_.get(), second_transport_.client());
-    ex_data_.Set(server_.get(), second_transport_.server());
-    return true;
+    return std::move(g_last_session);
+  }
+
+  void ExpectHandshakeSuccess() {
+    EXPECT_TRUE(transport_->SecretsMatch(ssl_encryption_application));
+    EXPECT_EQ(ssl_encryption_application, SSL_quic_read_level(client_.get()));
+    EXPECT_EQ(ssl_encryption_application, SSL_quic_write_level(client_.get()));
+    EXPECT_EQ(ssl_encryption_application, SSL_quic_read_level(server_.get()));
+    EXPECT_EQ(ssl_encryption_application, SSL_quic_write_level(server_.get()));
+    EXPECT_FALSE(transport_->client()->has_alert());
+    EXPECT_FALSE(transport_->server()->has_alert());
+
+    // SSL_do_handshake is now idempotent.
+    EXPECT_EQ(SSL_do_handshake(client_.get()), 1);
+    EXPECT_EQ(SSL_do_handshake(server_.get()), 1);
   }
 
   // The following functions may be configured on an |SSL_QUIC_METHOD| as
@@ -4942,8 +5025,7 @@
   bssl::UniquePtr<SSL_CTX> server_ctx_;
 
   static UnownedSSLExData<MockQUICTransport> ex_data_;
-  MockQUICTransportPair transport_;
-  MockQUICTransportPair second_transport_;
+  std::unique_ptr<MockQUICTransportPair> transport_;
 
   bssl::UniquePtr<SSL> client_;
   bssl::UniquePtr<SSL> server_;
@@ -4966,33 +5048,13 @@
   SSL_CTX_sess_set_new_cb(client_ctx_.get(), SaveLastSession);
   ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method));
   ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method));
+
   ASSERT_TRUE(CreateClientAndServer());
+  ASSERT_TRUE(CompleteHandshakesForQUIC());
 
-  for (;;) {
-    ASSERT_TRUE(ProvideHandshakeData(client_.get()));
-    int client_ret = SSL_do_handshake(client_.get());
-    if (client_ret != 1) {
-      ASSERT_EQ(client_ret, -1);
-      ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ);
-    }
-
-    ASSERT_TRUE(ProvideHandshakeData(server_.get()));
-    int server_ret = SSL_do_handshake(server_.get());
-    if (server_ret != 1) {
-      ASSERT_EQ(server_ret, -1);
-      ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ);
-    }
-
-    if (client_ret == 1 && server_ret == 1) {
-      break;
-    }
-  }
-
-  EXPECT_EQ(SSL_do_handshake(client_.get()), 1);
-  EXPECT_EQ(SSL_do_handshake(server_.get()), 1);
-  EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application));
-  EXPECT_FALSE(transport_.client()->has_alert());
-  EXPECT_FALSE(transport_.server()->has_alert());
+  ExpectHandshakeSuccess();
+  EXPECT_FALSE(SSL_session_reused(client_.get()));
+  EXPECT_FALSE(SSL_session_reused(server_.get()));
 
   // The server sent NewSessionTicket messages in the handshake.
   EXPECT_FALSE(g_last_session);
@@ -5001,35 +5063,13 @@
   EXPECT_TRUE(g_last_session);
 
   // Create a second connection to verify resumption works.
-  ASSERT_TRUE(CreateSecondClientAndServer());
+  ASSERT_TRUE(CreateClientAndServer());
   bssl::UniquePtr<SSL_SESSION> session = std::move(g_last_session);
   SSL_set_session(client_.get(), session.get());
 
-  for (;;) {
-    ASSERT_TRUE(ProvideHandshakeData(client_.get()));
-    int client_ret = SSL_do_handshake(client_.get());
-    if (client_ret != 1) {
-      ASSERT_EQ(client_ret, -1);
-      ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ);
-    }
+  ASSERT_TRUE(CompleteHandshakesForQUIC());
 
-    ASSERT_TRUE(ProvideHandshakeData(server_.get()));
-    int server_ret = SSL_do_handshake(server_.get());
-    if (server_ret != 1) {
-      ASSERT_EQ(server_ret, -1);
-      ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ);
-    }
-
-    if (client_ret == 1 && server_ret == 1) {
-      break;
-    }
-  }
-
-  EXPECT_EQ(SSL_do_handshake(client_.get()), 1);
-  EXPECT_EQ(SSL_do_handshake(server_.get()), 1);
-  EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application));
-  EXPECT_FALSE(transport_.client()->has_alert());
-  EXPECT_FALSE(transport_.server()->has_alert());
+  ExpectHandshakeSuccess();
   EXPECT_TRUE(SSL_session_reused(client_.get()));
   EXPECT_TRUE(SSL_session_reused(server_.get()));
 }
@@ -5056,32 +5096,133 @@
                                   OPENSSL_ARRAY_SIZE(kServerPrefs)));
 
   ASSERT_TRUE(CreateClientAndServer());
+  ASSERT_TRUE(CompleteHandshakesForQUIC());
+  ExpectHandshakeSuccess();
+}
 
-  for (;;) {
-    ASSERT_TRUE(ProvideHandshakeData(client_.get()));
-    int client_ret = SSL_do_handshake(client_.get());
-    if (client_ret != 1) {
-      ASSERT_EQ(client_ret, -1);
-      ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ);
+TEST_F(QUICMethodTest, ZeroRTTAccept) {
+  const SSL_QUIC_METHOD quic_method = {
+      SetEncryptionSecretsCallback,
+      AddHandshakeDataCallback,
+      FlushFlightCallback,
+      SendAlertCallback,
+  };
+
+  SSL_CTX_set_session_cache_mode(client_ctx_.get(), SSL_SESS_CACHE_BOTH);
+  SSL_CTX_set_early_data_enabled(client_ctx_.get(), 1);
+  SSL_CTX_set_early_data_enabled(server_ctx_.get(), 1);
+  ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method));
+  ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method));
+
+  bssl::UniquePtr<SSL_SESSION> session = CreateClientSessionForQUIC();
+  ASSERT_TRUE(session);
+
+  ASSERT_TRUE(CreateClientAndServer());
+  SSL_set_session(client_.get(), session.get());
+
+  // The client handshake should return immediately into the early data state.
+  ASSERT_EQ(SSL_do_handshake(client_.get()), 1);
+  EXPECT_TRUE(SSL_in_early_data(client_.get()));
+  // The transport should have keys for sending 0-RTT data.
+  EXPECT_TRUE(
+      transport_->client()->HasSecrets(ssl_encryption_early_data));
+
+  // The server will consume the ClientHello and also enter the early data
+  // state.
+  ASSERT_TRUE(ProvideHandshakeData(server_.get()));
+  ASSERT_EQ(SSL_do_handshake(server_.get()), 1);
+  EXPECT_TRUE(SSL_in_early_data(server_.get()));
+  EXPECT_TRUE(transport_->SecretsMatch(ssl_encryption_early_data));
+  // The transport should have keys for sending half-RTT data.
+  EXPECT_TRUE(
+      transport_->server()->HasSecrets(ssl_encryption_application));
+
+  // Finish up the client and server handshakes.
+  ASSERT_TRUE(CompleteHandshakesForQUIC());
+
+  // Both sides can now exchange 1-RTT data.
+  ExpectHandshakeSuccess();
+  EXPECT_TRUE(SSL_session_reused(client_.get()));
+  EXPECT_TRUE(SSL_session_reused(server_.get()));
+  EXPECT_FALSE(SSL_in_early_data(client_.get()));
+  EXPECT_FALSE(SSL_in_early_data(server_.get()));
+  EXPECT_TRUE(SSL_early_data_accepted(client_.get()));
+  EXPECT_TRUE(SSL_early_data_accepted(server_.get()));
+}
+
+TEST_F(QUICMethodTest, ZeroRTTReject) {
+  const SSL_QUIC_METHOD quic_method = {
+      SetEncryptionSecretsCallback,
+      AddHandshakeDataCallback,
+      FlushFlightCallback,
+      SendAlertCallback,
+  };
+
+  SSL_CTX_set_session_cache_mode(client_ctx_.get(), SSL_SESS_CACHE_BOTH);
+  SSL_CTX_set_early_data_enabled(client_ctx_.get(), 1);
+  SSL_CTX_set_early_data_enabled(server_ctx_.get(), 1);
+  ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method));
+  ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method));
+
+  bssl::UniquePtr<SSL_SESSION> session = CreateClientSessionForQUIC();
+  ASSERT_TRUE(session);
+
+  for (bool reject_hrr : {false, true}) {
+    SCOPED_TRACE(reject_hrr);
+
+    ASSERT_TRUE(CreateClientAndServer());
+    if (reject_hrr) {
+      // Configure the server to prefer P-256, which will reject 0-RTT via
+      // HelloRetryRequest.
+      int p256 = NID_X9_62_prime256v1;
+      ASSERT_TRUE(SSL_set1_curves(server_.get(), &p256, 1));
+    } else {
+      // Disable 0-RTT on the server, so it will reject it.
+      SSL_set_early_data_enabled(server_.get(), 0);
     }
+    SSL_set_session(client_.get(), session.get());
 
+    // The client handshake should return immediately into the early data state.
+    ASSERT_EQ(SSL_do_handshake(client_.get()), 1);
+    EXPECT_TRUE(SSL_in_early_data(client_.get()));
+    // The transport should have keys for sending 0-RTT data.
+    EXPECT_TRUE(transport_->client()->HasSecrets(ssl_encryption_early_data));
+
+    // The server will consume the ClientHello, but it will not accept 0-RTT.
     ASSERT_TRUE(ProvideHandshakeData(server_.get()));
-    int server_ret = SSL_do_handshake(server_.get());
-    if (server_ret != 1) {
-      ASSERT_EQ(server_ret, -1);
-      ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ);
+    ASSERT_EQ(SSL_do_handshake(server_.get()), -1);
+    EXPECT_EQ(SSL_ERROR_WANT_READ, SSL_get_error(server_.get(), -1));
+    EXPECT_FALSE(SSL_in_early_data(server_.get()));
+    EXPECT_FALSE(transport_->server()->HasSecrets(ssl_encryption_early_data));
+
+    // The client consumes the server response and signals 0-RTT rejection.
+    for (;;) {
+      ASSERT_TRUE(ProvideHandshakeData(client_.get()));
+      ASSERT_EQ(-1, SSL_do_handshake(client_.get()));
+      int err = SSL_get_error(client_.get(), -1);
+      if (err == SSL_ERROR_EARLY_DATA_REJECTED) {
+        break;
+      }
+      ASSERT_EQ(SSL_ERROR_WANT_READ, err);
     }
 
-    if (client_ret == 1 && server_ret == 1) {
-      break;
-    }
+    // As in TLS over TCP, 0-RTT rejection is sticky.
+    ASSERT_EQ(-1, SSL_do_handshake(client_.get()));
+    ASSERT_EQ(SSL_ERROR_EARLY_DATA_REJECTED, SSL_get_error(client_.get(), -1));
+
+    // Finish up the client and server handshakes.
+    SSL_reset_early_data_reject(client_.get());
+    ASSERT_TRUE(CompleteHandshakesForQUIC());
+
+    // Both sides can now exchange 1-RTT data.
+    ExpectHandshakeSuccess();
+    EXPECT_TRUE(SSL_session_reused(client_.get()));
+    EXPECT_TRUE(SSL_session_reused(server_.get()));
+    EXPECT_FALSE(SSL_in_early_data(client_.get()));
+    EXPECT_FALSE(SSL_in_early_data(server_.get()));
+    EXPECT_FALSE(SSL_early_data_accepted(client_.get()));
+    EXPECT_FALSE(SSL_early_data_accepted(server_.get()));
   }
-
-  EXPECT_EQ(SSL_do_handshake(client_.get()), 1);
-  EXPECT_EQ(SSL_do_handshake(server_.get()), 1);
-  EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application));
-  EXPECT_FALSE(transport_.client()->has_alert());
-  EXPECT_FALSE(transport_.server()->has_alert());
 }
 
 // Test only releasing data to QUIC one byte at a time on request, to maximize
@@ -5137,11 +5278,7 @@
     }
   }
 
-  EXPECT_EQ(SSL_do_handshake(client_.get()), 1);
-  EXPECT_EQ(SSL_do_handshake(server_.get()), 1);
-  EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application));
-  EXPECT_FALSE(transport_.client()->has_alert());
-  EXPECT_FALSE(transport_.server()->has_alert());
+  ExpectHandshakeSuccess();
 }
 
 // Test buffering write data until explicit flushes.
@@ -5188,31 +5325,9 @@
   buffered_flights.Set(client_.get(), &client_flight);
   buffered_flights.Set(server_.get(), &server_flight);
 
-  for (;;) {
-    ASSERT_TRUE(ProvideHandshakeData(client_.get()));
-    int client_ret = SSL_do_handshake(client_.get());
-    if (client_ret != 1) {
-      ASSERT_EQ(client_ret, -1);
-      ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ);
-    }
+  ASSERT_TRUE(CompleteHandshakesForQUIC());
 
-    ASSERT_TRUE(ProvideHandshakeData(server_.get()));
-    int server_ret = SSL_do_handshake(server_.get());
-    if (server_ret != 1) {
-      ASSERT_EQ(server_ret, -1);
-      ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ);
-    }
-
-    if (client_ret == 1 && server_ret == 1) {
-      break;
-    }
-  }
-
-  EXPECT_EQ(SSL_do_handshake(client_.get()), 1);
-  EXPECT_EQ(SSL_do_handshake(server_.get()), 1);
-  EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application));
-  EXPECT_FALSE(transport_.client()->has_alert());
-  EXPECT_FALSE(transport_.server()->has_alert());
+  ExpectHandshakeSuccess();
 }
 
 // Test that excess data at one level is rejected. That is, if a single
@@ -5262,13 +5377,13 @@
   EXPECT_EQ(ERR_GET_REASON(err), SSL_R_BUFFERED_MESSAGES_ON_CIPHER_CHANGE);
 
   // The client sends an alert in response to this.
-  ASSERT_TRUE(transport_.client()->has_alert());
-  EXPECT_EQ(transport_.client()->alert_level(), ssl_encryption_initial);
-  EXPECT_EQ(transport_.client()->alert(), SSL_AD_UNEXPECTED_MESSAGE);
+  ASSERT_TRUE(transport_->client()->has_alert());
+  EXPECT_EQ(transport_->client()->alert_level(), ssl_encryption_initial);
+  EXPECT_EQ(transport_->client()->alert(), SSL_AD_UNEXPECTED_MESSAGE);
 
   // Sanity-check client did get far enough to process the ServerHello and
   // install keys.
-  EXPECT_TRUE(transport_.client()->HasSecrets(ssl_encryption_handshake));
+  EXPECT_TRUE(transport_->client()->HasSecrets(ssl_encryption_handshake));
 }
 
 // Test that |SSL_provide_quic_data| will reject data at the wrong level.
@@ -5298,7 +5413,7 @@
   // Data cannot be provided at the next level.
   std::vector<uint8_t> data;
   ASSERT_TRUE(
-      transport_.client()->ReadHandshakeData(&data, ssl_encryption_initial));
+      transport_->client()->ReadHandshakeData(&data, ssl_encryption_initial));
   ASSERT_FALSE(SSL_provide_quic_data(client_.get(), ssl_encryption_handshake,
                                      data.data(), data.size()));
   ERR_clear_error();
@@ -5312,7 +5427,7 @@
 
   // Data cannot be provided at the previous level.
   ASSERT_TRUE(
-      transport_.client()->ReadHandshakeData(&data, ssl_encryption_handshake));
+      transport_->client()->ReadHandshakeData(&data, ssl_encryption_handshake));
   ASSERT_FALSE(SSL_provide_quic_data(client_.get(), ssl_encryption_initial,
                                      data.data(), data.size()));
 }
@@ -5357,32 +5472,13 @@
   ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method));
   ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method));
   ASSERT_TRUE(CreateClientAndServer());
-
-  for (;;) {
-    ASSERT_TRUE(ProvideHandshakeData(client_.get()));
-    int client_ret = SSL_do_handshake(client_.get());
-    if (client_ret != 1) {
-      ASSERT_EQ(client_ret, -1);
-      ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ);
-    }
-
-    ASSERT_TRUE(ProvideHandshakeData(server_.get()));
-    int server_ret = SSL_do_handshake(server_.get());
-    if (server_ret != 1) {
-      ASSERT_EQ(server_ret, -1);
-      ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ);
-    }
-
-    if (client_ret == 1 && server_ret == 1) {
-      break;
-    }
-  }
+  ASSERT_TRUE(CompleteHandshakesForQUIC());
 
   EXPECT_EQ(SSL_do_handshake(client_.get()), 1);
   EXPECT_EQ(SSL_do_handshake(server_.get()), 1);
-  EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application));
-  EXPECT_FALSE(transport_.client()->has_alert());
-  EXPECT_FALSE(transport_.server()->has_alert());
+  EXPECT_TRUE(transport_->SecretsMatch(ssl_encryption_application));
+  EXPECT_FALSE(transport_->client()->has_alert());
+  EXPECT_FALSE(transport_->server()->has_alert());
 
   // Junk sent as part of post-handshake data should cause an error.
   uint8_t kJunk[] = {0x17, 0x0, 0x0, 0x4, 0xB, 0xE, 0xE, 0xF};
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index a7d0d89..12f4738 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -633,12 +633,16 @@
 
   if (ssl->s3->early_data_accepted) {
     hs->can_early_write = false;
-    ScopedCBB cbb;
-    CBB body;
-    if (!ssl->method->init_message(ssl, cbb.get(), &body,
-                                   SSL3_MT_END_OF_EARLY_DATA) ||
-        !ssl_add_message_cbb(ssl, cbb.get())) {
-      return ssl_hs_error;
+    // QUIC omits the EndOfEarlyData message. See draft-ietf-quic-tls-22,
+    // section 8.3.
+    if (ssl->quic_method == nullptr) {
+      ScopedCBB cbb;
+      CBB body;
+      if (!ssl->method->init_message(ssl, cbb.get(), &body,
+                                     SSL3_MT_END_OF_EARLY_DATA) ||
+          !ssl_add_message_cbb(ssl, cbb.get())) {
+        return ssl_hs_error;
+      }
     }
   }
 
@@ -911,6 +915,15 @@
       OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
       return false;
     }
+
+    // QUIC does not use the max_early_data_size parameter and always sets it to
+    // a fixed value. See draft-ietf-quic-tls-22, section 4.5.
+    if (ssl->quic_method != nullptr &&
+        session->ticket_max_early_data != 0xffffffff) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+      OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+      return false;
+    }
   }
 
   // Generate a session ID for this session. Some callers expect all sessions to
diff --git a/ssl/tls13_enc.cc b/ssl/tls13_enc.cc
index b8bebe1..83b3d62 100644
--- a/ssl/tls13_enc.cc
+++ b/ssl/tls13_enc.cc
@@ -178,6 +178,8 @@
     // encryption itself will be handled by the SSL_QUIC_METHOD.
     traffic_aead =
         SSLAEADContext::CreatePlaceholderForQUIC(version, session->cipher);
+    // QUIC never installs early data keys at the TLS layer.
+    assert(level != ssl_encryption_early_data);
   }
 
   if (!traffic_aead) {
@@ -226,7 +228,7 @@
 static const char kTLS13LabelClientApplicationTraffic[] = "c ap traffic";
 static const char kTLS13LabelServerApplicationTraffic[] = "s ap traffic";
 
-bool tls13_derive_early_secrets(SSL_HANDSHAKE *hs) {
+bool tls13_derive_early_secret(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
   if (!derive_secret(hs, hs->early_traffic_secret(),
                      label_to_span(kTLS13LabelClientEarlyTraffic)) ||
@@ -234,26 +236,30 @@
                       hs->early_traffic_secret())) {
     return false;
   }
+  return true;
+}
 
-  if (ssl->quic_method != nullptr) {
-    if (ssl->server) {
-      if (!ssl->quic_method->set_encryption_secrets(
-              ssl, ssl_encryption_early_data, nullptr,
-              hs->early_traffic_secret().data(),
-              hs->early_traffic_secret().size())) {
-        OPENSSL_PUT_ERROR(SSL, SSL_R_QUIC_INTERNAL_ERROR);
-        return false;
-      }
-    } else {
-      if (!ssl->quic_method->set_encryption_secrets(
-              ssl, ssl_encryption_early_data, hs->early_traffic_secret().data(),
-              nullptr, hs->early_traffic_secret().size())) {
-        OPENSSL_PUT_ERROR(SSL, SSL_R_QUIC_INTERNAL_ERROR);
-        return false;
-      }
+bool tls13_set_early_secret_for_quic(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  if (ssl->quic_method == nullptr) {
+    return true;
+  }
+  if (ssl->server) {
+    if (!ssl->quic_method->set_encryption_secrets(
+            ssl, ssl_encryption_early_data, hs->early_traffic_secret().data(),
+            /*write_secret=*/nullptr, hs->early_traffic_secret().size())) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_QUIC_INTERNAL_ERROR);
+      return false;
+    }
+  } else {
+    if (!ssl->quic_method->set_encryption_secrets(
+            ssl, ssl_encryption_early_data, /*read_secret=*/nullptr,
+            hs->early_traffic_secret().data(),
+            hs->early_traffic_secret().size())) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_QUIC_INTERNAL_ERROR);
+      return false;
     }
   }
-
   return true;
 }
 
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index f1891cf..a52a49c 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -146,7 +146,10 @@
     }
     session->ticket_age_add_valid = true;
     if (ssl->enable_early_data) {
-      session->ticket_max_early_data = kMaxEarlyDataAccepted;
+      // QUIC does not use the max_early_data_size parameter and always sets it
+      // to a fixed value. See draft-ietf-quic-tls-22, section 4.5.
+      session->ticket_max_early_data =
+          ssl->quic_method != nullptr ? 0xffffffff : kMaxEarlyDataAccepted;
     }
 
     static_assert(kNumTickets < 256, "Too many tickets");
@@ -442,7 +445,7 @@
   }
 
   if (ssl->s3->early_data_accepted) {
-    if (!tls13_derive_early_secrets(hs)) {
+    if (!tls13_derive_early_secret(hs)) {
       return ssl_hs_error;
     }
   } else if (hs->early_data_offered) {
@@ -468,6 +471,15 @@
     return ssl_hs_error;
   }
 
+  // Note we defer releasing the early traffic secret to QUIC until after ECDHE
+  // is resolved. The early traffic secret should be derived before the key
+  // schedule incorporates ECDHE, but doing so may reject 0-RTT. To avoid
+  // confusing the caller, we split derivation and releasing the secret to QUIC.
+  if (ssl->s3->early_data_accepted &&
+      !tls13_set_early_secret_for_quic(hs)) {
+    return ssl_hs_error;
+  }
+
   ssl->method->next_message(ssl);
   hs->tls13_state = state_send_server_hello;
   return ssl_hs_ok;
@@ -731,7 +743,8 @@
     // Finished early. See RFC 8446, section 4.6.1.
     static const uint8_t kEndOfEarlyData[4] = {SSL3_MT_END_OF_EARLY_DATA, 0,
                                                0, 0};
-    if (!hs->transcript.Update(kEndOfEarlyData)) {
+    if (ssl->quic_method == nullptr &&
+        !hs->transcript.Update(kEndOfEarlyData)) {
       OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
       return ssl_hs_error;
     }
@@ -772,7 +785,9 @@
 static enum ssl_hs_wait_t do_read_second_client_flight(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
   if (ssl->s3->early_data_accepted) {
-    if (!tls13_set_traffic_key(ssl, ssl_encryption_early_data, evp_aead_open,
+    // QUIC never receives handshake messages under 0-RTT keys.
+    if (ssl->quic_method == nullptr &&
+        !tls13_set_traffic_key(ssl, ssl_encryption_early_data, evp_aead_open,
                                hs->early_traffic_secret())) {
       return ssl_hs_error;
     }
@@ -780,6 +795,19 @@
     hs->can_early_read = true;
     hs->in_early_data = true;
   }
+
+  // QUIC doesn't use an EndOfEarlyData message (draft-ietf-quic-tls-22,
+  // section 8.3), so we switch to client_handshake_secret before the early
+  // return.
+  if (ssl->quic_method != nullptr) {
+    if (!tls13_set_traffic_key(ssl, ssl_encryption_handshake, evp_aead_open,
+                               hs->client_handshake_secret())) {
+      return ssl_hs_error;
+    }
+    hs->tls13_state = state_read_client_certificate;
+    return ssl->s3->early_data_accepted ? ssl_hs_early_return : ssl_hs_ok;
+  }
+
   hs->tls13_state = state_process_end_of_early_data;
   return ssl->s3->early_data_accepted ? ssl_hs_read_end_of_early_data
                                       : ssl_hs_ok;