use crate::error::TrustError;
use der::Encode;
use x509_cert::Certificate;
pub fn build_chain_from_pool(
leaf: &Certificate,
pool: &[Certificate],
trust_anchor_subjects: &[Vec<u8>],
max_depth: Option<usize>,
) -> Result<Vec<Certificate>, TrustError> {
build_chain_from_pool_with_policy(
leaf,
pool,
trust_anchor_subjects,
max_depth,
&crate::crypto::verify::SignaturePolicy::default(),
)
}
pub fn build_chain_from_pool_with_policy(
leaf: &Certificate,
pool: &[Certificate],
trust_anchor_subjects: &[Vec<u8>],
max_depth: Option<usize>,
policy: &crate::crypto::verify::SignaturePolicy,
) -> Result<Vec<Certificate>, TrustError> {
let max_depth = max_depth.unwrap_or(10);
let mut chain = vec![leaf.clone()];
let mut current = leaf.clone();
let mut visited: Vec<Vec<u8>> = vec![leaf
.to_der()
.map_err(|e| TrustError::CertificateParse(format!("DER encode failed: {e}")))?];
for _ in 0..max_depth {
let issuer_name_der = current.tbs_certificate.issuer.to_der().unwrap_or_default();
let subject_name_der = current.tbs_certificate.subject.to_der().unwrap_or_default();
if issuer_name_der == subject_name_der {
break;
}
if trust_anchor_subjects.contains(&issuer_name_der) {
break;
}
let mut found = false;
let mut matching_issuer_error = None;
for candidate in pool {
let candidate_der = candidate
.to_der()
.map_err(|e| TrustError::CertificateParse(format!("DER encode failed: {e}")))?;
if visited.contains(&candidate_der) {
continue; }
let candidate_subject_der = candidate
.tbs_certificate
.subject
.to_der()
.unwrap_or_default();
if candidate_subject_der == issuer_name_der {
match crate::crypto::verify::verify_certificate_signature_with_policy(
¤t, candidate, policy,
) {
Ok(()) => {
visited.push(candidate_der);
chain.push(candidate.clone());
current = candidate.clone();
found = true;
break;
}
Err(error) => matching_issuer_error = Some(error),
}
}
}
if !found {
if let Some(error) = matching_issuer_error {
return Err(error);
}
return Err(TrustError::ChainBroken {
index: chain.len() - 1,
expected_issuer: format!("{}", current.tbs_certificate.issuer),
found_subject: "no matching certificate in pool".into(),
});
}
}
Ok(chain)
}
pub fn trust_anchor_subjects(store: &super::TrustStore) -> Vec<Vec<u8>> {
store
.certificates()
.filter_map(|cert| cert.tbs_certificate.subject.to_der().ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::verify::SignaturePolicy;
fn issuer_and_sha1_leaf() -> (Certificate, Certificate) {
use der::asn1::BitString;
use der::{Any, Decode};
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 validity =
Validity::from_now(std::time::Duration::from_secs(3650 * 24 * 3600)).unwrap();
let issuer_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let issuer_signer = SigningKey::<Sha256>::new(issuer_key.clone());
let issuer_spki =
SubjectPublicKeyInfoOwned::from_key(issuer_signer.verifying_key()).unwrap();
let issuer_name: Name = "CN=Legacy Issuer,O=tsp-ltv tests".parse().unwrap();
let issuer = CertificateBuilder::new(
Profile::Root,
SerialNumber::new(&[0x01]).unwrap(),
validity,
issuer_name.clone(),
issuer_spki,
&issuer_signer,
)
.unwrap()
.build()
.unwrap();
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let leaf_spki = SubjectPublicKeyInfoOwned::from_key(
SigningKey::<Sha256>::new(leaf_key).verifying_key(),
)
.unwrap();
let leaf_subject: Name = "CN=Legacy Leaf,O=tsp-ltv tests".parse().unwrap();
let base = CertificateBuilder::new(
Profile::Leaf {
issuer: issuer.tbs_certificate.subject.clone(),
enable_key_agreement: false,
enable_key_encipherment: false,
},
SerialNumber::new(&[0x02]).unwrap(),
validity,
leaf_subject,
leaf_spki,
&issuer_signer,
)
.unwrap()
.build()
.unwrap();
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 = issuer_key
.sign(Pkcs1v15Sign::new::<Sha1>(), &hash)
.expect("SHA-1 RSA sign");
let leaf = Certificate {
tbs_certificate: tbs,
signature_algorithm: sha1_algid,
signature: BitString::from_bytes(&sig).unwrap(),
};
(issuer, leaf)
}
#[test]
fn test_build_chain_rejects_sha1_link_by_default_and_accepts_with_legacy() {
let (issuer, leaf) = issuer_and_sha1_leaf();
let pool = [issuer];
let err = build_chain_from_pool(&leaf, &pool, &[], None).unwrap_err();
assert!(
matches!(err, TrustError::WeakAlgorithm(_)),
"strict build must fail on a SHA-1 link, got {err:?}"
);
let chain = build_chain_from_pool_with_policy(
&leaf,
&pool,
&[],
None,
&SignaturePolicy::allow_legacy(),
)
.expect("legacy build must succeed on a SHA-1 link");
assert_eq!(chain.len(), 2, "chain should be [leaf, issuer]");
}
}