Restrict when 0-RTT will be accepted in QUIC.

QUIC imposes additional restrictions on when 0-RTT data can be accepted.
With this change, a QUIC server configured to support 0-RTT will only
accept early data if the transport parameters and application protocol
specific context are a byte-for-byte match from the original connection
to the 0-RTT resumption attempt.

Bug: 295
Change-Id: Ie5d4688d1c9076b49f2131bb66b27c87e2ba041a
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/41145
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index b9ed71e..613ab0a 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3138,14 +3138,40 @@
 // |SSL_quic_max_handshake_flight_len| to get the maximum buffer length at each
 // encryption level.
 //
-// Note: 0-RTT support is incomplete and does not currently handle QUIC
-// transport parameters and server SETTINGS frame.
-//
 // QUIC implementations must additionally configure transport parameters with
 // |SSL_set_quic_transport_params|. |SSL_get_peer_quic_transport_params| may be
 // used to query the value received from the peer. BoringSSL handles this
 // extension as an opaque byte string. The caller is responsible for serializing
 // and parsing them. See draft-ietf-quic-transport (section 7.3) for details.
+//
+// QUIC additionally imposes restrictions on 0-RTT. In particular, the QUIC
+// transport layer requires that if a server accepts 0-RTT data, then the
+// transport parameters sent on the resumed connection must not lower any limits
+// compared to the transport parameters that the server sent on the connection
+// where the ticket for 0-RTT was issued. In effect, the server must remember
+// the transport parameters with the ticket. Application protocols running on
+// QUIC may impose similar restrictions, for example HTTP/3's restrictions on
+// SETTINGS frames.
+//
+// BoringSSL imposes a stricter check on the server to enforce these
+// restrictions. BoringSSL requires that the transport parameters and
+// application protocol state be a byte-for-byte match between the connection
+// where the ticket was issued and the connection where it is used for 0-RTT. If
+// there is a mismatch, BoringSSL will reject early data (but not reject the
+// resumption attempt).
+//
+// BoringSSL does not perform any client-side checks on the transport
+// parameters received from a server that also accepted early data. It is up to
+// the caller to verify that the received transport parameters do not lower any
+// limits, and to close the QUIC connection if that is not the case. The same
+// holds for any application protocol state remembered for 0-RTT, e.g. HTTP/3
+// SETTINGS.
+//
+// The transport parameter check happens automatically with
+// |SSL_set_quic_transport_params|. QUIC servers must set application state via
+// |SSL_set_quic_early_data_context| to configure the application protocol
+// check. No other mechanisms are provided to have BoringSSL reject early data
+// because of QUIC transport or application protocol restrictions.
 
 // ssl_encryption_level_t represents a specific QUIC encryption level used to
 // transmit handshake messages.
@@ -3292,6 +3318,18 @@
 OPENSSL_EXPORT void SSL_get_peer_quic_transport_params(
     const SSL *ssl, const uint8_t **out_params, size_t *out_params_len);
 
+// SSL_set_quic_early_data_context configures a context string in QUIC servers
+// for accepting early data. If a resumption connection offers early data, the
+// server will check if the value matches that of the connection which minted
+// the ticket. If not, resumption still succeeds but early data is rejected. For
+// HTTP/3, this should be the serialized server SETTINGS frame.
+//
+// This function may be called before |SSL_do_handshake| or during server
+// certificate selection. It returns 1 on success and 0 on failure.
+OPENSSL_EXPORT int SSL_set_quic_early_data_context(SSL *ssl,
+                                                   const uint8_t *context,
+                                                   size_t context_len);
+
 
 // Early data.
 //
@@ -3426,8 +3464,10 @@
   ssl_early_data_token_binding = 11,
   // The client and server ticket age were too far apart.
   ssl_early_data_ticket_age_skew = 12,
+  // QUIC parameters differ between this connection and the original.
+  ssl_early_data_quic_parameter_mismatch = 13,
   // The value of the largest entry.
