Add QUIC client support for Trust Anchor IDs Design doc: https://docs.google.com/document/d/1aO36iL4lLve7X13j3COPz4D7465m64x8Si8i8W3JDDk/edit?resourcekey=0-x7c0yQk0k-MLysVvy4GI_g&tab=t.0#heading=h.bat9awopsp53 This CL adds QUIC client support for https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html. It adds a field to QuicSSLConfig that can be optionally be populated with a list of Trust Anchor IDs. If set, this will be used to configure BoringSSL to send the Trust Anchor IDs extension. To be able to test this, I added some rudimentary server support, namely adding a |trust_anchor_id| to ProofSource::Chain which, if non-empty, is configured alongside the server certificate using the SSL_CREDENTIAL API (see go/ssl-credential). Further work will be needed for true TAI server support, but this (along with a getter to check if the server indicated that the certificate matched the client's TAI list) is enough to test the client code. Protected by FLAGS_gfe2_reloadable_flag_enable_tls_trust_anchor_ids. PiperOrigin-RevId: 758041574
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h index bb8bf60..424b2b9 100755 --- a/quiche/common/quiche_feature_flags_list.h +++ b/quiche/common/quiche_feature_flags_list.h
@@ -9,6 +9,7 @@ #if defined(QUICHE_FLAG) QUICHE_FLAG(bool, quiche_reloadable_flag_enable_h3_origin_frame, false, true, "If true, enables support for parsing HTTP/3 ORIGIN frames.") +QUICHE_FLAG(bool, quiche_reloadable_flag_enable_tls_trust_anchor_ids, false, false, "When true, QUIC client and server will support TLS Trust Anchor IDs.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_act_upon_invalid_header, true, true, "If true, reject or send error response code upon receiving invalid request or response headers.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_add_stream_info_to_idle_close_detail, false, true, "If true, include stream information in idle timeout connection close detail.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_allow_client_enabled_bbr_v2, true, true, "If true, allow client to enable BBRv2 on server via connection option 'B2ON'.")
diff --git a/quiche/quic/core/crypto/proof_source.cc b/quiche/quic/core/crypto/proof_source.cc index 4114216..ff489c1 100644 --- a/quiche/quic/core/crypto/proof_source.cc +++ b/quiche/quic/core/crypto/proof_source.cc
@@ -18,8 +18,9 @@ } } -ProofSource::Chain::Chain(const std::vector<std::string>& certs) - : certs(certs) {} +ProofSource::Chain::Chain(const std::vector<std::string>& certs, + const std::string& trust_anchor_id) + : certs(certs), trust_anchor_id(trust_anchor_id) {} ProofSource::Chain::~Chain() {}
diff --git a/quiche/quic/core/crypto/proof_source.h b/quiche/quic/core/crypto/proof_source.h index 3976bfb..21276ed 100644 --- a/quiche/quic/core/crypto/proof_source.h +++ b/quiche/quic/core/crypto/proof_source.h
@@ -53,13 +53,17 @@ // Chain is a reference-counted wrapper for a vector of stringified // certificates. struct QUICHE_EXPORT Chain : public quiche::QuicheReferenceCounted { - explicit Chain(const std::vector<std::string>& certs); + Chain(const std::vector<std::string>& certs, + const std::string& trust_anchor_id = ""); Chain(const Chain&) = delete; Chain& operator=(const Chain&) = delete; CryptoBuffers ToCryptoBuffers() const; const std::vector<std::string> certs; + // Trust anchor ID to be configured alongside the certificate. If empty, no + // trust anchor ID will be set. + const std::string trust_anchor_id; protected: ~Chain() override;
diff --git a/quiche/quic/core/crypto/tls_server_connection.cc b/quiche/quic/core/crypto/tls_server_connection.cc index 9bf6dca..39e2f4a 100644 --- a/quiche/quic/core/crypto/tls_server_connection.cc +++ b/quiche/quic/core/crypto/tls_server_connection.cc
@@ -74,9 +74,34 @@ } void TlsServerConnection::SetCertChain( - const std::vector<CRYPTO_BUFFER*>& cert_chain) { - SSL_set_chain_and_key(ssl(), cert_chain.data(), cert_chain.size(), nullptr, - &TlsServerConnection::kPrivateKeyMethod); + const std::vector<CRYPTO_BUFFER*>& cert_chain, + const std::string& trust_anchor_id) { + if (GetQuicReloadableFlag(enable_tls_trust_anchor_ids)) { + QUIC_RELOADABLE_FLAG_COUNT_N(enable_tls_trust_anchor_ids, 1, 2); + SSL_CREDENTIAL* credential = SSL_CREDENTIAL_new_x509(); + SSL_CREDENTIAL_set1_cert_chain(credential, cert_chain.data(), + cert_chain.size()); + if (ssl_config().signing_algorithm_prefs.has_value()) { + SSL_CREDENTIAL_set1_signing_algorithm_prefs( + credential, ssl_config().signing_algorithm_prefs->data(), + ssl_config().signing_algorithm_prefs->size()); + } + SSL_CREDENTIAL_set_private_key_method( + credential, &TlsServerConnection::kPrivateKeyMethod); +#if defined(BORINGSSL_API_VERSION) && BORINGSSL_API_VERSION >= 36 + if (!trust_anchor_id.empty()) { + SSL_CREDENTIAL_set1_trust_anchor_id( + credential, reinterpret_cast<const uint8_t*>(trust_anchor_id.data()), + trust_anchor_id.size()); + SSL_CREDENTIAL_set_must_match_issuer(credential, 1); + } +#endif + SSL_add1_credential(ssl(), credential); + SSL_CREDENTIAL_free(credential); + } else { + SSL_set_chain_and_key(ssl(), cert_chain.data(), cert_chain.size(), nullptr, + &TlsServerConnection::kPrivateKeyMethod); + } } void TlsServerConnection::SetClientCertMode(ClientCertMode client_cert_mode) {
diff --git a/quiche/quic/core/crypto/tls_server_connection.h b/quiche/quic/core/crypto/tls_server_connection.h index 9301f2c..dc8a0a5 100644 --- a/quiche/quic/core/crypto/tls_server_connection.h +++ b/quiche/quic/core/crypto/tls_server_connection.h
@@ -133,7 +133,12 @@ absl::Status ConfigureSSL( ProofSourceHandleCallback::ConfigureSSLFunc configure_ssl); - void SetCertChain(const std::vector<CRYPTO_BUFFER*>& cert_chain); + // If |trust_anchor_id| is non-empty, it will be configured as the + // trust anchor ID for |cert_chain|, and BoringSSL will be + // configured to enforce issuer matching on this certificate. See + // https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html#section-4.1. + void SetCertChain(const std::vector<CRYPTO_BUFFER*>& cert_chain, + const std::string& trust_anchor_id); // Set the client cert mode to be used on this connection. This should be // called right after cert selection at the latest, otherwise it is too late
diff --git a/quiche/quic/core/quic_crypto_client_handshaker.h b/quiche/quic/core/quic_crypto_client_handshaker.h index 8e9ef05..e4dc402 100644 --- a/quiche/quic/core/quic_crypto_client_handshaker.h +++ b/quiche/quic/core/quic_crypto_client_handshaker.h
@@ -75,6 +75,7 @@ QUICHE_NOTREACHED(); return false; } + bool MatchedTrustAnchorIdForTesting() const override { return false; } // From QuicCryptoHandshaker void OnHandshakeMessage(const CryptoHandshakeMessage& message) override;
diff --git a/quiche/quic/core/quic_crypto_client_stream.cc b/quiche/quic/core/quic_crypto_client_stream.cc index aa11954..6b1abf7 100644 --- a/quiche/quic/core/quic_crypto_client_stream.cc +++ b/quiche/quic/core/quic_crypto_client_stream.cc
@@ -137,6 +137,10 @@ return handshaker_->chlo_hash(); } +bool QuicCryptoClientStream::MatchedTrustAnchorIdForTesting() const { + return handshaker_->MatchedTrustAnchorIdForTesting(); +} + void QuicCryptoClientStream::OnOneRttPacketAcknowledged() { handshaker_->OnOneRttPacketAcknowledged(); }
diff --git a/quiche/quic/core/quic_crypto_client_stream.h b/quiche/quic/core/quic_crypto_client_stream.h index 71844aa..ca7a39e 100644 --- a/quiche/quic/core/quic_crypto_client_stream.h +++ b/quiche/quic/core/quic_crypto_client_stream.h
@@ -235,6 +235,12 @@ absl::string_view context, size_t result_len, std::string* result) = 0; + + // Returns true if the server indicated during the handshake that it + // provided a certificate which matched one of the client-advertised trust + // anchor IDs + // (https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html#name-overview). + virtual bool MatchedTrustAnchorIdForTesting() const = 0; }; // ProofHandler is an interface that handles callbacks from the crypto @@ -307,6 +313,8 @@ size_t result_len, std::string* result) override; std::string chlo_hash() const; + bool MatchedTrustAnchorIdForTesting() const; + protected: void set_handshaker(std::unique_ptr<HandshakerInterface> handshaker) { handshaker_ = std::move(handshaker);
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h index f0649bd..836ee4f 100644 --- a/quiche/quic/core/quic_types.h +++ b/quiche/quic/core/quic_types.h
@@ -860,6 +860,12 @@ // As a client, whether ECH GREASE is enabled. If `ech_config_list` is // not empty, this value does nothing. bool ech_grease_enabled = false; + // If non-empty, the TLS Trust Anchor IDs to send in the TLS handshake. (See + // https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html.) + // The value should be a series of Trust Anchor IDs in wire format (a series + // of non-empty, 8-bit length-prefixed strings). If empty, the Trust Anchor + // IDs extension will not be sent. + std::string trust_anchor_ids; }; QUICHE_EXPORT bool operator==(const QuicSSLConfig& lhs,
diff --git a/quiche/quic/core/tls_client_handshaker.cc b/quiche/quic/core/tls_client_handshaker.cc index 4f18e59..6dc4ef9 100644 --- a/quiche/quic/core/tls_client_handshaker.cc +++ b/quiche/quic/core/tls_client_handshaker.cc
@@ -151,6 +151,26 @@ return false; } + // Configure TLS Trust Anchor IDs + // (https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html), + // if set. + if (GetQuicReloadableFlag(enable_tls_trust_anchor_ids)) { + QUIC_RELOADABLE_FLAG_COUNT_N(enable_tls_trust_anchor_ids, 2, 2); +#if defined(BORINGSSL_API_VERSION) && BORINGSSL_API_VERSION >= 36 + if (!tls_connection_.ssl_config().trust_anchor_ids.empty()) { + if (!SSL_set1_requested_trust_anchors( + ssl(), + reinterpret_cast<const uint8_t*>( + tls_connection_.ssl_config().trust_anchor_ids.data()), + tls_connection_.ssl_config().trust_anchor_ids.size())) { + CloseConnection(QUIC_HANDSHAKE_FAILED, + "Client failed to set TLS Trust Anchor IDs"); + return false; + } + } +#endif + } + // Start the handshake. AdvanceHandshake(); return session()->connection()->connected(); @@ -385,6 +405,10 @@ return ExportKeyingMaterialForLabel(label, context, result_len, result); } +bool TlsClientHandshaker::MatchedTrustAnchorIdForTesting() const { + return matched_trust_anchor_id_; +} + bool TlsClientHandshaker::encryption_established() const { return encryption_established_; } @@ -503,6 +527,10 @@ const std::vector<std::string>& certs, std::string* error_details, std::unique_ptr<ProofVerifyDetails>* details, uint8_t* out_alert, std::unique_ptr<ProofVerifierCallback> callback) { +#if defined(BORINGSSL_API_VERSION) && BORINGSSL_API_VERSION >= 36 + matched_trust_anchor_id_ = SSL_peer_matched_trust_anchor(ssl()); +#endif + const uint8_t* ocsp_response_raw; size_t ocsp_response_len; SSL_get0_ocsp_response(ssl(), &ocsp_response_raw, &ocsp_response_len);
diff --git a/quiche/quic/core/tls_client_handshaker.h b/quiche/quic/core/tls_client_handshaker.h index 0353ab1..6aab270 100644 --- a/quiche/quic/core/tls_client_handshaker.h +++ b/quiche/quic/core/tls_client_handshaker.h
@@ -52,6 +52,7 @@ std::string chlo_hash() const override; bool ExportKeyingMaterial(absl::string_view label, absl::string_view context, size_t result_len, std::string* result) override; + bool MatchedTrustAnchorIdForTesting() const override; // From QuicCryptoClientStream::HandshakerInterface and TlsHandshaker bool encryption_established() const override; @@ -168,6 +169,11 @@ std::unique_ptr<TransportParameters> received_transport_params_ = nullptr; std::unique_ptr<ApplicationState> received_application_state_ = nullptr; + + // True if the server indicated during the handshake that it served a + // certificate which matched a Trust Anchor ID sent by the client. This value + // is needed only for testing. + bool matched_trust_anchor_id_ = false; }; } // namespace quic
diff --git a/quiche/quic/core/tls_client_handshaker_test.cc b/quiche/quic/core/tls_client_handshaker_test.cc index 00b3734..675b994 100644 --- a/quiche/quic/core/tls_client_handshaker_test.cc +++ b/quiche/quic/core/tls_client_handshaker_test.cc
@@ -222,8 +222,10 @@ } // Initializes a fake server, and all its associated state, for testing. - void InitializeFakeServer() { + void InitializeFakeServer(const std::string& trust_anchor_id = "") { TestQuicSpdyServerSession* server_session = nullptr; + server_crypto_config_ = + crypto_test_utils::CryptoServerConfigForTesting(trust_anchor_id); CreateServerSessionForTest( server_id_, QuicTime::Delta::FromSeconds(100000), supported_versions_, &server_helper_, &alarm_factory_, server_crypto_config_.get(), @@ -301,6 +303,7 @@ CompleteCryptoHandshake(); EXPECT_EQ(PROTOCOL_TLS1_3, stream()->handshake_protocol()); EXPECT_TRUE(stream()->encryption_established()); + EXPECT_FALSE(stream()->MatchedTrustAnchorIdForTesting()); EXPECT_TRUE(stream()->one_rtt_keys_available()); EXPECT_FALSE(stream()->IsResumption()); } @@ -363,6 +366,40 @@ EXPECT_TRUE(stream()->one_rtt_keys_available()); } +#if defined(BORINGSSL_API_VERSION) && BORINGSSL_API_VERSION >= 36 +TEST_P(TlsClientHandshakerTest, HandshakeWithTrustAnchorIds) { + SetQuicReloadableFlag(enable_tls_trust_anchor_ids, true); + const std::string kTestTrustAnchorId = {0x03, 0x01, 0x02, 0x03}; + const std::string kTestServerTrustAnchorId = {0x01, 0x02, 0x03}; + InitializeFakeServer(kTestServerTrustAnchorId); + ssl_config_.emplace(); + ssl_config_->trust_anchor_ids = kTestTrustAnchorId; + CreateConnection(); + CompleteCryptoHandshake(); + ASSERT_TRUE(stream()->encryption_established()); + EXPECT_TRUE(stream()->MatchedTrustAnchorIdForTesting()); +} + +// Tests that the client can complete a handshake in which it sends multiple +// Trust Anchor IDs, one which matches the server's credential and one which +// doesn't. +TEST_P(TlsClientHandshakerTest, HandshakeWithMultipleTrustAnchorIds) { + SetQuicReloadableFlag(enable_tls_trust_anchor_ids, true); + // The client sends two trust anchor IDs, the first of which doesn't match the + // server's credential and the second does. + const std::string kTestTrustAnchorIds = {0x04, 0x00, 0x01, 0x02, 0x03, + 0x03, 0x01, 0x02, 0x03}; + const std::string kTestServerTrustAnchorId = {0x01, 0x02, 0x03}; + InitializeFakeServer(kTestServerTrustAnchorId); + ssl_config_.emplace(); + ssl_config_->trust_anchor_ids = kTestTrustAnchorIds; + CreateConnection(); + CompleteCryptoHandshake(); + ASSERT_TRUE(stream()->encryption_established()); + EXPECT_TRUE(stream()->MatchedTrustAnchorIdForTesting()); +} +#endif + TEST_P(TlsClientHandshakerTest, Resumption) { // Disable 0-RTT on the server so that we're only testing 1-RTT resumption: SSL_CTX_set_early_data_enabled(server_crypto_config_->ssl_ctx(), false);
diff --git a/quiche/quic/core/tls_server_handshaker.cc b/quiche/quic/core/tls_server_handshaker.cc index 8e84d57..f59f7bf 100644 --- a/quiche/quic/core/tls_server_handshaker.cc +++ b/quiche/quic/core/tls_server_handshaker.cc
@@ -1098,7 +1098,8 @@ local_config != nullptr) { if (local_config->chain && !local_config->chain->certs.empty()) { tls_connection_.SetCertChain( - local_config->chain->ToCryptoBuffers().value); + local_config->chain->ToCryptoBuffers().value, + local_config->chain->trust_anchor_id); select_cert_status_ = QUIC_SUCCESS; } else { QUIC_DLOG(ERROR) << "No certs provided for host '"
diff --git a/quiche/quic/test_tools/crypto_test_utils.cc b/quiche/quic/test_tools/crypto_test_utils.cc index f062ef2..5b5d2cb 100644 --- a/quiche/quic/test_tools/crypto_test_utils.cc +++ b/quiche/quic/test_tools/crypto_test_utils.cc
@@ -316,10 +316,11 @@ } // namespace -std::unique_ptr<QuicCryptoServerConfig> CryptoServerConfigForTesting() { +std::unique_ptr<QuicCryptoServerConfig> CryptoServerConfigForTesting( + const std::string& trust_anchor_id) { return std::make_unique<QuicCryptoServerConfig>( QuicCryptoServerConfig::TESTING, QuicRandom::GetInstance(), - ProofSourceForTesting(), KeyExchangeSource::Default()); + ProofSourceForTesting(trust_anchor_id), KeyExchangeSource::Default()); } int HandshakeWithFakeServer(QuicConfig* server_quic_config, @@ -903,11 +904,12 @@ class TestProofSource : public ProofSourceX509 { public: - TestProofSource() + explicit TestProofSource(const std::string& trust_anchor_id) : ProofSourceX509( quiche::QuicheReferenceCountedPointer<ProofSource::Chain>( new ProofSource::Chain( - std::vector<std::string>{std::string(kTestCertificate)})), + std::vector<std::string>{std::string(kTestCertificate)}, + trust_anchor_id)), std::move(*CertificatePrivateKey::LoadFromDer( kTestCertificatePrivateKey))) { QUICHE_DCHECK(valid()); @@ -989,8 +991,9 @@ } // namespace -std::unique_ptr<ProofSource> ProofSourceForTesting() { - return std::make_unique<TestProofSource>(); +std::unique_ptr<ProofSource> ProofSourceForTesting( + const std::string& trust_anchor_id) { + return std::make_unique<TestProofSource>(trust_anchor_id); } std::unique_ptr<ProofVerifier> ProofVerifierForTesting() {
diff --git a/quiche/quic/test_tools/crypto_test_utils.h b/quiche/quic/test_tools/crypto_test_utils.h index 3738885..79fc473 100644 --- a/quiche/quic/test_tools/crypto_test_utils.h +++ b/quiche/quic/test_tools/crypto_test_utils.h
@@ -75,7 +75,8 @@ // Returns a QuicCryptoServerConfig that is in a reasonable configuration to // pass into HandshakeWithFakeServer. -std::unique_ptr<QuicCryptoServerConfig> CryptoServerConfigForTesting(); +std::unique_ptr<QuicCryptoServerConfig> CryptoServerConfigForTesting( + const std::string& trust_anchor_id = ""); // returns: the number of client hellos that the client sent. int HandshakeWithFakeServer(QuicConfig* server_quic_config, @@ -170,8 +171,12 @@ // Returns the value for the tag |tag| in the tag value map of |message|. std::string GetValueForTag(const CryptoHandshakeMessage& message, QuicTag tag); -// Returns a new |ProofSource| that serves up test certificates. -std::unique_ptr<ProofSource> ProofSourceForTesting(); +// Returns a new |ProofSource| that serves up test certificates. If +// |trust_anchor_id| is provided, test certificates will be associated +// with it as described at +// https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html#section-4.1. +std::unique_ptr<ProofSource> ProofSourceForTesting( + const std::string& trust_anchor_id = ""); // Returns a new |ProofVerifier| that uses the QUIC testing root CA. std::unique_ptr<ProofVerifier> ProofVerifierForTesting();