mirror of https://github.com/ospab/ostp.git
220 lines
9.8 KiB
Rust
220 lines
9.8 KiB
Rust
#[cfg(test)]
|
|
mod tests {
|
|
use crate::crypto::obfuscation::*;
|
|
|
|
/// Verifies that derive_all_secrets is deterministic — same input always
|
|
/// produces the same output.
|
|
#[test]
|
|
fn test_derive_deterministic() {
|
|
let key = b"test_access_key_12345";
|
|
let s1 = derive_all_secrets(key);
|
|
let s2 = derive_all_secrets(key);
|
|
|
|
assert_eq!(s1.obfuscation_key, s2.obfuscation_key, "obf_key must be deterministic");
|
|
assert_eq!(s1.psk, s2.psk, "psk must be deterministic");
|
|
assert_eq!(s1.handshake_pad_min, s2.handshake_pad_min, "pad_min must be deterministic");
|
|
assert_eq!(s1.handshake_pad_max, s2.handshake_pad_max, "pad_max must be deterministic");
|
|
}
|
|
|
|
/// Verifies that different keys produce different secrets.
|
|
#[test]
|
|
fn test_derive_different_keys() {
|
|
let s1 = derive_all_secrets(b"key_alpha");
|
|
let s2 = derive_all_secrets(b"key_beta");
|
|
|
|
assert_ne!(s1.obfuscation_key, s2.obfuscation_key);
|
|
assert_ne!(s1.psk, s2.psk);
|
|
}
|
|
|
|
/// Verifies that the legacy API matches derive_all_secrets output.
|
|
#[test]
|
|
fn test_legacy_api_consistency() {
|
|
let key = b"consistency_check_key";
|
|
let secrets = derive_all_secrets(key);
|
|
assert_eq!(secrets.obfuscation_key, derive_obfuscation_key(key));
|
|
assert_eq!(secrets.psk, derive_psk(key));
|
|
}
|
|
|
|
/// Verifies handshake padding range is within valid bounds.
|
|
#[test]
|
|
fn test_padding_range_valid() {
|
|
for i in 0..100 {
|
|
let key = format!("test_key_{}", i);
|
|
let s = derive_all_secrets(key.as_bytes());
|
|
assert!(s.handshake_pad_min >= 16, "pad_min must be >= 16, got {}", s.handshake_pad_min);
|
|
assert!(s.handshake_pad_min < 80, "pad_min must be < 80, got {}", s.handshake_pad_min);
|
|
assert!(s.handshake_pad_max > s.handshake_pad_min, "pad_max must be > pad_min");
|
|
assert!(s.handshake_pad_max <= s.handshake_pad_min + 175,
|
|
"pad_max out of range: {} > {} + 175", s.handshake_pad_max, s.handshake_pad_min);
|
|
}
|
|
}
|
|
|
|
/// End-to-end test: obfuscate a handshake packet on the "client" side,
|
|
/// then deobfuscate on the "server" side using the same access key.
|
|
/// This simulates the exact flow that caused "Unauthorized probe" errors.
|
|
#[test]
|
|
fn test_handshake_obfuscation_roundtrip() {
|
|
let access_key = b"my_real_access_key_v2";
|
|
let secrets = derive_all_secrets(access_key);
|
|
|
|
// Simulate client building a handshake packet
|
|
let session_id: u32 = 0xDEADBEEF;
|
|
let fake_noise_payload = [0x42u8; 48]; // Typical Noise_NNpsk0 handshake size
|
|
let noise_len = fake_noise_payload.len() as u16;
|
|
|
|
let mut packet = Vec::new();
|
|
packet.extend_from_slice(&session_id.to_be_bytes()); // [0..4]
|
|
packet.extend_from_slice(&noise_len.to_be_bytes()); // [4..6]
|
|
packet.extend_from_slice(&fake_noise_payload); // [6..54]
|
|
packet.extend_from_slice(&[0xAA; 64]); // padding
|
|
|
|
// Obfuscate (client side)
|
|
obfuscate_packet_inplace(&mut packet, &secrets.obfuscation_key, true);
|
|
|
|
// At this point, bytes [0..6] are masked and should look random
|
|
let masked_sid = u32::from_be_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
|
assert_ne!(masked_sid, session_id, "session_id must be masked on wire");
|
|
|
|
// Deobfuscate (server side) — using same key
|
|
deobfuscate_packet_inplace(&mut packet, &secrets.obfuscation_key, true);
|
|
|
|
// Verify session_id is recovered
|
|
let recovered_sid = u32::from_be_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
|
assert_eq!(recovered_sid, session_id, "session_id must be recovered after deobfuscation");
|
|
|
|
// Verify noise_len is recovered
|
|
let recovered_noise_len = u16::from_be_bytes([packet[4], packet[5]]);
|
|
assert_eq!(recovered_noise_len, noise_len, "noise_len must be recovered");
|
|
|
|
// Verify noise payload is intact
|
|
assert_eq!(&packet[6..6 + noise_len as usize], &fake_noise_payload,
|
|
"noise payload must be intact after round-trip");
|
|
}
|
|
|
|
/// Verifies that deobfuscating with the WRONG key does NOT recover
|
|
/// the session_id — this is what prevents unauthorized probes.
|
|
#[test]
|
|
fn test_wrong_key_produces_garbage() {
|
|
let correct_key = b"correct_key";
|
|
let wrong_key = b"wrong_key";
|
|
|
|
let correct_secrets = derive_all_secrets(correct_key);
|
|
let wrong_secrets = derive_all_secrets(wrong_key);
|
|
|
|
let session_id: u32 = 0x12345678;
|
|
let fake_noise = [0x55u8; 48];
|
|
|
|
let mut packet = Vec::new();
|
|
packet.extend_from_slice(&session_id.to_be_bytes());
|
|
packet.extend_from_slice(&(48u16).to_be_bytes());
|
|
packet.extend_from_slice(&fake_noise);
|
|
packet.extend_from_slice(&[0x00; 32]);
|
|
|
|
// Obfuscate with correct key
|
|
obfuscate_packet_inplace(&mut packet, &correct_secrets.obfuscation_key, true);
|
|
|
|
// Try to deobfuscate with WRONG key
|
|
let mut wrong_trial = packet.clone();
|
|
deobfuscate_packet_inplace(&mut wrong_trial, &wrong_secrets.obfuscation_key, true);
|
|
let wrong_sid = u32::from_be_bytes([wrong_trial[0], wrong_trial[1], wrong_trial[2], wrong_trial[3]]);
|
|
|
|
// Should NOT match — this is what the dispatcher checks
|
|
assert_ne!(wrong_sid, session_id, "wrong key must NOT recover session_id");
|
|
|
|
// Deobfuscate with correct key — must work
|
|
deobfuscate_packet_inplace(&mut packet, &correct_secrets.obfuscation_key, true);
|
|
let correct_sid = u32::from_be_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
|
assert_eq!(correct_sid, session_id, "correct key must recover session_id");
|
|
}
|
|
|
|
/// §C version gate: a peer on a different PROTOCOL_VERSION derives
|
|
/// different secrets, so a handshake obfuscated with the OLD version's key
|
|
/// does NOT deobfuscate to a valid session_id under the current version.
|
|
/// This is exactly what makes an old (pre-0.4.0) client fail to connect to
|
|
/// a new server — with no plaintext version marker on the wire.
|
|
#[test]
|
|
fn test_protocol_version_gates_old_clients() {
|
|
let key = b"shared_access_key_across_versions";
|
|
let new = derive_all_secrets(key); // == derive_all_secrets_versioned(key, PROTOCOL_VERSION)
|
|
let old = derive_all_secrets_versioned(key, PROTOCOL_VERSION.wrapping_sub(1));
|
|
|
|
// Different protocol version → different derived secrets.
|
|
assert_ne!(new.obfuscation_key, old.obfuscation_key, "version must change obf_key");
|
|
assert_ne!(new.psk, old.psk, "version must change psk");
|
|
|
|
// Concretely: a handshake the old client obfuscated with its key does
|
|
// not recover a valid session_id when the new server deobfuscates it.
|
|
let session_id: u32 = 0x11223344;
|
|
let noise = [0x33u8; 48];
|
|
let mut pkt = Vec::new();
|
|
pkt.extend_from_slice(&session_id.to_be_bytes());
|
|
pkt.extend_from_slice(&(noise.len() as u16).to_be_bytes());
|
|
pkt.extend_from_slice(&noise);
|
|
pkt.extend_from_slice(&[0u8; 32]);
|
|
|
|
obfuscate_packet_inplace(&mut pkt, &old.obfuscation_key, true); // old client
|
|
deobfuscate_packet_inplace(&mut pkt, &new.obfuscation_key, true); // new server
|
|
let recovered = u32::from_be_bytes([pkt[0], pkt[1], pkt[2], pkt[3]]);
|
|
assert_ne!(recovered, session_id, "old-version client must NOT be accepted by new server");
|
|
}
|
|
|
|
/// Verifies data packet obfuscation round-trip (non-handshake path).
|
|
#[test]
|
|
fn test_data_packet_obfuscation_roundtrip() {
|
|
let secrets = derive_all_secrets(b"data_test_key");
|
|
|
|
let session_id: u32 = 0xCAFEBABE;
|
|
let nonce: u64 = 42;
|
|
let ciphertext = [0x77u8; 64];
|
|
|
|
let mut packet = Vec::new();
|
|
packet.extend_from_slice(&session_id.to_be_bytes()); // [0..4]
|
|
packet.extend_from_slice(&nonce.to_be_bytes()); // [4..12]
|
|
packet.extend_from_slice(&ciphertext); // [12..]
|
|
|
|
obfuscate_packet_inplace(&mut packet, &secrets.obfuscation_key, false);
|
|
|
|
// Masked
|
|
let masked_sid = u32::from_be_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
|
assert_ne!(masked_sid, session_id);
|
|
|
|
// Deobfuscate
|
|
deobfuscate_packet_inplace(&mut packet, &secrets.obfuscation_key, false);
|
|
|
|
let recovered_sid = u32::from_be_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
|
let recovered_nonce = u64::from_be_bytes([
|
|
packet[4], packet[5], packet[6], packet[7],
|
|
packet[8], packet[9], packet[10], packet[11],
|
|
]);
|
|
|
|
assert_eq!(recovered_sid, session_id);
|
|
assert_eq!(recovered_nonce, nonce);
|
|
assert_eq!(&packet[12..], &ciphertext);
|
|
}
|
|
|
|
/// The junk marker must: be stable within a window (client and server agree),
|
|
/// rotate across windows (no static on-wire fingerprint), and differ per key
|
|
/// (one user's marker never silently-drops on another user's flow).
|
|
#[test]
|
|
fn test_junk_marker_rotation() {
|
|
let key_a = b"access-key-alpha";
|
|
let key_b = b"access-key-bravo";
|
|
|
|
// Stable within a window.
|
|
assert_eq!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_a, 1000));
|
|
|
|
// Rotates across adjacent windows.
|
|
assert_ne!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_a, 1001));
|
|
assert_ne!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_a, 999));
|
|
|
|
// Distinct per key within the same window.
|
|
assert_ne!(derive_junk_marker(key_a, 1000), derive_junk_marker(key_b, 1000));
|
|
|
|
// A different protocol version yields a different marker (version gate).
|
|
assert_ne!(
|
|
derive_junk_marker_versioned(key_a, 1000, PROTOCOL_VERSION),
|
|
derive_junk_marker_versioned(key_a, 1000, PROTOCOL_VERSION.wrapping_add(1)),
|
|
);
|
|
}
|
|
}
|