-  ssl_early_data_reason_max_value = ssl_early_data_ticket_age_skew,
+  ssl_early_data_reason_max_value = ssl_early_data_quic_parameter_mismatch,
 };
 
 // SSL_get_early_data_reason returns details why 0-RTT was accepted or rejected
diff --git a/ssl/internal.h b/ssl/internal.h
index 04bf7a4..e1b0925 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -2685,6 +2685,9 @@
   // Contains the QUIC transport params that this endpoint will send.
   Array<uint8_t> quic_transport_params;
 
+  // Contains the context used to decide whether to accept early data in QUIC.
+  Array<uint8_t> quic_early_data_context;
+
   // verify_sigalgs, if not empty, is the set of signature algorithms
   // accepted from the peer in decreasing order of preference.
   Array<uint16_t> verify_sigalgs;
@@ -2737,6 +2740,11 @@
   bool jdk11_workaround : 1;
 };
 
+// Computes a SHA-256 hash of the transport parameters and early data context
+// for QUIC, putting the hash in |SHA256_DIGEST_LENGTH| bytes at |hash_out|.
+bool compute_quic_early_data_hash(const SSL_CONFIG *config,
+                                  uint8_t hash_out[SHA256_DIGEST_LENGTH]);
+
 // From RFC 8446, used in determining PSK modes.
 #define SSL_PSK_DHE_KE 0x1
 
@@ -3551,6 +3559,10 @@
   // is_quic indicates whether this session was created using QUIC.
   bool is_quic : 1;
 
+  // quic_early_data_hash is used to determine whether early data must be
+  // rejected when performing a QUIC handshake.
+  bssl::Array<uint8_t> quic_early_data_hash;
+
  private:
   ~ssl_session_st();
   friend void SSL_SESSION_free(SSL_SESSION *);
diff --git a/ssl/ssl_asn1.cc b/ssl/ssl_asn1.cc
index 98ea4fe..7401d09 100644
--- a/ssl/ssl_asn1.cc
+++ b/ssl/ssl_asn1.cc
@@ -130,6 +130,7 @@
 //     authTimeout             [25] INTEGER OPTIONAL, -- defaults to timeout
 //     earlyALPN               [26] OCTET STRING OPTIONAL,
 //     isQuic                  [27] BOOLEAN OPTIONAL,
+//     quicEarlyDataHash       [28] OCTET STRING OPTIONAL,
 // }
 //
 // Note: historically this serialization has included other optional
@@ -191,6 +192,8 @@
     CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 26;
 static const unsigned kIsQuicTag =
     CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 27;
+static const unsigned kQuicEarlyDataHashTag =
+    CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 28;
 
 static int SSL_SESSION_to_bytes_full(const SSL_SESSION *in, CBB *cbb,
                                      int for_ticket) {
@@ -399,6 +402,14 @@
     }
   }
 
+  if (!in->quic_early_data_hash.empty()) {
+    if (!CBB_add_asn1(&session, &child, kQuicEarlyDataHashTag) ||
+        !CBB_add_asn1_octet_string(&child, in->quic_early_data_hash.data(),
+                                   in->quic_early_data_hash.size())) {
+      OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+      return 0;
+    }
+  }
 
   return CBB_flush(cbb);
 }
@@ -741,6 +752,8 @@
                                       kEarlyALPNTag) ||
       !CBS_get_optional_asn1_bool(&session, &is_quic, kIsQuicTag,
                                   /*default_value=*/false) ||
+      !SSL_SESSION_parse_octet_string(&session, &ret->quic_early_data_hash,
+                                      kQuicEarlyDataHashTag) ||
       CBS_len(&session) != 0) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
     return nullptr;
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 3cebfe0..625f733 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -1248,6 +1248,12 @@
   *out_params_len = ssl->s3->peer_quic_transport_params.size();
 }
 
