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;