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