Skip to content

Commit 4ea0523

Browse files
cpuctz
authored andcommitted
verify_cert: enforce maximum number of signatures.
Pathbuilding complexity can be quadratic, particularly when the set of intermediates all have subjects matching a trust anchor. In these cases we need to bound the number of expensive signature validation operations that are performed to avoid a DoS on CPU usage. This commit implements a simple maximum signature check limit inspired by the approach taken in the Golang x509 package. No more than 100 signatures will be evaluated while pathbuilding. This limit works in practice for Go when processing real world certificate chains and so should be appropriate for our use case as well.
1 parent abb0c03 commit 4ea0523

File tree

4 files changed

+93
-7
lines changed

4 files changed

+93
-7
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ untrusted = "0.7.1"
8282

8383
[dev-dependencies]
8484
base64 = "0.13"
85+
rcgen = { version = "0.11.1", default-features = false }
8586

8687
[profile.bench]
8788
opt-level = 3

src/end_entity.rs

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ impl<'a> EndEntityCert<'a> {
9898
&self.inner,
9999
time,
100100
0,
101+
&mut 0_usize,
101102
)
102103
}
103104

@@ -130,6 +131,7 @@ impl<'a> EndEntityCert<'a> {
130131
&self.inner,
131132
time,
132133
0,
134+
&mut 0_usize,
133135
)
134136
}
135137

src/error.rs

