//! End-to-end test of the v2 in-band CRL push flow at the [`PacketConnection`] layer. //! //! We avoid spinning up a real transport (which needs root + privileged sockets) and instead drive //! the server-side helper `push_crl_if_configured` against an in-memory mock `PacketConnection`, //! then feed the bytes the server "sent" into a client-side `AcceptPushedCrlConn` wrapper and //! check that: //! //! * the wrapper consumes the envelope (does NOT deliver it to the TUN-bound `recv_packet`), //! * the wrapper verifies the signature against the CA and applies the CRL, //! * the wrapper persists the parsed CRL to the configured cache path, //! * a real IP packet that arrives *after* the envelope is delivered verbatim to the caller. //! //! The path runs entirely on mpsc channels, so it exercises the wrapping logic without any //! crypto/transport setup. use std::collections::VecDeque; use std::path::PathBuf; use std::sync::Arc; use async_trait::async_trait; use aura_cli::crl_push::{push_crl_if_configured, AcceptPushedCrlConn}; use aura_pki::{AuraCa, CrlStore}; use aura_proto::PacketConnection; use tokio::sync::Mutex; use uuid::Uuid; /// Mock connection with two roles in this test: /// * **server side**: the server's `push_crl_if_configured` calls `send_packet` on its `Arc`. We capture the bytes here. /// * **client side**: the client wraps this same struct (re-instantiated with the captured bytes /// in `to_recv`) and calls `recv_packet`. struct MockConn { to_recv: Mutex>>, sent: Mutex>>, } impl MockConn { fn new(packets: impl IntoIterator>) -> Self { Self { to_recv: Mutex::new(packets.into_iter().collect()), sent: Mutex::new(Vec::new()), } } async fn drain_sent(&self) -> Vec> { std::mem::take(&mut *self.sent.lock().await) } } #[async_trait] impl PacketConnection for MockConn { async fn send_packet(&self, packet: &[u8]) -> anyhow::Result<()> { self.sent.lock().await.push(packet.to_vec()); Ok(()) } async fn recv_packet(&self) -> anyhow::Result> { self.to_recv .lock() .await .pop_front() .ok_or_else(|| anyhow::anyhow!("mock conn drained")) } } fn temp_path(suffix: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!("aura-cli-in_band_crl-{}-{suffix}", Uuid::new_v4())); p } /// Happy path: server pushes a signed CRL of `{"alice"}`; client decodes + applies + persists. #[tokio::test] async fn server_push_is_applied_on_the_client() { // 1. CA + on-disk CA paths (save/load to get the key PEM string). let ca = AuraCa::generate("Aura CRL IT").unwrap(); let ca_cert_pem = ca.ca_cert_pem(); let ca_cert_path = temp_path("ca.crt"); let ca_key_path = temp_path("ca.key"); ca.save(&ca_cert_path, &ca_key_path).unwrap(); // 2. Server-side CRL file (unsigned v1 format). let crl_path = temp_path("revoked.crl"); let mut crl = CrlStore::new(); crl.revoke("alice"); crl.revoke("deadbeef"); crl.save(&crl_path).unwrap(); // 3. Server-side mock conn (its `sent` slot is what the wire would carry). let server_mock = Arc::new(MockConn::new([])); let server_conn: Arc = server_mock.clone(); // 4. Drive the server-side helper. let pushed = push_crl_if_configured( true, Some(crl_path.to_str().unwrap()), &ca_cert_pem, Some(ca_key_path.to_str().unwrap()), &server_conn, Some("test-peer"), ) .await .expect("push_crl_if_configured returns Ok"); assert!(pushed, "server should report a successful push"); // 5. Capture the envelope the server "sent" and inject it on the client side. let envelopes = server_mock.drain_sent().await; assert_eq!(envelopes.len(), 1, "exactly one envelope was sent"); let envelope = envelopes.into_iter().next().unwrap(); assert_eq!( &envelope[..4], &[0xAA, 0xAA, 0xC0, 0x01], "envelope starts with the CRL magic prefix" ); // 6. Build the client-side mock, feeding the envelope first and a real IPv4 packet second. let real_ipv4 = vec![0x45u8, 0x00, 0x00, 0x14, 0xab, 0xcd]; let client_inner: Arc = Arc::new(MockConn::new([envelope, real_ipv4.clone()])); let cache_path = temp_path("client_revoked.crl"); let wrap = AcceptPushedCrlConn::new( client_inner, ca_cert_pem.clone(), Some(cache_path.clone()), true, // accept_pushed_crl = true ); // 7. Client's first recv_packet consumes the envelope (not the IPv4 packet) and applies the // CRL. The next bytes pulled from `recv_packet` are the real IPv4 packet. let pkt = wrap.recv_packet().await.unwrap(); assert_eq!(pkt, real_ipv4, "real packet delivered after envelope"); // 8. Verify the CRL was applied + persisted. let applied = wrap.last_applied.read().await.clone(); let applied = applied.expect("CRL should have been applied"); assert!(applied.contains("alice")); assert!(applied.contains("deadbeef")); assert_eq!(applied.len(), 2); let from_disk = CrlStore::load(&cache_path).unwrap(); assert!(from_disk.contains("alice")); assert!(from_disk.contains("deadbeef")); let _ = std::fs::remove_file(ca_cert_path); let _ = std::fs::remove_file(ca_key_path); let _ = std::fs::remove_file(crl_path); let _ = std::fs::remove_file(cache_path); } /// When `crl_push_enabled = false`, the server never sends an envelope and the client recv path /// continues to behave exactly as in v1. #[tokio::test] async fn server_does_not_push_when_disabled() { let ca = AuraCa::generate("Aura CRL IT").unwrap(); let ca_cert_path = temp_path("ca.crt"); let ca_key_path = temp_path("ca.key"); ca.save(&ca_cert_path, &ca_key_path).unwrap(); let crl_path = temp_path("revoked.crl"); let mut crl = CrlStore::new(); crl.revoke("alice"); crl.save(&crl_path).unwrap(); let server_mock = Arc::new(MockConn::new([])); let server_conn: Arc = server_mock.clone(); let pushed = push_crl_if_configured( false, // disabled Some(crl_path.to_str().unwrap()), &ca.ca_cert_pem(), Some(ca_key_path.to_str().unwrap()), &server_conn, Some("peer"), ) .await .unwrap(); assert!(!pushed, "disabled server should not push"); assert!( server_mock.drain_sent().await.is_empty(), "no bytes should have been sent" ); let _ = std::fs::remove_file(ca_cert_path); let _ = std::fs::remove_file(ca_key_path); let _ = std::fs::remove_file(crl_path); } /// If the CRL file does not exist (no revocations yet), the helper silently skips. #[tokio::test] async fn server_skips_when_crl_file_missing() { let ca = AuraCa::generate("Aura").unwrap(); let ca_cert_path = temp_path("ca.crt"); let ca_key_path = temp_path("ca.key"); ca.save(&ca_cert_path, &ca_key_path).unwrap(); let nonexistent = temp_path("nope.crl"); let server_mock = Arc::new(MockConn::new([])); let server_conn: Arc = server_mock.clone(); let pushed = push_crl_if_configured( true, Some(nonexistent.to_str().unwrap()), &ca.ca_cert_pem(), Some(ca_key_path.to_str().unwrap()), &server_conn, Some("peer"), ) .await .unwrap(); assert!(!pushed, "missing CRL should not push"); assert!(server_mock.drain_sent().await.is_empty()); let _ = std::fs::remove_file(ca_cert_path); let _ = std::fs::remove_file(ca_key_path); } /// If the server pushes a CRL signed by a different CA, the client refuses to apply it. The real /// packet that follows the envelope is still delivered (the wrapper just drops the bad envelope /// and keeps looping). #[tokio::test] async fn client_rejects_push_signed_by_wrong_ca() { let real = AuraCa::generate("Real").unwrap(); let rogue = AuraCa::generate("Rogue").unwrap(); let rogue_cert_path = temp_path("rogue.crt"); let rogue_key_path = temp_path("rogue.key"); rogue.save(&rogue_cert_path, &rogue_key_path).unwrap(); let crl_path = temp_path("rogue.crl"); let mut crl = CrlStore::new(); crl.revoke("alice"); crl.save(&crl_path).unwrap(); // Server "pushes" using the rogue CA. let server_mock = Arc::new(MockConn::new([])); let server_conn: Arc = server_mock.clone(); let pushed = push_crl_if_configured( true, Some(crl_path.to_str().unwrap()), &rogue.ca_cert_pem(), Some(rogue_key_path.to_str().unwrap()), &server_conn, None, ) .await .unwrap(); assert!(pushed); let envelope = server_mock.drain_sent().await.into_iter().next().unwrap(); // Client trusts only `real`; the rogue's signature must fail verification. let real_ipv4 = vec![0x45u8, 0x00, 0x00, 0x14]; let client_inner: Arc = Arc::new(MockConn::new([envelope, real_ipv4.clone()])); let wrap = AcceptPushedCrlConn::new(client_inner, real.ca_cert_pem(), None, true); let pkt = wrap.recv_packet().await.unwrap(); assert_eq!(pkt, real_ipv4); assert!( wrap.last_applied.read().await.is_none(), "rogue-signed CRL must not be applied" ); let _ = std::fs::remove_file(rogue_cert_path); let _ = std::fs::remove_file(rogue_key_path); let _ = std::fs::remove_file(crl_path); } /// `accept_pushed_crl = false` makes the client drop pushes (the wrapper still strips the envelope /// so the TUN never sees the magic bytes). #[tokio::test] async fn client_drops_push_when_disabled() { let ca = AuraCa::generate("Aura").unwrap(); let ca_cert_path = temp_path("ca.crt"); let ca_key_path = temp_path("ca.key"); ca.save(&ca_cert_path, &ca_key_path).unwrap(); let crl_path = temp_path("revoked.crl"); let mut crl = CrlStore::new(); crl.revoke("alice"); crl.save(&crl_path).unwrap(); let server_mock = Arc::new(MockConn::new([])); let server_conn: Arc = server_mock.clone(); let _ = push_crl_if_configured( true, Some(crl_path.to_str().unwrap()), &ca.ca_cert_pem(), Some(ca_key_path.to_str().unwrap()), &server_conn, None, ) .await .unwrap(); let envelope = server_mock.drain_sent().await.into_iter().next().unwrap(); let real_ipv4 = vec![0x45u8, 0x00, 0x00, 0x14]; let client_inner: Arc = Arc::new(MockConn::new([envelope, real_ipv4.clone()])); let wrap = AcceptPushedCrlConn::new( client_inner, ca.ca_cert_pem(), None, false, /* accept */ ); let pkt = wrap.recv_packet().await.unwrap(); assert_eq!(pkt, real_ipv4); assert!( wrap.last_applied.read().await.is_none(), "disabled accept must not apply the CRL" ); let _ = std::fs::remove_file(ca_cert_path); let _ = std::fs::remove_file(ca_key_path); let _ = std::fs::remove_file(crl_path); }