//! The Aura certificate authority: generation, persistence and issuance. use std::fs; use std::path::Path; use anyhow::Context; use rcgen::string::Ia5String; use rcgen::{ BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType, }; use time::{Duration, OffsetDateTime}; /// Default lifetime of issued leaf certificates. const LEAF_VALIDITY_DAYS: i64 = 365; /// Default lifetime of the CA certificate. const CA_VALIDITY_DAYS: i64 = 3650; /// Small backdating of `not_before` to tolerate minor clock skew. const CLOCK_SKEW_MINUTES: i64 = 5; /// A freshly issued certificate together with its private key, both PEM-encoded. #[derive(Debug, Clone)] pub struct IssuedCert { /// The leaf certificate in PEM format. pub cert_pem: String, /// The leaf's private key in PKCS#8 PEM format. pub key_pem: String, } /// A self-signed Aura certificate authority. /// /// Holds the CA's self-signed certificate (PEM) and `KeyPair`. The issuing /// [`Issuer`] is rebuilt from the cert PEM + key for each signature, which makes /// [`AuraCa::generate`] and [`AuraCa::load`] share exactly the same signing /// path. pub struct AuraCa { cert_pem: String, key: KeyPair, } impl AuraCa { /// Generate a brand new self-signed CA with the given Common Name. pub fn generate(common_name: &str) -> anyhow::Result { let key = KeyPair::generate().context("generating CA key pair")?; let params = ca_params(common_name)?; let cert = params .self_signed(&key) .context("self-signing the CA certificate")?; Ok(AuraCa { cert_pem: cert.pem(), key, }) } /// Persist the CA certificate and private key to the given paths (PEM). pub fn save(&self, cert_path: &Path, key_path: &Path) -> anyhow::Result<()> { fs::write(cert_path, self.cert_pem.as_bytes()) .with_context(|| format!("writing CA cert to {}", cert_path.display()))?; fs::write(key_path, self.key.serialize_pem().as_bytes()) .with_context(|| format!("writing CA key to {}", key_path.display()))?; Ok(()) } /// Load a CA previously written with [`AuraCa::save`]. pub fn load(cert_path: &Path, key_path: &Path) -> anyhow::Result { let cert_pem = fs::read_to_string(cert_path) .with_context(|| format!("reading CA cert from {}", cert_path.display()))?; let key_pem = fs::read_to_string(key_path) .with_context(|| format!("reading CA key from {}", key_path.display()))?; let key = KeyPair::from_pem(&key_pem).context("parsing CA key PEM")?; let ca = AuraCa { cert_pem, key }; // Validate eagerly that the stored cert can be used as an issuer, so a // corrupt CA fails at load time rather than at first issuance. ca.issuer().context("validating loaded CA certificate")?; Ok(ca) } /// Issue a server certificate carrying `DNS:domain` as a SAN and the /// `serverAuth` extended key usage. pub fn issue_server_cert(&self, domain: &str) -> anyhow::Result { let leaf_key = KeyPair::generate().context("generating server leaf key")?; let mut params = leaf_params(domain)?; let dns = Ia5String::try_from(domain) .with_context(|| format!("'{domain}' is not a valid IA5/DNS name"))?; params.subject_alt_names.push(SanType::DnsName(dns)); params .extended_key_usages .push(ExtendedKeyUsagePurpose::ServerAuth); self.finish_leaf(params, leaf_key) } /// Issue a client certificate with `CN = client_id` and the `clientAuth` /// extended key usage. pub fn issue_client_cert(&self, client_id: &str) -> anyhow::Result { let leaf_key = KeyPair::generate().context("generating client leaf key")?; let mut params = leaf_params(client_id)?; params .extended_key_usages .push(ExtendedKeyUsagePurpose::ClientAuth); self.finish_leaf(params, leaf_key) } /// The CA's own self-signed certificate, PEM-encoded. Needed to build an /// [`crate::AuraCertVerifier`]. pub fn ca_cert_pem(&self) -> String { self.cert_pem.clone() } /// Rebuild an [`Issuer`] from the stored CA cert PEM + key. fn issuer(&self) -> anyhow::Result> { Issuer::from_ca_cert_pem(&self.cert_pem, &self.key) .context("reconstructing issuer from CA certificate") } /// Sign a prepared leaf and bundle it with its key. fn finish_leaf( &self, params: CertificateParams, leaf_key: KeyPair, ) -> anyhow::Result { let issuer = self.issuer()?; let cert = params .signed_by(&leaf_key, &issuer) .context("signing leaf certificate with the CA")?; Ok(IssuedCert { cert_pem: cert.pem(), key_pem: leaf_key.serialize_pem(), }) } } /// Build the `CertificateParams` for a self-signed CA. fn ca_params(common_name: &str) -> anyhow::Result { let mut params = CertificateParams::new(Vec::::new()) .context("constructing CA certificate params")?; params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); params .distinguished_name .push(DnType::CommonName, common_name); params.key_usages = vec![ KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign, KeyUsagePurpose::DigitalSignature, ]; let now = OffsetDateTime::now_utc(); params.not_before = now - Duration::minutes(CLOCK_SKEW_MINUTES); params.not_after = now + Duration::days(CA_VALIDITY_DAYS); Ok(params) } /// Build the common `CertificateParams` shared by client and server leaves. /// `cn` is placed in the subject Common Name. fn leaf_params(cn: &str) -> anyhow::Result { let mut params = CertificateParams::new(Vec::::new()) .context("constructing leaf certificate params")?; params.is_ca = IsCa::NoCa; params.distinguished_name.push(DnType::CommonName, cn); params.key_usages = vec![ KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::KeyEncipherment, ]; let now = OffsetDateTime::now_utc(); params.not_before = now - Duration::minutes(CLOCK_SKEW_MINUTES); params.not_after = now + Duration::days(LEAF_VALIDITY_DAYS); Ok(params) }