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>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
//! `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))
|
||||
}
|
||||
Reference in New Issue
Block a user