Add Trust Anchors extension

Bug: 398275713
Change-Id: I9d15693ae88440817585b2d1d5a62244529f45b7
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/73087
Commit-Queue: Bob Beck <bbe@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index d8eb3a4..7d77cfd 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -243,6 +243,8 @@
 SSL,237,UNSUPPORTED_CIPHER
 SSL,238,UNSUPPORTED_COMPRESSION_ALGORITHM
 SSL,327,UNSUPPORTED_CREDENTIAL_LIST
+SSL,328,INVALID_TRUST_ANCHOR_LIST
+SSL,329,INVALID_CERTIFICATE_PROPERTY_LIST
 SSL,312,UNSUPPORTED_ECH_SERVER_CONFIG
 SSL,239,UNSUPPORTED_ELLIPTIC_CURVE
 SSL,240,UNSUPPORTED_PROTOCOL
diff --git a/gen/crypto/err_data.cc b/gen/crypto/err_data.cc
index fd67be8..3ccad14 100644
--- a/gen/crypto/err_data.cc
+++ b/gen/crypto/err_data.cc
@@ -198,51 +198,51 @@
     0x283500f7,
     0x28358c81,
     0x2836099a,
-    0x2c32337d,
+    0x2c3233b9,
     0x2c3293a3,
-    0x2c33338b,
-    0x2c33b39d,
-    0x2c3433b1,
-    0x2c34b3c3,
-    0x2c3533de,
-    0x2c35b3f0,
-    0x2c363420,
+    0x2c3333c7,
+    0x2c33b3d9,
+    0x2c3433ed,
+    0x2c34b3ff,
+    0x2c35341a,
+    0x2c35b42c,
+    0x2c36345c,
     0x2c36833a,
-    0x2c37342d,
-    0x2c37b459,
-    0x2c383497,
-    0x2c38b4ae,
-    0x2c3934cc,
-    0x2c39b4dc,
-    0x2c3a34ee,
-    0x2c3ab502,
-    0x2c3b3513,
-    0x2c3bb532,
+    0x2c373469,
+    0x2c37b495,
+    0x2c3834d3,
+    0x2c38b4ea,
+    0x2c393508,
+    0x2c39b518,
+    0x2c3a352a,
+    0x2c3ab53e,
+    0x2c3b354f,
+    0x2c3bb56e,
     0x2c3c13b5,
     0x2c3c93cb,
-    0x2c3d3577,
+    0x2c3d35b3,
     0x2c3d93e4,
-    0x2c3e35a1,
-    0x2c3eb5af,
-    0x2c3f35c7,
-    0x2c3fb5df,
-    0x2c403609,
+    0x2c3e35dd,
+    0x2c3eb5eb,
+    0x2c3f3603,
+    0x2c3fb61b,
+    0x2c403645,
     0x2c409298,
-    0x2c41361a,
-    0x2c41b62d,
+    0x2c413656,
+    0x2c41b669,
     0x2c42125e,
-    0x2c42b63e,
+    0x2c42b67a,
     0x2c43076d,
-    0x2c43b524,
-    0x2c44346c,
-    0x2c44b5ec,
-    0x2c453403,
-    0x2c45b43f,
-    0x2c4634bc,
-    0x2c46b546,
-    0x2c47355b,
-    0x2c47b594,
-    0x2c48347e,
+    0x2c43b560,
+    0x2c4434a8,
+    0x2c44b628,
+    0x2c45343f,
+    0x2c45b47b,
+    0x2c4634f8,
+    0x2c46b582,
+    0x2c473597,
+    0x2c47b5d0,
+    0x2c4834ba,
     0x30320000,
     0x30328015,
     0x3033001f,
@@ -519,20 +519,20 @@
     0x407630ed,
     0x4076935b,
     0x40773112,
-    0x4077b16e,
-    0x40783189,
-    0x4078b1c2,
-    0x407931d9,
-    0x4079b1ef,
-    0x407a321b,
-    0x407ab22e,
-    0x407b3243,
-    0x407bb255,
-    0x407c3286,
-    0x407cb28f,
+    0x4077b1aa,
+    0x407831c5,
+    0x4078b1fe,
+    0x40793215,
+    0x4079b22b,
+    0x407a3257,
+    0x407ab26a,
+    0x407b327f,
+    0x407bb291,
+    0x407c32c2,
+    0x407cb2cb,
     0x407d2935,
     0x407da20d,
-    0x407e319e,
+    0x407e31da,
     0x407ea46a,
     0x407f1e32,
     0x407fa005,
@@ -558,7 +558,7 @@
     0x40899bc7,
     0x408a2bb4,
     0x408a99e5,
-    0x408b326a,
+    0x408b32a6,
     0x408bafc6,
     0x408c253a,
     0x408d1f56,
@@ -578,7 +578,7 @@
     0x40941e82,
     0x4094abcd,
     0x40952739,
-    0x4095b1fb,
+    0x4095b237,
     0x40962f23,
     0x4096a198,
     0x4097229e,
@@ -591,7 +591,7 @@
     0x409a9a01,
     0x409b1edc,
     0x409b9f07,
-    0x409c3150,
+    0x409c318c,
     0x409c9f2f,
     0x409d2154,
     0x409da122,
@@ -607,6 +607,8 @@
     0x40a2a608,
     0x40a3267c,
     0x40a3b134,
+    0x40a43150,
+    0x40a4b16a,
     0x41f42a6e,
     0x41f92b00,
     0x41fe29f3,
@@ -697,71 +699,71 @@
     0x4c4194ad,
     0x4c421616,
     0x4c4293f5,
-    0x50323650,
-    0x5032b65f,
-    0x5033366a,
-    0x5033b67a,
-    0x50343693,
-    0x5034b6ad,
-    0x503536bb,
-    0x5035b6d1,
-    0x503636e3,
-    0x5036b6f9,
-    0x50373712,
-    0x5037b725,
-    0x5038373d,
-    0x5038b74e,
-    0x50393763,
-    0x5039b777,
-    0x503a3797,
-    0x503ab7ad,
-    0x503b37c5,
-    0x503bb7d7,
-    0x503c37f3,
-    0x503cb80a,
-    0x503d3823,
-    0x503db839,
-    0x503e3846,
-    0x503eb85c,
-    0x503f386e,
+    0x5032368c,
+    0x5032b69b,
+    0x503336a6,
+    0x5033b6b6,
+    0x503436cf,
+    0x5034b6e9,
+    0x503536f7,
+    0x5035b70d,
+    0x5036371f,
+    0x5036b735,
+    0x5037374e,
+    0x5037b761,
+    0x50383779,
+    0x5038b78a,
+    0x5039379f,
+    0x5039b7b3,
+    0x503a37d3,
+    0x503ab7e9,
+    0x503b3801,
+    0x503bb813,
+    0x503c382f,
+    0x503cb846,
+    0x503d385f,
+    0x503db875,
+    0x503e3882,
+    0x503eb898,
+    0x503f38aa,
     0x503f83b3,
-    0x50403881,
-    0x5040b891,
-    0x504138ab,
-    0x5041b8ba,
-    0x504238d4,
-    0x5042b8f1,
-    0x50433901,
-    0x5043b911,
-    0x5044392e,
+    0x504038bd,
+    0x5040b8cd,
+    0x504138e7,
+    0x5041b8f6,
+    0x50423910,
+    0x5042b92d,
+    0x5043393d,
+    0x5043b94d,
+    0x5044396a,
     0x50448469,
-    0x50453942,
-    0x5045b960,
-    0x50463973,
-    0x5046b989,
-    0x5047399b,
-    0x5047b9b0,
-    0x504839d6,
-    0x5048b9e4,
-    0x504939f7,
-    0x5049ba0c,
-    0x504a3a22,
-    0x504aba32,
-    0x504b3a52,
-    0x504bba65,
-    0x504c3a88,
-    0x504cbab6,
-    0x504d3ae3,
-    0x504dbb00,
-    0x504e3b1b,
-    0x504ebb37,
-    0x504f3b49,
-    0x504fbb60,
-    0x50503b6f,
+    0x5045397e,
+    0x5045b99c,
+    0x504639af,
+    0x5046b9c5,
+    0x504739d7,
+    0x5047b9ec,
+    0x50483a12,
+    0x5048ba20,
+    0x50493a33,
+    0x5049ba48,
+    0x504a3a5e,
+    0x504aba6e,
+    0x504b3a8e,
+    0x504bbaa1,
+    0x504c3ac4,
+    0x504cbaf2,
+    0x504d3b1f,
+    0x504dbb3c,
+    0x504e3b57,
+    0x504ebb73,
+    0x504f3b85,
+    0x504fbb9c,
+    0x50503bab,
     0x50508729,
-    0x50513b82,
-    0x5051b920,
-    0x50523ac8,
+    0x50513bbe,
+    0x5051b95c,
+    0x50523b04,
     0x58320fd1,
     0x68320f93,
     0x68328ceb,
@@ -806,19 +808,19 @@
     0x7c321274,
     0x803214c0,
     0x80328090,
-    0x8033334c,
+    0x80333388,
     0x803380b9,
-    0x8034335b,
-    0x8034b2c3,
-    0x803532e1,
-    0x8035b36f,
-    0x80363323,
-    0x8036b2d2,
-    0x80373315,
-    0x8037b2b0,
-    0x80383336,
-    0x8038b2f2,
-    0x80393307,
+    0x80343397,
+    0x8034b2ff,
+    0x8035331d,
+    0x8035b3ab,
+    0x8036335f,
+    0x8036b30e,
+    0x80373351,
+    0x8037b2ec,
+    0x80383372,
+    0x8038b32e,
+    0x80393343,
 };
 
 extern const size_t kOpenSSLReasonValuesLen;
