mod steps;
use x509_cert::Certificate;
use super::cert;
use super::tsl::{TrustStore, TrustStoreCache};
use crate::constants::TSL_CACHE_TTL;
use crate::net::Transport;
use steps::{build_chain, find_matching_anchor, verify_chain_crypto};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustStatus {
Trusted,
Untrusted,
Indeterminate,
}
#[derive(Debug, Clone)]
pub struct ChainResult {
pub trust: TrustStatus,
pub trust_anchor: Option<String>,
pub chain_depth: usize,
pub details: Vec<String>,
}
impl ChainResult {
fn indeterminate(detail: impl Into<String>) -> Self {
ChainResult {
trust: TrustStatus::Indeterminate,
trust_anchor: None,
chain_depth: 0,
details: vec![detail.into()],
}
}
}
#[must_use]
pub fn validate_chain(cms_der: &[u8], trust_store: &TrustStore) -> ChainResult {
validate_chain_inner(cms_der, trust_store, verify_chain_crypto)
}
#[must_use]
pub fn validate_chain_for_profile(
transport: &Transport,
cache: &TrustStoreCache,
cms_der: &[u8],
tsl_url: &str,
) -> ChainResult {
let store = cache.get_or_fetch(transport, tsl_url, TSL_CACHE_TTL);
chain_result_for_store(cms_der, store.as_ref())
}
fn chain_result_for_store(cms_der: &[u8], store: Option<&TrustStore>) -> ChainResult {
match store {
Some(store) => validate_chain(cms_der, store),
None => ChainResult::indeterminate("Chain: trust store unavailable"),
}
}
fn validate_chain_inner(
cms_der: &[u8],
trust_store: &TrustStore,
verify: impl Fn(&[Certificate], &[Certificate]) -> Result<(), String>,
) -> ChainResult {
let Ok(cms_certs) = cert::all_certs_from_cms(cms_der) else {
return ChainResult::indeterminate("Chain: failed to parse CMS certificates");
};
let Some(leaf) = crate::cms::signer_certificate(cms_der) else {
return ChainResult::indeterminate(
"Chain: CMS does not name exactly one signer certificate",
);
};
let mut details = vec![format!("Chain: signer cert: {}", cert::subject_dn(&leaf))];
let mut pool = cms_certs;
pool.extend(
trust_store
.ca_anchors
.iter()
.filter_map(|anchor| cert::parse_der(&anchor.cert_der).ok()),
);
let chain = build_chain(&leaf, &pool);
let chain_depth = chain.len();
if chain_depth > 1 {
let subjects: Vec<String> = chain.iter().map(cert::subject_dn).collect();
details.push(format!(
"Chain: depth {chain_depth}: {}",
subjects.join(" -> ")
));
}
let Some(anchor_name) = find_matching_anchor(&chain, trust_store) else {
details.push(format!(
"Chain: no trusted CA found (operator: {})",
trust_store.scheme_operator
));
return ChainResult {
trust: TrustStatus::Untrusted,
trust_anchor: None,
chain_depth,
details,
};
};
let anchors: Vec<Certificate> = trust_store
.ca_anchors
.iter()
.filter_map(|anchor| cert::parse_der(&anchor.cert_der).ok())
.collect();
match verify(&chain, &anchors) {
Ok(()) => {
details.push(format!(
"Chain: trusted ({anchor_name}, {})",
trust_store.scheme_operator
));
ChainResult {
trust: TrustStatus::Trusted,
trust_anchor: Some(anchor_name),
chain_depth,
details,
}
}
Err(err) => {
log::debug!("Cryptographic chain verification failed: {err}");
details.push(format!(
"Chain: anchor matched ({anchor_name}) but cryptographic verification failed"
));
ChainResult {
trust: TrustStatus::Indeterminate,
trust_anchor: Some(anchor_name),
chain_depth,
details,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pki::tsl::TrustAnchor;
use std::time::Instant;
const CMS_LEAF_DIRECT: &[u8] = include_bytes!("../testdata/cms_leaf_direct.der");
const CMS_LEAF_ROOT2: &[u8] = include_bytes!("../testdata/cms_leaf_root2.der");
const CMS_CHAIN3: &[u8] = include_bytes!("../testdata/cms_chain3.der");
const CMS_LEAF_AIA: &[u8] = include_bytes!("../testdata/cms_leaf_aia.der");
const CMS_TRUSTED_FIRST: &[u8] =
include_bytes!("../testdata/cms_trusted_cert_listed_first.der");
const ROOT_DER: &[u8] = include_bytes!("../testdata/root.der");
fn trust_store_with(anchor_der: &[u8], subject_name: &str, service_name: &str) -> TrustStore {
let anchor = TrustAnchor {
subject_name: subject_name.to_owned(),
service_name: service_name.to_owned(),
service_type: "CA/QC".to_owned(),
status: "granted".to_owned(),
cert_der: anchor_der.to_vec(),
};
TrustStore {
anchors: vec![anchor.clone()],
ca_anchors: vec![anchor],
scheme_operator: "Test Operator".to_owned(),
tsl_url: "https://example.com".to_owned(),
fetched_at: Instant::now(),
}
}
#[test]
fn extracts_all_certs_from_cms() {
let certs = cert::all_certs_from_cms(CMS_CHAIN3).unwrap();
assert!(certs.len() >= 3, "got {}", certs.len());
}
#[test]
fn validate_chain_trusted() {
let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
let result = validate_chain(CMS_LEAF_DIRECT, &store);
assert_eq!(result.trust, TrustStatus::Trusted);
assert_eq!(result.trust_anchor.as_deref(), Some("TestRootCA"));
assert!(result.chain_depth >= 2);
}
#[test]
fn a_trusted_certificate_listed_first_does_not_lend_its_trust() {
let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
let result = validate_chain(CMS_TRUSTED_FIRST, &store);
assert_ne!(
result.trust,
TrustStatus::Trusted,
"certificate order must not confer trust"
);
assert_eq!(result.trust_anchor, None);
assert!(
result
.details
.iter()
.any(|detail| detail.contains("Untrusted Attacker Signer")),
"the signer named by the SignerInfo is the one to report, got {:?}",
result.details
);
}
#[test]
fn validate_chain_untrusted() {
let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
let result = validate_chain(CMS_LEAF_ROOT2, &store);
assert_eq!(result.trust, TrustStatus::Untrusted);
assert_eq!(result.trust_anchor, None);
}
#[test]
fn validate_chain_no_certs_is_indeterminate() {
let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
let result = validate_chain(b"\x30\x00", &store);
assert_eq!(result.trust, TrustStatus::Indeterminate);
assert_eq!(result.chain_depth, 0);
}
#[test]
fn validate_chain_parse_failure_is_indeterminate() {
let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
let result = validate_chain(b"not cms at all", &store);
assert_eq!(result.trust, TrustStatus::Indeterminate);
assert!(result.details[0].to_lowercase().contains("failed to parse"));
}
#[test]
fn crypto_failure_falls_back_to_indeterminate() {
let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
let result = validate_chain_inner(CMS_LEAF_DIRECT, &store, |_chain, _anchors| {
Err("forced failure".to_owned())
});
assert_eq!(result.trust, TrustStatus::Indeterminate); assert_eq!(result.trust_anchor.as_deref(), Some("TestRootCA"));
assert!(result
.details
.iter()
.any(|d| d.contains("cryptographic verification failed")));
}
#[test]
fn aia_urls_are_never_followed() {
let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
let result = validate_chain(CMS_LEAF_AIA, &store);
assert_eq!(result.chain_depth, 1);
assert_eq!(result.trust, TrustStatus::Untrusted);
}
#[test]
fn chain_result_for_missing_store_is_indeterminate() {
let result = chain_result_for_store(b"\x30\x00", None);
assert_eq!(result.trust, TrustStatus::Indeterminate);
assert!(result.details[0].contains("unavailable"));
}
}