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>
This commit is contained in:
xah30
2026-05-25 17:55:06 +03:00
parent f78633e04f
commit b8ce58ddf0
18 changed files with 1712 additions and 5 deletions
+178
View File
@@ -0,0 +1,178 @@
//! Integration tests for the Aura PKI: issuance, mutual-auth verification and
//! revocation.
use std::path::PathBuf;
use aura_pki::{AuraCa, AuraCertVerifier};
use rustls_pki_types::CertificateDer;
use uuid::Uuid;
use x509_parser::prelude::FromDer;
/// Decode the first CERTIFICATE block of a PEM string into owned DER.
fn pem_to_der(pem: &str) -> CertificateDer<'static> {
let (_, item) =
x509_parser::pem::parse_x509_pem(pem.as_bytes()).expect("issued cert is valid PEM");
assert_eq!(item.label, "CERTIFICATE");
CertificateDer::from(item.contents)
}
/// A unique temp path under the OS temp dir so parallel tests don't collide.
fn temp_path(suffix: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("aura-pki-test-{}-{suffix}", Uuid::new_v4()));
p
}
#[test]
fn test_ca_issue_server_cert() {
let ca = AuraCa::generate("Aura Test CA").unwrap();
let server = ca.issue_server_cert("vpn.example.com").unwrap();
let chain = [pem_to_der(&server.cert_pem)];
let verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
// Correct name verifies.
verifier
.verify_server_cert(&chain, "vpn.example.com")
.expect("server cert should verify for its SAN");
// Wrong name is rejected.
let err = verifier.verify_server_cert(&chain, "evil.example.com");
assert!(err.is_err(), "wrong server name must be rejected");
}
#[test]
fn test_ca_issue_client_cert() {
let ca = AuraCa::generate("Aura Test CA").unwrap();
let client = ca.issue_client_cert("alice").unwrap();
let chain = [pem_to_der(&client.cert_pem)];
let verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
let client_id = verifier
.verify_client_cert(&chain)
.expect("client cert should verify against its CA");
assert_eq!(client_id, "alice");
}
#[test]
fn test_ca_issue_client_cert_uuid_cn() {
// The spec notes client CNs are typically UUIDs; make sure that round-trips.
let ca = AuraCa::generate("Aura Test CA").unwrap();
let id = Uuid::new_v4().to_string();
let client = ca.issue_client_cert(&id).unwrap();
let chain = [pem_to_der(&client.cert_pem)];
let verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
assert_eq!(verifier.verify_client_cert(&chain).unwrap(), id);
}
#[test]
fn test_invalid_cert_rejected() {
// A leaf from an independent CA must not verify against the first CA.
let ca = AuraCa::generate("Aura Test CA").unwrap();
let rogue_ca = AuraCa::generate("Rogue CA").unwrap();
let rogue_client = rogue_ca.issue_client_cert("mallory").unwrap();
let rogue_server = rogue_ca.issue_server_cert("vpn.example.com").unwrap();
let client_chain = [pem_to_der(&rogue_client.cert_pem)];
let server_chain = [pem_to_der(&rogue_server.cert_pem)];
let verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
assert!(
verifier.verify_client_cert(&client_chain).is_err(),
"client cert from a different CA must be rejected"
);
assert!(
verifier
.verify_server_cert(&server_chain, "vpn.example.com")
.is_err(),
"server cert from a different CA must be rejected"
);
}
#[test]
fn test_revoked_cert_rejected() {
let ca = AuraCa::generate("Aura Test CA").unwrap();
let client = ca.issue_client_cert("bob").unwrap();
let chain = [pem_to_der(&client.cert_pem)];
let mut verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
// Before revocation: valid.
assert_eq!(verifier.verify_client_cert(&chain).unwrap(), "bob");
// Revoke by client id (Common Name) and confirm rejection.
verifier.set_revoked(["bob".to_string()]);
let err = verifier.verify_client_cert(&chain);
assert!(err.is_err(), "revoked client id must be rejected");
// Also exercise revoking by serial number (hex of the raw serial).
let serial = {
let der = pem_to_der(&client.cert_pem);
let (_, parsed) =
x509_parser::certificate::X509Certificate::from_der(der.as_ref()).unwrap();
parsed
.tbs_certificate
.raw_serial()
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>()
};
let mut verifier2 = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
verifier2.set_revoked([serial]);
assert!(
verifier2.verify_client_cert(&chain).is_err(),
"revoked serial must be rejected"
);
}
#[test]
fn test_save_load_roundtrip() {
let cert_path = temp_path("ca.pem");
let key_path = temp_path("ca.key");
let original = AuraCa::generate("Aura Persisted CA").unwrap();
original.save(&cert_path, &key_path).unwrap();
let loaded = AuraCa::load(&cert_path, &key_path).unwrap();
// The loaded CA presents the same anchor certificate...
assert_eq!(original.ca_cert_pem(), loaded.ca_cert_pem());
// ...and can still issue certs that verify against that anchor.
let client = loaded.issue_client_cert("carol").unwrap();
let chain = [pem_to_der(&client.cert_pem)];
let verifier = AuraCertVerifier::new(&loaded.ca_cert_pem()).unwrap();
assert_eq!(verifier.verify_client_cert(&chain).unwrap(), "carol");
// A cert issued by the loaded CA also verifies against the ORIGINAL CA's
// anchor (same key + subject), proving the identity survived the round-trip.
let verifier_orig = AuraCertVerifier::new(&original.ca_cert_pem()).unwrap();
assert_eq!(verifier_orig.verify_client_cert(&chain).unwrap(), "carol");
let _ = std::fs::remove_file(&cert_path);
let _ = std::fs::remove_file(&key_path);
}
#[test]
fn test_empty_chain_rejected() {
let ca = AuraCa::generate("Aura Test CA").unwrap();
let verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
let empty: [CertificateDer; 0] = [];
assert!(verifier.verify_client_cert(&empty).is_err());
assert!(verifier
.verify_server_cert(&empty, "vpn.example.com")
.is_err());
}
#[test]
fn test_client_cert_not_valid_as_server_name() {
// A client cert has no DNS SAN, so server-name verification must fail even
// though the chain itself is trusted.
let ca = AuraCa::generate("Aura Test CA").unwrap();
let client = ca.issue_client_cert("dave").unwrap();
let chain = [pem_to_der(&client.cert_pem)];
let verifier = AuraCertVerifier::new(&ca.ca_cert_pem()).unwrap();
assert!(verifier.verify_server_cert(&chain, "dave").is_err());
}