@@ -1399,6 +1401,8 @@
     "UNSAFE_LEGACY_RENEGOTIATION_DISABLED\0"
     "UNSUPPORTED_COMPRESSION_ALGORITHM\0"
     "UNSUPPORTED_CREDENTIAL_LIST\0"
+    "INVALID_TRUST_ANCHOR_LIST\0"
+    "INVALID_CERTIFICATE_PROPERTY_LIST\0"
     "UNSUPPORTED_ECH_SERVER_CONFIG\0"
     "UNSUPPORTED_ELLIPTIC_CURVE\0"
     "UNSUPPORTED_PROTOCOL\0"
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 7c6804d..c9783e2 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -827,6 +827,26 @@
 OPENSSL_EXPORT int SSL_CREDENTIAL_set1_ocsp_response(SSL_CREDENTIAL *cred,
                                                      CRYPTO_BUFFER *ocsp);
 
+// SSL_CREDENTIAL_set1_certificate_properties parses
+// |certificate_property_list| as a CertificatePropertyList (see Section 6 of
+// draft-ietf-tls-trust-anchor-ids-00) and applies recognized properties to
+// |cred|. It returns one on success and zero on error. It is an error if
+// |certificate_property_list| does not parse correctly, or if any recognized
+// properties from |certificate_property_list| cannot be applied to |cred|.
+//
+// CertificatePropertyList is an extensible structure which allows serving
+// properties of a certificate chain to be passed from a CA, through an
+// application's issuance and configuration pipeline, and to the TLS serving
+// logic, without requiring application changes for each property defined.
+//
+// BoringSSL currently supports the following properties:
+// * trust_anchor_identifier (see |SSL_CREDENTIAL_set1_trust_anchor_id|)
+//
+// Note this function does not automatically enable issuer matching. Callers
+// must separately call |SSL_CREDENTIAL_set_must_match_issuer| if desired.
+OPENSSL_EXPORT int SSL_CREDENTIAL_set1_certificate_properties(
+    SSL_CREDENTIAL *cred, CRYPTO_BUFFER *cert_property_list);
+
 // SSL_CREDENTIAL_set1_signed_cert_timestamp_list sets |cred|'s list of signed
 // certificate timestamps |sct_list|. |sct_list| must contain one or more SCT
 // structures serialised as a SignedCertificateTimestampList (see
@@ -841,18 +861,20 @@
 // if the peer supports the certificate chain's issuer.
 //
 // If |match| is non-zero, |cred| will only be applicable when the certificate
-// chain is issued by some CA requested by the peer, e.g. in the
-// certificate_authorities extension. This can be used for certificate chains
-// that may not be usable by all peers, e.g. chains with fewer cross-signs or
-// issued from a newer CA.
+// chain is issued by some CA requested by the peer in the
+// certificate_authorities extension or, if |cred| has a trust anchor ID (see
+// |SSL_CREDENTIAL_set1_trust_anchor_id|), the trust_anchors extension. |cred|'s
+// certificate chain must then be a correctly ordered certification path.
 //
 // If |match| is zero (default), |cred| will not be conditioned on the peer's
 // requested CAs. This can be used for certificate chains that are assumed to be
 // usable by most peers.
 //
-// The credential list is tried in order, so more specific credentials that
-// enable issuer matching should generally be ordered before less specific
-// credentials that do not.
+// This setting can be used for certificate chains that may not be usable by all
+// peers, e.g. chains with fewer cross-signs or issued from a newer CA. The
+// credential list is tried in order, so more specific credentials that enable
+// issuer matching should generally be ordered before less specific credentials
+// that do not.
 OPENSSL_EXPORT void SSL_CREDENTIAL_set_must_match_issuer(SSL_CREDENTIAL *cred,
                                                          int match);
 
@@ -2976,6 +2998,95 @@
                                                       BIO *bio);
 
 
+// Trust Anchor Identifiers.
+//
+// The trust_anchors extension, like certificate_authorities, allows clients to
+// communicate supported CAs to guide server certificate selection, or vice
+// versa. It better supports larger PKIs by referring to CAs by short "trust
+// anchor IDs" and, in the server certificate direction, allowing a client to
+// advertise only a subset of its full list, with DNS hinting and a retry
+// mechanism to manage the subset.
+//
+// See https://datatracker.ietf.org/doc/draft-ietf-tls-trust-anchor-ids/
+//
+// BoringSSL currently only implements this for server certificates, and not yet
+// client certificates.
+
+// SSL_CREDENTIAL_set1_trust_anchor_id sets |cred|'s trust anchor ID to |id|, or
+// clears it if |id_len| is zero. It returns one on success and zero on
+// error. If not clearing, |id| must be in binary format (Section 3 of
+// draft-ietf-tls-trust-anchor-ids-00) of length |id_len|, and describe the
+// issuer of the final certificate in |cred|'s certificate chain.
+//
+// Additionally, |cred| must enable issuer matching (see
+// SSL_CREDENTIAL_set_must_match_issuer|) for this value to take effect.
+//
+// For better extensibility, callers are recommended to configure this
+// information with a CertificatePropertyList instead. See
+// |SSL_CREDENTIAL_set1_certificate_properties|.
+OPENSSL_EXPORT int SSL_CREDENTIAL_set1_trust_anchor_id(SSL_CREDENTIAL *cred,
+                                                       const uint8_t *id,
+                                                       size_t id_len);
+
+// SSL_CTX_set1_requested_trust_anchors configures |ctx| to request a
+// certificate issued by one of the trust anchors in |ids|. It returns one on
+// success and zero on error. |ids| must be a list of trust anchor IDs in
+// wire-format (a series of non-empty, 8-bit length-prefixed strings).
+//
+// The list may describe application's full list of supported trust anchors, or
+// a, possibly empty, subset. Applications can select this subset using
+// out-of-band information, such as the DNS hint in Section 5 of
+// draft-ietf-tls-trust-anchor-ids-00. Client applications sending a subset
+// should use |SSL_get0_peer_available_trust_anchors| to implement the retry
+// flow from Section 4.3 of draft-ietf-tls-trust-anchor-ids-00.
+//
+// If empty (|ids_len| is zero), the trust_anchors extension will still be sent
+// in ClientHello. This may be used by a client application to signal support
+// for the retry flow without requesting specific trust anchors.
+//
+// This function does not directly impact certificate verification, only the
+// list of trust anchors sent to the peer.
+OPENSSL_EXPORT int SSL_CTX_set1_requested_trust_anchors(SSL_CTX *ctx,
+                                                        const uint8_t *ids,
+                                                        size_t ids_len);
+
+// SSL_set1_requested_trust_anchors behaves like
+// |SSL_CTX_set1_requested_trust_anchors| but configures the value on |ssl|.
+OPENSSL_EXPORT int SSL_set1_requested_trust_anchors(SSL *ssl,
+                                                    const uint8_t *ids,
+                                                    size_t ids_len);
+
+// SSL_peer_matched_trust_anchor returns one if the peer reported that its
+// certificate chain matched one of the trust anchor IDs requested by |ssl|, and
+// zero otherwise.
+//
+// This value is only available during the handshake and is expected to be
+// called during certificate verification, e.g. during |SSL_set_custom_verify|
+// or |SSL_CTX_set_cert_verify_callback| callbacks. If the value is one, callers
+// can safely treat the peer's certificate chain as a pre-built path and skip
+// path-building in certificate verification.
+OPENSSL_EXPORT int SSL_peer_matched_trust_anchor(const SSL *ssl);
+
+// SSL_get0_peer_available_trust_anchors gets the peer's available trust anchor
+// IDs. It sets |*out| and |*out_len| so that |*out| points to |*out_len| bytes
+// containing the list in wire format (i.e. a series of non-empty
+// 8-bit-length-prefixed strings). If the peer did not provide a list, the
+// function will output zero bytes. Only servers can provide available trust
+// anchor IDs, so this API will only output a list when |ssl| is a client.
+//
+// This value is only available during the handshake and is expected to be
+// called in the event of certificate verification failure. Client applications
+// can use it to retry the connection, requesting different trust anchors. See
+// Section 4.3 of draft-ietf-tls-trust-anchor-ids-00 for details.
+// |CBS_get_u8_length_prefixed| may be used to iterate over the format.
+//
+// If needed in other contexts, callers may save the value during certificate
+// verification, or at |SSL_CB_HANDSHAKE_DONE| with |SSL_CTX_set_info_callback|.
+OPENSSL_EXPORT void SSL_get0_peer_available_trust_anchors(const SSL *ssl,
+                                                          const uint8_t **out,
+                                                          size_t *out_len);
+
+
 // Server name indication.
 //
 // The server_name extension (RFC 3546) allows the client to advertise the name
@@ -6210,6 +6321,8 @@
 #define SSL_R_PAKE_EXHAUSTED 325
 #define SSL_R_PEER_PAKE_MISMATCH 326
 #define SSL_R_UNSUPPORTED_CREDENTIAL_LIST 327
+#define SSL_R_INVALID_TRUST_ANCHOR_LIST 328
+#define SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST 329
 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 7705e9c..ff408f8 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -132,6 +132,11 @@
 // This is not an IANA defined extension number
 #define TLSEXT_TYPE_channel_id 30032
 
+// This is not an IANA defined extension number
+// TODO(crbug.com/398275713): Replace with the final codepoint once
+// standardization completes.
+#define TLSEXT_TYPE_trust_anchors 0xca34
+
 // status request value from RFC 3546
 #define TLSEXT_STATUSTYPE_nothing (-1)
 #define TLSEXT_STATUSTYPE_ocsp 1
