//! 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::() }; 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()); }