From fbe3d84172b9bec983372866c150a86905f50ccf Mon Sep 17 00:00:00 2001 From: Jack Cutrara Date: Wed, 5 Aug 2026 10:18:16 -0400 Subject: [PATCH 1/2] chore: bump rustls to 23b2c17 for ML-DSA signature algorithms Picks up apify/rustls#22, which adds the ML-DSA variants to `FingerprintSignatureAlgorithm`. Required by the chrome151 fingerprint, which lists those three codepoints first. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1113d493..6cb3f9d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2267,8 +2267,8 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" -source = "git+https://github.com/apify/rustls?rev=1ebb6d466a557858cdd8c836ffcbb26d04d7a9f9#1ebb6d466a557858cdd8c836ffcbb26d04d7a9f9" +version = "0.23.43" +source = "git+https://github.com/apify/rustls?rev=23b2c17427c095b768e22ccf0dadb97266860cf1#23b2c17427c095b768e22ccf0dadb97266860cf1" dependencies = [ "aws-lc-rs", "brotli", diff --git a/Cargo.toml b/Cargo.toml index 689a48a3..655d00da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ [patch.crates-io] h2 = { git = "https://github.com/apify/h2", rev = "7f393a728a8db07cabb1b78d2094772b33943b9a" } -rustls = { git = "https://github.com/apify/rustls", rev="1ebb6d466a557858cdd8c836ffcbb26d04d7a9f9" } +rustls = { git = "https://github.com/apify/rustls", rev="23b2c17427c095b768e22ccf0dadb97266860cf1" } tower-http = { git = "https://github.com/apify/tower-http", rev="f9efc0d9193e774d33aedc1022b922efefc22052" } hyper-util = { git = "https://github.com/apify/hyper-util", rev="9b7795dfd7158fc55e7c84b65bf1dae1d2dea67d" } From 6c02449618a386cf363753c9bf7773f7fe6160de Mon Sep 17 00:00:00 2001 From: Jack Cutrara Date: Mon, 3 Aug 2026 13:39:52 -0400 Subject: [PATCH 2/2] feat: add chrome151 browser fingerprint Chrome stable is 151; the newest profile was chrome142. The only wire-level TLS difference between them is that Chrome now leads its signature_algorithms list with the three ML-DSA codepoints (0x0904/0x0905/0x0906), which accounts for the entire JA4_c divergence reported in #508. Captured against tls.peet.ws from consumer Chrome stable 151.0.7922.72 on Windows 11, cross-checked against Chrome for Testing 151.0.7922.71 on the same host. The two agree on everything except sec-ch-ua, which Chrome for Testing cannot supply because it is unbranded. Verified: JA4, JA4_r and all seventeen request headers are byte-identical to the consumer browser, and a real request completes with ML-DSA advertised. Requires the matching ML-DSA change in apify/rustls; until that lands and the rev in Cargo.toml is bumped, this will not build. Headers carry the captured Windows platform rather than the macOS strings used by chrome142, so the UA and sec-ch-ua-platform stay internally consistent. initial_connection_window_size is 15728640 (15 MiB) rather than the 15663105 used by sibling profiles: the field is the window size, not the increment, so this is what makes the emitted WINDOW_UPDATE equal the 15663105 seen on the wire. Closes #508 Co-Authored-By: Claude Fable 5 --- impit-node/index.d.ts | 1 + impit-node/src/impit_builder.rs | 2 + impit-python/python/impit/__init__.py | 1 + impit-python/src/fingerprint.rs | 1 + impit/src/fingerprint/database.rs | 2 +- impit/src/fingerprint/database/chrome.rs | 158 +++++++++++++++++++++++ impit/src/fingerprint/mod.rs | 3 + impit/src/fingerprint/types.rs | 6 + 8 files changed, 173 insertions(+), 1 deletion(-) diff --git a/impit-node/index.d.ts b/impit-node/index.d.ts index 08b21fb9..b4ac8a1b 100644 --- a/impit-node/index.d.ts +++ b/impit-node/index.d.ts @@ -263,6 +263,7 @@ export type Browser = 'chrome'| 'chrome131'| 'chrome136'| 'chrome142'| +'chrome151'| 'firefox'| 'firefox128'| 'firefox133'| diff --git a/impit-node/src/impit_builder.rs b/impit-node/src/impit_builder.rs index 85e6a20e..36a80c5e 100644 --- a/impit-node/src/impit_builder.rs +++ b/impit-node/src/impit_builder.rs @@ -27,6 +27,7 @@ pub enum Browser { Chrome131, Chrome136, Chrome142, + Chrome151, Firefox, Firefox128, Firefox133, @@ -135,6 +136,7 @@ impl From for BrowserFingerprint { Browser::Chrome131 => impit::fingerprint::database::chrome_131::fingerprint(), Browser::Chrome136 => impit::fingerprint::database::chrome_136::fingerprint(), Browser::Chrome142 => impit::fingerprint::database::chrome_142::fingerprint(), + Browser::Chrome151 => impit::fingerprint::database::chrome_151::fingerprint(), Browser::Firefox | Browser::Firefox128 => { impit::fingerprint::database::firefox_128::fingerprint() } diff --git a/impit-python/python/impit/__init__.py b/impit-python/python/impit/__init__.py index dabee4d6..970107a9 100644 --- a/impit-python/python/impit/__init__.py +++ b/impit-python/python/impit/__init__.py @@ -110,6 +110,7 @@ 'chrome131', 'chrome136', 'chrome142', + 'chrome151', 'firefox', 'firefox128', 'firefox133', diff --git a/impit-python/src/fingerprint.rs b/impit-python/src/fingerprint.rs index 19c73a79..7d5edd47 100644 --- a/impit-python/src/fingerprint.rs +++ b/impit-python/src/fingerprint.rs @@ -18,6 +18,7 @@ pub(crate) fn fingerprint_by_name(browser: &str) -> PyResult "chrome131" => database::chrome_131::fingerprint(), "chrome136" => database::chrome_136::fingerprint(), "chrome142" => database::chrome_142::fingerprint(), + "chrome151" => database::chrome_151::fingerprint(), "firefox" | "firefox128" => database::firefox_128::fingerprint(), "firefox133" => database::firefox_133::fingerprint(), "firefox135" => database::firefox_135::fingerprint(), diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs index ced5cd05..4719197b 100644 --- a/impit/src/fingerprint/database.rs +++ b/impit/src/fingerprint/database.rs @@ -9,7 +9,7 @@ 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, + chrome_131, chrome_133, chrome_136, chrome_142, chrome_151, }; pub use firefox::{firefox_128, firefox_133, firefox_135, firefox_144}; pub use okhttp::{okhttp3, okhttp4, okhttp5}; diff --git a/impit/src/fingerprint/database/chrome.rs b/impit/src/fingerprint/database/chrome.rs index 47848f3e..4fb5c675 100644 --- a/impit/src/fingerprint/database/chrome.rs +++ b/impit/src/fingerprint/database/chrome.rs @@ -2,6 +2,164 @@ use crate::fingerprint::*; +/// Chrome 151 fingerprint module +/// +/// Source: capture against from consumer Chrome stable +/// 151.0.7922.72 on Windows 11, cross-checked against Chrome for Testing +/// 151.0.7922.71 on the same host. The two agree on everything except +/// `sec-ch-ua`, which Chrome for Testing cannot supply because it is unbranded; +/// that header is taken from the consumer build and is stable across launches. +/// +/// Relative to Chrome 142 the only wire-level TLS difference is that Chrome now +/// leads its signature_algorithms list with the three ML-DSA codepoints. +pub mod chrome_151 { + use super::*; + + /// Returns the complete Chrome 151 fingerprint + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Chrome", + "151", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + /// Chrome 151 TLS fingerprint + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Cipher suites in Chrome 151 preference order + // GREASE cipher at position 1 (first) - same as Chrome 142 + vec![ + CipherSuite::Grease, + 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, + ], + // Key exchange groups (includes post-quantum hybrid X25519MLKEM768) + // GREASE at position 1 (first) - same as Chrome 142 + vec![ + KeyExchangeGroup::Grease, + KeyExchangeGroup::X25519MLKEM768, + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + ], + // Signature algorithms + // Chrome leads with the three ML-DSA codepoints (0x0904/0x0905/0x0906); + // the remainder is unchanged from Chrome 142. + vec![ + SignatureAlgorithm::MlDsa44, + SignatureAlgorithm::MlDsa65, + SignatureAlgorithm::MlDsa87, + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha512, + ], + // TLS extensions configuration + // Chrome 151 uses new ALPS codepoint (17613) + 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 (Chrome doesn't use) + None, // record_size_limit (Chrome doesn't use) + // Extension order (critical for fingerprinting) + 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), + // 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()], + ) + } + + /// Chrome 151 HTTP/2 fingerprint + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + pseudo_header_order: vec![ + ":method".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ":path".to_string(), + ":protocol".to_string(), + ":status".to_string(), + ], + initial_stream_window_size: Some(6_291_456), + // Chrome's connection window is 15 MiB. This field is the window size, + // not the increment, so the emitted WINDOW_UPDATE is 15728640 - 65535 = + // 15663105, which is what the capture shows on the wire. + initial_connection_window_size: Some(15_728_640), + max_header_list_size: Some(262_144), + } + } + + /// Chrome 151 HTTP headers + fn headers() -> Vec<(String, String)> { + vec![ + ("sec-ch-ua".to_string(), "\"Not=A?Brand\";v=\"99\", \"Google Chrome\";v=\"151\", \"Chromium\";v=\"151\"".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/151.0.0.0 Safari/537.36".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()), + ] + } +} + /// Chrome 142 fingerprint module pub mod chrome_142 { use super::*; diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index 73a2ef0d..7b249263 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -346,6 +346,9 @@ impl TlsFingerprint { SignatureAlgorithm::RsaPkcs1Sha1 => FingerprintSignatureAlgorithm::RsaPkcs1Sha1, SignatureAlgorithm::Ed25519 => FingerprintSignatureAlgorithm::Ed25519, SignatureAlgorithm::Ed448 => FingerprintSignatureAlgorithm::Ed448, + SignatureAlgorithm::MlDsa44 => FingerprintSignatureAlgorithm::MlDsa44, + SignatureAlgorithm::MlDsa65 => FingerprintSignatureAlgorithm::MlDsa65, + SignatureAlgorithm::MlDsa87 => FingerprintSignatureAlgorithm::MlDsa87, SignatureAlgorithm::EcdsaSha1Legacy => { FingerprintSignatureAlgorithm::EcdsaSha1Legacy } diff --git a/impit/src/fingerprint/types.rs b/impit/src/fingerprint/types.rs index d552dae4..1b815ae7 100644 --- a/impit/src/fingerprint/types.rs +++ b/impit/src/fingerprint/types.rs @@ -72,6 +72,12 @@ pub enum SignatureAlgorithm { // EdDSA algorithms Ed25519, Ed448, + // ML-DSA algorithms (draft-ietf-tls-mldsa). Advertised in the ClientHello + // for fingerprint accuracy only: no ML-DSA verifier is available, so a + // server that actually selects one fails the handshake. + MlDsa44, + MlDsa65, + MlDsa87, // Legacy ECDSA with SHA-1 (for backwards compatibility) EcdsaSha1Legacy, }