diff --git a/ssl/extensions.cc b/ssl/extensions.cc
index bf609f1..a5370fd 100644
--- a/ssl/extensions.cc
+++ b/ssl/extensions.cc
@@ -159,7 +159,7 @@
       CBS_len(&cipher_suites) < 2 || (CBS_len(&cipher_suites) & 1) != 0 ||
       !CBS_get_u8_length_prefixed(cbs, &compression_methods) ||
       CBS_len(&compression_methods) < 1) {
-      OPENSSL_PUT_ERROR(SSL, SSL_R_CLIENTHELLO_PARSE_FAILED);
+    OPENSSL_PUT_ERROR(SSL, SSL_R_CLIENTHELLO_PARSE_FAILED);
     return false;
   }
 
@@ -2510,7 +2510,7 @@
 static bool ext_certificate_authorities_add_clienthello(
     const SSL_HANDSHAKE *hs, CBB *out, CBB *out_compressible,
     ssl_client_hello_type_t type) {
-  // TODO(crbug.com/399937371) Decide what to do with this for ECH.
+  // TODO(crbug.com/398275713): What should this send in ClientHelloOuter?
   if (ssl_has_CA_names(hs->config)) {
     CBB ca_contents;
     if (!CBB_add_u16(out_compressible,
@@ -2544,6 +2544,124 @@
 }
 
 
+// Trust Anchor Identifiers
+//
+// https://datatracker.ietf.org/doc/draft-ietf-tls-trust-anchor-ids/
+
+bool ssl_is_valid_trust_anchor_list(Span<const uint8_t> in) {
+  CBS ids = in;
+  while (CBS_len(&ids) > 0) {
+    CBS id;
+    if (!CBS_get_u8_length_prefixed(&ids, &id) ||  //
+        CBS_len(&id) == 0) {
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool ext_trust_anchors_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                              CBB *out_compressible,
+                                              ssl_client_hello_type_t type) {
+  if (!hs->config->requested_trust_anchors.has_value()) {
+    return true;
+  }
+  // TODO(crbug.com/398275713): What should this send in ClientHelloOuter?
+  CBB contents, list;
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_trust_anchors) ||  //
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||  //
+      !CBB_add_u16_length_prefixed(&contents, &list) ||             //
+      !CBB_add_bytes(&list, hs->config->requested_trust_anchors->data(),
+                     hs->config->requested_trust_anchors->size()) ||
+      !CBB_flush(out_compressible)) {
+    return false;
+  }
+  return true;
+}
+
+static bool ext_trust_anchors_parse_clienthello(SSL_HANDSHAKE *hs,
+                                                uint8_t *out_alert,
+                                                CBS *contents) {
+  if (contents == nullptr || ssl_protocol_version(hs->ssl) < TLS1_3_VERSION) {
+    return true;
+  }
+
+  CBS child;
+  if (!CBS_get_u16_length_prefixed(contents, &child) ||
+      !ssl_is_valid_trust_anchor_list(child)) {
+    *out_alert = SSL_AD_DECODE_ERROR;
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    return false;
+  }
+
+  hs->peer_requested_trust_anchors.emplace();
+  if (!hs->peer_requested_trust_anchors->CopyFrom(child)) {
+    *out_alert = SSL_AD_INTERNAL_ERROR;
+    return false;
+  }
+  return true;
+}
+
+static bool ext_trust_anchors_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+  SSL *const ssl = hs->ssl;
+  const auto &creds = hs->config->cert->credentials;
+  if (ssl_protocol_version(ssl) < TLS1_3_VERSION ||
+      // Check if any credentials have trust anchor IDs.
+      std::none_of(creds.begin(), creds.end(), [](const auto &cred) {
+        return !cred->trust_anchor_id.empty();
+      })) {
+    return true;
+  }
+  CBB contents, list;
+  if (!CBB_add_u16(out, TLSEXT_TYPE_trust_anchors) ||  //
+      !CBB_add_u16_length_prefixed(out, &contents) ||  //
+      !CBB_add_u16_length_prefixed(&contents, &list)) {
+    return false;
+  }
+  for (const auto &cred : creds) {
+    if (!cred->trust_anchor_id.empty()) {
+      CBB child;
+      if (!CBB_add_u8_length_prefixed(&list, &child) ||  //
+          !CBB_add_bytes(&child, cred->trust_anchor_id.data(),
+                         cred->trust_anchor_id.size()) ||
+          !CBB_flush(&list)) {
+        return false;
+      }
+    }
+  }
+  return CBB_flush(out);
+}
+
+static bool ext_trust_anchors_parse_serverhello(SSL_HANDSHAKE *hs,
+                                                uint8_t *out_alert,
+                                                CBS *contents) {
+  if (contents == nullptr) {
+    return true;
+  }
+
+  if (ssl_protocol_version(hs->ssl) < TLS1_3_VERSION) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION);
+    *out_alert = SSL_AD_UNSUPPORTED_EXTENSION;
+    return false;
+  }
+
+  CBS child;
+  if (!CBS_get_u16_length_prefixed(contents, &child) ||
+      // The list of available trust anchors may not be empty.
+      CBS_len(&child) == 0 ||  //
+      !ssl_is_valid_trust_anchor_list(child)) {
+    *out_alert = SSL_AD_DECODE_ERROR;
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    return false;
+  }
+
+  if (!hs->peer_available_trust_anchors.CopyFrom(child)) {
+    *out_alert = SSL_AD_INTERNAL_ERROR;
+    return false;
+  }
+  return true;
+}
+
 // QUIC Transport Parameters
 
 static bool ext_quic_transport_params_add_clienthello_impl(
@@ -3492,6 +3610,13 @@
         ext_pake_parse_clienthello,
         dont_add_serverhello,
     },
+    {
+        TLSEXT_TYPE_trust_anchors,
+        ext_trust_anchors_add_clienthello,
+        ext_trust_anchors_parse_serverhello,
+        ext_trust_anchors_parse_clienthello,
+        ext_trust_anchors_add_serverhello,
+    },
 };
 
 #define kNumExtensions (sizeof(kExtensions) / sizeof(struct tls_extension))
diff --git a/ssl/handshake.cc b/ssl/handshake.cc
index 58a2406..34a0bdc 100644
--- a/ssl/handshake.cc
+++ b/ssl/handshake.cc
@@ -56,7 +56,9 @@
       apply_jdk11_workaround(false),
       can_release_private_key(false),
       channel_id_negotiated(false),
