use super::*; use crate::crypto::sha256_hmac; use crate::tls_front::emulator::build_emulated_server_hello; use crate::tls_front::types::{CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource}; use std::time::SystemTime; /// Build a TLS-handshake-like buffer that contains a valid HMAC digest /// for the given `secret` and `timestamp`. /// /// Layout (bytes): /// [0..TLS_DIGEST_POS] : fixed filler (0x42) /// [TLS_DIGEST_POS..+32] : digest = HMAC XOR [0..0 || timestamp_le] /// [TLS_DIGEST_POS+32] : session_id_len = 32 /// [TLS_DIGEST_POS+33..+65] : session_id filler (0x42) fn make_valid_tls_handshake_with_session_id( secret: &[u8], timestamp: u32, session_id: &[u8], ) -> Vec { let session_id_len = session_id.len(); assert!(session_id_len <= u8::MAX as usize); let len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1 + session_id_len; let mut handshake = vec![0x42u8; len]; handshake[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8; let sid_start = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1; handshake[sid_start..sid_start + session_id_len].copy_from_slice(session_id); // Zero the digest slot before computing HMAC (mirrors what validate does). handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].fill(0); let computed = sha256_hmac(secret, &handshake); // digest = HMAC such that XOR with stored digest yields [0..0, timestamp_le]. // bytes 0-27 of digest == computed[0..28] -> xored[..28] == 0 // bytes 28-31 of digest == computed[28..32] XOR timestamp_le let mut digest = computed; let ts = timestamp.to_le_bytes(); for i in 0..4 { digest[28 + i] ^= ts[i]; } handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN] .copy_from_slice(&digest); handshake } fn make_valid_tls_handshake(secret: &[u8], timestamp: u32) -> Vec { make_valid_tls_handshake_with_session_id(secret, timestamp, &[0x42; 32]) } // ------------------------------------------------------------------ // Happy-path sanity // ------------------------------------------------------------------ #[test] fn valid_handshake_with_correct_secret_accepted() { let secret = b"correct_horse_battery_staple_32b"; // timestamp = 0 triggers is_boot_time path, accepted without wall-clock check. let handshake = make_valid_tls_handshake(secret, 0); let secrets = vec![("alice".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&handshake, &secrets, true); assert!(result.is_some(), "Valid handshake must be accepted"); assert_eq!(result.unwrap().user, "alice"); } #[test] fn deterministic_external_vector_validates_without_helper() { // Deterministic vector generated by an external Python stdlib HMAC script, // not by this test module helper. This catches mirrored helper mistakes. let secret = hex::decode("00112233445566778899aabbccddeeff").unwrap(); let handshake = hex::decode( "4242424242424242424242a93225d1d6b46260bc9ce0cc48c7487d2b1ca5afa7ae9fc6609d9e60a3ca842b204242424242424242424242424242424242424242424242424242424242424242", ) .unwrap(); let secrets = vec![("vector_user".to_string(), secret)]; let result = validate_tls_handshake(&handshake, &secrets, true).unwrap(); assert_eq!(result.user, "vector_user"); assert_eq!(result.timestamp, 0x01020304); } #[test] fn valid_handshake_timestamp_extracted_correctly() { let secret = b"ts_extraction_test"; let ts: u32 = 0xDEAD_BEEF; let handshake = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&handshake, &secrets, true); assert!(result.is_some()); assert_eq!(result.unwrap().timestamp, ts); } // ------------------------------------------------------------------ // HMAC bit-flip rejection - adversarial HMAC forgery attempts // ------------------------------------------------------------------ /// Flip every single bit across the 28-byte HMAC check window one at a /// time. Each flip must cause rejection. This is the primary guard /// against a censor gradually narrowing down a valid HMAC via partial /// matches (which would be exploitable with a variable-time comparison). #[test] fn hmac_single_bit_flip_anywhere_in_check_window_rejected() { let secret = b"flip_test_secret"; let base = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; // First ensure the unmodified handshake is accepted. assert!( validate_tls_handshake(&base, &secrets, true).is_some(), "Baseline handshake must be accepted before flip tests" ); for byte_pos in 0..28usize { for bit in 0u8..8 { let mut h = base.clone(); h[TLS_DIGEST_POS + byte_pos] ^= 1 << bit; assert!( validate_tls_handshake(&h, &secrets, true).is_none(), "Flip of bit {bit} in HMAC byte {byte_pos} must be rejected" ); } } } /// XOR entire check window (bytes 0-27) with 0xFF - must still fail. #[test] fn hmac_full_window_corruption_rejected() { let secret = b"full_window_test"; let mut h = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; for i in 0..28 { h[TLS_DIGEST_POS + i] ^= 0xFF; } assert!(validate_tls_handshake(&h, &secrets, true).is_none()); } /// Byte 27 is the last byte in the checked window. A non-constant-time /// `all(|b| b == 0)` that short-circuits on byte 0 would never even reach /// byte 27, making this an effective "did the fix actually run to the end" /// sentinel: if this passes but the earlier byte-0 test fails, the check /// window is not being evaluated end-to-end. #[test] fn hmac_last_byte_of_check_window_enforced() { let secret = b"last_byte_sentinel"; let mut h = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; // Corrupt only byte 27. h[TLS_DIGEST_POS + 27] ^= 0x01; assert!( validate_tls_handshake(&h, &secrets, true).is_none(), "Corruption at byte 27 (end of HMAC window) must cause rejection" ); } // ------------------------------------------------------------------ // User enumeration / multi-user ordering // ------------------------------------------------------------------ #[test] fn wrong_user_secret_rejected_even_with_valid_structure() { let secret_a = b"secret_alpha"; let secret_b = b"secret_beta"; let handshake = make_valid_tls_handshake(secret_b, 0); // Only user_a is configured. let secrets = vec![("user_a".to_string(), secret_a.to_vec())]; assert!( validate_tls_handshake(&handshake, &secrets, true).is_none(), "Handshake for user_b must fail when only user_a is configured" ); } #[test] fn second_user_in_list_found_when_first_does_not_match() { let secret_a = b"secret_alpha"; let secret_b = b"secret_beta"; let handshake = make_valid_tls_handshake(secret_b, 0); let secrets = vec![ ("user_a".to_string(), secret_a.to_vec()), ("user_b".to_string(), secret_b.to_vec()), ]; let result = validate_tls_handshake(&handshake, &secrets, true); assert!(result.is_some(), "user_b must be found even though user_a comes first"); assert_eq!(result.unwrap().user, "user_b"); } #[test] fn duplicate_secret_keeps_first_user_identity() { // If multiple entries share the same secret, the selected identity must // stay stable and deterministic (first entry wins). let shared = b"same_secret_for_two_users"; let handshake = make_valid_tls_handshake(shared, 0); let secrets = vec![ ("first_user".to_string(), shared.to_vec()), ("second_user".to_string(), shared.to_vec()), ]; let result = validate_tls_handshake(&handshake, &secrets, true); assert!(result.is_some()); assert_eq!(result.unwrap().user, "first_user"); } #[test] fn no_user_matches_returns_none() { let secret_a = b"aaa"; let secret_b = b"bbb"; let secret_c = b"ccc"; let handshake = make_valid_tls_handshake(b"unknown_secret", 0); let secrets = vec![ ("a".to_string(), secret_a.to_vec()), ("b".to_string(), secret_b.to_vec()), ("c".to_string(), secret_c.to_vec()), ]; assert!(validate_tls_handshake(&handshake, &secrets, true).is_none()); } #[test] fn empty_secrets_list_rejects_everything() { let secret = b"test"; let handshake = make_valid_tls_handshake(secret, 0); let secrets: Vec<(String, Vec)> = Vec::new(); assert!(validate_tls_handshake(&handshake, &secrets, true).is_none()); } // ------------------------------------------------------------------ // Timestamp / time-skew boundary attacks // ------------------------------------------------------------------ #[test] fn timestamp_at_time_skew_boundaries_accepted() { let secret = b"skew_boundary_test_secret"; let now: i64 = 1_700_000_000; let secrets = vec![("u".to_string(), secret.to_vec())]; // time_diff = now - ts = TIME_SKEW_MIN = -1200 // -> ts = now - TIME_SKEW_MIN = now + 1200 (20 min in the future). let ts_at_future_limit = (now - TIME_SKEW_MIN) as u32; let h = make_valid_tls_handshake(secret, ts_at_future_limit); assert!( validate_tls_handshake_at_time(&h, &secrets, false, now).is_some(), "Timestamp at max-allowed future (time_diff = TIME_SKEW_MIN) must be accepted" ); // time_diff = now - ts = TIME_SKEW_MAX = 600 // -> ts = now - TIME_SKEW_MAX = now - 600 (10 min in the past). let ts_at_past_limit = (now - TIME_SKEW_MAX) as u32; let h = make_valid_tls_handshake(secret, ts_at_past_limit); assert!( validate_tls_handshake_at_time(&h, &secrets, false, now).is_some(), "Timestamp at max-allowed past (time_diff = TIME_SKEW_MAX) must be accepted" ); } #[test] fn timestamp_one_second_outside_skew_window_rejected() { let secret = b"skew_outside_test_secret"; let now: i64 = 1_700_000_000; let secrets = vec![("u".to_string(), secret.to_vec())]; // time_diff = TIME_SKEW_MAX + 1 = 601 (one second too far in the past) // -> ts = now - (TIME_SKEW_MAX + 1) = now - 601 let ts_too_past = (now - TIME_SKEW_MAX - 1) as u32; let h = make_valid_tls_handshake(secret, ts_too_past); assert!( validate_tls_handshake_at_time(&h, &secrets, false, now).is_none(), "Timestamp one second too far in the past must be rejected" ); // time_diff = TIME_SKEW_MIN - 1 = -1201 (one second too far in the future) // -> ts = now - (TIME_SKEW_MIN - 1) = now + 1201 let ts_too_future = (now - TIME_SKEW_MIN + 1) as u32; let h = make_valid_tls_handshake(secret, ts_too_future); assert!( validate_tls_handshake_at_time(&h, &secrets, false, now).is_none(), "Timestamp one second too far in the future must be rejected" ); } #[test] fn ignore_time_skew_accepts_far_future_timestamp() { let secret = b"ignore_skew_test"; let now: i64 = 1_700_000_000; let secrets = vec![("u".to_string(), secret.to_vec())]; // 1 hour in the future - outside TIME_SKEW_MAX but should pass with flag. let future_ts = (now + 3600) as u32; let h = make_valid_tls_handshake(secret, future_ts); assert!( validate_tls_handshake_at_time(&h, &secrets, true, now).is_some(), "ignore_time_skew=true must override window rejection" ); assert!( validate_tls_handshake_at_time(&h, &secrets, false, now).is_none(), "ignore_time_skew=false must still reject far-future timestamp" ); } #[test] fn boot_time_timestamp_accepted_without_ignore_flag() { // Timestamps below the boot-time threshold are treated as client uptime, // not real wall-clock time. The proxy allows them regardless of skew. let secret = b"boot_time_test"; // Keep this safely below compatibility cap to assert bypass behavior. let boot_ts: u32 = BOOT_TIME_COMPAT_MAX_SECS.saturating_sub(1); let handshake = make_valid_tls_handshake(secret, boot_ts); let secrets = vec![("u".to_string(), secret.to_vec())]; assert!( validate_tls_handshake(&handshake, &secrets, false).is_some(), "Boot-time timestamp must be accepted even with ignore_time_skew=false" ); } // ------------------------------------------------------------------ // Structural / length boundary attacks // ------------------------------------------------------------------ #[test] fn too_short_handshake_rejected_without_panic() { let secrets = vec![("u".to_string(), b"s".to_vec())]; // Exactly one byte short of the minimum required length. let h = vec![0u8; TLS_DIGEST_POS + TLS_DIGEST_LEN]; assert!(validate_tls_handshake(&h, &secrets, true).is_none()); // Empty buffer. assert!(validate_tls_handshake(&[], &secrets, true).is_none()); } #[test] fn all_prefix_lengths_below_minimum_rejected_without_panic() { let min_len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1; let secrets = vec![("u".to_string(), b"s".to_vec())]; for len in 0..min_len { let h = vec![0u8; len]; assert!( validate_tls_handshake(&h, &secrets, true).is_none(), "prefix length {len} below minimum must be rejected" ); } } #[test] fn claimed_session_id_overflows_buffer_rejected() { let session_id_len: usize = 32; let min_len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1 + session_id_len; let mut h = vec![0u8; min_len]; // Claim session_id is 33 bytes - one more than the buffer holds. h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = (session_id_len + 1) as u8; let secrets = vec![("u".to_string(), b"s".to_vec())]; assert!(validate_tls_handshake(&h, &secrets, true).is_none()); } #[test] fn max_session_id_len_255_does_not_panic() { // session_id_len = 255 with a buffer that is far too small for it. let min_len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1 + 32; let mut h = vec![0u8; min_len]; h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = 255; let secrets = vec![("u".to_string(), b"s".to_vec())]; assert!(validate_tls_handshake(&h, &secrets, true).is_none()); } #[test] fn one_byte_session_id_validates_and_is_preserved() { let secret = b"sid_len_1_test"; let handshake = make_valid_tls_handshake_with_session_id(secret, 0, &[0xAB]); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&handshake, &secrets, true) .expect("one-byte session_id handshake must validate"); assert_eq!(result.session_id, vec![0xAB]); } #[test] fn max_session_id_len_255_with_valid_digest_is_rejected_by_rfc_cap() { let secret = b"sid_len_255_test"; let session_id = vec![0xCCu8; 255]; let handshake = make_valid_tls_handshake_with_session_id(secret, 0, &session_id); let secrets = vec![("u".to_string(), secret.to_vec())]; assert!( validate_tls_handshake(&handshake, &secrets, true).is_none(), "legacy_session_id length > 32 must be rejected even with valid digest" ); } // ------------------------------------------------------------------ // Adversarial digest values // ------------------------------------------------------------------ #[test] fn all_zeros_digest_rejected() { // An all-zeros digest would only pass if HMAC(secret, msg) happens to // have its first 28 bytes all zero, which is computationally infeasible. let session_id_len: usize = 32; let min_len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1 + session_id_len; let mut h = vec![0x42u8; min_len]; h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8; h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].fill(0); let secrets = vec![("u".to_string(), b"test_secret".to_vec())]; assert!(validate_tls_handshake(&h, &secrets, true).is_none()); } #[test] fn all_ones_digest_rejected() { let session_id_len: usize = 32; let min_len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1 + session_id_len; let mut h = vec![0x42u8; min_len]; h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8; h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].fill(0xFF); let secrets = vec![("u".to_string(), b"test_secret".to_vec())]; assert!(validate_tls_handshake(&h, &secrets, true).is_none()); } /// Simulate a censor that sends 200 crafted packets with random digests. /// Every single one must be rejected; no random digest should accidentally /// pass (probability 2^{-224} per attempt; negligible for 200 trials). #[test] fn censor_probe_random_digests_all_rejected() { use crate::crypto::SecureRandom; let secret = b"production_like_secret_value_xyz"; let session_id_len: usize = 32; let min_len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1 + session_id_len; let secrets = vec![("u".to_string(), secret.to_vec())]; let rng = SecureRandom::new(); for attempt in 0..200 { let mut h = vec![0x42u8; min_len]; h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8; let rand_digest = rng.bytes(TLS_DIGEST_LEN); h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN] .copy_from_slice(&rand_digest); assert!( validate_tls_handshake(&h, &secrets, true).is_none(), "Random digest at attempt {attempt} must not match" ); } } /// The check window is bytes 0-27 of the XOR result. Bytes 28-31 encode /// the timestamp and must NOT affect whether the HMAC portion validates - /// only the timestamp range check uses them. Build a valid handshake with /// timestamp = 0 (boot-time), flip each of bytes 28-31 with ignore_time_skew /// enabled, and verify the HMAC portion still passes (the timestamp changes /// but the proxy still accepts the connection under ignore_time_skew). #[test] fn timestamp_bytes_28_31_do_not_affect_hmac_window() { let secret = b"window_boundary_test"; let base = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; // Baseline must pass. assert!(validate_tls_handshake(&base, &secrets, true).is_some()); // Flip each of the timestamp bytes; with ignore_time_skew the // modified timestamps (small absolute values) still pass boot-time check. for i in 28..32usize { let mut h = base.clone(); h[TLS_DIGEST_POS + i] ^= 0xFF; // The new timestamp is non-zero but potentially still < boot threshold; // use ignore_time_skew=true so wallet test is HMAC-only. assert!( validate_tls_handshake(&h, &secrets, true).is_some(), "Flipping byte {i} (timestamp region) must not invalidate HMAC window" ); } } // ------------------------------------------------------------------ // session_id preservation // ------------------------------------------------------------------ #[test] fn session_id_is_preserved_verbatim_in_validation_result() { // If session_id extraction is ever broken (wrong offset, wrong length, // off-by-one), this test will catch it before it silently corrupts the // ServerHello that echoes the session_id back to the client. let secret = b"session_id_preservation_test"; let handshake = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&handshake, &secrets, true).unwrap(); let sid_len_pos = TLS_DIGEST_POS + TLS_DIGEST_LEN; let sid_len = handshake[sid_len_pos] as usize; let expected = &handshake[sid_len_pos + 1..sid_len_pos + 1 + sid_len]; assert_eq!( result.session_id, expected, "session_id in TlsValidation must be the verbatim bytes from the handshake" ); } // ------------------------------------------------------------------ // Clock decoupling - ignore_time_skew must not consult the system clock // ------------------------------------------------------------------ /// When `ignore_time_skew = true`, a valid HMAC must be accepted even if /// `now = 0` (the sentinel used when the clock is not needed). A broken /// system clock cannot silently deny service when the admin has explicitly /// disabled timestamp checking. #[test] fn ignore_time_skew_accepts_valid_hmac_with_now_zero() { let secret = b"clock_decoupling_test"; // Use a realistic Unix timestamp that would be far outside the window // if compared against now=0 (time_diff would be ~-1_700_000_000). let realistic_ts: u32 = 1_700_000_000; let h = make_valid_tls_handshake(secret, realistic_ts); let secrets = vec![("u".to_string(), secret.to_vec())]; assert!( validate_tls_handshake_at_time(&h, &secrets, true, 0).is_some(), "ignore_time_skew=true must accept a valid HMAC regardless of `now`" ); // Confirm that the same handshake IS rejected when the window is enforced // and now=0 (time_diff very negative -> outside window). This distinguishes // "clock decoupling" from "always accept". assert!( validate_tls_handshake_at_time(&h, &secrets, false, 0).is_none(), "ignore_time_skew=false with now=0 must still reject out-of-window timestamps" ); } /// An HMAC-invalid handshake must be rejected even when ignore_time_skew=true /// and now=0. Verifies that the clock-decoupling fix did not weaken HMAC /// enforcement in the ignore_time_skew path. #[test] fn ignore_time_skew_with_now_zero_still_rejects_bad_hmac() { let secret = b"clock_no_backdoor_test"; let mut h = make_valid_tls_handshake(secret, 1_700_000_000); let secrets = vec![("u".to_string(), secret.to_vec())]; // Corrupt the HMAC check window. h[TLS_DIGEST_POS] ^= 0xFF; assert!( validate_tls_handshake_at_time(&h, &secrets, true, 0).is_none(), "Broken HMAC must be rejected even with ignore_time_skew=true and now=0" ); } #[test] fn system_time_before_unix_epoch_is_rejected_without_panic() { let before_epoch = UNIX_EPOCH .checked_sub(std::time::Duration::from_secs(1)) .expect("UNIX_EPOCH minus one second must be representable"); assert!(system_time_to_unix_secs(before_epoch).is_none()); } /// `i64::MAX` is 9_223_372_036_854_775_807 seconds (~292 billion years CE). /// Any `SystemTime` whose duration since epoch exceeds `i64::MAX` seconds /// must return `None` rather than silently wrapping to a large negative /// timestamp that would corrupt every subsequent time-skew comparison. #[test] fn system_time_far_future_overflowing_i64_returns_none() { // i64::MAX + 1 seconds past epoch overflows i64 when cast naively with `as`. let overflow_secs = u64::try_from(i64::MAX).unwrap() + 1; if let Some(far_future) = UNIX_EPOCH.checked_add(std::time::Duration::from_secs(overflow_secs)) { assert!( system_time_to_unix_secs(far_future).is_none(), "Seconds > i64::MAX must return None, not a wrapped negative timestamp" ); } // If the platform cannot represent this SystemTime, the test is vacuously // satisfied: `checked_add` returning None means the platform already rejects it. } // ------------------------------------------------------------------ // Message canonicalization — HMAC covers every byte of the handshake // ------------------------------------------------------------------ /// Every byte before TLS_DIGEST_POS is part of the HMAC input (because msg /// = full handshake with only the digest slot zeroed). An attacker cannot /// replay a valid handshake with a modified ClientHello header while keeping /// the stored digest; each such modification produces a different HMAC. #[test] fn pre_digest_bytes_are_hmac_covered() { // TLS_DIGEST_POS = 11, so 11 bytes precede the digest. let secret = b"pre_digest_coverage_test"; let base = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; for byte_pos in 0..TLS_DIGEST_POS { let mut h = base.clone(); h[byte_pos] ^= 0x01; assert!( validate_tls_handshake(&h, &secrets, true).is_none(), "Flip in pre-digest byte {byte_pos} must cause HMAC check failure" ); } } /// session_id bytes follow the digest in the buffer and are also part of the /// HMAC input. Flipping any of them invalidates the stored digest, preventing /// a censor from capturing a valid session_id and replaying it with a different /// one while keeping the rest of the packet intact. #[test] fn session_id_bytes_are_hmac_covered() { let secret = b"session_id_coverage_test"; let base = make_valid_tls_handshake(secret, 0); // session_id_len = 32 let secrets = vec![("u".to_string(), secret.to_vec())]; let sid_start = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1; for byte_pos in sid_start..base.len() { let mut h = base.clone(); h[byte_pos] ^= 0x01; assert!( validate_tls_handshake(&h, &secrets, true).is_none(), "Flip in session_id byte at offset {byte_pos} must cause HMAC check failure" ); } } /// Appending even one byte to a valid handshake changes the HMAC input (msg /// includes all bytes) and therefore invalidates the stored digest. This /// prevents a length-extension-style modification of the payload. #[test] fn appended_trailing_byte_causes_rejection() { let secret = b"trailing_byte_test"; let mut h = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; assert!(validate_tls_handshake(&h, &secrets, true).is_some(), "baseline"); h.push(0x00); assert!( validate_tls_handshake(&h, &secrets, true).is_none(), "Appending a trailing byte to a valid handshake must invalidate the HMAC" ); } // ------------------------------------------------------------------ // Zero-length session_id (structural edge case) // ------------------------------------------------------------------ /// session_id_len = 0 is legal in the TLS spec. The validator must accept a /// valid handshake with an empty session_id and return an empty session_id /// slice without panicking or accessing out-of-bounds memory. #[test] fn zero_length_session_id_accepted() { let secret = b"zero_sid_test"; // Buffer: pre-digest | digest | session_id_len=0 (no session_id bytes follow) let len = TLS_DIGEST_POS + TLS_DIGEST_LEN + 1; let mut handshake = vec![0x42u8; len]; handshake[TLS_DIGEST_POS + TLS_DIGEST_LEN] = 0; // session_id_len = 0 handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].fill(0); let computed = sha256_hmac(secret, &handshake); // timestamp = 0 → ts XOR bytes are all zero → digest = computed unchanged. handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN] .copy_from_slice(&computed); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&handshake, &secrets, true); assert!(result.is_some(), "zero-length session_id must be accepted"); assert!( result.unwrap().session_id.is_empty(), "session_id field must be empty when session_id_len = 0" ); } // ------------------------------------------------------------------ // Boot-time threshold — exact boundary precision // ------------------------------------------------------------------ /// timestamp = BOOT_TIME_COMPAT_MAX_SECS - 1 is the last value inside /// the runtime boot-time compatibility window. /// is_boot_time = true → skew check is skipped entirely → accepted even /// when `now` is far from the timestamp. #[test] fn timestamp_one_below_boot_threshold_bypasses_skew_check() { let secret = b"boot_last_value_test"; let ts: u32 = BOOT_TIME_COMPAT_MAX_SECS - 1; let h = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; // now = 0 → time_diff would be -86_399_999, way outside [-1200, 600]. // Boot-time bypass must prevent the skew check from running. assert!( validate_tls_handshake_at_time(&h, &secrets, false, 0).is_some(), "ts=BOOT_TIME_COMPAT_MAX_SECS-1 must bypass skew check regardless of now" ); } /// timestamp = BOOT_TIME_COMPAT_MAX_SECS is the first value outside the /// runtime boot-time compatibility window. /// is_boot_time = false → skew check IS applied. Two sub-cases confirm this: /// once with now chosen so the skew passes (accepted) and once where it fails. #[test] fn timestamp_at_boot_threshold_triggers_skew_check() { let secret = b"boot_exact_value_test"; let ts: u32 = BOOT_TIME_COMPAT_MAX_SECS; let h = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; // now = ts + 50 → time_diff = 50, within [-1200, 600] → accepted. let now_valid: i64 = ts as i64 + 50; assert!( validate_tls_handshake_at_time_with_boot_cap( &h, &secrets, false, now_valid, BOOT_TIME_COMPAT_MAX_SECS, ) .is_some(), "ts=BOOT_TIME_COMPAT_MAX_SECS within skew window must be accepted via skew check" ); // now = -1 → time_diff = -121 at the 120-second threshold, outside window // for TIME_SKEW_MIN=-120. If boot-time bypass were wrongly applied this // would pass. assert!( validate_tls_handshake_at_time_with_boot_cap( &h, &secrets, false, -1, BOOT_TIME_COMPAT_MAX_SECS, ) .is_none(), "ts=BOOT_TIME_COMPAT_MAX_SECS far from now must be rejected — no boot-time bypass" ); } #[test] fn replay_window_cap_disables_boot_bypass_for_old_timestamps() { let secret = b"boot_cap_disabled_test"; let ts: u32 = 900; let h = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake_with_replay_window(&h, &secrets, false, 300); assert!( result.is_none(), "timestamp above replay-window cap must not use boot-time bypass" ); } #[test] fn replay_window_cap_still_allows_small_boot_timestamp() { let secret = b"boot_cap_enabled_test"; let ts: u32 = BOOT_TIME_COMPAT_MAX_SECS.saturating_sub(1); let h = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake_with_replay_window(&h, &secrets, false, 300); assert!( result.is_some(), "timestamp below replay-window cap must retain boot-time compatibility" ); } #[test] fn large_replay_window_is_hard_capped_for_boot_compatibility() { let secret = b"boot_cap_hard_limit_test"; let ts: u32 = BOOT_TIME_COMPAT_MAX_SECS + 1; let h = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake_with_replay_window(&h, &secrets, false, u64::MAX); assert!( result.is_none(), "very large replay window must not expand boot-time bypass beyond hard compatibility cap" ); } #[test] fn ignore_time_skew_explicitly_decouples_from_boot_time_cap() { let secret = b"ignore_skew_boot_cap_decouple_test"; let ts: u32 = 1; let h = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; let cap_zero = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, 0); let cap_nonzero = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, BOOT_TIME_COMPAT_MAX_SECS); assert!(cap_zero.is_some(), "ignore_time_skew=true must accept valid HMAC"); assert!( cap_nonzero.is_some(), "ignore_time_skew path must not depend on boot-time cap" ); let a = cap_zero.unwrap(); let b = cap_nonzero.unwrap(); assert_eq!(a.user, b.user); assert_eq!(a.timestamp, b.timestamp); } #[test] fn adversarial_small_boot_timestamp_matrix_rejected_when_boot_cap_forced_zero() { let secret = b"boot_cap_zero_matrix_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; for ts in 0u32..1024u32 { let h = make_valid_tls_handshake(secret, ts); let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0); assert!( result.is_none(), "boot cap=0 must reject timestamp {ts} when skew checks are active" ); } } #[test] fn light_fuzz_boot_cap_zero_rejects_small_timestamp_space() { let secret = b"boot_cap_zero_fuzz_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; let mut s: u64 = 0x9E37_79B9_7F4A_7C15; for _ in 0..4096 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let ts = (s as u32) % 2048; let h = make_valid_tls_handshake(secret, ts); let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0); assert!( result.is_none(), "fuzzed boot-range timestamp {ts} must be rejected when cap=0" ); } } #[test] fn stress_boot_cap_zero_rejection_is_deterministic_under_high_iteration_count() { let secret = b"boot_cap_zero_stress_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; for i in 0u32..20_000u32 { let ts = i % 4096; let h = make_valid_tls_handshake(secret, ts); let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0); assert!( result.is_none(), "iteration {i}: timestamp {ts} must be rejected with cap=0" ); } } #[test] fn replay_window_one_allows_only_zero_timestamp_boot_bypass() { let secret = b"replay_window_one_boot_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let ts0 = make_valid_tls_handshake(secret, 0); let ts1 = make_valid_tls_handshake(secret, 1); assert!( validate_tls_handshake_with_replay_window(&ts0, &secrets, false, 1).is_some(), "replay_window=1 must allow timestamp 0 via boot-time compatibility" ); assert!( validate_tls_handshake_with_replay_window(&ts1, &secrets, false, 1).is_none(), "replay_window=1 must reject timestamp 1 on normal wall-clock systems" ); } #[test] fn replay_window_two_allows_ts0_ts1_but_rejects_ts2() { let secret = b"replay_window_two_boot_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let ts0 = make_valid_tls_handshake(secret, 0); let ts1 = make_valid_tls_handshake(secret, 1); let ts2 = make_valid_tls_handshake(secret, 2); assert!(validate_tls_handshake_with_replay_window(&ts0, &secrets, false, 2).is_some()); assert!(validate_tls_handshake_with_replay_window(&ts1, &secrets, false, 2).is_some()); assert!( validate_tls_handshake_with_replay_window(&ts2, &secrets, false, 2).is_none(), "timestamp equal to replay-window cap must not use boot-time bypass" ); } #[test] fn adversarial_skew_boundary_matrix_accepts_only_inclusive_window_when_boot_disabled() { let secret = b"skew_boundary_matrix_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; for offset in -1500i64..=1500i64 { let ts_i64 = now - offset; let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for test matrix"); let h = make_valid_tls_handshake(secret, ts); let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0) .is_some(); let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&offset); assert_eq!( accepted, expected, "offset {offset} must match inclusive skew window when boot bypass is disabled" ); } } #[test] fn light_fuzz_skew_window_rejects_outside_range_when_boot_disabled() { let secret = b"skew_outside_fuzz_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; let mut s: u64 = 0x0123_4567_89AB_CDEF; for _ in 0..4096 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let magnitude = 1300i64 + ((s % 2000u64) as i64); let sign = if (s & 1) == 0 { 1i64 } else { -1i64 }; let offset = sign * magnitude; let ts_i64 = now - offset; let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for fuzz test"); let h = make_valid_tls_handshake(secret, ts); let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0) .is_some(); assert!( !accepted, "offset {offset} must be rejected outside strict skew window" ); } } #[test] fn stress_boot_disabled_validation_matches_time_diff_oracle() { let secret = b"boot_disabled_oracle_stress_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; let mut s: u64 = 0xBADC_0FFE_EE11_2233; for _ in 0..25_000 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let ts = s as u32; let h = make_valid_tls_handshake(secret, ts); let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0) .is_some(); let time_diff = now - i64::from(ts); let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff); assert_eq!( accepted, expected, "boot-disabled validation must match pure time-diff oracle" ); } } #[test] fn integration_large_user_list_with_boot_disabled_finds_only_matching_user() { let now: i64 = 1_700_000_000; let target_secret = b"target_user_secret"; let target_ts = (now - 1) as u32; let handshake = make_valid_tls_handshake(target_secret, target_ts); let mut secrets = Vec::new(); for i in 0..512u32 { secrets.push((format!("noise-{i}"), format!("noise-secret-{i}").into_bytes())); } secrets.push(("target-user".to_string(), target_secret.to_vec())); let result = validate_tls_handshake_at_time_with_boot_cap(&handshake, &secrets, false, now, 0) .expect("matching user should validate within strict skew window"); assert_eq!(result.user, "target-user"); } #[test] fn light_fuzz_ignore_time_skew_accepts_wide_timestamp_range_with_valid_hmac() { let secret = b"ignore_skew_fuzz_accept_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let mut s: u64 = 0xC0FF_EE11_2233_4455; for _ in 0..2048 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let ts = s as u32; let h = make_valid_tls_handshake(secret, ts); let result = validate_tls_handshake_with_replay_window(&h, &secrets, true, 60); assert!( result.is_some(), "ignore_time_skew=true must accept valid HMAC for arbitrary timestamp" ); } } #[test] fn light_fuzz_small_replay_window_rejects_far_timestamps_when_skew_enabled() { let secret = b"replay_window_reject_fuzz_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; for ts in 300u32..=1323u32 { let h = make_valid_tls_handshake(secret, ts); let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, 0, 300); assert!( result.is_none(), "with skew checks enabled and boot cap=300, timestamp >=300 at now=0 must be rejected" ); } } // ------------------------------------------------------------------ // Extreme timestamp values // ------------------------------------------------------------------ /// u32::MAX is a valid timestamp value. When ignore_time_skew=true the HMAC /// is the only gate, and a correctly constructed handshake must be accepted. #[test] fn u32_max_timestamp_accepted_with_ignore_time_skew() { let secret = b"u32_max_ts_accept_test"; let h = make_valid_tls_handshake(secret, u32::MAX); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&h, &secrets, true); assert!(result.is_some(), "u32::MAX timestamp must be accepted with ignore_time_skew=true"); assert_eq!( result.unwrap().timestamp, u32::MAX, "timestamp field must equal u32::MAX verbatim" ); } /// u32::MAX > BOOT_TIME_MAX_SECS so the skew check runs. With any realistic `now` /// (~1.7 billion), time_diff = now - u32::MAX is deeply negative — far outside /// [-1200, 600] — so the handshake must be rejected without overflow. #[test] fn u32_max_timestamp_rejected_by_skew_enforcement() { let secret = b"u32_max_ts_reject_test"; let h = make_valid_tls_handshake(secret, u32::MAX); let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; assert!( validate_tls_handshake_at_time(&h, &secrets, false, now).is_none(), "u32::MAX timestamp must be rejected by skew check with realistic now" ); } // ------------------------------------------------------------------ // Validation result field correctness // ------------------------------------------------------------------ /// result.digest must be the verbatim bytes stored in the handshake buffer, /// not the freshly recomputed HMAC. Callers use this field directly when /// constructing the ServerHello response digest. #[test] fn result_digest_field_is_verbatim_stored_digest() { let secret = b"digest_field_verbatim_test"; let ts: u32 = 0xCAFE_BABE; let h = make_valid_tls_handshake(secret, ts); let secrets = vec![("u".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&h, &secrets, true).unwrap(); let stored: [u8; TLS_DIGEST_LEN] = h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN] .try_into() .unwrap(); assert_eq!( result.digest, stored, "result.digest must equal the stored bytes, not the computed HMAC" ); } // ------------------------------------------------------------------ // Secret length edge cases // ------------------------------------------------------------------ /// HMAC-SHA256 pads or hashes keys of any length; a single-byte key must work. #[test] fn single_byte_secret_works() { let secret = b"x"; let h = make_valid_tls_handshake(secret, 0); let secrets = vec![("u".to_string(), secret.to_vec())]; assert!( validate_tls_handshake(&h, &secrets, true).is_some(), "single-byte secret must produce a valid and verifiable HMAC" ); } /// Keys longer than the HMAC block size (64 bytes for SHA-256) are hashed /// before use. A 256-byte key must work without truncation or panic. #[test] fn very_long_secret_256_bytes_works() { let secret = vec![0xABu8; 256]; let h = make_valid_tls_handshake(&secret, 0); let secrets = vec![("u".to_string(), secret.clone())]; assert!( validate_tls_handshake(&h, &secrets, true).is_some(), "256-byte secret must be accepted without truncation" ); } // ------------------------------------------------------------------ // Determinism — same input must always produce same result // ------------------------------------------------------------------ /// Calling validate twice on the same input must return identical results. /// Non-determinism (e.g. from an accidentally global mutable state or a /// shared nonce) would be a critical security defect in a proxy that rejects /// censors by relying on stable authentication outcomes. #[test] fn validation_is_deterministic() { let secret = b"determinism_test_key"; let h = make_valid_tls_handshake(secret, 42); let secrets = vec![("u".to_string(), secret.to_vec())]; let r1 = validate_tls_handshake(&h, &secrets, true).unwrap(); let r2 = validate_tls_handshake(&h, &secrets, true).unwrap(); assert_eq!(r1.user, r2.user); assert_eq!(r1.session_id, r2.session_id); assert_eq!(r1.digest, r2.digest); assert_eq!(r1.timestamp, r2.timestamp); } // ------------------------------------------------------------------ // Multi-user: scan-all correctness guarantees // ------------------------------------------------------------------ /// The matching logic must scan through the entire secrets list. A user /// at position 99 of 100 must be found; an implementation that stops early /// on the first non-match would fail this test. #[test] fn last_user_in_large_list_is_found() { let target_secret = b"needle_in_haystack"; let h = make_valid_tls_handshake(target_secret, 0); let mut secrets: Vec<(String, Vec)> = (0..99) .map(|i| (format!("decoy_{i}"), format!("wrong_{i}").into_bytes())) .collect(); secrets.push(("needle".to_string(), target_secret.to_vec())); let result = validate_tls_handshake(&h, &secrets, true); assert!(result.is_some(), "100th user must be found"); assert_eq!(result.unwrap().user, "needle"); } /// When multiple users share the same secret the first occurrence must always /// win. The scan-all loop must not replace first_match with a later one. #[test] fn first_matching_user_wins_over_later_duplicate_secret() { let shared = b"duplicated_secret_key"; let h = make_valid_tls_handshake(shared, 0); let secrets = vec![ ("decoy_1".to_string(), b"wrong_1".to_vec()), ("winner".to_string(), shared.to_vec()), // first match ("decoy_2".to_string(), b"wrong_2".to_vec()), ("loser".to_string(), shared.to_vec()), // second match — must not win ("decoy_3".to_string(), b"wrong_3".to_vec()), ]; let result = validate_tls_handshake(&h, &secrets, true); assert!(result.is_some()); assert_eq!( result.unwrap().user, "winner", "first matching user must be returned even when a later entry also matches" ); } // ------------------------------------------------------------------ // Legacy tls.rs tests moved here // ------------------------------------------------------------------ #[test] fn test_is_tls_handshake() { assert!(is_tls_handshake(&[0x16, 0x03, 0x01])); assert!(is_tls_handshake(&[0x16, 0x03, 0x03])); assert!(is_tls_handshake(&[0x16, 0x03, 0x01, 0x02, 0x00])); assert!(is_tls_handshake(&[0x16, 0x03, 0x03, 0x02, 0x00])); assert!(!is_tls_handshake(&[0x17, 0x03, 0x01])); assert!(!is_tls_handshake(&[0x16, 0x03, 0x02])); assert!(!is_tls_handshake(&[0x16, 0x03])); } #[test] fn test_parse_tls_record_header() { let header = [0x16, 0x03, 0x01, 0x02, 0x00]; let result = parse_tls_record_header(&header).unwrap(); assert_eq!(result.0, TLS_RECORD_HANDSHAKE); assert_eq!(result.1, 512); let header = [0x17, 0x03, 0x03, 0x40, 0x00]; let result = parse_tls_record_header(&header).unwrap(); assert_eq!(result.0, TLS_RECORD_APPLICATION); assert_eq!(result.1, 16384); } #[test] fn parse_tls_record_header_rejects_invalid_versions() { let invalid = [ [0x16, 0x03, 0x00, 0x00, 0x10], [0x16, 0x02, 0x00, 0x00, 0x10], [0x16, 0x03, 0x02, 0x00, 0x10], [0x16, 0x04, 0x00, 0x00, 0x10], ]; for header in invalid { assert!( parse_tls_record_header(&header).is_none(), "invalid TLS record version {:?} must be rejected", [header[1], header[2]] ); } } #[test] fn test_gen_fake_x25519_key() { let rng = crate::crypto::SecureRandom::new(); let key1 = gen_fake_x25519_key(&rng); let key2 = gen_fake_x25519_key(&rng); assert_eq!(key1.len(), 32); assert_eq!(key2.len(), 32); assert_ne!(key1, key2); } #[test] fn test_fake_x25519_key_is_nonzero_and_varies() { let rng = crate::crypto::SecureRandom::new(); let mut unique = std::collections::HashSet::new(); let mut saw_non_zero = false; for _ in 0..64 { let key = gen_fake_x25519_key(&rng); if key != [0u8; 32] { saw_non_zero = true; } unique.insert(key); } assert!( saw_non_zero, "generated X25519 public keys must not collapse to all-zero output" ); assert!( unique.len() > 1, "generated X25519 public keys must vary across invocations" ); } #[test] fn validate_tls_handshake_rejects_session_id_longer_than_rfc_cap() { let secret = b"session_id_cap_secret"; let oversized_sid = vec![0x42u8; 33]; let handshake = make_valid_tls_handshake_with_session_id(secret, 0, &oversized_sid); let secrets = vec![("u".to_string(), secret.to_vec())]; assert!( validate_tls_handshake(&handshake, &secrets, true).is_none(), "legacy_session_id length > 32 must be rejected" ); } fn server_hello_extension_types(record: &[u8]) -> Vec { if record.len() < 9 || record[0] != TLS_RECORD_HANDSHAKE || record[5] != 0x02 { return Vec::new(); } let record_len = u16::from_be_bytes([record[3], record[4]]) as usize; if record.len() < 5 + record_len { return Vec::new(); } let hs_len = u32::from_be_bytes([0, record[6], record[7], record[8]]) as usize; let hs_start = 5; let hs_end = hs_start + 4 + hs_len; if hs_end > record.len() { return Vec::new(); } let mut pos = hs_start + 4 + 2 + 32; if pos >= hs_end { return Vec::new(); } let sid_len = record[pos] as usize; pos += 1 + sid_len; if pos + 2 + 1 + 2 > hs_end { return Vec::new(); } pos += 2 + 1; let ext_len = u16::from_be_bytes([record[pos], record[pos + 1]]) as usize; pos += 2; let ext_end = pos + ext_len; if ext_end > hs_end { return Vec::new(); } let mut out = Vec::new(); while pos + 4 <= ext_end { let etype = u16::from_be_bytes([record[pos], record[pos + 1]]); let elen = u16::from_be_bytes([record[pos + 2], record[pos + 3]]) as usize; pos += 4; if pos + elen > ext_end { break; } out.push(etype); pos += elen; } out } #[test] fn build_server_hello_never_places_alpn_in_server_hello_extensions() { let secret = b"alpn_sh_forbidden"; let client_digest = [0x11u8; 32]; let session_id = vec![0xAA; 32]; let rng = crate::crypto::SecureRandom::new(); let response = build_server_hello( secret, &client_digest, &session_id, 1024, &rng, Some(b"h2".to_vec()), 0, ); let exts = server_hello_extension_types(&response); assert!( !exts.contains(&0x0010), "ALPN extension must not appear in ServerHello" ); } #[test] fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() { let secret = b"alpn_emulated_forbidden"; let client_digest = [0x22u8; 32]; let session_id = vec![0xAB; 32]; let rng = crate::crypto::SecureRandom::new(); let cached = CachedTlsData { server_hello_template: ParsedServerHello { version: TLS_VERSION, random: [0u8; 32], session_id: Vec::new(), cipher_suite: [0x13, 0x01], compression: 0, extensions: Vec::new(), }, cert_info: None, cert_payload: None, app_data_records_sizes: vec![1024], total_app_data_len: 1024, behavior_profile: TlsBehaviorProfile { change_cipher_spec_count: 1, app_data_record_sizes: vec![1024], ticket_record_sizes: Vec::new(), source: TlsProfileSource::Default, }, fetched_at: SystemTime::now(), domain: "example.com".to_string(), }; let response = build_emulated_server_hello( secret, &client_digest, &session_id, &cached, false, &rng, Some(b"h2".to_vec()), 0, ); let exts = server_hello_extension_types(&response); assert!( !exts.contains(&0x0010), "ALPN extension must not appear in emulated ServerHello" ); } #[test] fn test_tls_extension_builder() { let key = [0x42u8; 32]; let mut builder = TlsExtensionBuilder::new(); builder.add_key_share(&key); builder.add_supported_versions(0x0304); let result = builder.build(); let len = u16::from_be_bytes([result[0], result[1]]) as usize; assert_eq!(len, result.len() - 2); assert!(result.len() > 40); } #[test] fn test_server_hello_builder() { let session_id = vec![0x01, 0x02, 0x03, 0x04]; let key = [0x55u8; 32]; let builder = ServerHelloBuilder::new(session_id.clone()) .with_x25519_key(&key) .with_tls13_version(); let record = builder.build_record(); validate_server_hello_structure(&record).expect("Invalid ServerHello structure"); assert_eq!(record[0], TLS_RECORD_HANDSHAKE); assert_eq!(&record[1..3], &TLS_VERSION); assert_eq!(record[5], 0x02); } #[test] fn test_build_server_hello_structure() { let secret = b"test secret"; let client_digest = [0x42u8; 32]; let session_id = vec![0xAA; 32]; let rng = crate::crypto::SecureRandom::new(); let response = build_server_hello(secret, &client_digest, &session_id, 2048, &rng, None, 0); assert!(response.len() > 100); assert_eq!(response[0], TLS_RECORD_HANDSHAKE); validate_server_hello_structure(&response).expect("Invalid ServerHello"); let server_hello_len = 5 + u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_start = server_hello_len; assert!(response.len() > ccs_start + 6); assert_eq!(response[ccs_start], TLS_RECORD_CHANGE_CIPHER); let ccs_len = 5 + u16::from_be_bytes([response[ccs_start + 3], response[ccs_start + 4]]) as usize; let app_start = ccs_start + ccs_len; assert!(response.len() > app_start + 5); assert_eq!(response[app_start], TLS_RECORD_APPLICATION); } #[test] fn test_build_server_hello_digest() { let secret = b"test secret key here"; let client_digest = [0x42u8; 32]; let session_id = vec![0xAA; 32]; let rng = crate::crypto::SecureRandom::new(); let response1 = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 0); let response2 = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 0); let digest1 = &response1[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]; assert!(!digest1.iter().all(|&b| b == 0)); let digest2 = &response2[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]; assert_ne!(digest1, digest2); } #[test] fn test_server_hello_extensions_length() { let session_id = vec![0x01; 32]; let key = [0x55u8; 32]; let builder = ServerHelloBuilder::new(session_id) .with_x25519_key(&key) .with_tls13_version(); let record = builder.build_record(); let msg_start = 5; let msg_len = u32::from_be_bytes([0, record[6], record[7], record[8]]) as usize; let session_id_pos = msg_start + 4 + 2 + 32; let session_id_len = record[session_id_pos] as usize; let ext_len_pos = session_id_pos + 1 + session_id_len + 2 + 1; let ext_len = u16::from_be_bytes([record[ext_len_pos], record[ext_len_pos + 1]]) as usize; let extensions_data = &record[ext_len_pos + 2..msg_start + 4 + msg_len]; assert_eq!( ext_len, extensions_data.len(), "Extension length mismatch: declared {}, actual {}", ext_len, extensions_data.len() ); } #[test] fn test_validate_tls_handshake_format() { let mut handshake = vec![0u8; 100]; handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&[0x42; 32]); handshake[TLS_DIGEST_POS + TLS_DIGEST_LEN] = 32; let secrets = vec![("test".to_string(), b"secret".to_vec())]; let result = validate_tls_handshake(&handshake, &secrets, true); assert!(result.is_none()); } fn build_client_hello_with_exts(exts: Vec<(u16, Vec)>, host: &str) -> Vec { let mut body = Vec::new(); body.extend_from_slice(&TLS_VERSION); body.extend_from_slice(&[0u8; 32]); body.push(0); body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&[0x13, 0x01]); body.push(1); body.push(0); let host_bytes = host.as_bytes(); let mut sni_ext = Vec::new(); sni_ext.extend_from_slice(&(host_bytes.len() as u16 + 3).to_be_bytes()); sni_ext.push(0); sni_ext.extend_from_slice(&(host_bytes.len() as u16).to_be_bytes()); sni_ext.extend_from_slice(host_bytes); let mut ext_blob = Vec::new(); for (typ, data) in exts { ext_blob.extend_from_slice(&typ.to_be_bytes()); ext_blob.extend_from_slice(&(data.len() as u16).to_be_bytes()); ext_blob.extend_from_slice(&data); } ext_blob.extend_from_slice(&0x0000u16.to_be_bytes()); ext_blob.extend_from_slice(&(sni_ext.len() as u16).to_be_bytes()); ext_blob.extend_from_slice(&sni_ext); body.extend_from_slice(&(ext_blob.len() as u16).to_be_bytes()); body.extend_from_slice(&ext_blob); let mut handshake = Vec::new(); handshake.push(0x01); let len_bytes = (body.len() as u32).to_be_bytes(); handshake.extend_from_slice(&len_bytes[1..4]); handshake.extend_from_slice(&body); let mut record = Vec::new(); record.push(TLS_RECORD_HANDSHAKE); record.extend_from_slice(&[0x03, 0x01]); record.extend_from_slice(&(handshake.len() as u16).to_be_bytes()); record.extend_from_slice(&handshake); record } fn build_client_hello_with_raw_extensions(ext_blob: &[u8]) -> Vec { let mut body = Vec::new(); body.extend_from_slice(&TLS_VERSION); body.extend_from_slice(&[0u8; 32]); body.push(0); body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&[0x13, 0x01]); body.push(1); body.push(0); body.extend_from_slice(&(ext_blob.len() as u16).to_be_bytes()); body.extend_from_slice(ext_blob); let mut handshake = Vec::new(); handshake.push(0x01); let len_bytes = (body.len() as u32).to_be_bytes(); handshake.extend_from_slice(&len_bytes[1..4]); handshake.extend_from_slice(&body); let mut record = Vec::new(); record.push(TLS_RECORD_HANDSHAKE); record.extend_from_slice(&[0x03, 0x01]); record.extend_from_slice(&(handshake.len() as u16).to_be_bytes()); record.extend_from_slice(&handshake); record } #[test] fn test_extract_sni_with_grease_extension() { let ch = build_client_hello_with_exts(vec![(0x0a0a, Vec::new())], "example.com"); let sni = extract_sni_from_client_hello(&ch); assert_eq!(sni.as_deref(), Some("example.com")); } #[test] fn test_extract_sni_tolerates_empty_unknown_extension() { let ch = build_client_hello_with_exts(vec![(0x1234, Vec::new())], "test.local"); let sni = extract_sni_from_client_hello(&ch); assert_eq!(sni.as_deref(), Some("test.local")); } #[test] fn test_extract_alpn_single() { let mut alpn_data = Vec::new(); alpn_data.extend_from_slice(&3u16.to_be_bytes()); alpn_data.push(2); alpn_data.extend_from_slice(b"h2"); let ch = build_client_hello_with_exts(vec![(0x0010, alpn_data)], "alpn.test"); let alpn = extract_alpn_from_client_hello(&ch); let alpn_str: Vec = alpn .iter() .map(|p| std::str::from_utf8(p).unwrap().to_string()) .collect(); assert_eq!(alpn_str, vec!["h2"]); } #[test] fn test_extract_alpn_multiple() { let mut alpn_data = Vec::new(); alpn_data.extend_from_slice(&11u16.to_be_bytes()); alpn_data.push(2); alpn_data.extend_from_slice(b"h2"); alpn_data.push(4); alpn_data.extend_from_slice(b"spdy"); alpn_data.push(2); alpn_data.extend_from_slice(b"h3"); let ch = build_client_hello_with_exts(vec![(0x0010, alpn_data)], "alpn.test"); let alpn = extract_alpn_from_client_hello(&ch); let alpn_str: Vec = alpn .iter() .map(|p| std::str::from_utf8(p).unwrap().to_string()) .collect(); assert_eq!(alpn_str, vec!["h2", "spdy", "h3"]); } #[test] fn extract_sni_rejects_zero_length_host_name() { let mut sni_ext = Vec::new(); sni_ext.extend_from_slice(&3u16.to_be_bytes()); sni_ext.push(0); sni_ext.extend_from_slice(&0u16.to_be_bytes()); let mut ext_blob = Vec::new(); ext_blob.extend_from_slice(&0x0000u16.to_be_bytes()); ext_blob.extend_from_slice(&(sni_ext.len() as u16).to_be_bytes()); ext_blob.extend_from_slice(&sni_ext); let ch = build_client_hello_with_raw_extensions(&ext_blob); assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_sni_rejects_raw_ipv4_literals() { let ch = build_client_hello_with_exts(Vec::new(), "203.0.113.10"); assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_sni_rejects_invalid_label_characters() { let ch = build_client_hello_with_exts(Vec::new(), "exa_mple.com"); assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_sni_rejects_oversized_label() { let oversized = format!("{}.example.com", "a".repeat(64)); let ch = build_client_hello_with_exts(Vec::new(), &oversized); assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_sni_rejects_when_extension_block_is_truncated() { let mut ext_blob = Vec::new(); ext_blob.extend_from_slice(&0x0000u16.to_be_bytes()); ext_blob.extend_from_slice(&5u16.to_be_bytes()); ext_blob.extend_from_slice(&[0, 3, 0]); let mut ch = build_client_hello_with_raw_extensions(&ext_blob); ch.pop(); assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_sni_rejects_session_id_len_overflow() { let mut ch = build_client_hello_with_exts(Vec::new(), "example.com"); let sid_len_pos = 5 + 4 + 2 + 32; ch[sid_len_pos] = 255; assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_sni_rejects_cipher_suites_len_overflow() { let mut ch = build_client_hello_with_exts(Vec::new(), "example.com"); let sid_len_pos = 5 + 4 + 2 + 32; let cipher_len_pos = sid_len_pos + 1 + ch[sid_len_pos] as usize; ch[cipher_len_pos] = 0xFF; ch[cipher_len_pos + 1] = 0xFF; assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_sni_rejects_compression_methods_len_overflow() { let mut ch = build_client_hello_with_exts(Vec::new(), "example.com"); let sid_len_pos = 5 + 4 + 2 + 32; let cipher_len_pos = sid_len_pos + 1 + ch[sid_len_pos] as usize; let cipher_len = u16::from_be_bytes([ch[cipher_len_pos], ch[cipher_len_pos + 1]]) as usize; let comp_len_pos = cipher_len_pos + 2 + cipher_len; ch[comp_len_pos] = 0xFF; assert!(extract_sni_from_client_hello(&ch).is_none()); } #[test] fn extract_alpn_returns_empty_on_session_id_len_overflow() { let mut alpn_data = Vec::new(); alpn_data.extend_from_slice(&3u16.to_be_bytes()); alpn_data.push(2); alpn_data.extend_from_slice(b"h2"); let mut ch = build_client_hello_with_exts(vec![(0x0010, alpn_data)], "alpn.test"); let sid_len_pos = 5 + 4 + 2 + 32; ch[sid_len_pos] = 255; assert!(extract_alpn_from_client_hello(&ch).is_empty()); } #[test] fn extract_alpn_rejects_when_extension_block_is_truncated() { let mut ext_blob = Vec::new(); ext_blob.extend_from_slice(&0x0010u16.to_be_bytes()); ext_blob.extend_from_slice(&5u16.to_be_bytes()); ext_blob.extend_from_slice(&[0, 3, 2, b'h']); let ch = build_client_hello_with_raw_extensions(&ext_blob); assert!(extract_alpn_from_client_hello(&ch).is_empty()); } #[test] fn extract_alpn_rejects_nested_length_overflow() { let mut alpn_data = Vec::new(); alpn_data.extend_from_slice(&10u16.to_be_bytes()); alpn_data.push(8); alpn_data.extend_from_slice(b"h2"); let mut ext_blob = Vec::new(); ext_blob.extend_from_slice(&0x0010u16.to_be_bytes()); ext_blob.extend_from_slice(&(alpn_data.len() as u16).to_be_bytes()); ext_blob.extend_from_slice(&alpn_data); let ch = build_client_hello_with_raw_extensions(&ext_blob); assert!(extract_alpn_from_client_hello(&ch).is_empty()); } // ------------------------------------------------------------------ // Additional adversarial checks // ------------------------------------------------------------------ #[test] fn empty_secret_hmac_is_supported() { let secret: &[u8] = b""; let handshake = make_valid_tls_handshake(secret, 0); let secrets = vec![("empty".to_string(), secret.to_vec())]; let result = validate_tls_handshake(&handshake, &secrets, true); assert!(result.is_some(), "Empty HMAC key must not panic and must validate when correct"); } #[test] fn server_hello_digest_verifies_against_full_response() { let secret = b"fronting_digest_verify_key"; let client_digest = [0x42u8; TLS_DIGEST_LEN]; let session_id = vec![0xAA; 32]; let rng = crate::crypto::SecureRandom::new(); let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 1); let mut zeroed = response.clone(); zeroed[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].fill(0); let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + zeroed.len()); hmac_input.extend_from_slice(&client_digest); hmac_input.extend_from_slice(&zeroed); let expected = sha256_hmac(secret, &hmac_input); assert_eq!( &response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN], &expected, "ServerHello digest must be verifiable by a client that recomputes HMAC over full response" ); } #[test] fn server_hello_digest_fails_after_single_byte_tamper() { let secret = b"fronting_tamper_detect_key"; let client_digest = [0x24u8; TLS_DIGEST_LEN]; let session_id = vec![0xBB; 32]; let rng = crate::crypto::SecureRandom::new(); let mut response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 0); response[TLS_DIGEST_POS + TLS_DIGEST_LEN + 1] ^= 0x01; let mut zeroed = response.clone(); zeroed[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].fill(0); let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + zeroed.len()); hmac_input.extend_from_slice(&client_digest); hmac_input.extend_from_slice(&zeroed); let expected = sha256_hmac(secret, &hmac_input); assert_ne!( &response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN], &expected, "Tampering any response byte must invalidate the embedded digest" ); } #[test] fn server_hello_application_data_payload_varies_across_runs() { use std::collections::HashSet; let secret = b"fronting_payload_variability_key"; let client_digest = [0x13u8; TLS_DIGEST_LEN]; let session_id = vec![0x44; 32]; let rng = crate::crypto::SecureRandom::new(); let mut unique_payloads: HashSet> = HashSet::new(); for _ in 0..16 { let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 0); let sh_len = u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_pos = 5 + sh_len; let ccs_len = u16::from_be_bytes([response[ccs_pos + 3], response[ccs_pos + 4]]) as usize; let app_pos = ccs_pos + 5 + ccs_len; assert_eq!(response[app_pos], TLS_RECORD_APPLICATION); let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize; let payload = response[app_pos + 5..app_pos + 5 + app_len].to_vec(); assert!(payload.iter().any(|&b| b != 0), "Payload must not be all-zero deterministic filler"); unique_payloads.insert(payload); } assert!( unique_payloads.len() >= 4, "ApplicationData payload should vary across runs to reduce fingerprintability" ); } #[test] fn replay_window_zero_disables_boot_bypass_for_any_nonzero_timestamp() { let secret = b"window_zero_boot_bypass_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let ts1 = make_valid_tls_handshake(secret, 1); assert!( validate_tls_handshake_with_replay_window(&ts1, &secrets, false, 0).is_none(), "replay_window_secs=0 must reject nonzero timestamps even in boot-time range" ); let ts0 = make_valid_tls_handshake(secret, 0); assert!( validate_tls_handshake_with_replay_window(&ts0, &secrets, false, 0).is_none(), "replay_window_secs=0 enforces strict skew check and rejects timestamp=0 on normal wall-clock systems" ); } #[test] fn large_replay_window_does_not_expand_time_skew_acceptance() { let secret = b"large_replay_window_skew_bound_test"; let secrets = vec![("u".to_string(), secret.to_vec())]; let now: i64 = 1_700_000_000; let ts_far_past = (now - 600) as u32; let valid = make_valid_tls_handshake(secret, ts_far_past); assert!( validate_tls_handshake_with_replay_window(&valid, &secrets, false, 86_400).is_none(), "large replay window must not relax strict skew check once boot-time bypass is not in play" ); } #[test] fn parse_tls_record_header_accepts_tls_version_constant() { let header = [TLS_RECORD_HANDSHAKE, TLS_VERSION[0], TLS_VERSION[1], 0x00, 0x2A]; let parsed = parse_tls_record_header(&header).expect("TLS_VERSION header should be accepted"); assert_eq!(parsed.0, TLS_RECORD_HANDSHAKE); assert_eq!(parsed.1, 42); } #[test] fn server_hello_clamps_fake_cert_len_lower_bound() { let secret = b"fake_cert_lower_bound_test"; let client_digest = [0x11u8; TLS_DIGEST_LEN]; let session_id = vec![0x77; 32]; let rng = crate::crypto::SecureRandom::new(); let response = build_server_hello(secret, &client_digest, &session_id, 1, &rng, None, 0); let sh_len = u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_pos = 5 + sh_len; let ccs_len = u16::from_be_bytes([response[ccs_pos + 3], response[ccs_pos + 4]]) as usize; let app_pos = ccs_pos + 5 + ccs_len; let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize; assert_eq!(response[app_pos], TLS_RECORD_APPLICATION); assert_eq!(app_len, 64, "fake cert payload must be clamped to minimum 64 bytes"); } #[test] fn server_hello_clamps_fake_cert_len_upper_bound() { let secret = b"fake_cert_upper_bound_test"; let client_digest = [0x22u8; TLS_DIGEST_LEN]; let session_id = vec![0x66; 32]; let rng = crate::crypto::SecureRandom::new(); let response = build_server_hello(secret, &client_digest, &session_id, 65_535, &rng, None, 0); let sh_len = u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_pos = 5 + sh_len; let ccs_len = u16::from_be_bytes([response[ccs_pos + 3], response[ccs_pos + 4]]) as usize; let app_pos = ccs_pos + 5 + ccs_len; let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize; assert_eq!(response[app_pos], TLS_RECORD_APPLICATION); assert_eq!(app_len, 16_640, "fake cert payload must be clamped to TLS record max bound"); } #[test] fn server_hello_new_session_ticket_count_matches_configuration() { let secret = b"ticket_count_surface_test"; let client_digest = [0x33u8; TLS_DIGEST_LEN]; let session_id = vec![0x55; 32]; let rng = crate::crypto::SecureRandom::new(); let tickets: u8 = 3; let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, tickets); let mut pos = 0usize; let mut app_records = 0usize; while pos + 5 <= response.len() { let rtype = response[pos]; let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize; let next = pos + 5 + rlen; assert!(next <= response.len(), "TLS record must stay inside response bounds"); if rtype == TLS_RECORD_APPLICATION { app_records += 1; } pos = next; } assert_eq!( app_records, 1 + tickets as usize, "response must contain one main application record plus configured ticket-like tail records" ); } #[test] fn server_hello_new_session_ticket_count_is_safely_capped() { let secret = b"ticket_count_cap_test"; let client_digest = [0x44u8; TLS_DIGEST_LEN]; let session_id = vec![0x54; 32]; let rng = crate::crypto::SecureRandom::new(); let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, u8::MAX); let mut pos = 0usize; let mut app_records = 0usize; while pos + 5 <= response.len() { let rtype = response[pos]; let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize; let next = pos + 5 + rlen; assert!(next <= response.len(), "TLS record must stay inside response bounds"); if rtype == TLS_RECORD_APPLICATION { app_records += 1; } pos = next; } assert_eq!( app_records, 5, "response must cap ticket-like tail records to four plus one main application record" ); } #[test] fn boot_time_handshake_replay_remains_blocked_after_cache_window_expires() { let secret = b"gap_t01_boot_replay"; let secrets = vec![("user".to_string(), secret.to_vec())]; let handshake = make_valid_tls_handshake(secret, 1); let validation = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2) .expect("boot-time handshake must validate on first use"); let checker = crate::stats::ReplayChecker::new(128, std::time::Duration::from_millis(40)); let digest_half = &validation.digest[..TLS_DIGEST_HALF_LEN]; assert!( !checker.check_and_add_tls_digest(digest_half), "first use must not be treated as replay" ); assert!( checker.check_and_add_tls_digest(digest_half), "immediate second use must be detected as replay" ); std::thread::sleep(std::time::Duration::from_millis(70)); let validation_after_expiry = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2) .expect("boot-time handshake must still cryptographically validate after cache expiry"); let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN]; assert_eq!(digest_half, digest_half_after_expiry, "replay key must be stable for same handshake"); assert!( checker.check_and_add_tls_digest(digest_half_after_expiry), "after cache window expiry, the same boot-time handshake must still be treated as replay" ); } #[test] fn adversarial_boot_time_handshake_should_not_be_replayable_after_cache_expiry() { let secret = b"gap_t01_boot_replay_adversarial"; let secrets = vec![("user".to_string(), secret.to_vec())]; let handshake = make_valid_tls_handshake(secret, 1); let validation = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2) .expect("boot-time handshake must validate on first use"); let checker = crate::stats::ReplayChecker::new(128, std::time::Duration::from_millis(40)); let digest_half = &validation.digest[..TLS_DIGEST_HALF_LEN]; assert!( !checker.check_and_add_tls_digest(digest_half), "first use must not be treated as replay" ); assert!( checker.check_and_add_tls_digest(digest_half), "immediate reuse must be rejected as replay" ); std::thread::sleep(std::time::Duration::from_millis(70)); let validation_after_expiry = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2) .expect("boot-time handshake still validates cryptographically after cache expiry"); let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN]; assert_eq!( digest_half, digest_half_after_expiry, "replay key must remain stable for the same captured handshake" ); assert!( checker.check_and_add_tls_digest(digest_half_after_expiry), "security expectation: a boot-time handshake should remain replay-protected even after cache expiry" ); } #[test] fn stress_short_replay_window_boot_timestamp_replay_cycles_remain_fail_closed_in_window() { let secret = b"gap_t01_boot_replay_stress"; let secrets = vec![("user".to_string(), secret.to_vec())]; let handshake = make_valid_tls_handshake(secret, 1); let checker = crate::stats::ReplayChecker::new(256, std::time::Duration::from_millis(25)); for cycle in 0..64 { let validation = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2) .expect("boot-time handshake must validate"); let digest_half = &validation.digest[..TLS_DIGEST_HALF_LEN]; if cycle == 0 { assert!( !checker.check_and_add_tls_digest(digest_half), "cycle 0: first use must be fresh" ); assert!( checker.check_and_add_tls_digest(digest_half), "cycle 0: second use must be replay" ); } else { assert!( checker.check_and_add_tls_digest(digest_half), "cycle {cycle}: digest must remain replay-protected across short-window churn" ); } std::thread::sleep(std::time::Duration::from_millis(30)); } } #[test] fn light_fuzz_boot_time_timestamp_matrix_with_short_replay_window_obeys_boot_cap() { let secret = b"gap_t01_boot_replay_fuzz"; let secrets = vec![("user".to_string(), secret.to_vec())]; let mut s: u64 = 0xA1B2_C3D4_55AA_7733; for _ in 0..2048 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let ts = (s as u32) % 8; let handshake = make_valid_tls_handshake(secret, ts); let accepted = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2) .is_some(); if ts < 2 { assert!(accepted, "timestamp {ts} must remain boot-time compatible under 2s cap"); } else { assert!( !accepted, "timestamp {ts} must be rejected when outside replay-window boot cap" ); } } } #[test] fn server_hello_application_data_contains_alpn_marker_when_selected() { let secret = b"alpn_marker_test"; let client_digest = [0x55u8; TLS_DIGEST_LEN]; let session_id = vec![0xAB; 32]; let rng = crate::crypto::SecureRandom::new(); let response = build_server_hello( secret, &client_digest, &session_id, 512, &rng, Some(b"h2".to_vec()), 0, ); let sh_len = u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_pos = 5 + sh_len; let ccs_len = u16::from_be_bytes([response[ccs_pos + 3], response[ccs_pos + 4]]) as usize; let app_pos = ccs_pos + 5 + ccs_len; let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize; let app_payload = &response[app_pos + 5..app_pos + 5 + app_len]; let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2']; assert!( app_payload.windows(expected.len()).any(|window| window == expected), "first application payload must carry ALPN marker for selected protocol" ); } #[test] fn server_hello_ignores_oversized_alpn_and_still_caps_ticket_tail() { let secret = b"alpn_oversize_ignore_test"; let client_digest = [0x56u8; TLS_DIGEST_LEN]; let session_id = vec![0xCD; 32]; let rng = crate::crypto::SecureRandom::new(); let oversized_alpn = vec![b'x'; u8::MAX as usize + 1]; let response = build_server_hello( secret, &client_digest, &session_id, 512, &rng, Some(oversized_alpn), u8::MAX, ); let mut pos = 0usize; let mut app_records = 0usize; let mut first_app_payload: Option<&[u8]> = None; while pos + 5 <= response.len() { let rtype = response[pos]; let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize; let next = pos + 5 + rlen; assert!(next <= response.len(), "TLS record must stay inside response bounds"); if rtype == TLS_RECORD_APPLICATION { app_records += 1; if first_app_payload.is_none() { first_app_payload = Some(&response[pos + 5..next]); } } pos = next; } let marker = [0x00u8, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, b'x', b'x', b'x', b'x']; assert_eq!( app_records, 5, "oversized ALPN must not change the four-ticket cap on tail records" ); assert!( !first_app_payload .expect("response must contain an application record") .windows(marker.len()) .any(|window| window == marker), "oversized ALPN must be ignored rather than embedded into the first application payload" ); } #[test] fn server_hello_ignores_oversized_alpn_when_marker_would_not_fit() { let secret = b"alpn_too_large_to_fit_test"; let client_digest = [0x57u8; TLS_DIGEST_LEN]; let session_id = vec![0xEF; 32]; let rng = crate::crypto::SecureRandom::new(); let oversized_alpn = vec![0xAB; u8::MAX as usize]; let response = build_server_hello( secret, &client_digest, &session_id, 64, &rng, Some(oversized_alpn), 0, ); let sh_len = u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_pos = 5 + sh_len; let ccs_len = u16::from_be_bytes([response[ccs_pos + 3], response[ccs_pos + 4]]) as usize; let app_pos = ccs_pos + 5 + ccs_len; let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize; let app_payload = &response[app_pos + 5..app_pos + 5 + app_len]; let mut marker_prefix = Vec::new(); marker_prefix.extend_from_slice(&0x0010u16.to_be_bytes()); marker_prefix.extend_from_slice(&0x0102u16.to_be_bytes()); marker_prefix.extend_from_slice(&0x0100u16.to_be_bytes()); marker_prefix.push(0xff); marker_prefix.extend_from_slice(&[0xab; 8]); assert!( !app_payload.starts_with(&marker_prefix), "oversized ALPN must not be partially embedded into the ServerHello application record" ); } #[test] fn server_hello_embeds_full_alpn_marker_when_it_exactly_fits_fake_cert_len() { let secret = b"alpn_exact_fit_test"; let client_digest = [0x58u8; TLS_DIGEST_LEN]; let session_id = vec![0xA5; 32]; let rng = crate::crypto::SecureRandom::new(); let proto = vec![b'z'; 57]; // marker_len = 4 + (2 + (1 + proto_len)) = 7 + proto_len = 64 let response = build_server_hello( secret, &client_digest, &session_id, 64, &rng, Some(proto.clone()), 0, ); let sh_len = u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_pos = 5 + sh_len; let ccs_len = u16::from_be_bytes([response[ccs_pos + 3], response[ccs_pos + 4]]) as usize; let app_pos = ccs_pos + 5 + ccs_len; let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize; let app_payload = &response[app_pos + 5..app_pos + 5 + app_len]; let mut expected_marker = Vec::new(); expected_marker.extend_from_slice(&0x0010u16.to_be_bytes()); expected_marker.extend_from_slice(&0x003Cu16.to_be_bytes()); expected_marker.extend_from_slice(&0x003Au16.to_be_bytes()); expected_marker.push(57u8); expected_marker.extend_from_slice(&proto); assert_eq!(app_payload.len(), expected_marker.len()); assert_eq!(app_payload, expected_marker.as_slice()); } #[test] fn server_hello_does_not_embed_partial_alpn_marker_when_one_byte_short() { let secret = b"alpn_one_byte_short_test"; let client_digest = [0x59u8; TLS_DIGEST_LEN]; let session_id = vec![0xA6; 32]; let rng = crate::crypto::SecureRandom::new(); let proto = vec![0xAB; 58]; // marker_len = 65, fake_cert_len = 64 => marker must be fully skipped. let response = build_server_hello( secret, &client_digest, &session_id, 64, &rng, Some(proto), 0, ); let sh_len = u16::from_be_bytes([response[3], response[4]]) as usize; let ccs_pos = 5 + sh_len; let ccs_len = u16::from_be_bytes([response[ccs_pos + 3], response[ccs_pos + 4]]) as usize; let app_pos = ccs_pos + 5 + ccs_len; let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize; let app_payload = &response[app_pos + 5..app_pos + 5 + app_len]; let mut marker_prefix = Vec::new(); marker_prefix.extend_from_slice(&0x0010u16.to_be_bytes()); marker_prefix.extend_from_slice(&0x003Du16.to_be_bytes()); marker_prefix.extend_from_slice(&0x003Bu16.to_be_bytes()); marker_prefix.push(58u8); marker_prefix.extend_from_slice(&[0xAB; 8]); assert!( !app_payload.starts_with(&marker_prefix), "one-byte-short ALPN marker must be skipped entirely, not partially embedded" ); } #[test] fn exhaustive_tls_minor_version_classification_matches_policy() { for minor in 0u8..=u8::MAX { let first = [TLS_RECORD_HANDSHAKE, 0x03, minor]; let expected = minor == 0x01 || minor == 0x03; assert_eq!( is_tls_handshake(&first), expected, "minor version {minor:#04x} classification mismatch" ); } } #[test] fn light_fuzz_tls_header_classifier_and_parser_policy_consistency() { // Deterministic xorshift state keeps this fuzz test reproducible. let mut s: u64 = 0x9E37_79B9_AA95_5A5D; for _ in 0..10_000 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let header = [ (s & 0xff) as u8, ((s >> 8) & 0xff) as u8, ((s >> 16) & 0xff) as u8, ((s >> 24) & 0xff) as u8, ((s >> 32) & 0xff) as u8, ]; let classified = is_tls_handshake(&header[..3]); let expected_classified = header[0] == TLS_RECORD_HANDSHAKE && header[1] == 0x03 && (header[2] == 0x01 || header[2] == 0x03); assert_eq!( classified, expected_classified, "classifier policy mismatch for header {header:02x?}" ); let parsed = parse_tls_record_header(&header); let expected_parsed = header[1] == 0x03 && (header[2] == 0x01 || header[2] == TLS_VERSION[1]); assert_eq!( parsed.is_some(), expected_parsed, "parser policy mismatch for header {header:02x?}" ); } } #[test] fn stress_random_noise_handshakes_never_authenticate() { let secret = b"stress_noise_secret"; let secrets = vec![("noise-user".to_string(), secret.to_vec())]; // Deterministic xorshift state keeps this stress test reproducible. let mut s: u64 = 0xD1B5_4A32_9C6E_77F1; for _ in 0..5_000 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let len = 1 + ((s as usize) % 196); let mut buf = vec![0u8; len]; for b in &mut buf { s ^= s << 7; s ^= s >> 9; s ^= s << 8; *b = (s & 0xff) as u8; } assert!( validate_tls_handshake(&buf, &secrets, true).is_none(), "random noise must never authenticate" ); } }