use crate::error::TrustError;
use der::{Decode, Encode};
use std::path::Path;
use x509_cert::Certificate;
fn basic_constraints(cert: &Certificate) -> Result<(bool, Option<u64>), TrustError> {
let bc_oid = const_oid::ObjectIdentifier::new_unwrap("2.5.29.19");
let Some(extensions) = &cert.tbs_certificate.extensions else {
return Ok((false, None));
};
for ext in extensions.iter() {
if ext.extn_id == bc_oid {
return crate::der_utils::parse_basic_constraints(ext.extn_value.as_bytes())
.map_err(TrustError::CertificateParse);
}
}
Ok((false, None))
}
fn is_self_issued(cert: &Certificate) -> bool {
cert.tbs_certificate.subject == cert.tbs_certificate.issuer
}
fn key_cert_sign(cert: &Certificate) -> Result<Option<bool>, TrustError> {
let ku_oid = const_oid::ObjectIdentifier::new_unwrap("2.5.29.15");
let Some(extensions) = &cert.tbs_certificate.extensions else {
return Ok(None);
};
let Some(ext) = extensions.iter().find(|e| e.extn_id == ku_oid) else {
return Ok(None);
};
parse_key_cert_sign_bit(ext.extn_value.as_bytes()).map(Some)
}
fn parse_key_cert_sign_bit(extn_value: &[u8]) -> Result<bool, TrustError> {
let (tag, body, rest) = crate::der_utils::parse_tlv_with_rest(extn_value)
.map_err(|e| TrustError::CertificateParse(format!("keyUsage: {e}")))?;
if !rest.is_empty() {
return Err(TrustError::CertificateParse(
"keyUsage: trailing data after BIT STRING".into(),
));
}
if tag != 0x03 {
return Err(TrustError::CertificateParse(format!(
"keyUsage: expected BIT STRING (0x03), got 0x{tag:02x}"
)));
}
if body.is_empty() {
return Err(TrustError::CertificateParse(
"keyUsage: BIT STRING missing the unused-bits octet".into(),
));
}
let unused_bits = body[0];
if unused_bits > 7 {
return Err(TrustError::CertificateParse(format!(
"keyUsage: invalid unused-bits count {unused_bits} (must be 0..=7)"
)));
}
let bit_bytes = &body[1..];
if bit_bytes.is_empty() {
return Err(TrustError::CertificateParse(
"keyUsage: BIT STRING has no content octets (no key-usage bits set)".into(),
));
}
if unused_bits > 0 {
let last = bit_bytes[bit_bytes.len() - 1];
let pad_mask = (1u8 << unused_bits) - 1;
if last & pad_mask != 0 {
return Err(TrustError::CertificateParse(format!(
"keyUsage: BIT STRING has {unused_bits} non-zero unused bit(s) (not valid DER)"
)));
}
}
Ok((bit_bytes[0] >> 2) & 1 == 1)
}
fn validate_intermediate_ca_extensions(cert: &Certificate, label: &str) -> Result<(), TrustError> {
let (is_ca, _) = basic_constraints(cert)?;
if !is_ca {
return Err(TrustError::ProfileViolation(format!(
"{label} is not a CA (basicConstraints cA is not TRUE)"
)));
}
match key_cert_sign(cert)? {
Some(true) | None => Ok(()),
Some(false) => Err(TrustError::ProfileViolation(format!(
"{label} keyUsage is present but does not assert keyCertSign"
))),
}
}
fn enforce_path_len(
issuer: &Certificate,
below: &[Certificate],
label: &str,
) -> Result<(), TrustError> {
let (_is_ca, path_len) = basic_constraints(issuer).map_err(|e| {
TrustError::ProfileViolation(format!("failed to parse basicConstraints for {label}: {e}"))
})?;
let Some(max_depth) = path_len else {
return Ok(());
};
let mut subordinate_ca_count = 0u64;
for cert in below {
let (is_ca, _) = basic_constraints(cert).map_err(|e| {
TrustError::ProfileViolation(format!(
"failed to parse basicConstraints below {label}: {e}"
))
})?;
if is_ca && !is_self_issued(cert) {
subordinate_ca_count += 1;
}
}
if subordinate_ca_count > max_depth {
return Err(TrustError::ProfileViolation(format!(
"pathLenConstraint ({max_depth}) exceeded for {label}: {subordinate_ca_count} non-self-issued subordinate CA certs below"
)));
}
Ok(())
}
const RECOGNIZED_CRITICAL_EXT_OIDS: &[&str] = &[
"2.5.29.19", "2.5.29.15", "2.5.29.37", ];
const PEM_ARMOR: &[u8] = b"-----BEGIN";
#[cfg(feature = "ltv")]
const RECOGNIZED_CRITICAL_EXT_OIDS_LTV: &[&str] = &[
"2.5.29.17", "2.5.29.31", "1.3.6.1.5.5.7.1.1", crate::ltv::name_constraints::NAME_CONSTRAINTS_OID, ];
fn reject_unknown_critical_extensions(cert: &Certificate, label: &str) -> Result<(), TrustError> {
let Some(extensions) = &cert.tbs_certificate.extensions else {
return Ok(());
};
for ext in extensions.iter() {
if !ext.critical {
continue;
}
let oid = ext.extn_id.to_string();
#[allow(unused_mut)]
let mut recognized = RECOGNIZED_CRITICAL_EXT_OIDS.contains(&oid.as_str());
#[cfg(feature = "ltv")]
{
if !recognized && RECOGNIZED_CRITICAL_EXT_OIDS_LTV.contains(&oid.as_str()) {
recognized = true;
}
}
if !recognized {
return Err(TrustError::ProfileViolation(format!(
"{label} has an unrecognized critical extension {oid} (RFC 5280 §4.2 MUST reject)"
)));
}
}
Ok(())
}
#[cfg(feature = "ltv")]
fn enforce_name_constraints_path(path: &[&Certificate]) -> Result<(), TrustError> {
use crate::ltv::name_constraints::NameConstraintState;
let mut state = NameConstraintState::default();
let n = path.len();
for top in (1..n).rev() {
let issuer = path[top];
state
.add_from_cert(issuer)
.map_err(|e| name_constraint_to_trust_error(issuer, e))?;
if state.is_empty() {
continue;
}
for sub in path.iter().take(top) {
state
.check_cert(sub)
.map_err(|e| name_constraint_to_trust_error(sub, e))?;
}
}
Ok(())
}
#[cfg(feature = "ltv")]
fn name_constraint_to_trust_error(
cert: &Certificate,
e: crate::ltv::name_constraints::NameConstraintError,
) -> TrustError {
let subject = format!("{}", cert.tbs_certificate.subject);
TrustError::ProfileViolation(format!("certificate '{subject}': {e}"))
}
#[derive(Clone)]
struct TrustAnchor {
cert: Certificate,
der: Vec<u8>,
}
#[derive(Clone)]
pub struct TrustStore {
anchors: Vec<TrustAnchor>,
label: Option<String>,
signature_policy: crate::crypto::verify::SignaturePolicy,
}
impl TrustStore {
pub fn new() -> Self {
Self {
anchors: Vec::new(),
label: None,
signature_policy: crate::crypto::verify::SignaturePolicy::default(),
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_signature_policy(mut self, policy: crate::crypto::verify::SignaturePolicy) -> Self {
self.signature_policy = policy;
self
}
pub fn allow_legacy_signatures(mut self) -> Self {
self.signature_policy = crate::crypto::verify::SignaturePolicy::allow_legacy();
self
}
pub fn signature_policy(&self) -> crate::crypto::verify::SignaturePolicy {
self.signature_policy
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn len(&self) -> usize {
self.anchors.len()
}
pub fn is_empty(&self) -> bool {
self.anchors.is_empty()
}
pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, TrustError> {
let data = std::fs::read(path.as_ref()).map_err(TrustError::Io)?;
let mut store = Self::new();
store.add_pem_data(&data)?;
Ok(store)
}
pub fn from_pem_directory(dir: impl AsRef<Path>) -> Result<Self, TrustError> {
let dir = dir.as_ref();
if !dir.is_dir() {
return Err(TrustError::NotADirectory(dir.display().to_string()));
}
let mut store = Self::new();
let mut entries: Vec<_> = std::fs::read_dir(dir)
.map_err(TrustError::Io)?
.collect::<std::io::Result<Vec<_>>>()
.map_err(TrustError::Io)?;
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
if ext == "pem" || ext == "crt" || ext == "cer" {
let data = std::fs::read(&path).map_err(|e| {
TrustError::Io(std::io::Error::new(
e.kind(),
format!("failed to read trust anchor file {}: {e}", path.display()),
))
})?;
store.add_anchor_file_data(&data).map_err(|e| {
let detail = match e {
TrustError::CertificateParse(m) => m,
other => other.to_string(),
};
TrustError::CertificateParse(format!(
"failed to parse trust anchor file {}: {detail}",
path.display()
))
})?;
}
}
}
Ok(store)
}
pub fn from_pem_directory_lenient(
dir: impl AsRef<Path>,
) -> Result<(Self, Vec<(std::path::PathBuf, String)>), TrustError> {
let dir = dir.as_ref();
if !dir.is_dir() {
return Err(TrustError::NotADirectory(dir.display().to_string()));
}
let mut store = Self::new();
let mut skipped: Vec<(std::path::PathBuf, String)> = Vec::new();
let mut entries: Vec<std::fs::DirEntry> = Vec::new();
for entry in std::fs::read_dir(dir).map_err(TrustError::Io)? {
match entry {
Ok(entry) => entries.push(entry),
Err(e) => {
log::warn!(
"skipping unreadable directory entry in {}: {e}",
dir.display()
);
skipped.push((dir.to_path_buf(), format!("directory entry error: {e}")));
}
}
}
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
if ext == "pem" || ext == "crt" || ext == "cer" {
match std::fs::read(&path) {
Ok(data) => {
if let Err(e) = store.add_anchor_file_data(&data) {
log::warn!(
"skipping unparseable trust anchor {}: {e}",
path.display()
);
skipped.push((path.clone(), e.to_string()));
}
}
Err(e) => {
log::warn!("skipping unreadable trust anchor {}: {e}", path.display());
skipped.push((path.clone(), e.to_string()));
}
}
}
}
}
Ok((store, skipped))
}
fn add_anchor_file_data(&mut self, data: &[u8]) -> Result<(), TrustError> {
if data.trim_ascii_start().starts_with(PEM_ARMOR) {
self.add_pem_data(data)
} else {
self.add_der_certificate(data)
}
}
pub fn add_der_certificate(&mut self, der: &[u8]) -> Result<(), TrustError> {
let cert = Certificate::from_der(der)
.map_err(|e| TrustError::CertificateParse(format!("DER decode failed: {e}")))?;
self.anchors.push(TrustAnchor {
cert,
der: der.to_vec(),
});
Ok(())
}
pub fn add_certificate(&mut self, cert: Certificate) -> Result<(), TrustError> {
let der = cert
.to_der()
.map_err(|e| TrustError::CertificateParse(format!("DER encode failed: {e}")))?;
self.anchors.push(TrustAnchor { cert, der });
Ok(())
}
pub fn add_pem_data(&mut self, pem_data: &[u8]) -> Result<(), TrustError> {
let pem_str = std::str::from_utf8(pem_data)
.map_err(|e| TrustError::CertificateParse(format!("invalid UTF-8 in PEM: {e}")))?;
let mut found_any = false;
let mut remaining = pem_str;
while let Some(begin_pos) = remaining.find("-----BEGIN CERTIFICATE-----") {
let block_start = &remaining[begin_pos..];
if let Some(end_pos) = block_start.find("-----END CERTIFICATE-----") {
let end = end_pos + "-----END CERTIFICATE-----".len();
let pem_block = &block_start[..end];
let b64: String = pem_block
.lines()
.filter(|line| !line.starts_with("-----"))
.collect();
use base64::Engine;
let der_bytes = base64::engine::general_purpose::STANDARD
.decode(&b64)
.map_err(|e| {
TrustError::CertificateParse(format!("base64 decode error: {e}"))
})?;
self.add_der_certificate(&der_bytes)?;
found_any = true;
remaining = &block_start[end..];
} else {
break;
}
}
if !found_any {
return Err(TrustError::CertificateParse(
"no CERTIFICATE blocks found in PEM data".into(),
));
}
Ok(())
}
pub fn contains_der(&self, cert_der: &[u8]) -> bool {
self.anchors.iter().any(|a| a.der == cert_der)
}
pub fn find_issuer(&self, cert: &Certificate) -> Option<&Certificate> {
let issuer = &cert.tbs_certificate.issuer;
self.anchors.iter().find_map(|anchor| {
if &anchor.cert.tbs_certificate.subject == issuer {
Some(&anchor.cert)
} else {
None
}
})
}
fn find_all_issuers(&self, cert: &Certificate) -> Vec<&Certificate> {
let issuer = &cert.tbs_certificate.issuer;
self.anchors
.iter()
.filter_map(|anchor| {
if &anchor.cert.tbs_certificate.subject == issuer {
Some(&anchor.cert)
} else {
None
}
})
.collect()
}
pub fn find_issuer_for_der(&self, cert_der: &[u8]) -> Option<&Certificate> {
let cert = Certificate::from_der(cert_der).ok()?;
self.find_issuer(&cert)
}
pub fn certificates(&self) -> impl Iterator<Item = &Certificate> {
self.anchors.iter().map(|a| &a.cert)
}
pub fn certificates_der(&self) -> impl Iterator<Item = &[u8]> {
self.anchors.iter().map(|a| a.der.as_slice())
}
pub fn verify_chain(
&self,
chain: &[Certificate],
validation_time: Option<der::DateTime>,
) -> Result<&Certificate, TrustError> {
self.verify_chain_inner(chain, validation_time, None)
}
#[cfg(feature = "ltv")]
pub fn verify_chain_for_purpose(
&self,
chain: &[Certificate],
validation_time: Option<der::DateTime>,
purpose: crate::ltv::CertRole,
) -> Result<&Certificate, TrustError> {
self.verify_chain_inner(chain, validation_time, Some(purpose))
}
fn verify_chain_inner(
&self,
chain: &[Certificate],
validation_time: Option<der::DateTime>,
#[cfg(feature = "ltv")] leaf_purpose: Option<crate::ltv::CertRole>,
#[cfg(not(feature = "ltv"))] leaf_purpose: Option<()>,
) -> Result<&Certificate, TrustError> {
let policy = &self.signature_policy;
if chain.is_empty() {
return Err(TrustError::EmptyChain);
}
for (i, cert) in chain.iter().enumerate() {
reject_unknown_critical_extensions(cert, &format!("certificate at index {i}"))?;
}
#[cfg(feature = "ltv")]
if let Some(role) = leaf_purpose {
crate::ltv::validate_extensions_for_role(&chain[0], role).map_err(|e| {
TrustError::ProfileViolation(format!(
"leaf certificate does not satisfy required purpose {role}: {e}"
))
})?;
}
#[cfg(not(feature = "ltv"))]
let _ = leaf_purpose;
if let Some(time) = validation_time {
for (i, cert) in chain.iter().enumerate() {
let validity = &cert.tbs_certificate.validity;
if time < validity.not_before.to_date_time() {
return Err(TrustError::NotYetValid {
index: i,
not_before: validity.not_before.to_date_time(),
});
}
if time > validity.not_after.to_date_time() {
return Err(TrustError::Expired {
index: i,
not_after: validity.not_after.to_date_time(),
});
}
}
}
for i in 0..chain.len().saturating_sub(1) {
let cert = &chain[i];
let issuer_cert = &chain[i + 1];
if cert.tbs_certificate.issuer != issuer_cert.tbs_certificate.subject {
return Err(TrustError::ChainBroken {
index: i,
expected_issuer: format!("{}", cert.tbs_certificate.issuer),
found_subject: format!("{}", issuer_cert.tbs_certificate.subject),
});
}
crate::crypto::verify::verify_certificate_signature_with_policy(
cert,
issuer_cert,
policy,
)?;
validate_intermediate_ca_extensions(
issuer_cert,
&format!("issuer at index {}", i + 1),
)?;
enforce_path_len(
issuer_cert,
&chain[0..=i],
&format!("intermediate index {}", i + 1),
)?;
}
let last = chain.last().unwrap();
if last.tbs_certificate.issuer == last.tbs_certificate.subject {
let last_der = last.to_der().map_err(|e| {
TrustError::CertificateParse(format!(
"failed to encode certificate for anchor lookup: {e}"
))
})?;
if let Some(anchor) = self.anchors.iter().find(|a| a.der == last_der) {
crate::crypto::verify::verify_certificate_signature_with_policy(
last, last, policy,
)?;
#[cfg(feature = "ltv")]
{
let path: Vec<&Certificate> = chain.iter().collect();
enforce_name_constraints_path(&path)?;
}
return Ok(&anchor.cert);
}
}
let candidates = self.find_all_issuers(last);
if candidates.is_empty() {
return Err(TrustError::UntrustedRoot {
issuer: format!("{}", last.tbs_certificate.issuer),
});
}
let mut last_err = None;
for anchor in &candidates {
match crate::crypto::verify::verify_certificate_signature_with_policy(
last, anchor, policy,
) {
Ok(()) => {
reject_unknown_critical_extensions(anchor, "trust anchor")?;
validate_intermediate_ca_extensions(anchor, "trust anchor")?;
enforce_path_len(anchor, chain, "trust anchor")?;
if let Some(time) = validation_time {
let validity = &anchor.tbs_certificate.validity;
if time < validity.not_before.to_date_time() {
return Err(TrustError::NotYetValid {
index: chain.len(),
not_before: validity.not_before.to_date_time(),
});
}
if time > validity.not_after.to_date_time() {
return Err(TrustError::Expired {
index: chain.len(),
not_after: validity.not_after.to_date_time(),
});
}
}
#[cfg(feature = "ltv")]
{
let mut path: Vec<&Certificate> = chain.iter().collect();
path.push(anchor);
enforce_name_constraints_path(&path)?;
}
return Ok(anchor);
}
Err(e) => {
last_err = Some(e);
}
}
}
Err(last_err.unwrap())
}
}
impl Default for TrustStore {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for TrustStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrustStore")
.field("label", &self.label)
.field("anchors", &self.anchors.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::verify::SignaturePolicy;
fn sha1_self_signed_root() -> Certificate {
use der::asn1::BitString;
use der::{Any, Encode};
use rsa::pkcs1v15::SigningKey;
use rsa::signature::Keypair;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
use sha1::{Digest, Sha1};
use sha2::Sha256;
use spki::AlgorithmIdentifierOwned;
use x509_cert::builder::{Builder, CertificateBuilder, Profile};
use x509_cert::name::Name;
use x509_cert::serial_number::SerialNumber;
use x509_cert::spki::SubjectPublicKeyInfoOwned;
use x509_cert::time::Validity;
let mut rng = rand::thread_rng();
let key = RsaPrivateKey::new(&mut rng, 2048).expect("RSA keygen");
let signer = SigningKey::<Sha256>::new(key.clone());
let spki =
SubjectPublicKeyInfoOwned::from_key(signer.verifying_key()).expect("SPKI from key");
let serial = SerialNumber::new(&[0x01]).unwrap();
let validity =
Validity::from_now(std::time::Duration::from_secs(3650 * 24 * 3600)).unwrap();
let subject: Name = "CN=Legacy SHA-1 Root,O=tsp-ltv tests".parse().unwrap();
let base = CertificateBuilder::new(Profile::Root, serial, validity, subject, spki, &signer)
.expect("cert builder")
.build()
.expect("build base root");
let sha1_algid = AlgorithmIdentifierOwned {
oid: crate::crypto::algorithm::OID_SHA1_WITH_RSA,
parameters: Some(Any::from_der(&[0x05, 0x00]).unwrap()),
};
let mut tbs = base.tbs_certificate.clone();
tbs.signature = sha1_algid.clone();
let tbs_der = tbs.to_der().unwrap();
let hash = Sha1::digest(&tbs_der);
let sig = key
.sign(Pkcs1v15Sign::new::<Sha1>(), &hash)
.expect("SHA-1 RSA sign");
Certificate {
tbs_certificate: tbs,
signature_algorithm: sha1_algid,
signature: BitString::from_bytes(&sig).unwrap(),
}
}
#[test]
fn test_verify_chain_rejects_sha1_by_default_and_accepts_with_legacy() {
let root = sha1_self_signed_root();
assert!(crate::crypto::verify::is_weak_signature_oid(
&root.signature_algorithm.oid
));
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
let chain = [root];
let err = store.verify_chain(&chain, None).unwrap_err();
assert!(
matches!(err, TrustError::WeakAlgorithm(_)),
"strict store must reject a SHA-1-signed chain, got {err:?}"
);
let legacy_store = TrustStore::new()
.allow_legacy_signatures()
.with_label("legacy");
let mut legacy_store = legacy_store;
legacy_store.add_certificate(chain[0].clone()).unwrap();
legacy_store
.verify_chain(&chain, None)
.expect("legacy store must accept a SHA-1-signed chain");
assert_eq!(
legacy_store.signature_policy(),
SignaturePolicy::allow_legacy()
);
assert_eq!(store.signature_policy(), SignaturePolicy::strict());
}
fn issue_cert(
profile: x509_cert::builder::Profile,
subject: &str,
subject_key: &rsa::RsaPrivateKey,
issuer_signer: &rsa::pkcs1v15::SigningKey<sha2::Sha256>,
) -> Certificate {
use rsa::pkcs1v15::SigningKey;
use rsa::signature::Keypair;
use sha2::Sha256;
use x509_cert::builder::{Builder, CertificateBuilder};
use x509_cert::name::Name;
use x509_cert::serial_number::SerialNumber;
use x509_cert::spki::SubjectPublicKeyInfoOwned;
use x509_cert::time::Validity;
let subject_signer = SigningKey::<Sha256>::new(subject_key.clone());
let spki = SubjectPublicKeyInfoOwned::from_key(subject_signer.verifying_key())
.expect("SPKI from key");
let serial = SerialNumber::new(&[0x01]).unwrap();
let validity =
Validity::from_now(std::time::Duration::from_secs(3650 * 24 * 3600)).unwrap();
let subject: Name = subject.parse().unwrap();
CertificateBuilder::new(profile, serial, validity, subject, spki, issuer_signer)
.expect("cert builder")
.build()
.expect("build cert")
}
fn ca_leaf_chain(path_len: Option<u8>) -> (TrustStore, Vec<Certificate>) {
use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
use x509_cert::name::Name;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).expect("root key");
let mid_key = RsaPrivateKey::new(&mut rng, 2048).expect("mid key");
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).expect("leaf key");
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let mid_signer = SigningKey::<Sha256>::new(mid_key.clone());
let root_name = "CN=PathLen Root,O=tsp-ltv tests";
let mid_name = "CN=PathLen Mid,O=tsp-ltv tests";
let leaf_name = "CN=PathLen Leaf CA,O=tsp-ltv tests";
let root_issuer: Name = root_name.parse().unwrap();
let mid_issuer: Name = mid_name.parse().unwrap();
let root = issue_cert(Profile::Root, root_name, &root_key, &root_signer);
let mid = issue_cert(
Profile::SubCA {
issuer: root_issuer,
path_len_constraint: path_len,
},
mid_name,
&mid_key,
&root_signer,
);
let leaf = issue_cert(
Profile::SubCA {
issuer: mid_issuer,
path_len_constraint: None,
},
leaf_name,
&leaf_key,
&mid_signer,
);
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
(store, vec![leaf, mid, root])
}
#[test]
fn test_verify_chain_pathlen_rejects_ca_leaf_below_zero_constraint() {
let (store, chain) = ca_leaf_chain(Some(0));
let err = store
.verify_chain(&chain, None)
.expect_err("pathLen=0 with a CA leaf below must be rejected");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("pathLenConstraint")),
"expected pathLenConstraint rejection, got: {err:?}"
);
}
#[test]
fn test_verify_chain_pathlen_allows_ca_leaf_within_constraint() {
let (store, chain) = ca_leaf_chain(Some(1));
store
.verify_chain(&chain, None)
.expect("pathLen=1 with one CA leaf below must be accepted");
}
fn anchor_only_chain(root_plen: Option<u8>) -> (TrustStore, Vec<Certificate>) {
use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
use x509_cert::name::Name;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).expect("root key");
let mid_key = RsaPrivateKey::new(&mut rng, 2048).expect("mid key");
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).expect("leaf key");
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let mid_signer = SigningKey::<Sha256>::new(mid_key.clone());
let root_name = "CN=Anchor Root,O=tsp-ltv tests";
let mid_name = "CN=Anchor Mid,O=tsp-ltv tests";
let leaf_name = "CN=Anchor Leaf CA,O=tsp-ltv tests";
let root_issuer: Name = root_name.parse().unwrap();
let mid_issuer: Name = mid_name.parse().unwrap();
let root = issue_cert(
Profile::SubCA {
issuer: root_issuer.clone(),
path_len_constraint: root_plen,
},
root_name,
&root_key,
&root_signer,
);
let mid = issue_cert(
Profile::SubCA {
issuer: root_issuer,
path_len_constraint: None,
},
mid_name,
&mid_key,
&root_signer,
);
let leaf = issue_cert(
Profile::SubCA {
issuer: mid_issuer,
path_len_constraint: None,
},
leaf_name,
&leaf_key,
&mid_signer,
);
let mut store = TrustStore::new();
store.add_certificate(root).unwrap();
(store, vec![leaf, mid])
}
#[test]
fn test_verify_chain_anchor_pathlen_enforced() {
let (store, chain) = anchor_only_chain(Some(0));
let err = store
.verify_chain(&chain, None)
.expect_err("anchor pathLen=0 with CAs below must be rejected");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("pathLenConstraint") && m.contains("trust anchor")),
"expected trust-anchor pathLenConstraint rejection, got: {err:?}"
);
}
#[test]
fn test_verify_chain_anchor_pathlen_within_constraint() {
let (store, chain) = anchor_only_chain(Some(2));
store
.verify_chain(&chain, None)
.expect("anchor pathLen=2 with two CAs below must be accepted");
}
fn self_issued_ca_below_anchor(root_plen: Option<u8>) -> (TrustStore, Vec<Certificate>) {
use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
use x509_cert::name::Name;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).expect("root key");
let rollover_key = RsaPrivateKey::new(&mut rng, 2048).expect("rollover key");
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let root_name = "CN=Rollover Root,O=tsp-ltv tests";
let root_issuer: Name = root_name.parse().unwrap();
let root = issue_cert(
Profile::SubCA {
issuer: root_issuer.clone(),
path_len_constraint: root_plen,
},
root_name,
&root_key,
&root_signer,
);
let rollover = issue_cert(
Profile::SubCA {
issuer: root_issuer,
path_len_constraint: None,
},
root_name,
&rollover_key,
&root_signer,
);
let mut store = TrustStore::new();
store.add_certificate(root).unwrap();
(store, vec![rollover])
}
#[test]
fn test_verify_chain_pathlen_ignores_self_issued_ca_below_anchor() {
let (store, chain) = self_issued_ca_below_anchor(Some(0));
store
.verify_chain(&chain, None)
.expect("self-issued CA below anchor must not consume pathLenConstraint");
}
#[test]
fn test_parse_key_cert_sign_bit() {
use crate::der_utils::encode_tlv;
assert!(parse_key_cert_sign_bit(&encode_tlv(0x03, &[0x01, 0x04])).unwrap());
assert!(!parse_key_cert_sign_bit(&encode_tlv(0x03, &[0x07, 0x80])).unwrap());
assert!(parse_key_cert_sign_bit(&encode_tlv(0x04, &[0x01, 0x04])).is_err());
let mut trailing = encode_tlv(0x03, &[0x01, 0x04]);
trailing.extend_from_slice(&[0x05, 0x00]); assert!(parse_key_cert_sign_bit(&trailing).is_err());
assert!(parse_key_cert_sign_bit(&encode_tlv(0x03, &[])).is_err());
assert!(parse_key_cert_sign_bit(&encode_tlv(0x03, &[0x00])).is_err());
assert!(parse_key_cert_sign_bit(&encode_tlv(0x03, &[0x08, 0x04])).is_err());
assert!(parse_key_cert_sign_bit(&encode_tlv(0x03, &[0x01, 0x05])).is_err());
assert!(parse_key_cert_sign_bit(&encode_tlv(0x03, &[0x01, 0x06])).unwrap());
}
#[test]
fn test_verify_chain_rejects_non_ca_intermediate() {
use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
use x509_cert::name::Name;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).expect("root key");
let mid_key = RsaPrivateKey::new(&mut rng, 2048).expect("mid key");
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).expect("leaf key");
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let mid_signer = SigningKey::<Sha256>::new(mid_key.clone());
let root_name = "CN=NonCA Root,O=tsp-ltv tests";
let mid_name = "CN=NonCA Mid,O=tsp-ltv tests";
let leaf_name = "CN=NonCA Leaf,O=tsp-ltv tests";
let root_issuer: Name = root_name.parse().unwrap();
let mid_issuer: Name = mid_name.parse().unwrap();
let root = issue_cert(Profile::Root, root_name, &root_key, &root_signer);
let mid = issue_cert(
Profile::Leaf {
issuer: root_issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
},
mid_name,
&mid_key,
&root_signer,
);
let leaf = issue_cert(
Profile::Leaf {
issuer: mid_issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
},
leaf_name,
&leaf_key,
&mid_signer,
);
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
let chain = vec![leaf, mid, root];
let err = store
.verify_chain(&chain, None)
.expect_err("a non-CA intermediate must be rejected");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("not a CA")),
"expected non-CA intermediate rejection, got: {err:?}"
);
}
#[test]
fn test_verify_chain_rejects_non_ca_trust_anchor_out_of_chain() {
use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
use x509_cert::name::Name;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).expect("root key");
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).expect("leaf key");
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let root_name = "CN=NonCA Anchor,O=tsp-ltv tests";
let leaf_name = "CN=NonCA Anchor Leaf,O=tsp-ltv tests";
let root_issuer: Name = root_name.parse().unwrap();
let root = issue_cert(
Profile::Leaf {
issuer: root_issuer.clone(),
enable_key_agreement: false,
enable_key_encipherment: false,
},
root_name,
&root_key,
&root_signer,
);
let leaf = issue_cert(
Profile::Leaf {
issuer: root_issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
},
leaf_name,
&leaf_key,
&root_signer,
);
let mut store = TrustStore::new();
store.add_certificate(root).unwrap();
let chain = vec![leaf];
let err = store
.verify_chain(&chain, None)
.expect_err("a non-CA trust anchor must be rejected");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("trust anchor") && m.contains("not a CA")),
"expected non-CA trust-anchor rejection, got: {err:?}"
);
}
fn root_with_extensions(
common_name: &str,
key: &rsa::RsaPrivateKey,
extra_extensions: Vec<x509_cert::ext::Extension>,
) -> Certificate {
use der::asn1::BitString;
use rsa::pkcs1v15::SigningKey;
use rsa::signature::{SignatureEncoding, Signer};
use sha2::Sha256;
use x509_cert::builder::Profile;
let signer = SigningKey::<Sha256>::new(key.clone());
let base = issue_cert(Profile::Root, common_name, key, &signer);
let mut tbs = base.tbs_certificate.clone();
let mut exts = tbs.extensions.clone().unwrap_or_default();
exts.extend(extra_extensions);
tbs.extensions = Some(exts);
let tbs_der = tbs.to_der().unwrap();
let sig = signer.sign(&tbs_der);
Certificate {
tbs_certificate: tbs,
signature_algorithm: base.signature_algorithm.clone(),
signature: BitString::from_bytes(&sig.to_bytes()).unwrap(),
}
}
fn ext(oid: &str, critical: bool, value: &[u8]) -> x509_cert::ext::Extension {
use der::asn1::OctetString;
x509_cert::ext::Extension {
extn_id: const_oid::ObjectIdentifier::new_unwrap(oid),
critical,
extn_value: OctetString::new(value.to_vec()).unwrap(),
}
}
#[test]
fn test_verify_chain_rejects_unknown_critical_extension() {
let mut rng = rand::thread_rng();
let key = rsa::RsaPrivateKey::new(&mut rng, 2048).unwrap();
let root = root_with_extensions(
"CN=Critical Ext Root,O=tsp-ltv tests",
&key,
vec![ext("1.2.3.4.5", true, &[0x05, 0x00])],
);
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
let chain = [root];
let err = store
.verify_chain(&chain, None)
.expect_err("unknown critical extension must be rejected");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("unrecognized critical extension")),
"expected unrecognized-critical-extension rejection, got: {err:?}"
);
}
#[test]
fn test_verify_chain_allows_unknown_noncritical_extension() {
let mut rng = rand::thread_rng();
let key = rsa::RsaPrivateKey::new(&mut rng, 2048).unwrap();
let root = root_with_extensions(
"CN=NonCritical Ext Root,O=tsp-ltv tests",
&key,
vec![ext("1.2.3.4.5", false, &[0x05, 0x00])],
);
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
let chain = [root];
store
.verify_chain(&chain, None)
.expect("a non-critical unknown extension must be accepted");
}
#[test]
#[cfg(not(feature = "ltv"))]
fn test_tsp_only_rejects_critical_ltv_only_extension() {
let mut rng = rand::thread_rng();
let key = rsa::RsaPrivateKey::new(&mut rng, 2048).unwrap();
let san_root = root_with_extensions(
"CN=Critical SAN Root,O=tsp-ltv tests",
&key,
vec![ext("2.5.29.17", true, &[0x30, 0x00])],
);
let mut store = TrustStore::new();
store.add_certificate(san_root.clone()).unwrap();
let err = store
.verify_chain(&[san_root], None)
.expect_err("critical subjectAltName must be rejected in a tsp-only build");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("unrecognized critical extension")),
"expected unrecognized-critical-extension rejection, got: {err:?}"
);
let eku_root = root_with_extensions(
"CN=Critical EKU Root,O=tsp-ltv tests",
&key,
vec![ext("2.5.29.37", true, &[0x30, 0x00])],
);
let mut store = TrustStore::new();
store.add_certificate(eku_root.clone()).unwrap();
store
.verify_chain(&[eku_root], None)
.expect("a critical extendedKeyUsage is processed and must be accepted");
}
#[cfg(feature = "ltv")]
fn leaf_and_root(leaf_profile: x509_cert::builder::Profile) -> (TrustStore, Vec<Certificate>) {
use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let root = issue_cert(
Profile::Root,
"CN=Purpose Root,O=tsp-ltv tests",
&root_key,
&root_signer,
);
let leaf = issue_cert(
leaf_profile,
"CN=Purpose Leaf,O=tsp-ltv tests",
&leaf_key,
&root_signer,
);
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
(store, vec![leaf, root])
}
#[cfg(feature = "ltv")]
#[test]
fn test_verify_chain_for_purpose_rejects_wrong_eku() {
use crate::ltv::CertRole;
use x509_cert::builder::Profile;
use x509_cert::name::Name;
let root_issuer: Name = "CN=Purpose Root,O=tsp-ltv tests".parse().unwrap();
let (store, chain) = leaf_and_root(Profile::Leaf {
issuer: root_issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
});
store
.verify_chain(&chain, None)
.expect("end-entity leaf should chain to anchor");
let err = store
.verify_chain_for_purpose(&chain, None, CertRole::OcspResponder)
.expect_err("leaf without OCSPSigning EKU must fail OcspResponder purpose");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("required purpose")),
"expected purpose-binding rejection, got: {err:?}"
);
}
#[cfg(feature = "ltv")]
#[test]
fn test_verify_chain_for_purpose_accepts_end_entity() {
use crate::ltv::CertRole;
use x509_cert::builder::Profile;
use x509_cert::name::Name;
let root_issuer: Name = "CN=Purpose Root,O=tsp-ltv tests".parse().unwrap();
let (store, chain) = leaf_and_root(Profile::Leaf {
issuer: root_issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
});
store
.verify_chain_for_purpose(&chain, None, CertRole::EndEntity)
.expect("end-entity leaf must satisfy EndEntity purpose");
let err = store
.verify_chain_for_purpose(&chain, None, CertRole::TimestampSigner)
.expect_err("leaf without timeStamping EKU must fail TimestampSigner purpose");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("TimestampSigner")),
"expected TimestampSigner purpose rejection, got: {err:?}"
);
}
#[cfg(feature = "ltv")]
#[test]
fn test_verify_chain_for_purpose_timestamp_signer() {
use crate::ltv::CertRole;
use rsa::pkcs1v15::SigningKey;
use rsa::signature::Keypair;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::{Builder, CertificateBuilder, Profile};
use x509_cert::ext::pkix::ExtendedKeyUsage;
use x509_cert::name::Name;
use x509_cert::serial_number::SerialNumber;
use x509_cert::spki::SubjectPublicKeyInfoOwned;
use x509_cert::time::Validity;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let root_name = "CN=TSA Purpose Root,O=tsp-ltv tests";
let root = issue_cert(Profile::Root, root_name, &root_key, &root_signer);
let leaf_signer = SigningKey::<Sha256>::new(leaf_key.clone());
let spki = SubjectPublicKeyInfoOwned::from_key(leaf_signer.verifying_key()).expect("SPKI");
let issuer: Name = root_name.parse().unwrap();
let subject: Name = "CN=TSA Purpose Leaf,O=tsp-ltv tests".parse().unwrap();
let validity =
Validity::from_now(std::time::Duration::from_secs(3650 * 24 * 3600)).unwrap();
let mut builder = CertificateBuilder::new(
Profile::Leaf {
issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
},
SerialNumber::new(&[0x51]).unwrap(),
validity,
subject,
spki,
&root_signer,
)
.expect("cert builder");
let ts_eku = const_oid::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.8");
builder
.add_extension(&ExtendedKeyUsage(vec![ts_eku]))
.expect("add EKU");
let leaf = builder
.build::<rsa::pkcs1v15::Signature>()
.expect("sign cert");
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
let chain = vec![leaf, root];
store
.verify_chain_for_purpose(&chain, None, CertRole::TimestampSigner)
.expect("leaf with critical timeStamping EKU must satisfy TimestampSigner");
}
#[cfg(feature = "ltv")]
fn name_constraints_dns(dns: &str, excluded: bool) -> Vec<u8> {
use crate::der_utils::{encode_sequence_raw, encode_tlv};
let gn = encode_tlv(0x82, dns.as_bytes());
let subtree = encode_sequence_raw(&gn);
let tag = if excluded { 0xA1 } else { 0xA0 };
let subtrees = encode_tlv(tag, &subtree);
encode_sequence_raw(&subtrees)
}
#[cfg(feature = "ltv")]
fn constrained_chain(
constraint_dns: &str,
excluded: bool,
leaf_dns: &str,
) -> (TrustStore, Vec<Certificate>) {
use crate::der_utils::{encode_sequence_raw, encode_tlv};
use der::asn1::BitString;
use rsa::pkcs1v15::SigningKey;
use rsa::signature::{SignatureEncoding, Signer};
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let nc_value = name_constraints_dns(constraint_dns, excluded);
let root = root_with_extensions(
"CN=NC Root,O=tsp-ltv tests",
&root_key,
vec![ext("2.5.29.30", true, &nc_value)],
);
let root_issuer: x509_cert::name::Name = root.tbs_certificate.subject.clone();
let leaf_base = issue_cert(
Profile::Leaf {
issuer: root_issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
},
"CN=NC Leaf,O=tsp-ltv tests",
&leaf_key,
&root_signer,
);
let san_gn = encode_tlv(0x82, leaf_dns.as_bytes());
let san_value = encode_sequence_raw(&san_gn);
let mut tbs = leaf_base.tbs_certificate.clone();
let mut exts = tbs.extensions.clone().unwrap_or_default();
exts.push(ext("2.5.29.17", false, &san_value));
tbs.extensions = Some(exts);
let tbs_der = tbs.to_der().unwrap();
let sig = root_signer.sign(&tbs_der);
let leaf = Certificate {
tbs_certificate: tbs,
signature_algorithm: leaf_base.signature_algorithm.clone(),
signature: BitString::from_bytes(&sig.to_bytes()).unwrap(),
};
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
(store, vec![leaf, root])
}
#[cfg(feature = "ltv")]
#[test]
fn test_name_constraints_permitted_violation_rejected() {
let (store, chain) = constrained_chain("example.com", false, "host.evil.com");
let err = store
.verify_chain(&chain, None)
.expect_err("SAN outside permitted subtree must be rejected");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("name constraint")),
"expected name-constraint violation, got: {err:?}"
);
}
#[cfg(feature = "ltv")]
#[test]
fn test_name_constraints_permitted_within_accepted() {
let (store, chain) = constrained_chain("example.com", false, "host.example.com");
store
.verify_chain(&chain, None)
.expect("SAN inside permitted subtree must be accepted");
}
#[cfg(feature = "ltv")]
#[test]
fn test_name_constraints_excluded_violation_rejected() {
let (store, chain) = constrained_chain("bad.example.com", true, "host.bad.example.com");
let err = store
.verify_chain(&chain, None)
.expect_err("SAN within excluded subtree must be rejected");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("name constraint")),
"expected excluded-subtree violation, got: {err:?}"
);
}
#[cfg(feature = "ltv")]
#[test]
fn test_name_constraints_unsupported_type_fails_closed() {
use crate::der_utils::{encode_sequence_raw, encode_tlv};
use der::asn1::BitString;
use rsa::pkcs1v15::SigningKey;
use rsa::signature::{SignatureEncoding, Signer};
use rsa::RsaPrivateKey;
use sha2::Sha256;
use x509_cert::builder::Profile;
let mut rng = rand::thread_rng();
let root_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let root_signer = SigningKey::<Sha256>::new(root_key.clone());
let gn = encode_tlv(0x86, b"http://example.com/");
let subtree = encode_sequence_raw(&gn);
let subtrees = encode_tlv(0xA0, &subtree);
let nc_value = encode_sequence_raw(&subtrees);
let root = root_with_extensions(
"CN=NC Unsupported Root,O=tsp-ltv tests",
&root_key,
vec![ext("2.5.29.30", true, &nc_value)],
);
let root_issuer: x509_cert::name::Name = root.tbs_certificate.subject.clone();
let leaf_base = issue_cert(
Profile::Leaf {
issuer: root_issuer,
enable_key_agreement: false,
enable_key_encipherment: false,
},
"CN=NC Unsupported Leaf,O=tsp-ltv tests",
&leaf_key,
&root_signer,
);
let tbs = leaf_base.tbs_certificate.clone();
let tbs_der = tbs.to_der().unwrap();
let sig = root_signer.sign(&tbs_der);
let leaf = Certificate {
tbs_certificate: tbs,
signature_algorithm: leaf_base.signature_algorithm.clone(),
signature: BitString::from_bytes(&sig.to_bytes()).unwrap(),
};
let mut store = TrustStore::new();
store.add_certificate(root.clone()).unwrap();
let chain = [leaf, root];
let err = store
.verify_chain(&chain, None)
.expect_err("unsupported name-constraint type must fail closed");
assert!(
matches!(err, TrustError::ProfileViolation(ref m) if m.contains("unsupported name constraint")),
"expected unsupported-name-constraint rejection, got: {err:?}"
);
}
#[test]
fn test_from_pem_directory_fails_closed_on_malformed_anchor() {
let dir = tempfile::tempdir().unwrap();
let ca_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
std::fs::write(dir.path().join("good.pem"), ca_pem).unwrap();
std::fs::write(
dir.path().join("bad.pem"),
"-----BEGIN CERTIFICATE-----\nnot base64!!!\n-----END CERTIFICATE-----\n",
)
.unwrap();
let err = TrustStore::from_pem_directory(dir.path())
.expect_err("a malformed anchor file must surface as an error");
assert!(
matches!(err, TrustError::CertificateParse(ref m) if m.contains("bad.pem")),
"error should name the offending file, got: {err:?}"
);
assert_eq!(
err.to_string().matches("certificate parse error").count(),
1,
"error prefix should not be duplicated: {err}"
);
}
#[test]
fn test_from_pem_directory_read_failure_is_io_error() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("unreadable.pem")).unwrap();
let err = TrustStore::from_pem_directory(dir.path())
.expect_err("a trust-anchor read failure must surface as I/O");
assert!(
matches!(err, TrustError::Io(ref e) if e.to_string().contains("unreadable.pem")),
"expected TrustError::Io naming the offending path, got: {err:?}"
);
}
#[test]
fn test_from_pem_directory_lenient_reports_skipped() {
let dir = tempfile::tempdir().unwrap();
let ca_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
std::fs::write(dir.path().join("good.pem"), ca_pem).unwrap();
std::fs::write(dir.path().join("bad.crt"), "garbage, not a PEM at all").unwrap();
let (store, skipped) =
TrustStore::from_pem_directory_lenient(dir.path()).expect("lenient load");
assert_eq!(store.len(), 1, "the valid anchor must still load");
assert_eq!(skipped.len(), 1, "the malformed file must be reported");
assert!(skipped[0].0.ends_with("bad.crt"));
}
#[test]
fn test_from_pem_directory_accepts_der_crt() {
let ca_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
let mut tmp = TrustStore::new();
tmp.add_pem_data(ca_pem.as_bytes()).unwrap();
let der = tmp.anchors[0].der.clone();
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("anchor.crt"), &der).unwrap();
let store = TrustStore::from_pem_directory(dir.path())
.expect("a DER-encoded .crt anchor must load");
assert_eq!(store.len(), 1);
let (store, skipped) =
TrustStore::from_pem_directory_lenient(dir.path()).expect("lenient load");
assert_eq!(store.len(), 1);
assert!(skipped.is_empty(), "a valid DER anchor must not be skipped");
}
#[test]
fn test_anchor_file_pem_detection_is_anchored() {
let mut store = TrustStore::new();
let mut der_like = vec![0x30u8, 0x03, 0x02, 0x01, 0x00];
der_like.extend_from_slice(b"-----BEGIN CERTIFICATE-----");
let err = store
.add_anchor_file_data(&der_like)
.expect_err("armor not at the start must take the DER path");
assert!(
matches!(err, TrustError::CertificateParse(ref m) if m.contains("DER")),
"embedded armor must not trigger PEM parsing, got: {err:?}"
);
let ca_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
let padded = format!("\n \t{ca_pem}");
store
.add_anchor_file_data(padded.as_bytes())
.expect("leading-whitespace PEM must be recognised and loaded");
assert_eq!(store.len(), 1);
}
}