-      received_hello_verify_request(false) {
+      received_hello_verify_request(false),
+      matched_peer_trust_anchor(false),
+      peer_matched_trust_anchor(false) {
   assert(ssl);
 
   // Draw entropy for all GREASE values at once. This avoids calling
diff --git a/ssl/internal.h b/ssl/internal.h
index 05af44f..d5bb96e 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -27,6 +27,7 @@
 #include <initializer_list>
 #include <limits>
 #include <new>
+#include <optional>
 #include <string_view>
 #include <type_traits>
 #include <utility>
@@ -1942,11 +1943,17 @@
   // |ClaimPAKEAttempt| call.
   void RestorePAKEAttempt() const;
 
+  // trust_anchor_id, if non-empty, is the trust anchor ID for the root of the
+  // chain in |chain|.
+  bssl::Array<uint8_t> trust_anchor_id;
+
   CRYPTO_EX_DATA ex_data;
 
   // must_match_issuer is a flag indicating that this credential should be
   // considered only when it matches a peer request for a particular issuer via
   // a negotiation mechanism (such as the certificate_authorities extension).
+  // This also implies that chain is a certificate path ending in a certificate
+  // issued by the certificate with that trust anchor identifier.
   bool must_match_issuer = false;
 
  private:
@@ -2266,6 +2273,16 @@
   // extension in our peer's CertificateRequest or ClientHello message
   UniquePtr<STACK_OF(CRYPTO_BUFFER)> ca_names;
 
+  // peer_requested_trust_anchors, if not nullopt, contains the trust anchor IDs
+  // (possibly none) the peer requested in ClientHello or CertificateRequest. If
+  // nullopt, the peer did not send the extension.
+  std::optional<Array<uint8_t>> peer_requested_trust_anchors;
+
+  // peer_available_trust_anchors, if not empty, is the list of trust anchor IDs
+  // the peer reported as available in EncryptedExtensions. This is only sent by
+  // servers to clients.
+  Array<uint8_t> peer_available_trust_anchors;
+
   // cached_x509_ca_names contains a cache of parsed versions of the elements of
   // |ca_names|. This pointer is left non-owning so only
   // |ssl_crypto_x509_method| needs to link against crypto/x509.
@@ -2414,6 +2431,14 @@
   // message from the server.
   bool received_hello_verify_request : 1;
 
+  // matched_peer_trust_anchor indicates that we have matched a trust anchor
+  // the peer requested in the trust anchors extension.
+  bool matched_peer_trust_anchor : 1;
+
+  // peer_matched_trust_anchor is true if the peer indicated a match with one of
+  // our requested trust anchors.
+  bool peer_matched_trust_anchor : 1;
+
   // client_version is the value sent or received in the ClientHello version.
   uint16_t client_version = 0;
 
@@ -2626,6 +2651,10 @@
 bool ssl_negotiate_alps(SSL_HANDSHAKE *hs, uint8_t *out_alert,
                         const SSL_CLIENT_HELLO *client_hello);
 
+// ssl_is_valid_trust_anchor_list returns whether |in| is a valid trust anchor
+// identifiers list.
+bool ssl_is_valid_trust_anchor_list(Span<const uint8_t> in);
+
 struct SSLExtension {
   SSLExtension(uint16_t type_arg, bool allowed_arg = true)
       : type(type_arg), allowed(allowed_arg), present(false) {
@@ -3615,6 +3644,9 @@
   // moment we are not crossing those streams.
   UniquePtr<STACK_OF(CRYPTO_BUFFER)> CA_names;
 
+  // Trust anchor IDs to be requested in the trust_anchors extension.
+  std::optional<Array<uint8_t>> requested_trust_anchors;
+
   Array<uint16_t> supported_group_list;  // our list
 
   // channel_id_private is the client's Channel ID private key, or null if
@@ -4148,6 +4180,9 @@
   // What we put in client hello in the CA extension.
   bssl::UniquePtr<STACK_OF(CRYPTO_BUFFER)> CA_names;
 
+  // What we request in the trust_anchors extension.
+  std::optional<bssl::Array<uint8_t>> requested_trust_anchors;
+
   // Default values to use in SSL structures follow (these are copied by
   // SSL_new)
 
diff --git a/ssl/ssl_credential.cc b/ssl/ssl_credential.cc
index 94eaeff..59e2323 100644
--- a/ssl/ssl_credential.cc
+++ b/ssl/ssl_credential.cc
@@ -80,7 +80,22 @@
       }
     }
   }
-  // TODO(bbe): Other forms of issuer matching go here.
+  // If the credential has a trust anchor ID and it matches one sent by the
+  // peer, it is good.
+  if (!cred->trust_anchor_id.empty() && hs->peer_requested_trust_anchors) {
+    CBS cbs = CBS(*hs->peer_requested_trust_anchors), candidate;
+    while (CBS_len(&cbs) > 0) {
+      if (!CBS_get_u8_length_prefixed(&cbs, &candidate) ||
+          CBS_len(&candidate) == 0) {
+        OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+        return false;
+      }
+      if (candidate == Span(cred->trust_anchor_id)) {
+        hs->matched_peer_trust_anchor = true;
+        return true;
+      }
+    }
+  }
 
   OPENSSL_PUT_ERROR(SSL, SSL_R_NO_MATCHING_ISSUER);
   return false;
@@ -599,3 +614,72 @@
 void SSL_CREDENTIAL_set_must_match_issuer(SSL_CREDENTIAL *cred, int match) {
   cred->must_match_issuer = !!match;
 }
+
+int SSL_CREDENTIAL_set1_trust_anchor_id(SSL_CREDENTIAL *cred, const uint8_t *id,
+                                        size_t id_len) {
+  // For now, this is only valid for X.509.
+  if (!cred->UsesX509()) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
+    return 0;
+  }
+
+  if (!cred->trust_anchor_id.CopyFrom(Span(id, id_len))) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+    return 0;
+  }
+
+  return 1;
+}
+
+int SSL_CREDENTIAL_set1_certificate_properties(
+    SSL_CREDENTIAL *cred, CRYPTO_BUFFER *cert_property_list) {
+  std::optional<CBS> trust_anchor;
+  CBS cbs, cpl;
+  CRYPTO_BUFFER_init_CBS(cert_property_list, &cbs);
+
+  if (!CBS_get_u16_length_prefixed(&cbs, &cpl)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST);
+    return 0;
+  }
+  while (CBS_len(&cpl) != 0) {
+    uint16_t cp_type;
+    CBS cp_data;
+    if (!CBS_get_u16(&cpl, &cp_type) ||
+        !CBS_get_u16_length_prefixed(&cpl, &cp_data)) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST);
+      return 0;
+    }
+    switch (cp_type) {
+      case 0:  // trust anchor identifier.
+        if (trust_anchor.has_value()) {
+          OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST);
+          return 0;
+        }
+        trust_anchor = cp_data;
+        break;
+      default:
+        break;
+    }
+  }
+  if (CBS_len(&cbs) != 0) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST);
+    return 0;
+  }
+  // Certificate property list has parsed correctly.
+
+  // We do not currently retain |cert_property_list|, but if we define another
+  // property with larger fields (e.g. stapled SCTs), it may make sense for
+  // those fields to retain |cert_property_list| and alias into it.
+  if (trust_anchor.has_value()) {
+    if (!CBS_len(&trust_anchor.value())) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_TRUST_ANCHOR_LIST);
+      return 0;
+    }
+    if (!SSL_CREDENTIAL_set1_trust_anchor_id(cred,
+                                             CBS_data(&trust_anchor.value()),
+                                             CBS_len(&trust_anchor.value()))) {
+      return 0;
+    }
+  }
+  return 1;
+}
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index c859ea6..c0c6579 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -531,6 +531,14 @@
     return nullptr;
   }
 
+  if (ctx->requested_trust_anchors) {
+    ssl->config->requested_trust_anchors.emplace();
+    if (!ssl->config->requested_trust_anchors->CopyFrom(
+            *ctx->requested_trust_anchors)) {
+      return nullptr;
+    }
+  }
+
   if (ctx->psk_identity_hint) {
     ssl->config->psk_identity_hint.reset(
         OPENSSL_strdup(ctx->psk_identity_hint.get()));
@@ -3288,3 +3296,50 @@
 enum ssl_compliance_policy_t SSL_get_compliance_policy(const SSL *ssl) {
   return ssl->config->compliance_policy;
 }
+
+int SSL_peer_matched_trust_anchor(const SSL *ssl) {
+  return ssl->s3->hs != nullptr && ssl->s3->hs->peer_matched_trust_anchor;
+}
+
+void SSL_get0_peer_available_trust_anchors(const SSL *ssl, const uint8_t **out,
+                                           size_t *out_len) {
+  Span<const uint8_t> ret;
+  if (SSL_HANDSHAKE *hs = ssl->s3->hs.get(); hs != nullptr) {
+    ret = hs->peer_available_trust_anchors;
+  }
+  *out = ret.data();
+  *out_len = ret.size();
+}
+
+int SSL_CTX_set1_requested_trust_anchors(SSL_CTX *ctx, const uint8_t *ids,
+                                         size_t ids_len) {
+  auto span = Span(ids, ids_len);
+  if (!ssl_is_valid_trust_anchor_list(span)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_TRUST_ANCHOR_LIST);
+    return 0;
+  }
+  Array<uint8_t> copy;
+  if (!copy.CopyFrom(span)) {
+    return 0;
+  }
+  ctx->requested_trust_anchors = std::move(copy);
+  return 1;
+}
+
+int SSL_set1_requested_trust_anchors(SSL *ssl, const uint8_t *ids,
+                                     size_t ids_len) {
+  if (!ssl->config) {
+    return 0;
+  }
+  auto span = Span(ids, ids_len);
+  if (!ssl_is_valid_trust_anchor_list(span)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_TRUST_ANCHOR_LIST);
+    return 0;
+  }
+  Array<uint8_t> copy;
+  if (!copy.CopyFrom(span)) {
+    return 0;
+  }
+  ssl->config->requested_trust_anchors = std::move(copy);
+  return 1;
+}
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index b7e0a65..9f9caf1 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -4951,6 +4951,102 @@
                            {leaf.get(), ca.get()}));
 }
 
