diff --git a/Cargo.toml b/Cargo.toml index dd739ad3..ff975ec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ license-file = "LICENSE" name = "rustls-webpki" readme = "README.md" repository = "https://github.com/rustls/webpki" -version = "0.100.2" +version = "0.100.3" include = [ "Cargo.toml", diff --git a/src/end_entity.rs b/src/end_entity.rs index 14569f1f..dd0a3147 100644 --- a/src/end_entity.rs +++ b/src/end_entity.rs @@ -97,8 +97,6 @@ impl<'a> EndEntityCert<'a> { intermediate_certs, &self.inner, time, - 0, - &mut 0_usize, ) } @@ -130,8 +128,6 @@ impl<'a> EndEntityCert<'a> { intermediate_certs, &self.inner, time, - 0, - &mut 0_usize, ) } diff --git a/src/error.rs b/src/error.rs index fbcf6f74..e35994b3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,7 @@ // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. use core::fmt; +use core::ops::ControlFlow; /// An error that occurs during certificate validation or name validation. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -48,23 +49,47 @@ pub enum Error { /// the notAfter time is earlier than the notBefore time. InvalidCertValidity, + /// A iPAddress name constraint was invalid: + /// - it had a sparse network mask (ie, cannot be written in CIDR form). + /// - it was too long or short + InvalidNetworkMaskConstraint, + /// The signature is invalid for the given public key. InvalidSignatureForPublicKey, + /// The certificate extensions are malformed. + /// + /// In particular, webpki requires the DNS name(s) be in the subjectAltName + /// extension as required by the CA/Browser Forum Baseline Requirements + /// and as recommended by RFC6125. + MalformedExtensions, + + /// The maximum number of name constraint comparisons has been reached. + MaximumNameConstraintComparisonsExceeded, + + /// The maximum number of internal path building calls has been reached. Path complexity is too great. + MaximumPathBuildCallsExceeded, + + /// The path search was terminated because it became too deep. + MaximumPathDepthExceeded, + + /// The maximum number of signature checks has been reached. Path complexity is too great. + MaximumSignatureChecksExceeded, + /// The certificate violates one or more name constraints. NameConstraintViolation, /// The certificate violates one or more path length constraints. PathLenConstraintViolated, - /// The algorithm in the TBSCertificate "signature" field of a certificate - /// does not match the algorithm in the signature of the certificate. - SignatureAlgorithmMismatch, - /// The certificate is not valid for the Extended Key Usage for which it is /// being validated. RequiredEkuNotFound, + /// The algorithm in the TBSCertificate "signature" field of a certificate + /// does not match the algorithm in the signature of the certificate. + SignatureAlgorithmMismatch, + /// A valid issuer for the certificate could not be found. UnknownIssuer, @@ -74,19 +99,13 @@ pub enum Error { /// is malformed. UnsupportedCertVersion, - /// The certificate extensions are malformed. - /// - /// In particular, webpki requires the DNS name(s) be in the subjectAltName - /// extension as required by the CA/Browser Forum Baseline Requirements - /// and as recommended by RFC6125. - MalformedExtensions, - - /// The maximum number of signature checks has been reached. Path complexity is too great. - MaximumSignatureChecksExceeded, - /// The certificate contains an unsupported critical extension. UnsupportedCriticalExtension, + /// The signature algorithm for a signature is not in the set of supported + /// signature algorithms given. + UnsupportedSignatureAlgorithm, + /// The signature's algorithm does not match the algorithm of the public /// key it is being validated for. This may be because the public key /// algorithm's OID isn't recognized (e.g. DSA), or the public key @@ -95,15 +114,31 @@ pub enum Error { /// algorithm and the signature algorithm simply don't match (e.g. /// verifying an RSA signature with an ECC public key). UnsupportedSignatureAlgorithmForPublicKey, +} - /// The signature algorithm for a signature is not in the set of supported - /// signature algorithms given. - UnsupportedSignatureAlgorithm, +impl Error { + /// Returns true for errors that should be considered fatal during path building. Errors of + /// this class should halt any further path building and be returned immediately. + #[inline] + pub(crate) fn is_fatal(&self) -> bool { + matches!( + self, + Error::MaximumSignatureChecksExceeded + | Error::MaximumPathBuildCallsExceeded + | Error::MaximumNameConstraintComparisonsExceeded + ) + } +} - /// A iPAddress name constraint was invalid: - /// - it had a sparse network mask (ie, cannot be written in CIDR form). - /// - it was too long or short - InvalidNetworkMaskConstraint, +impl From for ControlFlow { + fn from(value: Error) -> Self { + match value { + // If an error is fatal, we've exhausted the potential for continued search. + err if err.is_fatal() => Self::Break(err), + // Otherwise we've rejected one candidate chain, but may continue to search for others. + err => Self::Continue(err), + } + } } impl fmt::Display for Error { diff --git a/src/signed_data.rs b/src/signed_data.rs index eb36c44f..f29cfbca 100644 --- a/src/signed_data.rs +++ b/src/signed_data.rs @@ -12,6 +12,7 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +use crate::verify_cert::Budget; use crate::{der, Error}; use ring::signature; @@ -96,7 +97,10 @@ pub(crate) fn verify_signed_data( supported_algorithms: &[&SignatureAlgorithm], spki_value: untrusted::Input, signed_data: &SignedData, + budget: &mut Budget, ) -> Result<(), Error> { + budget.consume_signature()?; + // We need to verify the signature in `signed_data` using the public key // in `public_key`. In order to know which *ring* signature verification // algorithm to use, we need to know the public key algorithm (ECDSA, @@ -438,7 +442,8 @@ mod tests { signed_data::verify_signed_data( SUPPORTED_ALGORITHMS_IN_TESTS, spki_value, - &signed_data + &signed_data, + &mut Budget::default() ) ); } @@ -735,6 +740,7 @@ mod tests { } } + use crate::verify_cert::Budget; use alloc::str::Lines; fn read_pem_section(lines: &mut Lines, section_name: &str) -> Vec { diff --git a/src/subject_name/verify.rs b/src/subject_name/verify.rs index 0f50e8a0..ea3faecd 100644 --- a/src/subject_name/verify.rs +++ b/src/subject_name/verify.rs @@ -19,7 +19,9 @@ use super::{ }; use crate::{ cert::{Cert, EndEntityOrCa}, - der, Error, + der, + verify_cert::Budget, + Error, }; pub(crate) fn verify_cert_dns_name( @@ -33,7 +35,7 @@ pub(crate) fn verify_cert_dns_name( cert.subject_alt_name, SubjectCommonNameContents::Ignore, Err(Error::CertNotValidForName), - &|name| { + &mut |name| { if let GeneralName::DnsName(presented_id) = name { match dns_name::presented_id_matches_reference_id(presented_id, dns_name) { Some(true) => return NameIteration::Stop(Ok(())), @@ -67,7 +69,7 @@ pub(crate) fn verify_cert_subject_name( cert.inner().subject_alt_name, SubjectCommonNameContents::Ignore, Err(Error::CertNotValidForName), - &|name| { + &mut |name| { if let GeneralName::IpAddress(presented_id) = name { match ip_address::presented_id_matches_reference_id(presented_id, ip_address) { Ok(true) => return NameIteration::Stop(Ok(())), @@ -87,6 +89,7 @@ pub(crate) fn check_name_constraints( input: Option<&mut untrusted::Reader>, subordinate_certs: &Cert, subject_common_name_contents: SubjectCommonNameContents, + budget: &mut Budget, ) -> Result<(), Error> { let input = match input { Some(input) => input, @@ -115,11 +118,12 @@ pub(crate) fn check_name_constraints( child.subject_alt_name, subject_common_name_contents, Ok(()), - &|name| { + &mut |name| { check_presented_id_conforms_to_constraints( name, permitted_subtrees, excluded_subtrees, + budget, ) }, )?; @@ -139,11 +143,13 @@ fn check_presented_id_conforms_to_constraints( name: GeneralName, permitted_subtrees: Option, excluded_subtrees: Option, + budget: &mut Budget, ) -> NameIteration { match check_presented_id_conforms_to_constraints_in_subtree( name, Subtrees::PermittedSubtrees, permitted_subtrees, + budget, ) { stop @ NameIteration::Stop(..) => { return stop; @@ -155,6 +161,7 @@ fn check_presented_id_conforms_to_constraints( name, Subtrees::ExcludedSubtrees, excluded_subtrees, + budget, ) } @@ -168,6 +175,7 @@ fn check_presented_id_conforms_to_constraints_in_subtree( name: GeneralName, subtrees: Subtrees, constraints: Option, + budget: &mut Budget, ) -> NameIteration { let mut constraints = match constraints { Some(constraints) => untrusted::Reader::new(constraints), @@ -180,6 +188,10 @@ fn check_presented_id_conforms_to_constraints_in_subtree( let mut has_permitted_subtrees_mismatch = false; while !constraints.at_end() { + if let Err(e) = budget.consume_name_constraint_comparison() { + return NameIteration::Stop(Err(e)); + } + // http://tools.ietf.org/html/rfc5280#section-4.2.1.10: "Within this // profile, the minimum and maximum fields are not used with any name // forms, thus, the minimum MUST be zero, and maximum MUST be absent." @@ -307,7 +319,7 @@ fn iterate_names( subject_alt_name: Option, subject_common_name_contents: SubjectCommonNameContents, result_if_never_stopped_early: Result<(), Error>, - f: &dyn Fn(GeneralName) -> NameIteration, + f: &mut dyn FnMut(GeneralName) -> NameIteration, ) -> Result<(), Error> { if let Some(subject_alt_name) = subject_alt_name { let mut subject_alt_name = untrusted::Reader::new(subject_alt_name); diff --git a/src/verify_cert.rs b/src/verify_cert.rs index 6166218f..5bb2bab1 100644 --- a/src/verify_cert.rs +++ b/src/verify_cert.rs @@ -12,6 +12,9 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +use core::default::Default; +use core::ops::ControlFlow; + use crate::{ cert::{self, Cert, EndEntityOrCa}, der, signed_data, subject_name, time, Error, SignatureAlgorithm, TrustAnchor, @@ -25,9 +28,34 @@ pub(crate) fn build_chain( intermediate_certs: &[&[u8]], cert: &Cert, time: time::Time, - sub_ca_count: usize, - signatures: &mut usize, ) -> Result<(), Error> { + build_chain_inner( + required_eku_if_present, + supported_sig_algs, + trust_anchors, + intermediate_certs, + cert, + time, + 0, + &mut Budget::default(), + ) + .map_err(|e| match e { + ControlFlow::Break(err) => err, + ControlFlow::Continue(err) => err, + }) +} + +#[allow(clippy::too_many_arguments)] +fn build_chain_inner( + required_eku_if_present: KeyPurposeId, + supported_sig_algs: &[&SignatureAlgorithm], + trust_anchors: &[TrustAnchor], + intermediate_certs: &[&[u8]], + cert: &Cert, + time: time::Time, + sub_ca_count: usize, + budget: &mut Budget, +) -> Result<(), ControlFlow> { let used_as_ca = used_as_ca(&cert.ee_or_ca); check_issuer_independent_properties( @@ -45,7 +73,7 @@ pub(crate) fn build_chain( const MAX_SUB_CA_COUNT: usize = 6; if sub_ca_count >= MAX_SUB_CA_COUNT { - return Err(Error::UnknownIssuer); + return Err(Error::MaximumPathDepthExceeded.into()); } } UsedAsCa::No => { @@ -68,20 +96,20 @@ pub(crate) fn build_chain( let result = loop_while_non_fatal_error(trust_anchors, |trust_anchor: &TrustAnchor| { let trust_anchor_subject = untrusted::Input::from(trust_anchor.subject); if cert.issuer != trust_anchor_subject { - return Err(Error::UnknownIssuer); + return Err(Error::UnknownIssuer.into()); } - let name_constraints = trust_anchor.name_constraints.map(untrusted::Input::from); - - untrusted::read_all_optional(name_constraints, Error::BadDer, |value| { - subject_name::check_name_constraints(value, cert, subject_common_name_contents) - })?; + // TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?; let trust_anchor_spki = untrusted::Input::from(trust_anchor.spki); + check_signed_chain(supported_sig_algs, cert, trust_anchor_spki, budget)?; - // TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?; - - check_signatures(supported_sig_algs, cert, trust_anchor_spki, signatures)?; + check_signed_chain_name_constraints( + cert, + trust_anchor, + subject_common_name_contents, + budget, + )?; Ok(()) }); @@ -89,8 +117,10 @@ pub(crate) fn build_chain( // If the error is not fatal, then keep going. match result { Ok(()) => return Ok(()), - err @ Err(Error::MaximumSignatureChecksExceeded) => return err, - _ => {} + // Fatal errors should halt further path building. + res @ Err(ControlFlow::Break(_)) => return res, + // Non-fatal errors should allow path building to continue. + Err(ControlFlow::Continue(_)) => {} }; loop_while_non_fatal_error(intermediate_certs, |cert_der| { @@ -98,7 +128,7 @@ pub(crate) fn build_chain( cert::parse_cert(untrusted::Input::from(cert_der), EndEntityOrCa::Ca(cert))?; if potential_issuer.subject != cert.issuer { - return Err(Error::UnknownIssuer); + return Err(Error::UnknownIssuer.into()); } // Prevent loops; see RFC 4158 section 5.2. @@ -107,7 +137,7 @@ pub(crate) fn build_chain( if potential_issuer.spki.value() == prev.spki.value() && potential_issuer.subject == prev.subject { - return Err(Error::UnknownIssuer); + return Err(Error::UnknownIssuer.into()); } match &prev.ee_or_ca { EndEntityOrCa::EndEntity => { @@ -119,16 +149,13 @@ pub(crate) fn build_chain( } } - untrusted::read_all_optional(potential_issuer.name_constraints, Error::BadDer, |value| { - subject_name::check_name_constraints(value, cert, subject_common_name_contents) - })?; - let next_sub_ca_count = match used_as_ca { UsedAsCa::No => sub_ca_count, UsedAsCa::Yes => sub_ca_count + 1, }; - build_chain( + budget.consume_build_chain_call()?; + build_chain_inner( required_eku_if_present, supported_sig_algs, trust_anchors, @@ -136,26 +163,21 @@ pub(crate) fn build_chain( &potential_issuer, time, next_sub_ca_count, - signatures, + budget, ) }) } -fn check_signatures( +fn check_signed_chain( supported_sig_algs: &[&SignatureAlgorithm], cert_chain: &Cert, trust_anchor_key: untrusted::Input, - signatures: &mut usize, -) -> Result<(), Error> { + budget: &mut Budget, +) -> Result<(), ControlFlow> { let mut spki_value = trust_anchor_key; let mut cert = cert_chain; loop { - *signatures += 1; - if *signatures > 100 { - return Err(Error::MaximumSignatureChecksExceeded); - } - - signed_data::verify_signed_data(supported_sig_algs, spki_value, &cert.signed_data)?; + signed_data::verify_signed_data(supported_sig_algs, spki_value, &cert.signed_data, budget)?; // TODO: check revocation @@ -173,6 +195,92 @@ fn check_signatures( Ok(()) } +fn check_signed_chain_name_constraints( + cert_chain: &Cert, + trust_anchor: &TrustAnchor, + subject_common_name_contents: subject_name::SubjectCommonNameContents, + budget: &mut Budget, +) -> Result<(), ControlFlow> { + let mut cert = cert_chain; + let mut name_constraints = trust_anchor + .name_constraints + .as_ref() + .map(|der| untrusted::Input::from(der)); + + loop { + untrusted::read_all_optional(name_constraints, Error::BadDer, |value| { + subject_name::check_name_constraints(value, cert, subject_common_name_contents, budget) + })?; + + match &cert.ee_or_ca { + EndEntityOrCa::Ca(child_cert) => { + name_constraints = cert.name_constraints; + cert = child_cert; + } + EndEntityOrCa::EndEntity => { + break; + } + } + } + + Ok(()) +} + +pub(crate) struct Budget { + signatures: usize, + build_chain_calls: usize, + name_constraint_comparisons: usize, +} + +impl Budget { + #[inline] + pub(crate) fn consume_signature(&mut self) -> Result<(), Error> { + self.signatures = self + .signatures + .checked_sub(1) + .ok_or(Error::MaximumSignatureChecksExceeded)?; + Ok(()) + } + + #[inline] + fn consume_build_chain_call(&mut self) -> Result<(), Error> { + self.build_chain_calls = self + .build_chain_calls + .checked_sub(1) + .ok_or(Error::MaximumPathBuildCallsExceeded)?; + Ok(()) + } + + #[inline] + pub(crate) fn consume_name_constraint_comparison(&mut self) -> Result<(), Error> { + self.name_constraint_comparisons = self + .name_constraint_comparisons + .checked_sub(1) + .ok_or(Error::MaximumNameConstraintComparisonsExceeded)?; + Ok(()) + } +} + +impl Default for Budget { + fn default() -> Self { + Self { + // This limit is taken from the remediation for golang CVE-2018-16875. However, + // note that golang subsequently implemented AKID matching due to this limit + // being hit in real applications (see ). + // So this may actually be too aggressive. + signatures: 100, + + // This limit is taken from NSS libmozpkix, see: + // + build_chain_calls: 200_000, + + // This limit is taken from golang crypto/x509's default, see: + // + name_constraint_comparisons: 250_000, + } + } +} + fn check_issuer_independent_properties( cert: &Cert, time: time::Time, @@ -352,8 +460,8 @@ fn check_eku( fn loop_while_non_fatal_error( values: V, - mut f: impl FnMut(V::Item) -> Result<(), Error>, -) -> Result<(), Error> + mut f: impl FnMut(V::Item) -> Result<(), ControlFlow>, +) -> Result<(), ControlFlow> where V: IntoIterator, { @@ -361,76 +469,266 @@ where // If the error is not fatal, then keep going. match f(v) { Ok(()) => return Ok(()), - err @ Err(Error::MaximumSignatureChecksExceeded) => return err, - _ => {} + // Fatal errors should halt further looping. + res @ Err(ControlFlow::Break(_)) => return res, + // Non-fatal errors should allow looping to continue. + Err(ControlFlow::Continue(_)) => {} } } - Err(Error::UnknownIssuer) + Err(Error::UnknownIssuer.into()) } #[cfg(test)] mod tests { use super::*; + use core::convert::TryFrom; + + #[cfg(feature = "alloc")] + enum TrustAnchorIsActualIssuer { + Yes, + No, + } + + #[cfg(feature = "alloc")] + fn build_degenerate_chain( + intermediate_count: usize, + trust_anchor_is_actual_issuer: TrustAnchorIsActualIssuer, + budget: Option, + ) -> ControlFlow { + let ca_cert = make_issuer("Bogus Subject", None); + let ca_cert_der = ca_cert.serialize_der().unwrap(); + + let mut intermediates = Vec::with_capacity(intermediate_count); + let mut issuer = ca_cert; + for _ in 0..intermediate_count { + let intermediate = make_issuer("Bogus Subject", None); + let intermediate_der = intermediate.serialize_der_with_signer(&issuer).unwrap(); + intermediates.push(intermediate_der); + issuer = intermediate; + } + + if let TrustAnchorIsActualIssuer::No = trust_anchor_is_actual_issuer { + intermediates.pop(); + } + + verify_chain( + &ca_cert_der, + &intermediates, + &make_end_entity(&issuer), + budget, + ) + .unwrap_err() + } #[test] #[cfg(feature = "alloc")] fn test_too_many_signatures() { - use std::convert::TryFrom; - - use crate::{EndEntityCert, Time, ECDSA_P256_SHA256}; - - let alg = &rcgen::PKCS_ECDSA_P256_SHA256; - - let make_issuer = || { - let mut ca_params = rcgen::CertificateParams::new(Vec::new()); - ca_params - .distinguished_name - .push(rcgen::DnType::OrganizationName, "Bogus Subject"); - ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); - ca_params.key_usages = vec![ - rcgen::KeyUsagePurpose::KeyCertSign, - rcgen::KeyUsagePurpose::DigitalSignature, - rcgen::KeyUsagePurpose::CrlSign, - ]; - ca_params.alg = alg; - rcgen::Certificate::from_params(ca_params).unwrap() - }; + assert!(matches!( + build_degenerate_chain(5, TrustAnchorIsActualIssuer::Yes, None), + ControlFlow::Break(Error::MaximumSignatureChecksExceeded) + )); + } + + #[test] + #[cfg(feature = "alloc")] + fn test_too_many_path_calls() { + assert!(matches!( + build_degenerate_chain( + 10, + TrustAnchorIsActualIssuer::No, + Some(Budget { + // Crafting a chain that will expend the build chain calls budget without + // first expending the signature checks budget is tricky, so we artificially + // inflate the signature limit to make this test easier to write. + signatures: usize::MAX, + ..Budget::default() + }) + ), + ControlFlow::Break(Error::MaximumPathBuildCallsExceeded) + )); + } - let ca_cert = make_issuer(); + #[cfg(feature = "alloc")] + fn build_linear_chain(chain_length: usize) -> Result<(), ControlFlow> { + let ca_cert = make_issuer(format!("Bogus Subject {chain_length}"), None); let ca_cert_der = ca_cert.serialize_der().unwrap(); - let mut intermediates = Vec::with_capacity(101); + let mut intermediates = Vec::with_capacity(chain_length); let mut issuer = ca_cert; - for _ in 0..101 { - let intermediate = make_issuer(); + for i in 0..chain_length { + let intermediate = make_issuer(format!("Bogus Subject {i}"), None); let intermediate_der = intermediate.serialize_der_with_signer(&issuer).unwrap(); intermediates.push(intermediate_der); issuer = intermediate; } - let mut ee_params = rcgen::CertificateParams::new(vec!["example.com".to_string()]); - ee_params.is_ca = rcgen::IsCa::ExplicitNoCa; - ee_params.alg = alg; - let ee_cert = rcgen::Certificate::from_params(ee_params).unwrap(); - let ee_cert_der = ee_cert.serialize_der_with_signer(&issuer).unwrap(); + verify_chain( + &ca_cert_der, + &intermediates, + &make_end_entity(&issuer), + None, + ) + } + + #[test] + #[cfg(feature = "alloc")] + fn longest_allowed_path() { + assert!(build_linear_chain(1).is_ok()); + assert!(build_linear_chain(2).is_ok()); + assert!(build_linear_chain(3).is_ok()); + assert!(build_linear_chain(4).is_ok()); + assert!(build_linear_chain(5).is_ok()); + assert!(build_linear_chain(6).is_ok()); + } + + #[test] + #[cfg(feature = "alloc")] + fn path_too_long() { + // Note: webpki 0.101.x and earlier surface all non-fatal errors as UnknownIssuer, + // eating the more specific MaximumPathDepthExceeded error. + assert!(matches!( + build_linear_chain(7), + Err(ControlFlow::Continue(Error::UnknownIssuer)) + )); + } + + #[test] + #[cfg(feature = "alloc")] + fn name_constraint_budget() { + // Issue a trust anchor that imposes name constraints. The constraint should match + // the end entity certificate SAN. + let ca_cert = make_issuer( + "Constrained Root", + Some(rcgen::NameConstraints { + permitted_subtrees: vec![rcgen::GeneralSubtree::DnsName(".com".into())], + excluded_subtrees: vec![], + }), + ); + let ca_cert_der = ca_cert.serialize_der().unwrap(); + + // Create a series of intermediate issuers. We'll only use one in the actual built path, + // helping demonstrate that the name constraint budget is not expended checking certificates + // that are not part of the path we compute. + const NUM_INTERMEDIATES: usize = 5; + let mut intermediates = Vec::with_capacity(NUM_INTERMEDIATES); + for i in 0..NUM_INTERMEDIATES { + intermediates.push(make_issuer(format!("Intermediate {i}"), None)); + } + + // Each intermediate should be issued by the trust anchor. + let mut intermediates_der = Vec::with_capacity(NUM_INTERMEDIATES); + for intermediate in &intermediates { + intermediates_der.push(intermediate.serialize_der_with_signer(&ca_cert).unwrap()); + } + + // Create an end-entity cert that is issued by the last of the intermediates. + let ee_cert = make_end_entity(intermediates.last().unwrap()); + + // We use a custom budget to make it easier to write a test, otherwise it is tricky to + // stuff enough names/constraints into the potential chains while staying within the path + // depth limit and the build chain call limit. + let passing_budget = Budget { + // One comparison against the intermediate's distinguished name. + // One comparison against the EE's distinguished name. + // One comparison against the EE's SAN. + // = 3 total comparisons. + name_constraint_comparisons: 3, + ..Budget::default() + }; + + // Validation should succeed with the name constraint comparison budget allocated above. + // This shows that we're not consuming budget on unused intermediates: we didn't budget + // enough comparisons for that to pass the overall chain building. + assert!(verify_chain( + &ca_cert_der, + &intermediates_der, + &ee_cert, + Some(passing_budget), + ) + .is_ok()); + + let failing_budget = Budget { + // See passing_budget: 2 comparisons is not sufficient. + name_constraint_comparisons: 2, + ..Budget::default() + }; + // Validation should fail when the budget is smaller than the number of comparisons performed + // on the validated path. This demonstrates we properly fail path building when too many + // name constraint comparisons occur. + let result = verify_chain( + &ca_cert_der, + &intermediates_der, + &ee_cert, + Some(failing_budget), + ); + + assert!(matches!( + result, + Err(ControlFlow::Break( + Error::MaximumNameConstraintComparisonsExceeded + )) + )); + } - let anchors = &[TrustAnchor::try_from_cert_der(&ca_cert_der).unwrap()]; + #[cfg(feature = "alloc")] + fn verify_chain( + trust_anchor_der: &[u8], + intermediates_der: &[Vec], + ee_cert_der: &[u8], + budget: Option, + ) -> Result<(), ControlFlow> { + use crate::ECDSA_P256_SHA256; + use crate::{EndEntityCert, Time}; + + let anchors = &[TrustAnchor::try_from_cert_der(trust_anchor_der).unwrap()]; let time = Time::from_seconds_since_unix_epoch(0x1fed_f00d); - let cert = EndEntityCert::try_from(&ee_cert_der[..]).unwrap(); - let intermediates_der: Vec<&[u8]> = intermediates.iter().map(|x| x.as_ref()).collect(); - let intermediate_certs: &[&[u8]] = intermediates_der.as_ref(); + let cert = EndEntityCert::try_from(ee_cert_der).unwrap(); + let intermediates_der = intermediates_der + .iter() + .map(|x| x.as_ref()) + .collect::>(); - let result = build_chain( + build_chain_inner( EKU_SERVER_AUTH, &[&ECDSA_P256_SHA256], anchors, - intermediate_certs, + &intermediates_der, cert.inner(), time, 0, - &mut 0_usize, - ); + &mut budget.unwrap_or_default(), + ) + } + + #[cfg(feature = "alloc")] + fn make_issuer( + org_name: impl Into, + name_constraints: Option, + ) -> rcgen::Certificate { + let mut ca_params = rcgen::CertificateParams::new(Vec::new()); + ca_params + .distinguished_name + .push(rcgen::DnType::OrganizationName, org_name); + ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + ca_params.key_usages = vec![ + rcgen::KeyUsagePurpose::KeyCertSign, + rcgen::KeyUsagePurpose::DigitalSignature, + rcgen::KeyUsagePurpose::CrlSign, + ]; + ca_params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; + ca_params.name_constraints = name_constraints; + rcgen::Certificate::from_params(ca_params).unwrap() + } + + #[cfg(feature = "alloc")] + fn make_end_entity(issuer: &rcgen::Certificate) -> Vec { + let mut ee_params = rcgen::CertificateParams::new(vec!["example.com".to_string()]); + ee_params.is_ca = rcgen::IsCa::ExplicitNoCa; + ee_params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; - assert!(matches!(result, Err(Error::MaximumSignatureChecksExceeded))); + rcgen::Certificate::from_params(ee_params) + .unwrap() + .serialize_der_with_signer(issuer) + .unwrap() } }