+3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ pub enum Error {
8181
/// and as recommended by RFC6125.
8282
MalformedExtensions,
8383

84+
/// The maximum number of signature checks has been reached. Path complexity is too great.
85+
MaximumSignatureChecksExceeded,
86+
8487
/// The certificate contains an unsupported critical extension.
8588
UnsupportedCriticalExtension,
8689

src/verify_cert.rs

+87-7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::{
1717
der, signed_data, subject_name, time, Error, SignatureAlgorithm, TrustAnchor,
1818
};
1919

20+
#[allow(clippy::too_many_arguments)]
2021
pub(crate) fn build_chain(
2122
required_eku_if_present: KeyPurposeId,
2223
supported_sig_algs: &[&SignatureAlgorithm],
@@ -25,6 +26,7 @@ pub(crate) fn build_chain(
2526
cert: &Cert,
2627
time: time::Time,
2728
sub_ca_count: usize,
29+
signatures: &mut usize,
2830
) -> Result<(), Error> {
2931
let used_as_ca = used_as_ca(&cert.ee_or_ca);
3032

@@ -79,15 +81,17 @@ pub(crate) fn build_chain(
7981

8082
// TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?;
8183

82-
check_signatures(supported_sig_algs, cert, trust_anchor_spki)?;
84+
check_signatures(supported_sig_algs, cert, trust_anchor_spki, signatures)?;
8385

8486
Ok(())
8587
});
8688

8789
// If the error is not fatal, then keep going.
88-
if result.is_ok() {
89-
return Ok(());
90-
}
90+
match result {
91+
Ok(()) => return Ok(()),
92+
err @ Err(Error::MaximumSignatureChecksExceeded) => return err,
93+
_ => {}
94+
};
9195

9296
loop_while_non_fatal_error(intermediate_certs, |cert_der| {
9397
let potential_issuer =
@@ -132,6 +136,7 @@ pub(crate) fn build_chain(
132136
&potential_issuer,
133137
time,
134138
next_sub_ca_count,
139+
signatures,
135140
)
136141
})
137142
}
@@ -140,10 +145,16 @@ fn check_signatures(
140145
supported_sig_algs: &[&SignatureAlgorithm],
141146
cert_chain: &Cert,
142147
trust_anchor_key: untrusted::Input,
148+
signatures: &mut usize,
143149
) -> Result<(), Error> {
144150
let mut spki_value = trust_anchor_key;
145151
let mut cert = cert_chain;
146152
loop {
153+
*signatures += 1;
154+
if *signatures > 100 {
155+
return Err(Error::MaximumSignatureChecksExceeded);
156+
}
157+
147158
signed_data::verify_signed_data(supported_sig_algs, spki_value, &cert.signed_data)?;
148159

149160
// TODO: check revocation
@@ -341,16 +352,85 @@ fn check_eku(
341352

342353
fn loop_while_non_fatal_error<V>(
343354
values: V,
344-
f: impl Fn(V::Item) -> Result<(), Error>,
355+
mut f: impl FnMut(V::Item) -> Result<(), Error>,
345356
) -> Result<(), Error>
346357
where
347358
V: IntoIterator,
348359
{
349360
for v in values {
350361
// If the error is not fatal, then keep going.
351-
if f(v).is_ok() {
352-
return Ok(());
362+
match f(v) {
363+
Ok(()) => return Ok(()),
364+
err @ Err(Error::MaximumSignatureChecksExceeded) => return err,
365+
_ => {}
353366
}
354367
}
355368
Err(Error::UnknownIssuer)
356369
}
370+
371+
#[cfg(test)]
372+
mod tests {
373+
use super::*;
374+
375+
#[test]
376+
#[cfg(feature = "alloc")]
377+
fn test_too_many_signatures() {
378+
use std::convert::TryFrom;
379+
380+
use crate::{EndEntityCert, Time, ECDSA_P256_SHA256};
381+
382+
let alg = &rcgen::PKCS_ECDSA_P256_SHA256;
383+
384+
let make_issuer = || {
385+
let mut ca_params = rcgen::CertificateParams::new(Vec::new());
386+
ca_params
387+
.distinguished_name
388+
.push(rcgen::DnType::OrganizationName, "Bogus Subject");
389+
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
390+
ca_params.key_usages = vec![
391+
rcgen::KeyUsagePurpose::KeyCertSign,
392+
rcgen::KeyUsagePurpose::DigitalSignature,
393+
rcgen::KeyUsagePurpose::CrlSign,
394+
];
395+
ca_params.alg = alg;
396+
rcgen::Certificate::from_params(ca_params).unwrap()
397+
};
398+
399+
let ca_cert = make_issuer();
400+
let ca_cert_der = ca_cert.serialize_der().unwrap();
401+
402+
let mut intermediates = Vec::with_capacity(101);
403+
let mut issuer = ca_cert;
404+
for _ in 0..101 {
405+
let intermediate = make_issuer();
406+
let intermediate_der = intermediate.serialize_der_with_signer(&issuer).unwrap();
407+
intermediates.push(intermediate_der);
408+
issuer = intermediate;
409+
}
410+
411+
let mut ee_params = rcgen::CertificateParams::new(vec!["example.com".to_string()]);
412+
ee_params.is_ca = rcgen::IsCa::ExplicitNoCa;
413+
ee_params.alg = alg;
414+
let ee_cert = rcgen::Certificate::from_params(ee_params).unwrap();
415+
let ee_cert_der = ee_cert.serialize_der_with_signer(&issuer).unwrap();
416+
417+
let anchors = &[TrustAnchor::try_from_cert_der(&ca_cert_der).unwrap()];
418+
let time = Time::from_seconds_since_unix_epoch(0x1fed_f00d);
419+
let cert = EndEntityCert::try_from(&ee_cert_der[..]).unwrap();
420+
let intermediates_der: Vec<&[u8]> = intermediates.iter().map(|x| x.as_ref()).collect();
421+
let intermediate_certs: &[&[u8]] = intermediates_der.as_ref();
422+
423+
let result = build_chain(
424+
EKU_SERVER_AUTH,
425+
&[&ECDSA_P256_SHA256],
426+
anchors,
427+
intermediate_certs,
428+
cert.inner(),
429+
time,
430+
0,
431+
&mut 0_usize,
432+
);
433+
434+
assert!(matches!(result, Err(Error::MaximumSignatureChecksExceeded)));
435+
}
436+
}

0 commit comments

Comments
 (0)