+int SSL_set_quic_early_data_context(SSL *ssl, const uint8_t *context,
+                                    size_t context_len) {
+  return ssl->config && ssl->config->quic_early_data_context.CopyFrom(
+                            MakeConstSpan(context, context_len));
+}
+
 void SSL_CTX_set_early_data_enabled(SSL_CTX *ctx, int enabled) {
   ctx->enable_early_data = !!enabled;
 }
diff --git a/ssl/ssl_session.cc b/ssl/ssl_session.cc
index 0d372cb..fa994e8 100644
--- a/ssl/ssl_session.cc
+++ b/ssl/ssl_session.cc
@@ -268,6 +268,11 @@
     if (!new_session->early_alpn.CopyFrom(session->early_alpn)) {
       return nullptr;
     }
+
+    if (!new_session->quic_early_data_hash.CopyFrom(
+            session->quic_early_data_hash)) {
+      return nullptr;
+    }
   }
 
   // Copy the ticket.
@@ -344,6 +349,25 @@
                                   session->cipher);
 }
 
+bool compute_quic_early_data_hash(const SSL_CONFIG *config,
+                                  uint8_t hash_out[SHA256_DIGEST_LENGTH]) {
+  ScopedEVP_MD_CTX hash_ctx;
+  uint32_t transport_param_len = config->quic_transport_params.size();
+  uint32_t context_len = config->quic_early_data_context.size();
+  if (!EVP_DigestInit(hash_ctx.get(), EVP_sha256()) ||
+      !EVP_DigestUpdate(hash_ctx.get(), &transport_param_len,
+                        sizeof(transport_param_len)) ||
+      !EVP_DigestUpdate(hash_ctx.get(), config->quic_transport_params.data(),
+                        config->quic_transport_params.size()) ||
+      !EVP_DigestUpdate(hash_ctx.get(), &context_len, sizeof(context_len)) ||
+      !EVP_DigestUpdate(hash_ctx.get(), config->quic_early_data_context.data(),
+                        config->quic_early_data_context.size()) ||
+      !EVP_DigestFinal(hash_ctx.get(), hash_out, nullptr)) {
+    return false;
+  }
+  return true;
+}
+
 int ssl_get_new_session(SSL_HANDSHAKE *hs, int is_server) {
   SSL *const ssl = hs->ssl;
   if (ssl->mode & SSL_MODE_NO_SESSION_CREATION) {
@@ -359,6 +383,13 @@
   session->is_server = is_server;
   session->ssl_version = ssl->version;
   session->is_quic = ssl->quic_method != nullptr;
+  if (is_server && ssl->enable_early_data && session->is_quic) {
+    if (!session->quic_early_data_hash.Init(SHA256_DIGEST_LENGTH) ||
+        !compute_quic_early_data_hash(hs->config,
+                                      session->quic_early_data_hash.data())) {
+      return 0;
+    }
+  }
 
   // Fill in the time from the |SSL_CTX|'s clock.
   struct OPENSSL_timeval now;
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 424f7d2..fc7976e 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -5106,11 +5106,15 @@
       transport_->client()->AllowOutOfOrderWrites();
       transport_->server()->AllowOutOfOrderWrites();
     }
-    static const uint8_t transport_params[] = {0};
-    if (!SSL_set_quic_transport_params(client_.get(), transport_params,
-                                       sizeof(transport_params)) ||
-        !SSL_set_quic_transport_params(server_.get(), transport_params,
-                                       sizeof(transport_params))) {
+    static const uint8_t client_transport_params[] = {0};
+    if (!SSL_set_quic_transport_params(client_.get(), client_transport_params,
+                                       sizeof(client_transport_params)) ||
+        !SSL_set_quic_transport_params(server_.get(),
+                                       server_transport_params_.data(),
+                                       server_transport_params_.size()) ||
+        !SSL_set_quic_early_data_context(
+            server_.get(), server_quic_early_data_context_.data(),
+            server_quic_early_data_context_.size())) {
       return false;
     }
     return true;
@@ -5256,6 +5260,9 @@
   bssl::UniquePtr<SSL> client_;
   bssl::UniquePtr<SSL> server_;
 
+  std::vector<uint8_t> server_transport_params_ = {1};
+  std::vector<uint8_t> server_quic_early_data_context_ = {2};
+
   bool allow_out_of_order_writes_ = false;
 };
 
@@ -5413,6 +5420,88 @@
   EXPECT_TRUE(SSL_early_data_accepted(server_.get()));
 }
 
