//! Tests for the v2 signed-CRL format ([`CrlStore::save_signed`] / [`CrlStore::load_signed_verified`]). //! //! Covers: //! * happy-path round-trip (encode + decode + verify against the same CA), //! * tampered body rejection (mutate any character in the id list), //! * tampered signature rejection (flip a nibble in the hex signature), //! * cross-CA rejection (decode against a different CA's public key fails), //! * missing-marker rejection. use std::path::PathBuf; use aura_pki::{AuraCa, CrlStore}; use uuid::Uuid; /// A unique temp file path so parallel tests do not 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 } /// Helper: build a CA + a small CRL of two ids. fn make_ca_and_crl() -> (AuraCa, String, CrlStore) { let ca = AuraCa::generate("Aura Test CRL CA").unwrap(); let ca_cert_pem = ca.ca_cert_pem(); let mut crl = CrlStore::new(); crl.revoke("alice"); crl.revoke("deadbeef"); (ca, ca_cert_pem, crl) } #[test] fn signed_crl_round_trip_verifies() { // Borrow a CA + key from the in-memory AuraCa via save/load. let cert_path = temp_path("ca.crt"); let key_path = temp_path("ca.key"); let (ca, ca_cert_pem, crl) = make_ca_and_crl(); ca.save(&cert_path, &key_path).unwrap(); let ca_key_pem = std::fs::read_to_string(&key_path).unwrap(); let crl_path = temp_path("revoked.crl"); crl.save_signed(&crl_path, &ca_cert_pem, &ca_key_pem) .expect("save_signed succeeds"); let loaded = CrlStore::load_signed_verified(&crl_path, &ca_cert_pem).expect("verification succeeds"); assert!(loaded.contains("alice")); assert!(loaded.contains("deadbeef")); assert!(!loaded.contains("bob")); assert_eq!(loaded.len(), 2); let _ = std::fs::remove_file(cert_path); let _ = std::fs::remove_file(key_path); let _ = std::fs::remove_file(crl_path); } #[test] fn tampered_body_fails_verification() { let cert_path = temp_path("ca.crt"); let key_path = temp_path("ca.key"); let (ca, ca_cert_pem, crl) = make_ca_and_crl(); ca.save(&cert_path, &key_path).unwrap(); let ca_key_pem = std::fs::read_to_string(&key_path).unwrap(); let bytes = crl.encode_signed(&ca_cert_pem, &ca_key_pem).unwrap(); let mut text = String::from_utf8(bytes).unwrap(); // Tamper with an id: replace 'alice' with 'allice' (one byte more, sig over original body). text = text.replacen("alice", "allice", 1); let res = CrlStore::decode_signed_verified(text.as_bytes(), &ca_cert_pem); assert!(res.is_err(), "tampered body must fail verification"); let _ = std::fs::remove_file(cert_path); let _ = std::fs::remove_file(key_path); } #[test] fn tampered_signature_fails_verification() { let cert_path = temp_path("ca.crt"); let key_path = temp_path("ca.key"); let (ca, ca_cert_pem, crl) = make_ca_and_crl(); ca.save(&cert_path, &key_path).unwrap(); let ca_key_pem = std::fs::read_to_string(&key_path).unwrap(); let bytes = crl.encode_signed(&ca_cert_pem, &ca_key_pem).unwrap(); let mut text = String::from_utf8(bytes).unwrap(); // Flip the last hex nibble of the signature. let last_idx = text.rfind(|c: char| c.is_ascii_hexdigit()).unwrap(); let ch = text.as_bytes()[last_idx]; let new = if ch == b'0' { b'1' } else { b'0' }; unsafe { text.as_bytes_mut()[last_idx] = new; } let res = CrlStore::decode_signed_verified(text.as_bytes(), &ca_cert_pem); assert!(res.is_err(), "tampered signature must fail verification"); let _ = std::fs::remove_file(cert_path); let _ = std::fs::remove_file(key_path); } #[test] fn signature_against_wrong_ca_fails() { let cert_path = temp_path("ca.crt"); let key_path = temp_path("ca.key"); let (ca, ca_cert_pem, crl) = make_ca_and_crl(); ca.save(&cert_path, &key_path).unwrap(); let ca_key_pem = std::fs::read_to_string(&key_path).unwrap(); let bytes = crl.encode_signed(&ca_cert_pem, &ca_key_pem).unwrap(); // A different CA's anchor cannot verify a CRL signed by the original. let rogue = AuraCa::generate("Rogue CA").unwrap(); let res = CrlStore::decode_signed_verified(&bytes, &rogue.ca_cert_pem()); assert!(res.is_err(), "wrong CA must fail verification"); let _ = std::fs::remove_file(cert_path); let _ = std::fs::remove_file(key_path); } #[test] fn missing_marker_is_rejected() { let (_, ca_cert_pem, _) = make_ca_and_crl(); let bogus = b"CRL-Aura-v1\nalice\nbob\nno-marker-here\n"; assert!(CrlStore::decode_signed_verified(bogus, &ca_cert_pem).is_err()); } #[test] fn unknown_header_is_rejected() { let cert_path = temp_path("ca.crt"); let key_path = temp_path("ca.key"); let (ca, ca_cert_pem, crl) = make_ca_and_crl(); ca.save(&cert_path, &key_path).unwrap(); let ca_key_pem = std::fs::read_to_string(&key_path).unwrap(); let bytes = crl.encode_signed(&ca_cert_pem, &ca_key_pem).unwrap(); // Mutate the header line to something else and re-sign would be needed — but here we just // check that the parser rejects an unknown header verbatim (signature also fails because we // mutated the signed body, but the header check fires first). let mut text = String::from_utf8(bytes).unwrap(); text = text.replacen("CRL-Aura-v1", "CRL-Aura-v9", 1); let res = CrlStore::decode_signed_verified(text.as_bytes(), &ca_cert_pem); assert!(res.is_err(), "unknown header must be rejected"); let _ = std::fs::remove_file(cert_path); let _ = std::fs::remove_file(key_path); } #[test] fn empty_crl_round_trip() { let cert_path = temp_path("ca.crt"); let key_path = temp_path("ca.key"); let ca = AuraCa::generate("Aura Test CRL CA").unwrap(); ca.save(&cert_path, &key_path).unwrap(); let ca_cert_pem = ca.ca_cert_pem(); let ca_key_pem = std::fs::read_to_string(&key_path).unwrap(); let crl = CrlStore::new(); let bytes = crl.encode_signed(&ca_cert_pem, &ca_key_pem).unwrap(); let loaded = CrlStore::decode_signed_verified(&bytes, &ca_cert_pem).unwrap(); assert!(loaded.is_empty(), "empty signed CRL round-trips as empty"); let _ = std::fs::remove_file(cert_path); let _ = std::fs::remove_file(key_path); }