mirror of
https://github.com/telemt/telemt.git
synced 2026-06-11 21:41:43 +03:00
181
Cargo.lock
generated
181
Cargo.lock
generated
@@ -8,7 +8,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"crypto-common 0.1.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
@@ -249,6 +249,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
@@ -397,7 +406,7 @@ version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"crypto-common 0.1.7",
|
||||
"inout",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -436,6 +445,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -452,6 +467,12 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
@@ -611,6 +632,16 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
@@ -620,6 +651,15 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -672,7 +712,17 @@ version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"const-oid 0.9.6",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b"
|
||||
dependencies = [
|
||||
"const-oid 0.10.2",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -705,11 +755,21 @@ version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.0",
|
||||
"crypto-common 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
@@ -753,7 +813,7 @@ version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"pkcs8 0.10.2",
|
||||
"signature",
|
||||
]
|
||||
|
||||
@@ -1135,7 +1195,7 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1183,6 +1243,17 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
|
||||
dependencies = [
|
||||
"ctutils",
|
||||
"typenum",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.0"
|
||||
@@ -1532,6 +1603,26 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kem"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd"
|
||||
dependencies = [
|
||||
"crypto-common 0.2.2",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
@@ -1634,7 +1725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1670,6 +1761,33 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-kem"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
"kem",
|
||||
"module-lattice",
|
||||
"pkcs8 0.11.0",
|
||||
"rand_core 0.10.1",
|
||||
"sha3",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-lattice"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe"
|
||||
dependencies = [
|
||||
"ctutils",
|
||||
"hybrid-array",
|
||||
"num-traits",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.15"
|
||||
@@ -1888,8 +2006,18 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
"der 0.7.10",
|
||||
"spki 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
|
||||
dependencies = [
|
||||
"der 0.8.0",
|
||||
"spki 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2280,7 +2408,7 @@ dependencies = [
|
||||
"aead",
|
||||
"ed25519",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"pkcs8 0.10.2",
|
||||
"ring",
|
||||
]
|
||||
|
||||
@@ -2567,7 +2695,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2578,7 +2706,17 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha3"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1"
|
||||
dependencies = [
|
||||
"digest 0.11.3",
|
||||
"keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2724,7 +2862,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
"der 0.7.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2790,7 +2938,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "telemt"
|
||||
version = "3.4.15"
|
||||
version = "3.4.16"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
@@ -2816,6 +2964,7 @@ dependencies = [
|
||||
"libc",
|
||||
"lru",
|
||||
"md-5",
|
||||
"ml-kem",
|
||||
"nix",
|
||||
"notify",
|
||||
"num-bigint",
|
||||
@@ -3259,7 +3408,7 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telemt"
|
||||
version = "3.4.15"
|
||||
version = "3.4.16"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
@@ -27,6 +27,7 @@ crc32c = "0.6"
|
||||
zeroize = { version = "1.8", features = ["derive"] }
|
||||
subtle = "2.6"
|
||||
static_assertions = "1.1"
|
||||
ml-kem = { version = "0.3.2", default-features = false, features = ["alloc", "zeroize"] }
|
||||
|
||||
# Network
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
|
||||
@@ -620,6 +620,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
||||
|| old.censorship.tls_domains != new.censorship.tls_domains
|
||||
|| old.censorship.tls_fetch_scope != new.censorship.tls_fetch_scope
|
||||
|| old.censorship.mask != new.censorship.mask
|
||||
|| old.censorship.mask_dynamic != new.censorship.mask_dynamic
|
||||
|| old.censorship.mask_host != new.censorship.mask_host
|
||||
|| old.censorship.mask_port != new.censorship.mask_port
|
||||
|| old.censorship.exclusive_mask != new.censorship.exclusive_mask
|
||||
|
||||
@@ -372,6 +372,7 @@ const CENSORSHIP_CONFIG_KEYS: &[&str] = &[
|
||||
"tls_fetch_scope",
|
||||
"tls_fetch",
|
||||
"mask",
|
||||
"mask_dynamic",
|
||||
"mask_host",
|
||||
"mask_port",
|
||||
"exclusive_mask",
|
||||
@@ -2047,11 +2048,6 @@ impl ProxyConfig {
|
||||
*mask_host = normalize_mask_host_to_ascii(mask_host, "censorship.mask_host")?;
|
||||
}
|
||||
|
||||
// Default mask_host to tls_domain if not set and no unix socket configured.
|
||||
if config.censorship.mask_host.is_none() && config.censorship.mask_unix_sock.is_none() {
|
||||
config.censorship.mask_host = Some(config.censorship.tls_domain.clone());
|
||||
}
|
||||
|
||||
for (domain, target) in &config.censorship.exclusive_mask {
|
||||
if !is_valid_tls_domain_name(domain) {
|
||||
return Err(ProxyError::Config(format!(
|
||||
|
||||
@@ -1726,6 +1726,10 @@ pub struct AntiCensorshipConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub mask: bool,
|
||||
|
||||
/// Use the ClientHello SNI as the mask TCP target for configured TLS domains.
|
||||
#[serde(default = "default_true")]
|
||||
pub mask_dynamic: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub mask_host: Option<String>,
|
||||
|
||||
@@ -1861,6 +1865,7 @@ impl Default for AntiCensorshipConfig {
|
||||
tls_fetch_scope: default_tls_fetch_scope(),
|
||||
tls_fetch: TlsFetchConfig::default(),
|
||||
mask: default_true(),
|
||||
mask_dynamic: default_true(),
|
||||
mask_host: None,
|
||||
mask_port: default_mask_port(),
|
||||
exclusive_mask: HashMap::new(),
|
||||
|
||||
@@ -381,11 +381,32 @@ async fn render_tls_front_profile_health(
|
||||
"# HELP telemt_tls_front_profile_info TLS front profile source and feature flags per configured domain"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_info gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_quality_info TLS front profile quality and key-share group per configured domain"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_quality_info gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_age_seconds Age of cached TLS front profile data per configured domain"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_tls_front_profile_age_seconds gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_server_hello_bytes TLS front cached ServerHello record body bytes per configured domain"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_tls_front_profile_server_hello_bytes gauge"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_server_hello_extensions TLS front cached visible ServerHello extension count per configured domain"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# TYPE telemt_tls_front_profile_server_hello_extensions gauge"
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_tls_front_profile_app_data_records TLS front cached app-data record count per configured domain"
|
||||
@@ -420,11 +441,26 @@ async fn render_tls_front_profile_health(
|
||||
"telemt_tls_front_profile_info{{domain=\"{}\",source=\"{}\",is_default=\"{}\",has_cert_info=\"{}\",has_cert_payload=\"{}\"}} 1",
|
||||
domain, item.source, item.is_default, item.has_cert_info, item.has_cert_payload
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_quality_info{{domain=\"{}\",quality=\"{}\",key_share_group=\"{}\"}} 1",
|
||||
domain, item.quality, item.key_share_group
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_age_seconds{{domain=\"{}\"}} {}",
|
||||
domain, item.age_seconds
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_server_hello_bytes{{domain=\"{}\"}} {}",
|
||||
domain, item.server_hello_record_len
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_server_hello_extensions{{domain=\"{}\"}} {}",
|
||||
domain, item.server_hello_extensions
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_tls_front_profile_app_data_records{{domain=\"{}\"}} {}",
|
||||
@@ -3901,7 +3937,20 @@ mod tests {
|
||||
session_id: Vec::new(),
|
||||
cipher_suite: [0x13, 0x01],
|
||||
compression: 0,
|
||||
extensions: Vec::new(),
|
||||
extensions: {
|
||||
let mut key_share = vec![0x00, 0x1d, 0x00, 0x20];
|
||||
key_share.resize(36, 0x42);
|
||||
vec![
|
||||
crate::tls_front::types::TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
crate::tls_front::types::TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: key_share,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
cert_info: None,
|
||||
cert_payload: Some(TlsCertPayload {
|
||||
@@ -3915,6 +3964,7 @@ mod tests {
|
||||
app_data_record_sizes: vec![1024, 512],
|
||||
ticket_record_sizes: vec![69],
|
||||
source: TlsProfileSource::Merged,
|
||||
..TlsBehaviorProfile::default()
|
||||
},
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: "primary.example".to_string(),
|
||||
@@ -3933,6 +3983,22 @@ mod tests {
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_info{domain=\"fallback.example\",source=\"default\",is_default=\"true\",has_cert_info=\"false\",has_cert_payload=\"false\"} 1")
|
||||
);
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_quality_info{domain=\"primary.example\",quality=\"raw_strict\",key_share_group=\"x25519\"} 1")
|
||||
);
|
||||
assert!(
|
||||
output.contains("telemt_tls_front_profile_quality_info{domain=\"fallback.example\",quality=\"fallback\",key_share_group=\"none\"} 1")
|
||||
);
|
||||
assert!(
|
||||
output.contains(
|
||||
"telemt_tls_front_profile_server_hello_bytes{domain=\"primary.example\"} 90"
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
output.contains(
|
||||
"telemt_tls_front_profile_server_hello_extensions{domain=\"primary.example\"} 2"
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
output.contains(
|
||||
"telemt_tls_front_profile_app_data_records{domain=\"primary.example\"} 2"
|
||||
@@ -4045,7 +4111,10 @@ mod tests {
|
||||
);
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_domains gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_info gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_quality_info gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_age_seconds gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_server_hello_bytes gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_server_hello_extensions gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_app_data_records gauge"));
|
||||
assert!(output.contains("# TYPE telemt_tls_front_profile_ticket_records gauge"));
|
||||
assert!(
|
||||
|
||||
@@ -1239,6 +1239,18 @@ fn test_gen_fake_x25519_key() {
|
||||
assert_ne!(key1, key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gen_fake_x25519mlkem768_server_key_share_shape() {
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
let key_share = gen_fake_x25519mlkem768_server_key_share(&rng);
|
||||
|
||||
assert_eq!(key_share.len(), X25519MLKEM768_SERVER_KEY_SHARE_LEN);
|
||||
assert!(
|
||||
key_share.iter().any(|byte| *byte != 0),
|
||||
"hybrid ServerHello key_share must not collapse to all-zero bytes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fake_x25519_key_is_nonzero_and_varies() {
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
@@ -1325,6 +1337,69 @@ fn server_hello_extension_types(record: &[u8]) -> Vec<u16> {
|
||||
out
|
||||
}
|
||||
|
||||
fn server_hello_key_share(record: &[u8]) -> Option<(u16, usize)> {
|
||||
if record.len() < 9 || record[0] != TLS_RECORD_HANDSHAKE || record[5] != 0x02 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let record_len = u16::from_be_bytes([record[3], record[4]]) as usize;
|
||||
if record.len() < 5 + record_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 None;
|
||||
}
|
||||
|
||||
let mut pos = hs_start + 4 + 2 + 32;
|
||||
if pos >= hs_end {
|
||||
return None;
|
||||
}
|
||||
let sid_len = record[pos] as usize;
|
||||
pos += 1 + sid_len;
|
||||
if pos + 2 + 1 + 2 > hs_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 None;
|
||||
}
|
||||
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
if etype == extension_type::KEY_SHARE {
|
||||
if elen < 4 {
|
||||
return None;
|
||||
}
|
||||
let group = u16::from_be_bytes([record[pos], record[pos + 1]]);
|
||||
let key_exchange_len = u16::from_be_bytes([record[pos + 2], record[pos + 3]]) as usize;
|
||||
if 4 + key_exchange_len != elen {
|
||||
return None;
|
||||
}
|
||||
return Some((group, key_exchange_len));
|
||||
}
|
||||
pos += elen;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn test_server_key_share(group: u16, len: usize) -> ServerHelloKeyShare {
|
||||
ServerHelloKeyShare::new(group, vec![0x42; len])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_server_hello_never_places_alpn_in_server_hello_extensions() {
|
||||
let secret = b"alpn_sh_forbidden";
|
||||
@@ -1372,6 +1447,7 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
|
||||
app_data_record_sizes: vec![1024],
|
||||
ticket_record_sizes: Vec::new(),
|
||||
source: TlsProfileSource::Default,
|
||||
..TlsBehaviorProfile::default()
|
||||
},
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: "example.com".to_string(),
|
||||
@@ -1386,6 +1462,10 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_SERVER_KEY_SHARE_LEN,
|
||||
),
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -1395,14 +1475,21 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
|
||||
!exts.contains(&0x0010),
|
||||
"ALPN extension must not appear in emulated ServerHello"
|
||||
);
|
||||
assert_eq!(
|
||||
server_hello_key_share(&response),
|
||||
Some((
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_SERVER_KEY_SHARE_LEN
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tls_extension_builder() {
|
||||
let key = [0x42u8; 32];
|
||||
let key = vec![0x42u8; X25519MLKEM768_SERVER_KEY_SHARE_LEN];
|
||||
|
||||
let mut builder = TlsExtensionBuilder::new();
|
||||
builder.add_key_share(&key);
|
||||
builder.add_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key);
|
||||
builder.add_supported_versions(0x0304);
|
||||
|
||||
let result = builder.build();
|
||||
@@ -1415,10 +1502,10 @@ fn test_tls_extension_builder() {
|
||||
#[test]
|
||||
fn test_server_hello_builder() {
|
||||
let session_id = vec![0x01, 0x02, 0x03, 0x04];
|
||||
let key = [0x55u8; 32];
|
||||
let key = vec![0x55u8; X25519MLKEM768_SERVER_KEY_SHARE_LEN];
|
||||
|
||||
let builder = ServerHelloBuilder::new(session_id.clone())
|
||||
.with_x25519_key(&key)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key)
|
||||
.with_tls13_version();
|
||||
|
||||
let record = builder.build_record();
|
||||
@@ -1452,6 +1539,41 @@ fn test_build_server_hello_structure() {
|
||||
let app_start = ccs_start + ccs_len;
|
||||
assert!(response.len() > app_start + 5);
|
||||
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
|
||||
|
||||
assert_eq!(
|
||||
server_hello_key_share(&response),
|
||||
Some((
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_SERVER_KEY_SHARE_LEN
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_server_hello_with_cipher_uses_selected_key_share_group() {
|
||||
let secret = b"test secret";
|
||||
let client_digest = [0x42u8; 32];
|
||||
let session_id = vec![0xAA; 32];
|
||||
let key_share =
|
||||
ServerHelloKeyShare::new(TLS_NAMED_GROUP_X25519, vec![0x55u8; X25519_KEY_SHARE_LEN]);
|
||||
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
let response = build_server_hello_with_cipher(
|
||||
secret,
|
||||
&client_digest,
|
||||
&session_id,
|
||||
2048,
|
||||
&rng,
|
||||
[0x13, 0x01],
|
||||
&key_share,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server_hello_key_share(&response),
|
||||
Some((TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1474,10 +1596,10 @@ fn test_build_server_hello_digest() {
|
||||
#[test]
|
||||
fn test_server_hello_extensions_length() {
|
||||
let session_id = vec![0x01; 32];
|
||||
let key = [0x55u8; 32];
|
||||
let key = vec![0x55u8; X25519MLKEM768_SERVER_KEY_SHARE_LEN];
|
||||
|
||||
let builder = ServerHelloBuilder::new(session_id)
|
||||
.with_x25519_key(&key)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key)
|
||||
.with_tls13_version();
|
||||
|
||||
let record = builder.build_record();
|
||||
@@ -1513,6 +1635,39 @@ fn build_client_hello_with_exts(exts: Vec<(u16, Vec<u8>)>, host: &str) -> Vec<u8
|
||||
build_client_hello_with_ciphers_and_exts(&[[0x13, 0x01]], exts, host)
|
||||
}
|
||||
|
||||
fn client_key_share_extension(entries: &[(u16, usize)]) -> Vec<u8> {
|
||||
let mut shares = Vec::new();
|
||||
for (group, key_exchange_len) in entries {
|
||||
assert!(*key_exchange_len <= u16::MAX as usize);
|
||||
shares.extend_from_slice(&group.to_be_bytes());
|
||||
shares.extend_from_slice(&(*key_exchange_len as u16).to_be_bytes());
|
||||
let start = shares.len();
|
||||
shares.resize(start + *key_exchange_len, 0x42);
|
||||
}
|
||||
|
||||
assert!(shares.len() <= u16::MAX as usize);
|
||||
let mut extension = Vec::new();
|
||||
extension.extend_from_slice(&(shares.len() as u16).to_be_bytes());
|
||||
extension.extend_from_slice(&shares);
|
||||
extension
|
||||
}
|
||||
|
||||
fn client_key_share_extension_with_payloads(entries: &[(u16, &[u8])]) -> Vec<u8> {
|
||||
let mut shares = Vec::new();
|
||||
for (group, key_exchange) in entries {
|
||||
assert!(key_exchange.len() <= u16::MAX as usize);
|
||||
shares.extend_from_slice(&group.to_be_bytes());
|
||||
shares.extend_from_slice(&(key_exchange.len() as u16).to_be_bytes());
|
||||
shares.extend_from_slice(key_exchange);
|
||||
}
|
||||
|
||||
assert!(shares.len() <= u16::MAX as usize);
|
||||
let mut extension = Vec::new();
|
||||
extension.extend_from_slice(&(shares.len() as u16).to_be_bytes());
|
||||
extension.extend_from_slice(&shares);
|
||||
extension
|
||||
}
|
||||
|
||||
fn build_client_hello_with_ciphers_and_exts(
|
||||
cipher_suites: &[[u8; 2]],
|
||||
exts: Vec<(u16, Vec<u8>)>,
|
||||
@@ -1674,7 +1829,7 @@ fn select_server_hello_cipher_suite_keeps_profile_cipher_when_offered() {
|
||||
);
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x03]),
|
||||
[0x13, 0x03]
|
||||
Some([0x13, 0x03])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1687,30 +1842,222 @@ fn select_server_hello_cipher_suite_ignores_profile_tls12_cipher() {
|
||||
);
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0xc0, 0x2f]),
|
||||
[0x13, 0x03]
|
||||
Some([0x13, 0x03])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_rejects_without_offered_tls13_suite() {
|
||||
let ch = build_client_hello_with_ciphers_and_exts(&[[0xc0, 0x2f]], Vec::new(), "example.com");
|
||||
assert_eq!(select_server_hello_cipher_suite(&ch, [0x13, 0x01]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_falls_back_to_offered_tls13_suite() {
|
||||
let ch = build_client_hello_with_ciphers_and_exts(&[[0x13, 0x03]], Vec::new(), "example.com");
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x01]),
|
||||
[0x13, 0x03]
|
||||
Some([0x13, 0x03])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_keeps_preferred_for_malformed_clienthello() {
|
||||
fn select_server_hello_cipher_suite_rejects_malformed_clienthello() {
|
||||
let mut ch =
|
||||
build_client_hello_with_ciphers_and_exts(&[[0x13, 0x03]], Vec::new(), "example.com");
|
||||
ch.truncate(12);
|
||||
assert_eq!(select_server_hello_cipher_suite(&ch, [0x13, 0x01]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_prefers_hybrid_when_valid_share_is_offered() {
|
||||
let key_share = client_key_share_extension(&[
|
||||
(0x0a0a, 1),
|
||||
(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
),
|
||||
(TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN),
|
||||
]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x01]),
|
||||
[0x13, 0x01]
|
||||
select_server_hello_key_share_group(&ch),
|
||||
Some(TLS_NAMED_GROUP_X25519MLKEM768)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_prefers_profiled_x25519_when_valid_share_is_offered() {
|
||||
let key_share = client_key_share_extension(&[
|
||||
(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
),
|
||||
(TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN),
|
||||
]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(
|
||||
select_server_hello_key_share_group_with_preference(&ch, Some(TLS_NAMED_GROUP_X25519)),
|
||||
Some(TLS_NAMED_GROUP_X25519)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_x25519mlkem768_server_key_share_accepts_tdesktop_canonical_share() {
|
||||
let key_share = client_key_share_extension(&[
|
||||
(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
),
|
||||
(TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN),
|
||||
]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
|
||||
let server_key_share = build_x25519mlkem768_server_key_share(&ch, &rng)
|
||||
.expect("tdesktop-like canonical share must build a ServerHello share");
|
||||
|
||||
assert_eq!(server_key_share.len(), X25519MLKEM768_SERVER_KEY_SHARE_LEN);
|
||||
assert!(
|
||||
server_key_share[..MLKEM768_SERVER_CIPHERTEXT_LEN]
|
||||
.iter()
|
||||
.any(|byte| *byte != 0),
|
||||
"ML-KEM ciphertext must not be all zero"
|
||||
);
|
||||
assert!(
|
||||
server_key_share[MLKEM768_SERVER_CIPHERTEXT_LEN..]
|
||||
.iter()
|
||||
.any(|byte| *byte != 0),
|
||||
"X25519 server share must not be all zero"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_x25519_server_key_share_accepts_tdesktop_fallback_share() {
|
||||
let key_share = client_key_share_extension(&[
|
||||
(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
),
|
||||
(TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN),
|
||||
]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
|
||||
let server_key_share = build_x25519_server_key_share(&ch, &rng)
|
||||
.expect("tdesktop-like X25519 share must build a ServerHello share");
|
||||
|
||||
assert_eq!(server_key_share.len(), X25519_KEY_SHARE_LEN);
|
||||
assert!(
|
||||
server_key_share.iter().any(|byte| *byte != 0),
|
||||
"X25519 server share must not be all zero"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_server_hello_key_share_prefers_profiled_x25519() {
|
||||
let key_share = client_key_share_extension(&[
|
||||
(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
),
|
||||
(TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN),
|
||||
]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
|
||||
let server_key_share = build_server_hello_key_share(&ch, Some(TLS_NAMED_GROUP_X25519), &rng)
|
||||
.expect("profiled X25519 share must be selected when client offers it");
|
||||
|
||||
assert_eq!(server_key_share.group(), TLS_NAMED_GROUP_X25519);
|
||||
assert_eq!(server_key_share.key_exchange().len(), X25519_KEY_SHARE_LEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_server_hello_key_share_falls_back_from_bad_profiled_x25519_to_hybrid() {
|
||||
let key_share = client_key_share_extension(&[(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
)]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
|
||||
let server_key_share = build_server_hello_key_share(&ch, Some(TLS_NAMED_GROUP_X25519), &rng)
|
||||
.expect("hybrid share must be selected when profiled X25519 is unavailable");
|
||||
|
||||
assert_eq!(server_key_share.group(), TLS_NAMED_GROUP_X25519MLKEM768);
|
||||
assert_eq!(
|
||||
server_key_share.key_exchange().len(),
|
||||
X25519MLKEM768_SERVER_KEY_SHARE_LEN
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_x25519mlkem768_server_key_share_rejects_noncanonical_mlkem_key() {
|
||||
let mut key_exchange = vec![0x42; X25519MLKEM768_CLIENT_KEY_SHARE_LEN];
|
||||
key_exchange[..3].copy_from_slice(&[0xff, 0xff, 0xff]);
|
||||
let key_share = client_key_share_extension_with_payloads(&[(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
&key_exchange,
|
||||
)]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
|
||||
assert!(build_x25519mlkem768_server_key_share(&ch, &rng).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_x25519mlkem768_server_key_share_rejects_all_zero_x25519_share() {
|
||||
let mut key_exchange = vec![0x42; X25519MLKEM768_CLIENT_KEY_SHARE_LEN];
|
||||
key_exchange[MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN..].fill(0);
|
||||
let key_share = client_key_share_extension_with_payloads(&[(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
&key_exchange,
|
||||
)]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
|
||||
assert!(build_x25519mlkem768_server_key_share(&ch, &rng).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_accepts_x25519_when_hybrid_is_absent() {
|
||||
let key_share = client_key_share_extension(&[(TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN)]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(
|
||||
select_server_hello_key_share_group(&ch),
|
||||
Some(TLS_NAMED_GROUP_X25519)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_rejects_malformed_hybrid_len() {
|
||||
let key_share = client_key_share_extension(&[(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN - 1,
|
||||
)]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(select_server_hello_key_share_group(&ch), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_rejects_malformed_key_share_tail() {
|
||||
let mut key_share = client_key_share_extension(&[(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
)]);
|
||||
let shares_len = u16::from_be_bytes([key_share[0], key_share[1]]) + 1;
|
||||
key_share[0..2].copy_from_slice(&shares_len.to_be_bytes());
|
||||
key_share.push(0);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(select_server_hello_key_share_group(&ch), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_sni_rejects_zero_length_host_name() {
|
||||
let mut sni_ext = Vec::new();
|
||||
|
||||
@@ -65,6 +65,7 @@ use super::constants::*;
|
||||
use crate::crypto::{SecureRandom, sha256_hmac};
|
||||
#[cfg(test)]
|
||||
use crate::error::ProxyError;
|
||||
use ml_kem::{B32, EncapsulationKey as MlKemEncapsulationKey, Key as MlKemKey, MlKem768};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use subtle::ConstantTimeEq;
|
||||
use x25519_dalek::{X25519_BASEPOINT_BYTES, x25519};
|
||||
@@ -109,9 +110,45 @@ mod cipher_suite {
|
||||
pub const TLS_CHACHA20_POLY1305_SHA256: [u8; 2] = [0x13, 0x03];
|
||||
}
|
||||
|
||||
/// TLS Named Curves
|
||||
/// TLS named groups used in KeyShare extensions.
|
||||
mod named_curve {
|
||||
pub const X25519: u16 = 0x001d;
|
||||
pub const X25519MLKEM768: u16 = 0x11ec;
|
||||
}
|
||||
|
||||
/// TLS X25519 named group.
|
||||
pub(crate) const TLS_NAMED_GROUP_X25519: u16 = named_curve::X25519;
|
||||
/// TLS X25519MLKEM768 named group.
|
||||
pub(crate) const TLS_NAMED_GROUP_X25519MLKEM768: u16 = named_curve::X25519MLKEM768;
|
||||
|
||||
const X25519_KEY_SHARE_LEN: usize = 32;
|
||||
const X25519MLKEM768_CLIENT_KEY_SHARE_LEN: usize = 1216;
|
||||
const X25519MLKEM768_SERVER_KEY_SHARE_LEN: usize = 1120;
|
||||
const MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN: usize = 1184;
|
||||
const MLKEM768_SERVER_CIPHERTEXT_LEN: usize = 1088;
|
||||
|
||||
/// ServerHello key_share selected for the authenticated ClientHello.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ServerHelloKeyShare {
|
||||
group: u16,
|
||||
key_exchange: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ServerHelloKeyShare {
|
||||
pub(crate) fn new(group: u16, key_exchange: Vec<u8>) -> Self {
|
||||
Self {
|
||||
group,
|
||||
key_exchange,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn group(&self) -> u16 {
|
||||
self.group
|
||||
}
|
||||
|
||||
pub(crate) fn key_exchange(&self) -> &[u8] {
|
||||
&self.key_exchange
|
||||
}
|
||||
}
|
||||
|
||||
// ============= TLS Validation Result =============
|
||||
@@ -144,26 +181,28 @@ impl TlsExtensionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Key Share extension with X25519 key
|
||||
fn add_key_share(&mut self, public_key: &[u8; 32]) -> &mut Self {
|
||||
/// Add KeyShare extension with the selected named group.
|
||||
fn add_key_share(&mut self, group: u16, key_exchange: &[u8]) -> &mut Self {
|
||||
let Ok(key_exchange_len) = u16::try_from(key_exchange.len()) else {
|
||||
return self;
|
||||
};
|
||||
let Some(entry_len) = key_exchange.len().checked_add(4) else {
|
||||
return self;
|
||||
};
|
||||
let Ok(entry_len) = u16::try_from(entry_len) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
// Extension type: key_share (0x0033)
|
||||
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
|
||||
// ServerHello key_share data is exactly one KeyShareEntry.
|
||||
self.extensions.extend_from_slice(&entry_len.to_be_bytes());
|
||||
|
||||
// Named curve: x25519
|
||||
self.extensions.extend_from_slice(&group.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);
|
||||
.extend_from_slice(&key_exchange_len.to_be_bytes());
|
||||
self.extensions.extend_from_slice(key_exchange);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -232,8 +271,8 @@ impl ServerHelloBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_x25519_key(mut self, key: &[u8; 32]) -> Self {
|
||||
self.extensions.add_key_share(key);
|
||||
fn with_key_share(mut self, group: u16, key_exchange: &[u8]) -> Self {
|
||||
self.extensions.add_key_share(group, key_exchange);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -508,9 +547,131 @@ fn validate_tls_handshake_at_time_with_boot_cap(
|
||||
/// Uses RFC 7748 X25519 scalar multiplication over the canonical basepoint,
|
||||
/// yielding distribution-consistent public keys for anti-fingerprinting.
|
||||
pub fn gen_fake_x25519_key(rng: &SecureRandom) -> [u8; 32] {
|
||||
let mut scalar = [0u8; 32];
|
||||
scalar.copy_from_slice(&rng.bytes(32));
|
||||
x25519(scalar, X25519_BASEPOINT_BYTES)
|
||||
let (_scalar, public_key) = gen_x25519_key_pair(rng);
|
||||
public_key
|
||||
}
|
||||
|
||||
fn gen_x25519_key_pair(rng: &SecureRandom) -> ([u8; 32], [u8; 32]) {
|
||||
let mut scalar = [0u8; X25519_KEY_SHARE_LEN];
|
||||
rng.fill(&mut scalar);
|
||||
let public_key = x25519(scalar, X25519_BASEPOINT_BYTES);
|
||||
(scalar, public_key)
|
||||
}
|
||||
|
||||
/// Generate a fake X25519MLKEM768 ServerHello key_share payload.
|
||||
pub(crate) fn gen_fake_x25519mlkem768_server_key_share(rng: &SecureRandom) -> Vec<u8> {
|
||||
let mut key_share = vec![0u8; X25519MLKEM768_SERVER_KEY_SHARE_LEN];
|
||||
// FakeTLS never derives TLS traffic secrets from this payload; only the
|
||||
// externally visible named group and vector lengths are protocol-facing.
|
||||
rng.fill(&mut key_share[..MLKEM768_SERVER_CIPHERTEXT_LEN]);
|
||||
let x25519_key = gen_fake_x25519_key(rng);
|
||||
key_share[MLKEM768_SERVER_CIPHERTEXT_LEN..].copy_from_slice(&x25519_key);
|
||||
key_share
|
||||
}
|
||||
|
||||
fn mlkem768_encapsulate_to_client(client_key: &[u8], rng: &SecureRandom) -> Option<Vec<u8>> {
|
||||
let key_bytes = MlKemKey::<MlKemEncapsulationKey<MlKem768>>::try_from(client_key).ok()?;
|
||||
let encapsulation_key = MlKemEncapsulationKey::<MlKem768>::new(&key_bytes).ok()?;
|
||||
let mut randomness = [0u8; 32];
|
||||
rng.fill(&mut randomness);
|
||||
let randomness = B32::try_from(randomness.as_slice()).ok()?;
|
||||
let (ciphertext, _shared_key) = encapsulation_key.encapsulate_deterministic(&randomness);
|
||||
let ciphertext = ciphertext.as_slice().to_vec();
|
||||
if ciphertext.len() == MLKEM768_SERVER_CIPHERTEXT_LEN {
|
||||
Some(ciphertext)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a valid X25519MLKEM768 ServerHello key_share for the authenticated ClientHello.
|
||||
pub(crate) fn build_x25519mlkem768_server_key_share(
|
||||
handshake: &[u8],
|
||||
rng: &SecureRandom,
|
||||
) -> Option<Vec<u8>> {
|
||||
let client_key_exchange = client_hello_key_share_group_entry(
|
||||
handshake,
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
)?;
|
||||
let client_mlkem_key = client_key_exchange.get(..MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN)?;
|
||||
let client_x25519_key = client_key_exchange.get(MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN..)?;
|
||||
let mlkem_ciphertext = mlkem768_encapsulate_to_client(client_mlkem_key, rng)?;
|
||||
|
||||
let mut client_x25519 = [0u8; X25519_KEY_SHARE_LEN];
|
||||
client_x25519.copy_from_slice(client_x25519_key);
|
||||
let (server_x25519_scalar, server_x25519_key) = gen_x25519_key_pair(rng);
|
||||
let x25519_shared = x25519(server_x25519_scalar, client_x25519);
|
||||
if bool::from(x25519_shared.ct_eq(&[0u8; X25519_KEY_SHARE_LEN])) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut key_share = Vec::with_capacity(X25519MLKEM768_SERVER_KEY_SHARE_LEN);
|
||||
key_share.extend_from_slice(&mlkem_ciphertext);
|
||||
key_share.extend_from_slice(&server_x25519_key);
|
||||
Some(key_share)
|
||||
}
|
||||
|
||||
/// Build a valid X25519 ServerHello key_share for the authenticated ClientHello.
|
||||
pub(crate) fn build_x25519_server_key_share(
|
||||
handshake: &[u8],
|
||||
rng: &SecureRandom,
|
||||
) -> Option<Vec<u8>> {
|
||||
let client_key_exchange = client_hello_key_share_group_entry(
|
||||
handshake,
|
||||
TLS_NAMED_GROUP_X25519,
|
||||
X25519_KEY_SHARE_LEN,
|
||||
)?;
|
||||
let mut client_x25519 = [0u8; X25519_KEY_SHARE_LEN];
|
||||
client_x25519.copy_from_slice(client_key_exchange);
|
||||
let (server_x25519_scalar, server_x25519_key) = gen_x25519_key_pair(rng);
|
||||
let x25519_shared = x25519(server_x25519_scalar, client_x25519);
|
||||
if bool::from(x25519_shared.ct_eq(&[0u8; X25519_KEY_SHARE_LEN])) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(server_x25519_key.to_vec())
|
||||
}
|
||||
|
||||
fn build_server_hello_key_share_for_group(
|
||||
handshake: &[u8],
|
||||
group: u16,
|
||||
rng: &SecureRandom,
|
||||
) -> Option<ServerHelloKeyShare> {
|
||||
match group {
|
||||
TLS_NAMED_GROUP_X25519MLKEM768 => {
|
||||
let key_exchange = build_x25519mlkem768_server_key_share(handshake, rng)?;
|
||||
Some(ServerHelloKeyShare::new(group, key_exchange))
|
||||
}
|
||||
TLS_NAMED_GROUP_X25519 => {
|
||||
let key_exchange = build_x25519_server_key_share(handshake, rng)?;
|
||||
Some(ServerHelloKeyShare::new(group, key_exchange))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn server_hello_key_share_candidate_order(preferred_group: Option<u16>) -> [u16; 2] {
|
||||
if preferred_group == Some(TLS_NAMED_GROUP_X25519) {
|
||||
[TLS_NAMED_GROUP_X25519, TLS_NAMED_GROUP_X25519MLKEM768]
|
||||
} else {
|
||||
[TLS_NAMED_GROUP_X25519MLKEM768, TLS_NAMED_GROUP_X25519]
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a ServerHello key_share using a profile-preferred group when possible.
|
||||
pub(crate) fn build_server_hello_key_share(
|
||||
handshake: &[u8],
|
||||
preferred_group: Option<u16>,
|
||||
rng: &SecureRandom,
|
||||
) -> Option<ServerHelloKeyShare> {
|
||||
for group in server_hello_key_share_candidate_order(preferred_group) {
|
||||
if let Some(key_share) = build_server_hello_key_share_for_group(handshake, group, rng) {
|
||||
return Some(key_share);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Build TLS ServerHello response
|
||||
@@ -530,6 +691,10 @@ pub fn build_server_hello(
|
||||
alpn: Option<Vec<u8>>,
|
||||
new_session_tickets: u8,
|
||||
) -> Vec<u8> {
|
||||
let server_key_share = ServerHelloKeyShare::new(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
gen_fake_x25519mlkem768_server_key_share(rng),
|
||||
);
|
||||
build_server_hello_with_cipher(
|
||||
secret,
|
||||
client_digest,
|
||||
@@ -537,6 +702,7 @@ pub fn build_server_hello(
|
||||
fake_cert_len,
|
||||
rng,
|
||||
cipher_suite::TLS_AES_128_GCM_SHA256,
|
||||
&server_key_share,
|
||||
alpn,
|
||||
new_session_tickets,
|
||||
)
|
||||
@@ -554,18 +720,18 @@ pub(crate) fn build_server_hello_with_cipher(
|
||||
fake_cert_len: usize,
|
||||
rng: &SecureRandom,
|
||||
selected_cipher_suite: [u8; 2],
|
||||
server_key_share: &ServerHelloKeyShare,
|
||||
alpn: Option<Vec<u8>>,
|
||||
new_session_tickets: u8,
|
||||
) -> Vec<u8> {
|
||||
const MIN_APP_DATA: usize = 64;
|
||||
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_cipher_suite(selected_cipher_suite)
|
||||
.with_x25519_key(&x25519_key)
|
||||
.with_key_share(server_key_share.group(), server_key_share.key_exchange())
|
||||
.with_tls13_version()
|
||||
.build_record();
|
||||
|
||||
@@ -1003,6 +1169,148 @@ fn client_hello_cipher_suites_range(handshake: &[u8]) -> Option<(usize, usize)>
|
||||
Some((pos, cipher_end))
|
||||
}
|
||||
|
||||
fn client_hello_extensions_range(handshake: &[u8]) -> Option<(usize, usize)> {
|
||||
if handshake.len() < 5 || handshake[0] != TLS_RECORD_HANDSHAKE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let record_len = u16::from_be_bytes([handshake[3], handshake[4]]) as usize;
|
||||
let record_end = 5usize.checked_add(record_len)?;
|
||||
if record_end > handshake.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut pos = 5;
|
||||
if handshake.get(pos) != Some(&0x01) {
|
||||
return None;
|
||||
}
|
||||
pos += 1;
|
||||
|
||||
if pos + 3 > record_end {
|
||||
return None;
|
||||
}
|
||||
let handshake_len = ((handshake[pos] as usize) << 16)
|
||||
| ((handshake[pos + 1] as usize) << 8)
|
||||
| handshake[pos + 2] as usize;
|
||||
pos += 3;
|
||||
let handshake_end = pos.checked_add(handshake_len)?;
|
||||
if handshake_end > record_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
if pos + 2 + 32 > handshake_end {
|
||||
return None;
|
||||
}
|
||||
pos += 2 + 32;
|
||||
|
||||
let session_id_len = *handshake.get(pos)? as usize;
|
||||
pos = pos.checked_add(1)?.checked_add(session_id_len)?;
|
||||
if pos + 2 > handshake_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||
if cipher_len == 0 || cipher_len % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
pos += 2;
|
||||
pos = pos.checked_add(cipher_len)?;
|
||||
if pos + 1 > handshake_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let compression_len = *handshake.get(pos)? as usize;
|
||||
pos = pos.checked_add(1)?.checked_add(compression_len)?;
|
||||
if pos == handshake_end {
|
||||
return Some((handshake_end, handshake_end));
|
||||
}
|
||||
if pos + 2 > handshake_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let extensions_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
let extensions_end = pos.checked_add(extensions_len)?;
|
||||
if extensions_end > handshake_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((pos, extensions_end))
|
||||
}
|
||||
|
||||
fn key_share_extension_group_entry<'a>(
|
||||
data: &'a [u8],
|
||||
group: u16,
|
||||
expected_key_exchange_len: usize,
|
||||
) -> Option<&'a [u8]> {
|
||||
if data.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let shares_len = u16::from_be_bytes([data[0], data[1]]) as usize;
|
||||
if shares_len != data.len().saturating_sub(2) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut pos = 2usize;
|
||||
let shares_end = 2 + shares_len;
|
||||
let mut found_group = None;
|
||||
while pos + 4 <= shares_end {
|
||||
let entry_group = u16::from_be_bytes([data[pos], data[pos + 1]]);
|
||||
let key_exchange_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
let Some(key_exchange_end) = pos.checked_add(key_exchange_len) else {
|
||||
return None;
|
||||
};
|
||||
if key_exchange_end > shares_end {
|
||||
return None;
|
||||
}
|
||||
if entry_group == group {
|
||||
if key_exchange_len != expected_key_exchange_len || found_group.is_some() {
|
||||
return None;
|
||||
}
|
||||
found_group = Some(&data[pos..key_exchange_end]);
|
||||
}
|
||||
pos = key_exchange_end;
|
||||
}
|
||||
|
||||
if pos == shares_end { found_group } else { None }
|
||||
}
|
||||
|
||||
fn client_hello_key_share_group_entry<'a>(
|
||||
handshake: &'a [u8],
|
||||
group: u16,
|
||||
expected_key_exchange_len: usize,
|
||||
) -> Option<&'a [u8]> {
|
||||
let Some((mut pos, extensions_end)) = client_hello_extensions_range(handshake) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
while pos + 4 <= extensions_end {
|
||||
let ext_type = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]);
|
||||
let ext_len = u16::from_be_bytes([handshake[pos + 2], handshake[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
let Some(ext_end) = pos.checked_add(ext_len) else {
|
||||
return None;
|
||||
};
|
||||
if ext_end > extensions_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
if ext_type == extension_type::KEY_SHARE {
|
||||
return key_share_extension_group_entry(
|
||||
&handshake[pos..ext_end],
|
||||
group,
|
||||
expected_key_exchange_len,
|
||||
);
|
||||
}
|
||||
|
||||
pos = ext_end;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn client_hello_offers_cipher_suite(
|
||||
handshake: &[u8],
|
||||
range: (usize, usize),
|
||||
@@ -1027,20 +1335,23 @@ fn is_tls13_cipher_suite(suite: [u8; 2]) -> bool {
|
||||
/// Select the ServerHello cipher suite from the already-received ClientHello.
|
||||
///
|
||||
/// This is intentionally a borrowed, zero-allocation scan. It runs only for an
|
||||
/// authenticated success response and keeps malformed or unexpected ClientHello
|
||||
/// shapes on the previous fallback behavior.
|
||||
pub(crate) fn select_server_hello_cipher_suite(handshake: &[u8], preferred: [u8; 2]) -> [u8; 2] {
|
||||
/// authenticated success response and fails closed for malformed or unsupported
|
||||
/// ClientHello shapes that cannot produce a DPI-consistent ServerHello.
|
||||
pub(crate) fn select_server_hello_cipher_suite(
|
||||
handshake: &[u8],
|
||||
preferred: [u8; 2],
|
||||
) -> Option<[u8; 2]> {
|
||||
let preferred = if is_tls13_cipher_suite(preferred) {
|
||||
preferred
|
||||
} else {
|
||||
cipher_suite::TLS_AES_128_GCM_SHA256
|
||||
};
|
||||
let Some(range) = client_hello_cipher_suites_range(handshake) else {
|
||||
return preferred;
|
||||
return None;
|
||||
};
|
||||
|
||||
if client_hello_offers_cipher_suite(handshake, range, preferred) {
|
||||
return preferred;
|
||||
return Some(preferred);
|
||||
}
|
||||
|
||||
for fallback in [
|
||||
@@ -1049,11 +1360,43 @@ pub(crate) fn select_server_hello_cipher_suite(handshake: &[u8], preferred: [u8;
|
||||
cipher_suite::TLS_AES_256_GCM_SHA384,
|
||||
] {
|
||||
if client_hello_offers_cipher_suite(handshake, range, fallback) {
|
||||
return fallback;
|
||||
return Some(fallback);
|
||||
}
|
||||
}
|
||||
|
||||
preferred
|
||||
None
|
||||
}
|
||||
|
||||
fn client_hello_key_share_group_len(group: u16) -> Option<usize> {
|
||||
match group {
|
||||
TLS_NAMED_GROUP_X25519MLKEM768 => Some(X25519MLKEM768_CLIENT_KEY_SHARE_LEN),
|
||||
TLS_NAMED_GROUP_X25519 => Some(X25519_KEY_SHARE_LEN),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the ServerHello key_share named group from the authenticated ClientHello.
|
||||
///
|
||||
/// Malformed key_share structures fail closed so authenticated but
|
||||
/// DPI-inconsistent ClientHellos take the ordinary masking fallback path.
|
||||
pub(crate) fn select_server_hello_key_share_group(handshake: &[u8]) -> Option<u16> {
|
||||
select_server_hello_key_share_group_with_preference(handshake, None)
|
||||
}
|
||||
|
||||
/// Select the ServerHello key_share named group with an origin-profile preference.
|
||||
pub(crate) fn select_server_hello_key_share_group_with_preference(
|
||||
handshake: &[u8],
|
||||
preferred_group: Option<u16>,
|
||||
) -> Option<u16> {
|
||||
for group in server_hello_key_share_candidate_order(preferred_group) {
|
||||
let expected_key_exchange_len = client_hello_key_share_group_len(group)?;
|
||||
if client_hello_key_share_group_entry(handshake, group, expected_key_exchange_len).is_some()
|
||||
{
|
||||
return Some(group);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if bytes look like a TLS ClientHello
|
||||
|
||||
@@ -1473,14 +1473,60 @@ where
|
||||
return HandshakeResult::BadClient { reader, writer };
|
||||
}
|
||||
|
||||
let cached = if config.censorship.tls_emulation {
|
||||
let cached_entry = if config.censorship.tls_emulation {
|
||||
if let Some(cache) = tls_cache.as_ref() {
|
||||
let selected_domain =
|
||||
matched_tls_domain.unwrap_or(config.censorship.tls_domain.as_str());
|
||||
let cached_entry = cache.get(selected_domain).await;
|
||||
let use_full_cert_payload = if config.censorship.serverhello_compact
|
||||
&& matches!(client_tls_version, tls::ClientHelloTlsVersion::Tls12)
|
||||
{
|
||||
Some(cached_entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let preferred_key_share_group = cached_entry
|
||||
.as_ref()
|
||||
.and_then(|cached_entry| emulator::profiled_server_hello_key_share_group(cached_entry));
|
||||
let Some(server_key_share) =
|
||||
tls::build_server_hello_key_share(handshake, preferred_key_share_group, rng)
|
||||
else {
|
||||
auth_probe_record_failure_in(shared, peer.ip(), Instant::now());
|
||||
maybe_apply_server_hello_delay(config).await;
|
||||
debug!(
|
||||
peer = %peer,
|
||||
"TLS handshake rejected: ClientHello did not offer a usable TLS 1.3 key_share"
|
||||
);
|
||||
return HandshakeResult::BadClient { reader, writer };
|
||||
};
|
||||
|
||||
let preferred_cipher_suite = if let Some(cached_entry) = cached_entry.as_ref() {
|
||||
if cached_entry.server_hello_template.cipher_suite == [0, 0] {
|
||||
[0x13, 0x01]
|
||||
} else {
|
||||
cached_entry.server_hello_template.cipher_suite
|
||||
}
|
||||
} else {
|
||||
[0x13, 0x01]
|
||||
};
|
||||
let Some(selected_cipher_suite) =
|
||||
tls::select_server_hello_cipher_suite(handshake, preferred_cipher_suite)
|
||||
else {
|
||||
auth_probe_record_failure_in(shared, peer.ip(), Instant::now());
|
||||
maybe_apply_server_hello_delay(config).await;
|
||||
debug!(
|
||||
peer = %peer,
|
||||
"TLS handshake rejected: ClientHello did not offer a supported TLS 1.3 cipher suite"
|
||||
);
|
||||
return HandshakeResult::BadClient { reader, writer };
|
||||
};
|
||||
|
||||
let cached = if let Some(cached_entry) = cached_entry {
|
||||
let use_full_cert_payload = if config.censorship.serverhello_compact
|
||||
&& matches!(client_tls_version, tls::ClientHelloTlsVersion::Tls12)
|
||||
{
|
||||
if let Some(cache) = tls_cache.as_ref() {
|
||||
cache
|
||||
.take_full_cert_budget_for_ip(
|
||||
peer.ip(),
|
||||
@@ -1489,11 +1535,11 @@ where
|
||||
.await
|
||||
} else {
|
||||
true
|
||||
};
|
||||
Some((cached_entry, use_full_cert_payload))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
true
|
||||
};
|
||||
Some((cached_entry, use_full_cert_payload))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1504,13 +1550,6 @@ where
|
||||
let validation_session_id_slice = &validation_session_id[..validation_session_id_len];
|
||||
|
||||
let response = if let Some((cached_entry, use_full_cert_payload)) = cached {
|
||||
let preferred_cipher_suite = if cached_entry.server_hello_template.cipher_suite == [0, 0] {
|
||||
[0x13, 0x01]
|
||||
} else {
|
||||
cached_entry.server_hello_template.cipher_suite
|
||||
};
|
||||
let selected_cipher_suite =
|
||||
tls::select_server_hello_cipher_suite(handshake, preferred_cipher_suite);
|
||||
emulator::build_emulated_server_hello(
|
||||
&validated_secret,
|
||||
&validation_digest,
|
||||
@@ -1520,12 +1559,12 @@ where
|
||||
config.censorship.serverhello_compact,
|
||||
client_tls_version,
|
||||
selected_cipher_suite,
|
||||
&server_key_share,
|
||||
rng,
|
||||
selected_alpn.clone(),
|
||||
config.censorship.tls_new_session_tickets,
|
||||
)
|
||||
} else {
|
||||
let selected_cipher_suite = tls::select_server_hello_cipher_suite(handshake, [0x13, 0x01]);
|
||||
tls::build_server_hello_with_cipher(
|
||||
&validated_secret,
|
||||
&validation_digest,
|
||||
@@ -1533,6 +1572,7 @@ where
|
||||
config.censorship.fake_cert_len,
|
||||
rng,
|
||||
selected_cipher_suite,
|
||||
&server_key_share,
|
||||
selected_alpn.clone(),
|
||||
config.censorship.tls_new_session_tickets,
|
||||
)
|
||||
|
||||
@@ -385,7 +385,7 @@ mod tls_domain_mask_host_tests {
|
||||
let mut config = ProxyConfig::default();
|
||||
config.censorship.tls_domain = "a.com".to_string();
|
||||
config.censorship.tls_domains = vec!["b.com".to_string(), "c.com".to_string()];
|
||||
config.censorship.mask_host = Some("a.com".to_string());
|
||||
config.censorship.mask_host = None;
|
||||
config
|
||||
}
|
||||
|
||||
@@ -419,6 +419,15 @@ mod tls_domain_mask_host_tests {
|
||||
assert_eq!(mask_host_for_initial_data(&config, &initial_data), "b.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_host_uses_primary_domain_when_dynamic_masking_is_disabled() {
|
||||
let mut config = config_with_tls_domains();
|
||||
config.censorship.mask_dynamic = false;
|
||||
let initial_data = client_hello_with_sni("b.com");
|
||||
|
||||
assert_eq!(mask_host_for_initial_data(&config, &initial_data), "a.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclusive_mask_target_overrides_only_matching_sni() {
|
||||
let mut config = config_with_tls_domains();
|
||||
@@ -577,24 +586,32 @@ fn default_mask_tcp_target_for_initial_data<'a>(
|
||||
.as_deref()
|
||||
.unwrap_or(&config.censorship.tls_domain);
|
||||
|
||||
if !configured_mask_host.eq_ignore_ascii_case(&config.censorship.tls_domain) {
|
||||
if config.censorship.mask_host.is_none() && config.censorship.mask_dynamic {
|
||||
let extracted_sni = if sni.is_none() {
|
||||
tls::extract_sni_from_client_hello(initial_data)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(host) = sni
|
||||
.or(extracted_sni.as_deref())
|
||||
.and_then(|sni| matching_tls_domain_for_sni(config, sni))
|
||||
{
|
||||
return MaskTcpTarget {
|
||||
host,
|
||||
port: config.censorship.mask_port,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mask_host) = config.censorship.mask_host.as_deref() {
|
||||
return MaskTcpTarget {
|
||||
host: configured_mask_host,
|
||||
host: mask_host,
|
||||
port: config.censorship.mask_port,
|
||||
};
|
||||
}
|
||||
|
||||
let extracted_sni = if sni.is_none() {
|
||||
tls::extract_sni_from_client_hello(initial_data)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let host = sni
|
||||
.or(extracted_sni.as_deref())
|
||||
.and_then(|sni| matching_tls_domain_for_sni(config, sni))
|
||||
.unwrap_or(configured_mask_host);
|
||||
MaskTcpTarget {
|
||||
host,
|
||||
host: configured_mask_host,
|
||||
port: config.censorship.mask_port,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ use tokio::time::sleep;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsFetchResult, TlsProfileSource,
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsFetchResult, TlsProfileQuality,
|
||||
TlsProfileSource,
|
||||
};
|
||||
|
||||
const FULL_CERT_SENT_SWEEP_INTERVAL_SECS: u64 = 30;
|
||||
@@ -47,10 +48,14 @@ pub struct TlsFrontCache {
|
||||
pub(crate) struct TlsFrontProfileHealth {
|
||||
pub(crate) domain: String,
|
||||
pub(crate) source: &'static str,
|
||||
pub(crate) quality: &'static str,
|
||||
pub(crate) key_share_group: &'static str,
|
||||
pub(crate) age_seconds: u64,
|
||||
pub(crate) is_default: bool,
|
||||
pub(crate) has_cert_info: bool,
|
||||
pub(crate) has_cert_payload: bool,
|
||||
pub(crate) server_hello_record_len: usize,
|
||||
pub(crate) server_hello_extensions: usize,
|
||||
pub(crate) app_data_records: usize,
|
||||
pub(crate) ticket_records: usize,
|
||||
pub(crate) change_cipher_spec_count: u8,
|
||||
@@ -66,6 +71,23 @@ fn profile_source_label(source: TlsProfileSource) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_quality_label(quality: TlsProfileQuality) -> &'static str {
|
||||
match quality {
|
||||
TlsProfileQuality::Fallback => "fallback",
|
||||
TlsProfileQuality::RawPartial => "raw_partial",
|
||||
TlsProfileQuality::RawStrict => "raw_strict",
|
||||
}
|
||||
}
|
||||
|
||||
fn key_share_group_label(group: Option<u16>) -> &'static str {
|
||||
match group {
|
||||
Some(0x001d) => "x25519",
|
||||
Some(0x11ec) => "x25519mlkem768",
|
||||
Some(_) => "other",
|
||||
None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TlsFrontCache {
|
||||
pub fn new(domains: &[String], default_len: usize, disk_path: impl AsRef<Path>) -> Self {
|
||||
@@ -137,7 +159,8 @@ impl TlsFrontCache {
|
||||
.get(domain)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.default.clone());
|
||||
let behavior = &cached.behavior_profile;
|
||||
let mut behavior = cached.behavior_profile.clone();
|
||||
behavior.refresh_server_hello_summary(&cached.server_hello_template);
|
||||
let age_seconds = now
|
||||
.duration_since(cached.fetched_at)
|
||||
.map(|duration| duration.as_secs())
|
||||
@@ -146,10 +169,14 @@ impl TlsFrontCache {
|
||||
snapshot.push(TlsFrontProfileHealth {
|
||||
domain: domain.clone(),
|
||||
source: profile_source_label(behavior.source),
|
||||
quality: profile_quality_label(behavior.quality),
|
||||
key_share_group: key_share_group_label(behavior.server_hello_key_share_group),
|
||||
age_seconds,
|
||||
is_default: cached.domain == "default",
|
||||
has_cert_info: cached.cert_info.is_some(),
|
||||
has_cert_payload: cached.cert_payload.is_some(),
|
||||
server_hello_record_len: behavior.server_hello_record_len,
|
||||
server_hello_extensions: behavior.server_hello_extension_types.len(),
|
||||
app_data_records: cached
|
||||
.app_data_records_sizes
|
||||
.len()
|
||||
@@ -337,6 +364,9 @@ impl TlsFrontCache {
|
||||
warn!(domain = %cached.domain, "Skipping stale TLS cache entry (>72h)");
|
||||
continue;
|
||||
}
|
||||
cached
|
||||
.behavior_profile
|
||||
.refresh_server_hello_summary(&cached.server_hello_template);
|
||||
let domain = cached.domain.clone();
|
||||
self.set(&domain, cached).await;
|
||||
loaded += 1;
|
||||
@@ -378,20 +408,39 @@ impl TlsFrontCache {
|
||||
|
||||
/// Replace cached entry from a fetch result.
|
||||
pub async fn update_from_fetch(&self, domain: &str, fetched: TlsFetchResult) {
|
||||
let TlsFetchResult {
|
||||
server_hello_parsed,
|
||||
app_data_records_sizes,
|
||||
total_app_data_len,
|
||||
mut behavior_profile,
|
||||
cert_info,
|
||||
cert_payload,
|
||||
} = fetched;
|
||||
behavior_profile.refresh_server_hello_summary(&server_hello_parsed);
|
||||
let quality = behavior_profile.quality;
|
||||
let data = CachedTlsData {
|
||||
server_hello_template: fetched.server_hello_parsed,
|
||||
cert_info: fetched.cert_info,
|
||||
cert_payload: fetched.cert_payload,
|
||||
app_data_records_sizes: fetched.app_data_records_sizes.clone(),
|
||||
total_app_data_len: fetched.total_app_data_len,
|
||||
behavior_profile: fetched.behavior_profile,
|
||||
server_hello_template: server_hello_parsed,
|
||||
cert_info,
|
||||
cert_payload,
|
||||
app_data_records_sizes: app_data_records_sizes.clone(),
|
||||
total_app_data_len,
|
||||
behavior_profile,
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: domain.to_string(),
|
||||
};
|
||||
|
||||
self.set(domain, data.clone()).await;
|
||||
self.persist(domain, &data).await;
|
||||
debug!(domain = %domain, len = fetched.total_app_data_len, "TLS cache updated");
|
||||
if quality == TlsProfileQuality::RawStrict {
|
||||
debug!(domain = %domain, len = total_app_data_len, "TLS cache updated");
|
||||
} else {
|
||||
warn!(
|
||||
domain = %domain,
|
||||
quality = profile_quality_label(quality),
|
||||
len = total_app_data_len,
|
||||
"TLS cache updated with non-strict front profile"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_entry(&self) -> Arc<CachedTlsData> {
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::protocol::constants::{
|
||||
TLS_RECORD_HANDSHAKE, TLS_VERSION,
|
||||
};
|
||||
use crate::protocol::tls::{
|
||||
ClientHelloTlsVersion, TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key,
|
||||
ClientHelloTlsVersion, ServerHelloKeyShare, TLS_DIGEST_LEN, TLS_DIGEST_POS,
|
||||
TLS_NAMED_GROUP_X25519, TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
};
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedCertificateInfo, TlsExtension, TlsProfileSource,
|
||||
@@ -20,6 +21,61 @@ const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
|
||||
const EXT_KEY_SHARE: u16 = 0x0033;
|
||||
const EXT_ALPN: u16 = 0x0010;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FallbackShapeFamily {
|
||||
NginxLike,
|
||||
BoringSslLike,
|
||||
RustlsLike,
|
||||
}
|
||||
|
||||
fn parse_profiled_key_share_group(data: &[u8]) -> Option<u16> {
|
||||
if data.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let group = u16::from_be_bytes([data[0], data[1]]);
|
||||
let key_exchange_len = u16::from_be_bytes([data[2], data[3]]) as usize;
|
||||
if data.len() != 4 + key_exchange_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
match group {
|
||||
TLS_NAMED_GROUP_X25519 | TLS_NAMED_GROUP_X25519MLKEM768 => Some(group),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_profiled_server_hello_record_len(cached: &CachedTlsData) -> usize {
|
||||
if cached.behavior_profile.server_hello_record_len == 0 {
|
||||
cached.server_hello_template.record_body_len()
|
||||
} else {
|
||||
cached.behavior_profile.server_hello_record_len
|
||||
}
|
||||
}
|
||||
|
||||
fn should_replay_profiled_server_hello_shape(cached: &CachedTlsData) -> bool {
|
||||
matches!(
|
||||
cached.behavior_profile.source,
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged
|
||||
) && cached
|
||||
.server_hello_template
|
||||
.is_replay_safe_tls13_shape(effective_profiled_server_hello_record_len(cached))
|
||||
}
|
||||
|
||||
/// Return the origin-profiled ServerHello key_share group when it is replay-safe.
|
||||
pub(crate) fn profiled_server_hello_key_share_group(cached: &CachedTlsData) -> Option<u16> {
|
||||
if !should_replay_profiled_server_hello_shape(cached) {
|
||||
return None;
|
||||
}
|
||||
|
||||
cached
|
||||
.server_hello_template
|
||||
.extensions
|
||||
.iter()
|
||||
.find(|ext| ext.ext_type == EXT_KEY_SHARE)
|
||||
.and_then(|ext| parse_profiled_key_share_group(&ext.data))
|
||||
}
|
||||
|
||||
fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> {
|
||||
sizes
|
||||
.iter()
|
||||
@@ -70,31 +126,91 @@ fn ensure_payload_capacity(mut sizes: Vec<usize>, payload_len: usize) -> Vec<usi
|
||||
sizes
|
||||
}
|
||||
|
||||
fn fallback_shape_family(cached: &CachedTlsData) -> FallbackShapeFamily {
|
||||
match cached.behavior_profile.source {
|
||||
TlsProfileSource::Rustls => FallbackShapeFamily::RustlsLike,
|
||||
TlsProfileSource::Default => {
|
||||
let mut hasher = Hasher::new();
|
||||
hasher.update(cached.domain.as_bytes());
|
||||
hasher.update(&cached.total_app_data_len.to_le_bytes());
|
||||
if hasher.finalize() & 1 == 0 {
|
||||
FallbackShapeFamily::NginxLike
|
||||
} else {
|
||||
FallbackShapeFamily::BoringSslLike
|
||||
}
|
||||
}
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged => FallbackShapeFamily::NginxLike,
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_total_app_data_len(cached: &CachedTlsData) -> usize {
|
||||
cached
|
||||
.total_app_data_len
|
||||
.max(cached.app_data_records_sizes.iter().sum())
|
||||
.max(1024)
|
||||
}
|
||||
|
||||
fn push_fallback_size(sizes: &mut Vec<usize>, size: usize) {
|
||||
sizes.push(size.clamp(MIN_APP_DATA, MAX_APP_DATA));
|
||||
}
|
||||
|
||||
fn fallback_family_app_data_sizes(cached: &CachedTlsData) -> Vec<usize> {
|
||||
if matches!(cached.behavior_profile.source, TlsProfileSource::Rustls)
|
||||
&& !cached.app_data_records_sizes.is_empty()
|
||||
{
|
||||
return cached.app_data_records_sizes.clone();
|
||||
}
|
||||
|
||||
let family = fallback_shape_family(cached);
|
||||
let mut remaining = fallback_total_app_data_len(cached);
|
||||
let preferred_chunk = match family {
|
||||
FallbackShapeFamily::NginxLike => 2896,
|
||||
FallbackShapeFamily::BoringSslLike => 1369,
|
||||
FallbackShapeFamily::RustlsLike => 2048,
|
||||
};
|
||||
let split_threshold = match family {
|
||||
FallbackShapeFamily::NginxLike => 4096,
|
||||
FallbackShapeFamily::BoringSslLike => 1536,
|
||||
FallbackShapeFamily::RustlsLike => 3072,
|
||||
};
|
||||
|
||||
if remaining <= split_threshold {
|
||||
return vec![remaining.clamp(MIN_APP_DATA, MAX_APP_DATA)];
|
||||
}
|
||||
|
||||
let mut sizes: Vec<usize> = Vec::new();
|
||||
while remaining > 0 {
|
||||
let chunk = remaining.min(preferred_chunk).min(MAX_APP_DATA);
|
||||
if chunk < MIN_APP_DATA {
|
||||
if let Some(last) = sizes.last_mut() {
|
||||
*last = (*last).saturating_add(chunk).min(MAX_APP_DATA);
|
||||
} else {
|
||||
push_fallback_size(&mut sizes, chunk);
|
||||
}
|
||||
break;
|
||||
}
|
||||
push_fallback_size(&mut sizes, chunk);
|
||||
remaining = remaining.saturating_sub(chunk);
|
||||
}
|
||||
|
||||
sizes
|
||||
}
|
||||
|
||||
fn emulated_app_data_sizes(cached: &CachedTlsData) -> Vec<usize> {
|
||||
match cached.behavior_profile.source {
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged => {
|
||||
return cached
|
||||
.app_data_records_sizes
|
||||
.first()
|
||||
.copied()
|
||||
.or_else(|| {
|
||||
cached
|
||||
.behavior_profile
|
||||
.app_data_record_sizes
|
||||
.first()
|
||||
.copied()
|
||||
})
|
||||
.map(|size| vec![size])
|
||||
.unwrap_or_else(|| vec![cached.total_app_data_len.max(1024)]);
|
||||
if !cached.behavior_profile.app_data_record_sizes.is_empty() {
|
||||
return cached.behavior_profile.app_data_record_sizes.clone();
|
||||
}
|
||||
if !cached.app_data_records_sizes.is_empty() {
|
||||
return cached.app_data_records_sizes.clone();
|
||||
}
|
||||
return vec![cached.total_app_data_len.max(1024)];
|
||||
}
|
||||
TlsProfileSource::Default | TlsProfileSource::Rustls => {
|
||||
return fallback_family_app_data_sizes(cached);
|
||||
}
|
||||
TlsProfileSource::Default | TlsProfileSource::Rustls => {}
|
||||
}
|
||||
|
||||
let mut sizes = cached.app_data_records_sizes.clone();
|
||||
if sizes.is_empty() {
|
||||
sizes.push(cached.total_app_data_len.max(1024));
|
||||
}
|
||||
sizes
|
||||
}
|
||||
|
||||
fn emulated_change_cipher_spec_count(_cached: &CachedTlsData) -> usize {
|
||||
@@ -122,7 +238,13 @@ fn emulated_ticket_record_sizes(
|
||||
sizes.extend(profiled_sizes.iter().copied().take(target_count));
|
||||
|
||||
while sizes.len() < target_count {
|
||||
sizes.push(rng.range(48) + 48);
|
||||
let family = fallback_shape_family(cached);
|
||||
let base = match family {
|
||||
FallbackShapeFamily::NginxLike => 96,
|
||||
FallbackShapeFamily::BoringSslLike => 80,
|
||||
FallbackShapeFamily::RustlsLike => 112,
|
||||
};
|
||||
sizes.push(base + rng.range(64));
|
||||
}
|
||||
|
||||
sizes
|
||||
@@ -196,19 +318,36 @@ fn push_supported_versions_extension(extensions: &mut Vec<u8>) {
|
||||
extensions.extend_from_slice(&0x0304u16.to_be_bytes());
|
||||
}
|
||||
|
||||
fn push_key_share_extension(extensions: &mut Vec<u8>, rng: &SecureRandom) {
|
||||
let key = gen_fake_x25519_key(rng);
|
||||
fn push_key_share_entry(extensions: &mut Vec<u8>, group: u16, key_exchange: &[u8]) {
|
||||
let Ok(key_exchange_len) = u16::try_from(key_exchange.len()) else {
|
||||
return;
|
||||
};
|
||||
let Some(entry_len) = key_exchange.len().checked_add(4) else {
|
||||
return;
|
||||
};
|
||||
let Ok(entry_len) = u16::try_from(entry_len) else {
|
||||
return;
|
||||
};
|
||||
|
||||
extensions.extend_from_slice(&EXT_KEY_SHARE.to_be_bytes());
|
||||
extensions.extend_from_slice(&(2 + 2 + 32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&0x001du16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&key);
|
||||
extensions.extend_from_slice(&entry_len.to_be_bytes());
|
||||
extensions.extend_from_slice(&group.to_be_bytes());
|
||||
extensions.extend_from_slice(&key_exchange_len.to_be_bytes());
|
||||
extensions.extend_from_slice(key_exchange);
|
||||
}
|
||||
|
||||
fn push_key_share_extension(extensions: &mut Vec<u8>, server_key_share: &ServerHelloKeyShare) {
|
||||
push_key_share_entry(
|
||||
extensions,
|
||||
server_key_share.group(),
|
||||
server_key_share.key_exchange(),
|
||||
);
|
||||
}
|
||||
|
||||
fn replay_profiled_server_hello_extension(
|
||||
ext: &TlsExtension,
|
||||
extensions: &mut Vec<u8>,
|
||||
rng: &SecureRandom,
|
||||
server_key_share: &ServerHelloKeyShare,
|
||||
saw_supported_versions: &mut bool,
|
||||
saw_key_share: &mut bool,
|
||||
) {
|
||||
@@ -218,7 +357,7 @@ fn replay_profiled_server_hello_extension(
|
||||
*saw_supported_versions = true;
|
||||
}
|
||||
EXT_KEY_SHARE if !*saw_key_share => {
|
||||
push_key_share_extension(extensions, rng);
|
||||
push_key_share_extension(extensions, server_key_share);
|
||||
*saw_key_share = true;
|
||||
}
|
||||
EXT_ALPN => {}
|
||||
@@ -226,7 +365,10 @@ fn replay_profiled_server_hello_extension(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profiled_server_hello_extensions(cached: &CachedTlsData, rng: &SecureRandom) -> Vec<u8> {
|
||||
fn build_profiled_server_hello_extensions(
|
||||
cached: &CachedTlsData,
|
||||
server_key_share: &ServerHelloKeyShare,
|
||||
) -> Vec<u8> {
|
||||
let capacity = cached
|
||||
.server_hello_template
|
||||
.extensions
|
||||
@@ -238,22 +380,24 @@ fn build_profiled_server_hello_extensions(cached: &CachedTlsData, rng: &SecureRa
|
||||
let mut saw_supported_versions = false;
|
||||
let mut saw_key_share = false;
|
||||
|
||||
for ext in &cached.server_hello_template.extensions {
|
||||
replay_profiled_server_hello_extension(
|
||||
ext,
|
||||
&mut extensions,
|
||||
rng,
|
||||
&mut saw_supported_versions,
|
||||
&mut saw_key_share,
|
||||
);
|
||||
if should_replay_profiled_server_hello_shape(cached) {
|
||||
for ext in &cached.server_hello_template.extensions {
|
||||
replay_profiled_server_hello_extension(
|
||||
ext,
|
||||
&mut extensions,
|
||||
server_key_share,
|
||||
&mut saw_supported_versions,
|
||||
&mut saw_key_share,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !saw_key_share {
|
||||
push_key_share_extension(&mut extensions, rng);
|
||||
}
|
||||
if !saw_supported_versions {
|
||||
push_supported_versions_extension(&mut extensions);
|
||||
}
|
||||
if !saw_key_share {
|
||||
push_key_share_extension(&mut extensions, server_key_share);
|
||||
}
|
||||
|
||||
extensions
|
||||
}
|
||||
@@ -268,12 +412,13 @@ pub fn build_emulated_server_hello(
|
||||
serverhello_compact: bool,
|
||||
client_tls_version: ClientHelloTlsVersion,
|
||||
selected_cipher_suite: [u8; 2],
|
||||
server_key_share: &ServerHelloKeyShare,
|
||||
rng: &SecureRandom,
|
||||
alpn: Option<Vec<u8>>,
|
||||
new_session_tickets: u8,
|
||||
) -> Vec<u8> {
|
||||
// --- ServerHello ---
|
||||
let extensions = build_profiled_server_hello_extensions(cached, rng);
|
||||
let extensions = build_profiled_server_hello_extensions(cached, server_key_share);
|
||||
let extensions_len = extensions.len() as u16;
|
||||
|
||||
let body_len = 2 + 32 + 1 + session_id.len() + 2 + 1 + 2 + extensions.len();
|
||||
@@ -368,6 +513,7 @@ pub fn build_emulated_server_hello(
|
||||
// ALPN selection is encrypted inside EncryptedExtensions in real TLS 1.3.
|
||||
// Keeping the FakeTLS record body opaque avoids a stable plaintext marker.
|
||||
let _ = alpn;
|
||||
let mut payload_offset = 0usize;
|
||||
for size in sizes {
|
||||
let mut rec = Vec::with_capacity(5 + size);
|
||||
rec.push(TLS_RECORD_APPLICATION);
|
||||
@@ -377,10 +523,11 @@ pub fn build_emulated_server_hello(
|
||||
if let Some(payload) = selected_payload {
|
||||
if size > 17 {
|
||||
let body_len = size - 17;
|
||||
let remaining = payload.len();
|
||||
let remaining = payload.len().saturating_sub(payload_offset);
|
||||
let copy_len = remaining.min(body_len);
|
||||
if copy_len > 0 {
|
||||
rec.extend_from_slice(&payload[..copy_len]);
|
||||
rec.extend_from_slice(&payload[payload_offset..payload_offset + copy_len]);
|
||||
payload_offset += copy_len;
|
||||
}
|
||||
if body_len > copy_len {
|
||||
rec.extend_from_slice(&rng.bytes(body_len - copy_len));
|
||||
@@ -452,13 +599,16 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
build_compact_cert_info_payload, build_emulated_server_hello,
|
||||
hash_compact_cert_info_payload,
|
||||
hash_compact_cert_info_payload, profiled_server_hello_key_share_group,
|
||||
};
|
||||
use crate::crypto::SecureRandom;
|
||||
use crate::protocol::constants::{
|
||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
|
||||
};
|
||||
use crate::protocol::tls::ClientHelloTlsVersion;
|
||||
use crate::protocol::tls::{
|
||||
ClientHelloTlsVersion, ServerHelloKeyShare, TLS_NAMED_GROUP_X25519,
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
};
|
||||
|
||||
fn first_app_data_payload(response: &[u8]) -> &[u8] {
|
||||
let hello_len = u16::from_be_bytes([response[3], response[4]]) as usize;
|
||||
@@ -523,6 +673,50 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_server_key_share() -> ServerHelloKeyShare {
|
||||
ServerHelloKeyShare::new(TLS_NAMED_GROUP_X25519MLKEM768, vec![0x42; 1120])
|
||||
}
|
||||
|
||||
fn server_key_share_extension_data(group: u16, len: usize) -> Vec<u8> {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&group.to_be_bytes());
|
||||
data.extend_from_slice(&(len as u16).to_be_bytes());
|
||||
data.resize(4 + len, 0x42);
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiled_server_hello_key_share_group_reads_raw_x25519_profile() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.behavior_profile.source = TlsProfileSource::Raw;
|
||||
cached.server_hello_template.extensions = vec![
|
||||
TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
profiled_server_hello_key_share_group(&cached),
|
||||
Some(TLS_NAMED_GROUP_X25519)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiled_server_hello_key_share_group_ignores_default_profile() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.server_hello_template.extensions = vec![TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
}];
|
||||
|
||||
assert_eq!(profiled_server_hello_key_share_group(&cached), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_uses_cached_cert_payload() {
|
||||
let cert_msg = vec![0x0b, 0x00, 0x00, 0x05, 0x00, 0xaa, 0xbb, 0xcc, 0xdd];
|
||||
@@ -540,6 +734,7 @@ mod tests {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -569,6 +764,7 @@ mod tests {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x03],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -604,6 +800,7 @@ mod tests {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -615,6 +812,82 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_replays_safe_raw_extension_order() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.behavior_profile.source = TlsProfileSource::Raw;
|
||||
cached.server_hello_template.extensions = vec![
|
||||
TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
];
|
||||
let rng = SecureRandom::new();
|
||||
let response = build_emulated_server_hello(
|
||||
b"secret",
|
||||
&[0x21; 32],
|
||||
&[0x22; 16],
|
||||
&cached,
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server_hello_extension_types(&response),
|
||||
vec![0x0033, 0x002b]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_uses_canonical_order_for_unsafe_raw_shape() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.behavior_profile.source = TlsProfileSource::Raw;
|
||||
cached.server_hello_template.extensions = vec![
|
||||
TlsExtension {
|
||||
ext_type: 0x0010,
|
||||
data: vec![0x00, 0x03, 0x02, b'h', b'2'],
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: server_key_share_extension_data(TLS_NAMED_GROUP_X25519, 32),
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
];
|
||||
let rng = SecureRandom::new();
|
||||
let response = build_emulated_server_hello(
|
||||
b"secret",
|
||||
&[0x21; 32],
|
||||
&[0x22; 16],
|
||||
&cached,
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server_hello_extension_types(&response),
|
||||
vec![0x002b, 0x0033]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_random_fallback_when_no_cert_payload() {
|
||||
let cached = make_cached(None);
|
||||
@@ -628,6 +901,7 @@ mod tests {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -663,6 +937,7 @@ mod tests {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -704,6 +979,7 @@ mod tests {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -737,6 +1013,7 @@ mod tests {
|
||||
false,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -769,6 +1046,7 @@ mod tests {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -776,11 +1054,15 @@ mod tests {
|
||||
|
||||
let hello_len = u16::from_be_bytes([response[3], response[4]]) as usize;
|
||||
let ccs_start = 5 + hello_len;
|
||||
let app_start = ccs_start + 6;
|
||||
let app_len =
|
||||
u16::from_be_bytes([response[app_start + 3], response[app_start + 4]]) as usize;
|
||||
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
|
||||
assert_eq!(app_len, 64);
|
||||
assert_eq!(app_start + 5 + app_len, response.len());
|
||||
let mut pos = ccs_start + 6;
|
||||
let mut app_lens = Vec::new();
|
||||
while pos + 5 <= response.len() {
|
||||
let record_len = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
||||
assert_eq!(response[pos], TLS_RECORD_APPLICATION);
|
||||
app_lens.push(record_len);
|
||||
pos += 5 + record_len;
|
||||
}
|
||||
assert_eq!(app_lens, vec![64, 3905, 537]);
|
||||
assert_eq!(pos, response.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use ml_kem::{DecapsulationKey as MlKemDecapsulationKey, KeyExport, MlKem768, Seed as MlKemSeed};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
#[cfg(unix)]
|
||||
@@ -33,6 +34,7 @@ use crate::network::dns_overrides::resolve_socket_addr;
|
||||
use crate::protocol::constants::{
|
||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
|
||||
};
|
||||
use crate::protocol::tls::{TLS_NAMED_GROUP_X25519, TLS_NAMED_GROUP_X25519MLKEM768};
|
||||
use crate::tls_front::types::{
|
||||
ParsedCertificateInfo, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsExtension,
|
||||
TlsFetchResult, TlsProfileSource,
|
||||
@@ -40,6 +42,10 @@ use crate::tls_front::types::{
|
||||
use crate::transport::UpstreamStream;
|
||||
use crate::transport::proxy_protocol::{ProxyProtocolV1Builder, ProxyProtocolV2Builder};
|
||||
|
||||
#[cfg(test)]
|
||||
const X25519_KEY_SHARE_LEN: usize = 32;
|
||||
const MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN: usize = 1184;
|
||||
|
||||
/// No-op verifier: accept any certificate (we only need lengths and metadata).
|
||||
#[derive(Debug)]
|
||||
struct NoVerify;
|
||||
@@ -393,8 +399,13 @@ fn profile_cipher_suites(profile: TlsFetchProfile) -> &'static [u16] {
|
||||
}
|
||||
|
||||
fn profile_groups(profile: TlsFetchProfile) -> &'static [u16] {
|
||||
const MODERN: &[u16] = &[0x001d, 0x0017, 0x0018]; // x25519, secp256r1, secp384r1
|
||||
const COMPAT: &[u16] = &[0x001d, 0x0017];
|
||||
const MODERN: &[u16] = &[
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
TLS_NAMED_GROUP_X25519,
|
||||
0x0017,
|
||||
0x0018,
|
||||
];
|
||||
const COMPAT: &[u16] = &[TLS_NAMED_GROUP_X25519, 0x0017];
|
||||
const LEGACY: &[u16] = &[0x0017];
|
||||
|
||||
match profile {
|
||||
@@ -475,6 +486,48 @@ fn grease_value(rng: &SecureRandom, deterministic: bool, seed: &str) -> u16 {
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_mlkem768_client_encapsulation_key(
|
||||
rng: &SecureRandom,
|
||||
deterministic: bool,
|
||||
seed: &str,
|
||||
) -> Option<Vec<u8>> {
|
||||
let seed_bytes = if deterministic {
|
||||
deterministic_bytes(seed, 64)
|
||||
} else {
|
||||
rng.bytes(64)
|
||||
};
|
||||
let seed = MlKemSeed::try_from(seed_bytes.as_slice()).ok()?;
|
||||
let decapsulation_key = MlKemDecapsulationKey::<MlKem768>::from_seed(seed);
|
||||
let encapsulation_key = decapsulation_key.encapsulation_key().to_bytes();
|
||||
let bytes = encapsulation_key.as_slice();
|
||||
if bytes.len() == MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN {
|
||||
Some(bytes.to_vec())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_x25519mlkem768_client_key_share(
|
||||
rng: &SecureRandom,
|
||||
deterministic: bool,
|
||||
seed: &str,
|
||||
) -> Option<Vec<u8>> {
|
||||
let mlkem_key =
|
||||
gen_mlkem768_client_encapsulation_key(rng, deterministic, &format!("{seed}:mlkem768"))?;
|
||||
let x25519_key = gen_key_share(rng, deterministic, &format!("{seed}:x25519"));
|
||||
let mut key_share =
|
||||
Vec::with_capacity(MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN + x25519_key.len());
|
||||
key_share.extend_from_slice(&mlkem_key);
|
||||
key_share.extend_from_slice(&x25519_key);
|
||||
Some(key_share)
|
||||
}
|
||||
|
||||
fn push_client_key_share_entry(keyshare: &mut Vec<u8>, group: u16, key: &[u8]) {
|
||||
keyshare.extend_from_slice(&group.to_be_bytes());
|
||||
keyshare.extend_from_slice(&(key.len() as u16).to_be_bytes());
|
||||
keyshare.extend_from_slice(key);
|
||||
}
|
||||
|
||||
fn build_client_hello(
|
||||
sni: &str,
|
||||
rng: &SecureRandom,
|
||||
@@ -597,16 +650,20 @@ fn build_client_hello(
|
||||
push_extension(0x002d, &[0x01, 0x01]);
|
||||
}
|
||||
|
||||
// key_share (x25519)
|
||||
let key = gen_key_share(
|
||||
rng,
|
||||
deterministic,
|
||||
&format!("tls-fetch-keyshare:{sni}:{}", profile.as_str()),
|
||||
);
|
||||
let mut keyshare = Vec::with_capacity(4 + key.len());
|
||||
keyshare.extend_from_slice(&0x001du16.to_be_bytes());
|
||||
keyshare.extend_from_slice(&(key.len() as u16).to_be_bytes());
|
||||
keyshare.extend_from_slice(&key);
|
||||
// key_share
|
||||
let key_share_seed = format!("tls-fetch-keyshare:{sni}:{}", profile.as_str());
|
||||
let mut keyshare = Vec::new();
|
||||
if matches!(
|
||||
profile,
|
||||
TlsFetchProfile::ModernChromeLike | TlsFetchProfile::ModernFirefoxLike
|
||||
) {
|
||||
if let Some(key) = gen_x25519mlkem768_client_key_share(rng, deterministic, &key_share_seed)
|
||||
{
|
||||
push_client_key_share_entry(&mut keyshare, TLS_NAMED_GROUP_X25519MLKEM768, &key);
|
||||
}
|
||||
}
|
||||
let key = gen_key_share(rng, deterministic, &key_share_seed);
|
||||
push_client_key_share_entry(&mut keyshare, TLS_NAMED_GROUP_X25519, &key);
|
||||
let mut keyshare_ext = Vec::with_capacity(2 + keyshare.len());
|
||||
keyshare_ext.extend_from_slice(&(keyshare.len() as u16).to_be_bytes());
|
||||
keyshare_ext.extend_from_slice(&keyshare);
|
||||
@@ -776,6 +833,7 @@ fn derive_behavior_profile(records: &[(u8, Vec<u8>)]) -> TlsBehaviorProfile {
|
||||
app_data_record_sizes,
|
||||
ticket_record_sizes,
|
||||
source: TlsProfileSource::Raw,
|
||||
..TlsBehaviorProfile::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,24 +1083,26 @@ where
|
||||
}
|
||||
|
||||
let mut server_hello = None;
|
||||
let mut server_hello_record_len = 0usize;
|
||||
for (t, body) in &records {
|
||||
if *t == TLS_RECORD_HANDSHAKE && server_hello.is_none() {
|
||||
server_hello = parse_server_hello(body);
|
||||
server_hello_record_len = body.len();
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = server_hello.ok_or_else(|| anyhow!("ServerHello not received"))?;
|
||||
let behavior_profile = derive_behavior_profile(&records);
|
||||
let mut behavior_profile = derive_behavior_profile(&records);
|
||||
behavior_profile.server_hello_record_len = server_hello_record_len;
|
||||
behavior_profile.refresh_server_hello_summary(&parsed);
|
||||
let mut app_sizes = behavior_profile.app_data_record_sizes.clone();
|
||||
app_sizes.extend_from_slice(&behavior_profile.ticket_record_sizes);
|
||||
let total_app_data_len = app_sizes.iter().sum::<usize>().max(1024);
|
||||
let app_data_records_sizes = behavior_profile
|
||||
.app_data_record_sizes
|
||||
.first()
|
||||
.copied()
|
||||
.or_else(|| behavior_profile.ticket_record_sizes.first().copied())
|
||||
.map(|size| vec![size])
|
||||
.unwrap_or_else(|| vec![total_app_data_len]);
|
||||
let app_data_records_sizes = if app_sizes.is_empty() {
|
||||
vec![total_app_data_len]
|
||||
} else {
|
||||
app_sizes
|
||||
};
|
||||
|
||||
Ok(TlsFetchResult {
|
||||
server_hello_parsed: parsed,
|
||||
@@ -1212,6 +1272,7 @@ where
|
||||
app_data_record_sizes: app_data_records_sizes,
|
||||
ticket_record_sizes: Vec::new(),
|
||||
source: TlsProfileSource::Rustls,
|
||||
..TlsBehaviorProfile::default()
|
||||
},
|
||||
cert_info,
|
||||
cert_payload,
|
||||
@@ -1411,6 +1472,8 @@ pub async fn fetch_real_tls_with_strategy(
|
||||
raw.cert_info = rustls.cert_info;
|
||||
raw.cert_payload = rustls.cert_payload;
|
||||
raw.behavior_profile.source = TlsProfileSource::Merged;
|
||||
raw.behavior_profile
|
||||
.refresh_server_hello_summary(&raw.server_hello_parsed);
|
||||
debug!(sni = %sni, "Fetched TLS metadata via adaptive raw probe + rustls cert chain");
|
||||
Ok(raw)
|
||||
} else {
|
||||
@@ -1462,9 +1525,10 @@ mod tests {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{
|
||||
ProfileCacheValue, TlsFetchStrategy, build_client_hello, build_tls_fetch_proxy_header,
|
||||
derive_behavior_profile, encode_tls13_certificate_message, fetch_via_rustls_stream,
|
||||
order_profiles, profile_alpn, profile_cache, profile_cache_key,
|
||||
MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN, ProfileCacheValue, TLS_NAMED_GROUP_X25519,
|
||||
TLS_NAMED_GROUP_X25519MLKEM768, TlsFetchStrategy, X25519_KEY_SHARE_LEN, build_client_hello,
|
||||
build_tls_fetch_proxy_header, derive_behavior_profile, encode_tls13_certificate_message,
|
||||
fetch_via_rustls_stream, order_profiles, profile_alpn, profile_cache, profile_cache_key,
|
||||
};
|
||||
use crate::config::TlsFetchProfile;
|
||||
use crate::crypto::SecureRandom;
|
||||
@@ -1790,11 +1854,40 @@ mod tests {
|
||||
key_share_data.len() - 2,
|
||||
"key_share list length mismatch"
|
||||
);
|
||||
let group = u16::from_be_bytes([key_share_data[2], key_share_data[3]]);
|
||||
let key_len = u16::from_be_bytes([key_share_data[4], key_share_data[5]]) as usize;
|
||||
let key = &key_share_data[6..6 + key_len];
|
||||
assert_eq!(group, 0x001d, "key_share group must be x25519");
|
||||
assert_eq!(key_len, 32, "x25519 key length must be 32");
|
||||
let mut pos = 2usize;
|
||||
let hybrid_group = u16::from_be_bytes([key_share_data[pos], key_share_data[pos + 1]]);
|
||||
let hybrid_len =
|
||||
u16::from_be_bytes([key_share_data[pos + 2], key_share_data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
let hybrid_key = &key_share_data[pos..pos + hybrid_len];
|
||||
pos += hybrid_len;
|
||||
assert_eq!(
|
||||
hybrid_group, TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
"first key_share group must be X25519MLKEM768"
|
||||
);
|
||||
assert_eq!(
|
||||
hybrid_len,
|
||||
MLKEM768_CLIENT_ENCAPSULATION_KEY_LEN + X25519_KEY_SHARE_LEN,
|
||||
"hybrid key length must match X25519MLKEM768"
|
||||
);
|
||||
assert!(
|
||||
hybrid_key.iter().any(|b| *b != 0),
|
||||
"hybrid key must not be all zero"
|
||||
);
|
||||
|
||||
let group = u16::from_be_bytes([key_share_data[pos], key_share_data[pos + 1]]);
|
||||
let key_len =
|
||||
u16::from_be_bytes([key_share_data[pos + 2], key_share_data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
let key = &key_share_data[pos..pos + key_len];
|
||||
assert_eq!(
|
||||
group, TLS_NAMED_GROUP_X25519,
|
||||
"second key_share group must be x25519"
|
||||
);
|
||||
assert_eq!(
|
||||
key_len, X25519_KEY_SHARE_LEN,
|
||||
"x25519 key length must be 32"
|
||||
);
|
||||
assert!(
|
||||
key.iter().any(|b| *b != 0),
|
||||
"x25519 key must not be all zero"
|
||||
|
||||
@@ -4,7 +4,9 @@ use crate::crypto::SecureRandom;
|
||||
use crate::protocol::constants::{
|
||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
|
||||
};
|
||||
use crate::protocol::tls::ClientHelloTlsVersion;
|
||||
use crate::protocol::tls::{
|
||||
ClientHelloTlsVersion, ServerHelloKeyShare, TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
};
|
||||
use crate::tls_front::emulator::build_emulated_server_hello;
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource,
|
||||
@@ -29,6 +31,7 @@ fn make_cached() -> CachedTlsData {
|
||||
app_data_record_sizes: vec![1200, 900],
|
||||
ticket_record_sizes: vec![220, 180],
|
||||
source: TlsProfileSource::Merged,
|
||||
..TlsBehaviorProfile::default()
|
||||
},
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: "example.com".to_string(),
|
||||
@@ -52,6 +55,10 @@ fn record_lengths_by_type(response: &[u8], wanted_type: u8) -> Vec<usize> {
|
||||
out
|
||||
}
|
||||
|
||||
fn test_server_key_share() -> ServerHelloKeyShare {
|
||||
ServerHelloKeyShare::new(TLS_NAMED_GROUP_X25519MLKEM768, vec![0x42; 1120])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emulated_server_hello_keeps_single_change_cipher_spec_for_client_compatibility() {
|
||||
let cached = make_cached();
|
||||
@@ -66,6 +73,7 @@ fn emulated_server_hello_keeps_single_change_cipher_spec_for_client_compatibilit
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -91,13 +99,14 @@ fn emulated_server_hello_does_not_emit_profile_ticket_tail_when_disabled() {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
let app_records = record_lengths_by_type(&response, TLS_RECORD_APPLICATION);
|
||||
assert_eq!(app_records, vec![1200]);
|
||||
assert_eq!(app_records, vec![1200, 900]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -114,11 +123,12 @@ fn emulated_server_hello_uses_profile_ticket_lengths_when_enabled() {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
None,
|
||||
2,
|
||||
);
|
||||
|
||||
let app_records = record_lengths_by_type(&response, TLS_RECORD_APPLICATION);
|
||||
assert_eq!(app_records, vec![1200, 220, 180]);
|
||||
assert_eq!(app_records, vec![1200, 900, 220, 180]);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ use crate::crypto::SecureRandom;
|
||||
use crate::protocol::constants::{
|
||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
|
||||
};
|
||||
use crate::protocol::tls::ClientHelloTlsVersion;
|
||||
use crate::protocol::tls::{
|
||||
ClientHelloTlsVersion, ServerHelloKeyShare, TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
};
|
||||
use crate::tls_front::emulator::build_emulated_server_hello;
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsProfileSource,
|
||||
@@ -29,6 +31,7 @@ fn make_cached(cert_payload: Option<crate::tls_front::types::TlsCertPayload>) ->
|
||||
app_data_record_sizes: vec![64],
|
||||
ticket_record_sizes: Vec::new(),
|
||||
source: TlsProfileSource::Default,
|
||||
..TlsBehaviorProfile::default()
|
||||
},
|
||||
fetched_at: SystemTime::now(),
|
||||
domain: "example.com".to_string(),
|
||||
@@ -44,6 +47,10 @@ fn first_app_data_payload(response: &[u8]) -> &[u8] {
|
||||
&response[app_start + 5..app_start + 5 + app_len]
|
||||
}
|
||||
|
||||
fn test_server_key_share() -> ServerHelloKeyShare {
|
||||
ServerHelloKeyShare::new(TLS_NAMED_GROUP_X25519MLKEM768, vec![0x42; 1120])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
|
||||
let cached = make_cached(None);
|
||||
@@ -59,6 +66,7 @@ fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
Some(oversized_alpn),
|
||||
0,
|
||||
@@ -98,6 +106,7 @@ fn emulated_server_hello_keeps_alpn_marker_out_of_appdata() {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -129,6 +138,7 @@ fn emulated_server_hello_prefers_cert_payload_over_alpn_marker() {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&test_server_key_share(),
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::SystemTime;
|
||||
|
||||
const EXT_ALPN: u16 = 0x0010;
|
||||
const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
|
||||
const EXT_KEY_SHARE: u16 = 0x0033;
|
||||
const TLS_LEGACY_SERVER_HELLO_VERSION: [u8; 2] = [0x03, 0x03];
|
||||
const TLS_VERSION_13: [u8; 2] = [0x03, 0x04];
|
||||
const TLS_NAMED_GROUP_X25519: u16 = 0x001d;
|
||||
const TLS_NAMED_GROUP_X25519MLKEM768: u16 = 0x11ec;
|
||||
|
||||
/// Parsed representation of an unencrypted TLS ServerHello.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ParsedServerHello {
|
||||
@@ -19,6 +27,96 @@ pub struct TlsExtension {
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ParsedServerHello {
|
||||
/// Return the TLS record body length that would contain this ServerHello.
|
||||
pub(crate) fn record_body_len(&self) -> usize {
|
||||
let extensions_len = self
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|extension| 4 + extension.data.len())
|
||||
.sum::<usize>();
|
||||
|
||||
4 + 2 + 32 + 1 + self.session_id.len() + 2 + 1 + 2 + extensions_len
|
||||
}
|
||||
|
||||
/// Return visible ServerHello extension types in wire order.
|
||||
pub(crate) fn extension_types(&self) -> Vec<u16> {
|
||||
self.extensions
|
||||
.iter()
|
||||
.map(|extension| extension.ext_type)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return a replay-safe ServerHello key_share group when the extension is well-formed.
|
||||
pub(crate) fn key_share_group(&self) -> Option<u16> {
|
||||
self.extensions
|
||||
.iter()
|
||||
.find(|extension| extension.ext_type == EXT_KEY_SHARE)
|
||||
.and_then(|extension| parse_key_share_group(&extension.data))
|
||||
}
|
||||
|
||||
/// Return true when the cached ServerHello can safely drive visible TLS 1.3 replay.
|
||||
pub(crate) fn is_replay_safe_tls13_shape(&self, record_body_len: usize) -> bool {
|
||||
if self.version != TLS_LEGACY_SERVER_HELLO_VERSION
|
||||
|| self.compression != 0
|
||||
|| self.session_id.len() > 32
|
||||
|| !is_supported_tls13_cipher_suite(self.cipher_suite)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if record_body_len != 0 && record_body_len != self.record_body_len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut saw_supported_versions = false;
|
||||
let mut saw_key_share = false;
|
||||
for extension in &self.extensions {
|
||||
match extension.ext_type {
|
||||
EXT_SUPPORTED_VERSIONS => {
|
||||
if saw_supported_versions || extension.data.as_slice() != TLS_VERSION_13 {
|
||||
return false;
|
||||
}
|
||||
saw_supported_versions = true;
|
||||
}
|
||||
EXT_KEY_SHARE => {
|
||||
if saw_key_share || parse_key_share_group(&extension.data).is_none() {
|
||||
return false;
|
||||
}
|
||||
saw_key_share = true;
|
||||
}
|
||||
EXT_ALPN => {
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
saw_supported_versions && saw_key_share
|
||||
}
|
||||
}
|
||||
|
||||
fn is_supported_tls13_cipher_suite(cipher_suite: [u8; 2]) -> bool {
|
||||
matches!(u16::from_be_bytes(cipher_suite), 0x1301 | 0x1302 | 0x1303)
|
||||
}
|
||||
|
||||
fn parse_key_share_group(data: &[u8]) -> Option<u16> {
|
||||
if data.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let group = u16::from_be_bytes([data[0], data[1]]);
|
||||
let key_exchange_len = u16::from_be_bytes([data[2], data[3]]) as usize;
|
||||
if data.len() != 4 + key_exchange_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
match group {
|
||||
TLS_NAMED_GROUP_X25519 | TLS_NAMED_GROUP_X25519MLKEM768 => Some(group),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic certificate metadata (optional, informative).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ParsedCertificateInfo {
|
||||
@@ -54,6 +152,19 @@ pub enum TlsProfileSource {
|
||||
Merged,
|
||||
}
|
||||
|
||||
/// DPI-facing quality class of a cached TLS front profile.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TlsProfileQuality {
|
||||
/// No raw origin ServerHello shape is available.
|
||||
#[default]
|
||||
Fallback,
|
||||
/// Raw origin ServerHello was captured, but encrypted flight shape is incomplete.
|
||||
RawPartial,
|
||||
/// Raw origin ServerHello and encrypted flight record sizes were captured.
|
||||
RawStrict,
|
||||
}
|
||||
|
||||
/// Coarse-grained TLS response behavior captured per SNI.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TlsBehaviorProfile {
|
||||
@@ -69,6 +180,18 @@ pub struct TlsBehaviorProfile {
|
||||
/// Source of this behavior profile.
|
||||
#[serde(default)]
|
||||
pub source: TlsProfileSource,
|
||||
/// DPI-facing quality of this profile.
|
||||
#[serde(default)]
|
||||
pub quality: TlsProfileQuality,
|
||||
/// Captured ServerHello TLS record body length.
|
||||
#[serde(default)]
|
||||
pub server_hello_record_len: usize,
|
||||
/// Captured visible ServerHello extension types in wire order.
|
||||
#[serde(default)]
|
||||
pub server_hello_extension_types: Vec<u16>,
|
||||
/// Captured ServerHello key_share group when replay-safe.
|
||||
#[serde(default)]
|
||||
pub server_hello_key_share_group: Option<u16>,
|
||||
}
|
||||
|
||||
fn default_change_cipher_spec_count() -> u8 {
|
||||
@@ -82,10 +205,54 @@ impl Default for TlsBehaviorProfile {
|
||||
app_data_record_sizes: Vec::new(),
|
||||
ticket_record_sizes: Vec::new(),
|
||||
source: TlsProfileSource::Default,
|
||||
quality: TlsProfileQuality::Fallback,
|
||||
server_hello_record_len: 0,
|
||||
server_hello_extension_types: Vec::new(),
|
||||
server_hello_key_share_group: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsBehaviorProfile {
|
||||
/// Refresh cached visible ServerHello summary fields and quality.
|
||||
pub(crate) fn refresh_server_hello_summary(&mut self, server_hello: &ParsedServerHello) {
|
||||
let mut has_replay_safe_server_hello = false;
|
||||
if matches!(
|
||||
self.source,
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged
|
||||
) {
|
||||
if self.server_hello_record_len == 0 {
|
||||
self.server_hello_record_len = server_hello.record_body_len();
|
||||
}
|
||||
self.server_hello_extension_types = server_hello.extension_types();
|
||||
self.server_hello_key_share_group = server_hello.key_share_group();
|
||||
has_replay_safe_server_hello =
|
||||
server_hello.is_replay_safe_tls13_shape(self.server_hello_record_len);
|
||||
} else {
|
||||
self.server_hello_record_len = 0;
|
||||
self.server_hello_extension_types.clear();
|
||||
self.server_hello_key_share_group = None;
|
||||
}
|
||||
|
||||
self.refresh_quality(has_replay_safe_server_hello);
|
||||
}
|
||||
|
||||
/// Recompute the profile quality from current source and record-size evidence.
|
||||
fn refresh_quality(&mut self, has_replay_safe_server_hello: bool) {
|
||||
let has_raw_server_hello = matches!(
|
||||
self.source,
|
||||
TlsProfileSource::Raw | TlsProfileSource::Merged
|
||||
) && has_replay_safe_server_hello;
|
||||
self.quality = if has_raw_server_hello && !self.app_data_record_sizes.is_empty() {
|
||||
TlsProfileQuality::RawStrict
|
||||
} else if has_raw_server_hello {
|
||||
TlsProfileQuality::RawPartial
|
||||
} else {
|
||||
TlsProfileQuality::Fallback
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached data per SNI used by the emulator.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CachedTlsData {
|
||||
@@ -122,6 +289,34 @@ pub struct TlsFetchResult {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tls13_key_share_extension() -> TlsExtension {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&TLS_NAMED_GROUP_X25519.to_be_bytes());
|
||||
data.extend_from_slice(&32u16.to_be_bytes());
|
||||
data.resize(36, 0x42);
|
||||
TlsExtension {
|
||||
ext_type: EXT_KEY_SHARE,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
fn replay_safe_server_hello() -> ParsedServerHello {
|
||||
ParsedServerHello {
|
||||
version: TLS_LEGACY_SERVER_HELLO_VERSION,
|
||||
random: [0u8; 32],
|
||||
session_id: vec![0x11; 32],
|
||||
cipher_suite: [0x13, 0x01],
|
||||
compression: 0,
|
||||
extensions: vec![
|
||||
TlsExtension {
|
||||
ext_type: EXT_SUPPORTED_VERSIONS,
|
||||
data: TLS_VERSION_13.to_vec(),
|
||||
},
|
||||
tls13_key_share_extension(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_tls_data_deserializes_without_behavior_profile() {
|
||||
let json = r#"
|
||||
@@ -147,5 +342,59 @@ mod tests {
|
||||
assert!(cached.behavior_profile.app_data_record_sizes.is_empty());
|
||||
assert!(cached.behavior_profile.ticket_record_sizes.is_empty());
|
||||
assert_eq!(cached.behavior_profile.source, TlsProfileSource::Default);
|
||||
assert_eq!(cached.behavior_profile.quality, TlsProfileQuality::Fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_safe_raw_server_hello_with_app_data_is_raw_strict() {
|
||||
let server_hello = replay_safe_server_hello();
|
||||
let mut behavior = TlsBehaviorProfile {
|
||||
source: TlsProfileSource::Raw,
|
||||
app_data_record_sizes: vec![1200],
|
||||
..TlsBehaviorProfile::default()
|
||||
};
|
||||
|
||||
behavior.refresh_server_hello_summary(&server_hello);
|
||||
|
||||
assert_eq!(behavior.quality, TlsProfileQuality::RawStrict);
|
||||
assert_eq!(
|
||||
behavior.server_hello_extension_types,
|
||||
vec![EXT_SUPPORTED_VERSIONS, EXT_KEY_SHARE]
|
||||
);
|
||||
assert_eq!(
|
||||
behavior.server_hello_key_share_group,
|
||||
Some(TLS_NAMED_GROUP_X25519)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_safe_raw_server_hello_without_app_data_is_raw_partial() {
|
||||
let server_hello = replay_safe_server_hello();
|
||||
let mut behavior = TlsBehaviorProfile {
|
||||
source: TlsProfileSource::Raw,
|
||||
..TlsBehaviorProfile::default()
|
||||
};
|
||||
|
||||
behavior.refresh_server_hello_summary(&server_hello);
|
||||
|
||||
assert_eq!(behavior.quality, TlsProfileQuality::RawPartial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_raw_server_hello_is_fallback_quality() {
|
||||
let mut server_hello = replay_safe_server_hello();
|
||||
server_hello.extensions.push(TlsExtension {
|
||||
ext_type: EXT_ALPN,
|
||||
data: vec![0x00, 0x03, 0x02, b'h', b'2'],
|
||||
});
|
||||
let mut behavior = TlsBehaviorProfile {
|
||||
source: TlsProfileSource::Raw,
|
||||
app_data_record_sizes: vec![1200],
|
||||
..TlsBehaviorProfile::default()
|
||||
};
|
||||
|
||||
behavior.refresh_server_hello_summary(&server_hello);
|
||||
|
||||
assert_eq!(behavior.quality, TlsProfileQuality::Fallback);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user