use super::*;
use der::Encode;
use rcgen::{
BasicConstraints, CertificateParams, CustomExtension, DistinguishedName, DnType,
ExtendedKeyUsagePurpose, IsCa, KeyPair, SerialNumber,
};
use time::macros::datetime;
use x509_cert::ext::pkix::ExtendedKeyUsage as X509Eku;
fn make_cert(configure: impl FnOnce(&mut CertificateParams)) -> (Vec<u8>, String) {
let key = KeyPair::generate().expect("keypair");
let mut params = CertificateParams::new(vec!["test.example".to_string()]).expect("params");
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "Encypher Test Root CA");
params.distinguished_name = dn;
params.not_before = datetime!(2025-01-01 0:00 UTC);
params.not_after = datetime!(2027-01-01 0:00 UTC);
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
configure(&mut params);
let cert = params.self_signed(&key).expect("self-signed cert");
(cert.der().as_ref().to_vec(), cert.pem())
}
fn make_named_ca(common_name: &str) -> (rcgen::Certificate, KeyPair) {
let key = KeyPair::generate().expect("CA key");
let mut params = CertificateParams::new(vec!["ca.example".to_string()]).expect("CA params");
let mut name = DistinguishedName::new();
name.push(DnType::CommonName, common_name);
params.distinguished_name = name;
params.not_before = datetime!(2025-01-01 0:00 UTC);
params.not_after = datetime!(2030-01-01 0:00 UTC);
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
rcgen::KeyUsagePurpose::KeyCertSign,
rcgen::KeyUsagePurpose::DigitalSignature,
];
let certificate = params.self_signed(&key).expect("self-signed CA");
(certificate, key)
}
fn eku_value(oid: &str) -> Vec<u8> {
let oid = const_oid::ObjectIdentifier::new_unwrap(oid);
X509Eku(vec![oid]).to_der().expect("encode eku")
}
fn eku_value_multi(oids: &[&str]) -> Vec<u8> {
X509Eku(
oids.iter()
.map(|oid| const_oid::ObjectIdentifier::new_unwrap(oid))
.collect(),
)
.to_der()
.expect("encode eku")
}
fn leaf_with_ekus(oids: &[&str]) -> Vec<u8> {
let (der, _) = make_cert(|p| {
p.is_ca = IsCa::ExplicitNoCa;
p.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
p.custom_extensions.push(CustomExtension::from_oid_content(
&[2, 5, 29, 37],
eku_value_multi(oids),
));
});
der
}
#[test]
fn claim_signer_profile_accepts_the_full_reference_eku_set() {
for oid in [
OID_C2PA_CLAIM_SIGNING,
OID_ADOBE_DOCUMENT_SIGNING,
OID_IETF_DOCUMENT_SIGNING,
OID_EMAIL_PROTECTION,
OID_MICROSOFT_C2PA,
] {
assert!(
leaf_acceptable_der(&leaf_with_ekus(&[oid])),
"{oid} must be an acceptable claim-signer EKU"
);
}
assert!(!leaf_acceptable_der(&leaf_with_ekus(&[
"1.3.6.1.5.5.7.3.1"
])));
assert!(!leaf_acceptable_der(&leaf_with_ekus(&[
OID_EMAIL_PROTECTION,
"2.5.29.37.0"
])));
}
#[test]
fn timestamping_and_ocsp_ekus_are_acceptable_only_when_sole() {
assert!(leaf_acceptable_der(&leaf_with_ekus(&[
OID_KP_TIME_STAMPING
])));
assert!(leaf_acceptable_der(&leaf_with_ekus(&[OID_KP_OCSP_SIGNING])));
assert!(!leaf_acceptable_der(&leaf_with_ekus(&[
OID_KP_TIME_STAMPING,
OID_EMAIL_PROTECTION
])));
assert!(!leaf_acceptable_der(&leaf_with_ekus(&[
OID_KP_OCSP_SIGNING,
OID_C2PA_CLAIM_SIGNING
])));
}
#[test]
fn trust_list_anchor_subjects() {
let (_, pem) = make_cert(|_| {});
let trust = TrustList::from_pem(&pem).expect("parse pem");
assert_eq!(trust.anchors.len(), 1);
assert_eq!(
trust.anchor_subjects(),
vec!["Encypher Test Root CA".to_string()]
);
}
#[test]
fn from_pem_rejects_empty_input() {
assert!(matches!(
TrustList::from_pem("not a certificate"),
Err(TrustError::NoCertificates) | Err(TrustError::Decode(_))
));
assert!(matches!(
TrustList::from_pem(""),
Err(TrustError::NoCertificates)
));
assert!(matches!(
TrustList::from_pem("\n"),
Err(TrustError::NoCertificates)
));
}
#[test]
fn self_signed_cert_chains_to_itself_as_anchor() {
let (der, _) = make_cert(|_| {});
let trust = TrustList {
anchors: vec![der.clone()],
};
let at = Some(datetime!(2026-01-01 0:00 UTC));
let result = validate_chain(&der, &[], &trust, at);
assert!(
result.trusted,
"self-signed anchor should be trusted: {:?}",
result.reason
);
assert!(result.reason.is_none());
}
#[test]
fn untrusted_when_anchor_not_present() {
let (der, _) = make_cert(|_| {});
let empty = TrustList::default();
let at = Some(datetime!(2026-01-01 0:00 UTC));
let result = validate_chain(&der, &[], &empty, at);
assert!(!result.trusted);
assert!(result.reason.unwrap().contains("does not chain"));
}
#[test]
fn chain_builder_skips_same_subject_ca_with_wrong_key() {
let (wrong_issuer, _) = make_named_ca("Shared Test CA");
let (issuer, issuer_key) = make_named_ca("Shared Test CA");
let leaf_key = KeyPair::generate().expect("leaf key");
let mut params = CertificateParams::new(vec!["leaf.example".to_string()]).expect("leaf params");
params.not_before = datetime!(2025-01-01 0:00 UTC);
params.not_after = datetime!(2027-01-01 0:00 UTC);
params.is_ca = IsCa::NoCa;
params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
let leaf = params
.signed_by(&leaf_key, &issuer, &issuer_key)
.expect("issued leaf");
let trust = TrustList {
anchors: vec![
wrong_issuer.der().as_ref().to_vec(),
issuer.der().as_ref().to_vec(),
],
};
let result = validate_chain(
leaf.der().as_ref(),
&[],
&trust,
Some(datetime!(2026-01-01 0:00 UTC)),
);
assert!(result.trusted, "{:?}", result.reason);
}
#[test]
fn validation_time_before_not_before_is_untrusted() {
let (der, _) = make_cert(|_| {});
let trust = TrustList {
anchors: vec![der.clone()],
};
let before = Some(datetime!(2020-06-01 0:00 UTC));
let result = validate_chain(&der, &[], &trust, before);
assert!(!result.trusted);
assert!(result
.reason
.unwrap()
.contains("outside its validity window"));
assert_eq!(result.validated_at, datetime!(2020-06-01 0:00 UTC));
}
#[test]
fn validation_time_within_window_is_trusted() {
let (der, _) = make_cert(|_| {});
let trust = TrustList {
anchors: vec![der.clone()],
};
let within = Some(datetime!(2026-06-01 0:00 UTC));
let result = validate_chain(&der, &[], &trust, within);
assert!(
result.trusted,
"should be trusted within validity: {:?}",
result.reason
);
assert_eq!(result.validated_at, datetime!(2026-06-01 0:00 UTC));
}
#[test]
fn validation_time_after_not_after_is_untrusted() {
let (der, _) = make_cert(|_| {});
let trust = TrustList {
anchors: vec![der.clone()],
};
let after = Some(datetime!(2030-01-01 0:00 UTC));
let result = validate_chain(&der, &[], &trust, after);
assert!(!result.trusted);
assert!(result
.reason
.unwrap()
.contains("outside its validity window"));
}
#[test]
fn eku_policy_accepts_claim_signing_oid() {
let (der, _) = make_cert(|p| {
p.custom_extensions.push(CustomExtension::from_oid_content(
&[2, 5, 29, 37],
eku_value(OID_C2PA_CLAIM_SIGNING),
));
});
let policy = EkuPolicy::default();
assert!(policy.cert_has_required_eku(&der));
}
#[test]
fn eku_policy_rejects_cert_without_allowed_oid() {
let (der, _) = make_cert(|p| {
p.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
});
let policy = EkuPolicy::default();
assert!(!policy.cert_has_required_eku(&der));
}
#[test]
fn eku_policy_rejects_cert_without_eku_extension() {
let (der, _) = make_cert(|_| {});
let policy = EkuPolicy::default();
assert!(!policy.cert_has_required_eku(&der));
}
#[test]
fn revocation_denylist_matches_by_serial() {
let (der, _) = make_cert(|p| {
p.serial_number = Some(SerialNumber::from(0x1234u64));
});
let denylist = RevocationDenylist::new(["1234".to_string()], std::iter::empty());
assert!(denylist.is_revoked(&der));
let empty = RevocationDenylist::default();
assert!(!empty.is_revoked(&der));
let other = RevocationDenylist::new(["dead".to_string()], std::iter::empty());
assert!(!other.is_revoked(&der));
}
#[test]
fn revocation_denylist_matches_by_fingerprint() {
let (der, _) = make_cert(|_| {});
let fp = hex::encode(Sha256::digest(&der));
let denylist = RevocationDenylist::new(std::iter::empty(), [fp.to_uppercase()]);
assert!(denylist.is_revoked(&der));
}