203 lines
7.6 KiB
Rust
203 lines
7.6 KiB
Rust
use core::time::Duration;
|
|
use std::convert::TryFrom;
|
|
|
|
use pki_types::{CertificateDer, ServerName, SignatureVerificationAlgorithm, UnixTime};
|
|
use rcgen::{
|
|
BasicConstraints, CertificateParams, CertifiedIssuer, DnType, IsCa, Issuer, KeyPair,
|
|
KeyUsagePurpose,
|
|
};
|
|
use webpki::{anchor_from_trusted_cert, EndEntityCert, Error, KeyUsage};
|
|
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.as_ref())
|
|
{
|
|
let time = UnixTime::since_unix_epoch(Duration::from_secs(0x40000000)); // Time matching rcgen default.
|
|
let test_case = ConstraintTest::new(name_constraints.as_ref());
|
|
let trust_anchors = &[anchor_from_trusted_cert(&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)
|
|
.unwrap()
|
|
.verify_for_usage(
|
|
ALL_ALGORITHMS,
|
|
trust_anchors,
|
|
&[],
|
|
time,
|
|
KeyUsage::server_auth(),
|
|
None,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
// Each forbidden EE should fail to verify with the expected name constraint error.
|
|
for forbidden_ee in test_case.forbidden_certs {
|
|
let ee = EndEntityCert::try_from(&forbidden_ee).unwrap();
|
|
let result = ee.verify_for_usage(
|
|
ALL_ALGORITHMS,
|
|
trust_anchors,
|
|
&[],
|
|
time,
|
|
KeyUsage::server_auth(),
|
|
None,
|
|
None,
|
|
);
|
|
assert!(matches!(result, Err(Error::NameConstraintViolation)));
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ConstraintTest {
|
|
trust_anchor: CertificateDer<'static>,
|
|
permitted_certs: Vec<CertificateDer<'static>>,
|
|
forbidden_certs: Vec<CertificateDer<'static>>,
|
|
}
|
|
|
|
impl ConstraintTest {
|
|
fn new(webpki_name_constraints: &[u8]) -> Self {
|
|
let name_constraints = rcgen_name_constraints(webpki_name_constraints);
|
|
|
|
// Create a trust anchor CA certificate that has the name constraints we want to test.
|
|
let trust_anchor = {
|
|
let mut params = CertificateParams::new([]).unwrap();
|
|
params
|
|
.distinguished_name
|
|
.push(DnType::CommonName, "Name Constraint Test CA");
|
|
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
|
params.key_usages = vec![
|
|
KeyUsagePurpose::KeyCertSign,
|
|
KeyUsagePurpose::DigitalSignature,
|
|
];
|
|
params.name_constraints = Some(name_constraints.clone());
|
|
|
|
let key = KeyPair::generate().unwrap();
|
|
CertifiedIssuer::self_signed(params, key).unwrap()
|
|
};
|
|
|
|
let certs_for_subtrees = |suffix| {
|
|
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 {
|
|
// 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"),
|
|
trust_anchor: trust_anchor.der().to_owned(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rcgen_ee_for_name(name: String, issuer: &Issuer<'_, KeyPair>) -> CertificateDer<'static> {
|
|
let mut ee = CertificateParams::new(vec![name.clone()]).unwrap();
|
|
ee.distinguished_name.push(DnType::CommonName, name);
|
|
ee.is_ca = IsCa::NoCa;
|
|
let key_pair = KeyPair::generate().unwrap();
|
|
ee.signed_by(&key_pair, issuer).unwrap().into()
|
|
}
|
|
|
|
/// 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.
|
|
let wrapped_der = yasna::construct_der(|writer| {
|
|
writer.write_sequence(|writer| {
|
|
writer.next().write_der(der);
|
|
})
|
|
});
|
|
|
|
// 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_some(),
|
|
"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 = CertificateDer::from(&include_bytes!("data/tubitak/root.der")[..]);
|
|
let inter = CertificateDer::from(&include_bytes!("data/tubitak/inter.der")[..]);
|
|
let subj = CertificateDer::from(&include_bytes!("data/tubitak/subj.der")[..]);
|
|
|
|
let roots = [anchor_from_trusted_cert(&root).unwrap().to_owned()];
|
|
let now = UnixTime::since_unix_epoch(Duration::from_secs(1493668479));
|
|
let cert = EndEntityCert::try_from(&subj).unwrap();
|
|
cert.verify_for_usage(
|
|
ALL_ALGORITHMS,
|
|
&roots,
|
|
&[inter, root],
|
|
now,
|
|
KeyUsage::server_auth(),
|
|
None,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let subject = ServerName::try_from("testssl.kamusm.gov.tr").unwrap();
|
|
cert.verify_is_valid_for_subject_name(&subject).unwrap();
|
|
}
|
|
|
|
static ALL_ALGORITHMS: &[&dyn SignatureVerificationAlgorithm] = &[
|
|
webpki::aws_lc_rs::ECDSA_P256_SHA256,
|
|
webpki::aws_lc_rs::ECDSA_P256_SHA384,
|
|
webpki::aws_lc_rs::ECDSA_P384_SHA256,
|
|
webpki::aws_lc_rs::ECDSA_P384_SHA384,
|
|
webpki::aws_lc_rs::RSA_PKCS1_2048_8192_SHA256,
|
|
webpki::aws_lc_rs::RSA_PKCS1_2048_8192_SHA384,
|
|
webpki::aws_lc_rs::RSA_PKCS1_2048_8192_SHA512,
|
|
webpki::aws_lc_rs::RSA_PKCS1_3072_8192_SHA384,
|
|
webpki::aws_lc_rs::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
|
|
webpki::aws_lc_rs::RSA_PSS_2048_8192_SHA384_LEGACY_KEY,
|
|
webpki::aws_lc_rs::RSA_PSS_2048_8192_SHA512_LEGACY_KEY,
|
|
];
|