fix: handshake padding wire format (breaking fix)

The previous commit added random padding after Noise handshake payloads
but the receiver passed the entire raw buffer (including padding) to
snow::read_handshake(), which cannot handle trailing bytes.

New wire format:
  [session_id:4][noise_len:2][noise_payload:N][random_padding:32-128]

Changes:
- wrap_datagram_handshake: puts noise_len (u16 BE) at bytes [4..6]
  before the Noise payload, followed by 32-128 random padding bytes
- handle_inbound: reads noise_len from [4..6], passes only
  raw_vec[6..6+noise_len] to snow, ignoring trailing padding
- obfuscation: handshake mask extended from 4 to 6 bytes to also
  cover the noise_len field (prevents DPI from seeing constant u16)
- dispatcher: key-trial loop updated to deobfuscate 6-byte header

Both client and server now produce/consume the same padded format.
This commit is contained in:
ospab 2026-05-17 15:16:02 +03:00
parent bb7d471864
commit 8fe0589ea6
3 changed files with 38 additions and 21 deletions

View File

@ -23,14 +23,15 @@ pub fn derive_psk(access_key: &[u8]) -> [u8; 32] {
psk psk
} }
/// Derives a unique 4-byte session_id mask using HMAC-SHA256(key, nonce). /// Derives a 6-byte handshake mask using HMAC-SHA256(key, nonce).
/// Used strictly for handshake phase obfuscation. /// Covers session_id (4 bytes) + noise_len (2 bytes) to prevent
fn derive_session_mask(key: &[u8; 8], nonce: u64) -> [u8; 4] { /// DPI from seeing a constant length field in the handshake header.
fn derive_handshake_mask(key: &[u8; 8], nonce: u64) -> [u8; 6] {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length"); let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(&nonce.to_be_bytes()); mac.update(&nonce.to_be_bytes());
let result = mac.finalize().into_bytes(); let result = mac.finalize().into_bytes();
let mut mask = [0u8; 4]; let mut mask = [0u8; 6];
mask.copy_from_slice(&result[..4]); mask.copy_from_slice(&result[..6]);
mask mask
} }
@ -61,10 +62,10 @@ pub fn obfuscate_packet_inplace(raw: &mut [u8], key: &[u8; 8], is_handshake: boo
raw[i] ^= mask_result[i]; raw[i] ^= mask_result[i];
} }
} }
} else if raw.len() >= 4 { } else if raw.len() >= 6 {
// Handshake packets: mask session_id with a fixed handshake-phase mask // Handshake: mask session_id (4 bytes) + noise_len (2 bytes)
let mask = derive_session_mask(key, u64::MAX); let mask = derive_handshake_mask(key, u64::MAX);
for i in 0..4 { for i in 0..6 {
raw[i] ^= mask[i]; raw[i] ^= mask[i];
} }
} }
@ -90,8 +91,8 @@ pub fn deobfuscate_header_inplace(
header[i] ^= mask_result[i]; header[i] ^= mask_result[i];
} }
} else { } else {
let mask = derive_session_mask(key, u64::MAX); let mask = derive_handshake_mask(key, u64::MAX);
for i in 0..4 { for i in 0..header.len().min(6) {
header[i] ^= mask[i]; header[i] ^= mask[i];
} }
} }
@ -104,9 +105,9 @@ pub fn deobfuscate_packet_inplace(raw: &mut [u8], key: &[u8; 8], is_handshake: b
header.copy_from_slice(header_slice); header.copy_from_slice(header_slice);
deobfuscate_header_inplace(&mut header, ciphertext, key, is_handshake); deobfuscate_header_inplace(&mut header, ciphertext, key, is_handshake);
header_slice.copy_from_slice(&header); header_slice.copy_from_slice(&header);
} else if raw.len() >= 4 { } else if raw.len() >= 6 {
let mask = derive_session_mask(key, u64::MAX); let mask = derive_handshake_mask(key, u64::MAX);
for i in 0..4 { for i in 0..6 {
raw[i] ^= mask[i]; raw[i] ^= mask[i];
} }
} }

