use super::crls::CrlStore;
use super::store::{RootCertStore, TrustAnchor};
use crate::signature_registry::{SignaturePolicy, find_by_oid};
use crate::tls::Error;
use crate::x509::{AnyPublicKey, Certificate, Time, Validity, oid};
use alloc::vec::Vec;
const KU_DIGITAL_SIGNATURE: u16 = 0x80; const KU_KEY_CERT_SIGN: u16 = 0x04;
const MAX_CHAIN_LEN: usize = 10;
#[allow(dead_code)] pub(crate) fn verify_chain(
store: &RootCertStore,
chain: &[Vec<u8>],
now: Option<&Time>,
policy: &SignaturePolicy,
) -> Result<AnyPublicKey, Error> {
verify_chain_for_purpose(store, chain, now, policy, ChainPurpose::Server)
}
pub(crate) fn verify_chain_with_crls(
store: &RootCertStore,
crls: &CrlStore,
chain: &[Vec<u8>],
now: Option<&Time>,
policy: &SignaturePolicy,
) -> Result<AnyPublicKey, Error> {
verify_chain_with_crls_for_purpose(store, crls, chain, now, policy, ChainPurpose::Server)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ChainPurpose {
Server,
Client,
}
#[allow(dead_code)] pub(crate) fn verify_chain_for_purpose(
store: &RootCertStore,
chain: &[Vec<u8>],
now: Option<&Time>,
policy: &SignaturePolicy,
purpose: ChainPurpose,
) -> Result<AnyPublicKey, Error> {
let empty = CrlStore::new();
verify_chain_with_crls_for_purpose(store, &empty, chain, now, policy, purpose)
}
pub(crate) fn verify_chain_with_crls_for_purpose(
store: &RootCertStore,
crls: &CrlStore,
chain: &[Vec<u8>],
now: Option<&Time>,
policy: &SignaturePolicy,
purpose: ChainPurpose,
) -> Result<AnyPublicKey, Error> {
if chain.is_empty() {
return Err(Error::BadCertificate);
}
if chain.len() > MAX_CHAIN_LEN {
return Err(Error::BadCertificate);
}
let certs: Vec<Certificate> = chain
.iter()
.map(|der| Certificate::from_der(der.clone()))
.collect::<Result<_, _>>()
.map_err(|_| Error::BadCertificate)?;
let mut anchor_at: Option<usize> = None;
let mut matched_anchor: Option<&TrustAnchor> = None;
for i in 0..certs.len() {
let issuer_der = certs[i].issuer_der().map_err(|_| Error::BadCertificate)?;
let mut anchored: Option<&TrustAnchor> = None;
for anchor in store.anchors_with_subject(issuer_der) {
if verify_cert_against_issuer(&certs[i], &anchor.key, policy).is_ok() {
anchored = Some(anchor);
break;
}
}
if let Some(anchor) = anchored {
check_revocation(&certs[i], &anchor.key, crls, now, policy)?;
anchor_at = Some(i + 1);
matched_anchor = Some(anchor);
break;
}
let Some(issuer) = certs.get(i + 1) else {
return Err(Error::BadCertificate);
};
let issuer_key = issuer
.subject_public_key()
.map_err(|_| Error::BadCertificate)?;
verify_cert_against_issuer(&certs[i], &issuer_key, policy)?;
if names_differ(&certs[i], issuer)? {
return Err(Error::BadCertificate);
}
check_revocation(&certs[i], &issuer_key, crls, now, policy)?;
}
let anchor_at = anchor_at.ok_or(Error::BadCertificate)?;
let path = &certs[..anchor_at];
for cert in path {
cert.check_signature_algid_consistent()
.map_err(|_| Error::BadCertificate)?;
check_critical_extensions_recognized(cert)?;
}
if let Some(now) = now {
for cert in path {
let validity = cert.validity().map_err(|_| Error::BadCertificate)?;
if !validity.accepts(now) {
return Err(Error::BadCertificate);
}
}
}
let anchor_nc = matched_anchor.and_then(|a| a.name_constraints.as_ref());
enforce_name_constraints(path, anchor_nc)?;
enforce_constraints(path, purpose)?;
certs[0]
.subject_public_key()
.map_err(|_| Error::BadCertificate)
}
fn check_revocation(
cert: &Certificate,
issuer_key: &AnyPublicKey,
crls: &CrlStore,
now: Option<&Time>,
policy: &SignaturePolicy,
) -> Result<(), Error> {
let cert_issuer = cert.issuer_der().map_err(|_| Error::BadCertificate)?;
let serial = cert.serial_bytes().map_err(|_| Error::BadCertificate)?;
let issuer_spki = issuer_key.to_spki_der();
for crl in crls.crls_with_issuer(cert_issuer) {
let Ok(crl_sig_alg) = crl.signature_algorithm_oid() else {
continue;
};
let Some(crl_algo) = find_by_oid(&crl_sig_alg) else {
continue;
};
if !policy.permits(crl_algo, &issuer_spki) {
continue;
}
if crl.verify_signature_with(issuer_key).is_err() {
continue;
}
if let Some(n) = now {
let this_update = crl.this_update().map_err(|_| Error::BadCertificate)?;
let next_update = crl.next_update().map_err(|_| Error::BadCertificate)?;
let covers = match next_update {
Some(na) => Validity::new(this_update.clone(), na).accepts(n),
None => true,
};
if !covers {
continue;
}
}
let revoked = crl.is_revoked(serial).map_err(|_| Error::BadCertificate)?;
if revoked {
return Err(Error::BadCertificate);
}
}
Ok(())
}
fn verify_cert_against_issuer(
cert: &Certificate,
issuer_key: &AnyPublicKey,
policy: &SignaturePolicy,
) -> Result<(), Error> {
let sig_alg = cert
.signature_algorithm_oid()
.map_err(|_| Error::BadCertificate)?;
let algo = find_by_oid(&sig_alg).ok_or(Error::BadCertificate)?;
let issuer_spki = issuer_key.to_spki_der();
if !policy.permits(algo, &issuer_spki) {
return Err(Error::BadCertificate);
}
cert.verify_signature_with(issuer_key)
.map_err(|_| Error::BadCertificate)
}
fn enforce_constraints(certs: &[Certificate], purpose: ChainPurpose) -> Result<(), Error> {
for (i, cert) in certs.iter().enumerate().skip(1) {
let bc = cert
.basic_constraints()
.map_err(|_| Error::BadCertificate)?
.ok_or(Error::BadCertificate)?;
if !bc.0 {
return Err(Error::BadCertificate);
}
if let Some(mask) = cert.key_usage().map_err(|_| Error::BadCertificate)?
&& (mask & KU_KEY_CERT_SIGN) == 0
{
return Err(Error::BadCertificate);
}
if let Some(plc) = bc.1 {
let intermediates_below = i.saturating_sub(1);
if (plc as usize) < intermediates_below {
return Err(Error::BadCertificate);
}
}
}
let leaf = &certs[0];
if let Some(mask) = leaf.key_usage().map_err(|_| Error::BadCertificate)?
&& (mask & KU_DIGITAL_SIGNATURE) == 0
{
return Err(Error::BadCertificate);
}
let ekus = leaf
.extended_key_usages()
.map_err(|_| Error::BadCertificate)?;
let required = match purpose {
ChainPurpose::Server => oid::ID_KP_SERVER_AUTH,
ChainPurpose::Client => oid::ID_KP_CLIENT_AUTH,
};
if !ekus.is_empty() && !ekus.iter().any(|o| o.as_slice() == required) {
return Err(Error::BadCertificate);
}
Ok(())
}
fn enforce_name_constraints(
certs: &[Certificate],
anchor_constraints: Option<&crate::x509::NameConstraints>,
) -> Result<(), Error> {
let mut ca_constraints: Vec<Option<crate::x509::NameConstraints>> =
Vec::with_capacity(certs.len());
ca_constraints.push(None); for cert in certs.iter().skip(1) {
ca_constraints.push(cert.name_constraints().map_err(|_| Error::BadCertificate)?);
}
let mut in_scope: Vec<&crate::x509::NameConstraints> = Vec::new();
if let Some(nc) = anchor_constraints {
in_scope.push(nc);
}
for idx in (0..certs.len()).rev() {
if !in_scope.is_empty() {
enforce_constraints_on_cert(&certs[idx], &in_scope)?;
}
if let Some(nc) = &ca_constraints[idx] {
in_scope.push(nc);
}
}
Ok(())
}
fn enforce_constraints_on_cert(
cert: &Certificate,
active: &[&crate::x509::NameConstraints],
) -> Result<(), Error> {
let mut dns = cert
.subject_alt_names()
.map_err(|_| Error::BadCertificate)?;
let ips = cert.subject_alt_ips().map_err(|_| Error::BadCertificate)?;
if dns.is_empty()
&& let Some(cn) = cert
.subject()
.map_err(|_| Error::BadCertificate)?
.common_name
&& cn_is_plausible_dns_name(&cn)
{
dns.push(cn);
}
if dns.is_empty() && ips.is_empty() {
let any_permitted = active
.iter()
.any(|nc| !nc.permitted_dns.is_empty() || !nc.permitted_ip.is_empty());
if any_permitted {
return Err(Error::BadCertificate);
}
return Ok(());
}
for nc in active {
for name in &dns {
for base in &nc.excluded_dns {
if dns_in_subtree(name, base) {
return Err(Error::BadCertificate);
}
}
}
for ip in &ips {
let bytes = match ip {
crate::x509::SanIp::V4(b) => &b[..],
crate::x509::SanIp::V6(b) => &b[..],
};
for (addr, mask) in &nc.excluded_ip {
if ip_in_subtree(bytes, addr, mask) {
return Err(Error::BadCertificate);
}
}
}
if !nc.permitted_dns.is_empty() {
for name in &dns {
if !nc
.permitted_dns
.iter()
.any(|base| dns_in_subtree(name, base))
{
return Err(Error::BadCertificate);
}
}
}
if !nc.permitted_ip.is_empty() {
for ip in &ips {
let bytes = match ip {
crate::x509::SanIp::V4(b) => &b[..],
crate::x509::SanIp::V6(b) => &b[..],
};
if !nc
.permitted_ip
.iter()
.any(|(addr, mask)| ip_in_subtree(bytes, addr, mask))
{
return Err(Error::BadCertificate);
}
}
}
}
Ok(())
}
fn dns_in_subtree(name: &str, base: &str) -> bool {
let name_l = name.to_ascii_lowercase();
let base_l = base.to_ascii_lowercase();
if let Some(suffix) = base_l.strip_prefix('.') {
if name_l.len() <= suffix.len() {
return false;
}
let cut = name_l.len() - suffix.len();
return name_l.as_bytes()[cut - 1] == b'.' && name_l[cut..] == *suffix;
}
if name_l == base_l {
return true;
}
if name_l.len() > base_l.len() {
let cut = name_l.len() - base_l.len();
return name_l.as_bytes()[cut - 1] == b'.' && name_l[cut..] == base_l;
}
false
}
fn ip_in_subtree(host: &[u8], addr: &[u8], mask: &[u8]) -> bool {
if host.len() != addr.len() || host.len() != mask.len() {
return false;
}
for i in 0..host.len() {
if (host[i] & mask[i]) != (addr[i] & mask[i]) {
return false;
}
}
true
}
fn check_critical_extensions_recognized(cert: &Certificate) -> Result<(), Error> {
let critical = cert
.critical_extension_oids()
.map_err(|_| Error::BadCertificate)?;
for o in critical {
let bytes = o.as_slice();
if bytes == oid::BASIC_CONSTRAINTS
|| bytes == oid::KEY_USAGE
|| bytes == oid::EXT_KEY_USAGE
|| bytes == oid::SUBJECT_ALT_NAME
{
continue;
}
if bytes == oid::NAME_CONSTRAINTS {
let nc = cert
.name_constraints()
.map_err(|_| Error::BadCertificate)?
.ok_or(Error::BadCertificate)?;
if nc.has_unenforceable_permitted || nc.has_unenforceable_excluded {
return Err(Error::BadCertificate);
}
continue;
}
return Err(Error::BadCertificate);
}
Ok(())
}
fn names_differ(cert: &Certificate, issuer: &Certificate) -> Result<bool, Error> {
let cert_issuer = cert.issuer_der().map_err(|_| Error::BadCertificate)?;
let issuer_subject = issuer.subject_der().map_err(|_| Error::BadCertificate)?;
Ok(cert_issuer != issuer_subject)
}
pub(crate) fn verify_hostname(cert: &Certificate, host: &str) -> Result<(), Error> {
if let Some(host_bytes) = parse_host_ip(host) {
let ips = cert.subject_alt_ips().map_err(|_| Error::BadCertificate)?;
let matched = ips.iter().any(|san| match (san, &host_bytes) {
(crate::x509::SanIp::V4(a), HostIp::V4(b)) => a == b,
(crate::x509::SanIp::V6(a), HostIp::V6(b)) => a == b,
_ => false,
});
return if matched {
Ok(())
} else {
Err(Error::BadCertificate)
};
}
let sans = cert
.subject_alt_names()
.map_err(|_| Error::BadCertificate)?;
let matched = if !sans.is_empty() {
sans.iter().any(|pattern| dns_name_matches(pattern, host))
} else {
cert.subject()
.map_err(|_| Error::BadCertificate)?
.common_name
.as_deref()
.map(|cn| dns_name_matches(cn, host))
.unwrap_or(false)
};
if matched {
Ok(())
} else {
Err(Error::BadCertificate)
}
}
enum HostIp {
V4([u8; 4]),
V6([u8; 16]),
}
fn parse_host_ip(host: &str) -> Option<HostIp> {
if !host.bytes().any(|b| b == b':') {
return crate::x509::cert::parse_ipv4(host).map(HostIp::V4);
}
parse_ipv6(host).map(HostIp::V6)
}
fn parse_ipv6(s: &str) -> Option<[u8; 16]> {
let (head, tail) = if let Some(idx) = s.find("::") {
let head = &s[..idx];
let tail = &s[idx + 2..];
if head.contains("::") || tail.contains("::") {
return None;
}
(head, tail)
} else {
(s, "")
};
let mut head_groups: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
let mut tail_groups: alloc::vec::Vec<u16> = alloc::vec::Vec::new();
for (target, src) in [(&mut head_groups, head), (&mut tail_groups, tail)] {
if src.is_empty() {
continue;
}
for group in src.split(':') {
if group.contains('.') {
let v4 = crate::x509::cert::parse_ipv4(group)?;
target.push(((v4[0] as u16) << 8) | v4[1] as u16);
target.push(((v4[2] as u16) << 8) | v4[3] as u16);
continue;
}
if group.is_empty() || group.len() > 4 {
return None;
}
let g = u16::from_str_radix(group, 16).ok()?;
target.push(g);
}
}
let total = head_groups.len() + tail_groups.len();
if total > 8 {
return None;
}
let zero_groups = 8 - total;
if zero_groups > 0 && !s.contains("::") {
return None;
}
let mut out = [0u8; 16];
let mut i = 0;
for g in head_groups
.into_iter()
.chain(core::iter::repeat_n(0, zero_groups))
.chain(tail_groups)
{
out[i] = (g >> 8) as u8;
out[i + 1] = (g & 0xff) as u8;
i += 2;
}
Some(out)
}
fn dns_name_matches(pattern: &str, host: &str) -> bool {
if looks_like_ip(pattern) || looks_like_ip(host) {
return false;
}
if let Some(suffix) = pattern.strip_prefix("*.") {
match host.split_once('.') {
Some((label, rest)) => {
!label.is_empty() && !rest.is_empty() && rest.eq_ignore_ascii_case(suffix)
}
None => false,
}
} else {
!pattern.is_empty() && pattern.eq_ignore_ascii_case(host)
}
}
fn cn_is_plausible_dns_name(cn: &str) -> bool {
!cn.is_empty() && cn.bytes().all(|b| (0x20..=0x7E).contains(&b)) && !looks_like_ip(cn)
}
fn looks_like_ip(s: &str) -> bool {
if s.bytes().any(|b| b == b':') {
return true;
}
let mut count = 0usize;
for label in s.split('.') {
count += 1;
if count > 4 {
return false;
}
if label.is_empty() || label.len() > 3 {
return false;
}
if !label.bytes().all(|b| b.is_ascii_digit()) {
return false;
}
}
count == 4
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{from_hex_vec, rsa_test_key_a, rsa_test_key_b};
use crate::x509::{Certificate, DistinguishedName, Time, Validity};
fn validity() -> Validity {
Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
)
}
fn policy() -> SignaturePolicy {
SignaturePolicy::modern()
}
#[test]
fn rfc8448_self_signed_anchor() {
let flight = from_hex_vec(include_str!(
"../../../testdata/rfc8448_server_flight_payload.hex"
));
let cert_der = flight[51..483].to_vec();
let mut store = RootCertStore::new();
store.add_der(cert_der.clone()).unwrap();
let relaxed = SignaturePolicy::modern().with_min_rsa_bits(1024);
let leaf_key = verify_chain(&store, &[cert_der], None, &relaxed).unwrap();
assert!(matches!(leaf_key, AnyPublicKey::Rsa(_)));
}
#[test]
fn two_cert_chain_to_root() {
let ca_key = rsa_test_key_a();
let leaf_key = rsa_test_key_b();
let ca_name = DistinguishedName::common_name("purecrypto Root");
let leaf_name = DistinguishedName::common_name("leaf.example");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let leaf = Certificate::issue(
&ca_key,
&ca_name,
&leaf_name,
&leaf_key.public_key(),
&validity(),
2,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
let policy = policy();
verify_chain(&store, &[leaf.to_der().to_vec()], Some(&now), &policy).unwrap();
verify_chain(
&store,
&[leaf.to_der().to_vec(), root.to_der().to_vec()],
Some(&now),
&policy,
)
.unwrap();
}
#[test]
fn rejects_untrusted_and_empty() {
let ca_key = rsa_test_key_a();
let ca_name = DistinguishedName::common_name("Untrusted Root");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let empty = RootCertStore::new();
let policy = policy();
assert!(matches!(
verify_chain(&empty, &[root.to_der().to_vec()], None, &policy),
Err(Error::BadCertificate)
));
assert!(matches!(
verify_chain(&empty, &[], None, &policy),
Err(Error::BadCertificate)
));
}
#[test]
fn rejects_broken_signature() {
let ca_key = rsa_test_key_a();
let leaf_key = rsa_test_key_b();
let ca_name = DistinguishedName::common_name("purecrypto Root");
let leaf_name = DistinguishedName::common_name("leaf.example");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let bogus = Certificate::issue(
&leaf_key,
&ca_name,
&leaf_name,
&leaf_key.public_key(),
&validity(),
3,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
assert!(matches!(
verify_chain(&store, &[bogus.to_der().to_vec()], None, &policy()),
Err(Error::BadCertificate)
));
}
#[test]
fn rejects_non_ca_as_intermediate() {
let ca_key = rsa_test_key_a();
let leaf_key = rsa_test_key_b();
let ca_name = DistinguishedName::common_name("purecrypto Root");
let leaf_name = DistinguishedName::common_name("leaf.example");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let bad_int = Certificate::issue(
&ca_key,
&ca_name,
&DistinguishedName::common_name("fake-intermediate"),
&leaf_key.public_key(),
&validity(),
2,
false,
)
.unwrap();
let leaf = Certificate::issue(
&leaf_key,
&DistinguishedName::common_name("fake-intermediate"),
&leaf_name,
&leaf_key.public_key(),
&validity(),
3,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let chain = alloc::vec![leaf.to_der().to_vec(), bad_int.to_der().to_vec()];
assert!(matches!(
verify_chain(&store, &chain, None, &policy()),
Err(Error::BadCertificate)
));
}
#[test]
fn anchors_at_first_trusted_ca_ignoring_cross_signed_tail() {
let root_key = rsa_test_key_a(); let other_key = rsa_test_key_b(); let root_name = DistinguishedName::common_name("SSL.com TLS ECC Root CA 2022");
let transit_name = DistinguishedName::common_name("SSL.com TLS Transit ECC CA R2");
let leaf_name = DistinguishedName::common_name("example.com");
let other_ca_name = DistinguishedName::common_name("AAA Certificate Services");
let transit = Certificate::issue(
&root_key,
&root_name,
&transit_name,
&other_key.public_key(),
&validity(),
2,
true,
)
.unwrap();
let leaf = Certificate::issue(
&other_key,
&transit_name,
&leaf_name,
&other_key.public_key(),
&validity(),
3,
false,
)
.unwrap();
let cross_root = Certificate::issue(
&other_key,
&other_ca_name,
&root_name,
&root_key.public_key(),
&validity(),
4,
true,
)
.unwrap();
let real_root =
Certificate::self_signed(&root_key, &root_name, &validity(), 1, true).unwrap();
let mut store = RootCertStore::new();
store.add_der(real_root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
let chain = alloc::vec![
leaf.to_der().to_vec(),
transit.to_der().to_vec(),
cross_root.to_der().to_vec(),
];
verify_chain(&store, &chain, Some(&now), &policy()).unwrap();
}
#[test]
fn ignores_expired_root_supplied_above_anchor() {
let root_key = rsa_test_key_a();
let int_key = rsa_test_key_b();
let root_name = DistinguishedName::common_name("Root");
let int_name = DistinguishedName::common_name("Intermediate");
let leaf_name = DistinguishedName::common_name("leaf.example");
let intermediate = Certificate::issue(
&root_key,
&root_name,
&int_name,
&int_key.public_key(),
&validity(),
2,
true,
)
.unwrap();
let leaf = Certificate::issue(
&int_key,
&int_name,
&leaf_name,
&int_key.public_key(),
&validity(),
3,
false,
)
.unwrap();
let expired_root = Certificate::self_signed(
&root_key,
&root_name,
&Validity::new(
Time::utc(2020, 1, 1, 0, 0, 0),
Time::utc(2021, 1, 1, 0, 0, 0),
),
1,
true,
)
.unwrap();
let valid_root =
Certificate::self_signed(&root_key, &root_name, &validity(), 1, true).unwrap();
let mut store = RootCertStore::new();
store.add_der(valid_root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
let chain = alloc::vec![
leaf.to_der().to_vec(),
intermediate.to_der().to_vec(),
expired_root.to_der().to_vec(),
];
verify_chain(&store, &chain, Some(&now), &policy()).unwrap();
}
#[test]
fn anchors_at_trusted_intermediate() {
let root_key = rsa_test_key_a();
let int_key = rsa_test_key_b();
let root_name = DistinguishedName::common_name("Root");
let int_name = DistinguishedName::common_name("Intermediate");
let leaf_name = DistinguishedName::common_name("leaf.example");
let intermediate = Certificate::issue(
&root_key,
&root_name,
&int_name,
&int_key.public_key(),
&validity(),
2,
true,
)
.unwrap();
let leaf = Certificate::issue(
&int_key,
&int_name,
&leaf_name,
&int_key.public_key(),
&validity(),
3,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(intermediate.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain(
&store,
&[leaf.to_der().to_vec(), intermediate.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
verify_chain(&store, &[leaf.to_der().to_vec()], Some(&now), &policy()).unwrap();
}
#[test]
fn rejects_expired_certificate() {
let ca_key = rsa_test_key_a();
let name = DistinguishedName::common_name("expired.example");
let past = Validity::new(
Time::utc(2020, 1, 1, 0, 0, 0),
Time::utc(2021, 1, 1, 0, 0, 0),
);
let cert = Certificate::self_signed(&ca_key, &name, &past, 1, true).unwrap();
let mut store = RootCertStore::new();
store.add_der(cert.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
let policy = policy();
assert!(matches!(
verify_chain(&store, &[cert.to_der().to_vec()], Some(&now), &policy),
Err(Error::BadCertificate)
));
verify_chain(&store, &[cert.to_der().to_vec()], None, &policy).unwrap();
}
#[test]
fn hostname_san_and_cn() {
let key = rsa_test_key_a();
let san_cert = Certificate::self_signed_with_sans(
&key,
&DistinguishedName::common_name("ignored"),
&validity(),
1,
false,
&["example.com", "*.svc.example.com"],
)
.unwrap();
verify_hostname(&san_cert, "example.com").unwrap();
verify_hostname(&san_cert, "api.svc.example.com").unwrap();
assert!(verify_hostname(&san_cert, "ignored").is_err()); assert!(verify_hostname(&san_cert, "other.com").is_err());
assert!(verify_hostname(&san_cert, "svc.example.com").is_err()); assert!(verify_hostname(&san_cert, "a.b.svc.example.com").is_err());
let cn_cert = Certificate::self_signed(
&key,
&DistinguishedName::common_name("host.example"),
&validity(),
2,
false,
)
.unwrap();
verify_hostname(&cn_cert, "HOST.example").unwrap(); assert!(verify_hostname(&cn_cert, "wrong.example").is_err());
}
#[test]
fn mldsa_self_signed_chain() {
use crate::hash::Sha256;
use crate::mldsa::MlDsa65PrivateKey;
use crate::rng::HmacDrbg;
use crate::x509::CertSigner;
let mut rng = HmacDrbg::<Sha256>::new(b"verify-mldsa65", b"n", &[]);
let (sk, _pk) = MlDsa65PrivateKey::generate(&mut rng);
let signer = CertSigner::MlDsa65(&sk);
let name = DistinguishedName::common_name("pqc.example");
let cert =
Certificate::self_signed_general(&signer, &name, &validity(), 1, true, &[]).unwrap();
let mut store = RootCertStore::new();
store.add_der(cert.to_der().to_vec()).unwrap();
let leaf_key = verify_chain(&store, &[cert.to_der().to_vec()], None, &policy()).unwrap();
assert!(matches!(leaf_key, AnyPublicKey::MlDsa65(_)));
}
#[test]
fn secp256k1_chain_under_extended_policy() {
use crate::ec::{BoxedEcdsaPrivateKey, CurveId};
use crate::hash::Sha256;
use crate::rng::HmacDrbg;
use crate::x509::CertSigner;
let mut rng = HmacDrbg::<Sha256>::new(b"verify-k1", b"n", &[]);
let sk = BoxedEcdsaPrivateKey::generate(CurveId::Secp256k1, &mut rng);
let signer = CertSigner::Ecdsa(&sk);
let name = DistinguishedName::common_name("k1.example");
let cert =
Certificate::self_signed_general(&signer, &name, &validity(), 1, true, &[]).unwrap();
let mut store = RootCertStore::new();
store.add_der(cert.to_der().to_vec()).unwrap();
verify_chain(&store, &[cert.to_der().to_vec()], None, &policy()).unwrap();
let restrictive = SignaturePolicy::empty().permit("ed25519");
assert!(matches!(
verify_chain(&store, &[cert.to_der().to_vec()], None, &restrictive),
Err(Error::BadCertificate)
));
}
#[test]
fn slhdsa_chain_under_extended_policy() {
use crate::hash::Sha256;
use crate::rng::HmacDrbg;
use crate::slhdsa::{ParamSet, PrivateKey};
use crate::x509::CertSigner;
let mut rng = HmacDrbg::<Sha256>::new(b"verify-slhdsa", b"n", &[]);
let (sk, _pk) = PrivateKey::generate(ParamSet::Sha2_128f, &mut rng);
let signer = CertSigner::SlhDsa(&sk);
let name = DistinguishedName::common_name("slhdsa.example");
let cert =
Certificate::self_signed_general(&signer, &name, &validity(), 1, true, &[]).unwrap();
let mut store = RootCertStore::new();
store.add_der(cert.to_der().to_vec()).unwrap();
assert!(matches!(
verify_chain(&store, &[cert.to_der().to_vec()], None, &policy()),
Err(Error::BadCertificate)
));
let extended = SignaturePolicy::modern().permit("slh-dsa-sha2-128f");
verify_chain(&store, &[cert.to_der().to_vec()], None, &extended).unwrap();
}
#[test]
fn legacy_sha1_chain_only_under_opt_in() {
use crate::der::{encode_bit_string, encode_sequence};
use crate::hash::Sha1;
use crate::rsa::Pkcs1Digest;
use crate::test_util::rsa_test_key_a;
use crate::x509::cert::build_tbs_raw;
use crate::x509::{AnyPublicKey, Certificate, algorithm_identifier};
assert_eq!(Sha1::DIGEST_INFO_PREFIX.len(), 15);
let key = rsa_test_key_a();
let subj = DistinguishedName::common_name("legacy.example");
let mut n_bytes = alloc::vec![0u8; 256];
key.public_key().modulus().write_be_bytes(&mut n_bytes);
let mut e_bytes = alloc::vec![0u8; 256];
key.public_key().exponent().write_be_bytes(&mut e_bytes);
let boxed_pub = crate::rsa::BoxedRsaPublicKey::new(
crate::bignum::BoxedUint::from_be_bytes(&n_bytes),
crate::bignum::BoxedUint::from_be_bytes(&e_bytes),
);
let spki = AnyPublicKey::Rsa(boxed_pub).to_spki_der();
let algid = algorithm_identifier(oid::SHA1_WITH_RSA, true);
let exts = crate::x509::cert::legacy_extensions(true, &[]);
let tbs = build_tbs_raw(1, &subj, &subj, &validity(), &spki, &algid, &exts);
let sig = key.sign_pkcs1v15::<Sha1>(&tbs).unwrap();
let der = encode_sequence(&[tbs.clone(), algid.clone(), encode_bit_string(&sig)].concat());
let legacy = Certificate::from_der(der).unwrap();
let mut store = RootCertStore::new();
store.add_der(legacy.to_der().to_vec()).unwrap();
assert!(matches!(
verify_chain(&store, &[legacy.to_der().to_vec()], None, &policy()),
Err(Error::BadCertificate)
));
let with_sha1 = SignaturePolicy::modern().permit("rsa-pkcs1-sha1");
verify_chain(&store, &[legacy.to_der().to_vec()], None, &with_sha1).unwrap();
}
#[test]
fn rejects_inner_outer_algid_mismatch() {
use crate::der::{encode_bit_string, encode_sequence};
use crate::hash::Sha1;
use crate::rsa::Pkcs1Digest;
use crate::test_util::rsa_test_key_a;
use crate::x509::cert::build_tbs_raw;
use crate::x509::{AnyPublicKey, Certificate, algorithm_identifier};
assert_eq!(Sha1::DIGEST_INFO_PREFIX.len(), 15);
let key = rsa_test_key_a();
let subj = DistinguishedName::common_name("mismatch.example");
let mut n_bytes = alloc::vec![0u8; 256];
key.public_key().modulus().write_be_bytes(&mut n_bytes);
let mut e_bytes = alloc::vec![0u8; 256];
key.public_key().exponent().write_be_bytes(&mut e_bytes);
let boxed_pub = crate::rsa::BoxedRsaPublicKey::new(
crate::bignum::BoxedUint::from_be_bytes(&n_bytes),
crate::bignum::BoxedUint::from_be_bytes(&e_bytes),
);
let spki = AnyPublicKey::Rsa(boxed_pub).to_spki_der();
let inner = algorithm_identifier(oid::SHA256_WITH_RSA, true);
let outer = algorithm_identifier(oid::SHA1_WITH_RSA, true);
let exts = crate::x509::cert::legacy_extensions(true, &[]);
let tbs = build_tbs_raw(1, &subj, &subj, &validity(), &spki, &inner, &exts);
let sig = key.sign_pkcs1v15::<Sha1>(&tbs).unwrap();
let der = encode_sequence(&[tbs, outer, encode_bit_string(&sig)].concat());
let mismatched = Certificate::from_der(der).unwrap();
let mut store = RootCertStore::new();
store.add_der(mismatched.to_der().to_vec()).unwrap();
let with_sha1 = SignaturePolicy::modern().permit("rsa-pkcs1-sha1");
assert!(matches!(
verify_chain(&store, &[mismatched.to_der().to_vec()], None, &with_sha1),
Err(Error::BadCertificate)
));
}
#[test]
fn crl_revokes_leaf_serial() {
use crate::tls::pki::CrlStore;
use crate::x509::{CertSigner, CrlBuilder};
let ca_key = rsa_test_key_a();
let leaf_key = rsa_test_key_b();
let ca_name = DistinguishedName::common_name("CRL Test CA");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let leaf_revoked = Certificate::issue(
&ca_key,
&ca_name,
&DistinguishedName::common_name("revoked.example"),
&leaf_key.public_key(),
&validity(),
42,
false,
)
.unwrap();
let leaf_ok = Certificate::issue(
&ca_key,
&ca_name,
&DistinguishedName::common_name("ok.example"),
&leaf_key.public_key(),
&validity(),
43,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let signer = CertSigner::Rsa(
&crate::rsa::BoxedRsaPrivateKey::from_pkcs1_pem(include_str!(
"../../../testdata/rsa2048_test_a.pem"
))
.unwrap(),
);
let mut b = CrlBuilder::new(&ca_name, Time::utc(2026, 1, 1, 0, 0, 0), None);
b.revoke(&[42], Time::utc(2026, 1, 2, 0, 0, 0), None);
let crl = b.sign(&signer).unwrap();
let mut crls = CrlStore::new();
crls.add_der(crl.to_der().to_vec()).unwrap();
verify_chain_with_crls(&store, &crls, &[leaf_ok.to_der().to_vec()], None, &policy())
.unwrap();
assert!(matches!(
verify_chain_with_crls(
&store,
&crls,
&[leaf_revoked.to_der().to_vec()],
None,
&policy(),
),
Err(Error::BadCertificate)
));
verify_chain_with_crls(
&store,
&CrlStore::new(),
&[leaf_revoked.to_der().to_vec()],
None,
&policy(),
)
.unwrap();
}
#[test]
fn expired_crl_is_advisory() {
use crate::tls::pki::CrlStore;
use crate::x509::{CertSigner, CrlBuilder};
let ca_key = rsa_test_key_a();
let leaf_key = rsa_test_key_b();
let ca_name = DistinguishedName::common_name("CRL Test CA");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let leaf = Certificate::issue(
&ca_key,
&ca_name,
&DistinguishedName::common_name("leaf.example"),
&leaf_key.public_key(),
&validity(),
7,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let signer = CertSigner::Rsa(
&crate::rsa::BoxedRsaPrivateKey::from_pkcs1_pem(include_str!(
"../../../testdata/rsa2048_test_a.pem"
))
.unwrap(),
);
let mut b = CrlBuilder::new(
&ca_name,
Time::utc(2024, 1, 1, 0, 0, 0),
Some(Time::utc(2024, 12, 31, 0, 0, 0)),
);
b.revoke(&[7], Time::utc(2024, 6, 1, 0, 0, 0), None);
let crl = b.sign(&signer).unwrap();
let mut crls = CrlStore::new();
crls.add_der(crl.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain_with_crls(
&store,
&crls,
&[leaf.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
}
#[test]
fn crl_signed_by_wrong_key_is_ignored() {
use crate::tls::pki::CrlStore;
use crate::x509::{CertSigner, CrlBuilder};
let ca_key = rsa_test_key_a();
let other_key = rsa_test_key_b();
let ca_name = DistinguishedName::common_name("CRL Test CA");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let leaf = Certificate::issue(
&ca_key,
&ca_name,
&DistinguishedName::common_name("leaf.example"),
&other_key.public_key(),
&validity(),
55,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let bogus_signer = CertSigner::Rsa(
&crate::rsa::BoxedRsaPrivateKey::from_pkcs1_pem(include_str!(
"../../../testdata/rsa2048_test_b.pem"
))
.unwrap(),
);
let mut b = CrlBuilder::new(&ca_name, Time::utc(2026, 1, 1, 0, 0, 0), None);
b.revoke(&[55], Time::utc(2026, 1, 2, 0, 0, 0), None);
let crl = b.sign(&bogus_signer).unwrap();
let mut crls = CrlStore::new();
crls.add_der(crl.to_der().to_vec()).unwrap();
verify_chain_with_crls(&store, &crls, &[leaf.to_der().to_vec()], None, &policy()).unwrap();
}
#[test]
fn rejects_unpermitted_algorithm() {
let key = rsa_test_key_a();
let cert = Certificate::self_signed(
&key,
&DistinguishedName::common_name("rsa.example"),
&validity(),
1,
true,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(cert.to_der().to_vec()).unwrap();
let ed_only = SignaturePolicy::empty().permit("ed25519");
assert!(matches!(
verify_chain(&store, &[cert.to_der().to_vec()], None, &ed_only),
Err(Error::BadCertificate)
));
verify_chain(&store, &[cert.to_der().to_vec()], None, &policy()).unwrap();
}
#[test]
fn crl_signed_with_sha1_rejected_under_modern_policy() {
use crate::der::{encode_bit_string, encode_sequence};
use crate::hash::Sha1;
use crate::tls::pki::CrlStore;
use crate::x509::{CertificateRevocationList, algorithm_identifier};
let ca_key = rsa_test_key_a();
let leaf_key = rsa_test_key_b();
let ca_name = DistinguishedName::common_name("CRL SHA-1 Test CA");
let root = Certificate::self_signed(&ca_key, &ca_name, &validity(), 1, true).unwrap();
let leaf = Certificate::issue(
&ca_key,
&ca_name,
&DistinguishedName::common_name("sha1crl.example"),
&leaf_key.public_key(),
&validity(),
99,
false,
)
.unwrap();
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let algid_sha1 = algorithm_identifier(oid::SHA1_WITH_RSA, true);
let serial = crate::der::encode_integer(&[99]);
let revoked_at = Time::utc(2026, 1, 2, 0, 0, 0).to_der_choice();
let entry = encode_sequence(&[serial, revoked_at].concat());
let mut body = alloc::vec::Vec::new();
body.extend_from_slice(&crate::der::encode_integer(&[1])); body.extend_from_slice(&algid_sha1);
body.extend_from_slice(&ca_name.to_der());
body.extend_from_slice(&Time::utc(2026, 1, 1, 0, 0, 0).to_der_choice());
body.extend_from_slice(&encode_sequence(&entry));
let tbs = encode_sequence(&body);
let sig = ca_key.sign_pkcs1v15::<Sha1>(&tbs).unwrap();
let crl_der = encode_sequence(&[tbs, algid_sha1, encode_bit_string(&sig)].concat());
let crl = CertificateRevocationList::from_der(crl_der).unwrap();
let mut crls = CrlStore::new();
crls.add_der(crl.to_der().to_vec()).unwrap();
verify_chain_with_crls(&store, &crls, &[leaf.to_der().to_vec()], None, &policy()).unwrap();
let with_sha1 = SignaturePolicy::modern().permit("rsa-pkcs1-sha1");
assert!(matches!(
verify_chain_with_crls(&store, &crls, &[leaf.to_der().to_vec()], None, &with_sha1,),
Err(Error::BadCertificate)
));
}
#[test]
fn dns_matcher_refuses_ip_pattern_or_host() {
assert!(!super::dns_name_matches("10.0.0.1", "10.0.0.1"));
assert!(!super::dns_name_matches("example.com", "10.0.0.1"));
assert!(!super::dns_name_matches("*.example.com", "10.0.0.1"));
assert!(!super::dns_name_matches("::1", "::1"));
assert!(!super::dns_name_matches("2001:db8::1", "2001:db8::1"));
assert!(!super::dns_name_matches(
"::ffff:10.0.0.1",
"::ffff:10.0.0.1"
));
assert!(super::dns_name_matches("example.com", "example.com"));
assert!(super::dns_name_matches("*.example.com", "host.example.com"));
assert!(!super::dns_name_matches("*.example.com", "a.b.example.com"));
assert!(!super::dns_name_matches("*.example.com", "example.com"));
assert!(!super::dns_name_matches(
"f*.example.com",
"foo.example.com"
));
}
#[test]
fn dns_in_subtree_label_alignment() {
assert!(super::dns_in_subtree("example.com", "example.com"));
assert!(super::dns_in_subtree("foo.example.com", "example.com"));
assert!(super::dns_in_subtree("a.b.example.com", "example.com"));
assert!(!super::dns_in_subtree("notexample.com", "example.com"));
assert!(super::dns_in_subtree("foo.example.com", ".example.com"));
assert!(!super::dns_in_subtree("example.com", ".example.com"));
assert!(super::dns_in_subtree("FOO.Example.Com", "example.com"));
}
#[test]
fn ip_in_subtree_cidr() {
let addr = [10u8, 0, 0, 0];
let mask = [0xffu8, 0, 0, 0];
assert!(super::ip_in_subtree(&[10, 1, 2, 3], &addr, &mask));
assert!(super::ip_in_subtree(&[10, 0, 0, 0], &addr, &mask));
assert!(!super::ip_in_subtree(&[11, 0, 0, 1], &addr, &mask));
assert!(!super::ip_in_subtree(&[10, 0, 0, 1], &[0; 16], &[0; 16]));
}
fn build_chain_with_nc(
nc_ext: crate::x509::Extension,
leaf_cn: &str,
leaf_sans: &[crate::x509::GeneralName],
) -> (Certificate, Certificate, Certificate) {
use crate::ec::{BoxedEcdsaPrivateKey, CurveId};
use crate::rng::HmacDrbg;
use crate::x509::{
CertSigner, DistinguishedName, Extension, GeneralName, KeyUsageBits,
extension::{basic_constraints, extended_key_usage, key_usage, subject_alt_name},
};
let _ = GeneralName::Dns; let mut rng = HmacDrbg::<crate::hash::Sha256>::new(b"nc-chain", b"n", &[]);
let root_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let int_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let leaf_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let root_signer = CertSigner::Ecdsa(&root_key);
let int_signer = CertSigner::Ecdsa(&int_key);
let root_name = DistinguishedName::common_name("nc-root");
let int_name = DistinguishedName::common_name("nc-intermediate");
let leaf_name = DistinguishedName::common_name(leaf_cn);
let root_exts = [
basic_constraints(true, None),
key_usage(KeyUsageBits::KEY_CERT_SIGN | KeyUsageBits::CRL_SIGN),
];
let root = Certificate::self_signed_with_extensions(
&root_signer,
&root_name,
&validity(),
1,
&root_exts,
)
.unwrap();
let int_exts: alloc::vec::Vec<Extension> = alloc::vec![
basic_constraints(true, Some(0)),
key_usage(KeyUsageBits::KEY_CERT_SIGN | KeyUsageBits::CRL_SIGN),
nc_ext,
];
let int_pub = crate::x509::AnyPublicKey::Ecdsa(int_key.public_key());
let int = Certificate::issue_with_extensions(
&root_signer,
&root_name,
&int_name,
&int_pub,
&validity(),
2,
&int_exts,
)
.unwrap();
let leaf_exts = [
basic_constraints(false, None),
key_usage(KeyUsageBits::DIGITAL_SIGNATURE),
extended_key_usage(&[oid::ID_KP_SERVER_AUTH]),
subject_alt_name(leaf_sans),
];
let leaf_pub = crate::x509::AnyPublicKey::Ecdsa(leaf_key.public_key());
let leaf = Certificate::issue_with_extensions(
&int_signer,
&int_name,
&leaf_name,
&leaf_pub,
&validity(),
3,
&leaf_exts,
)
.unwrap();
(root, int, leaf)
}
#[test]
fn name_constraints_permitted_dns_accepts_matching() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Dns(".good.example".into())],
&[],
);
let leaf_sans = [GeneralName::Dns("host.good.example".into())];
let (root, int, leaf) = build_chain_with_nc(nc, "nc-leaf", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
}
#[test]
fn name_constraints_permitted_dns_rejects_outside_subtree() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Dns(".good.example".into())],
&[],
);
let leaf_sans = [GeneralName::Dns("attacker.example".into())];
let (root, int, leaf) = build_chain_with_nc(nc, "nc-leaf", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[test]
fn name_constraints_excluded_dns_rejects_matching() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[],
&[GeneralName::Dns(".bad.example".into())],
);
let leaf_sans = [GeneralName::Dns("host.bad.example".into())];
let (root, int, leaf) = build_chain_with_nc(nc, "nc-leaf", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[test]
fn name_constraints_chain_rejects_san_less_leaf() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Dns(".good.example".into())],
&[],
);
let leaf_sans: [GeneralName; 0] = [];
let (root, int, leaf) = build_chain_with_nc(nc, "nc-leaf", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[test]
fn name_constraints_excluded_cn_fallback_rejected() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[],
&[GeneralName::Dns(".bad.example".into())],
);
let leaf_sans: [GeneralName; 0] = [];
let (root, int, leaf) = build_chain_with_nc(nc, "host.bad.example", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[test]
fn name_constraints_permitted_cn_fallback_accepted() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Dns(".good.example".into())],
&[],
);
let leaf_sans: [GeneralName; 0] = [];
let (root, int, leaf) = build_chain_with_nc(nc, "host.good.example", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
}
#[test]
fn name_constraints_cn_ignored_when_dns_san_present() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[],
&[GeneralName::Dns(".bad.example".into())],
);
let leaf_sans = [GeneralName::Dns("host.good.example".into())];
let (root, int, leaf) = build_chain_with_nc(nc, "host.bad.example", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
}
#[test]
fn name_constraints_ip_shaped_cn_not_dns_evaluated() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[],
&[GeneralName::Dns(".bad.example".into())],
);
let leaf_sans: [GeneralName; 0] = [];
let (root, int, leaf) = build_chain_with_nc(nc, "10.0.0.1", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
}
#[test]
fn name_constraints_critical_with_unenforceable_type_rejected() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Email("admin@example.com".into())],
&[],
);
let leaf_sans = [GeneralName::Dns("leaf.example".into())];
let (root, int, leaf) = build_chain_with_nc(nc, "nc-leaf", &leaf_sans);
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[allow(clippy::type_complexity)]
fn build_propagation_chain(
sub2_san: &str,
leaf_san: &str,
) -> (Certificate, Certificate, Certificate, Certificate) {
use crate::ec::{BoxedEcdsaPrivateKey, CurveId};
use crate::rng::HmacDrbg;
use crate::x509::{
CertSigner, GeneralName, KeyUsageBits,
extension::{
basic_constraints, extended_key_usage, key_usage, name_constraints,
subject_alt_name,
},
};
let mut rng = HmacDrbg::<crate::hash::Sha256>::new(b"nc-propagate", b"n", &[]);
let root_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let sub1_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let sub2_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let leaf_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let root_signer = CertSigner::Ecdsa(&root_key);
let sub1_signer = CertSigner::Ecdsa(&sub1_key);
let sub2_signer = CertSigner::Ecdsa(&sub2_key);
let root_name = DistinguishedName::common_name("prop-root");
let sub1_name = DistinguishedName::common_name("prop-sub1");
let sub2_name = DistinguishedName::common_name("prop-sub2");
let leaf_name = DistinguishedName::common_name("prop-leaf");
let root = Certificate::self_signed_with_extensions(
&root_signer,
&root_name,
&validity(),
1,
&[
basic_constraints(true, None),
key_usage(KeyUsageBits::KEY_CERT_SIGN | KeyUsageBits::CRL_SIGN),
],
)
.unwrap();
let sub1_pub = crate::x509::AnyPublicKey::Ecdsa(sub1_key.public_key());
let sub1 = Certificate::issue_with_extensions(
&root_signer,
&root_name,
&sub1_name,
&sub1_pub,
&validity(),
2,
&[
basic_constraints(true, None),
key_usage(KeyUsageBits::KEY_CERT_SIGN | KeyUsageBits::CRL_SIGN),
name_constraints(&[GeneralName::Dns(".example.com".into())], &[]),
],
)
.unwrap();
let sub2_pub = crate::x509::AnyPublicKey::Ecdsa(sub2_key.public_key());
let sub2 = Certificate::issue_with_extensions(
&sub1_signer,
&sub1_name,
&sub2_name,
&sub2_pub,
&validity(),
3,
&[
basic_constraints(true, None),
key_usage(KeyUsageBits::KEY_CERT_SIGN | KeyUsageBits::CRL_SIGN),
subject_alt_name(&[GeneralName::Dns(sub2_san.into())]),
],
)
.unwrap();
let leaf_pub = crate::x509::AnyPublicKey::Ecdsa(leaf_key.public_key());
let leaf = Certificate::issue_with_extensions(
&sub2_signer,
&sub2_name,
&leaf_name,
&leaf_pub,
&validity(),
4,
&[
basic_constraints(false, None),
key_usage(KeyUsageBits::DIGITAL_SIGNATURE),
extended_key_usage(&[oid::ID_KP_SERVER_AUTH]),
subject_alt_name(&[GeneralName::Dns(leaf_san.into())]),
],
)
.unwrap();
(root, sub1, sub2, leaf)
}
#[test]
fn name_constraints_reject_out_of_scope_intermediate() {
let (root, sub1, sub2, leaf) = build_propagation_chain("evil.com", "host.example.com");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
let chain = alloc::vec![
leaf.to_der().to_vec(),
sub2.to_der().to_vec(),
sub1.to_der().to_vec(),
];
assert!(matches!(
verify_chain(&store, &chain, Some(&now), &policy()),
Err(Error::BadCertificate)
));
}
#[test]
fn name_constraints_accept_fully_in_scope_chain() {
let (root, sub1, sub2, leaf) =
build_propagation_chain("ca2.example.com", "host.example.com");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
let chain = alloc::vec![
leaf.to_der().to_vec(),
sub2.to_der().to_vec(),
sub1.to_der().to_vec(),
];
verify_chain(&store, &chain, Some(&now), &policy()).unwrap();
}
#[test]
fn name_constraints_reject_out_of_scope_leaf_below_in_scope_intermediate() {
let (root, sub1, sub2, leaf) = build_propagation_chain("ca2.example.com", "host.evil.com");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
let chain = alloc::vec![
leaf.to_der().to_vec(),
sub2.to_der().to_vec(),
sub1.to_der().to_vec(),
];
assert!(matches!(
verify_chain(&store, &chain, Some(&now), &policy()),
Err(Error::BadCertificate)
));
}
fn build_anchor_nc_chain(
root_nc: Option<crate::x509::Extension>,
int_san: &str,
leaf_san: &str,
) -> (Certificate, Certificate, Certificate) {
use crate::ec::{BoxedEcdsaPrivateKey, CurveId};
use crate::rng::HmacDrbg;
use crate::x509::{
CertSigner, Extension, GeneralName, KeyUsageBits,
extension::{basic_constraints, extended_key_usage, key_usage, subject_alt_name},
};
let mut rng = HmacDrbg::<crate::hash::Sha256>::new(b"anchor-nc", b"n", &[]);
let root_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let int_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let leaf_key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
let root_signer = CertSigner::Ecdsa(&root_key);
let int_signer = CertSigner::Ecdsa(&int_key);
let root_name = DistinguishedName::common_name("anchor-nc-root");
let int_name = DistinguishedName::common_name("anchor-nc-int");
let leaf_name = DistinguishedName::common_name("anchor-nc-leaf");
let mut root_exts: alloc::vec::Vec<Extension> = alloc::vec![
basic_constraints(true, None),
key_usage(KeyUsageBits::KEY_CERT_SIGN | KeyUsageBits::CRL_SIGN),
];
if let Some(nc) = root_nc {
root_exts.push(nc);
}
let root = Certificate::self_signed_with_extensions(
&root_signer,
&root_name,
&validity(),
1,
&root_exts,
)
.unwrap();
let int_pub = crate::x509::AnyPublicKey::Ecdsa(int_key.public_key());
let int = Certificate::issue_with_extensions(
&root_signer,
&root_name,
&int_name,
&int_pub,
&validity(),
2,
&[
basic_constraints(true, Some(0)),
key_usage(KeyUsageBits::KEY_CERT_SIGN | KeyUsageBits::CRL_SIGN),
subject_alt_name(&[GeneralName::Dns(int_san.into())]),
],
)
.unwrap();
let leaf_pub = crate::x509::AnyPublicKey::Ecdsa(leaf_key.public_key());
let leaf = Certificate::issue_with_extensions(
&int_signer,
&int_name,
&leaf_name,
&leaf_pub,
&validity(),
3,
&[
basic_constraints(false, None),
key_usage(KeyUsageBits::DIGITAL_SIGNATURE),
extended_key_usage(&[oid::ID_KP_SERVER_AUTH]),
subject_alt_name(&[GeneralName::Dns(leaf_san.into())]),
],
)
.unwrap();
(root, int, leaf)
}
#[test]
fn anchor_name_constraints_permitted_accepts_in_subtree_leaf() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Dns(".good.example".into())],
&[],
);
let (root, int, leaf) =
build_anchor_nc_chain(Some(nc), "int.good.example", "host.good.example");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
}
#[test]
fn anchor_name_constraints_permitted_rejects_outside_leaf() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Dns(".good.example".into())],
&[],
);
let (root, int, leaf) =
build_anchor_nc_chain(Some(nc), "int.good.example", "host.evil.example");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[test]
fn anchor_name_constraints_permitted_rejects_outside_intermediate() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Dns(".good.example".into())],
&[],
);
let (root, int, leaf) =
build_anchor_nc_chain(Some(nc), "int.evil.example", "host.good.example");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[test]
fn anchor_name_constraints_excluded_rejects_matching_leaf() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[],
&[GeneralName::Dns(".bad.example".into())],
);
let (root, int, leaf) =
build_anchor_nc_chain(Some(nc), "int.good.example", "host.bad.example");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
assert!(matches!(
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
),
Err(Error::BadCertificate)
));
}
#[test]
fn anchor_without_name_constraints_unaffected() {
let (root, int, leaf) =
build_anchor_nc_chain(None, "int.evil.example", "host.evil.example");
let mut store = RootCertStore::new();
store.add_der(root.to_der().to_vec()).unwrap();
let now = Time::utc(2026, 1, 1, 0, 0, 0);
verify_chain(
&store,
&[leaf.to_der().to_vec(), int.to_der().to_vec()],
Some(&now),
&policy(),
)
.unwrap();
}
#[test]
fn add_der_rejects_anchor_with_unenforceable_constraints() {
use crate::x509::GeneralName;
let nc = crate::x509::extension::name_constraints(
&[GeneralName::Email("admin@example.com".into())],
&[],
);
let (root, _int, _leaf) =
build_anchor_nc_chain(Some(nc), "int.good.example", "host.good.example");
let mut store = RootCertStore::new();
assert!(matches!(
store.add_der(root.to_der().to_vec()),
Err(Error::BadCertificate)
));
assert!(store.is_empty());
}
#[test]
fn add_der_rejects_anchor_with_malformed_constraints() {
let garbage_nc = crate::x509::Extension {
oid: oid::NAME_CONSTRAINTS.to_vec(),
critical: true,
value: alloc::vec![0xff, 0x00],
};
let (root, _int, _leaf) =
build_anchor_nc_chain(Some(garbage_nc), "int.good.example", "host.good.example");
let mut store = RootCertStore::new();
assert!(matches!(
store.add_der(root.to_der().to_vec()),
Err(Error::BadCertificate)
));
assert!(store.is_empty());
}
}