diff --git a/Cargo.lock b/Cargo.lock index 18d66f6c..ad29a1de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2088,7 +2088,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?rev=d19ecc2b4e38978c1c3acabc98a221fa98b9314f#d19ecc2b4e38978c1c3acabc98a221fa98b9314f" dependencies = [ "aws-lc-rs", "brotli", diff --git a/Cargo.toml b/Cargo.toml index e83548bf..16569d58 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="c908559019c6521fbc4b487317a4ea24088a99f3" } +rustls = { git = "https://github.com/apify/rustls", rev="d19ecc2b4e38978c1c3acabc98a221fa98b9314f" } 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-node/index.d.ts b/impit-node/index.d.ts index 66f53282..53c05759 100644 --- a/impit-node/index.d.ts +++ b/impit-node/index.d.ts @@ -255,7 +255,11 @@ export type Browser = 'chrome'| 'firefox128'| 'firefox133'| 'firefox135'| -'firefox144'; +'firefox144'| +'okhttp'| +'okhttp3'| +'okhttp4'| +'okhttp5'; export type HttpMethod = 'GET'| 'POST'| diff --git a/impit-node/src/impit_builder.rs b/impit-node/src/impit_builder.rs index 56c27a6f..2ff9db0d 100644 --- a/impit-node/src/impit_builder.rs +++ b/impit-node/src/impit_builder.rs @@ -32,6 +32,10 @@ pub enum Browser { Firefox133, Firefox135, Firefox144, + OkHttp, + OkHttp3, + OkHttp4, + OkHttp5, } /// Options for configuring an {@link Impit} instance. @@ -136,6 +140,9 @@ impl From for BrowserFingerprint { Browser::Firefox133 => impit::fingerprint::database::firefox_133::fingerprint(), Browser::Firefox135 => impit::fingerprint::database::firefox_135::fingerprint(), Browser::Firefox144 => impit::fingerprint::database::firefox_144::fingerprint(), + Browser::OkHttp3 => impit::fingerprint::database::okhttp3::fingerprint(), + Browser::OkHttp | Browser::OkHttp4 => impit::fingerprint::database::okhttp4::fingerprint(), + Browser::OkHttp5 => impit::fingerprint::database::okhttp5::fingerprint(), } } } diff --git a/impit-python/src/async_client.rs b/impit-python/src/async_client.rs index 13a55698..2e99089b 100644 --- a/impit-python/src/async_client.rs +++ b/impit-python/src/async_client.rs @@ -59,46 +59,53 @@ impl AsyncClient { ) -> PyResult { let builder = ImpitBuilder::default(); - let builder = match browser { - Some(browser) => match browser.to_lowercase().as_str() { - "chrome" | "chrome124" => builder - .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), - "chrome100" => builder - .with_fingerprint(impit::fingerprint::database::chrome_100::fingerprint()), - "chrome101" => builder - .with_fingerprint(impit::fingerprint::database::chrome_101::fingerprint()), - "chrome104" => builder - .with_fingerprint(impit::fingerprint::database::chrome_104::fingerprint()), - "chrome107" => builder - .with_fingerprint(impit::fingerprint::database::chrome_107::fingerprint()), - "chrome110" => builder - .with_fingerprint(impit::fingerprint::database::chrome_110::fingerprint()), - "chrome116" => builder - .with_fingerprint(impit::fingerprint::database::chrome_116::fingerprint()), - "chrome125" => builder - .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), - "chrome131" => builder - .with_fingerprint(impit::fingerprint::database::chrome_131::fingerprint()), - "chrome136" => builder - .with_fingerprint(impit::fingerprint::database::chrome_136::fingerprint()), - "chrome142" => builder - .with_fingerprint(impit::fingerprint::database::chrome_142::fingerprint()), - "firefox128" | "firefox" => builder - .with_fingerprint(impit::fingerprint::database::firefox_128::fingerprint()), - "firefox133" => builder - .with_fingerprint(impit::fingerprint::database::firefox_133::fingerprint()), - "firefox135" => builder - .with_fingerprint(impit::fingerprint::database::firefox_135::fingerprint()), - "firefox144" => builder - .with_fingerprint(impit::fingerprint::database::firefox_144::fingerprint()), - _ => { - return Err(PyErr::new::( - "Unsupported browser", - )) - } - }, - None => builder, - }; + let builder = + match browser { + Some(browser) => match browser.to_lowercase().as_str() { + "chrome" | "chrome124" => builder + .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), + "chrome100" => builder + .with_fingerprint(impit::fingerprint::database::chrome_100::fingerprint()), + "chrome101" => builder + .with_fingerprint(impit::fingerprint::database::chrome_101::fingerprint()), + "chrome104" => builder + .with_fingerprint(impit::fingerprint::database::chrome_104::fingerprint()), + "chrome107" => builder + .with_fingerprint(impit::fingerprint::database::chrome_107::fingerprint()), + "chrome110" => builder + .with_fingerprint(impit::fingerprint::database::chrome_110::fingerprint()), + "chrome116" => builder + .with_fingerprint(impit::fingerprint::database::chrome_116::fingerprint()), + "chrome125" => builder + .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), + "chrome131" => builder + .with_fingerprint(impit::fingerprint::database::chrome_131::fingerprint()), + "chrome136" => builder + .with_fingerprint(impit::fingerprint::database::chrome_136::fingerprint()), + "chrome142" => builder + .with_fingerprint(impit::fingerprint::database::chrome_142::fingerprint()), + "firefox128" | "firefox" => builder + .with_fingerprint(impit::fingerprint::database::firefox_128::fingerprint()), + "firefox133" => builder + .with_fingerprint(impit::fingerprint::database::firefox_133::fingerprint()), + "firefox135" => builder + .with_fingerprint(impit::fingerprint::database::firefox_135::fingerprint()), + "firefox144" => builder + .with_fingerprint(impit::fingerprint::database::firefox_144::fingerprint()), + "okhttp3" => builder + .with_fingerprint(impit::fingerprint::database::okhttp3::fingerprint()), + "okhttp" | "okhttp4" => builder + .with_fingerprint(impit::fingerprint::database::okhttp4::fingerprint()), + "okhttp5" => builder + .with_fingerprint(impit::fingerprint::database::okhttp5::fingerprint()), + _ => { + return Err(PyErr::new::( + "Unsupported browser", + )) + } + }, + None => builder, + }; let builder = match http3 { Some(true) => builder.with_http3(), diff --git a/impit-python/src/client.rs b/impit-python/src/client.rs index d5192f2d..2485ae4b 100644 --- a/impit-python/src/client.rs +++ b/impit-python/src/client.rs @@ -54,40 +54,47 @@ impl Client { ) -> Result { let builder = ImpitBuilder::default(); - let builder = match browser { - Some(browser) => match browser.to_lowercase().as_str() { - "chrome" | "chrome125" => builder - .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), - "chrome100" => builder - .with_fingerprint(impit::fingerprint::database::chrome_100::fingerprint()), - "chrome101" => builder - .with_fingerprint(impit::fingerprint::database::chrome_101::fingerprint()), - "chrome104" => builder - .with_fingerprint(impit::fingerprint::database::chrome_104::fingerprint()), - "chrome107" => builder - .with_fingerprint(impit::fingerprint::database::chrome_107::fingerprint()), - "chrome110" => builder - .with_fingerprint(impit::fingerprint::database::chrome_110::fingerprint()), - "chrome116" => builder - .with_fingerprint(impit::fingerprint::database::chrome_116::fingerprint()), - "chrome131" => builder - .with_fingerprint(impit::fingerprint::database::chrome_131::fingerprint()), - "chrome136" => builder - .with_fingerprint(impit::fingerprint::database::chrome_136::fingerprint()), - "chrome142" => builder - .with_fingerprint(impit::fingerprint::database::chrome_142::fingerprint()), - "firefox128" | "firefox" => builder - .with_fingerprint(impit::fingerprint::database::firefox_128::fingerprint()), - "firefox133" => builder - .with_fingerprint(impit::fingerprint::database::firefox_133::fingerprint()), - "firefox135" => builder - .with_fingerprint(impit::fingerprint::database::firefox_135::fingerprint()), - "firefox144" => builder - .with_fingerprint(impit::fingerprint::database::firefox_144::fingerprint()), - _ => panic!("Unsupported browser"), - }, - None => builder, - }; + let builder = + match browser { + Some(browser) => match browser.to_lowercase().as_str() { + "chrome" | "chrome125" => builder + .with_fingerprint(impit::fingerprint::database::chrome_125::fingerprint()), + "chrome100" => builder + .with_fingerprint(impit::fingerprint::database::chrome_100::fingerprint()), + "chrome101" => builder + .with_fingerprint(impit::fingerprint::database::chrome_101::fingerprint()), + "chrome104" => builder + .with_fingerprint(impit::fingerprint::database::chrome_104::fingerprint()), + "chrome107" => builder + .with_fingerprint(impit::fingerprint::database::chrome_107::fingerprint()), + "chrome110" => builder + .with_fingerprint(impit::fingerprint::database::chrome_110::fingerprint()), + "chrome116" => builder + .with_fingerprint(impit::fingerprint::database::chrome_116::fingerprint()), + "chrome131" => builder + .with_fingerprint(impit::fingerprint::database::chrome_131::fingerprint()), + "chrome136" => builder + .with_fingerprint(impit::fingerprint::database::chrome_136::fingerprint()), + "chrome142" => builder + .with_fingerprint(impit::fingerprint::database::chrome_142::fingerprint()), + "firefox128" | "firefox" => builder + .with_fingerprint(impit::fingerprint::database::firefox_128::fingerprint()), + "firefox133" => builder + .with_fingerprint(impit::fingerprint::database::firefox_133::fingerprint()), + "firefox135" => builder + .with_fingerprint(impit::fingerprint::database::firefox_135::fingerprint()), + "firefox144" => builder + .with_fingerprint(impit::fingerprint::database::firefox_144::fingerprint()), + "okhttp3" => builder + .with_fingerprint(impit::fingerprint::database::okhttp3::fingerprint()), + "okhttp" | "okhttp4" => builder + .with_fingerprint(impit::fingerprint::database::okhttp4::fingerprint()), + "okhttp5" => builder + .with_fingerprint(impit::fingerprint::database::okhttp5::fingerprint()), + _ => panic!("Unsupported browser"), + }, + None => builder, + }; let builder = match http3 { Some(true) => builder.with_http3(), diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs index afd9da6d..12e9fd2e 100644 --- a/impit/src/fingerprint/database.rs +++ b/impit/src/fingerprint/database.rs @@ -4,9 +4,11 @@ mod chrome; mod firefox; +mod okhttp; 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 firefox::{firefox_128, firefox_133, firefox_135, firefox_144}; +pub use okhttp::{okhttp3, okhttp4, okhttp5}; diff --git a/impit/src/fingerprint/database/okhttp.rs b/impit/src/fingerprint/database/okhttp.rs new file mode 100644 index 00000000..f5dd00df --- /dev/null +++ b/impit/src/fingerprint/database/okhttp.rs @@ -0,0 +1,259 @@ +//! OkHttp client fingerprints +//! +//! OkHttp is the most popular HTTP client for Android applications. +//! These fingerprints cover OkHttp 3.x through 5.x using Conscrypt (BoringSSL) +//! as the TLS provider, which is the default on modern Android devices. +//! +//! Key differences from browser fingerprints: +//! - No GREASE values (Conscrypt doesn't use GREASE) +//! - No post-quantum key exchange (no X25519MLKEM768) +//! - No ECH/ESNI support +//! - No certificate compression +//! - No ALPS/application_settings +//! - Includes ec_point_formats extension (browsers have dropped this) +//! - Simpler cipher suite list (ConnectionSpec.MODERN_TLS) +//! - HTTP/2 window sizes set to 16 MiB (vs 64 KiB browser defaults) + +use crate::fingerprint::*; + +/// Shared HTTP/2 fingerprint for all OkHttp versions. +/// +/// OkHttp's HTTP/2 implementation (okhttp2 codec) uses the same settings +/// across versions 3.x through 5.x. +fn shared_http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + // OkHttp pseudo-header order (from Http2ExchangeCodec) + pseudo_header_order: vec![ + ":method".to_string(), + ":path".to_string(), + ":authority".to_string(), + ":scheme".to_string(), + ":protocol".to_string(), + ":status".to_string(), + ], + // OkHttp sets INITIAL_WINDOW_SIZE to 16 MiB + // (okHttpClientWindowSize = 16 * 1024 * 1024) + initial_stream_window_size: Some(16_777_216), + // Connection window is also increased to 16 MiB via WINDOW_UPDATE + initial_connection_window_size: Some(16_777_216), + max_header_list_size: None, + } +} + +/// Shared signature algorithms for all OkHttp versions with Conscrypt. +fn shared_signature_algorithms() -> Vec { + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::RsaPkcs1Sha1, + ] +} + +/// OkHttp 3 fingerprint module (pre-TLS 1.3, Conscrypt on Android) +/// +/// Represents OkHttp 3.9-3.11 before TLS 1.3 support was added in 3.12. +/// Common in older Android apps that haven't updated their OkHttp dependency. +/// TLS 1.2 only — no key_share, psk_key_exchange_modes, or supported_versions. +/// Includes CBC and RSA cipher suites from ConnectionSpec.MODERN_TLS. +pub mod okhttp3 { + use super::*; + + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "OkHttp", + "3", + tls_fingerprint(), + shared_http2_fingerprint(), + headers(), + ) + } + + pub(crate) fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // OkHttp 3 cipher suites — TLS 1.2 only, no TLS 1.3 ciphers + // Includes CBC and RSA suites from MODERN_TLS + vec![ + 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 + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + ], + shared_signature_algorithms(), + // TLS extensions — no TLS 1.3 extensions + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + false, // signed_certificate_timestamp + false, // key_share (TLS 1.3 only) + false, // psk_key_exchange_modes (TLS 1.3 only) + false, // supported_versions (TLS 1.3 only) + None, // compress_certificate + false, // application_settings + false, // delegated_credentials + None, // record_size_limit + // Extension order for Conscrypt TLS 1.2 + vec![ + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::RenegotiationInfo, + ExtensionType::SupportedGroups, + ExtensionType::EcPointFormats, + ExtensionType::SessionTicket, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::StatusRequest, + ExtensionType::SignatureAlgorithms, + ], + ), + None, // No ECH + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + fn headers() -> Vec<(String, String)> { + vec![ + ("user-agent".to_string(), "okhttp/3.14.9".to_string()), + ("accept-encoding".to_string(), "gzip".to_string()), + ] + } +} + +/// OkHttp 4 fingerprint module (Conscrypt on Android 13+) +/// +/// Represents OkHttp 4.12.x with Conscrypt TLS provider on Android 13-14. +/// This covers the vast majority of Android app traffic in production. +pub mod okhttp4 { + use super::*; + + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "OkHttp", + "4", + tls_fingerprint(), + shared_http2_fingerprint(), + headers(), + ) + } + + /// OkHttp 4 TLS fingerprint (Conscrypt/BoringSSL on Android) + pub(crate) fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + // Cipher suites in OkHttp 4 preference order (Conscrypt) + // No GREASE — Conscrypt doesn't use GREASE values + // Filtered to OkHttp's ConnectionSpec.MODERN_TLS approved list + 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, + ], + // Key exchange groups (Conscrypt on Android 13+) + // No post-quantum, no GREASE + vec![ + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + ], + shared_signature_algorithms(), + // TLS extensions configuration + // OkHttp/Conscrypt has a simpler extension set than browsers + TlsExtensions::new( + true, // server_name + true, // status_request + true, // supported_groups + true, // signature_algorithms + true, // application_layer_protocol_negotiation + false, // signed_certificate_timestamp (not used by OkHttp) + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions + None, // compress_certificate (not used by OkHttp) + false, // application_settings (not used by OkHttp) + false, // delegated_credentials (not used by OkHttp) + None, // record_size_limit (not used by OkHttp) + // Extension order (critical for fingerprinting) + // Conscrypt sends extensions in this order (from JA3 capture) + vec![ + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::RenegotiationInfo, + ExtensionType::SupportedGroups, + ExtensionType::EcPointFormats, + ExtensionType::SessionTicket, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::StatusRequest, + ExtensionType::SignatureAlgorithms, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ], + ), + // No ECH — Conscrypt doesn't support ECH/GREASE + None, + // ALPN protocols + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + fn headers() -> Vec<(String, String)> { + vec![ + ("user-agent".to_string(), "okhttp/4.12.0".to_string()), + ("accept-encoding".to_string(), "gzip".to_string()), + ] + } +} + +/// OkHttp 5 fingerprint module (Conscrypt on Android) +/// +/// Represents OkHttp 5.x with Conscrypt TLS provider. +/// TLS fingerprint is identical to OkHttp 4.x on Android/Conscrypt. +/// The main behavioral difference is that OkHttp 5 enforces its own +/// cipher suite preference order rather than deferring to the JVM. +pub mod okhttp5 { + use super::*; + + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "OkHttp", + "5", + // Same TLS fingerprint as OkHttp 4 on Conscrypt + okhttp4::tls_fingerprint(), + shared_http2_fingerprint(), + headers(), + ) + } + + fn headers() -> Vec<(String, String)> { + vec![ + ("user-agent".to_string(), "okhttp/5.0.0".to_string()), + ("accept-encoding".to_string(), "gzip".to_string()), + ] + } +} diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index ab260cc3..f77648ee 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -318,6 +318,48 @@ impl TlsFingerprint { .iter() .any(|e| matches!(e, ExtensionType::Grease)); + use rustls::internal::msgs::enums::ExtensionType as RustlsExtType; + + let extension_order: Vec = self + .extensions + .extension_order + .iter() + .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::KeyShare => Some(RustlsExtType::KeyShare), + ExtensionType::PskKeyExchangeModes => Some(RustlsExtType::PSKKeyExchangeModes), + ExtensionType::SupportedVersions => Some(RustlsExtType::SupportedVersions), + ExtensionType::CompressCertificate => Some(RustlsExtType::CompressCertificate), + ExtensionType::ApplicationSettings => Some(RustlsExtType::ApplicationSettings), + ExtensionType::ExtendedMasterSecret => { + Some(RustlsExtType::ExtendedMasterSecret) + } + ExtensionType::SessionTicket => Some(RustlsExtType::SessionTicket), + ExtensionType::RenegotiationInfo => Some(RustlsExtType::RenegotiationInfo), + ExtensionType::Padding => Some(RustlsExtType::Padding), + ExtensionType::Grease => Some(RustlsExtType::ReservedGrease), + ExtensionType::EarlyData => Some(RustlsExtType::EarlyData), + ExtensionType::PostHandshakeAuth => Some(RustlsExtType::PostHandshakeAuth), + ExtensionType::SignatureAlgorithmsCert => { + Some(RustlsExtType::SignatureAlgorithmsCert) + } + // PreSharedKey and ECH are handled separately by rustls (always last) + ExtensionType::PreSharedKey => None, + // Skip types that don't map cleanly + _ => None, + } + }) + .collect(); + let extensions_config = TlsExtensionsConfig { grease: has_grease, signed_certificate_timestamp: self.extensions.signed_certificate_timestamp, @@ -327,6 +369,8 @@ impl TlsFingerprint { record_size_limit: self.extensions.record_size_limit, renegotiation_info: true, // Common for both browsers padding: self.extensions.padding, + supported_versions: self.extensions.supported_versions, + extension_order, }; let cert_compression = self.extensions.compress_certificate.clone().map(|algos| { diff --git a/impit/src/tls/mod.rs b/impit/src/tls/mod.rs index c71005e7..719f888c 100644 --- a/impit/src/tls/mod.rs +++ b/impit/src/tls/mod.rs @@ -156,8 +156,16 @@ impl TlsConfigBuilder { (provider, verifier) }; - // Only enable ECH if the fingerprint requests it + // Select protocol versions based on fingerprint + let protocol_versions: &[&rustls::SupportedProtocolVersion] = + if fp.extensions.supported_versions { + rustls::DEFAULT_VERSIONS + } else { + &[&rustls::version::TLS12] + }; + let mut config: rustls::ClientConfig = if ech_enabled { + // ECH requires a different builder chain (no explicit protocol versions) rustls::ClientConfig::builder_with_provider(crypto_provider_arc) .with_ech(get_ech_mode()) .unwrap() @@ -167,7 +175,7 @@ impl TlsConfigBuilder { .with_no_client_auth() } else { rustls::ClientConfig::builder_with_provider(crypto_provider_arc) - .with_safe_default_protocol_versions() + .with_protocol_versions(protocol_versions) .unwrap() .dangerous() .with_custom_certificate_verifier(verifier)