diff --git a/Cargo.lock b/Cargo.lock index 18d66f6c..a7ad1e34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -919,8 +919,8 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.7" -source = "git+https://github.com/apify/h2?rev=7f393a728a8db07cabb1b78d2094772b33943b9a#7f393a728a8db07cabb1b78d2094772b33943b9a" +version = "0.4.13" +source = "git+https://github.com/apify/h2?rev=0c053aae81ba3be37511d7e2f472203588c8c025#0c053aae81ba3be37511d7e2f472203588c8c025" dependencies = [ "atomic-waker", "bytes", @@ -999,7 +999,7 @@ dependencies = [ "hickory-proto", "once_cell", "radix_trie", - "rand", + "rand 0.9.2", "thiserror 2.0.18", "tokio", "tracing", @@ -1021,7 +1021,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand", + "rand 0.9.2", "ring", "thiserror 2.0.18", "tinyvec", @@ -1245,6 +1245,7 @@ dependencies = [ "log", "lol_html", "mime", + "rand 0.8.5", "reqwest", "rustls", "rustls-platform-verifier", @@ -1907,7 +1908,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.2", "ring", "rustc-hash", "rustls", @@ -1958,14 +1959,35 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -1975,7 +1997,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -2088,7 +2119,7 @@ dependencies = [ [[package]] name = "rustls" version = "0.23.37" -source = "git+https://github.com/apify/rustls?rev=c908559019c6521fbc4b487317a4ea24088a99f3#c908559019c6521fbc4b487317a4ea24088a99f3" +source = "git+https://github.com/apify/rustls?branch=impit-main#d19ecc2b4e38978c1c3acabc98a221fa98b9314f" dependencies = [ "aws-lc-rs", "brotli", diff --git a/Cargo.toml b/Cargo.toml index e83548bf..42718613 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ members = [ ] [patch.crates-io] -h2 = { git = "https://github.com/apify/h2", rev = "7f393a728a8db07cabb1b78d2094772b33943b9a" } -rustls = { git = "https://github.com/apify/rustls", rev="c908559019c6521fbc4b487317a4ea24088a99f3" } +h2 = { git = "https://github.com/apify/h2", rev = "0c053aae81ba3be37511d7e2f472203588c8c025" } +rustls = { git = "https://github.com/apify/rustls", branch = "impit-main" } tower-http = { git = "https://github.com/apify/tower-http", rev="f9efc0d9193e774d33aedc1022b922efefc22052" } hyper-util = { git = "https://github.com/apify/hyper-util", rev="9b7795dfd7158fc55e7c84b65bf1dae1d2dea67d" } diff --git a/impit/Cargo.toml b/impit/Cargo.toml index e5cb51fc..1bc9cd40 100644 --- a/impit/Cargo.toml +++ b/impit/Cargo.toml @@ -10,6 +10,7 @@ encoding = "0.2.33" hickory-client = "0.25.1" hickory-proto = "0.25.1" log = "0.4.22" +rand = "0.8" mime = "0.3.17" reqwest = { version="0.13.1", features = ["json", "gzip", "brotli", "zstd", "deflate", "http3", "cookies", "stream", "socks"] } rustls = { version="0.23.36", features=["impit"] } diff --git a/impit/h2 b/impit/h2 new file mode 160000 index 00000000..0c053aae --- /dev/null +++ b/impit/h2 @@ -0,0 +1 @@ +Subproject commit 0c053aae81ba3be37511d7e2f472203588c8c025 diff --git a/impit/rustls b/impit/rustls new file mode 160000 index 00000000..43960225 --- /dev/null +++ b/impit/rustls @@ -0,0 +1 @@ +Subproject commit 439602253d6bd119b80443a99ecce125e85aafa4 diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs index afd9da6d..35ab4865 100644 --- a/impit/src/fingerprint/database.rs +++ b/impit/src/fingerprint/database.rs @@ -3,10 +3,18 @@ //! This module contains fingerprint definitions for various browsers. mod chrome; +mod edge; mod firefox; +mod safari; pub use chrome::{ chrome_100, chrome_101, chrome_104, chrome_107, chrome_110, chrome_116, chrome_124, chrome_125, chrome_131, chrome_133, chrome_136, chrome_142, }; +pub use chrome::chrome_headers_for_os; +pub use edge::{edge_131, edge_136}; +pub use edge::edge_headers_for_os; pub use firefox::{firefox_128, firefox_133, firefox_135, firefox_144}; +pub use firefox::firefox_headers_for_os; +pub use safari::{safari_17_0, safari_17_2_ios, safari_18_0, safari_18_4}; +pub use safari::safari_headers_for_os; diff --git a/impit/src/fingerprint/database/chrome.rs b/impit/src/fingerprint/database/chrome.rs index 47848f3e..156bc01b 100644 --- a/impit/src/fingerprint/database/chrome.rs +++ b/impit/src/fingerprint/database/chrome.rs @@ -2,6 +2,52 @@ use crate::fingerprint::*; +/// Helper to create OS-specific Chrome headers for a given version. +/// This provides Windows, macOS, and Linux header variants to increase +/// fingerprint diversity beyond a single OS profile. +pub fn chrome_headers_for_os(version: &str, os: &str) -> Vec<(String, String)> { + let (ua_os, platform) = match os { + "windows" => ("Windows NT 10.0; Win64; x64", "\"Windows\""), + "linux" => ("X11; Linux x86_64", "\"Linux\""), + _ => ("Macintosh; Intel Mac OS X 10_15_7", "\"macOS\""), + }; + + let sec_ch_ua = match version { + "142" => format!("\"Chromium\";v=\"142\", \"Google Chrome\";v=\"142\", \"Not_A Brand\";v=\"99\""), + "136" => format!("\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\""), + "133" => format!("\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\""), + "131" => format!("\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""), + _ => format!("\"Chromium\";v=\"{version}\", \"Google Chrome\";v=\"{version}\", \"Not_A Brand\";v=\"99\""), + }; + + let ua = format!( + "Mozilla/5.0 ({ua_os}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version}.0.0.0 Safari/537.36" + ); + + let mut headers = vec![ + ("sec-ch-ua".to_string(), sec_ch_ua), + ("sec-ch-ua-mobile".to_string(), "?0".to_string()), + ("sec-ch-ua-platform".to_string(), platform.to_string()), + ("upgrade-insecure-requests".to_string(), "1".to_string()), + ("user-agent".to_string(), ua), + ("accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-user".to_string(), "?1".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("accept-encoding".to_string(), "gzip, deflate, br, zstd".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), + ]; + + // Chrome 125+ includes priority header + let ver_num: u32 = version.parse().unwrap_or(0); + if ver_num >= 125 { + headers.push(("priority".to_string(), "u=0, i".to_string())); + } + + headers +} + /// Chrome 142 fingerprint module pub mod chrome_142 { use super::*; @@ -17,13 +63,25 @@ pub mod chrome_142 { ) } + /// Returns Chrome 142 fingerprint with OS-specific headers. + /// `os` can be "windows", "macos", or "linux". + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Chrome", + "142", + tls_fingerprint(), + http2_fingerprint(), + super::chrome_headers_for_os("142", os), + ) + } + /// Chrome 142 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( // Cipher suites in Chrome 142 preference order // GREASE cipher at position 1 (first) - same as Chrome 136 vec![ - CipherSuite::Grease, + CipherSuite::Grease(0x0a0a), CipherSuite::TLS13_AES_128_GCM_SHA256, CipherSuite::TLS13_AES_256_GCM_SHA384, CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, @@ -43,7 +101,7 @@ pub mod chrome_142 { // Key exchange groups (includes post-quantum hybrid X25519MLKEM768) // GREASE at position 1 (first) - same as Chrome 136 vec![ - KeyExchangeGroup::Grease, + KeyExchangeGroup::Grease(0x8a8a), KeyExchangeGroup::X25519MLKEM768, KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1, @@ -93,7 +151,8 @@ pub mod chrome_142 { ExtensionType::ApplicationSettings, ], ) - .with_new_alps_codepoint(true), + .with_new_alps_codepoint(true) + .with_permute_extensions(true), // Chrome 110+ randomizes extension order // ECH configuration (GREASE mode) Some(EchConfig::new( EchMode::Grease { @@ -114,12 +173,14 @@ pub mod chrome_142 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(6_291_456), - initial_connection_window_size: Some(15_663_105), - max_header_list_size: Some(262_144), + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), } } @@ -158,13 +219,24 @@ pub mod chrome_136 { ) } + /// Returns Chrome 136 fingerprint with OS-specific headers. + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Chrome", + "136", + tls_fingerprint(), + http2_fingerprint(), + super::chrome_headers_for_os("136", os), + ) + } + /// Chrome 136 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( // Cipher suites in Chrome 136 preference order // GREASE cipher at position 1 (first) based on Wireshark capture vec![ - CipherSuite::Grease, + CipherSuite::Grease(0x0a0a), CipherSuite::TLS13_AES_128_GCM_SHA256, CipherSuite::TLS13_AES_256_GCM_SHA384, CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, @@ -184,7 +256,7 @@ pub mod chrome_136 { // Key exchange groups (includes post-quantum hybrid X25519MLKEM768) // GREASE at position 1 (first) based on Wireshark capture vec![ - KeyExchangeGroup::Grease, + KeyExchangeGroup::Grease(0x8a8a), KeyExchangeGroup::X25519MLKEM768, KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1, @@ -234,7 +306,8 @@ pub mod chrome_136 { ExtensionType::ApplicationSettings, ], ) - .with_new_alps_codepoint(true), + .with_new_alps_codepoint(true) + .with_permute_extensions(true), // Chrome 110+ randomizes extension order // ECH configuration (GREASE mode) Some(EchConfig::new( EchMode::Grease { @@ -255,12 +328,14 @@ pub mod chrome_136 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(6_291_456), - initial_connection_window_size: Some(15_663_105), - max_header_list_size: Some(262_144), + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), } } @@ -299,6 +374,17 @@ pub mod chrome_133 { ) } + /// Returns Chrome 133 fingerprint with OS-specific headers. + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Chrome", + "133", + tls_fingerprint(), + http2_fingerprint(), + super::chrome_headers_for_os("133", os), + ) + } + /// Chrome 133 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( @@ -369,7 +455,8 @@ pub mod chrome_133 { ExtensionType::CompressCertificate, ExtensionType::ApplicationSettings, ], - ), + ) + .with_permute_extensions(true), // Chrome 110+ randomizes extension order // ECH configuration (GREASE mode) Some(EchConfig::new( EchMode::Grease { @@ -390,12 +477,14 @@ pub mod chrome_133 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(6_291_456), - initial_connection_window_size: Some(15_663_105), - max_header_list_size: Some(262_144), + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), } } @@ -504,7 +593,8 @@ pub mod chrome_124 { ExtensionType::CompressCertificate, ExtensionType::ApplicationSettings, ], - ), + ) + .with_permute_extensions(true), // Chrome 110+ randomizes extension order // ECH configuration (GREASE mode) Some(EchConfig::new( EchMode::Grease { @@ -525,12 +615,14 @@ pub mod chrome_124 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(6_291_456), - initial_connection_window_size: Some(15_663_105), - max_header_list_size: Some(262_144), + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), } } @@ -569,16 +661,27 @@ pub mod chrome_131 { ) } + /// Returns Chrome 131 fingerprint with OS-specific headers. + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Chrome", + "131", + tls_fingerprint(), + http2_fingerprint(), + super::chrome_headers_for_os("131", os), + ) + } + /// Chrome 131 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( - // Cipher suites in Chrome 131 preference order (matching CHROME_CIPHER_SUITES) + // Cipher suites in Chrome 131 preference order + // GREASE at end (Chrome 131 places GREASE after real suites) vec![ CipherSuite::TLS13_AES_128_GCM_SHA256, CipherSuite::TLS13_AES_256_GCM_SHA384, CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite::Grease, CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, @@ -590,6 +693,7 @@ pub mod chrome_131 { CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::Grease(0x0a0a), // GREASE at end for Chrome 131 ], // Key exchange groups (includes post-quantum hybrid X25519MLKEM768) vec![ @@ -597,7 +701,7 @@ pub mod chrome_131 { KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1, KeyExchangeGroup::Secp384r1, - KeyExchangeGroup::Grease, + KeyExchangeGroup::Grease(0x8a8a), ], // Signature algorithms - order must match DEFAULT_SIGNATURE_VERIFICATION_ALGOS // Note: No SHA1 algorithms for Chrome (matches original implementation) @@ -642,7 +746,8 @@ pub mod chrome_131 { ExtensionType::CompressCertificate, ExtensionType::ApplicationSettings, ], - ), + ) + .with_permute_extensions(true), // Chrome 110+ randomizes extension order // ECH configuration (GREASE mode) Some(EchConfig::new( EchMode::Grease { @@ -663,12 +768,14 @@ pub mod chrome_131 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(6_291_456), - initial_connection_window_size: Some(15_663_105), - max_header_list_size: Some(262_144), + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), } } @@ -711,7 +818,7 @@ pub mod chrome_100 { pub(crate) fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( vec![ - CipherSuite::Grease, + CipherSuite::Grease(0x0a0a), CipherSuite::TLS13_AES_128_GCM_SHA256, CipherSuite::TLS13_AES_256_GCM_SHA384, CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, @@ -729,7 +836,7 @@ pub mod chrome_100 { CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, ], vec![ - KeyExchangeGroup::Grease, + KeyExchangeGroup::Grease(0x8a8a), KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1, KeyExchangeGroup::Secp384r1, @@ -759,7 +866,7 @@ pub mod chrome_100 { false, None, vec![ - ExtensionType::Grease, + ExtensionType::Grease(0xbaba), ExtensionType::ServerName, ExtensionType::ExtendedMasterSecret, ExtensionType::RenegotiationInfo, @@ -775,7 +882,7 @@ pub mod chrome_100 { ExtensionType::SupportedVersions, ExtensionType::CompressCertificate, ExtensionType::ApplicationSettings, - ExtensionType::Grease, + ExtensionType::Grease(0xbaba), ExtensionType::Padding, ], ) @@ -795,12 +902,14 @@ pub mod chrome_100 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(6_291_456), - initial_connection_window_size: Some(15_663_105), - max_header_list_size: Some(262_144), + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), } } @@ -1008,7 +1117,7 @@ pub mod chrome_125 { TlsFingerprint::new( // Cipher suites in Chrome 125 preference order (matching CHROME_CIPHER_SUITES) vec![ - CipherSuite::Grease, + CipherSuite::Grease(0x0a0a), CipherSuite::TLS13_AES_128_GCM_SHA256, CipherSuite::TLS13_AES_256_GCM_SHA384, CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, @@ -1030,7 +1139,7 @@ pub mod chrome_125 { KeyExchangeGroup::X25519, KeyExchangeGroup::Secp256r1, KeyExchangeGroup::Secp384r1, - KeyExchangeGroup::Grease, + KeyExchangeGroup::Grease(0x8a8a), ], // Signature algorithms - order must match DEFAULT_SIGNATURE_VERIFICATION_ALGOS // Note: No SHA1 algorithms for Chrome (matches original implementation) @@ -1075,7 +1184,8 @@ pub mod chrome_125 { ExtensionType::CompressCertificate, ExtensionType::ApplicationSettings, ], - ), + ) + .with_permute_extensions(true), // Chrome 110+ randomizes extension order // ECH configuration (GREASE mode) Some(EchConfig::new( EchMode::Grease { @@ -1096,12 +1206,14 @@ pub mod chrome_125 { ":authority".to_string(), ":scheme".to_string(), ":path".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(6_291_456), - initial_connection_window_size: Some(15_663_105), - max_header_list_size: Some(262_144), + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), } } diff --git a/impit/src/fingerprint/database/edge.rs b/impit/src/fingerprint/database/edge.rs new file mode 100644 index 00000000..46d7e828 --- /dev/null +++ b/impit/src/fingerprint/database/edge.rs @@ -0,0 +1,341 @@ +//! Microsoft Edge browser fingerprints +//! +//! Edge is Chromium-based so the TLS fingerprint is identical to Chrome. +//! The key differences are in HTTP headers: +//! - `sec-ch-ua` includes "Microsoft Edge" brand instead of "Google Chrome" +//! - `User-Agent` includes "Edg/" suffix +//! - Different "Not A Brand" version strings + +use crate::fingerprint::*; + +/// Helper to create OS-specific Edge headers for a given version. +pub fn edge_headers_for_os(version: &str, os: &str) -> Vec<(String, String)> { + let (ua_os, platform) = match os { + "macos" => ("Macintosh; Intel Mac OS X 10_15_7", "\"macOS\""), + "linux" => ("X11; Linux x86_64", "\"Linux\""), + _ => ("Windows NT 10.0; Win64; x64", "\"Windows\""), + }; + + let sec_ch_ua = match version { + "136" => "\"Microsoft Edge\";v=\"136\", \"Chromium\";v=\"136\", \"Not.A/Brand\";v=\"99\"".to_string(), + "131" => "\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"".to_string(), + _ => format!("\"Microsoft Edge\";v=\"{version}\", \"Chromium\";v=\"{version}\", \"Not_A Brand\";v=\"99\""), + }; + + let ua = format!( + "Mozilla/5.0 ({ua_os}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version}.0.0.0 Safari/537.36 Edg/{version}.0.0.0" + ); + + vec![ + ("sec-ch-ua".to_string(), sec_ch_ua), + ("sec-ch-ua-mobile".to_string(), "?0".to_string()), + ("sec-ch-ua-platform".to_string(), platform.to_string()), + ("upgrade-insecure-requests".to_string(), "1".to_string()), + ("user-agent".to_string(), ua), + ("accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-user".to_string(), "?1".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("accept-encoding".to_string(), "gzip, deflate, br, zstd".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), + ("priority".to_string(), "u=0, i".to_string()), + ] +} + +/// Edge 136 fingerprint module +/// Uses Chrome 136 TLS (same Chromium version) with Edge-specific headers +pub mod edge_136 { + use super::*; + + /// Returns the complete Edge 136 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Edge", + "136", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Returns Edge 136 fingerprint with OS-specific headers. + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Edge", + "136", + tls_fingerprint(), + http2_fingerprint(), + super::edge_headers_for_os("136", os), + ) + } + + /// Edge 136 TLS fingerprint (identical to Chrome 136 - same Chromium engine) + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Same cipher suites as Chrome 136 + vec![ + CipherSuite::Grease(0x0a0a), + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + ], + // Same key exchange groups as Chrome 136 (includes post-quantum) + vec![ + KeyExchangeGroup::Grease(0x8a8a), + KeyExchangeGroup::X25519MLKEM768, + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + ], + // Same signature algorithms as Chrome 136 + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha512, + ], + // Same extensions as Chrome 136 (with new ALPS codepoint) + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + true, // signed_certificate_timestamp + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + Some(vec![CertificateCompressionAlgorithm::Brotli]), // compress_certificate + true, // application_settings + false, // delegated_credentials + None, // record_size_limit + // Same extension order as Chrome 136 + vec![ + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::SessionTicket, + ExtensionType::SignatureAlgorithms, + ExtensionType::StatusRequest, + ExtensionType::SupportedGroups, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::SignedCertificateTimestamp, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ExtensionType::CompressCertificate, + ExtensionType::ApplicationSettings, + ], + ) + .with_new_alps_codepoint(true) + .with_permute_extensions(true), // Chromium-based: randomizes extension order + // ECH configuration (GREASE mode) + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + // ALPN protocols + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + /// Edge 136 HTTP/2 fingerprint (same as Chrome) + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ], + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), + } + } + + /// Edge 136 HTTP headers - key difference: sec-ch-ua and User-Agent identify as Edge + fn headers() -> Vec<(String, String)> { + vec![ + ("sec-ch-ua".to_string(), "\"Microsoft Edge\";v=\"136\", \"Chromium\";v=\"136\", \"Not.A/Brand\";v=\"99\"".to_string()), + ("sec-ch-ua-mobile".to_string(), "?0".to_string()), + ("sec-ch-ua-platform".to_string(), "\"Windows\"".to_string()), + ("upgrade-insecure-requests".to_string(), "1".to_string()), + ("user-agent".to_string(), "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0".to_string()), + ("accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-user".to_string(), "?1".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("accept-encoding".to_string(), "gzip, deflate, br, zstd".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), + ("priority".to_string(), "u=0, i".to_string()), + ] + } +} + +/// Edge 131 fingerprint module +pub mod edge_131 { + use super::*; + + /// Returns the complete Edge 131 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Edge", + "131", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Returns Edge 131 fingerprint with OS-specific headers. + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Edge", + "131", + tls_fingerprint(), + http2_fingerprint(), + super::edge_headers_for_os("131", os), + ) + } + + /// Edge 131 TLS fingerprint (based on Chrome 131 Chromium engine) + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + vec![ + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::Grease(0x0a0a), + ], + vec![ + KeyExchangeGroup::X25519MLKEM768, + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Grease(0x8a8a), + ], + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha512, + ], + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + true, // signed_certificate_timestamp + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + Some(vec![CertificateCompressionAlgorithm::Brotli]), // compress_certificate + true, // application_settings + false, // delegated_credentials + None, // record_size_limit + vec![ + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::SessionTicket, + ExtensionType::SignatureAlgorithms, + ExtensionType::StatusRequest, + ExtensionType::SupportedGroups, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::SignedCertificateTimestamp, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ExtensionType::CompressCertificate, + ExtensionType::ApplicationSettings, + ], + ) + .with_permute_extensions(true), // Chromium-based: randomizes extension order + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + /// Edge 131 HTTP/2 fingerprint + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ], + settings_header_table_size: Some(65536), + settings_enable_push: Some(false), + settings_max_concurrent_streams: Some(1000), + settings_initial_window_size: Some(6291456), + settings_max_frame_size: None, + settings_max_header_list_size: Some(262144), + connection_window_size_increment: Some(15663105), + } + } + + /// Edge 131 HTTP headers + fn headers() -> Vec<(String, String)> { + vec![ + ("sec-ch-ua".to_string(), "\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"".to_string()), + ("sec-ch-ua-mobile".to_string(), "?0".to_string()), + ("sec-ch-ua-platform".to_string(), "\"Windows\"".to_string()), + ("upgrade-insecure-requests".to_string(), "1".to_string()), + ("user-agent".to_string(), "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0".to_string()), + ("accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-user".to_string(), "?1".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("accept-encoding".to_string(), "gzip, deflate, br, zstd".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), + ("priority".to_string(), "u=0, i".to_string()), + ] + } +} diff --git a/impit/src/fingerprint/database/firefox.rs b/impit/src/fingerprint/database/firefox.rs index 5ccc8561..631276cf 100644 --- a/impit/src/fingerprint/database/firefox.rs +++ b/impit/src/fingerprint/database/firefox.rs @@ -2,6 +2,50 @@ use crate::fingerprint::*; +/// Helper to create OS-specific Firefox headers for a given version. +/// Firefox uses the same TLS fingerprint (NSS) across all platforms, +/// but the User-Agent string and some header casing differs by OS. +pub fn firefox_headers_for_os(version: &str, os: &str) -> Vec<(String, String)> { + let ua_os = match os { + "windows" => "Windows NT 10.0; Win64; x64".to_string(), + "linux" => "X11; Linux x86_64".to_string(), + // macOS — Firefox uses dot notation (10.15) unlike Chrome (10_15_7) + _ => "Macintosh; Intel Mac OS X 10.15".to_string(), + }; + + let ua = format!( + "Mozilla/5.0 ({ua_os}; rv:{version}.0) Gecko/20100101 Firefox/{version}.0" + ); + + let ver_num: u32 = version.parse().unwrap_or(0); + + let accept_encoding = if ver_num >= 133 { + "gzip, deflate, br, zstd" + } else { + "gzip, deflate, br" + }; + + let mut headers = vec![ + ("user-agent".to_string(), ua), + ("accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.5".to_string()), + ("accept-encoding".to_string(), accept_encoding.to_string()), + ("upgrade-insecure-requests".to_string(), "1".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-user".to_string(), "?1".to_string()), + ("Priority".to_string(), "u=0, i".to_string()), + ]; + + // Firefox 133+ includes te: trailers + if ver_num >= 133 { + headers.push(("te".to_string(), "trailers".to_string())); + } + + headers +} + /// Firefox 128 fingerprint module pub mod firefox_128 { use super::*; @@ -17,6 +61,18 @@ pub mod firefox_128 { ) } + /// Returns Firefox 128 fingerprint with OS-specific headers. + /// `os` can be "windows", "macos", or "linux". + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Firefox", + "128", + tls_fingerprint(), + http2_fingerprint(), + super::firefox_headers_for_os("128", os), + ) + } + /// Firefox 128 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( @@ -117,12 +173,14 @@ pub mod firefox_128 { ":path".to_string(), ":authority".to_string(), ":scheme".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(131_072), - initial_connection_window_size: Some(12_517_377), - max_header_list_size: None, + settings_header_table_size: Some(65536), + settings_enable_push: None, + settings_max_concurrent_streams: None, + settings_initial_window_size: Some(131072), + settings_max_frame_size: Some(16384), + settings_max_header_list_size: None, + connection_window_size_increment: Some(12517377), } } @@ -158,6 +216,18 @@ pub mod firefox_133 { ) } + /// Returns Firefox 133 fingerprint with OS-specific headers. + /// `os` can be "windows", "macos", or "linux". + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Firefox", + "133", + tls_fingerprint(), + http2_fingerprint(), + super::firefox_headers_for_os("133", os), + ) + } + /// Firefox 133 TLS fingerprint fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( @@ -258,12 +328,14 @@ pub mod firefox_133 { ":path".to_string(), ":authority".to_string(), ":scheme".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(131_072), - initial_connection_window_size: Some(12_517_377), - max_header_list_size: None, + settings_header_table_size: Some(65536), + settings_enable_push: None, + settings_max_concurrent_streams: None, + settings_initial_window_size: Some(131072), + settings_max_frame_size: Some(16384), + settings_max_header_list_size: None, + connection_window_size_increment: Some(12517377), } } @@ -300,6 +372,18 @@ pub mod firefox_135 { ) } + /// Returns Firefox 135 fingerprint with OS-specific headers. + /// `os` can be "windows", "macos", or "linux". + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Firefox", + "135", + tls_fingerprint(), + http2_fingerprint(), + super::firefox_headers_for_os("135", os), + ) + } + /// Firefox 135 TLS fingerprint pub(crate) fn tls_fingerprint() -> TlsFingerprint { TlsFingerprint::new( @@ -404,12 +488,14 @@ pub mod firefox_135 { ":path".to_string(), ":authority".to_string(), ":scheme".to_string(), - ":protocol".to_string(), - ":status".to_string(), ], - initial_stream_window_size: Some(131_072), - initial_connection_window_size: Some(12_517_377), - max_header_list_size: None, + settings_header_table_size: Some(65536), + settings_enable_push: None, + settings_max_concurrent_streams: None, + settings_initial_window_size: Some(131072), + settings_max_frame_size: Some(16384), + settings_max_header_list_size: None, + connection_window_size_increment: Some(12517377), } } @@ -447,6 +533,18 @@ pub mod firefox_144 { ) } + /// Returns Firefox 144 fingerprint with OS-specific headers. + /// `os` can be "windows", "macos", or "linux". + pub fn fingerprint_with_os(os: &str) -> BrowserFingerprint { + BrowserFingerprint::new( + "Firefox", + "144", + firefox_135::tls_fingerprint(), + firefox_135::http2_fingerprint(), + super::firefox_headers_for_os("144", os), + ) + } + /// Firefox 144 HTTP headers fn headers() -> Vec<(String, String)> { vec![ diff --git a/impit/src/fingerprint/database/safari.rs b/impit/src/fingerprint/database/safari.rs new file mode 100644 index 00000000..72a9b57e --- /dev/null +++ b/impit/src/fingerprint/database/safari.rs @@ -0,0 +1,531 @@ +//! Safari browser fingerprints +//! +//! Safari uses Apple's Network.framework TLS stack which produces a distinctly +//! different JA3/JA4 fingerprint from Chrome (BoringSSL) and Firefox (NSS). +//! Key differences: +//! - No GREASE in cipher suites (only in extensions via ECH GREASE) +//! - No session_ticket extension +//! - No compress_certificate +//! - No application_settings (ALPS) +//! - No signed_certificate_timestamp +//! - No post-quantum key exchange groups +//! - Uses ec_point_formats extension +//! - Different cipher suite ordering (ECDSA prioritized differently) + +use crate::fingerprint::*; + +/// Helper to create macOS-version-specific Safari headers. +/// Safari only runs on macOS/iOS, so the OS parameter here is a macOS version variant. +/// `os` is "macos" (the only valid OS for Safari — always macOS). +/// `macos_version` selects the macOS version string: "10_15_7", "13_6_9", "14_7_1", or "15_2". +pub fn safari_headers_for_os(version: &str, macos_version: Option<&str>) -> Vec<(String, String)> { + let mac_ver = macos_version.unwrap_or("10_15_7"); + let ua = format!( + "Mozilla/5.0 (Macintosh; Intel Mac OS X {mac_ver}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{version} Safari/605.1.15" + ); + + vec![ + ("User-Agent".to_string(), ua), + ("Accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string()), + ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()), + ("Accept-Encoding".to_string(), "gzip, deflate, br".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ] +} + +/// Safari 18.0 fingerprint module (macOS Sequoia 15.x) +/// Based on Safari 18.0 / WebKit on macOS Sequoia with Apple TLS stack +pub mod safari_18_0 { + use super::*; + + /// Returns the complete Safari 18.0 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "18.0", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Returns Safari 18.0 fingerprint with macOS-version-specific headers. + /// `os` should always be "macos" (Safari doesn't run on other OSes). + /// `macos_version` can be "10_15_7", "13_6_9", "14_7_1", or "15_2". + pub fn fingerprint_with_os(_os: &str, macos_version: Option<&str>) -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "18.0", + tls_fingerprint(), + http2_fingerprint(), + super::safari_headers_for_os("18.0", macos_version), + ) + } + + /// Safari 18.0 TLS fingerprint + pub(crate) fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Cipher suites in Safari 18.0 preference order + // Safari does NOT use GREASE in cipher suites (key differentiator from Chrome) + // TLS 1.3 suites first, then ECDHE-ECDSA, then ECDHE-RSA, then RSA + vec![ + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + ], + // Key exchange groups - Safari does NOT include post-quantum (no X25519MLKEM768) + // No GREASE in supported_groups either + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Secp521r1, + ], + // Signature algorithms - Safari includes Ed25519 and legacy SHA1 + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::EcdsaSecp521r1Sha512, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::EcdsaSha1Legacy, + SignatureAlgorithm::RsaPkcs1Sha1, + ], + // TLS extensions configuration + // Safari has very different extension set from Chrome: + // - No session_ticket + // - No compress_certificate + // - No application_settings (ALPS) + // - No signed_certificate_timestamp + // - No delegated_credentials + // - Uses ec_point_formats + TlsExtensions::new( + true, // server_name + true, // status_request (OCSP stapling) + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + false, // signed_certificate_timestamp (Safari doesn't send this) + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + None, // compress_certificate (Safari doesn't support cert compression) + false, // application_settings (Safari doesn't use ALPS) + false, // delegated_credentials (Safari doesn't use this) + None, // record_size_limit (Safari doesn't send this) + // Safari extension order is distinctly different from Chrome + vec![ + ExtensionType::Grease(0xbaba), + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::RenegotiationInfo, + ExtensionType::SupportedGroups, + ExtensionType::EcPointFormats, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::StatusRequest, + ExtensionType::SignatureAlgorithms, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ExtensionType::Grease(0xbaba), + ExtensionType::Padding, + ], + ) + .with_session_ticket(false) // Safari doesn't send session_ticket + .with_padding(true), // Safari uses padding extension + // ECH configuration (GREASE mode - Safari 17.0+ supports ECH GREASE) + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + // ALPN protocols + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + /// Safari 18.0 HTTP/2 fingerprint + /// Safari uses different pseudo-header order than Chrome + pub(crate) fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ":authority".to_string(), + ], + settings_header_table_size: Some(65536), + settings_enable_push: Some(true), + settings_max_concurrent_streams: Some(100), + settings_initial_window_size: Some(4194304), + settings_max_frame_size: Some(16384), + settings_max_header_list_size: None, + connection_window_size_increment: Some(10485760), + } + } + + /// Safari 18.0 HTTP headers (macOS Sequoia) + fn headers() -> Vec<(String, String)> { + vec![ + ("User-Agent".to_string(), "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15".to_string()), + ("Accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string()), + ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()), + ("Accept-Encoding".to_string(), "gzip, deflate, br".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ] + } +} + +/// Safari 18.4 fingerprint module (macOS Sequoia 15.4+) +pub mod safari_18_4 { + use super::*; + + /// Returns the complete Safari 18.4 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "18.4", + safari_18_0::tls_fingerprint(), // Same TLS as 18.0 + safari_18_0::http2_fingerprint(), + headers(), + ) + } + + /// Returns Safari 18.4 fingerprint with macOS-version-specific headers. + pub fn fingerprint_with_os(_os: &str, macos_version: Option<&str>) -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "18.4", + safari_18_0::tls_fingerprint(), + safari_18_0::http2_fingerprint(), + super::safari_headers_for_os("18.4", macos_version), + ) + } + + /// Safari 18.4 HTTP headers + fn headers() -> Vec<(String, String)> { + vec![ + ("User-Agent".to_string(), "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15".to_string()), + ("Accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string()), + ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()), + ("Accept-Encoding".to_string(), "gzip, deflate, br".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ] + } +} + +/// Safari 17.0 fingerprint module (macOS Sonoma) +pub mod safari_17_0 { + use super::*; + + /// Returns the complete Safari 17.0 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "17.0", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Returns Safari 17.0 fingerprint with macOS-version-specific headers. + pub fn fingerprint_with_os(_os: &str, macos_version: Option<&str>) -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "17.0", + tls_fingerprint(), + http2_fingerprint(), + super::safari_headers_for_os("17.0", macos_version), + ) + } + + /// Safari 17.0 TLS fingerprint + /// Slightly different from 18.0 - no ECH GREASE support + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Same cipher suite order as Safari 18.0 + vec![ + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + ], + // Key exchange groups - same as 18.0 + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Secp521r1, + ], + // Signature algorithms + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::EcdsaSecp521r1Sha512, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::EcdsaSha1Legacy, + SignatureAlgorithm::RsaPkcs1Sha1, + ], + // TLS extensions - Safari 17 has GREASE and padding + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + false, // signed_certificate_timestamp + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + None, // compress_certificate + false, // application_settings + false, // delegated_credentials + None, // record_size_limit + vec![ + ExtensionType::Grease(0xbaba), + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::RenegotiationInfo, + ExtensionType::SupportedGroups, + ExtensionType::EcPointFormats, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::StatusRequest, + ExtensionType::SignatureAlgorithms, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ExtensionType::Grease(0xbaba), + ExtensionType::Padding, + ], + ) + .with_session_ticket(false) + .with_padding(true), + // Safari 17.0 also uses ECH GREASE + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + // ALPN protocols + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + /// Safari 17.0 HTTP/2 fingerprint + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ":authority".to_string(), + ], + settings_header_table_size: Some(65536), + settings_enable_push: Some(true), + settings_max_concurrent_streams: Some(100), + settings_initial_window_size: Some(4194304), + settings_max_frame_size: Some(16384), + settings_max_header_list_size: None, + connection_window_size_increment: Some(10485760), + } + } + + /// Safari 17.0 HTTP headers (macOS Sonoma) + fn headers() -> Vec<(String, String)> { + vec![ + ("User-Agent".to_string(), "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15".to_string()), + ("Accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string()), + ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()), + ("Accept-Encoding".to_string(), "gzip, deflate, br".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ] + } +} + +/// Safari 17.2 iOS fingerprint module +pub mod safari_17_2_ios { + use super::*; + + /// Returns the complete Safari 17.2 iOS fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "17.2", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Returns Safari 17.2 iOS fingerprint with macOS-version-specific headers. + /// Note: iOS Safari ignores the macos_version parameter and uses default iOS UA. + pub fn fingerprint_with_os(_os: &str, _macos_version: Option<&str>) -> BrowserFingerprint { + // iOS Safari always uses its own UA format + fingerprint() + } + + /// Safari 17.2 iOS TLS fingerprint + /// iOS Safari has similar but not identical TLS to macOS Safari + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Same cipher suite order as macOS Safari + vec![ + CipherSuite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + ], + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Secp521r1, + ], + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::EcdsaSecp521r1Sha512, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::EcdsaSha1Legacy, + SignatureAlgorithm::RsaPkcs1Sha1, + ], + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + false, // signed_certificate_timestamp + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + None, // compress_certificate + false, // application_settings + false, // delegated_credentials + None, // record_size_limit + vec![ + ExtensionType::Grease(0xbaba), + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::RenegotiationInfo, + ExtensionType::SupportedGroups, + ExtensionType::EcPointFormats, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::StatusRequest, + ExtensionType::SignatureAlgorithms, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ExtensionType::Grease(0xbaba), + ExtensionType::Padding, + ], + ) + .with_session_ticket(false) + .with_padding(true), + Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )), + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + /// Safari 17.2 iOS HTTP/2 fingerprint + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ":authority".to_string(), + ], + settings_header_table_size: Some(65536), + settings_enable_push: Some(true), + settings_max_concurrent_streams: Some(100), + settings_initial_window_size: Some(4194304), + settings_max_frame_size: Some(16384), + settings_max_header_list_size: None, + connection_window_size_increment: Some(10485760), + } + } + + /// Safari 17.2 iOS HTTP headers + fn headers() -> Vec<(String, String)> { + vec![ + ("User-Agent".to_string(), "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1".to_string()), + ("Accept".to_string(), "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string()), + ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()), + ("Accept-Encoding".to_string(), "gzip, deflate, br".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-site".to_string(), "none".to_string()), + ] + } +} diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index ab260cc3..cb41fb46 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -34,6 +34,304 @@ impl BrowserFingerprint { headers, } } + + /// Produces a deterministically-mutated clone of this fingerprint based on + /// the given `seed`. Uses a seeded PRNG (StdRng) for unlimited randomization + /// dimensions while remaining fully deterministic for the same seed. + /// + /// ## Randomization dimensions + /// + /// | Dimension | Choices | JA4 impact | JA3 impact | + /// |-------------------------------|----------|------------------|------------| + /// | GREASE cipher value | 16 | No (excluded) | Yes | + /// | GREASE key exchange value | 16 | No (excluded) | Yes | + /// | GREASE extension value | 16 | No (excluded) | Yes | + /// | Optional TLS 1.2 ciphers (8) | 2^8=256 | Yes | Yes | + /// | Optional extensions (6) | 2^6=64 | Yes | Yes | + /// | Optional sig algs (6) | 2^6=64 | Yes (extHash) | Yes | + /// | Key exchange group toggle (2) | 2^2=4 | Yes | Yes | + /// | ALPS codepoint (old/new) | 2 | Yes | Yes | + /// | ECH GREASE toggle | 2 | Yes | Yes | + /// | TLS 1.2 cipher order shuffle | many | No | Yes | + /// | Header micro-variations | many | N/A | N/A | + /// + /// **JA4 diversity per profile**: ~16.7 million unique hashes + /// **JA3 diversity**: practically unlimited (GREASE + order permutations) + /// **With 15 profiles**: ~250 million unique JA4 hashes + pub fn randomize(&self, seed: u64) -> BrowserFingerprint { + use rand::rngs::StdRng; + use rand::seq::SliceRandom; + use rand::{Rng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + let mut fp = self.clone(); + + // ── RFC 8701 GREASE values ────────────────────────────────────── + const GREASE_VALUES: [u16; 16] = [ + 0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, + 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa, + ]; + + // ── 1. Randomize GREASE values ────────────────────────────────── + // Each GREASE slot gets a random value from the 16 RFC 8701 values. + // This affects JA3 (which includes GREASE) but not JA4 (which excludes it). + let grease_cipher_val = GREASE_VALUES[rng.gen_range(0..16)]; + let grease_kex_val = GREASE_VALUES[rng.gen_range(0..16)]; + let grease_ext_val = GREASE_VALUES[rng.gen_range(0..16)]; + + // Replace GREASE cipher suite values + for cs in &mut fp.tls.cipher_suites { + if matches!(cs, CipherSuite::Grease(_)) { + *cs = CipherSuite::Grease(grease_cipher_val); + } + } + + // Replace GREASE key exchange group values + for kg in &mut fp.tls.key_exchange_groups { + if matches!(kg, KeyExchangeGroup::Grease(_)) { + *kg = KeyExchangeGroup::Grease(grease_kex_val); + } + } + + // Replace GREASE extension type values + for ext in &mut fp.tls.extensions.extension_order { + if matches!(ext, ExtensionType::Grease(_)) { + *ext = ExtensionType::Grease(grease_ext_val); + } + } + + // ── 2. Optional TLS 1.2 cipher suites (8 toggleable) ─────────── + // These legacy/CBC/RSA-only suites can be independently toggled. + // Core TLS 1.3 + ECDHE-GCM/ChaCha20 suites always remain. + let optional_ciphers: &[CipherSuite] = &[ + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + ]; + + for cipher in optional_ciphers { + if rng.gen_bool(0.5) { + fp.tls.cipher_suites.retain(|c| c != cipher); + } + } + + // ── 3. Optional TLS extensions (6 toggleable) ─────────────────── + // compress_certificate + if rng.gen_bool(0.5) { + if fp.tls.extensions.compress_certificate.is_some() { + fp.tls.extensions.compress_certificate = None; + fp.tls.extensions.extension_order + .retain(|e| *e != ExtensionType::CompressCertificate); + } else { + fp.tls.extensions.compress_certificate = + Some(vec![CertificateCompressionAlgorithm::Brotli]); + if !fp.tls.extensions.extension_order + .contains(&ExtensionType::CompressCertificate) + { + let len = fp.tls.extensions.extension_order.len(); + fp.tls.extensions.extension_order + .insert(len.saturating_sub(1), ExtensionType::CompressCertificate); + } + } + } + + // application_settings (ALPS) + if rng.gen_bool(0.5) { + fp.tls.extensions.application_settings = !fp.tls.extensions.application_settings; + if !fp.tls.extensions.application_settings { + fp.tls.extensions.extension_order + .retain(|e| *e != ExtensionType::ApplicationSettings); + } else if !fp.tls.extensions.extension_order + .contains(&ExtensionType::ApplicationSettings) + { + let len = fp.tls.extensions.extension_order.len(); + fp.tls.extensions.extension_order + .insert(len.saturating_sub(1), ExtensionType::ApplicationSettings); + } + } + + // signed_certificate_timestamp + if rng.gen_bool(0.5) { + fp.tls.extensions.signed_certificate_timestamp = + !fp.tls.extensions.signed_certificate_timestamp; + if !fp.tls.extensions.signed_certificate_timestamp { + fp.tls.extensions.extension_order + .retain(|e| *e != ExtensionType::SignedCertificateTimestamp); + } else if !fp.tls.extensions.extension_order + .contains(&ExtensionType::SignedCertificateTimestamp) + { + let len = fp.tls.extensions.extension_order.len(); + fp.tls.extensions.extension_order + .insert(len.saturating_sub(1), ExtensionType::SignedCertificateTimestamp); + } + } + + // delegated_credentials + if rng.gen_bool(0.5) { + fp.tls.extensions.delegated_credentials = !fp.tls.extensions.delegated_credentials; + } + + // padding extension + if rng.gen_bool(0.5) { + fp.tls.extensions.padding = !fp.tls.extensions.padding; + if fp.tls.extensions.padding { + if !fp.tls.extensions.extension_order.contains(&ExtensionType::Padding) { + fp.tls.extensions.extension_order.push(ExtensionType::Padding); + } + } else { + fp.tls.extensions.extension_order + .retain(|e| *e != ExtensionType::Padding); + } + } + + // record_size_limit + if rng.gen_bool(0.5) { + if fp.tls.extensions.record_size_limit.is_some() { + fp.tls.extensions.record_size_limit = None; + } else { + // Firefox uses 16385, some use 4096 + fp.tls.extensions.record_size_limit = Some(if rng.gen_bool(0.7) { 16385 } else { 4096 }); + } + } + + // ── 4. Optional signature algorithms (6 toggleable) ───────────── + let optional_sigalgs: &[SignatureAlgorithm] = &[ + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::Ed25519, + SignatureAlgorithm::EcdsaSha1Legacy, + SignatureAlgorithm::RsaPkcs1Sha1, + ]; + + for sigalg in optional_sigalgs { + if rng.gen_bool(0.5) { + fp.tls.signature_algorithms.retain(|s| s != sigalg); + } + } + + // ── 5. Key exchange group variations ──────────────────────────── + // Toggle post-quantum hybrid (X25519MLKEM768) - only present in Chrome 142+ + if rng.gen_bool(0.5) { + if fp.tls.key_exchange_groups.contains(&KeyExchangeGroup::X25519MLKEM768) { + fp.tls.key_exchange_groups.retain(|g| *g != KeyExchangeGroup::X25519MLKEM768); + } + } + + // Toggle Secp384r1 + if rng.gen_bool(0.5) { + if fp.tls.key_exchange_groups.contains(&KeyExchangeGroup::Secp384r1) { + fp.tls.key_exchange_groups.retain(|g| *g != KeyExchangeGroup::Secp384r1); + } + } + + // ── 6. ALPS codepoint toggle (old 17513 vs new 17613) ─────────── + // Chrome 136+ uses new, older versions use old. Toggling changes + // the extension type code in JA4. + if rng.gen_bool(0.5) && fp.tls.extensions.application_settings { + fp.tls.extensions.use_new_alps_codepoint = !fp.tls.extensions.use_new_alps_codepoint; + } + + // ── 7. ECH GREASE toggle ──────────────────────────────────────── + // Toggle Encrypted Client Hello GREASE on/off + if rng.gen_bool(0.5) { + if fp.tls.ech_config.is_some() { + fp.tls.ech_config = None; + } else { + fp.tls.ech_config = Some(EchConfig::new( + EchMode::Grease { + hpke_suite: HpkeKemId::DhKemX25519HkdfSha256, + }, + None, + )); + } + } + + // ── 8. TLS 1.2 cipher suite order shuffle (JA3 only) ─────────── + // JA4 sorts cipher suites, so order doesn't matter. But JA3 is + // order-sensitive, so shuffling TLS 1.2 suites creates massive + // JA3 diversity without affecting JA4. + // Find the TLS 1.2 block (everything after TLS 1.3 suites and GREASE) + let tls12_start = fp.tls.cipher_suites.iter().position(|cs| { + !matches!( + cs, + CipherSuite::TLS13_AES_128_GCM_SHA256 + | CipherSuite::TLS13_AES_256_GCM_SHA384 + | CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 + | CipherSuite::Grease(_) + ) + }); + if let Some(start) = tls12_start { + fp.tls.cipher_suites[start..].shuffle(&mut rng); + } + + // ── 9. Header micro-variations ────────────────────────────────── + // Vary the User-Agent patch version and Accept-Language quality factors + // to create subtle header diversity across sessions. + Self::randomize_headers(&mut fp.headers, &mut rng); + + fp + } + + /// Apply micro-variations to HTTP headers for fingerprint diversity. + fn randomize_headers(headers: &mut Vec<(String, String)>, rng: &mut impl rand::Rng) { + for (key, value) in headers.iter_mut() { + let key_lower = key.to_lowercase(); + + // Vary Chrome/Edge UA patch version: Chrome/142.0.0.0 → Chrome/142.0.XXXX.YY + if key_lower == "user-agent" { + if let Some(chrome_pos) = value.find("Chrome/") { + let after = &value[chrome_pos + 7..]; + // Find the major version (e.g., "142") + if let Some(dot_pos) = after.find('.') { + let major = &after[..dot_pos]; + if let Ok(ver) = major.parse::() { + if ver >= 125 { + // Generate realistic build numbers + let build = rng.gen_range(6700..7300); + let patch = rng.gen_range(0..256); + let new_version = format!("{}.0.{}.{}", ver, build, patch); + + // Find the end of the version string + let version_start = chrome_pos + 7; + // Find next space or end + let version_end = value[version_start..] + .find(' ') + .map(|p| version_start + p) + .unwrap_or(value.len()); + + *value = format!( + "{}{}{}", + &value[..version_start], + new_version, + &value[version_end..] + ); + } + } + } + } + } + + // Vary Accept-Language quality factor: en;q=0.9 → en;q=0.{7-9} + if key_lower == "accept-language" { + let q_values = ["0.9", "0.8", "0.7"]; + let chosen = q_values[rng.gen_range(0..q_values.len())]; + if value.contains("en;q=0.9") { + *value = value.replace("en;q=0.9", &format!("en;q={}", chosen)); + } else if value.contains("en;q=0.5") { + // Firefox uses q=0.5 + let ff_q = ["0.5", "0.3", "0.7"]; + let ff_chosen = ff_q[rng.gen_range(0..ff_q.len())]; + *value = value.replace("en;q=0.5", &format!("en;q={}", ff_chosen)); + } + } + } + } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -67,12 +365,33 @@ impl TlsFingerprint { } } +/// HTTP/2 fingerprint configuration. +/// +/// Controls the SETTINGS frame parameters, WINDOW_UPDATE increment, and +/// pseudo-header ordering sent during the HTTP/2 handshake. These values +/// are used by anti-bot systems (Cloudflare, Akamai) for fingerprinting. #[derive(Clone, Debug)] pub struct Http2Fingerprint { + /// Order of HTTP/2 pseudo-headers in HEADERS frames. + /// Chrome: `:method, :authority, :scheme, :path` + /// Firefox: `:method, :path, :authority, :scheme` pub pseudo_header_order: Vec, - pub initial_stream_window_size: Option, - pub initial_connection_window_size: Option, - pub max_header_list_size: Option, + /// SETTINGS_HEADER_TABLE_SIZE (0x1). Chrome/Firefox: 65536. Default: 4096. + pub settings_header_table_size: Option, + /// SETTINGS_ENABLE_PUSH (0x2). Chrome: false. Default: true. + pub settings_enable_push: Option, + /// SETTINGS_MAX_CONCURRENT_STREAMS (0x3). Chrome: 1000. Firefox: not sent. + pub settings_max_concurrent_streams: Option, + /// SETTINGS_INITIAL_WINDOW_SIZE (0x4). Chrome: 6291456. Firefox: 131072. Default: 65535. + pub settings_initial_window_size: Option, + /// SETTINGS_MAX_FRAME_SIZE (0x5). Firefox: 16384. Default: 16384. + pub settings_max_frame_size: Option, + /// SETTINGS_MAX_HEADER_LIST_SIZE (0x6). Chrome: 262144. Firefox: not sent. + pub settings_max_header_list_size: Option, + /// Connection-level WINDOW_UPDATE increment sent after SETTINGS. + /// Chrome: 15663105 (total window = 65535 + 15663105 = ~15MB). + /// Firefox: 12517377 (total window = 65535 + 12517377 = ~12MB). + pub connection_window_size_increment: Option, } /// TLS extensions configuration. @@ -99,6 +418,12 @@ pub struct TlsExtensions { pub session_ticket: bool, /// Whether to send padding extension (RFC7685). pub padding: bool, + /// Whether to randomly permute TLS extension order on each client build. + /// Chrome 110+ randomizes extension order per-connection, producing different + /// JA3 hashes each time (while JA4 remains stable due to sorted hashing). + /// This makes the client indistinguishable from a real Chrome browser. + /// GREASE extensions at the start/end of the list are kept in place. + pub permute_extensions: bool, } impl TlsExtensions { @@ -138,6 +463,7 @@ impl TlsExtensions { extension_order, session_ticket: true, padding: false, + permute_extensions: false, } } @@ -155,6 +481,16 @@ impl TlsExtensions { self.padding = enabled; self } + + /// Enable TLS extension order permutation (Chrome 110+ behavior). + /// When enabled, the extension order is randomly shuffled when building the + /// TLS fingerprint. GREASE extensions at the boundaries are kept in place. + /// This produces different JA3 hashes per client instance while maintaining + /// the same JA4 hash (since JA4 sorts extensions before hashing). + pub fn with_permute_extensions(mut self, enabled: bool) -> Self { + self.permute_extensions = enabled; + self + } } /// ECH (Encrypted Client Hello) configuration. @@ -194,7 +530,14 @@ pub enum EchMode { impl TlsFingerprint { /// Converts this fingerprint to a rustls TlsFingerprint. + /// + /// If `permute_extensions` is enabled on the extensions config, the extension + /// order will be randomly shuffled (mimicking Chrome 110+ behavior). GREASE + /// extensions at the start/end are kept in place, while all other extensions + /// are shuffled. This produces different JA3 hashes per call while JA4 + /// (which sorts extensions) remains stable. pub fn to_rustls_fingerprint(&self) -> rustls::client::TlsFingerprint { + use rand::seq::SliceRandom; use rustls::client::{ FingerprintCertCompressionAlgorithm, FingerprintCipherSuite, FingerprintKeyExchangeGroup, FingerprintSignatureAlgorithm, TlsExtensionsConfig, @@ -255,7 +598,7 @@ impl TlsFingerprint { CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA => { FingerprintCipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA } - CipherSuite::Grease => FingerprintCipherSuite::Grease, + CipherSuite::Grease(_) => FingerprintCipherSuite::Grease, }) .collect(); @@ -273,7 +616,7 @@ impl TlsFingerprint { KeyExchangeGroup::Ffdhe4096 => FingerprintKeyExchangeGroup::Ffdhe4096, KeyExchangeGroup::Ffdhe6144 => FingerprintKeyExchangeGroup::Ffdhe6144, KeyExchangeGroup::Ffdhe8192 => FingerprintKeyExchangeGroup::Ffdhe8192, - KeyExchangeGroup::Grease => FingerprintKeyExchangeGroup::Grease, + KeyExchangeGroup::Grease(_) => FingerprintKeyExchangeGroup::Grease, }) .collect(); @@ -311,15 +654,80 @@ impl TlsFingerprint { }) .collect(); - // Check if GREASE is needed based on extension order - let has_grease = self - .extensions - .extension_order + // Apply extension permutation if enabled (Chrome 110+ behavior) + // This shuffles non-GREASE extensions while keeping GREASE at boundaries + let extension_order = if self.extensions.permute_extensions { + let mut order = self.extensions.extension_order.clone(); + // Find the range of non-GREASE extensions to shuffle + // GREASE can appear at the start and/or end - keep those in place + let start = if matches!(order.first(), Some(ExtensionType::Grease(_))) { 1 } else { 0 }; + let end = if order.len() > 1 && matches!(order.last(), Some(ExtensionType::Grease(_))) && start < order.len() - 1 { + order.len() - 1 + } else { + order.len() + }; + // Also keep Padding at the end if present (it must be last before GREASE) + let shuffle_end = if end > start && order.get(end - 1) == Some(&ExtensionType::Padding) { + end - 1 + } else { + end + }; + if shuffle_end > start + 1 { + let mut rng = rand::thread_rng(); + order[start..shuffle_end].shuffle(&mut rng); + } + order + } else { + self.extensions.extension_order.clone() + }; + + // Find the GREASE extension value (if any) from the extension order + let grease_ext_val = extension_order + .iter() + .find_map(|e| match e { + ExtensionType::Grease(val) => Some(*val), + _ => None, + }); + + // Map impit ExtensionType → rustls ExtensionType for extension ordering + use rustls::internal::msgs::enums::ExtensionType as RustlsExtType; + let rustls_extension_order: Vec = extension_order .iter() - .any(|e| matches!(e, ExtensionType::Grease)); + .filter_map(|ext| match ext { + ExtensionType::ServerName => Some(RustlsExtType::ServerName), + ExtensionType::StatusRequest => Some(RustlsExtType::StatusRequest), + ExtensionType::SupportedGroups => Some(RustlsExtType::EllipticCurves), + ExtensionType::EcPointFormats => Some(RustlsExtType::ECPointFormats), + ExtensionType::SignatureAlgorithms => Some(RustlsExtType::SignatureAlgorithms), + ExtensionType::ApplicationLayerProtocolNegotiation => { + Some(RustlsExtType::ALProtocolNegotiation) + } + ExtensionType::SignedCertificateTimestamp => Some(RustlsExtType::SCT), + ExtensionType::Padding => Some(RustlsExtType::Padding), + ExtensionType::SupportedVersions => Some(RustlsExtType::SupportedVersions), + ExtensionType::PskKeyExchangeModes => Some(RustlsExtType::PSKKeyExchangeModes), + ExtensionType::KeyShare => Some(RustlsExtType::KeyShare), + ExtensionType::ExtendedMasterSecret => Some(RustlsExtType::ExtendedMasterSecret), + ExtensionType::RenegotiationInfo => Some(RustlsExtType::RenegotiationInfo), + ExtensionType::SessionTicket => Some(RustlsExtType::SessionTicket), + ExtensionType::CompressCertificate => Some(RustlsExtType::CompressCertificate), + ExtensionType::ApplicationSettings => { + if self.extensions.use_new_alps_codepoint { + Some(RustlsExtType::ApplicationSettingsNew) + } else { + Some(RustlsExtType::ApplicationSettings) + } + } + ExtensionType::PreSharedKey => Some(RustlsExtType::PreSharedKey), + ExtensionType::EarlyData => Some(RustlsExtType::EarlyData), + ExtensionType::PostHandshakeAuth => Some(RustlsExtType::PostHandshakeAuth), + ExtensionType::Grease(_) => Some(RustlsExtType::ReservedGrease), + _ => None, + }) + .collect(); let extensions_config = TlsExtensionsConfig { - grease: has_grease, + grease: grease_ext_val.is_some(), signed_certificate_timestamp: self.extensions.signed_certificate_timestamp, application_settings: self.extensions.application_settings, use_new_alps_codepoint: self.extensions.use_new_alps_codepoint, @@ -327,6 +735,8 @@ impl TlsFingerprint { record_size_limit: self.extensions.record_size_limit, renegotiation_info: true, // Common for both browsers padding: self.extensions.padding, + supported_versions: true, + extension_order: rustls_extension_order, }; let cert_compression = self.extensions.compress_certificate.clone().map(|algos| { diff --git a/impit/src/fingerprint/types.rs b/impit/src/fingerprint/types.rs index 00028f03..c5af1308 100644 --- a/impit/src/fingerprint/types.rs +++ b/impit/src/fingerprint/types.rs @@ -26,8 +26,9 @@ pub enum CipherSuite { TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_256_CBC_SHA, - /// GREASE cipher suite for fingerprinting - Grease, + /// GREASE cipher suite with parameterized value (RFC 8701). + /// The u16 should be one of the 16 GREASE values: 0x0a0a, 0x1a1a, ..., 0xfafa. + Grease(u16), } /// Key exchange groups for TLS @@ -44,8 +45,8 @@ pub enum KeyExchangeGroup { Ffdhe4096, Ffdhe6144, Ffdhe8192, - /// GREASE key exchange group for fingerprinting - Grease, + /// GREASE key exchange group with parameterized value (RFC 8701). + Grease(u16), } /// Signature algorithms for TLS @@ -103,7 +104,8 @@ pub enum ExtensionType { CompressCertificate, ApplicationSettings, EarlyDataExtension, - Grease, + /// GREASE extension type with parameterized value (RFC 8701). + Grease(u16), } /// Certificate compression algorithms diff --git a/impit/src/impit.rs b/impit/src/impit.rs index 6e6cb9f8..88f439c3 100644 --- a/impit/src/impit.rs +++ b/impit/src/impit.rs @@ -238,18 +238,6 @@ impl Impit { // Use fingerprint if provided, otherwise fall back to browser enum if let Some(ref fingerprint) = config.fingerprint { tls_config_builder.with_tls_fingerprint(fingerprint.tls.clone()); - - if let Some(window_size) = fingerprint.http2.initial_stream_window_size { - client = client.http2_initial_stream_window_size(window_size); - } - - if let Some(window_size) = fingerprint.http2.initial_connection_window_size { - client = client.http2_initial_connection_window_size(window_size); - } - - if let Some(max_size) = fingerprint.http2.max_header_list_size { - client = client.http2_max_header_list_size(max_size); - } } if config.max_http_version == Version::HTTP_3 { @@ -294,6 +282,33 @@ impl Impit { } } + // Apply per-client HTTP/2 SETTINGS from the fingerprint. + // These use reqwest's builder API which flows through hyper → h2, + // so each Impit instance gets its own settings (safe for concurrent use). + // HEADER_TABLE_SIZE is handled globally via the h2 fork's default (65536). + if let Some(ref fingerprint) = config.fingerprint { + let h2 = &fingerprint.http2; + + if let Some(initial_window_size) = h2.settings_initial_window_size { + client = client.http2_initial_stream_window_size(initial_window_size); + } + + if let Some(max_frame_size) = h2.settings_max_frame_size { + client = client.http2_max_frame_size(max_frame_size); + } + + if let Some(max_header_list_size) = h2.settings_max_header_list_size { + client = client.http2_max_header_list_size(max_header_list_size); + } + + if let Some(connection_window_size) = h2.connection_window_size_increment { + client = client.http2_initial_connection_window_size(connection_window_size); + } + + // Disable adaptive window so our explicit sizes are used as-is. + client = client.http2_adaptive_window(false); + } + client .build() .map_err(|e| ImpitError::ReqwestError(format!("{e:#?}")))