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}