use super::{Admission, NameSpace};
use crate::constraints::NameConstraints;
use crate::fixtures::{
CertificateParts, cat, context, extension, ia5, oid as oid_of, printable, seq, set, tlv, utf8,
};
use crate::{Certificate, oid};
use alloc::vec::Vec;
use structured_asn1::Parser;
fn named(common: &str) -> Vec<u8> {
seq(&[set(&[seq(&[oid_of(oid::COMMON_NAME), printable(common)])])])
}
fn organization(name: &str) -> Vec<u8> {
seq(&[set(&[seq(&[oid_of(oid::ORGANIZATION), printable(name)])])])
}
fn under(organization: &str, common: &str) -> Vec<u8> {
seq(&[
set(&[seq(&[oid_of(oid::ORGANIZATION), printable(organization)])]),
set(&[seq(&[oid_of(oid::COMMON_NAME), printable(common)])]),
])
}
fn subtree(base: Vec<u8>) -> Vec<u8> {
seq(&[base])
}
fn of<'a>(certificate: &Certificate<'a>) -> NameConstraints<'a> {
certificate
.tbs()
.extensions()
.name_constraints(certificate.tbs().extensions().memory())
.expect("the extension this test wrote")
.expect("a value this test wrote")
}
fn constraints(permitted: &[Vec<u8>], excluded: &[Vec<u8>]) -> Vec<u8> {
let mut parts = Vec::new();
if !permitted.is_empty() {
parts.push(context(0, permitted));
}
if !excluded.is_empty() {
parts.push(context(1, excluded));
}
seq(&parts)
}
fn mail(address: &str) -> Vec<u8> {
tlv(0x81, address.as_bytes())
}
fn dns(host: &str) -> Vec<u8> {
tlv(0x82, host.as_bytes())
}
fn directory(name: Vec<u8>) -> Vec<u8> {
context(4, &[name])
}
fn uri(location: &str) -> Vec<u8> {
tlv(0x86, location.as_bytes())
}
fn address(octets: &[u8]) -> Vec<u8> {
tlv(0x87, octets)
}
fn registered() -> Vec<u8> {
tlv(0x88, oid::ANY_POLICY.as_bytes())
}
fn other() -> Vec<u8> {
tlv(
0xa0,
&cat(&[oid_of(oid::ANY_POLICY), context(0, &[utf8("x")])]),
)
}
fn admission(
permitted: &[Vec<u8>],
excluded: &[Vec<u8>],
subject: Vec<u8>,
alternatives: &[Vec<u8>],
) -> Admission {
let mut extensions = alloc::vec![extension(
oid::NAME_CONSTRAINTS,
true,
&constraints(permitted, excluded),
)];
if !alternatives.is_empty() {
extensions.push(extension(oid::SUBJECT_ALT_NAME, true, &seq(alternatives)));
}
let der = CertificateParts {
subject,
extensions: Some(seq(&extensions)),
..CertificateParts::default()
}
.build();
let parser = Parser::der(crate::fixtures::memory());
let certificate = Certificate::read(&parser, &der, 0).expect("a certificate this test wrote");
let read = certificate
.tbs()
.extensions()
.name_constraints(certificate.tbs().extensions().memory())
.expect("the extension this test wrote")
.expect("a value this test wrote");
let names = certificate
.tbs()
.extensions()
.subject_alt_name(certificate.tbs().extensions().memory())
.expect("a value this test wrote")
.unwrap_or_else(|| parser.memory().vector());
let mut space = NameSpace::new(parser.memory());
space.narrow(read).unwrap();
space
.admits(certificate.tbs().subject(), &names, &(), parser.memory())
.unwrap()
}
#[test]
fn a_space_nobody_narrowed_admits_every_name() {
let space = NameSpace::new(&crate::fixtures::memory());
assert!(space.is_open());
assert_eq!(space.regions(), 0);
let der = CertificateParts::default().build();
let parser = Parser::der(crate::fixtures::memory());
let certificate = Certificate::read(&parser, &der, 0).unwrap();
assert_eq!(
space
.admits(certificate.tbs().subject(), &[], &(), parser.memory())
.unwrap(),
Admission::Admitted
);
}
#[test]
fn constraint_dispatch_is_admitted_before_a_short_name_comparison() {
use structured_asn1::memory::{Memory, MemoryAccount};
let der = CertificateParts {
subject: seq(&[]),
extensions: Some(seq(&[
extension(
oid::NAME_CONSTRAINTS,
true,
&constraints(&[subtree(dns("example.ua"))], &[]),
),
extension(oid::SUBJECT_ALT_NAME, true, &seq(&[dns("example.ua")])),
])),
..CertificateParts::default()
}
.build();
let parser = Parser::der(crate::fixtures::memory());
let certificate = Certificate::read(&parser, &der, 0).unwrap();
let names = certificate
.tbs()
.extensions()
.subject_alt_name(parser.memory())
.unwrap()
.unwrap();
let mut space = NameSpace::new(parser.memory());
space.narrow(of(&certificate)).unwrap();
let execution = Memory::new(alloc::sync::Arc::new(MemoryAccount::new(0, 1)));
let error = space
.admits(certificate.tbs().subject(), &names, &(), &execution)
.unwrap_err();
assert!(matches!(error, crate::error::CallbackError::Processing(error) if error.is_resource()));
assert_eq!(execution.work_used(), 1);
}
#[test]
fn constraint_bytes_are_admitted_before_a_retained_dns_comparison() {
use structured_asn1::memory::{Memory, MemoryAccount};
let der = CertificateParts {
subject: seq(&[]),
extensions: Some(seq(&[
extension(
oid::NAME_CONSTRAINTS,
true,
&constraints(&[subtree(dns("example.ua"))], &[]),
),
extension(oid::SUBJECT_ALT_NAME, true, &seq(&[dns("example.ua")])),
])),
..CertificateParts::default()
}
.build();
let parser = Parser::der(crate::fixtures::memory());
let certificate = Certificate::read(&parser, &der, 0).unwrap();
let names = certificate
.tbs()
.extensions()
.subject_alt_name(parser.memory())
.unwrap()
.unwrap();
let mut space = NameSpace::new(parser.memory());
space.narrow(of(&certificate)).unwrap();
let execution = Memory::new(alloc::sync::Arc::new(MemoryAccount::new(0, 2)));
let error = space
.admits(certificate.tbs().subject(), &names, &(), &execution)
.unwrap_err();
assert!(matches!(error, crate::error::CallbackError::Processing(error) if error.is_resource()));
assert_eq!(execution.work_used(), 2);
}
#[test]
fn a_name_below_the_permitted_one_is_admitted_and_one_beside_it_is_not() {
let base = subtree(directory(organization("Example")));
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
under("Example", "Leaf"),
&[]
),
Admission::Admitted
);
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
organization("Example"),
&[]
),
Admission::Admitted
);
assert_eq!(
admission(&[base], &[], under("Other", "Leaf"), &[]),
Admission::NotPermitted
);
}
#[test]
fn an_excluded_name_is_refused_however_widely_it_was_permitted() {
let permitted = subtree(directory(organization("Example")));
let excluded = subtree(directory(under("Example", "Secret")));
assert_eq!(
admission(&[permitted], &[excluded], under("Example", "Secret"), &[]),
Admission::Excluded
);
}
#[test]
fn a_constraint_of_one_form_says_nothing_about_a_name_of_another() {
assert_eq!(
admission(
&[subtree(mail("example.com"))],
&[],
named("Example"),
&[dns("host.example.org")]
),
Admission::Admitted
);
}
#[test]
fn a_domain_region_holds_what_is_under_it_and_not_what_merely_ends_with_it() {
let base = subtree(dns("example.com"));
for host in ["example.com", "host.example.com", "a.b.example.com"] {
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
named("Leaf"),
&[dns(host)]
),
Admission::Admitted,
"{host}"
);
}
for host in ["notexample.com", "example.com.evil.net", "example.co"] {
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
named("Leaf"),
&[dns(host)]
),
Admission::NotPermitted,
"{host}"
);
}
}
#[test]
fn a_domain_region_is_read_without_regard_to_case() {
assert_eq!(
admission(
&[subtree(dns("Example.COM"))],
&[],
named("Leaf"),
&[dns("HOST.example.com")]
),
Admission::Admitted
);
}
#[test]
fn a_mail_region_naming_a_host_does_not_hold_the_hosts_below_it() {
assert_eq!(
admission(
&[subtree(mail("example.com"))],
&[],
named("Leaf"),
&[mail("someone@example.com")]
),
Admission::Admitted
);
assert_eq!(
admission(
&[subtree(mail("example.com"))],
&[],
named("Leaf"),
&[mail("someone@mail.example.com")]
),
Admission::NotPermitted
);
assert_eq!(
admission(
&[subtree(mail(".example.com"))],
&[],
named("Leaf"),
&[mail("someone@mail.example.com")]
),
Admission::Admitted
);
assert_eq!(
admission(
&[subtree(mail(".example.com"))],
&[],
named("Leaf"),
&[mail("someone@example.com")]
),
Admission::NotPermitted
);
}
#[test]
fn a_mail_region_naming_one_mailbox_holds_that_mailbox_and_no_other() {
let base = subtree(mail("Someone@Example.com"));
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
named("Leaf"),
&[mail("Someone@EXAMPLE.COM")]
),
Admission::Admitted
);
assert_eq!(
admission(&[base], &[], named("Leaf"), &[mail("someone@example.com")]),
Admission::NotPermitted
);
}
#[test]
fn a_mailbox_written_into_the_subject_is_held_to_the_mail_regions() {
let subject = seq(&[set(&[seq(&[
oid_of(oid::EMAIL_ADDRESS),
ia5("someone@elsewhere.net"),
])])]);
assert_eq!(
admission(&[subtree(mail("example.com"))], &[], subject, &[]),
Admission::NotPermitted
);
}
#[test]
fn a_location_is_held_by_its_host_and_nothing_else_it_names() {
let base = subtree(uri(".example.com"));
for location in [
"http://host.example.com/path",
"https://user@host.example.com:443/path",
"https://a.b.example.com/",
] {
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
named("Leaf"),
&[uri(location)]
),
Admission::Admitted,
"{location}"
);
}
for location in [
"http://evil.net/host.example.com",
"http://host.example.com@evil.net/",
"http://host.example.com.evil.net/",
] {
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
named("Leaf"),
&[uri(location)]
),
Admission::NotPermitted,
"{location}"
);
}
}
#[test]
fn a_location_that_names_no_server_is_in_no_host_region() {
assert_eq!(
admission(
&[subtree(uri(".example.com"))],
&[],
named("Leaf"),
&[uri("urn:a//host.example.com/x")]
),
Admission::Unenforceable
);
}
#[test]
fn a_location_host_written_to_the_root_is_the_same_host() {
assert_eq!(
admission(
&[subtree(uri("example.com"))],
&[],
named("Leaf"),
&[uri("https://example.com./")]
),
Admission::Admitted
);
assert_eq!(
admission(
&[subtree(uri(".example.com"))],
&[],
named("Leaf"),
&[uri("https://host.example.com./")]
),
Admission::Admitted
);
}
#[test]
fn an_address_is_in_a_network_when_the_masked_bytes_agree() {
let base = subtree(address(&[192, 0, 2, 0, 255, 255, 255, 0]));
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
named("Leaf"),
&[address(&[192, 0, 2, 77])]
),
Admission::Admitted
);
assert_eq!(
admission(
core::slice::from_ref(&base),
&[],
named("Leaf"),
&[address(&[192, 0, 3, 77])]
),
Admission::NotPermitted
);
assert_eq!(
admission(
&[base],
&[],
named("Leaf"),
&[address(&[
0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
])]
),
Admission::NotPermitted
);
}
#[test]
fn a_certificate_with_alternative_names_is_not_held_to_its_legacy_mailbox() {
let subject = seq(&[set(&[seq(&[
oid_of(oid::EMAIL_ADDRESS),
ia5("someone@elsewhere.net"),
])])]);
assert_eq!(
admission(
&[subtree(mail("example.com"))],
&[],
subject,
&[mail("someone@example.com")]
),
Admission::Admitted
);
}
#[test]
fn a_location_a_uri_constraint_cannot_be_applied_to_lets_no_path_through() {
for location in [
"urn:example:value",
"http://192.0.2.1/x",
"http://[2001:db8::1]/x",
] {
assert_eq!(
admission(
&[],
&[subtree(uri(".example.com"))],
named("Leaf"),
&[uri(location)]
),
Admission::Unenforceable,
"excluded: {location}"
);
assert_eq!(
admission(
&[subtree(uri(".example.com"))],
&[],
named("Leaf"),
&[uri(location)]
),
Admission::Unenforceable,
"permitted: {location}"
);
}
}
#[test]
fn an_address_of_the_other_family_is_outside_a_network_rather_than_unreadable() {
let excluded = subtree(address(&[192, 0, 2, 0, 255, 255, 255, 0]));
let six = address(&[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
assert_eq!(
admission(&[], &[excluded], named("Leaf"), core::slice::from_ref(&six)),
Admission::Admitted
);
let permitted = subtree(address(&[192, 0, 2, 0, 255, 255, 255, 0]));
assert_eq!(
admission(&[permitted], &[], named("Leaf"), &[six]),
Admission::NotPermitted
);
}
#[test]
fn a_region_of_a_form_this_crate_does_not_match_lets_no_path_through() {
assert_eq!(
admission(
&[subtree(registered())],
&[],
named("Leaf"),
&[registered()]
),
Admission::Unenforceable
);
assert_eq!(
admission(&[subtree(other())], &[], named("Leaf"), &[other()]),
Admission::Unenforceable
);
}
#[test]
fn a_region_of_an_unmatched_form_still_says_nothing_about_other_forms() {
assert_eq!(
admission(
&[subtree(registered())],
&[],
named("Leaf"),
&[dns("host.example.com")]
),
Admission::Admitted
);
}
#[test]
fn every_authority_that_spoke_has_to_admit_the_name() {
let der = CertificateParts {
subject: under("Example", "Leaf"),
..CertificateParts::default()
}
.build();
let parser = Parser::der(crate::fixtures::memory());
let certificate = Certificate::read(&parser, &der, 0).unwrap();
let wide = extension(
oid::NAME_CONSTRAINTS,
true,
&constraints(&[subtree(directory(organization("Example")))], &[]),
);
let narrow = extension(
oid::NAME_CONSTRAINTS,
true,
&constraints(&[subtree(directory(organization("Elsewhere")))], &[]),
);
let carrying = |value: Vec<u8>| {
CertificateParts {
extensions: Some(seq(&[value])),
..CertificateParts::default()
}
.build()
};
let wide_der = carrying(wide);
let narrow_der = carrying(narrow);
let wide = Certificate::read(&parser, &wide_der, 0).unwrap();
let narrow = Certificate::read(&parser, &narrow_der, 0).unwrap();
let mut space = NameSpace::new(parser.memory());
space.narrow(of(&wide)).unwrap();
assert_eq!(
space
.admits(certificate.tbs().subject(), &[], &(), parser.memory())
.unwrap(),
Admission::Admitted
);
space.narrow(of(&narrow)).unwrap();
assert_eq!(space.regions(), 2);
assert_eq!(
space
.admits(certificate.tbs().subject(), &[], &(), parser.memory())
.unwrap(),
Admission::NotPermitted
);
}
#[test]
fn an_empty_subject_carries_no_identity_to_hold_to_the_space() {
let empty = seq(&[]);
assert_eq!(
admission(
&[subtree(directory(organization("Example")))],
&[],
empty,
&[dns("host.example.com")]
),
Admission::Admitted
);
}
#[test]
fn a_legacy_mailbox_that_is_not_one_lets_no_path_through() {
for value in [
"user@example.com@evil.net",
"notamailbox",
"@example.com",
"someone@",
] {
let subject = seq(&[set(&[seq(&[oid_of(oid::EMAIL_ADDRESS), ia5(value)])])]);
assert_eq!(
admission(&[], &[subtree(mail("example.com"))], subject, &[]),
Admission::Unenforceable,
"excluded: {value}"
);
let subject = seq(&[set(&[seq(&[oid_of(oid::EMAIL_ADDRESS), ia5(value)])])]);
assert_eq!(
admission(&[subtree(mail("evil.net"))], &[], subject, &[]),
Admission::Unenforceable,
"permitted: {value}"
);
}
}