Skip to content

Commit

Permalink
tests: add name constraint integration test.
Browse files Browse the repository at this point in the history
This commit adds integration testing for trust anchors in webpki-roots
with name constraints.

The general idea is that for each name constraints extension we:

* parse the name constraints with x509-parser, verifying that the
  encoding is well formed and contains something approximating what we
  expect (e.g. at least one permitted subtree, no excluded subtrees).
* convert the name constraints into the form rcgen expects for
  certificate generation parameters.
* issue our own trust anchor CA certificate with the name constraints
  from the webpki trust anchor.
* for each permitted subtree base dns name in the name constraints we
  use our generated CA to issue end entity certificates that will be
  permitted, and rejected by the name constraints.
* we then translate our issued CA back to a webpki trust anchor, and use
  webpki to verify each of the permitted and rejected end entity
  certificates, asserting the result matches what we expect for the name
  constraint.
  • Loading branch information
cpu committed Aug 9, 2023
1 parent 4730449 commit 4c59e8a
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ repository = "https://github.com/rustls/webpki-roots"

[dev-dependencies]
percent-encoding = "2.3"
rcgen = "0.11.1"
reqwest = { version = "0.11", features = ["rustls-tls-native-roots"] }
ring = "0.16.20"
rustls-pemfile = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
webpki = { package = "rustls-webpki", version = "0.101.2" }
x509-parser = "0.15.1"
151 changes: 150 additions & 1 deletion tests/verify.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,157 @@
use std::convert::TryFrom;

use webpki::{EndEntityCert, KeyUsage, SubjectNameRef, Time};
use rcgen::{BasicConstraints, Certificate, CertificateParams, DnType, IsCa, KeyUsagePurpose};
use webpki::{EndEntityCert, Error, KeyUsage, SubjectNameRef, Time};
use x509_parser::extensions::{GeneralName, NameConstraints as X509ParserNameConstraints};
use x509_parser::prelude::FromDer;

use webpki_roots::TLS_SERVER_ROOTS;

#[test]
fn name_constraints() {
for name_constraints in TLS_SERVER_ROOTS.iter().filter_map(|ta| ta.name_constraints) {
let time = Time::from_seconds_since_unix_epoch(0x40000000); // Time matching rcgen default.
let test_case = ConstraintTest::new(name_constraints);
let trust_anchors =
&[webpki::TrustAnchor::try_from_cert_der(&test_case.trust_anchor).unwrap()];

// Each permitted EE should verify without error.
for permitted_ee in test_case.permitted_certs {
webpki::EndEntityCert::try_from(permitted_ee.as_slice())
.unwrap()
.verify_for_usage(
ALL_ALGORITHMS,
trust_anchors,
&[],
time,
KeyUsage::server_auth(),
&[],
)
.unwrap();
}

// Each forbidden EE should fail to verify with the expected name constraint error.
for forbidden_ee in test_case.forbidden_certs {
let result = webpki::EndEntityCert::try_from(forbidden_ee.as_slice())
.unwrap()
.verify_for_usage(
ALL_ALGORITHMS,
trust_anchors,
&[],
time,
KeyUsage::server_auth(),
&[],
);
assert!(matches!(result, Err(Error::NameConstraintViolation)));
}
}
}

struct ConstraintTest {
trust_anchor: Vec<u8>,
permitted_certs: Vec<Vec<u8>>,
forbidden_certs: Vec<Vec<u8>>,
}

impl ConstraintTest {
fn new(webpki_name_constraints: &[u8]) -> Self {
// Create a trust anchor CA certificate that has the name constraints we want to test.
let mut trust_anchor = CertificateParams::new([]);
trust_anchor
.distinguished_name
.push(DnType::CommonName, "Name Constraint Test CA");
trust_anchor.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
trust_anchor.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::DigitalSignature,
];
let name_constraints = rcgen_name_constraints(webpki_name_constraints);
trust_anchor.name_constraints = Some(name_constraints.clone());
let trust_anchor = Certificate::from_params(trust_anchor).unwrap();

let certs_for_subtrees = |suffix| -> Vec<Vec<u8>> {
name_constraints
.permitted_subtrees
.iter()
.filter_map(|subtree| match subtree {
rcgen::GeneralSubtree::DnsName(dns_name) => Some(rcgen_ee_for_name(
format!("valid{}{}", dns_name, suffix),
&trust_anchor,
)),
_ => None,
})
.collect()
};

Self {
trust_anchor: trust_anchor.serialize_der().unwrap(),
// For each permitted subtree in the name constraints, issue an end entity certificate
// that contains a DNS name matching the permitted subtree base.
permitted_certs: certs_for_subtrees(""),
// For each permitted subtree in the name constraints, issue an end entity certificate
// that contains a DNS name that will **not** match the permitted subtree base.
forbidden_certs: certs_for_subtrees(".invalid"),
}
}
}

fn rcgen_ee_for_name(name: String, issuer: &Certificate) -> Vec<u8> {
let mut ee = CertificateParams::new(vec![name.clone()]);
ee.distinguished_name.push(DnType::CommonName, name);
ee.is_ca = IsCa::NoCa;
Certificate::from_params(ee)
.unwrap()
.serialize_der_with_signer(&issuer)
.unwrap()
}

/// Convert the webpki trust anchor DER encoding of name constraints to rcgen NameConstraints.
fn rcgen_name_constraints(der: &[u8]) -> rcgen::NameConstraints {
// x509 parser expects the outer SEQUENCE that the webpki trust anchor representation elides
// so wrap the DER up.
//
// Note: We take the cheap way out here and assume single byte length - if the following
// assert fails we'll need to more intelligently encode the sequence DER length.
assert!(der.len() < 0x80, "name constraint too long");
let wrapped_der = [&[0x30, der.len() as u8], der].concat();

// Constraints should parse with no trailing data.
let (trailing, constraints) = X509ParserNameConstraints::from_der(&wrapped_der).unwrap();
assert!(
trailing.is_empty(),
"unexpected trailing DER in name constraint"
);

// There should be at least one permitted subtree.
assert!(
!constraints.permitted_subtrees.is_none(),
"empty permitted subtrees in constraints"
);

// We don't expect any excluded subtrees as this time.
assert!(constraints.excluded_subtrees.is_none());

// Collect all of the DNS names from the x509-parser representation, mapping to the rcgen
// representation usable in cert parameters. We don't expect to find any other types of general
// name and x509-parser doesn't parse the subtree minimum and maximum (which we would assert to
// be missing for proper encoding anyway).
let permitted_subtrees = match constraints.permitted_subtrees {
None => Vec::default(),
Some(subtrees) => subtrees
.iter()
.map(|subtree| match &subtree.base {
GeneralName::DNSName(base) => rcgen::GeneralSubtree::DnsName(base.to_string()),
name => panic!("unexpected subtree base general name type: {}", name),
})
.collect(),
};

rcgen::NameConstraints {
permitted_subtrees,
excluded_subtrees: Vec::default(),
}
}

#[test]
fn tubitak_name_constraint_works() {
let root = include_bytes!("data/tubitak/root.der");
Expand Down

0 comments on commit 4c59e8a

Please sign in to comment.