Skip to content

Commit 4be53bf

Browse files
authored
Added a budget for NC checks to protect against DoS (#10467) (#10468)
1 parent 8e9de30 commit 4be53bf

4 files changed

Lines changed: 62 additions & 9 deletions

File tree

.github/actions/fetch-vectors/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ runs:
1616
with:
1717
repository: "C2SP/x509-limbo"
1818
path: "x509-limbo"
19-
# Latest commit on the x509-limbo main branch, as of Jan 23, 2024.
20-
ref: "cf66142f5c27b64c987c6f0aa4c10b8c9677b41c" # x509-limbo-ref
19+
# Latest commit on the x509-limbo main branch, as of Feb 23, 2024.
20+
ref: "c8f6a4f4946076db55778ed7b3cffdab082a1a12" # x509-limbo-ref

src/rust/cryptography-x509-verification/src/lib.rs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,35 @@ pub enum ValidationError {
3333
CandidatesExhausted(Box<ValidationError>),
3434
Malformed(asn1::ParseError),
3535
DuplicateExtension(DuplicateExtensionsError),
36+
FatalError(&'static str),
3637
Other(String),
3738
}
3839

40+
struct Budget {
41+
name_constraint_checks: usize,
42+
}
43+
44+
impl Budget {
45+
// Same limit as other validators
46+
const DEFAULT_NAME_CONSTRAINT_CHECK_LIMIT: usize = 1 << 20;
47+
48+
fn new() -> Budget {
49+
Budget {
50+
name_constraint_checks: Self::DEFAULT_NAME_CONSTRAINT_CHECK_LIMIT,
51+
}
52+
}
53+
54+
fn name_constraint_check(&mut self) -> Result<(), ValidationError> {
55+
self.name_constraint_checks =
56+
self.name_constraint_checks
57+
.checked_sub(1)
58+
.ok_or(ValidationError::FatalError(
59+
"Exceeded maximum name constraint check limit",
60+
))?;
61+
Ok(())
62+
}
63+
}
64+
3965
impl From<asn1::ParseError> for ValidationError {
4066
fn from(value: asn1::ParseError) -> Self {
4167
Self::Malformed(value)
@@ -76,7 +102,10 @@ impl<'a, 'chain> NameChain<'a, 'chain> {
76102
&self,
77103
constraint: &GeneralName<'chain>,
78104
san: &GeneralName<'chain>,
105+
budget: &mut Budget,
79106
) -> Result<ApplyNameConstraintStatus, ValidationError> {
107+
budget.name_constraint_check()?;
108+
80109
match (constraint, san) {
81110
(GeneralName::DNSName(pattern), GeneralName::DNSName(name)) => {
82111
match (DNSConstraint::new(pattern.0), DNSName::new(name.0)) {
@@ -114,17 +143,18 @@ impl<'a, 'chain> NameChain<'a, 'chain> {
114143
fn evaluate_constraints(
115144
&self,
116145
constraints: &NameConstraints<'chain>,
146+
budget: &mut Budget,
117147
) -> Result<(), ValidationError> {
118148
if let Some(child) = self.child {
119-
child.evaluate_constraints(constraints)?;
149+
child.evaluate_constraints(constraints, budget)?;
120150
}
121151

122152
for san in self.sans.clone() {
123153
// If there are no applicable constraints, the SAN is considered valid so the default is true.
124154
let mut permit = true;
125155
if let Some(permitted_subtrees) = &constraints.permitted_subtrees {
126156
for p in permitted_subtrees.unwrap_read().clone() {
127-
let status = self.evaluate_single_constraint(&p.base, &san)?;
157+
let status = self.evaluate_single_constraint(&p.base, &san, budget)?;
128158
if status.is_applied() {
129159
permit = status.is_match();
130160
if permit {
@@ -142,7 +172,7 @@ impl<'a, 'chain> NameChain<'a, 'chain> {
142172

143173
if let Some(excluded_subtrees) = &constraints.excluded_subtrees {
144174
for e in excluded_subtrees.unwrap_read().clone() {
145-
let status = self.evaluate_single_constraint(&e.base, &san)?;
175+
let status = self.evaluate_single_constraint(&e.base, &san, budget)?;
146176
if status.is_match() {
147177
return Err(ValidationError::Other(
148178
"excluded name constraint matched SAN".into(),
@@ -166,7 +196,8 @@ pub fn verify<'chain, B: CryptoOps>(
166196
) -> Result<Chain<'chain, B>, ValidationError> {
167197
let builder = ChainBuilder::new(intermediates.into_iter().collect(), policy, store);
168198

169-
builder.build_chain(leaf)
199+
let mut budget = Budget::new();
200+
builder.build_chain(leaf, &mut budget)
170201
}
171202

172203
struct ChainBuilder<'a, 'chain, B: CryptoOps> {
@@ -227,9 +258,10 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> {
227258
current_depth: u8,
228259
working_cert_extensions: &Extensions<'chain>,
229260
name_chain: NameChain<'_, 'chain>,
261+
budget: &mut Budget,
230262
) -> Result<Chain<'chain, B>, ValidationError> {
231263
if let Some(nc) = working_cert_extensions.get_extension(&NAME_CONSTRAINTS_OID) {
232-
name_chain.evaluate_constraints(&nc.value()?)?;
264+
name_chain.evaluate_constraints(&nc.value()?, budget)?;
233265
}
234266

235267
// 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> {
295327
// candidate (which is a non-leaf by definition) isn't self-issued.
296328
cert_is_self_issued(issuing_cert_candidate.certificate()),
297329
)?,
330+
budget,
298331
) {
299332
Ok(mut chain) => {
300333
chain.push(working_cert.clone());
301334
return Ok(chain);
302335
}
336+
// Immediately return on fatal error.
337+
Err(e @ ValidationError::FatalError(..)) => return Err(e),
303338
Err(e) => last_err = Some(e),
304339
};
305340
}
@@ -326,6 +361,7 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> {
326361
fn build_chain(
327362
&self,
328363
leaf: &VerificationCertificate<'chain, B>,
364+
budget: &mut Budget,
329365
) -> Result<Chain<'chain, B>, ValidationError> {
330366
// Before anything else, check whether the given leaf cert
331367
// is well-formed according to our policy (and its underlying
@@ -342,6 +378,7 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> {
342378
0,
343379
&leaf_extensions,
344380
NameChain::new(None, &leaf_extensions, false)?,
381+
budget,
345382
)?;
346383
// We build the chain in reverse order, fix it now.
347384
chain.reverse();

src/rust/cryptography-x509-verification/src/policy/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,9 +468,11 @@ impl<'a, B: CryptoOps> Policy<'a, B> {
468468
self.permits_ca(issuer.certificate(), current_depth, issuer_extensions)?;
469469

470470
// CA/B 7.1.3.1 SubjectPublicKeyInfo
471+
// NOTE: We check the issuer's SPKI here, since the issuer is
472+
// definitionally a CA and thus subject to CABF key requirements.
471473
if !self
472474
.permitted_public_key_algorithms
473-
.contains(&child.tbs_cert.spki.algorithm)
475+
.contains(&issuer.certificate().tbs_cert.spki.algorithm)
474476
{
475477
return Err(ValidationError::Other(format!(
476478
"Forbidden public key algorithm: {:?}",
@@ -479,6 +481,11 @@ impl<'a, B: CryptoOps> Policy<'a, B> {
479481
}
480482

481483
// CA/B 7.1.3.2 Signature AlgorithmIdentifier
484+
// NOTE: We check the child's signature here, since the issuer's
485+
// signature is not necessarily subject to signature checks (e.g.
486+
// if it's a root). This works out transitively, as any non root-issuer
487+
// will be checked in its recursive step (where it'll be in the child
488+
// position).
482489
if !self
483490
.permitted_signature_algorithms
484491
.contains(&child.signature_alg)

tests/x509/verification/test_limbo.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
# Our support for custom EKUs is limited, and we (like most impls.) don't
2828
# handle all EKU conditions under CABF.
2929
"pedantic-webpki-eku",
30-
# Similarly: contains tests that fail based on a strict reading of RFC 5280
30+
# Most CABF validators do not enforce the CABF key requirements on
31+
# subscriber keys (i.e., in the leaf certificate).
32+
"pedantic-webpki-subscriber-key",
33+
# Tests that fail based on a strict reading of RFC 5280
3134
# but are widely ignored by validators.
3235
"pedantic-rfc5280",
3336
# In rare circumstances, CABF relaxes RFC 5280's prescriptions in
@@ -62,11 +65,17 @@
6265
# forbidden under CABF. This is consistent with what
6366
# Go's crypto/x509 and Rust's webpki crate do.
6467
"webpki::aki::root-with-aki-ski-mismatch",
68+
# We allow RSA keys that aren't divisible by 8, which is technically
69+
# forbidden under CABF. No other implementation checks this either.
70+
"webpki::forbidden-rsa-not-divisable-by-8-in-root",
6571
# We disallow CAs in the leaf position, which is explicitly forbidden
6672
# by CABF (but implicitly permitted under RFC 5280). This is consistent
6773
# with what webpki and rustls do, but inconsistent with Go and OpenSSL.
6874
"rfc5280::ca-as-leaf",
6975
"pathlen::validation-ignores-pathlen-in-leaf",
76+
# Implemented on main, but not backported to this branch.
77+
"webpki::forbidden-p192-root",
78+
"webpki::forbidden-weak-rsa-key-in-root",
7079
}
7180

7281

0 commit comments

Comments
 (0)