c95e1a482c
Both server and client deterministically rotate the on-wire obfuscation mask (SNI, HTTP Host/User-Agent/Server headers, UDP padding profile) at 05:00 Moscow time (02:00 UTC) every day, derived from the CA fingerprint + UTC date — no network coordination needed. - aura-crypto::masks: MaskSet + 4 palettes (16 SNI, 10 UA, 5 Server, 4 padding profiles); derive_mask_for_msk_date via HKDF-SHA256(salt="aura-mask-v1-salt", ikm=ca_fp||"YYYY-MM-DD", info="aura-mask-v1"); ca_fingerprint with built-in base64 PEM decode (no new deps). - aura-cli::masks: MaskRotator (Arc<RwLock<MaskSet>>) + Hinnant's civil_from_days for manual UTC date math; scheduler picks next 02:00 UTC strictly (avoids busy-loop at boundary); spawned at startup in server::run/client::run. - aura-transport: PADDING_PROFILES + next_bucket_for_profile (profile 0 byte-for- byte equals legacy pad_to_https_size); TcpOpts gains user_agent/server_header; UdpOpts gains padding_profile; MultiServer holds Arc<UdpServer>/Arc<TcpServer> with set_udp_opts/set_tcp_opts so rotation propagates without restart. - Backward-compatible: defaults preserve previous behavior; existing 97 tests unchanged. 17 new tests (derive determinism + date variation, civil-from-days known points incl. 1970-01-01/2000-02-29/2024->2025, next-rotation boundary, msk_today offset, profile equivalence, base64 round-trip, full mask-driven UDP loopback). Total: 114 passed, clippy/fmt clean. No new workspace deps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
98 lines
3.9 KiB
Rust
98 lines
3.9 KiB
Rust
//! End-to-end smoke test for the daily mask rotator: build a CA, derive today's [`MaskSet`], plug
|
|
//! its `padding_profile_id` into the server / client `UdpOpts`, run a UDP loopback handshake, and
|
|
//! exchange a packet. This proves:
|
|
//!
|
|
//! * The crypto-layer derivation produces values that the transport layer accepts.
|
|
//! * The padding profile id derived from `MaskSet` is a valid argument for `pad_to_bucket` /
|
|
//! `next_bucket_for_profile`.
|
|
//! * Wire compatibility is preserved when both ends use the same mask.
|
|
//!
|
|
//! It does NOT exercise the time-based rotation (that runs at 05:00 MSK and would require freezing
|
|
//! the clock); the algorithm itself is unit-tested in `aura_cli::masks::tests`.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use aura_cli::masks::MaskRotator;
|
|
use aura_crypto::derive_mask_for_msk_date;
|
|
use aura_pki::AuraCa;
|
|
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
|
|
use aura_transport::{UdpClient, UdpOpts, UdpServer};
|
|
|
|
const SERVER_NAME: &str = "localhost";
|
|
const CLIENT_ID: &str = "cli-mask-client";
|
|
|
|
fn make_configs(ca: &AuraCa) -> (ServerConfig, ClientConfig) {
|
|
let server_cert = ca
|
|
.issue_server_cert(SERVER_NAME)
|
|
.expect("issue server cert");
|
|
let client_cert = ca.issue_client_cert(CLIENT_ID).expect("issue client cert");
|
|
let ca_pem = ca.ca_cert_pem();
|
|
let server_cfg = ServerConfig {
|
|
ca_cert_pem: ca_pem.clone(),
|
|
server_cert_pem: server_cert.cert_pem.clone(),
|
|
server_key_pem: server_cert.key_pem.clone(),
|
|
};
|
|
let client_cfg = ClientConfig {
|
|
ca_cert_pem: ca_pem,
|
|
client_cert_pem: client_cert.cert_pem,
|
|
client_key_pem: client_cert.key_pem,
|
|
server_name: SERVER_NAME.to_string(),
|
|
};
|
|
(server_cfg, client_cfg)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mask_drives_udp_loopback_with_obfuscation() {
|
|
// Real CA → real CA PEM → real fingerprint → today's MaskSet, both sides.
|
|
let ca = AuraCa::generate("aura-mask-loopback-ca").expect("CA");
|
|
let pem = ca.ca_cert_pem();
|
|
|
|
// Rotator drives the *current* mask (matches a direct derive for today).
|
|
let rotator = MaskRotator::new(&pem).expect("rotator");
|
|
let current = rotator.current().await;
|
|
|
|
// Cross-check against a direct crypto-layer derive for today's MSK day.
|
|
let now = aura_cli::masks::unix_now_utc();
|
|
let (y, m, d) = aura_cli::masks::msk_today(now);
|
|
let fp = aura_crypto::ca_fingerprint(&pem).expect("fp");
|
|
let direct = derive_mask_for_msk_date(&fp, y, m, d);
|
|
assert_eq!(current, direct, "rotator should match direct derivation");
|
|
|
|
// Build UdpOpts with obfuscation on and the *mask's* padding profile id. Both sides agree
|
|
// because they derived from the same CA and date.
|
|
let opts = UdpOpts {
|
|
obfuscate: true,
|
|
padding_profile: current.padding_profile_id,
|
|
..UdpOpts::default()
|
|
};
|
|
|
|
let (server_cfg, client_cfg) = make_configs(&ca);
|
|
|
|
let server =
|
|
UdpServer::bind("127.0.0.1:0".parse().unwrap(), server_cfg, opts).expect("bind udp server");
|
|
let server_addr = server.local_addr().expect("server addr");
|
|
|
|
let accept_task = tokio::spawn(async move { server.accept().await });
|
|
let connect_task =
|
|
tokio::spawn(async move { UdpClient::connect(server_addr, client_cfg, opts).await });
|
|
|
|
let server_conn = accept_task.await.expect("accept join").expect("accept");
|
|
let client_conn = connect_task.await.expect("connect join").expect("connect");
|
|
|
|
assert_eq!(server_conn.peer_id(), Some(CLIENT_ID));
|
|
assert_eq!(client_conn.peer_id(), Some(SERVER_NAME));
|
|
|
|
let server_conn: Arc<dyn PacketConnection> = Arc::new(server_conn);
|
|
let client_conn: Arc<dyn PacketConnection> = Arc::new(client_conn);
|
|
|
|
for pkt in [
|
|
b"hello-mask".to_vec(),
|
|
vec![0xA5u8; 1300],
|
|
(0..200u8).collect::<Vec<u8>>(),
|
|
] {
|
|
client_conn.send_packet(&pkt).await.expect("client send");
|
|
let got = server_conn.recv_packet().await.expect("server recv");
|
|
assert_eq!(got, pkt, "padded round trip preserves the payload");
|
|
}
|
|
}
|