From 63bc29608ff301034bd5e452dae6428b30b3e310 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 23 Feb 2024 19:04:47 -0500 Subject: [PATCH] Added a budget for NC checks to protect against DoS (#10467) --- .github/actions/fetch-vectors/action.yml | 4 +- .../cryptography-x509-verification/src/lib.rs | 47 +++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/.github/actions/fetch-vectors/action.yml b/.github/actions/fetch-vectors/action.yml index c1df58824014a..966749bab108a 100644 --- a/.github/actions/fetch-vectors/action.yml +++ b/.github/actions/fetch-vectors/action.yml @@ -16,5 +16,5 @@ runs: with: repository: "C2SP/x509-limbo" path: "x509-limbo" - # Latest commit on the x509-limbo main branch, as of Jan 23, 2024. - ref: "cf66142f5c27b64c987c6f0aa4c10b8c9677b41c" # x509-limbo-ref + # Latest commit on the x509-limbo main branch, as of Feb 23, 2024. + ref: "c8f6a4f4946076db55778ed7b3cffdab082a1a12" # x509-limbo-ref diff --git a/src/rust/cryptography-x509-verification/src/lib.rs b/src/rust/cryptography-x509-verification/src/lib.rs index 6265f75c55023..5ded892d5cbbc 100644 --- a/src/rust/cryptography-x509-verification/src/lib.rs +++ b/src/rust/cryptography-x509-verification/src/lib.rs @@ -33,9 +33,35 @@ pub enum ValidationError { CandidatesExhausted(Box), Malformed(asn1::ParseError), DuplicateExtension(DuplicateExtensionsError), + FatalError(&'static str), Other(String), } +struct Budget { + name_constraint_checks: usize, +} + +impl Budget { + // Same limit as other validators + const DEFAULT_NAME_CONSTRAINT_CHECK_LIMIT: usize = 1 << 20; + + fn new() -> Budget { + Budget { + name_constraint_checks: Self::DEFAULT_NAME_CONSTRAINT_CHECK_LIMIT, + } + } + + fn name_constraint_check(&mut self) -> Result<(), ValidationError> { + self.name_constraint_checks = + self.name_constraint_checks + .checked_sub(1) + .ok_or(ValidationError::FatalError( + "Exceeded maximum name constraint check limit", + ))?; + Ok(()) + } +} + impl From for ValidationError { fn from(value: asn1::ParseError) -> Self { Self::Malformed(value) @@ -76,7 +102,10 @@ impl<'a, 'chain> NameChain<'a, 'chain> { &self, constraint: &GeneralName<'chain>, san: &GeneralName<'chain>, + budget: &mut Budget, ) -> Result { + budget.name_constraint_check()?; + match (constraint, san) { (GeneralName::DNSName(pattern), GeneralName::DNSName(name)) => { match (DNSConstraint::new(pattern.0), DNSName::new(name.0)) { @@ -114,9 +143,10 @@ impl<'a, 'chain> NameChain<'a, 'chain> { fn evaluate_constraints( &self, constraints: &NameConstraints<'chain>, + budget: &mut Budget, ) -> Result<(), ValidationError> { if let Some(child) = self.child { - child.evaluate_constraints(constraints)?; + child.evaluate_constraints(constraints, budget)?; } for san in self.sans.clone() { @@ -124,7 +154,7 @@ impl<'a, 'chain> NameChain<'a, 'chain> { let mut permit = true; if let Some(permitted_subtrees) = &constraints.permitted_subtrees { for p in permitted_subtrees.unwrap_read().clone() { - let status = self.evaluate_single_constraint(&p.base, &san)?; + let status = self.evaluate_single_constraint(&p.base, &san, budget)?; if status.is_applied() { permit = status.is_match(); if permit { @@ -142,7 +172,7 @@ impl<'a, 'chain> NameChain<'a, 'chain> { if let Some(excluded_subtrees) = &constraints.excluded_subtrees { for e in excluded_subtrees.unwrap_read().clone() { - let status = self.evaluate_single_constraint(&e.base, &san)?; + let status = self.evaluate_single_constraint(&e.base, &san, budget)?; if status.is_match() { return Err(ValidationError::Other( "excluded name constraint matched SAN".into(), @@ -166,7 +196,8 @@ pub fn verify<'chain, B: CryptoOps>( ) -> Result, ValidationError> { let builder = ChainBuilder::new(intermediates.into_iter().collect(), policy, store); - builder.build_chain(leaf) + let mut budget = Budget::new(); + builder.build_chain(leaf, &mut budget) } struct ChainBuilder<'a, 'chain, B: CryptoOps> { @@ -227,9 +258,10 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { current_depth: u8, working_cert_extensions: &Extensions<'chain>, name_chain: NameChain<'_, 'chain>, + budget: &mut Budget, ) -> Result, ValidationError> { if let Some(nc) = working_cert_extensions.get_extension(&NAME_CONSTRAINTS_OID) { - name_chain.evaluate_constraints(&nc.value()?)?; + name_chain.evaluate_constraints(&nc.value()?, budget)?; } // Look in the store's root set to see if the working cert is listed. @@ -295,11 +327,14 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { // candidate (which is a non-leaf by definition) isn't self-issued. cert_is_self_issued(issuing_cert_candidate.certificate()), )?, + budget, ) { Ok(mut chain) => { chain.push(working_cert.clone()); return Ok(chain); } + // Immediately return on fatal error. + Err(e @ ValidationError::FatalError(..)) => return Err(e), Err(e) => last_err = Some(e), }; } @@ -326,6 +361,7 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { fn build_chain( &self, leaf: &VerificationCertificate<'chain, B>, + budget: &mut Budget, ) -> Result, ValidationError> { // Before anything else, check whether the given leaf cert // is well-formed according to our policy (and its underlying @@ -342,6 +378,7 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { 0, &leaf_extensions, NameChain::new(None, &leaf_extensions, false)?, + budget, )?; // We build the chain in reverse order, fix it now. chain.reverse();