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;
}