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;