Files
AuraVPN/crates/aura-cli/src/pki.rs
T
xah30 cb89312a27 feat(cli): implement Wave 4 — aura binary (PKI, server/client, admin, bench)
aura-cli: clap command tree (pki init/issue-server/issue-client/revoke/list,
server, client, route add/list/remove, status, bench-crypto); TOML config with
~ expansion and split-tunnel rules -> RouteTable; JSON-over-Unix-socket admin
IPC; server/client data paths wiring transport + tunnel (TUN run needs root).
config/{server,client}.toml.example. 15 tests (pki roundtrip, config parse,
admin-socket roundtrip, loopback connection). Verified the real binary: --help,
bench-crypto, and a full CA->server->client cert workflow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:36:13 +03:00

113 lines
4.2 KiB
Rust

//! `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<Vec<String>> {
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<AuraCa> {
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 `<stem>.crt` / `<stem>.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))
}