+TEST(SSLTest, CredentialCertProperties) {
+  // A CertificatePropertyList containing a trust_anchors property, and an
+  // unknown property 0xbb with 0 bytes of data.
+  bssl::UniquePtr<SSL_CREDENTIAL> cred(SSL_CREDENTIAL_new_x509());
+  ASSERT_TRUE(cred);
+  static const uint8_t kTestProperties1[] = {0x00, 0x0b, 0x00, 0x00, 0x00,
+                                             0x03, 0xba, 0xdb, 0x0b, 0x00,
+                                             0xbb, 0x00, 0x00};
+  bssl::UniquePtr<CRYPTO_BUFFER> pl(
+      CRYPTO_BUFFER_new(kTestProperties1, sizeof(kTestProperties1), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_TRUE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+
+  // A CertificatePropertyList containing a trust_anchors property, and an
+  // unknown property 0xbb with 1 byte of data.
+  static const uint8_t kTestProperties2[] = {0x00, 0x0c, 0x00, 0x00, 0x00,
+                                             0x03, 0xba, 0xdb, 0x0b, 0x00,
+                                             0xbb, 0x00, 0x01, 0xba};
+  pl.reset(
+      CRYPTO_BUFFER_new(kTestProperties2, sizeof(kTestProperties2), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_TRUE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+
+  // A CertificatePropertyList containing a trust_anchors property, and an
+  // unknown but malformed property 0xbb with missing data.
+  static const uint8_t kTestProperties3[] = {0x00, 0x09, 0x00, 0x00, 0x00, 0x03,
+                                      0xba, 0xdb, 0x0b, 0x00, 0xbb};
+  pl.reset(
+      CRYPTO_BUFFER_new(kTestProperties3, sizeof(kTestProperties3), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_FALSE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+  EXPECT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL,
+                          SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST));
+
+  // A CertificatePropertyList containing a trust_anchors property, and an
+  // unknown but malformed property 0xbb with incorrect length data.
+  static const uint8_t kTestProperties4[] = {0x00, 0x0c, 0x00, 0x00, 0x00,
+                                             0x03, 0xba, 0xdb, 0x0b, 0x00,
+                                             0xbb, 0x00, 0x03, 0xba};
+  pl.reset(
+      CRYPTO_BUFFER_new(kTestProperties4, sizeof(kTestProperties4), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_FALSE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+  EXPECT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL,
+                          SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST));
+
+  // A CertificatePropertyList containing a trust_anchors property with 0 bytes
+  // of data.
+  static const uint8_t kTestProperties5[] = {0x00, 0x04, 0x00,
+                                             0x00, 0x00, 0x00};
+  pl.reset(
+      CRYPTO_BUFFER_new(kTestProperties5, sizeof(kTestProperties5), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_FALSE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+  EXPECT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL,
+                          SSL_R_INVALID_TRUST_ANCHOR_LIST));
+
+  // A CertificatePropertyList containing a trust_anchors property with extra
+  // data.
+  static const uint8_t kTestProperties6[] = {0x00, 0x08, 0x00, 0x00, 0x00,
+                                             0x03, 0xba, 0xdb, 0x0b, 0xbb};
+  pl.reset(
+      CRYPTO_BUFFER_new(kTestProperties6, sizeof(kTestProperties6), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_FALSE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+  EXPECT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL,
+                          SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST));
+
+  // A CertificatePropertyList containing a trust_anchors property with missing
+  // data.
+  static const uint8_t kTestProperties7[] = {0x00, 0x06, 0x00, 0x00,
+                                             0x00, 0x03, 0xba, 0xdb};
+  pl.reset(
+      CRYPTO_BUFFER_new(kTestProperties7, sizeof(kTestProperties7), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_FALSE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+  EXPECT_TRUE(ErrorEquals(ERR_get_error(), ERR_LIB_SSL,
+                          SSL_R_INVALID_CERTIFICATE_PROPERTY_LIST));
+
+  // A CertificatePropertyList containing only a trust_anchors property.
+  static const uint8_t kTestProperties8[] = {0x00, 0x07, 0x00, 0x00, 0x00,
+                                             0x03, 0xba, 0xdb, 0x0b};
+  pl.reset(
+      CRYPTO_BUFFER_new(kTestProperties8, sizeof(kTestProperties8), nullptr));
+  ASSERT_TRUE(pl);
+  EXPECT_TRUE(
+      SSL_CREDENTIAL_set1_certificate_properties(cred.get(), pl.get()));
+}
+
 TEST(SSLTest, SetChainAndKeyCtx) {
   bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_with_buffers_method()));
   ASSERT_TRUE(client_ctx);
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 1f24361..535137e 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -180,6 +180,7 @@
 	extensionQUICTransportParamsLegacy  uint16 = 0xffa5 // draft-ietf-quic-tls-32 and earlier
 	extensionChannelID                  uint16 = 30032  // not IANA assigned
 	extensionPAKE                       uint16 = 35387  // not IANA assigned
+	extensionTrustAnchors               uint16 = 0xca34 // not IANA assigned
 	extensionDuplicate                  uint16 = 0xffff // not IANA assigned
 	extensionEncryptedClientHello       uint16 = 0xfe0d // not IANA assigned
 	extensionECHOuterExtensions         uint16 = 0xfd00 // not IANA assigned
@@ -683,6 +684,14 @@
 	// field.
 	DTLSRecordHeaderOmitLength bool
 
+	// RequestTrustAnchors, if not nil, is the list of trust anchor IDs to
+	// request in ClientHello.
+	RequestTrustAnchors [][]byte
+
+	// AvailableTrustAnchors, if not empty, is the list of trust anchor IDs
+	// to report as available in EncryptedExtensions.
+	AvailableTrustAnchors [][]byte
+
 	// Bugs specifies optional misbehaviour to be used for testing other
 	// implementations.
 	Bugs ProtocolBugs
@@ -1907,6 +1916,37 @@
 	// request signed certificate timestamps.
 	NoSignedCertificateTimestamps bool
 
+	// ExpectPeerRequestedTrustAnchors, if not nil, causes the server to
+	// require the client to request the specified trust anchors in the
+	// ClientHello.
+	ExpectPeerRequestedTrustAnchors [][]byte
+
+	// ExpectPeerAvailableTrustAnchors, if not nil, causes the client to
+	// require the server to list the specified trust anchors as available
+	// in EncryptedExtensions.
+	ExpectPeerAvailableTrustAnchors [][]byte
+
+	// ExpectPeerMatchTrustAnchor, if not nil, causes the client to require the
+	// server to acknowledge, or not acknowledge the trust_anchors extension in
+	// Certificate.
+	ExpectPeerMatchTrustAnchor *bool
+
+	// AlwaysMatchTrustAnchorID, if true, causes the server to always indicate
+	// a trust anchor ID match in the Certificate message.
+	AlwaysMatchTrustAnchorID bool
+
+	// SendTrustAnchorWrongCertificate sends a trust anchor ID extension
+	// on the second certificate in the Certificate message.
+	SendTrustAnchorWrongCertificate bool
+
+	// SendNonEmptyTrustAnchorMatch sends a non-empty trust anchor ID
+	// extension to indicate a match.
+	SendNonEmptyTrustAnchorMatch bool
+
+	// AlwaysSendAvailableTrustAnchors, if true, causese the server to always
+	// send available trust anchors in EncryptedExtensions, even if unsolicited.
+	AlwaysSendAvailableTrustAnchors bool
+
 	// SendSupportedPointFormats, if not nil, is the list of supported point
 	// formats to send in ClientHello or ServerHello. If set to a non-nil
 	// empty slice, no extension will be sent.
@@ -2335,6 +2375,9 @@
 	// OverridePAKECodepoint, if non-zero, causes the runner to send the
 	// specified value instead of the actual PAKE codepoint.
 	OverridePAKECodepoint uint16
+	// TrustAnchorID, if not empty, is the trust anchor ID for the issuer
+	// of the certificate chain.
+	TrustAnchorID []byte
 }
 
 func (c *Credential) WithSignatureAlgorithms(sigAlgs ...signatureAlgorithm) *Credential {
@@ -2368,6 +2411,13 @@
 	return supportedSignatureAlgorithms
 }
 
+func (c *Credential) WithTrustAnchorID(id []byte) *Credential {
+	ret := *c
+	ret.TrustAnchorID = id
+	ret.MustMatchIssuer = true
+	return &ret
+}
+
 type handshakeMessage interface {
 	marshal() []byte
 	unmarshal([]byte) bool
@@ -2647,3 +2697,6 @@
 
 	return cert
 }
+
+// https://github.com/golang/go/issues/45624
+func ptrTo[T any](t T) *T { return &t }
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 5fd5b7b..ed94cab 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -513,6 +513,7 @@
 		omitExtensions:            c.config.Bugs.OmitExtensions,
 		emptyExtensions:           c.config.Bugs.EmptyExtensions,
 		delegatedCredential:       c.config.DelegatedCredentialAlgorithms,
+		trustAnchors:              c.config.RequestTrustAnchors,
 	}
 
 	// Translate the bugs that modify ClientHello extension order into a
@@ -1334,6 +1335,15 @@
 				return errors.New("tls: unexpected extensions in the server certificate")
 			}
 		}
+		if c.config.RequestTrustAnchors == nil && certMsg.matchedTrustAnchor {
+			return errors.New("tls: unsolicited trust_anchors extension in the server certificate")
+		}
+		if expected := c.config.Bugs.ExpectPeerMatchTrustAnchor; expected != nil && certMsg.matchedTrustAnchor != *expected {
+			if certMsg.matchedTrustAnchor {
+				return errors.New("tls: server certificate unexpectedly matched trust anchor")
+			}
+			return errors.New("tls: server certificate unexpectedly did not match trust anchor")
+		}
 
 		if err := hs.verifyCertificates(certMsg); err != nil {
 			return err
@@ -2038,6 +2048,13 @@
 		return errors.New("tls: server advertised unrequested SCTs")
 	}
 
+	if len(serverExtensions.trustAnchors) > 0 && c.config.RequestTrustAnchors == nil {
+		return errors.New("tls: server advertised unrequested trust anchor IDs")
+	}
+	if expected := c.config.Bugs.ExpectPeerAvailableTrustAnchors; expected != nil && !slices.EqualFunc(expected, serverExtensions.trustAnchors, slices.Equal) {
+		return errors.New("tls: server advertised trust anchor IDs that did not match expectations")
+	}
+
 	if serverExtensions.srtpProtectionProfile != 0 {
 		if serverExtensions.srtpMasterKeyIdentifier != "" {
 			return errors.New("tls: server selected SRTP MKI value")
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 32a2732..0d9ded1 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -206,6 +206,7 @@
 	pakeServerID                             []byte
 	pakeShares                               []pakeShare
 	certificateAuthorities                   [][]byte
+	trustAnchors                             [][]byte
 	outerExtensions                          []uint16
 	reorderOuterExtensionsWithoutCompressing bool
 	prefixExtensions                         []uint16
@@ -573,6 +574,19 @@
 			body: body.BytesOrPanic(),
 		})
 	}
+	// Check against nil to distinguish missing and empty.
+	if m.trustAnchors != nil {
+		body := cryptobyte.NewBuilder(nil)
+		body.AddUint16LengthPrefixed(func(trustAnchorList *cryptobyte.Builder) {
+			for _, id := range m.trustAnchors {
+				addUint8LengthPrefixedBytes(trustAnchorList, id)
+			}
+		})
+		extensions = append(extensions, extension{
+			id:   extensionTrustAnchors,
+			body: body.BytesOrPanic(),
+		})
+	}
 	// The PSK extension must be last. See https://tools.ietf.org/html/rfc8446#section-4.2.11
 	if len(m.pskIdentities) > 0 {
 		pskExtension := cryptobyte.NewBuilder(nil)
@@ -1120,6 +1134,11 @@
 				len(m.certificateAuthorities) == 0 {
 				return false
 			}
+		case extensionTrustAnchors:
+			// An empty list is allowed here.
+			if !parseTrustAnchors(&body, &m.trustAnchors) {
+				return false
+			}
 		}
 
 		if isGREASEValue(extension) {
@@ -1530,6 +1549,7 @@
 	applicationSettingsOld    []byte
 	hasApplicationSettingsOld bool
 	echRetryConfigs           []byte
+	trustAnchors              [][]byte
 }
 
 func (m *serverExtensions) marshal(extensions *cryptobyte.Builder) {
@@ -1664,6 +1684,16 @@
 		extensions.AddUint16(extensionEncryptedClientHello)
 		addUint16LengthPrefixedBytes(extensions, m.echRetryConfigs)
 	}
