From 78feabc816462909d2fb760240e103fe2ba38c50 Mon Sep 17 00:00:00 2001 From: bekesibeni <124487317+bekesibeni@users.noreply.github.com> Date: Sat, 30 May 2026 07:34:25 +0200 Subject: [PATCH 1/2] feat: add iOS 18 system TLS fingerprint --- Cargo.lock | 9 +- Cargo.toml | 2 +- impit-node/index.d.ts | 3 +- impit-node/src/impit_builder.rs | 2 + impit-python/python/impit/impit.pyi | 2 +- impit-python/src/async_client.rs | 2 + impit-python/src/client.rs | 2 + impit/src/fingerprint/database.rs | 2 + impit/src/fingerprint/database/safari.rs | 159 +++++++++++++++++++++++ impit/src/fingerprint/mod.rs | 9 ++ impit/src/fingerprint/types.rs | 5 + 11 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 impit/src/fingerprint/database/safari.rs diff --git a/Cargo.lock b/Cargo.lock index eb1aa73a..fac4c5a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2154,7 +2154,7 @@ dependencies = [ [[package]] name = "rustls" version = "0.23.39" -source = "git+https://github.com/apify/rustls?rev=d0a6a3eb5526176bd2e0a366f4f1b83598e2cd83#d0a6a3eb5526176bd2e0a366f4f1b83598e2cd83" +source = "git+https://github.com/apify/rustls?rev=1ebb6d466a557858cdd8c836ffcbb26d04d7a9f9#1ebb6d466a557858cdd8c836ffcbb26d04d7a9f9" dependencies = [ "aws-lc-rs", "brotli", @@ -2166,6 +2166,7 @@ dependencies = [ "rustls-webpki", "subtle", "zeroize", + "zlib-rs", ] [[package]] @@ -3422,6 +3423,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 82311295..689a48a3 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="d0a6a3eb5526176bd2e0a366f4f1b83598e2cd83" } +rustls = { git = "https://github.com/apify/rustls", rev="1ebb6d466a557858cdd8c836ffcbb26d04d7a9f9" } 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 531d6d42..08b21fb9 100644 --- a/impit-node/index.d.ts +++ b/impit-node/index.d.ts @@ -271,7 +271,8 @@ export type Browser = 'chrome'| 'okhttp'| 'okhttp3'| 'okhttp4'| -'okhttp5'; +'okhttp5'| +'ios18'; export type HttpMethod = 'GET'| 'POST'| diff --git a/impit-node/src/impit_builder.rs b/impit-node/src/impit_builder.rs index 2ff9db0d..85e6a20e 100644 --- a/impit-node/src/impit_builder.rs +++ b/impit-node/src/impit_builder.rs @@ -36,6 +36,7 @@ pub enum Browser { OkHttp3, OkHttp4, OkHttp5, + Ios18, } /// Options for configuring an {@link Impit} instance. @@ -143,6 +144,7 @@ impl From for BrowserFingerprint { Browser::OkHttp3 => impit::fingerprint::database::okhttp3::fingerprint(), Browser::OkHttp | Browser::OkHttp4 => impit::fingerprint::database::okhttp4::fingerprint(), Browser::OkHttp5 => impit::fingerprint::database::okhttp5::fingerprint(), + Browser::Ios18 => impit::fingerprint::database::ios_18::fingerprint(), } } } diff --git a/impit-python/python/impit/impit.pyi b/impit-python/python/impit/impit.pyi index f66df365..7be1fbf1 100644 --- a/impit-python/python/impit/impit.pyi +++ b/impit-python/python/impit/impit.pyi @@ -7,7 +7,7 @@ from collections.abc import Iterator, AsyncIterator from contextlib import AbstractAsyncContextManager, AbstractContextManager -Browser = Literal['chrome', 'firefox', 'chrome125', 'chrome100', 'chrome101', 'chrome104', 'chrome107', 'chrome110', 'chrome116', 'chrome131', 'chrome136', 'chrome142', 'firefox128', 'firefox133', 'firefox135', 'firefox144'] +Browser = Literal['chrome', 'firefox', 'chrome125', 'chrome100', 'chrome101', 'chrome104', 'chrome107', 'chrome110', 'chrome116', 'chrome131', 'chrome136', 'chrome142', 'firefox128', 'firefox133', 'firefox135', 'firefox144', 'ios18'] USE_CLIENT_DEFAULT: str """Sentinel that, when passed as a per-request ``timeout``, causes the client-level default timeout to be used. diff --git a/impit-python/src/async_client.rs b/impit-python/src/async_client.rs index 2e99089b..1401f7fd 100644 --- a/impit-python/src/async_client.rs +++ b/impit-python/src/async_client.rs @@ -98,6 +98,8 @@ impl AsyncClient { .with_fingerprint(impit::fingerprint::database::okhttp4::fingerprint()), "okhttp5" => builder .with_fingerprint(impit::fingerprint::database::okhttp5::fingerprint()), + "ios18" => builder + .with_fingerprint(impit::fingerprint::database::ios_18::fingerprint()), _ => { return Err(PyErr::new::( "Unsupported browser", diff --git a/impit-python/src/client.rs b/impit-python/src/client.rs index 2485ae4b..ece8bb8d 100644 --- a/impit-python/src/client.rs +++ b/impit-python/src/client.rs @@ -91,6 +91,8 @@ impl Client { .with_fingerprint(impit::fingerprint::database::okhttp4::fingerprint()), "okhttp5" => builder .with_fingerprint(impit::fingerprint::database::okhttp5::fingerprint()), + "ios18" => builder + .with_fingerprint(impit::fingerprint::database::ios_18::fingerprint()), _ => panic!("Unsupported browser"), }, None => builder, diff --git a/impit/src/fingerprint/database.rs b/impit/src/fingerprint/database.rs index 12e9fd2e..ced5cd05 100644 --- a/impit/src/fingerprint/database.rs +++ b/impit/src/fingerprint/database.rs @@ -5,6 +5,7 @@ mod chrome; mod firefox; mod okhttp; +mod safari; pub use chrome::{ chrome_100, chrome_101, chrome_104, chrome_107, chrome_110, chrome_116, chrome_124, chrome_125, @@ -12,3 +13,4 @@ pub use chrome::{ }; pub use firefox::{firefox_128, firefox_133, firefox_135, firefox_144}; pub use okhttp::{okhttp3, okhttp4, okhttp5}; +pub use safari::ios_18; diff --git a/impit/src/fingerprint/database/safari.rs b/impit/src/fingerprint/database/safari.rs new file mode 100644 index 00000000..418b1e9a --- /dev/null +++ b/impit/src/fingerprint/database/safari.rs @@ -0,0 +1,159 @@ +//! iOS system TLS fingerprints +//! +//! On iOS, Apple's App Store policy forces every app (Safari, Chrome iOS, +//! Firefox iOS, Edge iOS, and any native app using `NSURLSession` / +//! `Network.framework` / `WKWebView`) to use the OS networking stack, so a +//! single iOS TLS profile transparently covers the entire iOS browser and +//! native-app ecosystem. +//! +//! Source: capture against from an iPhone running +//! iOS 18.7 (Safari 26.5). Verified identical to a Chrome iOS 148 capture on +//! the same iOS version (same JA3, JA4, peetprint, and Akamai HTTP/2 +//! fingerprints), confirming the fingerprint is the OS stack rather than the +//! browser. + +use crate::fingerprint::*; + +/// iOS 18 system TLS fingerprint module +pub mod ios_18 { + use super::*; + + pub fn fingerprint() -> BrowserFingerprint { + BrowserFingerprint::new( + "Safari", + "iOS 18", + tls_fingerprint(), + http2_fingerprint(), + headers(), + ) + } + + fn tls_fingerprint() -> TlsFingerprint { + TlsFingerprint::new( + vec![ + CipherSuite::Grease, + CipherSuite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS13_AES_128_GCM_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, + CipherSuite::TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA, + CipherSuite::TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + CipherSuite::TLS_RSA_WITH_3DES_EDE_CBC_SHA, + ], + vec![ + KeyExchangeGroup::Grease, + KeyExchangeGroup::X25519MLKEM768, + KeyExchangeGroup::X25519, + KeyExchangeGroup::Secp256r1, + KeyExchangeGroup::Secp384r1, + KeyExchangeGroup::Secp521r1, + ], + // The duplicate RsaPssRsaSha384 entry is intentional — every + // observed iOS capture sends this list with the duplicate at + // indexes 4 and 5. + vec![ + SignatureAlgorithm::EcdsaSecp256r1Sha256, + SignatureAlgorithm::RsaPssRsaSha256, + SignatureAlgorithm::RsaPkcs1Sha256, + SignatureAlgorithm::EcdsaSecp384r1Sha384, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPssRsaSha384, + SignatureAlgorithm::RsaPkcs1Sha384, + SignatureAlgorithm::RsaPssRsaSha512, + SignatureAlgorithm::RsaPkcs1Sha512, + SignatureAlgorithm::RsaPkcs1Sha1, + ], + 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::Zlib]), // compress_certificate + false, // application_settings + false, // delegated_credentials + None, // record_size_limit + vec![ + ExtensionType::Grease, + ExtensionType::ServerName, + ExtensionType::ExtendedMasterSecret, + ExtensionType::RenegotiationInfo, + ExtensionType::SupportedGroups, + ExtensionType::EcPointFormats, + ExtensionType::ApplicationLayerProtocolNegotiation, + ExtensionType::StatusRequest, + ExtensionType::SignatureAlgorithms, + ExtensionType::SignedCertificateTimestamp, + ExtensionType::KeyShare, + ExtensionType::PskKeyExchangeModes, + ExtensionType::SupportedVersions, + ExtensionType::CompressCertificate, + ExtensionType::Grease, + ], + ) + .with_session_ticket(false), + None, + vec![b"h2".to_vec(), b"http/1.1".to_vec()], + ) + } + + fn http2_fingerprint() -> Http2Fingerprint { + Http2Fingerprint { + // iOS sends :method :scheme :authority :path on requests. + // :protocol (extended CONNECT) and :status (response) are + // required by the impit h2 fork to be in the order list even + // when not used on a given message. + pseudo_header_order: vec![ + ":method".to_string(), + ":scheme".to_string(), + ":authority".to_string(), + ":path".to_string(), + ":protocol".to_string(), + ":status".to_string(), + ], + initial_stream_window_size: Some(2_097_152), + // 65_535 (h2 default) + 10_420_225 WINDOW_UPDATE = 10_485_760. + initial_connection_window_size: Some(10_485_760), + max_header_list_size: None, + } + } + + fn headers() -> Vec<(String, String)> { + vec![ + ( + "user-agent".to_string(), + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 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(), + ), + ("sec-fetch-site".to_string(), "none".to_string()), + ("sec-fetch-mode".to_string(), "navigate".to_string()), + ("sec-fetch-dest".to_string(), "document".to_string()), + ("accept-language".to_string(), "en-US,en;q=0.9".to_string()), + ("priority".to_string(), "u=0, i".to_string()), + ( + "accept-encoding".to_string(), + "gzip, deflate, br, zstd".to_string(), + ), + ] + } +} diff --git a/impit/src/fingerprint/mod.rs b/impit/src/fingerprint/mod.rs index 1d6bae8e..73a2ef0d 100644 --- a/impit/src/fingerprint/mod.rs +++ b/impit/src/fingerprint/mod.rs @@ -287,6 +287,15 @@ impl TlsFingerprint { CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA => { FingerprintCipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA } + CipherSuite::TLS_RSA_WITH_3DES_EDE_CBC_SHA => { + FingerprintCipherSuite::TLS_RSA_WITH_3DES_EDE_CBC_SHA + } + CipherSuite::TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA => { + FingerprintCipherSuite::TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA + } + CipherSuite::TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA => { + FingerprintCipherSuite::TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA + } CipherSuite::Grease => FingerprintCipherSuite::Grease, }) .collect(); diff --git a/impit/src/fingerprint/types.rs b/impit/src/fingerprint/types.rs index 00028f03..d552dae4 100644 --- a/impit/src/fingerprint/types.rs +++ b/impit/src/fingerprint/types.rs @@ -26,6 +26,11 @@ pub enum CipherSuite { TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_256_CBC_SHA, + // Legacy 3DES suites: advertised in ClientHello for fingerprint + // accuracy only. Never actually negotiated (aws-lc-rs has no 3DES). + TLS_RSA_WITH_3DES_EDE_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA, + TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, /// GREASE cipher suite for fingerprinting Grease, } From 658c324855dce1343277e6cf572ca643d20a0106 Mon Sep 17 00:00:00 2001 From: bekesibeni <124487317+bekesibeni@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:57:56 +0200 Subject: [PATCH 2/2] style: rustfmt --- impit/src/fingerprint/database/safari.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/impit/src/fingerprint/database/safari.rs b/impit/src/fingerprint/database/safari.rs index 418b1e9a..23b926bd 100644 --- a/impit/src/fingerprint/database/safari.rs +++ b/impit/src/fingerprint/database/safari.rs @@ -81,15 +81,15 @@ pub mod ios_18 { 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 + true, // application_layer_protocol_negotiation + true, // signed_certificate_timestamp + true, // key_share + true, // psk_key_exchange_modes + true, // supported_versions Some(vec![CertificateCompressionAlgorithm::Zlib]), // compress_certificate - false, // application_settings - false, // delegated_credentials - None, // record_size_limit + false, // application_settings + false, // delegated_credentials + None, // record_size_limit vec![ ExtensionType::Grease, ExtensionType::ServerName,