//! `aura pki` subcommand handlers (project ยง10): CA init, server/client issuance, revocation list. //! //! Each handler is a thin, side-effecting wrapper over [`aura_pki`] that writes PEM/CRL files into //! a directory. They are split out from the clap layer (see [`crate::cli`]) and take plain values so //! the test suite can drive a full init -> issue -> verify roundtrip without spawning the binary. use std::path::{Path, PathBuf}; use anyhow::Context; use aura_pki::{AuraCa, CrlStore}; use crate::config::expand_tilde; /// File name of the CA certificate within a CA directory. pub const CA_CERT: &str = "ca.crt"; /// File name of the CA private key within a CA directory. pub const CA_KEY: &str = "ca.key"; /// Default CRL file name. pub const CRL_FILE: &str = "revoked.crl"; /// `aura pki init`: generate a new CA into `out_dir` as `ca.crt` / `ca.key`. /// /// Creates `out_dir` (and parents) if needed. Returns the paths written. pub fn init(ca_name: &str, out_dir: &Path) -> anyhow::Result<(PathBuf, PathBuf)> { std::fs::create_dir_all(out_dir) .with_context(|| format!("creating output dir {}", out_dir.display()))?; let ca = AuraCa::generate(ca_name).context("generating CA")?; let cert_path = out_dir.join(CA_CERT); let key_path = out_dir.join(CA_KEY); ca.save(&cert_path, &key_path)?; Ok((cert_path, key_path)) } /// `aura pki issue-server`: issue a server cert for `domain` into `out_dir`. /// /// Loads the CA from `ca_dir` (`ca.crt`/`ca.key`) and writes `server.crt` / `server.key`. pub fn issue_server( domain: &str, out_dir: &Path, ca_dir: &Path, ) -> anyhow::Result<(PathBuf, PathBuf)> { let ca = load_ca(ca_dir)?; let issued = ca.issue_server_cert(domain)?; write_leaf(out_dir, "server", &issued.cert_pem, &issued.key_pem) } /// `aura pki issue-client`: issue a client cert with `CN = id` into `out_dir`. /// /// Loads the CA from `ca_dir` and writes `client.crt` / `client.key`. pub fn issue_client(id: &str, out_dir: &Path, ca_dir: &Path) -> anyhow::Result<(PathBuf, PathBuf)> { let ca = load_ca(ca_dir)?; let issued = ca.issue_client_cert(id)?; write_leaf(out_dir, "client", &issued.cert_pem, &issued.key_pem) } /// `aura pki revoke`: add `id` (a client id or serial) to the CRL file, creating it if absent. pub fn revoke(id: &str, crl_path: &Path) -> anyhow::Result<()> { let mut crl = if crl_path.exists() { CrlStore::load(crl_path)? } else { if let Some(parent) = crl_path.parent() { if !parent.as_os_str().is_empty() { std::fs::create_dir_all(parent) .with_context(|| format!("creating CRL dir {}", parent.display()))?; } } CrlStore::new() }; crl.revoke(id.to_string()); crl.save(crl_path)?; Ok(()) } /// `aura pki list`: return the revoked identifiers in the CRL file (empty if the file is absent). pub fn list(crl_path: &Path) -> anyhow::Result> { if !crl_path.exists() { return Ok(Vec::new()); } let crl = CrlStore::load(crl_path)?; Ok(crl.iter().map(str::to_string).collect()) } /// Load a CA from a directory, expanding a leading `~` in the directory path. fn load_ca(ca_dir: &Path) -> anyhow::Result { let dir = expand_tilde(&ca_dir.to_string_lossy()); let cert = dir.join(CA_CERT); let key = dir.join(CA_KEY); AuraCa::load(&cert, &key).with_context(|| { format!( "loading CA from {} (expected {CA_CERT} + {CA_KEY})", dir.display() ) }) } /// Write a leaf cert/key pair as `.crt` / `.key` into `out_dir`. fn write_leaf( out_dir: &Path, stem: &str, cert_pem: &str, key_pem: &str, ) -> anyhow::Result<(PathBuf, PathBuf)> { std::fs::create_dir_all(out_dir) .with_context(|| format!("creating output dir {}", out_dir.display()))?; let cert_path = out_dir.join(format!("{stem}.crt")); let key_path = out_dir.join(format!("{stem}.key")); std::fs::write(&cert_path, cert_pem) .with_context(|| format!("writing {}", cert_path.display()))?; std::fs::write(&key_path, key_pem) .with_context(|| format!("writing {}", key_path.display()))?; Ok((cert_path, key_path)) }