+	if len(m.trustAnchors) > 0 {
+		extensions.AddUint16(extensionTrustAnchors)
+		extensions.AddUint16LengthPrefixed(func(extension *cryptobyte.Builder) {
+			extension.AddUint16LengthPrefixed(func(trustAnchorList *cryptobyte.Builder) {
+				for _, id := range m.trustAnchors {
+					addUint8LengthPrefixedBytes(trustAnchorList, id)
+				}
+			})
+		})
+	}
 }
 
 func (m *serverExtensions) unmarshal(data cryptobyte.String, version uint16) bool {
@@ -1795,6 +1825,13 @@
 			if len(body) > 0 {
 				return false
 			}
+		case extensionTrustAnchors:
+			if version < VersionTLS13 {
+				return false
+			}
+			if !parseTrustAnchors(&body, &m.trustAnchors) || len(body) != 0 {
+				return false
+			}
 		default:
 			// Unknown extensions are illegal from the server.
 			return false
@@ -2042,10 +2079,13 @@
 }
 
 type certificateMsg struct {
-	raw               []byte
-	hasRequestContext bool
-	requestContext    []byte
-	certificates      []certificateEntry
+	raw                             []byte
+	hasRequestContext               bool
+	requestContext                  []byte
+	certificates                    []certificateEntry
+	matchedTrustAnchor              bool
+	sendTrustAnchorWrongCertificate bool
+	sendNonEmptyTrustAnchorMatch    bool
 }
 
 func (m *certificateMsg) marshal() (x []byte) {
@@ -2060,10 +2100,18 @@
 			addUint8LengthPrefixedBytes(certificate, m.requestContext)
 		}
 		certificate.AddUint24LengthPrefixed(func(certificateList *cryptobyte.Builder) {
-			for _, cert := range m.certificates {
+			for i, cert := range m.certificates {
 				addUint24LengthPrefixedBytes(certificateList, cert.data)
 				if m.hasRequestContext {
 					certificateList.AddUint16LengthPrefixed(func(extensions *cryptobyte.Builder) {
+						if (i == 0 && m.matchedTrustAnchor) || (i == 1 && m.sendTrustAnchorWrongCertificate) {
+							extensions.AddUint16(extensionTrustAnchors)
+							if m.sendNonEmptyTrustAnchorMatch {
+								addUint16LengthPrefixedBytes(extensions, []byte{0x03, 0xba, 0xdb, 0x0b})
+							} else {
+								extensions.AddUint16(0) // Empty extension
+							}
+						}
 						count := 1
 						if cert.duplicateExtensions {
 							count = 2
@@ -2161,6 +2209,14 @@
 					dc.raw = origBody
 					dc.signedBytes = []byte(origBody)[:4+2+3+len(dc.pkixPublicKey)]
 					cert.delegatedCredential = dc
+				case extensionTrustAnchors:
+					if len(m.certificates) != 0 {
+						return false // Only allowed in first certificate.
+					}
+					if len(body) != 0 {
+						return false
+					}
+					m.matchedTrustAnchor = true
 				default:
 					return false
 				}
@@ -2455,7 +2511,6 @@
 						})
 					})
 				}
-
 				if m.customExtension > 0 {
 					extensions.AddUint16(m.customExtension)
 					extensions.AddUint16(0) // Empty extension
@@ -2499,6 +2554,23 @@
 	return true
 }
 
+func parseTrustAnchors(reader *cryptobyte.String, out *[][]byte) bool {
+	var ids cryptobyte.String
+	if !reader.ReadUint16LengthPrefixed(&ids) {
+		return false
+	}
+	// Distinguish nil and empty.
+	*out = [][]byte{}
+	for len(ids) > 0 {
+		var id []byte
+		if !readUint8LengthPrefixedBytes(&ids, &id) {
+			return false
+		}
+		*out = append(*out, id)
+	}
+	return true
+}
+
 func (m *certificateRequestMsg) unmarshal(data []byte) bool {
 	m.raw = data
 	reader := cryptobyte.String(data[4:])
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 8dc6348..d9a710b 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -452,6 +452,15 @@
 		return errors.New("tls: expected non-empty session ID from client")
 	}
 
+	if expected := c.config.Bugs.ExpectPeerRequestedTrustAnchors; expected != nil {
+		if hs.clientHello.trustAnchors == nil {
+			return errors.New("tls: client did not send trust anchors")
+		}
+		if !slices.EqualFunc(expected, hs.clientHello.trustAnchors, slices.Equal) {
+			return fmt.Errorf("tls: client offered trust anchors %v, but expected %v", hs.clientHello.trustAnchors, expected)
+		}
+	}
+
 	applyBugsToClientHello(hs.clientHello, config)
 
 	return nil
@@ -1160,6 +1169,19 @@
 		certMsg := &certificateMsg{
 			hasRequestContext: true,
 		}
+		certMsg.sendTrustAnchorWrongCertificate = config.Bugs.SendTrustAnchorWrongCertificate
+		certMsg.sendNonEmptyTrustAnchorMatch = config.Bugs.SendNonEmptyTrustAnchorMatch
+		if config.Bugs.AlwaysMatchTrustAnchorID {
+			certMsg.matchedTrustAnchor = true
+		} else {
+			if hs.clientHello.trustAnchors != nil && useCert.TrustAnchorID != nil {
+				for _, id := range hs.clientHello.trustAnchors {
+					if bytes.Equal(useCert.TrustAnchorID, id) {
+						certMsg.matchedTrustAnchor = true
+					}
+				}
+			}
+		}
 		if !config.Bugs.EmptyCertificateList {
 			for i, certData := range useCert.Certificate {
 				cert := certificateEntry{
@@ -1751,6 +1773,9 @@
 		return errors.New("tls: no GREASE extension found")
 	}
 
+	if hs.clientHello.trustAnchors != nil || config.Bugs.AlwaysSendAvailableTrustAnchors {
+		serverExtensions.trustAnchors = c.config.AvailableTrustAnchors
+	}
 	serverExtensions.serverNameAck = c.config.Bugs.SendServerNameAck
 
 	if (c.vers >= VersionTLS13 && hs.clientHello.echOuter != nil) || c.config.Bugs.AlwaysSendECHRetryConfigs {
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 238e958..47fac16 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -1510,6 +1510,7 @@
 	if cred.WrongPAKERole {
 		flags = append(flags, prefix+"-wrong-pake-role")
 	}
+	handleBase64Field("trust-anchor-id", cred.TrustAnchorID)
 	return flags
 }
 
@@ -18401,6 +18402,266 @@
 	}
 }
 
+func trustAnchorListFlagValue(ids ...[]byte) string {
+	b := cryptobyte.NewBuilder(nil)
+	for _, id := range ids {
+		addUint8LengthPrefixedBytes(b, id)
+	}
+	return base64FlagValue(b.BytesOrPanic())
+}
+
+func addTrustAnchorTests() {
+	id1 := []byte{1}
+	id2 := []byte{2, 2}
+	id3 := []byte{3, 3, 3}
+
+	// Unsolicited trust_anchors extensions should be rejected.
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-Unsolicited-Certificate",
+		config: Config{
+			MinVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				AlwaysMatchTrustAnchorID: true,
+			},
+		},
+		shouldFail:         true,
+		expectedLocalError: "remote error: unsupported extension",
+		expectedError:      ":UNEXPECTED_EXTENSION:",
+	})
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-Unsolicited-EncryptedExtensions",
+		config: Config{
+			MinVersion:            VersionTLS13,
+			AvailableTrustAnchors: [][]byte{id1, id2},
+			Bugs: ProtocolBugs{
+				AlwaysSendAvailableTrustAnchors: true,
+			},
+		},
+		shouldFail:         true,
+		expectedLocalError: "remote error: unsupported extension",
+		expectedError:      ":UNEXPECTED_EXTENSION:",
+	})
+
+	// Test that the client sends trust anchors when configured, and correctly
+	// reports the server's response.
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-ClientRequest-Match",
+		config: Config{
+			MinVersion:            VersionTLS13,
+			AvailableTrustAnchors: [][]byte{id1, id2},
+			Credential:            rsaChainCertificate.WithTrustAnchorID(id1),
+			Bugs: ProtocolBugs{
+				ExpectPeerRequestedTrustAnchors: [][]byte{id1, id3},
+			},
+		},
+		flags: []string{
+			"-requested-trust-anchors", trustAnchorListFlagValue(id1, id3),
+			"-expect-peer-match-trust-anchor",
+			"-expect-peer-available-trust-anchors", trustAnchorListFlagValue(id1, id2),
+		},
+	})
+	// The client should not like it if the server indicates the match with a non-empty
+	// extension.
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-ClientRequest-Match-Non-Empty-Extension",
+		config: Config{
+			MinVersion:            VersionTLS13,
+			AvailableTrustAnchors: [][]byte{id1, id2},
+			Credential:            rsaChainCertificate.WithTrustAnchorID(id1),
+			Bugs: ProtocolBugs{
+				SendNonEmptyTrustAnchorMatch:    true,
+				ExpectPeerRequestedTrustAnchors: [][]byte{id1, id3},
+			},
+		},
+		flags: []string{
+			"-requested-trust-anchors", trustAnchorListFlagValue(id1, id3),
+		},
+		shouldFail:         true,
+		expectedLocalError: "remote error: error decoding message",
+		expectedError:      ":ERROR_PARSING_EXTENSION:",
+	})
+	// The client should not like it if the server indicates the match on the incorrect
+	// certificate in the Certificate message.
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-ClientRequest-Match-On-Incorrect-Certificate",
+		config: Config{
+			MinVersion:            VersionTLS13,
+			AvailableTrustAnchors: [][]byte{id1, id2},
+			Credential:            rsaChainCertificate.WithTrustAnchorID(id1),
+			Bugs: ProtocolBugs{
+				SendTrustAnchorWrongCertificate: true,
+				ExpectPeerRequestedTrustAnchors: [][]byte{id1, id3},
+			},
+		},
+		flags: []string{
+			"-requested-trust-anchors", trustAnchorListFlagValue(id1, id3),
+		},
+		shouldFail:         true,
+		expectedLocalError: "remote error: unsupported extension",
+		expectedError:      ":UNEXPECTED_EXTENSION:",
+	})
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-ClientRequest-NoMatch",
+		config: Config{
+			MinVersion:            VersionTLS13,
+			AvailableTrustAnchors: [][]byte{id1, id2},
+			Bugs: ProtocolBugs{
+				ExpectPeerRequestedTrustAnchors: [][]byte{id3},
+			},
+		},
+		flags: []string{
+			"-requested-trust-anchors", trustAnchorListFlagValue(id3),
+			"-expect-no-peer-match-trust-anchor",
+			"-expect-peer-available-trust-anchors", trustAnchorListFlagValue(id1, id2),
+		},
+	})
+
+	// An empty trust anchor ID is a syntax error, so most be rejected in both
+	// ClientHello and EncryptedExtensions.
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TrustAnchors-EmptyID-ClientHello",
+		config: Config{
+			MinVersion:          VersionTLS13,
+			RequestTrustAnchors: [][]byte{{}},
+		},
+		shouldFail:    true,
+		expectedError: ":DECODE_ERROR:",
+	})
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-EmptyID-EncryptedExtensions",
+		config: Config{
+			MinVersion:            VersionTLS13,
+			AvailableTrustAnchors: [][]byte{{}},
+		},
+		flags:         []string{"-requested-trust-anchors", trustAnchorListFlagValue(id1)},
+		shouldFail:    true,
+		expectedError: ":DECODE_ERROR:",
+	})
+
+	// Test the server selection logic, as well as whether it correctly reports
+	// available trust anchors and the match status. (The general selection flow
+	// is covered in addCertificateSelectionTests.)
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TrustAnchors-ServerSelect-Match",
+		config: Config{
+			MinVersion:          VersionTLS13,
+			RequestTrustAnchors: [][]byte{id2},
+			Bugs: ProtocolBugs{
+				ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2},
+				ExpectPeerMatchTrustAnchor:      ptrTo(true),
+			},
+		},
+		shimCredentials: []*Credential{
+			rsaCertificate.WithTrustAnchorID(id1),
+			rsaCertificate.WithTrustAnchorID(id2),
+		},
+		flags: []string{"-expect-selected-credential", "1"},
+	})
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TrustAnchors-ServerSelect-None",
+		config: Config{
+			MinVersion:          VersionTLS13,
+			RequestTrustAnchors: [][]byte{id1},
+		},
+		shimCredentials: []*Credential{
+			rsaCertificate.WithTrustAnchorID(id2),
+			rsaCertificate.WithTrustAnchorID(id3),
+		},
+		shouldFail:    true,
+		expectedError: ":NO_MATCHING_ISSUER:",
+	})
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TrustAnchors-ServerSelect-Fallback",
+		config: Config{
+			MinVersion:          VersionTLS13,
+			RequestTrustAnchors: [][]byte{id1},
+			Bugs: ProtocolBugs{
+				ExpectPeerAvailableTrustAnchors: [][]byte{id2, id3},
+				ExpectPeerMatchTrustAnchor:      ptrTo(false),
+			},
+		},
+		shimCredentials: []*Credential{
+			rsaCertificate.WithTrustAnchorID(id2),
+			rsaCertificate.WithTrustAnchorID(id3),
+			&rsaCertificate,
+		},
+		flags: []string{"-expect-selected-credential", "2"},
+	})
+
+	// The ClientHello list may be empty. The client must be able to send it and
+	// receive available trust anchors.
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-ClientRequestEmpty",
+		config: Config{
+			MinVersion:            VersionTLS13,
+			AvailableTrustAnchors: [][]byte{id1, id2},
+			Bugs: ProtocolBugs{
+				ExpectPeerRequestedTrustAnchors: [][]byte{},
+			},
+		},
+		flags: []string{
+			"-requested-trust-anchors", trustAnchorListFlagValue(),
+			"-expect-peer-available-trust-anchors", trustAnchorListFlagValue(id1, id2),
+		},
+	})
+	// The server must be able to process it, and send available trust anchors.
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TrustAnchors-ServerReceiveEmptyRequest",
+		config: Config{
+			MinVersion:          VersionTLS13,
+			RequestTrustAnchors: [][]byte{},
+			Bugs: ProtocolBugs{
+				ExpectPeerAvailableTrustAnchors: [][]byte{id1, id2},
+				ExpectPeerMatchTrustAnchor:      ptrTo(false),
+			},
+		},
+		shimCredentials: []*Credential{
+			rsaCertificate.WithTrustAnchorID(id1),
+			rsaCertificate.WithTrustAnchorID(id2),
+			&rsaCertificate,
+		},
+		flags: []string{"-expect-selected-credential", "2"},
+	})
+
+	// This extension requires TLS 1.3. If a server receives this and negotiates
+	// TLS 1.2, it should ignore the extension and not accidentally send
+	// something in ServerHello (implicitly checked by runner).
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TrustAnchors-TLS12-Server",
+		config: Config{
+			MaxVersion:          VersionTLS12,
+			RequestTrustAnchors: [][]byte{id1},
+		},
+		shimCredentials: []*Credential{
+			rsaCertificate.WithTrustAnchorID(id1),
+			&rsaCertificate,
+		},
+		// The first credential is skipped because the extension is ignored.
+		flags: []string{"-expect-selected-credential", "1"},
+	})
+	// The client should reject the extension in TLS 1.2 ServerHello.
+	testCases = append(testCases, testCase{
+		name: "TrustAnchors-TLS12-Client",
+		config: Config{
+			MaxVersion:            VersionTLS12,
+			AvailableTrustAnchors: [][]byte{id1},
+			Bugs: ProtocolBugs{
+				AlwaysSendAvailableTrustAnchors: true,
+			},
+		},
+		flags:              []string{"-requested-trust-anchors", trustAnchorListFlagValue(id1)},
+		shouldFail:         true,
+		expectedError:      ":UNEXPECTED_EXTENSION:",
+		expectedLocalError: "remote error: unsupported extension",
+	})
+}
+
 func addDelegatedCredentialTests() {
 	p256DC := createDelegatedCredential(&rsaCertificate, delegatedCredentialConfig{
 		dcAlgo: signatureECDSAWithP256AndSHA256,
@@ -21745,7 +22006,7 @@
 
 func canBeShimCertificate(c *Credential) bool {
 	// Some options can only be set with the credentials API.
-	return c.Type == CredentialTypeX509 && !c.MustMatchIssuer
+	return c.Type == CredentialTypeX509 && !c.MustMatchIssuer && c.TrustAnchorID == nil
 }
 
 func addCertificateSelectionTests() {
@@ -22192,6 +22453,56 @@
 			mismatch:      ecdsaP256Certificate.WithMustMatchIssuer(true),
 			expectedError: ":NO_MATCHING_ISSUER:",
 		},
+
+		// Trust anchor IDs can also be used to match issuers.
+		// TODO(crbug.com/398275713): Implement this for client certificates.
+		{
+			name:       "Server-CheckIssuer-TrustAnchorIDs",
+			testType:   serverTest,
+			minVersion: VersionTLS13,
+			config: Config{
+				RequestTrustAnchors: [][]byte{{1, 1, 1}},
+			},
+			match:         rsaChainCertificate.WithTrustAnchorID([]byte{1, 1, 1}),
+			mismatch:      ecdsaP256Certificate.WithTrustAnchorID([]byte{2, 2, 2}),
+			expectedError: ":NO_MATCHING_ISSUER:",
+		},
+
+		// When an issuer-gated credential fails, a normal credential may be
+		// selected instead.
+		{
+			name:     "Client-CheckIssuerFallback",
+			testType: clientTest,
+			config: Config{
+				ClientAuth: RequestClientCert,
+				ClientCAs:  makeCertPoolFromRoots(&ecdsaP384Certificate),
+			},
+			match:         &rsaChainCertificate,
+			mismatch:      ecdsaP256Certificate.WithMustMatchIssuer(true),
+			expectedError: ":NO_MATCHING_ISSUER:",
+		},
+		{
+			name:     "Server-CheckIssuerFallback",
+			testType: serverTest,
+			config: Config{
+				RootCAs:     makeCertPoolFromRoots(&ecdsaP384Certificate),
+				SendRootCAs: true,
+			},
+			match:         &rsaChainCertificate,
+			mismatch:      ecdsaP256Certificate.WithMustMatchIssuer(true),
+			expectedError: ":NO_MATCHING_ISSUER:",
+		},
+		{
+			name:       "Server-CheckIssuerFallback-TrustAnchorIDs",
+			testType:   serverTest,
+			minVersion: VersionTLS13,
+			config: Config{
+				RequestTrustAnchors: [][]byte{{1, 1, 1}},
+			},
+			match:         &rsaChainCertificate,
+			mismatch:      ecdsaP256Certificate.WithTrustAnchorID([]byte{2, 2, 2}),
+			expectedError: ":NO_MATCHING_ISSUER:",
+		},
 	}
 
 	for _, protocol := range []protocol{tls, dtls} {
@@ -23690,6 +24001,7 @@
 	addCertificateSelectionTests()
 	addKeyUpdateTests()
 	addPAKETests()
+	addTrustAnchorTests()
 
 	toAppend, err := convertToSplitHandshakeTests(testCases)
 	if err != nil {
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 6d3613a..439286c 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -74,6 +74,28 @@
                       }};
 }
 
+template <typename Config>
+Flag<Config> OptionalBoolTrueFlag(const char *name,
+                                  std::optional<bool> Config::*field,
+                                  bool skip_handshaker = false) {
+  return Flag<Config>{name, false, skip_handshaker,
+                      [=](Config *config, const char *) -> bool {
+                        config->*field = true;
+                        return true;
+                      }};
+}
+
+template <typename Config>
+Flag<Config> OptionalBoolFalseFlag(const char *name,
+                                   std::optional<bool> Config::*field,
+                                   bool skip_handshaker = false) {
+  return Flag<Config>{name, false, skip_handshaker,
+                      [=](Config *config, const char *) -> bool {
+                        config->*field = false;
+                        return true;
+                      }};
+}
+
 template <typename T>
 bool StringToInt(T *out, const char *str) {
   static_assert(std::is_integral<T>::value, "not an integral type");
@@ -196,6 +218,17 @@
 }
 
 template <typename Config>
+Flag<Config> OptionalBase64Flag(
+    const char *name, std::optional<std::vector<uint8_t>> Config::*field,
+    bool skip_handshaker = false) {
+  return Flag<Config>{name, true, skip_handshaker,
+                      [=](Config *config, const char *param) -> bool {
+                        (config->*field).emplace();
+                        return DecodeBase64(&*(config->*field), param);
+                      }};
+}
+
+template <typename Config>
 Flag<Config> Base64VectorFlag(const char *name,
                               std::vector<std::vector<uint8_t>> Config::*field,
                               bool skip_handshaker = false) {
@@ -504,6 +537,14 @@
         BoolFlag("-fips-202205", &TestConfig::fips_202205),
         BoolFlag("-wpa-202304", &TestConfig::wpa_202304),
         BoolFlag("-cnsa-202407", &TestConfig::cnsa_202407),
+        OptionalBoolTrueFlag("-expect-peer-match-trust-anchor",
+                             &TestConfig::expect_peer_match_trust_anchor),
+        OptionalBoolFalseFlag("-expect-no-peer-match-trust-anchor",
+                              &TestConfig::expect_peer_match_trust_anchor),
+        OptionalBase64Flag("-expect-peer-available-trust-anchors",
+                           &TestConfig::expect_peer_available_trust_anchors),
+        OptionalBase64Flag("-requested-trust-anchors",
+                           &TestConfig::requested_trust_anchors),
         OptionalIntFlag("-expect-selected-credential",
                         &TestConfig::expect_selected_credential),
         // Credential flags are stateful. First, use one of the
@@ -546,6 +587,8 @@
             Base64Flag("-pake-password", &CredentialConfig::pake_password)),
         CredentialFlag(
             BoolFlag("-wrong-pake-role", &CredentialConfig::wrong_pake_role)),
+        CredentialFlag(
+            Base64Flag("-trust-anchor-id", &CredentialConfig::trust_anchor_id)),
         IntFlag("-private-key-delay-ms", &TestConfig::private_key_delay_ms),
     };
     std::sort(ret.begin(), ret.end(), FlagNameComparator{});