+TEST_F(QUICMethodTest, ZeroRTTRejectMismatchedParameters) {
+  const SSL_QUIC_METHOD quic_method = DefaultQUICMethod();
+
+  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 change_transport_params : {false, true}) {
+    SCOPED_TRACE(change_transport_params);
+    for (bool change_context : {false, true}) {
+      if (!change_transport_params && !change_context) {
+        continue;
+      }
+      SCOPED_TRACE(change_context);
+
+      ASSERT_TRUE(CreateClientAndServer());
+      static const uint8_t new_transport_params[] = {3};
+      static const uint8_t new_context[] = {4};
+      if (change_transport_params) {
+        ASSERT_TRUE(SSL_set_quic_transport_params(
+            server_.get(), new_transport_params, sizeof(new_transport_params)));
+      }
+      if (change_context) {
+        ASSERT_TRUE(SSL_set_quic_early_data_context(server_.get(), new_context,
+                                                    sizeof(new_context)));
+      }
+      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()->HasWriteSecret(ssl_encryption_early_data));
+
+      // The server will consume the ClientHello, but it will not accept 0-RTT.
+      ASSERT_TRUE(ProvideHandshakeData(server_.get()));
+      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()->HasReadSecret(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);
+      }
+
+      // 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()));
+    }
+  }
+}
+
 TEST_F(QUICMethodTest, ZeroRTTReject) {
   const SSL_QUIC_METHOD quic_method = DefaultQUICMethod();
 
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 4e2fa50..d8652ea 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -424,6 +424,8 @@
       return "token_binding";
     case ssl_early_data_ticket_age_skew:
       return "ticket_age_skew";
+    case ssl_early_data_quic_parameter_mismatch:
+      return "quic_parameter_mismatch";
   }
 
   abort();
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 6730f83..683a2ca 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -309,6 +309,23 @@
   return ssl_ticket_aead_success;
 }
 
+static bool quic_ticket_compatible(const SSL_SESSION *session,
+                                   const SSL_CONFIG *config) {
+  if (!session->is_quic) {
+    return true;
+  }
+  if (session->quic_early_data_hash.size() != SHA256_DIGEST_LENGTH) {
+    return false;
+  }
+  uint8_t early_data_hash[SHA256_DIGEST_LENGTH];
+  if (!compute_quic_early_data_hash(config, early_data_hash) ||
+      CRYPTO_memcmp(session->quic_early_data_hash.data(), early_data_hash,
+                    SHA256_DIGEST_LENGTH) != 0) {
+    return false;
+  }
+  return true;
+}
+
 static enum ssl_hs_wait_t do_select_session(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
   SSLMessage msg;
@@ -374,6 +391,8 @@
       } else if (ssl->s3->ticket_age_skew < -kMaxTicketAgeSkewSeconds ||
                  kMaxTicketAgeSkewSeconds < ssl->s3->ticket_age_skew) {
         ssl->s3->early_data_reason = ssl_early_data_ticket_age_skew;
+      } else if (!quic_ticket_compatible(session.get(), hs->config)) {
+        ssl->s3->early_data_reason = ssl_early_data_quic_parameter_mismatch;
       } else {
         ssl->s3->early_data_reason = ssl_early_data_accepted;
         ssl->s3->early_data_accepted = true;