Modify how QUIC 0-RTT go/no-go decision is made.
The previous implementation was too strict in its byte-for-byte equality
check including Transport Parameters, because the Transport Parameters
contain a field that QUIC requires be different on each connection. This
change still has BoringSSL do a byte-for-byte check, but now it is only
done over the quic_early_data_context. An additional requirement is
imposed that the quic_early_data_context must be set for early data
capable tickets to be issued.
Bug: 295
Change-Id: I5145c10752b41908b6807c3a3c967653b0c13f37
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/41427
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index a61488e..5a5cbdc 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3153,12 +3153,15 @@
// 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 implements this check by doing a byte-for-byte comparison of an
+// opaque context passed in by the server. This context must be the same on the
+// connection where the ticket was issued and the connection where that ticket
+// is used for 0-RTT. If there is a mismatch, or the context was not set,
+// BoringSSL will reject early data (but not reject the resumption attempt).
+// This context is set via |SSL_set_quic_early_data_context| and should cover
+// both transport parameters and any application state.
+// |SSL_set_quic_early_data_context| must be called on the server with a
+// non-empty context if the server is to support 0-RTT in QUIC.
//
// 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
@@ -3166,12 +3169,6 @@
// 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.
@@ -3321,8 +3318,12 @@
// 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.
+// the ticket. If not, resumption still succeeds but early data is rejected.
+// This should include all QUIC Transport Parameters except ones specified that
+// the client MUST NOT remember. This should also include any application
+// protocol-specific state. For HTTP/3, this should be the serialized server
+// SETTINGS frame and the QUIC Transport Parameters (except the stateless reset
+// token).
//
// This function may be called before |SSL_do_handshake| or during server
// certificate selection. It returns 1 on success and 0 on failure.
diff --git a/ssl/internal.h b/ssl/internal.h
index e1b0925..b1c8bd1 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -2740,11 +2740,6 @@
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
@@ -3559,9 +3554,9 @@
// 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
+ // quic_early_data_context is used to determine whether early data must be
// rejected when performing a QUIC handshake.
- bssl::Array<uint8_t> quic_early_data_hash;
+ bssl::Array<uint8_t> quic_early_data_context;
private:
~ssl_session_st();
diff --git a/ssl/ssl_asn1.cc b/ssl/ssl_asn1.cc
index 7401d09..e6274f1 100644
--- a/ssl/ssl_asn1.cc
+++ b/ssl/ssl_asn1.cc
@@ -192,7 +192,7 @@
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 26;
static const unsigned kIsQuicTag =
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 27;
-static const unsigned kQuicEarlyDataHashTag =
+static const unsigned kQuicEarlyDataContextTag =
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 28;
static int SSL_SESSION_to_bytes_full(const SSL_SESSION *in, CBB *cbb,
@@ -402,10 +402,10 @@
}
}
- 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())) {
+ if (!in->quic_early_data_context.empty()) {
+ if (!CBB_add_asn1(&session, &child, kQuicEarlyDataContextTag) ||
+ !CBB_add_asn1_octet_string(&child, in->quic_early_data_context.data(),
+ in->quic_early_data_context.size())) {
OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
return 0;
}
@@ -752,8 +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) ||
+ !SSL_SESSION_parse_octet_string(&session, &ret->quic_early_data_context,
+ kQuicEarlyDataContextTag) ||
CBS_len(&session) != 0) {
OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
return nullptr;
diff --git a/ssl/ssl_session.cc b/ssl/ssl_session.cc
index fa994e8..4c6d045 100644
--- a/ssl/ssl_session.cc
+++ b/ssl/ssl_session.cc
@@ -269,8 +269,8 @@
return nullptr;
}
- if (!new_session->quic_early_data_hash.CopyFrom(
- session->quic_early_data_hash)) {
+ if (!new_session->quic_early_data_context.CopyFrom(
+ session->quic_early_data_context)) {
return nullptr;
}
}
@@ -349,25 +349,6 @@
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) {
@@ -384,9 +365,8 @@
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())) {
+ if (!session->quic_early_data_context.CopyFrom(
+ hs->config->quic_early_data_context)) {
return 0;
}
}
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index fc7976e..73f0b9d 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -5433,73 +5433,68 @@
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_context[] = {4};
+ ASSERT_TRUE(SSL_set_quic_early_data_context(server_.get(), new_context,
+ sizeof(new_context)));
+ SSL_set_session(client_.get(), session.get());
- 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 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 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()));
+ // 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, NoZeroRTTTicketWithoutEarlyDataContext) {
+ server_quic_early_data_context_ = {};
+ 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);
+ EXPECT_FALSE(SSL_SESSION_early_data_capable(session.get()));
}
TEST_F(QUICMethodTest, ZeroRTTReject) {
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 683a2ca..33f821e 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -127,7 +127,10 @@
return false;
}
session->ticket_age_add_valid = true;
- if (ssl->enable_early_data) {
+ bool enable_early_data =
+ ssl->enable_early_data &&
+ (!ssl->quic_method || !ssl->config->quic_early_data_context.empty());
+ if (enable_early_data) {
// 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 =
@@ -152,7 +155,7 @@
return false;
}
- if (ssl->enable_early_data) {
+ if (enable_early_data) {
CBB early_data;
if (!CBB_add_u16(&extensions, TLSEXT_TYPE_early_data) ||
!CBB_add_u16_length_prefixed(&extensions, &early_data) ||
@@ -314,13 +317,13 @@
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) {
+
+ if (session->quic_early_data_context.empty() ||
+ config->quic_early_data_context.size() !=
+ session->quic_early_data_context.size() ||
+ CRYPTO_memcmp(config->quic_early_data_context.data(),
+ session->quic_early_data_context.data(),
+ session->quic_early_data_context.size()) != 0) {
return false;
}
return true;