Files
AuraVPN/crates/aura-pki/src/ca.rs
T
xah30 b8ce58ddf0 feat(crypto,pki): implement Wave 1 — hybrid KEM + PKI
aura-crypto: X25519 + ML-KEM-768 (FIPS 203) hybrid KEM, HKDF-SHA256 session
key derivation, ChaCha20-Poly1305 AeadSession with counter nonces; genuine
NIST ACVP ML-KEM-768 KAT (decapsulation vector). 16 tests green, clippy clean.

aura-pki: self-signed CA, server/client cert issuance (rcgen 0.14), mutual
X.509 chain verification via rustls-webpki, CRL revocation. 8 tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:55:06 +03:00

167 lines
6.4 KiB
Rust

//! 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<AuraCa> {
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<AuraCa> {
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<IssuedCert> {
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<IssuedCert> {
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<'_, &KeyPair>> {
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<IssuedCert> {
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<CertificateParams> {
let mut params = CertificateParams::new(Vec::<String>::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<CertificateParams> {
let mut params = CertificateParams::new(Vec::<String>::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)
}