#![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 der::Decode as _;
use x509_cert::Certificate;
#[derive(Clone, Debug, Default)]
pub struct CertPool {
certs: Vec<Certificate>,
}
impl CertPool {
#[must_use]
pub const fn new() -> Self {
Self { certs: Vec::new() }
}
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()
}
pub(crate) fn as_slice(&self) -> &[Certificate] {
&self.certs
}
}
impl FromIterator<Certificate> for CertPool {
fn from_iter<I: IntoIterator<Item = Certificate>>(iter: I) -> Self {
Self {
certs: iter.into_iter().collect(),
}
}
}
impl Extend<Certificate> for CertPool {
fn extend<I: IntoIterator<Item = Certificate>>(&mut self, iter: I) {
self.certs.extend(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,
BudgetExceeded,
MalformedIntermediate,
}
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(
"configured maximum intermediate chain depth exceeded; the chain may require a deeper path than this builder is configured to attempt",
),
Self::BudgetExceeded => f.write_str(
"DFS node-visit budget exceeded; pool may be adversarially large",
),
Self::MalformedIntermediate => f.write_str(
"a candidate intermediate's BasicConstraints extension is present but cannot be decoded",
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
pub type Result<T> = core::result::Result<T, Error>;
fn cert_is_ca(cert: &Certificate) -> Result<bool> {
pkix_path::cert_is_ca(cert).map_err(|_| Error::MalformedIntermediate)
}
const OID_AUTHORITY_KEY_IDENTIFIER: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.35");
const OID_SUBJECT_KEY_IDENTIFIER: der::asn1::ObjectIdentifier =
der::asn1::ObjectIdentifier::new_unwrap("2.5.29.14");
fn cert_aki_key_id(cert: &Certificate) -> Option<Vec<u8>> {
use x509_cert::ext::pkix::AuthorityKeyIdentifier;
let extns = cert.tbs_certificate.extensions.as_deref()?;
let extn = extns
.iter()
.find(|e| e.extn_id == OID_AUTHORITY_KEY_IDENTIFIER)?;
let aki = AuthorityKeyIdentifier::from_der(extn.extn_value.as_bytes()).ok()?;
aki.key_identifier.map(|oct| oct.as_bytes().to_vec())
}
fn cert_ski_key_id(cert: &Certificate) -> Option<Vec<u8>> {
use x509_cert::ext::pkix::SubjectKeyIdentifier;
let extns = cert.tbs_certificate.extensions.as_deref()?;
let extn = extns
.iter()
.find(|e| e.extn_id == OID_SUBJECT_KEY_IDENTIFIER)?;
let ski = SubjectKeyIdentifier::from_der(extn.extn_value.as_bytes()).ok()?;
Some(ski.0.as_bytes().to_vec())
}
fn rank_candidates(cur: &Certificate, pool: &[Certificate]) -> Vec<(u8, usize)> {
let cur_issuer = &cur.tbs_certificate.issuer;
let target_aki_kid = cert_aki_key_id(cur);
let mut ranked: Vec<(u8, usize)> = Vec::with_capacity(pool.len());
for (idx, candidate) in pool.iter().enumerate() {
if !pkix_path::names_match(&candidate.tbs_certificate.subject, cur_issuer) {
continue;
}
let tier: u8 = match (
target_aki_kid.as_deref(),
cert_ski_key_id(candidate).as_deref(),
) {
(Some(aki), Some(ski)) if aki == ski => 0,
_ => 1,
};
ranked.push((tier, idx));
}
ranked.sort_by_key(|&(tier, _)| tier);
ranked
}
fn spki_already_in_path(candidate: &Certificate, path: &[Certificate]) -> bool {
let candidate_spki = &candidate.tbs_certificate.subject_public_key_info;
path.iter().any(|in_path| {
let s = &in_path.tbs_certificate.subject_public_key_info;
s.algorithm.oid == candidate_spki.algorithm.oid
&& s.subject_public_key == candidate_spki.subject_public_key
})
}
pub const DEFAULT_DFS_BUDGET: usize = 10_000;
pub const DEFAULT_MAX_DEPTH: usize = 10;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub struct PathBuilderConfig {
pub max_depth: usize,
pub dfs_budget: usize,
}
impl PathBuilderConfig {
#[must_use]
pub const fn new() -> Self {
Self {
max_depth: DEFAULT_MAX_DEPTH,
dfs_budget: DEFAULT_DFS_BUDGET,
}
}
}
impl Default for PathBuilderConfig {
fn default() -> Self {
Self::new()
}
}
struct Frame {
ranked: Option<Vec<(u8, usize)>>,
cursor: usize,
anchor_checked: bool,
anchor_yielded: bool,
}
impl Frame {
const fn new() -> Self {
Self {
ranked: None,
cursor: 0,
anchor_checked: false,
anchor_yielded: false,
}
}
}
pub struct PathCandidates<'a> {
pool: &'a [Certificate],
anchors: &'a [pkix_path::TrustAnchor],
max_depth: usize,
path: Vec<Certificate>,
frames: Vec<Frame>,
budget: usize,
started: bool,
done: bool,
}
impl<'a> PathCandidates<'a> {
fn new(
target: &Certificate,
pool: &'a [Certificate],
anchors: &'a [pkix_path::TrustAnchor],
config: &PathBuilderConfig,
) -> Self {
let path = alloc::vec![target.clone()];
let frames = alloc::vec![Frame::new()];
Self {
pool,
anchors,
max_depth: config.max_depth,
path,
frames,
budget: config.dfs_budget,
started: false,
done: false,
}
}
}
impl<'a> Iterator for PathCandidates<'a> {
type Item = Result<Vec<Certificate>>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
self.started = true;
loop {
if self.frames.is_empty() {
self.done = true;
return None;
}
if self.frames.last().expect("non-empty").anchor_yielded {
self.frames.pop();
self.path.pop();
continue;
}
if !self.frames.last().expect("non-empty").anchor_checked {
if self.budget == 0 {
self.done = true;
return Some(Err(Error::BudgetExceeded));
}
self.budget -= 1;
self.frames.last_mut().expect("non-empty").anchor_checked = true;
let cur_issuer = &self
.path
.last()
.expect("path mirrors frames; non-empty")
.tbs_certificate
.issuer;
let matched = self
.anchors
.iter()
.any(|a| pkix_path::names_match(&a.subject, cur_issuer));
if matched {
self.frames.last_mut().expect("non-empty").anchor_yielded = true;
return Some(Ok(self.path.clone()));
}
}
if self.frames.len() > self.max_depth {
self.frames.pop();
self.path.pop();
continue;
}
if self.frames.last().expect("non-empty").ranked.is_none() {
let cur = self.path.last().expect("non-empty");
let ranked = rank_candidates(cur, self.pool);
self.frames.last_mut().expect("non-empty").ranked = Some(ranked);
}
let frame = self.frames.last_mut().expect("non-empty");
let ranked = frame.ranked.as_ref().expect("set above");
if frame.cursor >= ranked.len() {
self.frames.pop();
self.path.pop();
continue;
}
let (_tier, idx) = ranked[frame.cursor];
frame.cursor += 1;
let candidate = &self.pool[idx];
match cert_is_ca(candidate) {
Err(_) | Ok(false) => continue,
Ok(true) => {}
}
if spki_already_in_path(candidate, &self.path) {
continue;
}
self.path.push(candidate.clone());
self.frames.push(Frame::new());
}
}
}
#[must_use]
pub fn build_path_candidates<'a>(
target: &Certificate,
pool: &'a CertPool,
anchors: &'a [pkix_path::TrustAnchor],
) -> PathCandidates<'a> {
PathCandidates::new(target, pool.as_slice(), anchors, &PathBuilderConfig::new())
}
#[must_use]
pub fn build_path_candidates_with_config<'a>(
target: &Certificate,
pool: &'a CertPool,
anchors: &'a [pkix_path::TrustAnchor],
config: &PathBuilderConfig,
) -> PathCandidates<'a> {
PathCandidates::new(target, pool.as_slice(), anchors, config)
}
pub fn build_path(
target: &Certificate,
pool: &CertPool,
anchors: &[pkix_path::TrustAnchor],
) -> Result<Vec<Certificate>> {
build_path_with_config(target, pool, anchors, &PathBuilderConfig::new())
}
pub fn build_path_with_config(
target: &Certificate,
pool: &CertPool,
anchors: &[pkix_path::TrustAnchor],
config: &PathBuilderConfig,
) -> Result<Vec<Certificate>> {
let pool_slice = pool.as_slice();
let mut iter = PathCandidates::new(target, pool_slice, anchors, config);
match iter.next() {
Some(Ok(chain)) => return Ok(chain),
Some(Err(e)) => return Err(e),
None => {}
}
let probe_config = PathBuilderConfig {
max_depth: config.max_depth.saturating_add(1),
dfs_budget: config.dfs_budget,
};
let mut probe = PathCandidates::new(target, pool_slice, anchors, &probe_config);
match probe.next() {
Some(Ok(_)) => Err(Error::DepthExceeded),
Some(Err(e)) => Err(e),
None => Err(Error::NoPathFound),
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::{cert_aki_key_id, cert_ski_key_id};
use der::Decode as _;
use std::path::PathBuf;
use x509_cert::Certificate;
fn pkits_cert(name: &str) -> Certificate {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../pkix-path/tests/pkits/certs")
.join(std::format!("{name}.crt"));
let bytes = std::fs::read(&path)
.unwrap_or_else(|e| std::panic!("fixture not found at {}: {}", path.display(), e));
Certificate::from_der(&bytes).unwrap_or_else(|e| std::panic!("failed to parse {name}: {e}"))
}
#[test]
fn cert_aki_key_id_test4ee_matches_oldkey_ski() {
const EXPECTED: [u8; 20] = [
0xdd, 0x0d, 0x75, 0x8d, 0x53, 0x68, 0x12, 0xc4, 0xcb, 0x15, 0x40, 0xc0, 0x14, 0x86,
0x14, 0x16, 0x30, 0xa1, 0xbe, 0xaf,
];
let ee = pkits_cert("ValidBasicSelfIssuedNewWithOldTest4EE");
let aki = cert_aki_key_id(&ee).expect("Test4EE has an AKI extension");
assert_eq!(aki.as_slice(), &EXPECTED);
}
#[test]
fn cert_ski_key_id_oldkey_matches_test4ee_aki() {
const EXPECTED: [u8; 20] = [
0xdd, 0x0d, 0x75, 0x8d, 0x53, 0x68, 0x12, 0xc4, 0xcb, 0x15, 0x40, 0xc0, 0x14, 0x86,
0x14, 0x16, 0x30, 0xa1, 0xbe, 0xaf,
];
let oldkey = pkits_cert("BasicSelfIssuedOldKeyCACert");
let ski = cert_ski_key_id(&oldkey).expect("OldKeyCACert has an SKI extension");
assert_eq!(ski.as_slice(), &EXPECTED);
}
#[test]
fn cert_ski_key_id_bridge_ca_differs_from_oldkey() {
const EXPECTED: [u8; 20] = [
0x88, 0x5f, 0xbe, 0x3f, 0x35, 0x39, 0x66, 0x9a, 0xeb, 0x4d, 0xc2, 0x26, 0x1b, 0x26,
0xb1, 0x2a, 0x27, 0xb5, 0x08, 0x2a,
];
let bridge = pkits_cert("BasicSelfIssuedOldKeyNewWithOldCACert");
let ski = cert_ski_key_id(&bridge).expect("bridge cert has an SKI extension");
assert_eq!(ski.as_slice(), &EXPECTED);
}
#[test]
fn cert_aki_key_id_returns_none_when_aki_absent() {
let anchor = pkits_cert("TrustAnchorRootCertificate");
assert!(cert_aki_key_id(&anchor).is_none());
}
#[test]
fn cert_ski_key_id_present_on_trust_anchor() {
const EXPECTED: [u8; 20] = [
0xe4, 0x7d, 0x5f, 0xd1, 0x5c, 0x95, 0x86, 0x08, 0x2c, 0x05, 0xae, 0xbe, 0x75, 0xb6,
0x65, 0xa7, 0xd9, 0x5d, 0xa8, 0x66,
];
let anchor = pkits_cert("TrustAnchorRootCertificate");
let ski = cert_ski_key_id(&anchor).expect("trust anchor has an SKI");
assert_eq!(ski.as_slice(), &EXPECTED);
}
}