Skip to main content

specter/fingerprint/
tls.rs

1//! TLS fingerprint configuration for browser impersonation.
2//!
3//! Chrome randomizes TLS extension order since v110, making static
4//! JA3 fingerprints unreliable. Modern fingerprint detection systems use JA4 which sorts
5//! extensions alphabetically. This implementation provides cipher suite,
6//! signature algorithm, and curve ordering - but extension ordering may not
7//! match real browsers.
8//!
9//! Current implementation: Chrome 142-148, Firefox 133-151, Firefox ESR 115/128/140
10//!
11//! ## Post-Quantum Cryptography (Kyber)
12//!
13//! Chrome 124+ enables X25519Kyber768 hybrid key exchange by default. This requires
14//! BoringSSL compiled with post-quantum cryptography support. The `enable_kyber` flag
15//! in `TlsFingerprint` will attempt to enable Kyber, but will silently fail if the
16//! BoringSSL build does not support it.
17//!
18//! To verify Kyber support, check if connections show "X25519Kyber768" in the key
19//! exchange algorithm when connecting to servers that support it (e.g., Google, Cloudflare).
20
21/// Chrome 142-148 cipher suites in exact order.
22/// Unchanged across Chrome 142 through 148.
23pub const CHROME_CIPHER_SUITES: &[&str] = &[
24    "TLS_AES_128_GCM_SHA256",
25    "TLS_AES_256_GCM_SHA384",
26    "TLS_CHACHA20_POLY1305_SHA256",
27    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
28    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
29    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
30    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
31    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
32    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
33    "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
34    "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
35    "TLS_RSA_WITH_AES_128_GCM_SHA256",
36    "TLS_RSA_WITH_AES_256_GCM_SHA384",
37    "TLS_RSA_WITH_AES_128_CBC_SHA",
38    "TLS_RSA_WITH_AES_256_CBC_SHA",
39];
40
41/// Backwards-compatible alias for Chrome 142 cipher suites.
42pub const CHROME_142_CIPHER_SUITES: &[&str] = CHROME_CIPHER_SUITES;
43
44/// Chrome 142-148 signature algorithms.
45/// Unchanged across Chrome 142 through 148.
46pub const CHROME_SIGNATURE_ALGORITHMS: &[&str] = &[
47    "ecdsa_secp256r1_sha256",
48    "rsa_pss_rsae_sha256",
49    "rsa_pkcs1_sha256",
50    "ecdsa_secp384r1_sha384",
51    "rsa_pss_rsae_sha384",
52    "rsa_pkcs1_sha384",
53    "rsa_pss_rsae_sha512",
54    "rsa_pkcs1_sha512",
55];
56
57/// Backwards-compatible alias for Chrome 142 signature algorithms.
58pub const CHROME_142_SIGNATURE_ALGORITHMS: &[&str] = CHROME_SIGNATURE_ALGORITHMS;
59
60/// Chrome 142-148 supported curves.
61/// Unchanged across Chrome 142 through 148.
62pub const CHROME_CURVES: &[&str] = &["x25519", "P-256", "P-384"];
63
64/// Backwards-compatible alias for Chrome 142 curves.
65pub const CHROME_142_CURVES: &[&str] = CHROME_CURVES;
66
67/// Chrome 142-148 extension IDs in exact order.
68/// Unchanged across Chrome 142 through 148.
69pub const CHROME_EXTENSION_IDS: &[u16] =
70    &[0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 21];
71
72/// Backwards-compatible alias for Chrome 142 extension IDs.
73pub const CHROME_142_EXTENSION_IDS: &[u16] = CHROME_EXTENSION_IDS;
74
75/// Shared Firefox desktop cipher suites in exact order.
76/// Firefox prefers AES-GCM over ChaCha20 (unlike some mobile-optimized builds).
77pub const FIREFOX_CIPHER_SUITES: &[&str] = &[
78    // TLS 1.3 cipher suites
79    "TLS_AES_128_GCM_SHA256",
80    "TLS_AES_256_GCM_SHA384",
81    "TLS_CHACHA20_POLY1305_SHA256",
82    // TLS 1.2 ECDHE cipher suites
83    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
84    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
85    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
86    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
87    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
88    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
89    // Legacy TLS 1.2 cipher suites
90    "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
91    "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
92    "TLS_RSA_WITH_AES_128_GCM_SHA256",
93    "TLS_RSA_WITH_AES_256_GCM_SHA384",
94    "TLS_RSA_WITH_AES_128_CBC_SHA",
95    "TLS_RSA_WITH_AES_256_CBC_SHA",
96];
97
98/// Backwards-compatible alias for Firefox 133 cipher suites.
99pub const FIREFOX_133_CIPHER_SUITES: &[&str] = FIREFOX_CIPHER_SUITES;
100
101/// Shared Firefox desktop signature algorithms.
102/// Similar to Chrome but may have slight ordering differences.
103pub const FIREFOX_SIGNATURE_ALGORITHMS: &[&str] = &[
104    "ecdsa_secp256r1_sha256",
105    "rsa_pss_rsae_sha256",
106    "rsa_pkcs1_sha256",
107    "ecdsa_secp384r1_sha384",
108    "rsa_pss_rsae_sha384",
109    "rsa_pkcs1_sha384",
110    "rsa_pss_rsae_sha512",
111    "rsa_pkcs1_sha512",
112];
113
114/// Backwards-compatible alias for Firefox 133 signature algorithms.
115pub const FIREFOX_133_SIGNATURE_ALGORITHMS: &[&str] = FIREFOX_SIGNATURE_ALGORITHMS;
116
117/// Shared Firefox desktop supported curves.
118/// Firefox supports more curves than Chrome, including P-521.
119/// BoringSSL uses curve names "P-256", "P-384", "P-521" rather than
120/// the standard "secp256r1", "secp384r1", "secp521r1" identifiers.
121pub const FIREFOX_CURVES: &[&str] = &["x25519", "P-256", "P-384", "P-521"];
122
123/// Backwards-compatible alias for Firefox 133 curves.
124pub const FIREFOX_133_CURVES: &[&str] = FIREFOX_CURVES;
125
126/// Shared Firefox desktop extension IDs.
127/// Firefox randomizes extension order (similar to Chrome 110+),
128/// so JA3 fingerprints will vary. JA4 sorts extensions for stable fingerprinting.
129pub const FIREFOX_EXTENSION_IDS: &[u16] =
130    &[0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 21];
131
132/// Backwards-compatible alias for Firefox 133 extension IDs.
133pub const FIREFOX_133_EXTENSION_IDS: &[u16] = FIREFOX_EXTENSION_IDS;
134
135/// Certificate compression algorithm.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum CertCompression {
138    /// Brotli compression (Chrome uses this).
139    Brotli,
140    /// Zlib compression.
141    Zlib,
142    /// No certificate compression.
143    None,
144}
145
146/// Native TLS extension-order behavior.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum TlsExtensionOrderBehavior {
149    /// Let BoringSSL apply browser-like extension permutation.
150    BrowserPermuted,
151    /// Disable BoringSSL extension permutation so repeated captures are stable.
152    Deterministic,
153}
154
155/// Historical reason string preserved so downstream callers that pattern-match on the
156/// "TLS resumption: unsupported" diagnostic still link. Native H3 now wires
157/// `SSL_CTX_sess_set_new_cb` / `SSL_set_session` (RFC 8446 section 2.2) and
158/// `SSL_set_quic_early_data_context` (RFC 9001 section 4.6), so the constants
159/// are retained only as immutable historical text.
160pub const NATIVE_H3_SESSION_RESUMPTION_UNSUPPORTED_REASON: &str =
161    "native H3 does not yet wire BoringSSL session tickets into the QUIC handshake";
162pub const NATIVE_H3_ZERO_RTT_UNSUPPORTED_REASON: &str =
163    "native H3 cannot send 0-RTT until session resumption and early-data transport replay are implemented";
164
165/// Native HTTP/3 TLS feature support status.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum NativeH3TlsFeatureStatus {
168    Supported,
169    Unsupported { reason: &'static str },
170}
171
172impl NativeH3TlsFeatureStatus {
173    pub fn is_supported(self) -> bool {
174        matches!(self, Self::Supported)
175    }
176}
177
178/// Native HTTP/3 TLS capabilities derived from the current fingerprint/config surface.
179///
180/// Session resumption is reported as [`NativeH3TlsFeatureStatus::Supported`] because
181/// the native H3 TLS context now captures TLS 1.3 NewSessionTicket frames via
182/// `SSL_CTX_sess_set_new_cb` and replays them with `SSL_set_session` (RFC 8446
183/// section 4.6.1). 0-RTT is reported as [`NativeH3TlsFeatureStatus::Supported`]
184/// because the native H3 TLS context configures a QUIC early-data context via
185/// `SSL_set_quic_early_data_context` and surfaces accept/reject through
186/// `SSL_early_data_accepted` per RFC 9001 sections 4.6 / 9.2. Per-handshake
187/// outcome is reported separately by `NativeQuicTlsSession::handshake_status`.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct NativeH3TlsCapabilities {
190    pub session_resumption: NativeH3TlsFeatureStatus,
191    pub zero_rtt: NativeH3TlsFeatureStatus,
192}
193
194impl NativeH3TlsCapabilities {
195    pub fn current() -> Self {
196        Self {
197            session_resumption: NativeH3TlsFeatureStatus::Supported,
198            zero_rtt: NativeH3TlsFeatureStatus::Supported,
199        }
200    }
201
202    pub fn supports_session_resumption(self) -> bool {
203        self.session_resumption.is_supported()
204    }
205
206    pub fn supports_zero_rtt(self) -> bool {
207        self.zero_rtt.is_supported()
208    }
209}
210
211/// TLS fingerprint configuration.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct TlsFingerprint {
214    /// Cipher suites in order.
215    pub cipher_list: Vec<&'static str>,
216    /// Signature algorithms.
217    pub sigalgs: Vec<&'static str>,
218    /// Supported curves/groups.
219    pub curves: Vec<&'static str>,
220    /// TLS extensions.
221    pub extensions: Vec<u16>,
222    /// Extension order (for JA3 fingerprint).
223    pub extension_order: Vec<u16>,
224    /// Native extension-order behavior for ClientHello generation.
225    pub extension_order_behavior: TlsExtensionOrderBehavior,
226    /// Enable GREASE values.
227    pub grease: bool,
228    /// Certificate compression algorithm (compress_certificate extension).
229    /// Chrome 142 uses Brotli. Firefox does not use certificate compression.
230    pub cert_compression: CertCompression,
231    /// Enable post-quantum X25519Kyber768 hybrid key exchange.
232    /// Chrome 124+ enables this by default. Requires BoringSSL with post-quantum support.
233    /// Implemented by including "X25519Kyber768Draft00" in the curves/groups list.
234    pub enable_kyber: bool,
235}
236
237impl Default for TlsFingerprint {
238    fn default() -> Self {
239        Self {
240            cipher_list: vec![],
241            sigalgs: vec![],
242            curves: vec![],
243            extensions: vec![],
244            extension_order: vec![],
245            extension_order_behavior: TlsExtensionOrderBehavior::Deterministic,
246            grease: true,
247            cert_compression: CertCompression::None,
248            enable_kyber: false,
249        }
250    }
251}
252
253impl TlsFingerprint {
254    /// Create a TLS fingerprint matching Chrome 142-148.
255    /// The TLS configuration is identical across these versions.
256    pub fn chrome() -> Self {
257        Self {
258            cipher_list: CHROME_CIPHER_SUITES.to_vec(),
259            sigalgs: CHROME_SIGNATURE_ALGORITHMS.to_vec(),
260            curves: CHROME_CURVES.to_vec(),
261            extensions: CHROME_EXTENSION_IDS.to_vec(),
262            extension_order: CHROME_EXTENSION_IDS.to_vec(),
263            extension_order_behavior: TlsExtensionOrderBehavior::BrowserPermuted,
264            grease: true,
265            cert_compression: CertCompression::Brotli,
266            enable_kyber: true,
267        }
268    }
269
270    /// Create a TLS fingerprint for Chrome 142.
271    pub fn chrome_142() -> Self {
272        Self::chrome()
273    }
274
275    /// Create a TLS fingerprint for Chrome 143.
276    pub fn chrome_143() -> Self {
277        Self::chrome()
278    }
279
280    /// Create a TLS fingerprint for Chrome 144.
281    pub fn chrome_144() -> Self {
282        Self::chrome()
283    }
284
285    /// Create a TLS fingerprint for Chrome 145.
286    pub fn chrome_145() -> Self {
287        Self::chrome()
288    }
289
290    /// Create a TLS fingerprint for Chrome 146.
291    pub fn chrome_146() -> Self {
292        Self::chrome()
293    }
294
295    /// Create a TLS fingerprint for Chrome 147.
296    pub fn chrome_147() -> Self {
297        Self::chrome()
298    }
299
300    /// Create a TLS fingerprint for Chrome 148.
301    pub fn chrome_148() -> Self {
302        Self::chrome()
303    }
304
305    /// Create a shared TLS fingerprint for Firefox desktop profiles.
306    ///
307    /// Firefox differs from Chrome in:
308    /// - Cipher suite order (AES-GCM preferred, ChaCha20 third)
309    /// - More curves supported (includes P-521)
310    /// - No GREASE values (Firefox doesn't use GREASE)
311    /// - Extension order randomization (like Chrome 110+)
312    /// - No certificate compression (Firefox does not use compress_certificate)
313    /// - Post-quantum Kyber disabled by default (requires manual flag)
314    pub fn firefox() -> Self {
315        Self {
316            cipher_list: FIREFOX_CIPHER_SUITES.to_vec(),
317            sigalgs: FIREFOX_SIGNATURE_ALGORITHMS.to_vec(),
318            curves: FIREFOX_CURVES.to_vec(),
319            extensions: FIREFOX_EXTENSION_IDS.to_vec(),
320            extension_order: FIREFOX_EXTENSION_IDS.to_vec(),
321            extension_order_behavior: TlsExtensionOrderBehavior::BrowserPermuted,
322            grease: false,                           // Firefox does NOT use GREASE
323            cert_compression: CertCompression::None, // Firefox does not use certificate compression
324            enable_kyber: false,                     // Firefox requires manual flag for Kyber
325        }
326    }
327
328    /// Compatibility alias for the shared Firefox desktop TLS fingerprint.
329    pub fn firefox_133() -> Self {
330        Self::firefox()
331    }
332
333    /// Native HTTP/3 TLS capability status for this fingerprint/config surface.
334    pub fn native_h3_capabilities(&self) -> NativeH3TlsCapabilities {
335        NativeH3TlsCapabilities::current()
336    }
337
338    /// Native HTTP/3 ClientHello extension-order behavior for this TLS fingerprint.
339    pub fn native_h3_extension_order_behavior(&self) -> TlsExtensionOrderBehavior {
340        self.extension_order_behavior
341    }
342
343    /// Stable, explicit-field key suitable for use as a connection-pool discriminator.
344    ///
345    /// Unlike `format!("{self:?}")`, this representation enumerates each
346    /// fingerprint-affecting field individually so adding new struct fields
347    /// will not silently change the keying behavior of pooled connections.
348    pub fn pool_key_string(&self) -> String {
349        let cert_compression = match self.cert_compression {
350            CertCompression::Brotli => "brotli",
351            CertCompression::Zlib => "zlib",
352            CertCompression::None => "none",
353        };
354        let extension_order_behavior = match self.extension_order_behavior {
355            TlsExtensionOrderBehavior::BrowserPermuted => "browser-permuted",
356            TlsExtensionOrderBehavior::Deterministic => "deterministic",
357        };
358        format!(
359            "ciphers={}|sigalgs={}|curves={}|exts={}|order={}|order_behavior={}|grease={}|cc={}|kyber={}",
360            self.cipher_list.join(","),
361            self.sigalgs.join(","),
362            self.curves.join(","),
363            self.extensions
364                .iter()
365                .map(|e| e.to_string())
366                .collect::<Vec<_>>()
367                .join(","),
368            self.extension_order
369                .iter()
370                .map(|e| e.to_string())
371                .collect::<Vec<_>>()
372                .join(","),
373            extension_order_behavior,
374            self.grease,
375            cert_compression,
376            self.enable_kyber,
377        )
378    }
379}