est-ca 0.2.0

RFC 7030 Enrollment over Secure Transport (EST) — client, server, and an internal X.509 CA in pure Rust.
//! Issuing CA — loads the signing key+cert and applies the configured
//! [`CertProfile`] to sign PKCS#10 CSRs.
//!
//! This is the one place in the crate that holds private-key material.
//! Everything above it (EST handlers, auth) consumes [`Issuer::sign_csr`]
//! through an opaque interface.

use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, SystemTime};

use rcgen::{
    CertificateParams, CertificateSigningRequestParams, DnType, ExtendedKeyUsagePurpose,
    IsCa, KeyPair, KeyUsagePurpose, SerialNumber,
};
use rustls_pki_types::CertificateSigningRequestDer;

use crate::ca::profile::CertProfile;
use crate::ca::serial::{random_serial_bytes, SerialStore};
use crate::error::{Error, Result};

/// Issuing intermediate CA. Thread-safe — clone freely.
#[derive(Clone)]
pub struct Issuer {
    inner: Arc<IssuerInner>,
}

struct IssuerInner {
    ca_cert: rcgen::Certificate,
    ca_key: KeyPair,
    ca_der: Vec<u8>,
    profile: CertProfile,
    serials: Arc<dyn SerialStore>,
}

impl Issuer {
    /// Load an existing CA cert + private key from PEM files on disk.
    ///
    /// The cert PEM may contain a chain; only the first certificate is
    /// used as the signing cert. The key file must hold the matching
    /// private key.
    pub fn load_pem(
        cert_path: impl AsRef<Path>,
        key_path: impl AsRef<Path>,
        profile: CertProfile,
        serials: Arc<dyn SerialStore>,
    ) -> Result<Self> {
        let cert_pem = std::fs::read_to_string(cert_path.as_ref())?;
        let key_pem = std::fs::read_to_string(key_path.as_ref())?;
        Self::from_pem(&cert_pem, &key_pem, profile, serials)
    }

    /// Load a CA from in-memory PEM bytes.
    pub fn from_pem(
        cert_pem: &str,
        key_pem: &str,
        profile: CertProfile,
        serials: Arc<dyn SerialStore>,
    ) -> Result<Self> {
        let ca_key =
            KeyPair::from_pem(key_pem).map_err(|e| Error::Parse(format!("CA key PEM: {e}")))?;
        let params = CertificateParams::from_ca_cert_pem(cert_pem)
            .map_err(|e| Error::Parse(format!("CA cert PEM: {e}")))?;
        let ca_cert = params
            .self_signed(&ca_key)
            .map_err(|e| Error::Ca(format!("rebuild CA cert: {e}")))?;
        let ca_der = ca_cert.der().to_vec();
        Ok(Self { inner: Arc::new(IssuerInner { ca_cert, ca_key, ca_der, profile, serials }) })
    }

    /// Generate a fresh self-signed CA in memory. Useful for tests and
    /// for dev environments where a cert chain isn't required.
    pub fn generate_self_signed(
        common_name: &str,
        profile: CertProfile,
        serials: Arc<dyn SerialStore>,
    ) -> Result<Self> {
        let mut params = CertificateParams::new(vec![common_name.to_string()])
            .map_err(|e| Error::Ca(format!("CA params: {e}")))?;
        params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
        params.distinguished_name.push(DnType::CommonName, common_name);
        params.key_usages =
            vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign, KeyUsagePurpose::DigitalSignature];
        let ca_key = KeyPair::generate().map_err(|e| Error::Ca(format!("CA keygen: {e}")))?;
        let ca_cert = params
            .self_signed(&ca_key)
            .map_err(|e| Error::Ca(format!("self-sign CA: {e}")))?;
        let ca_der = ca_cert.der().to_vec();
        Ok(Self { inner: Arc::new(IssuerInner { ca_cert, ca_key, ca_der, profile, serials }) })
    }

    /// Return the CA certificate as DER bytes — what the EST
    /// `/cacerts` endpoint wraps in a PKCS#7 degenerate bundle.
    pub fn ca_cert_der(&self) -> &[u8] {
        &self.inner.ca_der
    }

    /// Sign a PKCS#10 CSR (DER-encoded), producing a leaf cert (DER)
    /// whose Subject CN equals `principal_id`. Any extensions in the
    /// CSR are ignored; the profile is the source of truth.
    pub fn sign_csr(&self, csr_der: &[u8], principal_id: &str) -> Result<Vec<u8>> {
        let profile = &self.inner.profile;
        profile.validate_cn(principal_id).map_err(Error::Profile)?;

        let der_wrapper = CertificateSigningRequestDer::from(csr_der.to_vec());
        let csr = CertificateSigningRequestParams::from_der(&der_wrapper)
            .map_err(|e| Error::InvalidCsr(format!("parse PKCS#10: {e}")))?;

        let mut params = CertificateParams::default();
        params.distinguished_name = Default::default();
        params.distinguished_name.push(DnType::CommonName, principal_id);
        params.is_ca = IsCa::NoCa;
        params.key_usages = vec![KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::KeyEncipherment];
        let mut ekus = Vec::new();
        if profile.client_auth {
            ekus.push(ExtendedKeyUsagePurpose::ClientAuth);
        }
        if profile.server_auth {
            ekus.push(ExtendedKeyUsagePurpose::ServerAuth);
        }
        params.extended_key_usages = ekus;
        let now = SystemTime::now();
        params.not_before = now.into();
        params.not_after = (now + profile.validity).into();

        // RFC 5280 §4.1.2.2 caps the serial at 20 bytes. We use 20 random
        // bytes with the high bit cleared (so the ASN.1 INTEGER stays
        // positive without a leading 0x00), and separately persist a
        // monotonic counter for operator audit (consumed but not placed
        // in the serial itself).
        let _counter = self.inner.serials.next()?;
        let sn_bytes = random_serial_bytes();
        params.serial_number = Some(SerialNumber::from_slice(&sn_bytes));

        let leaf = params
            .signed_by(&csr.public_key, &self.inner.ca_cert, &self.inner.ca_key)
            .map_err(|e| Error::Ca(format!("sign leaf: {e}")))?;
        Ok(leaf.der().to_vec())
    }

    /// Validity window configured on this issuer's profile. Exposed so
    /// renewal windows can be derived without duplicating the source of
    /// truth.
    pub fn leaf_validity(&self) -> Duration {
        self.inner.profile.validity
    }
}