mirror of
https://github.com/telemt/telemt.git
synced 2026-04-18 11:04:09 +03:00
Format
This commit is contained in:
@@ -33,35 +33,89 @@ pub static TG_DATACENTERS_V6: LazyLock<Vec<IpAddr>> = LazyLock::new(|| {
|
||||
|
||||
// ============= Middle Proxies (for advertising) =============
|
||||
|
||||
pub static TG_MIDDLE_PROXIES_V4: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||
pub static TG_MIDDLE_PROXIES_V4: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||
LazyLock::new(|| {
|
||||
let mut m = std::collections::HashMap::new();
|
||||
m.insert(1, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)]);
|
||||
m.insert(-1, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)]);
|
||||
m.insert(2, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)]);
|
||||
m.insert(-2, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)]);
|
||||
m.insert(3, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)]);
|
||||
m.insert(-3, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)]);
|
||||
m.insert(
|
||||
1,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
-1,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
2,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
-2,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
3,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
-3,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)],
|
||||
);
|
||||
m.insert(4, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888)]);
|
||||
m.insert(-4, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 165, 109)), 8888)]);
|
||||
m.insert(
|
||||
-4,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 165, 109)), 8888)],
|
||||
);
|
||||
m.insert(5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
||||
m.insert(-5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
||||
m.insert(
|
||||
-5,
|
||||
vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)],
|
||||
);
|
||||
m
|
||||
});
|
||||
|
||||
pub static TG_MIDDLE_PROXIES_V6: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||
pub static TG_MIDDLE_PROXIES_V6: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||
LazyLock::new(|| {
|
||||
let mut m = std::collections::HashMap::new();
|
||||
m.insert(1, vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)]);
|
||||
m.insert(-1, vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)]);
|
||||
m.insert(2, vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)]);
|
||||
m.insert(-2, vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)]);
|
||||
m.insert(3, vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)]);
|
||||
m.insert(-3, vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)]);
|
||||
m.insert(4, vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)]);
|
||||
m.insert(-4, vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)]);
|
||||
m.insert(5, vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)]);
|
||||
m.insert(-5, vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)]);
|
||||
m.insert(
|
||||
1,
|
||||
vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
-1,
|
||||
vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
2,
|
||||
vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)],
|
||||
);
|
||||
m.insert(
|
||||
-2,
|
||||
vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)],
|
||||
);
|
||||
m.insert(
|
||||
3,
|
||||
vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
-3,
|
||||
vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
4,
|
||||
vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
-4,
|
||||
vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
5,
|
||||
vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m.insert(
|
||||
-5,
|
||||
vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)],
|
||||
);
|
||||
m
|
||||
});
|
||||
|
||||
@@ -89,12 +143,12 @@ impl ProtoTag {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Convert to 4 bytes (little-endian)
|
||||
pub fn to_bytes(self) -> [u8; 4] {
|
||||
(self as u32).to_le_bytes()
|
||||
}
|
||||
|
||||
|
||||
/// Get protocol tag as bytes slice
|
||||
pub fn as_bytes(&self) -> &'static [u8; 4] {
|
||||
match self {
|
||||
@@ -222,9 +276,7 @@ pub const SMALL_BUFFER_SIZE: usize = 8192;
|
||||
// ============= Statistics =============
|
||||
|
||||
/// Duration buckets for histogram metrics
|
||||
pub static DURATION_BUCKETS: &[f64] = &[
|
||||
0.1, 0.5, 1.0, 2.0, 5.0, 15.0, 60.0, 300.0, 600.0, 1800.0,
|
||||
];
|
||||
pub static DURATION_BUCKETS: &[f64] = &[0.1, 0.5, 1.0, 2.0, 5.0, 15.0, 60.0, 300.0, 600.0, 1800.0];
|
||||
|
||||
// ============= Reserved Nonce Patterns =============
|
||||
|
||||
@@ -235,29 +287,27 @@ pub static RESERVED_NONCE_FIRST_BYTES: &[u8] = &[0xef];
|
||||
pub static RESERVED_NONCE_BEGINNINGS: &[[u8; 4]] = &[
|
||||
[0x48, 0x45, 0x41, 0x44], // HEAD
|
||||
[0x50, 0x4F, 0x53, 0x54], // POST
|
||||
[0x47, 0x45, 0x54, 0x20], // GET
|
||||
[0x47, 0x45, 0x54, 0x20], // GET
|
||||
[0xee, 0xee, 0xee, 0xee], // Intermediate
|
||||
[0xdd, 0xdd, 0xdd, 0xdd], // Secure
|
||||
[0x16, 0x03, 0x01, 0x02], // TLS
|
||||
];
|
||||
|
||||
/// Reserved continuation bytes (bytes 4-7)
|
||||
pub static RESERVED_NONCE_CONTINUES: &[[u8; 4]] = &[
|
||||
[0x00, 0x00, 0x00, 0x00],
|
||||
];
|
||||
pub static RESERVED_NONCE_CONTINUES: &[[u8; 4]] = &[[0x00, 0x00, 0x00, 0x00]];
|
||||
|
||||
// ============= RPC Constants (for Middle Proxy) =============
|
||||
|
||||
/// RPC Proxy Request
|
||||
/// RPC Flags (from Erlang mtp_rpc.erl)
|
||||
pub const RPC_FLAG_NOT_ENCRYPTED: u32 = 0x2;
|
||||
pub const RPC_FLAG_HAS_AD_TAG: u32 = 0x8;
|
||||
pub const RPC_FLAG_MAGIC: u32 = 0x1000;
|
||||
pub const RPC_FLAG_EXTMODE2: u32 = 0x20000;
|
||||
pub const RPC_FLAG_PAD: u32 = 0x8000000;
|
||||
pub const RPC_FLAG_INTERMEDIATE: u32 = 0x20000000;
|
||||
pub const RPC_FLAG_ABRIDGED: u32 = 0x40000000;
|
||||
pub const RPC_FLAG_QUICKACK: u32 = 0x80000000;
|
||||
pub const RPC_FLAG_HAS_AD_TAG: u32 = 0x8;
|
||||
pub const RPC_FLAG_MAGIC: u32 = 0x1000;
|
||||
pub const RPC_FLAG_EXTMODE2: u32 = 0x20000;
|
||||
pub const RPC_FLAG_PAD: u32 = 0x8000000;
|
||||
pub const RPC_FLAG_INTERMEDIATE: u32 = 0x20000000;
|
||||
pub const RPC_FLAG_ABRIDGED: u32 = 0x40000000;
|
||||
pub const RPC_FLAG_QUICKACK: u32 = 0x80000000;
|
||||
|
||||
pub const RPC_PROXY_REQ: [u8; 4] = [0xee, 0xf1, 0xce, 0x36];
|
||||
/// RPC Proxy Answer
|
||||
@@ -285,67 +335,66 @@ pub mod rpc_flags {
|
||||
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
||||
}
|
||||
|
||||
// ============= Middle-End Proxy Servers =============
|
||||
pub const ME_PROXY_PORT: u16 = 8888;
|
||||
|
||||
// ============= Middle-End Proxy Servers =============
|
||||
pub const ME_PROXY_PORT: u16 = 8888;
|
||||
|
||||
pub static TG_MIDDLE_PROXIES_FLAT_V4: LazyLock<Vec<(IpAddr, u16)>> = LazyLock::new(|| {
|
||||
vec![
|
||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888),
|
||||
]
|
||||
});
|
||||
|
||||
// ============= RPC Constants (u32 native endian) =============
|
||||
// From mtproto-common.h + net-tcp-rpc-common.h + mtproto-proxy.c
|
||||
|
||||
pub const RPC_NONCE_U32: u32 = 0x7acb87aa;
|
||||
pub const RPC_HANDSHAKE_U32: u32 = 0x7682eef5;
|
||||
pub const RPC_HANDSHAKE_ERROR_U32: u32 = 0x6a27beda;
|
||||
pub const TL_PROXY_TAG_U32: u32 = 0xdb1e26ae; // mtproto-proxy.c:121
|
||||
|
||||
// mtproto-common.h
|
||||
pub const RPC_PROXY_REQ_U32: u32 = 0x36cef1ee;
|
||||
pub const RPC_PROXY_ANS_U32: u32 = 0x4403da0d;
|
||||
pub const RPC_CLOSE_CONN_U32: u32 = 0x1fcf425d;
|
||||
pub const RPC_CLOSE_EXT_U32: u32 = 0x5eb634a2;
|
||||
pub const RPC_SIMPLE_ACK_U32: u32 = 0x3bac409b;
|
||||
pub const RPC_PING_U32: u32 = 0x5730a2df;
|
||||
pub const RPC_PONG_U32: u32 = 0x8430eaa7;
|
||||
|
||||
pub const RPC_CRYPTO_NONE_U32: u32 = 0;
|
||||
pub const RPC_CRYPTO_AES_U32: u32 = 1;
|
||||
|
||||
pub mod proxy_flags {
|
||||
pub const FLAG_HAS_AD_TAG: u32 = 1;
|
||||
pub const FLAG_NOT_ENCRYPTED: u32 = 0x2;
|
||||
pub const FLAG_HAS_AD_TAG2: u32 = 0x8;
|
||||
pub const FLAG_MAGIC: u32 = 0x1000;
|
||||
pub const FLAG_EXTMODE2: u32 = 0x20000;
|
||||
pub const FLAG_PAD: u32 = 0x8000000;
|
||||
pub const FLAG_INTERMEDIATE: u32 = 0x20000000;
|
||||
pub const FLAG_ABRIDGED: u32 = 0x40000000;
|
||||
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
||||
}
|
||||
pub static TG_MIDDLE_PROXIES_FLAT_V4: LazyLock<Vec<(IpAddr, u16)>> = LazyLock::new(|| {
|
||||
vec![
|
||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888),
|
||||
(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888),
|
||||
]
|
||||
});
|
||||
|
||||
pub mod rpc_crypto_flags {
|
||||
pub const USE_CRC32C: u32 = 0x800;
|
||||
}
|
||||
|
||||
pub const ME_CONNECT_TIMEOUT_SECS: u64 = 5;
|
||||
pub const ME_HANDSHAKE_TIMEOUT_SECS: u64 = 10;
|
||||
// ============= RPC Constants (u32 native endian) =============
|
||||
// From mtproto-common.h + net-tcp-rpc-common.h + mtproto-proxy.c
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/tls_size_constants_security_tests.rs"]
|
||||
mod tls_size_constants_security_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub const RPC_NONCE_U32: u32 = 0x7acb87aa;
|
||||
pub const RPC_HANDSHAKE_U32: u32 = 0x7682eef5;
|
||||
pub const RPC_HANDSHAKE_ERROR_U32: u32 = 0x6a27beda;
|
||||
pub const TL_PROXY_TAG_U32: u32 = 0xdb1e26ae; // mtproto-proxy.c:121
|
||||
|
||||
// mtproto-common.h
|
||||
pub const RPC_PROXY_REQ_U32: u32 = 0x36cef1ee;
|
||||
pub const RPC_PROXY_ANS_U32: u32 = 0x4403da0d;
|
||||
pub const RPC_CLOSE_CONN_U32: u32 = 0x1fcf425d;
|
||||
pub const RPC_CLOSE_EXT_U32: u32 = 0x5eb634a2;
|
||||
pub const RPC_SIMPLE_ACK_U32: u32 = 0x3bac409b;
|
||||
pub const RPC_PING_U32: u32 = 0x5730a2df;
|
||||
pub const RPC_PONG_U32: u32 = 0x8430eaa7;
|
||||
|
||||
pub const RPC_CRYPTO_NONE_U32: u32 = 0;
|
||||
pub const RPC_CRYPTO_AES_U32: u32 = 1;
|
||||
|
||||
pub mod proxy_flags {
|
||||
pub const FLAG_HAS_AD_TAG: u32 = 1;
|
||||
pub const FLAG_NOT_ENCRYPTED: u32 = 0x2;
|
||||
pub const FLAG_HAS_AD_TAG2: u32 = 0x8;
|
||||
pub const FLAG_MAGIC: u32 = 0x1000;
|
||||
pub const FLAG_EXTMODE2: u32 = 0x20000;
|
||||
pub const FLAG_PAD: u32 = 0x8000000;
|
||||
pub const FLAG_INTERMEDIATE: u32 = 0x20000000;
|
||||
pub const FLAG_ABRIDGED: u32 = 0x40000000;
|
||||
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
||||
}
|
||||
|
||||
pub mod rpc_crypto_flags {
|
||||
pub const USE_CRC32C: u32 = 0x800;
|
||||
}
|
||||
|
||||
pub const ME_CONNECT_TIMEOUT_SECS: u64 = 5;
|
||||
pub const ME_HANDSHAKE_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/tls_size_constants_security_tests.rs"]
|
||||
mod tls_size_constants_security_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_proto_tag_roundtrip() {
|
||||
for tag in [ProtoTag::Abridged, ProtoTag::Intermediate, ProtoTag::Secure] {
|
||||
@@ -354,20 +403,20 @@ mod tests {
|
||||
assert_eq!(tag, parsed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_proto_tag_values() {
|
||||
assert_eq!(ProtoTag::Abridged.to_bytes(), PROTO_TAG_ABRIDGED);
|
||||
assert_eq!(ProtoTag::Intermediate.to_bytes(), PROTO_TAG_INTERMEDIATE);
|
||||
assert_eq!(ProtoTag::Secure.to_bytes(), PROTO_TAG_SECURE);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_invalid_proto_tag() {
|
||||
assert!(ProtoTag::from_bytes([0, 0, 0, 0]).is_none());
|
||||
assert!(ProtoTag::from_bytes([0xff, 0xff, 0xff, 0xff]).is_none());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_datacenters_count() {
|
||||
assert_eq!(TG_DATACENTERS_V4.len(), 5);
|
||||
|
||||
@@ -22,7 +22,7 @@ impl FrameExtra {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
|
||||
/// Create with quickack flag set
|
||||
pub fn with_quickack() -> Self {
|
||||
Self {
|
||||
@@ -30,7 +30,7 @@ impl FrameExtra {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create with simple_ack flag set
|
||||
pub fn with_simple_ack() -> Self {
|
||||
Self {
|
||||
@@ -38,7 +38,7 @@ impl FrameExtra {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check if any flags are set
|
||||
pub fn has_flags(&self) -> bool {
|
||||
self.quickack || self.simple_ack || self.skip_send
|
||||
@@ -76,22 +76,22 @@ impl FrameMode {
|
||||
FrameMode::Abridged => 4,
|
||||
FrameMode::Intermediate => 4,
|
||||
FrameMode::SecureIntermediate => 4 + 3, // length + padding
|
||||
FrameMode::Full => 12 + 16, // header + max CBC padding
|
||||
FrameMode::Full => 12 + 16, // header + max CBC padding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate message length for MTProto
|
||||
pub fn validate_message_length(len: usize) -> bool {
|
||||
use super::constants::{MIN_MSG_LEN, MAX_MSG_LEN, PADDING_FILLER};
|
||||
|
||||
use super::constants::{MAX_MSG_LEN, MIN_MSG_LEN, PADDING_FILLER};
|
||||
|
||||
(MIN_MSG_LEN..=MAX_MSG_LEN).contains(&len) && len.is_multiple_of(PADDING_FILLER.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_frame_extra_default() {
|
||||
let extra = FrameExtra::default();
|
||||
@@ -100,18 +100,18 @@ mod tests {
|
||||
assert!(!extra.skip_send);
|
||||
assert!(!extra.has_flags());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_frame_extra_flags() {
|
||||
let extra = FrameExtra::with_quickack();
|
||||
assert!(extra.quickack);
|
||||
assert!(extra.has_flags());
|
||||
|
||||
|
||||
let extra = FrameExtra::with_simple_ack();
|
||||
assert!(extra.simple_ack);
|
||||
assert!(extra.has_flags());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_validate_message_length() {
|
||||
assert!(validate_message_length(12)); // MIN_MSG_LEN
|
||||
@@ -119,4 +119,4 @@ mod tests {
|
||||
assert!(!validate_message_length(8)); // Too small
|
||||
assert!(!validate_message_length(13)); // Not aligned to 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ pub use frame::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use obfuscation::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use tls::*;
|
||||
pub use tls::*;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use zeroize::Zeroize;
|
||||
use crate::crypto::{sha256, AesCtr};
|
||||
use super::constants::*;
|
||||
use crate::crypto::{AesCtr, sha256};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Obfuscation parameters from handshake
|
||||
///
|
||||
@@ -44,41 +44,40 @@ impl ObfuscationParams {
|
||||
let dec_prekey_iv = &handshake[SKIP_LEN..SKIP_LEN + PREKEY_LEN + IV_LEN];
|
||||
let dec_prekey = &dec_prekey_iv[..PREKEY_LEN];
|
||||
let dec_iv_bytes = &dec_prekey_iv[PREKEY_LEN..];
|
||||
|
||||
|
||||
let enc_prekey_iv: Vec<u8> = dec_prekey_iv.iter().rev().copied().collect();
|
||||
let enc_prekey = &enc_prekey_iv[..PREKEY_LEN];
|
||||
let enc_iv_bytes = &enc_prekey_iv[PREKEY_LEN..];
|
||||
|
||||
|
||||
for (username, secret) in secrets {
|
||||
let mut dec_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
|
||||
dec_key_input.extend_from_slice(dec_prekey);
|
||||
dec_key_input.extend_from_slice(secret);
|
||||
let decrypt_key = sha256(&dec_key_input);
|
||||
|
||||
|
||||
let decrypt_iv = u128::from_be_bytes(dec_iv_bytes.try_into().unwrap());
|
||||
|
||||
|
||||
let mut decryptor = AesCtr::new(&decrypt_key, decrypt_iv);
|
||||
let decrypted = decryptor.decrypt(handshake);
|
||||
|
||||
|
||||
let tag_bytes: [u8; 4] = decrypted[PROTO_TAG_POS..PROTO_TAG_POS + 4]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
|
||||
let proto_tag = match ProtoTag::from_bytes(tag_bytes) {
|
||||
Some(tag) => tag,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let dc_idx = i16::from_le_bytes(
|
||||
decrypted[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap()
|
||||
);
|
||||
|
||||
|
||||
let dc_idx =
|
||||
i16::from_le_bytes(decrypted[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap());
|
||||
|
||||
let mut enc_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
|
||||
enc_key_input.extend_from_slice(enc_prekey);
|
||||
enc_key_input.extend_from_slice(secret);
|
||||
let encrypt_key = sha256(&enc_key_input);
|
||||
let encrypt_iv = u128::from_be_bytes(enc_iv_bytes.try_into().unwrap());
|
||||
|
||||
|
||||
return Some((
|
||||
ObfuscationParams {
|
||||
decrypt_key,
|
||||
@@ -91,20 +90,20 @@ impl ObfuscationParams {
|
||||
username.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Create AES-CTR decryptor for client -> proxy direction
|
||||
pub fn create_decryptor(&self) -> AesCtr {
|
||||
AesCtr::new(&self.decrypt_key, self.decrypt_iv)
|
||||
}
|
||||
|
||||
|
||||
/// Create AES-CTR encryptor for proxy -> client direction
|
||||
pub fn create_encryptor(&self) -> AesCtr {
|
||||
AesCtr::new(&self.encrypt_key, self.encrypt_iv)
|
||||
}
|
||||
|
||||
|
||||
/// Get the combined encrypt key and IV for fast mode
|
||||
pub fn enc_key_iv(&self) -> Vec<u8> {
|
||||
let mut result = Vec::with_capacity(KEY_LEN + IV_LEN);
|
||||
@@ -120,7 +119,7 @@ pub fn generate_nonce<R: FnMut(usize) -> Vec<u8>>(mut random_bytes: R) -> [u8; H
|
||||
let nonce_vec = random_bytes(HANDSHAKE_LEN);
|
||||
let mut nonce = [0u8; HANDSHAKE_LEN];
|
||||
nonce.copy_from_slice(&nonce_vec);
|
||||
|
||||
|
||||
if is_valid_nonce(&nonce) {
|
||||
return nonce;
|
||||
}
|
||||
@@ -132,17 +131,17 @@ pub fn is_valid_nonce(nonce: &[u8; HANDSHAKE_LEN]) -> bool {
|
||||
if RESERVED_NONCE_FIRST_BYTES.contains(&nonce[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let first_four: [u8; 4] = nonce[..4].try_into().unwrap();
|
||||
if RESERVED_NONCE_BEGINNINGS.contains(&first_four) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let continue_four: [u8; 4] = nonce[4..8].try_into().unwrap();
|
||||
if RESERVED_NONCE_CONTINUES.contains(&continue_four) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -153,7 +152,7 @@ pub fn prepare_tg_nonce(
|
||||
enc_key_iv: Option<&[u8]>,
|
||||
) {
|
||||
nonce[PROTO_TAG_POS..PROTO_TAG_POS + 4].copy_from_slice(&proto_tag.to_bytes());
|
||||
|
||||
|
||||
if let Some(key_iv) = enc_key_iv {
|
||||
let reversed: Vec<u8> = key_iv.iter().rev().copied().collect();
|
||||
nonce[SKIP_LEN..SKIP_LEN + KEY_LEN + IV_LEN].copy_from_slice(&reversed);
|
||||
@@ -171,39 +170,39 @@ pub fn encrypt_nonce(nonce: &[u8; HANDSHAKE_LEN]) -> Vec<u8> {
|
||||
let key_iv = &nonce[SKIP_LEN..SKIP_LEN + KEY_LEN + IV_LEN];
|
||||
let enc_key = sha256(key_iv);
|
||||
let enc_iv = u128::from_be_bytes(key_iv[..IV_LEN].try_into().unwrap());
|
||||
|
||||
|
||||
let mut encryptor = AesCtr::new(&enc_key, enc_iv);
|
||||
|
||||
|
||||
let mut result = nonce.to_vec();
|
||||
let encrypted_part = encryptor.encrypt(&nonce[PROTO_TAG_POS..]);
|
||||
result[PROTO_TAG_POS..].copy_from_slice(&encrypted_part);
|
||||
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_nonce() {
|
||||
let mut valid = [0x42u8; HANDSHAKE_LEN];
|
||||
valid[4..8].copy_from_slice(&[1, 2, 3, 4]);
|
||||
assert!(is_valid_nonce(&valid));
|
||||
|
||||
|
||||
let mut invalid = [0x00u8; HANDSHAKE_LEN];
|
||||
invalid[0] = 0xef;
|
||||
assert!(!is_valid_nonce(&invalid));
|
||||
|
||||
|
||||
let mut invalid = [0x00u8; HANDSHAKE_LEN];
|
||||
invalid[..4].copy_from_slice(b"HEAD");
|
||||
assert!(!is_valid_nonce(&invalid));
|
||||
|
||||
|
||||
let mut invalid = [0x42u8; HANDSHAKE_LEN];
|
||||
invalid[4..8].copy_from_slice(&[0, 0, 0, 0]);
|
||||
assert!(!is_valid_nonce(&invalid));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_generate_nonce() {
|
||||
let mut counter = 0u8;
|
||||
@@ -211,7 +210,7 @@ mod tests {
|
||||
counter = counter.wrapping_add(1);
|
||||
vec![counter; n]
|
||||
});
|
||||
|
||||
|
||||
assert!(is_valid_nonce(&nonce));
|
||||
assert_eq!(nonce.len(), HANDSHAKE_LEN);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
use crate::crypto::sha256_hmac;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Helper to create a byte vector of specific length.
|
||||
fn make_garbage(len: usize) -> Vec<u8> {
|
||||
@@ -33,8 +33,7 @@ fn make_valid_tls_handshake_with_session_id(
|
||||
|
||||
let digest = make_digest(secret, &handshake, timestamp);
|
||||
|
||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||
.copy_from_slice(&digest);
|
||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&digest);
|
||||
handshake
|
||||
}
|
||||
|
||||
@@ -96,15 +95,15 @@ fn extract_sni_with_overlapping_extension_lengths_rejected() {
|
||||
h.push(0); // Session ID length: 0
|
||||
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]); // Cipher suites
|
||||
h.extend_from_slice(&[0x01, 0x00]); // Compression
|
||||
|
||||
|
||||
// Extensions start
|
||||
h.extend_from_slice(&[0x00, 0x20]); // Total Extensions length: 32
|
||||
|
||||
|
||||
// Extension 1: SNI (type 0)
|
||||
h.extend_from_slice(&[0x00, 0x00]);
|
||||
h.extend_from_slice(&[0x00, 0x00]);
|
||||
h.extend_from_slice(&[0x00, 0x40]); // Claimed len: 64 (OVERFLOWS total extensions len 32)
|
||||
h.extend_from_slice(&[0u8; 64]);
|
||||
|
||||
|
||||
assert!(extract_sni_from_client_hello(&h).is_none());
|
||||
}
|
||||
|
||||
@@ -118,19 +117,19 @@ fn extract_sni_with_infinite_loop_potential_extension_rejected() {
|
||||
h.push(0); // Session ID length: 0
|
||||
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]); // Cipher suites
|
||||
h.extend_from_slice(&[0x01, 0x00]); // Compression
|
||||
|
||||
|
||||
// Extensions start
|
||||
h.extend_from_slice(&[0x00, 0x10]); // Total Extensions length: 16
|
||||
|
||||
// Extension: zero length but claims more?
|
||||
|
||||
// Extension: zero length but claims more?
|
||||
// If our parser didn't advance, it might loop.
|
||||
// Telemt uses `pos += 4 + elen;` so it always advances.
|
||||
h.extend_from_slice(&[0x12, 0x34]); // Unknown type
|
||||
h.extend_from_slice(&[0x00, 0x00]); // Length 0
|
||||
|
||||
|
||||
// Fill the rest with garbage
|
||||
h.extend_from_slice(&[0x42; 12]);
|
||||
|
||||
|
||||
// We expect it to finish without SNI found
|
||||
assert!(extract_sni_from_client_hello(&h).is_none());
|
||||
}
|
||||
@@ -143,7 +142,7 @@ fn extract_sni_with_invalid_hostname_rejected() {
|
||||
sni.push(0);
|
||||
sni.extend_from_slice(&(host.len() as u16).to_be_bytes());
|
||||
sni.extend_from_slice(host);
|
||||
|
||||
|
||||
let mut h = vec![0x16, 0x03, 0x03, 0x00, 0x60]; // Record header
|
||||
h.push(0x01); // ClientHello
|
||||
h.extend_from_slice(&[0x00, 0x00, 0x5C]);
|
||||
@@ -152,16 +151,19 @@ fn extract_sni_with_invalid_hostname_rejected() {
|
||||
h.push(0);
|
||||
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]);
|
||||
h.extend_from_slice(&[0x01, 0x00]);
|
||||
|
||||
|
||||
let mut ext = Vec::new();
|
||||
ext.extend_from_slice(&0x0000u16.to_be_bytes());
|
||||
ext.extend_from_slice(&(sni.len() as u16).to_be_bytes());
|
||||
ext.extend_from_slice(&sni);
|
||||
|
||||
|
||||
h.extend_from_slice(&(ext.len() as u16).to_be_bytes());
|
||||
h.extend_from_slice(&ext);
|
||||
|
||||
assert!(extract_sni_from_client_hello(&h).is_none(), "Invalid SNI hostname must be rejected");
|
||||
|
||||
assert!(
|
||||
extract_sni_from_client_hello(&h).is_none(),
|
||||
"Invalid SNI hostname must be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -233,7 +235,7 @@ fn is_tls_handshake_robustness_against_probing() {
|
||||
assert!(is_tls_handshake(&[0x16, 0x03, 0x01]));
|
||||
// Valid TLS 1.2/1.3 ClientHello (Legacy Record Layer)
|
||||
assert!(is_tls_handshake(&[0x16, 0x03, 0x03]));
|
||||
|
||||
|
||||
// Invalid record type but matching version
|
||||
assert!(!is_tls_handshake(&[0x17, 0x03, 0x03]));
|
||||
// Plaintext HTTP request
|
||||
@@ -247,12 +249,12 @@ fn validate_tls_handshake_at_time_strict_boundary() {
|
||||
let secret = b"strict_boundary_secret_32_bytes_";
|
||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||
let now: i64 = 1_000_000_000;
|
||||
|
||||
|
||||
// Boundary: exactly TIME_SKEW_MAX (120s past)
|
||||
let ts_past = (now - TIME_SKEW_MAX) as u32;
|
||||
let h = make_valid_tls_handshake_with_session_id(secret, ts_past, &[0x42; 32]);
|
||||
assert!(validate_tls_handshake_at_time(&h, &secrets, false, now).is_some());
|
||||
|
||||
|
||||
// Boundary + 1s: should be rejected
|
||||
let ts_too_past = (now - TIME_SKEW_MAX - 1) as u32;
|
||||
let h2 = make_valid_tls_handshake_with_session_id(secret, ts_too_past, &[0x42; 32]);
|
||||
@@ -268,14 +270,14 @@ fn extract_sni_with_duplicate_extensions_rejected() {
|
||||
sni1.push(0);
|
||||
sni1.extend_from_slice(&(host1.len() as u16).to_be_bytes());
|
||||
sni1.extend_from_slice(host1);
|
||||
|
||||
|
||||
let host2 = b"second.com";
|
||||
let mut sni2 = Vec::new();
|
||||
sni2.extend_from_slice(&((host2.len() + 3) as u16).to_be_bytes());
|
||||
sni2.push(0);
|
||||
sni2.extend_from_slice(&(host2.len() as u16).to_be_bytes());
|
||||
sni2.extend_from_slice(host2);
|
||||
|
||||
|
||||
let mut ext = Vec::new();
|
||||
// Ext 1: SNI
|
||||
ext.extend_from_slice(&0x0000u16.to_be_bytes());
|
||||
@@ -285,7 +287,7 @@ fn extract_sni_with_duplicate_extensions_rejected() {
|
||||
ext.extend_from_slice(&0x0000u16.to_be_bytes());
|
||||
ext.extend_from_slice(&(sni2.len() as u16).to_be_bytes());
|
||||
ext.extend_from_slice(&sni2);
|
||||
|
||||
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(&[0x03, 0x03]);
|
||||
body.extend_from_slice(&[0u8; 32]);
|
||||
@@ -306,7 +308,7 @@ fn extract_sni_with_duplicate_extensions_rejected() {
|
||||
h.extend_from_slice(&[0x03, 0x03]);
|
||||
h.extend_from_slice(&(handshake.len() as u16).to_be_bytes());
|
||||
h.extend_from_slice(&handshake);
|
||||
|
||||
|
||||
// Duplicate SNI extensions are ambiguous and must fail closed.
|
||||
assert!(extract_sni_from_client_hello(&h).is_none());
|
||||
}
|
||||
@@ -317,21 +319,26 @@ fn extract_alpn_with_malformed_list_rejected() {
|
||||
alpn_payload.extend_from_slice(&0x0005u16.to_be_bytes()); // Total len 5
|
||||
alpn_payload.push(10); // Labeled len 10 (OVERFLOWS total 5)
|
||||
alpn_payload.extend_from_slice(b"h2");
|
||||
|
||||
|
||||
let mut ext = Vec::new();
|
||||
ext.extend_from_slice(&0x0010u16.to_be_bytes()); // Type: ALPN (16)
|
||||
ext.extend_from_slice(&(alpn_payload.len() as u16).to_be_bytes());
|
||||
ext.extend_from_slice(&alpn_payload);
|
||||
|
||||
let mut h = vec![0x16, 0x03, 0x03, 0x00, 0x40, 0x01, 0x00, 0x00, 0x3C, 0x03, 0x03];
|
||||
|
||||
let mut h = vec![
|
||||
0x16, 0x03, 0x03, 0x00, 0x40, 0x01, 0x00, 0x00, 0x3C, 0x03, 0x03,
|
||||
];
|
||||
h.extend_from_slice(&[0u8; 32]);
|
||||
h.push(0);
|
||||
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01, 0x01, 0x00]);
|
||||
h.extend_from_slice(&(ext.len() as u16).to_be_bytes());
|
||||
h.extend_from_slice(&ext);
|
||||
|
||||
|
||||
let res = extract_alpn_from_client_hello(&h);
|
||||
assert!(res.is_empty(), "Malformed ALPN list must return empty or fail");
|
||||
assert!(
|
||||
res.is_empty(),
|
||||
"Malformed ALPN list must return empty or fail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -343,9 +350,9 @@ fn extract_sni_with_huge_extension_header_rejected() {
|
||||
h.extend_from_slice(&[0u8; 32]);
|
||||
h.push(0);
|
||||
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01, 0x01, 0x00]);
|
||||
|
||||
|
||||
// Extensions start
|
||||
h.extend_from_slice(&[0xFF, 0xFF]); // Total extensions: 65535 (OVERFLOWS everything)
|
||||
|
||||
|
||||
assert!(extract_sni_from_client_hello(&h).is_none());
|
||||
}
|
||||
|
||||
@@ -84,7 +84,10 @@ fn make_valid_client_hello_record(host: &str, alpn_protocols: &[&[u8]]) -> Vec<u
|
||||
#[test]
|
||||
fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
||||
let valid = make_valid_client_hello_record("example.com", &[b"h2", b"http/1.1"]);
|
||||
assert_eq!(extract_sni_from_client_hello(&valid).as_deref(), Some("example.com"));
|
||||
assert_eq!(
|
||||
extract_sni_from_client_hello(&valid).as_deref(),
|
||||
Some("example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
extract_alpn_from_client_hello(&valid),
|
||||
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
||||
@@ -121,8 +124,14 @@ fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(extract_sni_from_client_hello(input).is_none(), "corpus item {idx} must fail closed for SNI");
|
||||
assert!(extract_alpn_from_client_hello(input).is_empty(), "corpus item {idx} must fail closed for ALPN");
|
||||
assert!(
|
||||
extract_sni_from_client_hello(input).is_none(),
|
||||
"corpus item {idx} must fail closed for SNI"
|
||||
);
|
||||
assert!(
|
||||
extract_alpn_from_client_hello(input).is_empty(),
|
||||
"corpus item {idx} must fail closed for ALPN"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +172,9 @@ fn tls_handshake_fuzz_corpus_never_panics_and_rejects_digest_mutations() {
|
||||
for _ in 0..32 {
|
||||
let mut mutated = base.clone();
|
||||
for _ in 0..2 {
|
||||
seed = seed.wrapping_mul(2862933555777941757).wrapping_add(3037000493);
|
||||
seed = seed
|
||||
.wrapping_mul(2862933555777941757)
|
||||
.wrapping_add(3037000493);
|
||||
let idx = TLS_DIGEST_POS + (seed as usize % TLS_DIGEST_LEN);
|
||||
mutated[idx] ^= ((seed >> 17) as u8).wrapping_add(1);
|
||||
}
|
||||
@@ -171,9 +182,13 @@ fn tls_handshake_fuzz_corpus_never_panics_and_rejects_digest_mutations() {
|
||||
}
|
||||
|
||||
for (idx, handshake) in corpus.iter().enumerate() {
|
||||
let result = catch_unwind(|| validate_tls_handshake_at_time(handshake, &secrets, false, now));
|
||||
let result =
|
||||
catch_unwind(|| validate_tls_handshake_at_time(handshake, &secrets, false, now));
|
||||
assert!(result.is_ok(), "corpus item {idx} must not panic");
|
||||
assert!(result.unwrap().is_none(), "corpus item {idx} must fail closed");
|
||||
assert!(
|
||||
result.unwrap().is_none(),
|
||||
"corpus item {idx} must fail closed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource,
|
||||
};
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Build a TLS-handshake-like buffer that contains a valid HMAC digest
|
||||
@@ -39,8 +41,7 @@ fn make_valid_tls_handshake_with_session_id(
|
||||
digest[28 + i] ^= ts[i];
|
||||
}
|
||||
|
||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||
.copy_from_slice(&digest);
|
||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&digest);
|
||||
handshake
|
||||
}
|
||||
|
||||
@@ -180,7 +181,10 @@ fn second_user_in_list_found_when_first_does_not_match() {
|
||||
("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!(
|
||||
result.is_some(),
|
||||
"user_b must be found even though user_a comes first"
|
||||
);
|
||||
assert_eq!(result.unwrap().user, "user_b");
|
||||
}
|
||||
|
||||
@@ -428,8 +432,7 @@ fn censor_probe_random_digests_all_rejected() {
|
||||
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);
|
||||
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"
|
||||
@@ -553,8 +556,7 @@ fn system_time_before_unix_epoch_is_rejected_without_panic() {
|
||||
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))
|
||||
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(),
|
||||
@@ -620,7 +622,10 @@ fn appended_trailing_byte_causes_rejection() {
|
||||
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");
|
||||
assert!(
|
||||
validate_tls_handshake(&h, &secrets, true).is_some(),
|
||||
"baseline"
|
||||
);
|
||||
|
||||
h.push(0x00);
|
||||
assert!(
|
||||
@@ -647,8 +652,7 @@ fn zero_length_session_id_accepted() {
|
||||
|
||||
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);
|
||||
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);
|
||||
@@ -773,10 +777,18 @@ fn ignore_time_skew_explicitly_decouples_from_boot_time_cap() {
|
||||
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);
|
||||
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_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"
|
||||
@@ -888,8 +900,8 @@ fn adversarial_skew_boundary_matrix_accepts_only_inclusive_window_when_boot_disa
|
||||
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 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,
|
||||
@@ -917,8 +929,8 @@ fn light_fuzz_skew_window_rejects_outside_range_when_boot_disabled() {
|
||||
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();
|
||||
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"
|
||||
@@ -940,8 +952,8 @@ fn stress_boot_disabled_validation_matches_time_diff_oracle() {
|
||||
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 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!(
|
||||
@@ -960,7 +972,10 @@ fn integration_large_user_list_with_boot_disabled_finds_only_matching_user() {
|
||||
|
||||
let mut secrets = Vec::new();
|
||||
for i in 0..512u32 {
|
||||
secrets.push((format!("noise-{i}"), format!("noise-secret-{i}").into_bytes()));
|
||||
secrets.push((
|
||||
format!("noise-{i}"),
|
||||
format!("noise-secret-{i}").into_bytes(),
|
||||
));
|
||||
}
|
||||
secrets.push(("target-user".to_string(), target_secret.to_vec()));
|
||||
|
||||
@@ -1018,7 +1033,10 @@ fn u32_max_timestamp_accepted_with_ignore_time_skew() {
|
||||
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!(
|
||||
result.is_some(),
|
||||
"u32::MAX timestamp must be accepted with ignore_time_skew=true"
|
||||
);
|
||||
assert_eq!(
|
||||
result.unwrap().timestamp,
|
||||
u32::MAX,
|
||||
@@ -1150,16 +1168,17 @@ fn first_matching_user_wins_over_later_duplicate_secret() {
|
||||
|
||||
let secrets = vec![
|
||||
("decoy_1".to_string(), b"wrong_1".to_vec()),
|
||||
("winner".to_string(), shared.to_vec()), // first match
|
||||
("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
|
||||
("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",
|
||||
result.unwrap().user,
|
||||
"winner",
|
||||
"first matching user must be returned even when a later entry also matches"
|
||||
);
|
||||
}
|
||||
@@ -1425,7 +1444,8 @@ fn test_build_server_hello_structure() {
|
||||
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 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);
|
||||
@@ -1729,7 +1749,10 @@ fn empty_secret_hmac_is_supported() {
|
||||
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");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"Empty HMAC key must not panic and must validate when correct"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1802,7 +1825,10 @@ fn server_hello_application_data_payload_varies_across_runs() {
|
||||
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");
|
||||
assert!(
|
||||
payload.iter().any(|&b| b != 0),
|
||||
"Payload must not be all-zero deterministic filler"
|
||||
);
|
||||
unique_payloads.insert(payload);
|
||||
}
|
||||
|
||||
@@ -1846,7 +1872,13 @@ fn large_replay_window_does_not_expand_time_skew_acceptance() {
|
||||
|
||||
#[test]
|
||||
fn parse_tls_record_header_accepts_tls_version_constant() {
|
||||
let header = [TLS_RECORD_HANDSHAKE, TLS_VERSION[0], TLS_VERSION[1], 0x00, 0x2A];
|
||||
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);
|
||||
@@ -1868,7 +1900,10 @@ fn server_hello_clamps_fake_cert_len_lower_bound() {
|
||||
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");
|
||||
assert_eq!(
|
||||
app_len, 64,
|
||||
"fake cert payload must be clamped to minimum 64 bytes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1887,7 +1922,10 @@ fn server_hello_clamps_fake_cert_len_upper_bound() {
|
||||
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, MAX_TLS_CIPHERTEXT_SIZE, "fake cert payload must be clamped to TLS record max bound");
|
||||
assert_eq!(
|
||||
app_len, MAX_TLS_CIPHERTEXT_SIZE,
|
||||
"fake cert payload must be clamped to TLS record max bound"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1898,7 +1936,15 @@ fn server_hello_new_session_ticket_count_matches_configuration() {
|
||||
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 response = build_server_hello(
|
||||
secret,
|
||||
&client_digest,
|
||||
&session_id,
|
||||
1024,
|
||||
&rng,
|
||||
None,
|
||||
tickets,
|
||||
);
|
||||
|
||||
let mut pos = 0usize;
|
||||
let mut app_records = 0usize;
|
||||
@@ -1906,7 +1952,10 @@ fn server_hello_new_session_ticket_count_matches_configuration() {
|
||||
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");
|
||||
assert!(
|
||||
next <= response.len(),
|
||||
"TLS record must stay inside response bounds"
|
||||
);
|
||||
if rtype == TLS_RECORD_APPLICATION {
|
||||
app_records += 1;
|
||||
}
|
||||
@@ -1927,7 +1976,15 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
||||
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 response = build_server_hello(
|
||||
secret,
|
||||
&client_digest,
|
||||
&session_id,
|
||||
1024,
|
||||
&rng,
|
||||
None,
|
||||
u8::MAX,
|
||||
);
|
||||
|
||||
let mut pos = 0usize;
|
||||
let mut app_records = 0usize;
|
||||
@@ -1935,7 +1992,10 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
||||
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");
|
||||
assert!(
|
||||
next <= response.len(),
|
||||
"TLS record must stay inside response bounds"
|
||||
);
|
||||
if rtype == TLS_RECORD_APPLICATION {
|
||||
app_records += 1;
|
||||
}
|
||||
@@ -1943,8 +2003,7 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
app_records,
|
||||
5,
|
||||
app_records, 5,
|
||||
"response must cap ticket-like tail records to four plus one main application record"
|
||||
);
|
||||
}
|
||||
@@ -1972,10 +2031,14 @@ fn boot_time_handshake_replay_remains_blocked_after_cache_window_expires() {
|
||||
|
||||
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 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_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),
|
||||
@@ -2006,8 +2069,9 @@ fn adversarial_boot_time_handshake_should_not_be_replayable_after_cache_expiry()
|
||||
|
||||
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 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!(
|
||||
@@ -2067,11 +2131,14 @@ fn light_fuzz_boot_time_timestamp_matrix_with_short_replay_window_obeys_boot_cap
|
||||
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();
|
||||
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");
|
||||
assert!(
|
||||
accepted,
|
||||
"timestamp {ts} must remain boot-time compatible under 2s cap"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
!accepted,
|
||||
@@ -2107,7 +2174,9 @@ fn server_hello_application_data_contains_alpn_marker_when_selected() {
|
||||
|
||||
let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
|
||||
assert!(
|
||||
app_payload.windows(expected.len()).any(|window| window == expected),
|
||||
app_payload
|
||||
.windows(expected.len())
|
||||
.any(|window| window == expected),
|
||||
"first application payload must carry ALPN marker for selected protocol"
|
||||
);
|
||||
}
|
||||
@@ -2137,7 +2206,10 @@ fn server_hello_ignores_oversized_alpn_and_still_caps_ticket_tail() {
|
||||
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");
|
||||
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() {
|
||||
@@ -2146,7 +2218,9 @@ fn server_hello_ignores_oversized_alpn_and_still_caps_ticket_tail() {
|
||||
}
|
||||
pos = next;
|
||||
}
|
||||
let marker = [0x00u8, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, b'x', b'x', b'x', b'x'];
|
||||
let marker = [
|
||||
0x00u8, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, b'x', b'x', b'x', b'x',
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
app_records, 5,
|
||||
@@ -2310,13 +2384,13 @@ fn light_fuzz_tls_header_classifier_and_parser_policy_consistency() {
|
||||
&& header[1] == 0x03
|
||||
&& (header[2] == 0x01 || header[2] == 0x03);
|
||||
assert_eq!(
|
||||
classified,
|
||||
expected_classified,
|
||||
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]);
|
||||
let expected_parsed =
|
||||
header[1] == 0x03 && (header[2] == 0x01 || header[2] == TLS_VERSION[1]);
|
||||
assert_eq!(
|
||||
parsed.is_some(),
|
||||
expected_parsed,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use super::{
|
||||
MAX_TLS_CIPHERTEXT_SIZE,
|
||||
MAX_TLS_PLAINTEXT_SIZE,
|
||||
MIN_TLS_CLIENT_HELLO_SIZE,
|
||||
};
|
||||
use super::{MAX_TLS_CIPHERTEXT_SIZE, MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
||||
|
||||
#[test]
|
||||
fn tls_size_constants_match_rfc_8446() {
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::crypto::{sha256_hmac, SecureRandom};
|
||||
use super::constants::*;
|
||||
use crate::crypto::{SecureRandom, sha256_hmac};
|
||||
#[cfg(test)]
|
||||
use crate::error::ProxyError;
|
||||
use super::constants::*;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use subtle::ConstantTimeEq;
|
||||
use x25519_dalek::{X25519_BASEPOINT_BYTES, x25519};
|
||||
@@ -31,7 +31,7 @@ pub const TLS_DIGEST_HALF_LEN: usize = 16;
|
||||
/// Operators with known clock-drifted clients should tune deployment config
|
||||
/// (for example replay-window policy) to match their environment.
|
||||
pub const TIME_SKEW_MIN: i64 = -2 * 60; // 2 minutes before
|
||||
pub const TIME_SKEW_MAX: i64 = 2 * 60; // 2 minutes after
|
||||
pub const TIME_SKEW_MAX: i64 = 2 * 60; // 2 minutes after
|
||||
/// Maximum accepted boot-time timestamp (seconds) before skew checks are enforced.
|
||||
pub const BOOT_TIME_MAX_SECS: u32 = 7 * 24 * 60 * 60;
|
||||
/// Hard cap for boot-time compatibility bypass to avoid oversized acceptance
|
||||
@@ -69,7 +69,6 @@ pub struct TlsValidation {
|
||||
/// Client digest for response generation
|
||||
pub digest: [u8; TLS_DIGEST_LEN],
|
||||
/// Timestamp extracted from digest
|
||||
|
||||
pub timestamp: u32,
|
||||
}
|
||||
|
||||
@@ -87,60 +86,63 @@ impl TlsExtensionBuilder {
|
||||
extensions: Vec::with_capacity(128),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add Key Share extension with X25519 key
|
||||
fn add_key_share(&mut self, public_key: &[u8; 32]) -> &mut Self {
|
||||
// Extension type: key_share (0x0033)
|
||||
self.extensions.extend_from_slice(&extension_type::KEY_SHARE.to_be_bytes());
|
||||
|
||||
self.extensions
|
||||
.extend_from_slice(&extension_type::KEY_SHARE.to_be_bytes());
|
||||
|
||||
// Key share entry: curve (2) + key_len (2) + key (32) = 36 bytes
|
||||
// Extension data length
|
||||
let entry_len: u16 = 2 + 2 + 32; // curve + length + key
|
||||
self.extensions.extend_from_slice(&entry_len.to_be_bytes());
|
||||
|
||||
|
||||
// Named curve: x25519
|
||||
self.extensions.extend_from_slice(&named_curve::X25519.to_be_bytes());
|
||||
|
||||
self.extensions
|
||||
.extend_from_slice(&named_curve::X25519.to_be_bytes());
|
||||
|
||||
// Key length
|
||||
self.extensions.extend_from_slice(&(32u16).to_be_bytes());
|
||||
|
||||
|
||||
// Key data
|
||||
self.extensions.extend_from_slice(public_key);
|
||||
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Add Supported Versions extension
|
||||
fn add_supported_versions(&mut self, version: u16) -> &mut Self {
|
||||
// Extension type: supported_versions (0x002b)
|
||||
self.extensions.extend_from_slice(&extension_type::SUPPORTED_VERSIONS.to_be_bytes());
|
||||
|
||||
self.extensions
|
||||
.extend_from_slice(&extension_type::SUPPORTED_VERSIONS.to_be_bytes());
|
||||
|
||||
// Extension data: length (2) + version (2)
|
||||
self.extensions.extend_from_slice(&(2u16).to_be_bytes());
|
||||
|
||||
|
||||
// Selected version
|
||||
self.extensions.extend_from_slice(&version.to_be_bytes());
|
||||
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Build final extensions with length prefix
|
||||
|
||||
|
||||
fn build(self) -> Vec<u8> {
|
||||
let mut result = Vec::with_capacity(2 + self.extensions.len());
|
||||
|
||||
|
||||
// Extensions length (2 bytes)
|
||||
let len = self.extensions.len() as u16;
|
||||
result.extend_from_slice(&len.to_be_bytes());
|
||||
|
||||
|
||||
// Extensions data
|
||||
result.extend_from_slice(&self.extensions);
|
||||
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
/// Get current extensions without length prefix (for calculation)
|
||||
|
||||
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
&self.extensions
|
||||
}
|
||||
@@ -172,12 +174,12 @@ impl ServerHelloBuilder {
|
||||
extensions: TlsExtensionBuilder::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn with_x25519_key(mut self, key: &[u8; 32]) -> Self {
|
||||
self.extensions.add_key_share(key);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
fn with_tls13_version(mut self) -> Self {
|
||||
// TLS 1.3 = 0x0304
|
||||
self.extensions.add_supported_versions(0x0304);
|
||||
@@ -188,7 +190,7 @@ impl ServerHelloBuilder {
|
||||
fn build_message(&self) -> Vec<u8> {
|
||||
let extensions = self.extensions.extensions.clone();
|
||||
let extensions_len = extensions.len() as u16;
|
||||
|
||||
|
||||
// Calculate total length
|
||||
let body_len = 2 + // version
|
||||
32 + // random
|
||||
@@ -196,55 +198,55 @@ impl ServerHelloBuilder {
|
||||
2 + // cipher suite
|
||||
1 + // compression
|
||||
2 + extensions.len(); // extensions length + data
|
||||
|
||||
|
||||
let mut message = Vec::with_capacity(4 + body_len);
|
||||
|
||||
|
||||
// Handshake header
|
||||
message.push(0x02); // ServerHello message type
|
||||
|
||||
|
||||
// 3-byte length
|
||||
let len_bytes = (body_len as u32).to_be_bytes();
|
||||
message.extend_from_slice(&len_bytes[1..4]);
|
||||
|
||||
|
||||
// Server version (TLS 1.2 in header, actual version in extension)
|
||||
message.extend_from_slice(&TLS_VERSION);
|
||||
|
||||
|
||||
// Random (32 bytes) - placeholder, will be replaced with digest
|
||||
message.extend_from_slice(&self.random);
|
||||
|
||||
|
||||
// Session ID
|
||||
message.push(self.session_id.len() as u8);
|
||||
message.extend_from_slice(&self.session_id);
|
||||
|
||||
|
||||
// Cipher suite
|
||||
message.extend_from_slice(&self.cipher_suite);
|
||||
|
||||
|
||||
// Compression method
|
||||
message.push(self.compression);
|
||||
|
||||
|
||||
// Extensions length
|
||||
message.extend_from_slice(&extensions_len.to_be_bytes());
|
||||
|
||||
|
||||
// Extensions data
|
||||
message.extend_from_slice(&extensions);
|
||||
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
|
||||
/// Build complete ServerHello TLS record
|
||||
fn build_record(&self) -> Vec<u8> {
|
||||
let message = self.build_message();
|
||||
|
||||
|
||||
let mut record = Vec::with_capacity(5 + message.len());
|
||||
|
||||
|
||||
// TLS record header
|
||||
record.push(TLS_RECORD_HANDSHAKE);
|
||||
record.extend_from_slice(&TLS_VERSION);
|
||||
record.extend_from_slice(&(message.len() as u16).to_be_bytes());
|
||||
|
||||
|
||||
// Message
|
||||
record.extend_from_slice(&message);
|
||||
|
||||
|
||||
record
|
||||
}
|
||||
}
|
||||
@@ -320,7 +322,6 @@ fn system_time_to_unix_secs(now: SystemTime) -> Option<i64> {
|
||||
i64::try_from(d.as_secs()).ok()
|
||||
}
|
||||
|
||||
|
||||
fn validate_tls_handshake_at_time(
|
||||
handshake: &[u8],
|
||||
secrets: &[(String, Vec<u8>)],
|
||||
@@ -346,12 +347,12 @@ fn validate_tls_handshake_at_time_with_boot_cap(
|
||||
if handshake.len() < TLS_DIGEST_POS + TLS_DIGEST_LEN + 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
// Extract digest
|
||||
let digest: [u8; TLS_DIGEST_LEN] = handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||
.try_into()
|
||||
.ok()?;
|
||||
|
||||
|
||||
// Extract session ID
|
||||
let session_id_len_pos = TLS_DIGEST_POS + TLS_DIGEST_LEN;
|
||||
let session_id_len = handshake.get(session_id_len_pos).copied()? as usize;
|
||||
@@ -359,17 +360,17 @@ fn validate_tls_handshake_at_time_with_boot_cap(
|
||||
return None;
|
||||
}
|
||||
let session_id_start = session_id_len_pos + 1;
|
||||
|
||||
|
||||
if handshake.len() < session_id_start + session_id_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
let session_id = handshake[session_id_start..session_id_start + session_id_len].to_vec();
|
||||
|
||||
|
||||
// Build message for HMAC (with zeroed digest)
|
||||
let mut msg = handshake.to_vec();
|
||||
msg[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].fill(0);
|
||||
|
||||
|
||||
let mut first_match: Option<(&String, u32)> = None;
|
||||
|
||||
for (user, secret) in secrets {
|
||||
@@ -408,7 +409,7 @@ fn validate_tls_handshake_at_time_with_boot_cap(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if first_match.is_none() {
|
||||
first_match = Some((user, timestamp));
|
||||
}
|
||||
@@ -453,25 +454,30 @@ pub fn build_server_hello(
|
||||
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||
let fake_cert_len = fake_cert_len.clamp(MIN_APP_DATA, MAX_APP_DATA);
|
||||
let x25519_key = gen_fake_x25519_key(rng);
|
||||
|
||||
|
||||
// Build ServerHello
|
||||
let server_hello = ServerHelloBuilder::new(session_id.to_vec())
|
||||
.with_x25519_key(&x25519_key)
|
||||
.with_tls13_version()
|
||||
.build_record();
|
||||
|
||||
|
||||
// Build Change Cipher Spec record
|
||||
let change_cipher_spec = [
|
||||
TLS_RECORD_CHANGE_CIPHER,
|
||||
TLS_VERSION[0], TLS_VERSION[1],
|
||||
0x00, 0x01, // length = 1
|
||||
0x01, // CCS byte
|
||||
TLS_VERSION[0],
|
||||
TLS_VERSION[1],
|
||||
0x00,
|
||||
0x01, // length = 1
|
||||
0x01, // CCS byte
|
||||
];
|
||||
|
||||
|
||||
// Build first encrypted flight mimic as opaque ApplicationData bytes.
|
||||
// Embed a compact EncryptedExtensions-like ALPN block when selected.
|
||||
let mut fake_cert = Vec::with_capacity(fake_cert_len);
|
||||
if let Some(proto) = alpn.as_ref().filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize) {
|
||||
if let Some(proto) = alpn
|
||||
.as_ref()
|
||||
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize)
|
||||
{
|
||||
let proto_list_len = 1usize + proto.len();
|
||||
let ext_data_len = 2usize + proto_list_len;
|
||||
let marker_len = 4usize + ext_data_len;
|
||||
@@ -496,7 +502,7 @@ pub fn build_server_hello(
|
||||
// Fill ApplicationData with fully random bytes of desired length to avoid
|
||||
// deterministic DPI fingerprints (fixed inner content type markers).
|
||||
app_data_record.extend_from_slice(&fake_cert);
|
||||
|
||||
|
||||
// Build optional NewSessionTicket records (TLS 1.3 handshake messages are encrypted;
|
||||
// here we mimic with opaque ApplicationData records of plausible size).
|
||||
let mut tickets = Vec::new();
|
||||
@@ -515,7 +521,10 @@ pub fn build_server_hello(
|
||||
|
||||
// Combine all records
|
||||
let mut response = Vec::with_capacity(
|
||||
server_hello.len() + change_cipher_spec.len() + app_data_record.len() + tickets.iter().map(|r| r.len()).sum::<usize>()
|
||||
server_hello.len()
|
||||
+ change_cipher_spec.len()
|
||||
+ app_data_record.len()
|
||||
+ tickets.iter().map(|r| r.len()).sum::<usize>(),
|
||||
);
|
||||
response.extend_from_slice(&server_hello);
|
||||
response.extend_from_slice(&change_cipher_spec);
|
||||
@@ -523,18 +532,17 @@ pub fn build_server_hello(
|
||||
for t in &tickets {
|
||||
response.extend_from_slice(t);
|
||||
}
|
||||
|
||||
|
||||
// Compute HMAC for the response
|
||||
let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + response.len());
|
||||
hmac_input.extend_from_slice(client_digest);
|
||||
hmac_input.extend_from_slice(&response);
|
||||
let response_digest = sha256_hmac(secret, &hmac_input);
|
||||
|
||||
|
||||
// Insert computed digest into ServerHello
|
||||
// Position: record header (5) + message type (1) + length (3) + version (2) = 11
|
||||
response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||
.copy_from_slice(&response_digest);
|
||||
|
||||
response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&response_digest);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
@@ -611,12 +619,14 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
|
||||
let sn_end = std::cmp::min(sn_pos + list_len, pos + elen);
|
||||
while sn_pos + 3 <= sn_end {
|
||||
let name_type = handshake[sn_pos];
|
||||
let name_len = u16::from_be_bytes([handshake[sn_pos + 1], handshake[sn_pos + 2]]) as usize;
|
||||
let name_len =
|
||||
u16::from_be_bytes([handshake[sn_pos + 1], handshake[sn_pos + 2]]) as usize;
|
||||
sn_pos += 3;
|
||||
if sn_pos + name_len > sn_end {
|
||||
break;
|
||||
}
|
||||
if name_type == 0 && name_len > 0
|
||||
if name_type == 0
|
||||
&& name_len > 0
|
||||
&& let Ok(host) = std::str::from_utf8(&handshake[sn_pos..sn_pos + name_len])
|
||||
{
|
||||
if is_valid_sni_hostname(host) {
|
||||
@@ -679,35 +689,49 @@ pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
||||
}
|
||||
pos += 4; // type + len
|
||||
pos += 2 + 32; // version + random
|
||||
if pos >= handshake.len() { return Vec::new(); }
|
||||
if pos >= handshake.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let session_id_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||
pos += 1 + session_id_len;
|
||||
if pos + 2 > handshake.len() { return Vec::new(); }
|
||||
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
||||
if pos + 2 > handshake.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||
pos += 2 + cipher_len;
|
||||
if pos >= handshake.len() { return Vec::new(); }
|
||||
if pos >= handshake.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let comp_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||
pos += 1 + comp_len;
|
||||
if pos + 2 > handshake.len() { return Vec::new(); }
|
||||
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
||||
if pos + 2 > handshake.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
let ext_end = pos + ext_len;
|
||||
if ext_end > handshake.len() { return Vec::new(); }
|
||||
if ext_end > handshake.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
while pos + 4 <= ext_end {
|
||||
let etype = u16::from_be_bytes([handshake[pos], handshake[pos+1]]);
|
||||
let elen = u16::from_be_bytes([handshake[pos+2], handshake[pos+3]]) as usize;
|
||||
let etype = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]);
|
||||
let elen = u16::from_be_bytes([handshake[pos + 2], handshake[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
if pos + elen > ext_end { break; }
|
||||
if pos + elen > ext_end {
|
||||
break;
|
||||
}
|
||||
if etype == extension_type::ALPN && elen >= 3 {
|
||||
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
||||
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||
let mut lp = pos + 2;
|
||||
let list_end = (pos + 2).saturating_add(list_len).min(pos + elen);
|
||||
while lp < list_end {
|
||||
let plen = handshake[lp] as usize;
|
||||
lp += 1;
|
||||
if lp + plen > list_end { break; }
|
||||
out.push(handshake[lp..lp+plen].to_vec());
|
||||
if lp + plen > list_end {
|
||||
break;
|
||||
}
|
||||
out.push(handshake[lp..lp + plen].to_vec());
|
||||
lp += plen;
|
||||
}
|
||||
break;
|
||||
@@ -717,16 +741,15 @@ pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
/// Check if bytes look like a TLS ClientHello
|
||||
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
||||
if first_bytes.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// TLS ClientHello commonly uses legacy record versions 0x0301 or 0x0303.
|
||||
first_bytes[0] == TLS_RECORD_HANDSHAKE
|
||||
&& first_bytes[1] == 0x03
|
||||
first_bytes[0] == TLS_RECORD_HANDSHAKE
|
||||
&& first_bytes[1] == 0x03
|
||||
&& (first_bytes[2] == 0x01 || first_bytes[2] == 0x03)
|
||||
}
|
||||
|
||||
@@ -735,12 +758,12 @@ pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
||||
pub fn parse_tls_record_header(header: &[u8; 5]) -> Option<(u8, u16)> {
|
||||
let record_type = header[0];
|
||||
let version = [header[1], header[2]];
|
||||
|
||||
|
||||
// We accept both TLS 1.0 header (for ClientHello) and TLS 1.2/1.3
|
||||
if version != [0x03, 0x01] && version != TLS_VERSION {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
let length = u16::from_be_bytes([header[3], header[4]]);
|
||||
Some((record_type, length))
|
||||
}
|
||||
@@ -756,7 +779,7 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
||||
version: [0, 0],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check record header
|
||||
if data[0] != TLS_RECORD_HANDSHAKE {
|
||||
return Err(ProxyError::InvalidTlsRecord {
|
||||
@@ -764,7 +787,7 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
||||
version: [data[1], data[2]],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check version
|
||||
if data[1..3] != TLS_VERSION {
|
||||
return Err(ProxyError::InvalidTlsRecord {
|
||||
@@ -772,31 +795,34 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
||||
version: [data[1], data[2]],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check record length
|
||||
let record_len = u16::from_be_bytes([data[3], data[4]]) as usize;
|
||||
if data.len() < 5 + record_len {
|
||||
return Err(ProxyError::InvalidHandshake(
|
||||
format!("ServerHello record truncated: expected {}, got {}",
|
||||
5 + record_len, data.len())
|
||||
));
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"ServerHello record truncated: expected {}, got {}",
|
||||
5 + record_len,
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
// Check message type
|
||||
if data[5] != 0x02 {
|
||||
return Err(ProxyError::InvalidHandshake(
|
||||
format!("Expected ServerHello (0x02), got 0x{:02x}", data[5])
|
||||
));
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Expected ServerHello (0x02), got 0x{:02x}",
|
||||
data[5]
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
// Parse message length
|
||||
let msg_len = u32::from_be_bytes([0, data[6], data[7], data[8]]) as usize;
|
||||
if msg_len + 4 != record_len {
|
||||
return Err(ProxyError::InvalidHandshake(
|
||||
format!("Message length mismatch: {} + 4 != {}", msg_len, record_len)
|
||||
));
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Message length mismatch: {} + 4 != {}",
|
||||
msg_len, record_len
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -806,7 +832,7 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
||||
/// Using `static_assertions` ensures these can never silently break across
|
||||
/// refactors without a compile error.
|
||||
mod compile_time_security_checks {
|
||||
use super::{TLS_DIGEST_LEN, TLS_DIGEST_HALF_LEN};
|
||||
use super::{TLS_DIGEST_HALF_LEN, TLS_DIGEST_LEN};
|
||||
use static_assertions::const_assert;
|
||||
|
||||
// The digest must be exactly one SHA-256 output.
|
||||
|
||||
Reference in New Issue
Block a user