use der::asn1::{BitString, ObjectIdentifier, OctetString};
use der::Decode as _;
use pkix_path::{DefaultVerifier, TrustAnchor, ValidationPolicy};
use pkix_path_builder::{
build_path, build_path_candidates, build_path_candidates_with_config, CertPool,
PathBuilderConfig,
};
use x509_cert::Certificate;
const PKITS_NOW: u64 = 1_580_000_000;
fn pkits_cert(name: &str) -> Certificate {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../pkix-path/tests/pkits/certs")
.join(format!("{name}.crt"));
let der_bytes = std::fs::read(&path)
.unwrap_or_else(|e| panic!("fixture not found at {}: {}", path.display(), e));
Certificate::from_der(&der_bytes).unwrap_or_else(|e| panic!("failed to parse cert {name}: {e}"))
}
fn pkits_trust_anchor() -> TrustAnchor {
TrustAnchor::from(&pkits_cert("TrustAnchorRootCertificate"))
}
const OID_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
fn corrupt_basic_constraints(mut cert: Certificate) -> Certificate {
let exts = cert
.tbs_certificate
.extensions
.as_mut()
.expect("template cert must have extensions");
let bc = exts
.iter_mut()
.find(|e| e.extn_id == OID_BASIC_CONSTRAINTS)
.expect("template cert must carry BasicConstraints");
bc.extn_value = OctetString::new(b"\xff\xff".to_vec()).expect("OctetString::new");
cert
}
#[test]
fn test_build_path_two_cert_chain() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let intermediate = pkits_cert("GoodCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(intermediate);
let path = build_path(&ee, &pool, std::slice::from_ref(&anchor))
.expect("build_path should succeed for PKITS §4.1.1 chain");
assert!(
path.len() >= 2,
"path should contain at least EE + intermediate"
);
let policy = ValidationPolicy::new(PKITS_NOW);
let verifier = DefaultVerifier;
pkix_path::validate_path(&path, &[anchor], &policy, &verifier)
.expect("validate_path should succeed on the built chain");
}
#[test]
fn test_build_path_shuffled_order() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let intermediate = pkits_cert("GoodCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(intermediate);
let path = build_path(&ee, &pool, std::slice::from_ref(&anchor))
.expect("build_path should succeed regardless of pool insertion order");
assert!(path.len() >= 2);
let policy = ValidationPolicy::new(PKITS_NOW);
let verifier = DefaultVerifier;
pkix_path::validate_path(&path, &[anchor], &policy, &verifier)
.expect("validate_path should succeed");
}
#[test]
fn test_build_path_no_path() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let anchor = pkits_trust_anchor();
let pool = CertPool::new();
let err =
build_path(&ee, &pool, &[anchor]).expect_err("build_path should fail with an empty pool");
assert!(
matches!(err, pkix_path_builder::Error::NoPathFound),
"expected NoPathFound, got {err}"
);
}
#[test]
fn test_build_path_self_signed_non_anchor_in_pool() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let intermediate = pkits_cert("GoodCACert");
let bad_ca = pkits_cert("BadSignedCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(intermediate);
pool.add(bad_ca);
let path = build_path(&ee, &pool, std::slice::from_ref(&anchor))
.expect("build_path must find the correct path ignoring the self-signed non-anchor");
let policy = pkix_path::ValidationPolicy::new(PKITS_NOW);
let verifier = pkix_path::DefaultVerifier;
pkix_path::validate_path(&path, &[anchor], &policy, &verifier)
.expect("validate_path should succeed on the built chain");
}
#[test]
fn test_build_path_wrong_pool() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let anchor = pkits_trust_anchor();
let wrong_cert = pkits_cert("BadSignedCACert");
let mut pool = CertPool::new();
pool.add(wrong_cert);
let err = build_path(&ee, &pool, &[anchor])
.expect_err("build_path should fail when pool contains unrelated cert");
assert!(
matches!(err, pkix_path_builder::Error::NoPathFound),
"expected NoPathFound, got {err}"
);
}
#[test]
fn test_build_path_duplicate_cert_in_pool_pruned_by_spki() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let intermediate = pkits_cert("GoodCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(intermediate.clone());
pool.add(intermediate);
let path = build_path(&ee, &pool, std::slice::from_ref(&anchor))
.expect("build_path must find path even with duplicate certs in pool");
let mut policy = ValidationPolicy::new(PKITS_NOW);
policy.enforce_key_usage = false;
pkix_path::validate_path(&path, &[anchor], &policy, &DefaultVerifier)
.expect("path returned with duplicate pool entries must be valid");
let spkis: Vec<_> = path
.iter()
.map(|c| {
use der::Encode as _;
let mut buf = Vec::new();
c.tbs_certificate
.subject_public_key_info
.encode_to_vec(&mut buf)
.unwrap();
buf
})
.collect();
let deduped_len = {
let mut seen = std::collections::HashSet::new();
spkis.iter().filter(|s| seen.insert(*s)).count()
};
assert_eq!(
spkis.len(),
deduped_len,
"path must not contain duplicate SPKI entries"
);
}
#[test]
fn test_build_path_adversarial_pool_budget_exceeded() {
const N: usize = 30;
const _: () = assert!(N <= u8::MAX as usize, "N must fit in u8");
let ee = pkits_cert("ValidCertificatePathTest1EE");
let template_ca = pkits_cert("GoodCACert");
let fake_anchor = TrustAnchor::from(&pkits_cert("BadSignedCACert"));
let mut pool = CertPool::new();
for i in 0..N {
let mut ca = template_ca.clone();
ca.tbs_certificate.issuer = ca.tbs_certificate.subject.clone();
ca.tbs_certificate
.subject_public_key_info
.subject_public_key = BitString::new(
0,
vec![u8::try_from(i).expect("loop bound N fits in u8"); 32],
)
.expect("BitString construction must succeed for valid parameters");
pool.add(ca);
}
let start = std::time::Instant::now();
let err = build_path(&ee, &pool, std::slice::from_ref(&fake_anchor))
.expect_err("adversarial pool should not find a valid path");
let elapsed = start.elapsed();
assert!(
matches!(err, pkix_path_builder::Error::BudgetExceeded),
"expected BudgetExceeded, got {err}"
);
assert!(
elapsed.as_secs() < 2,
"build_path took {elapsed:?}; budget enforcement must prevent exponential blowup"
);
}
fn encode_spki(cert: &Certificate) -> Vec<u8> {
use der::Encode as _;
let mut buf = Vec::new();
cert.tbs_certificate
.subject_public_key_info
.encode_to_vec(&mut buf)
.expect("SubjectPublicKeyInfo must encode");
buf
}
#[test]
fn test_build_path_aki_ranking_disambiguates_same_dn_different_spki() {
let ee = pkits_cert("ValidBasicSelfIssuedNewWithOldTest4EE");
let oldkey_ca = pkits_cert("BasicSelfIssuedOldKeyCACert");
let bridge_ca = pkits_cert("BasicSelfIssuedOldKeyNewWithOldCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(bridge_ca.clone());
pool.add(oldkey_ca.clone());
let path =
build_path(&ee, &pool, std::slice::from_ref(&anchor)).expect("build_path must succeed");
let selected = encode_spki(&path[1]);
let expected = encode_spki(&oldkey_ca);
let unexpected = encode_spki(&bridge_ca);
assert_eq!(
selected, expected,
"AKI ranking must select OldKeyCACert (SKI matches Test4EE.AKI), \
not the same-DN bridge cert"
);
assert_ne!(selected, unexpected);
let mut policy = ValidationPolicy::new(PKITS_NOW);
policy.enforce_key_usage = false;
pkix_path::validate_path(&path, &[anchor], &policy, &DefaultVerifier)
.expect("validate_path must succeed on the AKI-disambiguated chain");
}
#[test]
fn test_build_path_aki_ranking_unrelated_pool_cert_does_not_perturb_selection() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let good_ca = pkits_cert("GoodCACert");
let unrelated = pkits_cert("BasicSelfIssuedNewKeyCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(unrelated.clone());
pool.add(good_ca.clone());
let path =
build_path(&ee, &pool, std::slice::from_ref(&anchor)).expect("build_path must succeed");
let selected = encode_spki(&path[1]);
let expected = encode_spki(&good_ca);
assert_eq!(
selected, expected,
"GoodCACert must be selected; unrelated pool cert must not be chosen"
);
let policy = ValidationPolicy::new(PKITS_NOW);
pkix_path::validate_path(&path, &[anchor], &policy, &DefaultVerifier)
.expect("validate_path must succeed");
}
#[test]
fn test_iterator_smime_retry_loop_finds_verifying_chain() {
let ee = pkits_cert("ValidBasicSelfIssuedNewWithOldTest4EE");
let oldkey_ca = pkits_cert("BasicSelfIssuedOldKeyCACert");
let bridge_ca = pkits_cert("BasicSelfIssuedOldKeyNewWithOldCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(bridge_ca);
pool.add(oldkey_ca);
let mut policy = ValidationPolicy::new(PKITS_NOW);
policy.enforce_key_usage = false;
let verifier = DefaultVerifier;
let anchors = std::slice::from_ref(&anchor);
let mut iter = build_path_candidates(&ee, &pool, anchors);
let mut next_calls = 0_usize;
let mut verified_chain: Option<Vec<Certificate>> = None;
loop {
match iter.next() {
None => break,
Some(Err(e)) => panic!("iterator yielded fatal error: {e}"),
Some(Ok(chain)) => {
next_calls += 1;
if pkix_path::validate_path(&chain, anchors, &policy, &verifier).is_ok() {
verified_chain = Some(chain);
break;
}
}
}
assert!(next_calls < 10, "iterator should terminate quickly");
}
let chain = verified_chain.expect("at least one chain must verify");
assert!(
next_calls <= 2,
"iterator must find a verifying chain in ≤2 calls, took {next_calls}"
);
assert!(chain.len() >= 2, "chain has at least leaf + intermediate");
}
#[test]
fn test_iterator_enumerates_all_topologically_valid_chains() {
let ee = pkits_cert("ValidBasicSelfIssuedNewWithOldTest4EE");
let oldkey_ca = pkits_cert("BasicSelfIssuedOldKeyCACert");
let bridge_ca = pkits_cert("BasicSelfIssuedOldKeyNewWithOldCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(bridge_ca.clone());
pool.add(oldkey_ca.clone());
let oldkey_spki = encode_spki(&oldkey_ca);
let bridge_spki = encode_spki(&bridge_ca);
let iter = build_path_candidates(&ee, &pool, std::slice::from_ref(&anchor));
let mut chains: Vec<Vec<Certificate>> = Vec::new();
for item in iter {
let chain = item.expect("no fatal errors expected on this fixture");
let terminal_issuer = &chain
.last()
.expect("yielded chain non-empty")
.tbs_certificate
.issuer;
assert!(
pkix_path::names_match(&anchor.subject, terminal_issuer),
"yielded chain must terminate at the trust anchor"
);
chains.push(chain);
}
assert_eq!(
chains.len(),
2,
"exactly two topologically valid chains must be yielded; got {}",
chains.len()
);
let len2 = chains.iter().filter(|c| c.len() == 2).count();
let len3 = chains.iter().filter(|c| c.len() == 3).count();
assert_eq!(len2, 1, "one length-2 chain expected (direct via oldkey)");
assert_eq!(
len3, 1,
"one length-3 chain expected (via bridge then oldkey)"
);
let shortest = chains.iter().find(|c| c.len() == 2).expect("present");
assert_eq!(encode_spki(&shortest[1]), oldkey_spki);
let longer = chains.iter().find(|c| c.len() == 3).expect("present");
assert_eq!(encode_spki(&longer[1]), bridge_spki);
assert_eq!(encode_spki(&longer[2]), oldkey_spki);
}
#[test]
fn test_iterator_returns_none_after_exhaustion() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let intermediate = pkits_cert("GoodCACert");
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(intermediate);
let mut iter = build_path_candidates(&ee, &pool, std::slice::from_ref(&anchor));
let first = iter.next();
assert!(
matches!(first, Some(Ok(_))),
"first call must yield a chain"
);
let second = iter.next();
assert!(second.is_none(), "second call must return None");
let third = iter.next();
assert!(third.is_none(), "subsequent calls must remain None");
}
#[test]
fn test_iterator_budget_bounded_across_calls() {
const N: usize = 30;
const _: () = assert!(N <= u8::MAX as usize, "N must fit in u8");
let ee = pkits_cert("ValidCertificatePathTest1EE");
let template_ca = pkits_cert("GoodCACert");
let fake_anchor = TrustAnchor::from(&pkits_cert("BadSignedCACert"));
let mut pool = CertPool::new();
for i in 0..N {
let mut ca = template_ca.clone();
ca.tbs_certificate.issuer = ca.tbs_certificate.subject.clone();
ca.tbs_certificate
.subject_public_key_info
.subject_public_key = BitString::new(
0,
vec![u8::try_from(i).expect("loop bound N fits in u8"); 32],
)
.expect("BitString construction must succeed for valid parameters");
pool.add(ca);
}
let start = std::time::Instant::now();
let mut iter = build_path_candidates(&ee, &pool, std::slice::from_ref(&fake_anchor));
let mut got_budget_exceeded = false;
let mut yielded_chains = 0_usize;
let mut steps = 0_usize;
loop {
match iter.next() {
None => break,
Some(Ok(_)) => {
yielded_chains += 1;
}
Some(Err(pkix_path_builder::Error::BudgetExceeded)) => {
got_budget_exceeded = true;
break;
}
Some(Err(e)) => panic!("unexpected error: {e}"),
}
steps += 1;
assert!(
steps < 50_000,
"iterator did not terminate; budget appears unbounded"
);
}
let elapsed = start.elapsed();
assert!(
got_budget_exceeded,
"iterator must yield BudgetExceeded on adversarial pool; \
yielded {yielded_chains} chains and {steps} steps before termination"
);
assert!(
elapsed.as_secs() < 2,
"iterator took {elapsed:?}; budget enforcement must prevent exponential blowup"
);
let after_error = iter.next();
assert!(
after_error.is_none(),
"iterator must be exhausted after BudgetExceeded; got {after_error:?}"
);
}
#[test]
fn test_iterator_max_depth_zero_respects_trivial_chain() {
let target = pkits_cert("GoodCACert");
let anchor = pkits_trust_anchor();
let pool = CertPool::new();
let mut config = PathBuilderConfig::new();
config.max_depth = 0;
config.dfs_budget = 1000;
let mut iter =
build_path_candidates_with_config(&target, &pool, std::slice::from_ref(&anchor), &config);
let chain = iter
.next()
.expect("iterator must yield the trivial chain")
.expect("no error expected on trivial chain");
assert_eq!(
chain.len(),
1,
"max_depth=0 yields the target alone (anchor matches at frame 0)"
);
assert!(
iter.next().is_none(),
"iterator must exhaust after one yield"
);
}
#[test]
fn test_build_path_skips_malformed_bc_when_valid_alternative_exists() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let valid_intermediate = pkits_cert("GoodCACert");
let malformed = corrupt_basic_constraints(pkits_cert("GoodCACert"));
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(malformed);
pool.add(valid_intermediate);
let path = build_path(&ee, &pool, std::slice::from_ref(&anchor))
.expect("skip-not-fail: malformed BC must be skipped, valid intermediate selected");
let policy = ValidationPolicy::new(PKITS_NOW);
let verifier = DefaultVerifier;
pkix_path::validate_path(&path, &[anchor], &policy, &verifier)
.expect("validate_path must succeed; this proves the well-formed GoodCACert was selected");
}
#[test]
fn test_build_path_no_path_found_when_only_intermediate_has_malformed_bc() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let malformed = corrupt_basic_constraints(pkits_cert("GoodCACert"));
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(malformed);
let err = build_path(&ee, &pool, std::slice::from_ref(&anchor))
.expect_err("with only a malformed-BC intermediate, no path can be built");
assert!(
matches!(err, pkix_path_builder::Error::NoPathFound),
"expected NoPathFound (skip-not-fail semantics); got {err}"
);
}
#[test]
fn test_build_path_skips_malformed_bc_independent_of_pool_order() {
let ee = pkits_cert("ValidCertificatePathTest1EE");
let valid_intermediate = pkits_cert("GoodCACert");
let malformed = corrupt_basic_constraints(pkits_cert("GoodCACert"));
let anchor = pkits_trust_anchor();
let mut pool = CertPool::new();
pool.add(valid_intermediate);
pool.add(malformed);
let path = build_path(&ee, &pool, std::slice::from_ref(&anchor))
.expect("build_path must succeed regardless of which cert is first in the pool");
let policy = ValidationPolicy::new(PKITS_NOW);
let verifier = DefaultVerifier;
pkix_path::validate_path(&path, &[anchor], &policy, &verifier)
.expect("validate_path must succeed on the built chain");
}