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();