View File

@ -214,8 +214,20 @@ impl ProtocolMachine {
} }
if self.state == OstpState::Handshaking { if self.state == OstpState::Handshaking {
// Wire format: [session_id:4][noise_len:2][noise_payload:N][random_padding:*]
// Extract noise_len to pass exactly the right bytes to snow
if raw_vec.len() < 6 {
return Err(ProtocolError::Framing("handshake too short for length prefix".to_string()));
}
let noise_len = u16::from_be_bytes([raw_vec[4], raw_vec[5]]) as usize;
if raw_vec.len() < 6 + noise_len {
return Err(ProtocolError::Framing(format!(
"handshake truncated: expected {} noise bytes, got {}",
noise_len, raw_vec.len() - 6
)));
}
let mut read_out = vec![0_u8; 1024]; let mut read_out = vec![0_u8; 1024];
let n = self.noise.read_handshake(&raw_vec[4..], &mut read_out)?; let n = self.noise.read_handshake(&raw_vec[6..6 + noise_len], &mut read_out)?;
let response = match self.role { let response = match self.role {
NoiseRole::Responder => { NoiseRole::Responder => {
@ -380,15 +392,17 @@ impl ProtocolMachine {
// Anti-DPI: add random padding after the Noise payload to prevent // Anti-DPI: add random padding after the Noise payload to prevent
// size fingerprinting. Without this, every handshake is exactly 52 bytes // size fingerprinting. Without this, every handshake is exactly 52 bytes
// which is trivially detectable by TSPU/DPI systems. // which is trivially detectable by TSPU/DPI systems.
//
// Wire format: [session_id:4][noise_len:2][noise_payload:N][random_padding:32-128]
let pad_len: usize = rand::thread_rng().gen_range(32..=128); let pad_len: usize = rand::thread_rng().gen_range(32..=128);
let mut pad = vec![0u8; pad_len]; let mut pad = vec![0u8; pad_len];
rand::thread_rng().fill(&mut pad[..]); rand::thread_rng().fill(&mut pad[..]);
let mut out = Vec::with_capacity(4 + noise_payload.len() + 2 + pad_len); let noise_len = noise_payload.len() as u16;
let mut out = Vec::with_capacity(4 + 2 + noise_payload.len() + pad_len);
out.extend_from_slice(&self.session_id.to_be_bytes()); out.extend_from_slice(&self.session_id.to_be_bytes());
out.extend_from_slice(&noise_len.to_be_bytes());
out.extend_from_slice(noise_payload); out.extend_from_slice(noise_payload);
// 2-byte padding length prefix so receiver can strip it
out.extend_from_slice(&(pad_len as u16).to_be_bytes());
out.extend_from_slice(&pad); out.extend_from_slice(&pad);
crate::crypto::obfuscate_packet_inplace(&mut out, &self.obfuscation_key, true); crate::crypto::obfuscate_packet_inplace(&mut out, &self.obfuscation_key, true);
Ok(Bytes::from(out)) Ok(Bytes::from(out))

View File

@ -158,10 +158,12 @@ impl Dispatcher {
let psk = ostp_core::crypto::derive_psk(candidate_key.as_bytes()); let psk = ostp_core::crypto::derive_psk(candidate_key.as_bytes());
// Decode the session_id using this key's obfuscation // Decode the session_id using this key's obfuscation
let mut header = [0u8; 4]; // Handshake header is 6 bytes: [session_id:4][noise_len:2]
header.copy_from_slice(&packet[0..4]); let mut header = [0u8; 6];
if packet.len() < 6 { continue; }
header.copy_from_slice(&packet[0..6]);
ostp_core::crypto::deobfuscate_packet_inplace(&mut header, &obf_key, true); ostp_core::crypto::deobfuscate_packet_inplace(&mut header, &obf_key, true);
let candidate_session_id = u32::from_be_bytes(header); let candidate_session_id = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
let mut cfg = self.machine_config.clone(); let mut cfg = self.machine_config.clone();
cfg.session_id = candidate_session_id; cfg.session_id = candidate_session_id;