#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms)]
extern crate alloc;
use alloc::vec::Vec;
use x509_cert::Certificate;
const OID_BASIC_CONSTRAINTS: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.19");
#[derive(Debug, Default, PartialEq, Eq)]
pub struct CertPool {
certs: Vec<Certificate>,
}
impl CertPool {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, cert: Certificate) {
self.certs.push(cert);
}
#[must_use]
pub fn len(&self) -> usize {
self.certs.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.certs.is_empty()
}
pub fn iter(&self) -> core::slice::Iter<'_, x509_cert::Certificate> {
self.certs.iter()
}
}
impl<'a> IntoIterator for &'a CertPool {
type Item = &'a x509_cert::Certificate;
type IntoIter = core::slice::Iter<'a, x509_cert::Certificate>;
fn into_iter(self) -> Self::IntoIter {
self.certs.iter()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Error {
NoPathFound,
DepthExceeded,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NoPathFound => f.write_str("no certification path found to a trust anchor"),
Self::DepthExceeded => f.write_str(
"no certification path found within depth limit (try increasing max_depth)",
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
pub type Result<T> = core::result::Result<T, Error>;
fn cert_is_ca(cert: &Certificate) -> bool {
use der::Decode as _;
use x509_cert::ext::pkix::BasicConstraints;
cert.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.find(|ext| ext.extn_id == OID_BASIC_CONSTRAINTS)
.and_then(|ext| BasicConstraints::from_der(ext.extn_value.as_bytes()).ok())
.is_some_and(|bc| bc.ca)
}
fn dfs(
path: &mut Vec<Certificate>,
pool: &[Certificate],
anchors: &[pkix_path::TrustAnchor],
depth_remaining: usize,
) -> bool {
let current_issuer = match path.last() {
Some(c) => c.tbs_certificate.issuer.clone(),
None => {
debug_assert!(false, "dfs called with empty path — invariant violated");
return false;
}
};
for anchor in anchors {
if pkix_path::names_match(&anchor.subject, ¤t_issuer) {
return true;
}
}
if depth_remaining == 0 {
return false;
}
for candidate in pool {
if !pkix_path::names_match(&candidate.tbs_certificate.subject, ¤t_issuer) {
continue;
}
if !cert_is_ca(candidate) {
continue;
}
let already_in_path = path.iter().any(|in_path| {
pkix_path::names_match(
&in_path.tbs_certificate.subject,
&candidate.tbs_certificate.subject,
)
});
if already_in_path {
continue;
}
path.push(candidate.clone());
if dfs(path, pool, anchors, depth_remaining - 1) {
return true;
}
path.pop();
}
false
}
#[must_use = "path building result must be checked"]
pub fn build_path(
target: &Certificate,
pool: &CertPool,
anchors: &[pkix_path::TrustAnchor],
) -> Result<Vec<Certificate>> {
const MAX_DEPTH: usize = 10;
let pool_slice: &[Certificate] = &pool.certs;
for max_depth in 1..=MAX_DEPTH {
let mut path = alloc::vec![target.clone()];
if dfs(&mut path, pool_slice, anchors, max_depth) {
return Ok(path);
}
}
let mut probe = alloc::vec![target.clone()];
if dfs(&mut probe, pool_slice, anchors, MAX_DEPTH + 1) {
return Err(Error::DepthExceeded);
}
Err(Error::NoPathFound)
}