purecrypto 0.6.10

A pure-Rust cryptography toolkit with no foreign-code dependencies, from constant-time primitives up to keys, X.509 and TLS.
Documentation
//! A store of trusted root certificates (trust anchors).

use crate::tls::Error;
use crate::x509::{AnyPublicKey, Certificate, NameConstraints};
use alloc::vec::Vec;

/// A trust anchor: a root certificate's subject name (raw DER), its public
/// key, and any `nameConstraints` the root declared. The name + key are the
/// minimum needed to terminate a chain. We store the raw DER of the subject
/// `Name` so chain-building uses RFC 5280 §7.1 byte-exact equality, immune
/// to encoding differences (PrintableString vs UTF8String, extra attributes,
/// multi-valued RDNs). The anchor's `nameConstraints` (parsed once, at
/// [`RootCertStore::add_der`] time) are seeded into the RFC 5280 §6.1.4
/// constraint state so a deliberately constrained root (e.g. a corporate
/// root limited to `.corp.example`) governs every certificate in the
/// validated path, exactly as an in-chain CA's constraints would.
#[derive(Clone)]
pub(crate) struct TrustAnchor {
    pub(crate) subject_der: Vec<u8>,
    pub(crate) key: AnyPublicKey,
    pub(crate) name_constraints: Option<NameConstraints>,
}

/// A set of trusted root certificates against which peer chains are verified.
///
/// Roots that declare a `nameConstraints` extension keep it: the constraints
/// are enforced over every chain that anchors at that root (RFC 5280 §6.1.4),
/// the same way constraints declared by in-chain intermediate CAs are. See
/// [`RootCertStore::add_der`] for the fail-closed handling of constraint
/// shapes the validator cannot evaluate.
#[derive(Clone, Default)]
pub struct RootCertStore {
    anchors: Vec<TrustAnchor>,
}

impl RootCertStore {
    /// An empty store.
    pub fn new() -> Self {
        RootCertStore {
            anchors: Vec::new(),
        }
    }

    /// Adds a trust anchor from a DER-encoded root certificate, recording its
    /// subject name, public key, and any `nameConstraints` it declares.
    ///
    /// A `nameConstraints` extension on the root is retained and enforced
    /// over every chain that anchors at it (RFC 5280 §6.1.4), exactly as an
    /// in-chain CA's constraints would be. Because an admin installing a
    /// constrained root does so deliberately, this fails closed: a root
    /// whose `nameConstraints` extension does not parse, or whose subtrees
    /// reference a GeneralName variant the validator cannot evaluate
    /// (anything other than dNSName / iPAddress), is rejected rather than
    /// added with its constraints silently ignored.
    pub fn add_der(&mut self, der: Vec<u8>) -> Result<(), Error> {
        let cert = Certificate::from_der(der).map_err(|_| Error::BadCertificate)?;
        let subject_der = cert
            .subject_der()
            .map_err(|_| Error::BadCertificate)?
            .to_vec();
        let key = cert
            .subject_public_key()
            .map_err(|_| Error::BadCertificate)?;
        let name_constraints = cert.name_constraints().map_err(|_| Error::BadCertificate)?;
        if let Some(nc) = &name_constraints
            && (nc.has_unenforceable_permitted || nc.has_unenforceable_excluded)
        {
            return Err(Error::BadCertificate);
        }
        self.anchors.push(TrustAnchor {
            subject_der,
            key,
            name_constraints,
        });
        Ok(())
    }

    /// Adds a trust anchor from a PEM-encoded root certificate.
    pub fn add_pem(&mut self, pem: &str) -> Result<(), Error> {
        let cert = Certificate::from_pem(pem).map_err(|_| Error::BadCertificate)?;
        self.add_der(cert.to_der().to_vec())
    }

    /// The number of trust anchors held.
    pub fn len(&self) -> usize {
        self.anchors.len()
    }

    /// Whether the store has no trust anchors.
    pub fn is_empty(&self) -> bool {
        self.anchors.is_empty()
    }

    /// Builds a store pre-seeded with the embedded root-CA bundle from the
    /// first-party [`cacrt`](https://crates.io/crates/cacrt) crate (the
    /// Mozilla CA set, parsed into static DER at `cacrt` build time).
    ///
    /// This is the zero-configuration trust store: it requires no filesystem
    /// access, so it behaves identically across Linux, macOS, Windows, and
    /// `no_std`-with-alloc targets — unlike reading an OS bundle such as
    /// `/etc/ssl/certs/ca-certificates.crt`. Requires the `embedded-roots`
    /// feature.
    #[cfg(feature = "embedded-roots")]
    pub fn with_embedded_roots() -> Self {
        let mut store = RootCertStore::new();
        store.add_embedded_roots();
        store
    }

    /// Adds every certificate from the embedded [`cacrt`] bundle as a trust
    /// anchor, returning the number successfully added. Requires the
    /// `embedded-roots` feature.
    ///
    /// [`cacrt`]: https://crates.io/crates/cacrt
    #[cfg(feature = "embedded-roots")]
    pub fn add_embedded_roots(&mut self) -> usize {
        let mut added = 0;
        for ca in cacrt::all() {
            if self.add_der(ca.der().to_vec()).is_ok() {
                added += 1;
            }
        }
        added
    }

    /// Clone the entire store (compatibility shim for the unified
    /// [`crate::tls::Config`] builder, which holds a single
    /// [`RootCertStore`] used to seed both client trust anchors and
    /// server-side mTLS trust anchors).
    pub fn clone_store(&self) -> Self {
        self.clone()
    }

    /// Iterates over every trust anchor whose subject `Name` DER matches
    /// `name_der`. Multiple anchors may share a name (cross-signed renewal
    /// scenarios), so callers should try them all rather than stopping at
    /// the first hit.
    pub(crate) fn anchors_with_subject<'a>(
        &'a self,
        name_der: &'a [u8],
    ) -> impl Iterator<Item = &'a TrustAnchor> + 'a {
        self.anchors
            .iter()
            .filter(move |a| a.subject_der.as_slice() == name_der)
    }
}

#[cfg(all(test, feature = "embedded-roots"))]
mod embedded_roots_tests {
    use super::RootCertStore;

    #[test]
    fn with_embedded_roots_is_populated() {
        let store = RootCertStore::with_embedded_roots();
        // The cacrt bundle carries the full Mozilla CA set; sanity-check that a
        // substantial number of anchors loaded (not just a handful).
        assert!(
            store.len() > 50,
            "embedded root store unexpectedly small: {}",
            store.len()
        );
        assert!(!store.is_empty());
    }

    #[test]
    fn add_embedded_roots_reports_count() {
        let mut store = RootCertStore::new();
        let added = store.add_embedded_roots();
        assert_eq!(added, store.len());
        assert!(added > 50);
    }
}