use std::time::Duration;
use der::{Decode, Encode};
use reqwest::Client;
use x509_cert::Certificate;
use crate::error::LtvError;
use crate::trust::TrustStore;
use super::ocsp::{extract_aia_urls, AiaAccessMethod};
const MAX_CHAIN_DEPTH: usize = 10;
#[derive(Debug, Clone)]
pub struct ChainBuilder {
http_client: Client,
timeout: Duration,
}
impl ChainBuilder {
pub fn new() -> Self {
Self {
http_client: Client::new(),
timeout: Duration::from_secs(30),
}
}
pub fn http_client(mut self, client: Client) -> Self {
self.http_client = client;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub async fn build_chain(
&self,
leaf: &Certificate,
trust_store: &TrustStore,
) -> Result<Vec<Vec<u8>>, LtvError> {
let mut chain: Vec<Vec<u8>> = Vec::new();
let mut current = leaf.clone();
for depth in 0..MAX_CHAIN_DEPTH {
let current_der = current
.to_der()
.map_err(|e| LtvError::Chain(format!("failed to encode certificate: {e}")))?;
chain.push(current_der.clone());
if trust_store.find_issuer(¤t).is_some() {
log::debug!("Chain complete at depth {depth}: found trust anchor");
return Ok(chain);
}
if is_self_signed(¤t) {
log::debug!("Chain complete at depth {depth}: self-signed certificate");
return Ok(chain);
}
let ca_issuer_urls = extract_aia_urls(¤t, AiaAccessMethod::CaIssuers);
if ca_issuer_urls.is_empty() {
log::debug!(
"Chain building stopped at depth {depth}: no AIA caIssuers URLs"
);
return Ok(chain);
}
let mut found_issuer = false;
for url in &ca_issuer_urls {
match self.fetch_certificate(url).await {
Ok(issuer_cert) => {
current = issuer_cert;
found_issuer = true;
break;
}
Err(e) => {
log::warn!("Failed to fetch CA cert from {url}: {e}");
}
}
}
if !found_issuer {
log::debug!(
"Chain building stopped at depth {depth}: could not fetch issuer from any AIA URL"
);
return Ok(chain);
}
}
log::warn!("Chain building reached maximum depth ({MAX_CHAIN_DEPTH})");
Ok(chain)
}
pub fn build_chain_from_certs(
leaf: &Certificate,
available_certs: &[Certificate],
trust_store: &TrustStore,
) -> Vec<Vec<u8>> {
let mut chain: Vec<Vec<u8>> = Vec::new();
let mut current = leaf.clone();
for _ in 0..MAX_CHAIN_DEPTH {
let current_der = current.to_der().unwrap_or_default();
if current_der.is_empty() {
break;
}
chain.push(current_der);
if trust_store.find_issuer(¤t).is_some() {
break;
}
if is_self_signed(¤t) {
break;
}
let issuer_name = ¤t.tbs_certificate.issuer;
let found = available_certs.iter().find(|c| {
&c.tbs_certificate.subject == issuer_name
});
match found {
Some(issuer) => {
current = issuer.clone();
}
None => break,
}
}
chain
}
async fn fetch_certificate(&self, url: &str) -> Result<Certificate, LtvError> {
log::debug!("Fetching CA certificate from {url}");
let response = self
.http_client
.get(url)
.timeout(self.timeout)
.send()
.await
.map_err(|e| LtvError::Chain(format!("failed to fetch cert from {url}: {e}")))?;
if !response.status().is_success() {
return Err(LtvError::Chain(format!(
"cert fetch from {url} returned HTTP {}",
response.status()
)));
}
let cert_bytes = response
.bytes()
.await
.map_err(|e| LtvError::Chain(format!("failed to read cert response: {e}")))?
.to_vec();
if let Ok(cert) = Certificate::from_der(&cert_bytes) {
return Ok(cert);
}
if let Ok((_label, der)) = pem_rfc7468::decode_vec(&cert_bytes) {
if let Ok(cert) = Certificate::from_der(&der) {
return Ok(cert);
}
}
Err(LtvError::Chain(format!(
"could not parse certificate from {url} (tried DER and PEM)"
)))
}
}
impl Default for ChainBuilder {
fn default() -> Self {
Self::new()
}
}
fn is_self_signed(cert: &Certificate) -> bool {
cert.tbs_certificate.subject == cert.tbs_certificate.issuer
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_builder_default() {
let builder = ChainBuilder::new();
assert_eq!(builder.timeout, Duration::from_secs(30));
}
#[test]
fn test_is_self_signed() {
let cert_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
let (_label, der) = pem_rfc7468::decode_vec(cert_pem.as_bytes()).unwrap();
let cert = Certificate::from_der(&der).unwrap();
assert!(is_self_signed(&cert), "root CA should be self-signed");
}
#[test]
fn test_is_not_self_signed() {
let cert_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/signer_cert.pem"
));
let (_label, der) = pem_rfc7468::decode_vec(cert_pem.as_bytes()).unwrap();
let cert = Certificate::from_der(&der).unwrap();
assert!(!is_self_signed(&cert), "signer cert should not be self-signed");
}
#[test]
fn test_build_chain_from_certs() {
let ca_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
let signer_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/signer_cert.pem"
));
let (_label, ca_der) = pem_rfc7468::decode_vec(ca_pem.as_bytes()).unwrap();
let (_label, signer_der) = pem_rfc7468::decode_vec(signer_pem.as_bytes()).unwrap();
let signer_cert = Certificate::from_der(&signer_der).unwrap();
let mut trust_store = TrustStore::new();
trust_store.add_der_certificate(&ca_der).unwrap();
let chain_pem_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/chain.pem"
);
let chain_pem = std::fs::read_to_string(chain_pem_path).unwrap_or_default();
let mut available_certs = vec![signer_cert.clone()];
let pem_data = chain_pem.as_bytes();
while let Ok((_label, der)) = pem_rfc7468::decode_vec(pem_data) {
if let Ok(cert) = Certificate::from_der(&der) {
available_certs.push(cert);
}
break;
}
let chain = ChainBuilder::build_chain_from_certs(
&signer_cert,
&available_certs,
&trust_store,
);
assert!(!chain.is_empty(), "chain should not be empty");
assert_eq!(chain[0], signer_der, "first cert should be the signer");
}
}