@@ -1044,6 +1087,26 @@
     return false;
   }
 
+  if (config->expect_peer_match_trust_anchor.has_value() &&
+      !!SSL_peer_matched_trust_anchor(ssl) !=
+          config->expect_peer_match_trust_anchor.value()) {
+    fprintf(stderr, "Peer unexpected %s a requested trust anchor",
+            SSL_peer_matched_trust_anchor(ssl) ? "matched" : "failed to match");
+    return false;
+  }
+
+  if (config->expect_peer_available_trust_anchors.has_value()) {
+    const uint8_t *peer_ids;
+    size_t peer_ids_len;
+    SSL_get0_peer_available_trust_anchors(ssl, &peer_ids, &peer_ids_len);
+    if (bssl::Span(peer_ids, peer_ids_len) !=
+        *config->expect_peer_available_trust_anchors) {
+      fprintf(stderr,
+              "Peer's available trust anchors did not match expectations.");
+      return false;
+    }
+  }
+
   if (GetTestState(ssl)->cert_verified) {
     fprintf(stderr, "Certificate verified twice.\n");
     return false;
@@ -1486,6 +1549,14 @@
     SSL_CREDENTIAL_set_must_match_issuer(cred.get(), 1);
   }
 
+  if (!cred_config.trust_anchor_id.empty()) {
+    if (!SSL_CREDENTIAL_set1_trust_anchor_id(
+            cred.get(), cred_config.trust_anchor_id.data(),
+            cred_config.trust_anchor_id.size())) {
+      return nullptr;
+    }
+  }
+
   if (!SetCredentialInfo(cred.get(), std::move(info))) {
     return nullptr;
   }
