From 92e30be37870da30884dcf3d1f6b4626ecb84c22 Mon Sep 17 00:00:00 2001 From: Jack Cutrara Date: Fri, 16 Oct 2026 11:09:42 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20send=20the=20GREASE=20extension=20once?= =?UTF-8?q?=20in=20chrome100=E2=80=93116=20and=20ios18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These profiles listed `ExtensionType::Grease` twice in `extension_order`. rustls has a single GREASE extension type (0xbaba), so the ClientHello carried it twice. TLS 1.3 forbids repeated extensions, and BoringSSL-based servers (Google, Cloudflare, Let's Encrypt) rejected the handshake with `decode_error`. JA3 and JA4 are unchanged, since both ignore GREASE. Adds a test that builds every profile's ClientHello and checks that no extension type appears twice. Fixes #575 Co-Authored-By: Claude Opus 5.5 --- impit/src/fingerprint/database.rs | 82 ++++++++++++++++++++++++ impit/src/fingerprint/database/chrome.rs | 3 +- impit/src/fingerprint/database/safari.rs | 3 +- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs index 4719197b..72c0d8fa 100644 --- a/impit/src/fingerprint/database.rs +++ b/impit/src/fingerprint/database.rs @@ -14,3 +14,85 @@ pub use chrome::{ pub use firefox::{firefox_128, firefox_133, firefox_135, firefox_144}; pub use okhttp::{okhttp3, okhttp4, okhttp5}; pub use safari::ios_18; + +#[cfg(test)] +mod tests { + use super::*; + use crate::fingerprint::BrowserFingerprint; + use crate::tls::TlsConfig; + use std::sync::Arc; + + /// Extension types of the ClientHello that impit sends for `fingerprint`, in wire order. + fn client_hello_extension_types(fingerprint: BrowserFingerprint) -> Vec { + let mut builder = TlsConfig::builder(); + builder.with_tls_fingerprint(fingerprint.tls); + let server_name = rustls::pki_types::ServerName::try_from("example.com").unwrap(); + let mut conn = + rustls::ClientConnection::new(Arc::new(builder.build()), server_name).unwrap(); + let mut record = Vec::new(); + conn.write_tls(&mut record).unwrap(); + + // Skip the record (5) and handshake (4) headers, legacy_version (2) and random (32). + let hello = &record[9..]; + let u16_at = |i: usize| usize::from(u16::from_be_bytes([hello[i], hello[i + 1]])); + let mut i = 2 + 32; + i += 1 + usize::from(hello[i]); // legacy_session_id + i += 2 + u16_at(i); // cipher_suites + i += 1 + usize::from(hello[i]); // legacy_compression_methods + let end = i + 2 + u16_at(i); + i += 2; + let mut types = Vec::new(); + while i < end { + types.push(u16_at(i) as u16); + i += 4 + u16_at(i + 2); + } + types + } + + // TLS 1.3 forbids repeating an extension type, and BoringSSL-based servers (Google, Cloudflare) + // reject such a ClientHello with `decode_error` (#575). + #[test] + fn no_profile_sends_an_extension_type_twice() { + let profiles = [ + ("chrome_100", chrome_100::fingerprint()), + ("chrome_101", chrome_101::fingerprint()), + ("chrome_104", chrome_104::fingerprint()), + ("chrome_107", chrome_107::fingerprint()), + ("chrome_110", chrome_110::fingerprint()), + ("chrome_116", chrome_116::fingerprint()), + ("chrome_124", chrome_124::fingerprint()), + ("chrome_125", chrome_125::fingerprint()), + ("chrome_131", chrome_131::fingerprint()), + ("chrome_133", chrome_133::fingerprint()), + ("chrome_136", chrome_136::fingerprint()), + ("chrome_142", chrome_142::fingerprint()), + ("chrome_151", chrome_151::fingerprint()), + ("firefox_128", firefox_128::fingerprint()), + ("firefox_133", firefox_133::fingerprint()), + ("firefox_135", firefox_135::fingerprint()), + ("firefox_144", firefox_144::fingerprint()), + ("okhttp3", okhttp3::fingerprint()), + ("okhttp4", okhttp4::fingerprint()), + ("okhttp5", okhttp5::fingerprint()), + ("ios_18", ios_18::fingerprint()), + ]; + + let mut duplicates = Vec::new(); + for (name, fingerprint) in profiles { + let types = client_hello_extension_types(fingerprint); + assert!( + types.contains(&0x0000), + "{name}: no server_name, ClientHello not parsed" + ); + for (i, typ) in types.iter().enumerate() { + if types[..i].contains(typ) { + duplicates.push(format!("{name}: {typ:#06x}")); + } + } + } + assert!( + duplicates.is_empty(), + "repeated ClientHello extensions: {duplicates:?}" + ); + } +} diff --git a/impit/src/fingerprint/database/chrome.rs b/impit/src/fingerprint/database/chrome.rs index 55dfc332..584d9ea6 100644 --- a/impit/src/fingerprint/database/chrome.rs +++ b/impit/src/fingerprint/database/chrome.rs @@ -945,7 +945,8 @@ pub mod chrome_100 { ExtensionType::SupportedVersions, ExtensionType::CompressCertificate, ExtensionType::ApplicationSettings, - ExtensionType::Grease, + // No trailing GREASE: rustls has one GREASE extension type, and repeating it + // makes BoringSSL servers reject the ClientHello (#575). ExtensionType::Padding, ], ) diff --git a/impit/src/fingerprint/database/safari.rs b/impit/src/fingerprint/database/safari.rs index 9d900625..f9b5e37b 100644 --- a/impit/src/fingerprint/database/safari.rs +++ b/impit/src/fingerprint/database/safari.rs @@ -105,7 +105,8 @@ pub mod ios_18 { ExtensionType::PskKeyExchangeModes, ExtensionType::SupportedVersions, ExtensionType::CompressCertificate, - ExtensionType::Grease, + // No trailing GREASE: rustls has one GREASE extension type, and repeating it + // makes BoringSSL servers reject the ClientHello (#575). ], ) .with_session_ticket(false),