mirror of https://github.com/ospab/ostp.git
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:
parent
bb7d471864
commit
8fe0589ea6
|
|
@ -23,14 +23,15 @@ pub fn derive_psk(access_key: &[u8]) -> [u8; 32] {
|
|||
psk
|
||||
}
|
||||
|
||||
/// Derives a unique 4-byte session_id mask using HMAC-SHA256(key, nonce).
|
||||
/// Used strictly for handshake phase obfuscation.
|
||||
fn derive_session_mask(key: &[u8; 8], nonce: u64) -> [u8; 4] {
|
||||
/// Derives a 6-byte handshake mask using HMAC-SHA256(key, nonce).
|
||||
/// Covers session_id (4 bytes) + noise_len (2 bytes) to prevent
|
||||
/// 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");
|
||||
mac.update(&nonce.to_be_bytes());
|
||||
let result = mac.finalize().into_bytes();
|
||||
let mut mask = [0u8; 4];
|
||||
mask.copy_from_slice(&result[..4]);
|
||||
let mut mask = [0u8; 6];
|
||||
mask.copy_from_slice(&result[..6]);
|
||||
mask
|
||||
}
|
||||
|
||||
|
|
@ -61,10 +62,10 @@ pub fn obfuscate_packet_inplace(raw: &mut [u8], key: &[u8; 8], is_handshake: boo
|
|||
raw[i] ^= mask_result[i];
|
||||
}
|
||||
}
|
||||
} else if raw.len() >= 4 {
|
||||
// Handshake packets: mask session_id with a fixed handshake-phase mask
|
||||
let mask = derive_session_mask(key, u64::MAX);
|
||||
for i in 0..4 {
|
||||
} else if raw.len() >= 6 {
|
||||
// Handshake: mask session_id (4 bytes) + noise_len (2 bytes)
|
||||
let mask = derive_handshake_mask(key, u64::MAX);
|
||||
for i in 0..6 {
|
||||
raw[i] ^= mask[i];
|
||||
}
|
||||
}
|
||||
|
|
@ -90,8 +91,8 @@ pub fn deobfuscate_header_inplace(
|
|||
header[i] ^= mask_result[i];
|
||||
}
|
||||
} else {
|
||||
let mask = derive_session_mask(key, u64::MAX);
|
||||
for i in 0..4 {
|
||||
let mask = derive_handshake_mask(key, u64::MAX);
|
||||
for i in 0..header.len().min(6) {
|
||||
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);
|
||||
deobfuscate_header_inplace(&mut header, ciphertext, key, is_handshake);
|
||||
header_slice.copy_from_slice(&header);
|
||||
} else if raw.len() >= 4 {
|
||||
let mask = derive_session_mask(key, u64::MAX);
|
||||
for i in 0..4 {
|
||||
} else if raw.len() >= 6 {
|
||||
let mask = derive_handshake_mask(key, u64::MAX);
|
||||
for i in 0..6 {
|
||||
raw[i] ^= mask[i];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,8 +214,20 @@ impl ProtocolMachine {
|
|||
}
|
||||
|
||||
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 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 {
|
||||
NoiseRole::Responder => {
|
||||
|
|
@ -380,15 +392,17 @@ impl ProtocolMachine {
|
|||
// Anti-DPI: add random padding after the Noise payload to prevent
|
||||
// size fingerprinting. Without this, every handshake is exactly 52 bytes
|
||||
// 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 mut pad = vec![0u8; pad_len];
|
||||
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(&noise_len.to_be_bytes());
|
||||
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);
|
||||
crate::crypto::obfuscate_packet_inplace(&mut out, &self.obfuscation_key, true);
|
||||
Ok(Bytes::from(out))
|
||||
|
|
|
|||
|
|
@ -158,10 +158,12 @@ impl Dispatcher {
|
|||
let psk = ostp_core::crypto::derive_psk(candidate_key.as_bytes());
|
||||
|
||||
// Decode the session_id using this key's obfuscation
|
||||
let mut header = [0u8; 4];
|
||||
header.copy_from_slice(&packet[0..4]);
|
||||
// Handshake header is 6 bytes: [session_id:4][noise_len:2]
|
||||
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);
|
||||
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();
|
||||
cfg.session_id = candidate_session_id;
|
||||
|
|
|
|||
Loading…
Reference in New Issue