cb89312a27
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>
110 lines
4.0 KiB
Rust
110 lines
4.0 KiB
Rust
//! PKI roundtrip: drive the `aura pki` handlers to init a CA, issue server + client certs, then
|
|
//! verify each against [`aura_pki::AuraCertVerifier`]. A cert from a *different* CA must fail.
|
|
//!
|
|
//! Runs without root or network: everything is file I/O into a unique temp directory.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use aura_cli::pki;
|
|
use aura_pki::{AuraCa, AuraCertVerifier};
|
|
use rustls_pki_types::CertificateDer;
|
|
|
|
/// A unique temp directory for this test process (no `tempfile` dependency in the workspace).
|
|
fn temp_dir(tag: &str) -> PathBuf {
|
|
let mut dir = std::env::temp_dir();
|
|
dir.push(format!(
|
|
"aura-cli-test-{tag}-{}-{}",
|
|
std::process::id(),
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
));
|
|
std::fs::create_dir_all(&dir).expect("create temp dir");
|
|
dir
|
|
}
|
|
|
|
/// Decode a single-certificate PEM string into a DER chain for the verifier.
|
|
fn pem_chain(pem: &str) -> Vec<CertificateDer<'static>> {
|
|
let (_, parsed) = x509_parser::pem::parse_x509_pem(pem.as_bytes()).expect("parse PEM");
|
|
vec![CertificateDer::from(parsed.contents)]
|
|
}
|
|
|
|
#[test]
|
|
fn ca_init_issue_and_verify_roundtrip() {
|
|
let dir = temp_dir("pki");
|
|
|
|
// init the CA.
|
|
let (ca_cert_path, ca_key_path) = pki::init("Aura Roundtrip CA", &dir).expect("pki init");
|
|
assert!(ca_cert_path.exists() && ca_key_path.exists());
|
|
assert_eq!(ca_cert_path.file_name().unwrap(), "ca.crt");
|
|
|
|
// issue server + client certs (CA dir defaults to the same dir).
|
|
let (server_crt, server_key) =
|
|
pki::issue_server("vpn.example.com", &dir, &dir).expect("issue server");
|
|
let (client_crt, client_key) =
|
|
pki::issue_client("client-42", &dir, &dir).expect("issue client");
|
|
assert!(server_crt.exists() && server_key.exists());
|
|
assert!(client_crt.exists() && client_key.exists());
|
|
|
|
// Load the CA back and build a verifier from its PEM.
|
|
let ca = AuraCa::load(&ca_cert_path, &ca_key_path).expect("load CA");
|
|
let verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).expect("verifier");
|
|
|
|
// Verify the server cert for its SAN.
|
|
let server_pem = std::fs::read_to_string(&server_crt).unwrap();
|
|
verifier
|
|
.verify_server_cert(&pem_chain(&server_pem), "vpn.example.com")
|
|
.expect("server cert verifies for its SAN");
|
|
|
|
// Wrong name must fail.
|
|
assert!(verifier
|
|
.verify_server_cert(&pem_chain(&server_pem), "wrong.example.com")
|
|
.is_err());
|
|
|
|
// Verify the client cert; the returned CN must be the issued id.
|
|
let client_pem = std::fs::read_to_string(&client_crt).unwrap();
|
|
let cn = verifier
|
|
.verify_client_cert(&pem_chain(&client_pem))
|
|
.expect("client cert verifies");
|
|
assert_eq!(cn, "client-42");
|
|
|
|
// A certificate from a *different* CA must NOT verify against this CA.
|
|
let other_dir = temp_dir("pki-other");
|
|
pki::init("Other CA", &other_dir).expect("other CA");
|
|
let (other_client, _k) =
|
|
pki::issue_client("intruder", &other_dir, &other_dir).expect("other client");
|
|
let other_pem = std::fs::read_to_string(&other_client).unwrap();
|
|
assert!(
|
|
verifier.verify_client_cert(&pem_chain(&other_pem)).is_err(),
|
|
"a cert from a different CA must fail verification"
|
|
);
|
|
|
|
// Cleanup (best effort).
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
let _ = std::fs::remove_dir_all(&other_dir);
|
|
}
|
|
|
|
#[test]
|
|
fn revoke_then_list() {
|
|
let dir = temp_dir("crl");
|
|
let crl = dir.join("revoked.crl");
|
|
|
|
// Empty / absent CRL lists nothing.
|
|
assert!(pki::list(&crl).unwrap().is_empty());
|
|
|
|
pki::revoke("client-42", &crl).expect("revoke 1");
|
|
pki::revoke("deadbeef", &crl).expect("revoke 2");
|
|
// Re-revoking is idempotent (set semantics).
|
|
pki::revoke("client-42", &crl).expect("revoke dup");
|
|
|
|
let mut listed = pki::list(&crl).expect("list");
|
|
listed.sort();
|
|
assert_eq!(
|
|
listed,
|
|
vec!["client-42".to_string(), "deadbeef".to_string()]
|
|
);
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|