b8ce58ddf0
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>
179 lines
6.4 KiB
Rust
179 lines
6.4 KiB
Rust
//! 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());
|
|
}
|