use std::io;
use purecrypto::tls::RootCertStore;
use crate::error::{Error, Result};
pub(crate) const SYSTEM_CA_PATHS: &[&str] = &[
"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/cert.pem", "/etc/ssl/ca-bundle.pem", "/etc/ca-certificates/extracted/tls-ca-bundle.pem", ];
pub(crate) fn load_system_roots() -> Result<RootCertStore> {
for path in SYSTEM_CA_PATHS {
let pem = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
Err(e) => return Err(Error::Io(e)),
};
return parse_into_store(&pem, path);
}
Err(Error::BadResponse(
"no system CA bundle found; tried common Unix paths".into(),
))
}
#[allow(dead_code)]
pub(crate) fn load_from_file(path: &str) -> Result<RootCertStore> {
let pem = std::fs::read_to_string(path).map_err(Error::Io)?;
parse_into_store(&pem, path)
}
#[allow(dead_code)]
pub(crate) fn add_from_dir(roots: &mut RootCertStore, dir: &str) -> Result<()> {
let entries = std::fs::read_dir(dir).map_err(Error::Io)?;
let mut loaded = 0usize;
for entry in entries {
let entry = entry.map_err(Error::Io)?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Ok(pem) = std::fs::read_to_string(&path) else {
continue; };
for block in pem_blocks(&pem) {
if roots.add_pem(&block).is_ok() {
loaded += 1;
}
}
}
if loaded == 0 {
return Err(Error::BadResponse(format!(
"--capath {dir}: no usable CA certificates found"
)));
}
Ok(())
}
fn parse_into_store(pem: &str, path: &str) -> Result<RootCertStore> {
let mut roots = RootCertStore::new();
let mut loaded = 0usize;
for block in pem_blocks(pem) {
if roots.add_pem(&block).is_ok() {
loaded += 1;
}
}
if loaded == 0 {
return Err(Error::BadResponse(format!(
"no usable CA certificates parsed from {path}"
)));
}
Ok(roots)
}
pub(crate) fn pem_blocks(pem: &str) -> Vec<String> {
const BEGIN: &str = "-----BEGIN CERTIFICATE-----";
const END: &str = "-----END CERTIFICATE-----";
let mut out = Vec::new();
let mut rest = pem;
while let Some(start) = rest.find(BEGIN) {
let after_begin = &rest[start..];
let body = &after_begin[BEGIN.len()..];
let Some(end_rel) = body.find(END) else {
rest = body;
continue;
};
if let Some(next_begin) = body.find(BEGIN) {
if next_begin < end_rel {
rest = body;
continue;
}
}
let end_abs = start + BEGIN.len() + end_rel + END.len();
out.push(rest[start..end_abs].to_string());
rest = &rest[end_abs..];
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pem_blocks_splits() {
let pem = "junk\n\
-----BEGIN CERTIFICATE-----\nAAA\n-----END CERTIFICATE-----\n\
noise\n\
-----BEGIN CERTIFICATE-----\nBBB\n-----END CERTIFICATE-----\n";
let blocks = pem_blocks(pem);
assert_eq!(blocks.len(), 2);
assert!(blocks[0].contains("AAA"));
assert!(blocks[1].contains("BBB"));
}
#[test]
fn pem_blocks_skips_unterminated_begin() {
let pem = "-----BEGIN CERTIFICATE-----\nAAA\n-----END CERTIFICATE-----\n\
-----BEGIN CERTIFICATE-----\nTRUNCATED never terminated\n\
-----BEGIN CERTIFICATE-----\nBBB\n-----END CERTIFICATE-----\n";
let blocks = pem_blocks(pem);
assert_eq!(blocks.len(), 2);
assert!(blocks[0].contains("AAA"));
assert!(blocks[1].contains("BBB"));
assert!(!blocks[0].contains("TRUNCATED"));
assert!(!blocks[1].contains("TRUNCATED"));
}
#[test]
fn pem_blocks_stray_end_does_not_corrupt() {
let pem = "-----END CERTIFICATE-----\njunk\n\
-----BEGIN CERTIFICATE-----\nAAA\n-----END CERTIFICATE-----\n";
let blocks = pem_blocks(pem);
assert_eq!(blocks.len(), 1);
assert!(blocks[0].contains("AAA"));
assert!(blocks[0].starts_with("-----BEGIN CERTIFICATE-----"));
}
}