From 82a1e42a5f8329402f772eab023eb9da4e05e016 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Tue, 8 Aug 2023 20:31:54 -0400 Subject: [PATCH] tests: add name constraint integration test. 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. --- Cargo.toml | 2 + tests/verify.rs | 151 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b3ea2ea..6185913 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/tests/verify.rs b/tests/verify.rs index 967295b..49e4682 100644 --- a/tests/verify.rs +++ b/tests/verify.rs @@ -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, + permitted_certs: Vec>, + forbidden_certs: Vec>, +} + +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> { + 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 { + 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");