@@ -2341,6 +2412,12 @@
       !SSL_set_srtp_profiles(ssl.get(), srtp_profiles.c_str())) {
     return nullptr;
   }
+  if (requested_trust_anchors.has_value() &&
+      !SSL_set1_requested_trust_anchors(ssl.get(),
+                                        requested_trust_anchors->data(),
+                                        requested_trust_anchors->size())) {
+    return nullptr;
+  }
   if (enable_ocsp_stapling) {
     SSL_enable_ocsp_stapling(ssl.get());
   }
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index d573d21..08d0f3a 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -44,6 +44,7 @@
   std::vector<uint8_t> pake_client_id;
   std::vector<uint8_t> pake_server_id;
   std::vector<uint8_t> pake_password;
+  std::vector<uint8_t> trust_anchor_id;
   bool wrong_pake_role = false;
 };
 
@@ -228,6 +229,9 @@
   bool fips_202205 = false;
   bool wpa_202304 = false;
   bool cnsa_202407 = false;
+  std::optional<bool> expect_peer_match_trust_anchor;
+  std::optional<std::vector<uint8_t>> expect_peer_available_trust_anchors;
+  std::optional<std::vector<uint8_t>> requested_trust_anchors;
   std::optional<int> expect_selected_credential;
   std::vector<CredentialConfig> credentials;
   int private_key_delay_ms = 0;
diff --git a/ssl/tls13_both.cc b/ssl/tls13_both.cc
index 8202041..8a4c7cf 100644
--- a/ssl/tls13_both.cc
+++ b/ssl/tls13_both.cc
@@ -197,7 +197,8 @@
       return false;
     }
 
-    if (sk_CRYPTO_BUFFER_num(certs.get()) == 0) {
+    const bool is_leaf = sk_CRYPTO_BUFFER_num(certs.get()) == 0;
+    if (is_leaf) {
       pkey = ssl_cert_parse_pubkey(&certificate);
       if (!pkey) {
         ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
@@ -234,8 +235,13 @@
     SSLExtension sct(
         TLSEXT_TYPE_certificate_timestamp,
         !ssl->server && hs->config->signed_cert_timestamps_enabled);
+    SSLExtension trust_anchors(
+        TLSEXT_TYPE_trust_anchors,
+        !ssl->server && is_leaf &&
+            hs->config->requested_trust_anchors.has_value());
     uint8_t alert = SSL_AD_DECODE_ERROR;
-    if (!ssl_parse_extensions(&extensions, &alert, {&status_request, &sct},
+    if (!ssl_parse_extensions(&extensions, &alert,
+                              {&status_request, &sct, &trust_anchors},
                               /*ignore_unknown=*/false)) {
       ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
       return false;
@@ -280,6 +286,15 @@
         }
       }
     }
+
+    if (trust_anchors.present) {
+      if (CBS_len(&trust_anchors.data) != 0) {
+        OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_PARSING_EXTENSION);
+        ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+        return false;
+      }
+      hs->peer_matched_trust_anchor = true;
+    }
   }
 
   // Store a null certificate list rather than an empty one if the peer didn't
@@ -472,6 +487,16 @@
     }
   }
 
+  if (hs->matched_peer_trust_anchor) {
+    // Let the peer know we matched a requested trust anchor.
+    CBB empty_contents;
+    if (!CBB_add_u16(&extensions, TLSEXT_TYPE_trust_anchors) ||        //
+        !CBB_add_u16_length_prefixed(&extensions, &empty_contents) ||  //
+        !CBB_flush(&extensions)) {
+      return false;
+    }
+  }
+
   for (size_t i = 1; i < sk_CRYPTO_BUFFER_num(cred->chain.get()); i++) {
     CRYPTO_BUFFER *cert_buf = sk_CRYPTO_BUFFER_value(cred->chain.get(), i);
     CBB child;
@@ -642,8 +667,7 @@
   }
 
   // In DTLS, the actual key update is deferred until KeyUpdate is ACKed.
-  if (!SSL_is_dtls(ssl) &&
-      !tls13_rotate_traffic_key(ssl, evp_aead_seal)) {
+  if (!SSL_is_dtls(ssl) && !tls13_rotate_traffic_key(ssl, evp_aead_seal)) {
     return false;
   }