Skip to main content

hpx_browser/
tls.rs

1//! TLS ClientHello fingerprint configurations.
2//!
3//! Ported from browser_oxide's `net/tls.rs`. Provides per-browser TLS
4//! fingerprint presets (Chrome 147, Safari iOS 18, Firefox 135) that
5//! produce JA3/JA4-identical ClientHellos when used with hpx's TLS layer.
6//!
7//! Each preset defines cipher suites, signature algorithms, curves, and
8//! extension ordering as string/integer constants. The [`DeviceFingerprint`]
9//! struct converts these into hpx's [`TlsOptions`] for actual connection use.
10
11use hpx::tls::TlsOptions;
12use rand::prelude::SliceRandom;
13
14use crate::stealth::DeviceClass;
15
16// ---------------------------------------------------------------------------
17// Chrome 147 cipher suites (15 ciphers, order is JA3-critical)
18// ---------------------------------------------------------------------------
19
20const CHROME_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
21TLS_AES_256_GCM_SHA384:\
22TLS_CHACHA20_POLY1305_SHA256:\
23TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
24TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
25TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
26TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
27TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
28TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
29TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
30TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
31TLS_RSA_WITH_AES_128_GCM_SHA256:\
32TLS_RSA_WITH_AES_256_GCM_SHA384:\
33TLS_RSA_WITH_AES_128_CBC_SHA:\
34TLS_RSA_WITH_AES_256_CBC_SHA";
35
36// ---------------------------------------------------------------------------
37// Chrome 147 signature algorithms (8 entries)
38// ---------------------------------------------------------------------------
39
40const CHROME_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
41rsa_pss_rsae_sha256:\
42rsa_pkcs1_sha256:\
43ecdsa_secp384r1_sha384:\
44rsa_pss_rsae_sha384:\
45rsa_pkcs1_sha384:\
46rsa_pss_rsae_sha512:\
47rsa_pkcs1_sha512";
48
49// ---------------------------------------------------------------------------
50// Chrome 147 curves — MLKEM768 post-quantum (Chrome 131+)
51// ---------------------------------------------------------------------------
52
53const CHROME_CURVES_LIST: &str = "X25519MLKEM768:X25519:P-256:P-384";
54
55// ---------------------------------------------------------------------------
56// Safari iOS 18 cipher suites (20 ciphers, Apple's order)
57// ---------------------------------------------------------------------------
58
59const SAFARI_IOS_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
60TLS_AES_256_GCM_SHA384:\
61TLS_CHACHA20_POLY1305_SHA256:\
62TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
63TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
64TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
65TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
66TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
67TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
68TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:\
69TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:\
70TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
71TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
72TLS_RSA_WITH_AES_256_GCM_SHA384:\
73TLS_RSA_WITH_AES_128_GCM_SHA256:\
74TLS_RSA_WITH_AES_256_CBC_SHA:\
75TLS_RSA_WITH_AES_128_CBC_SHA:\
76TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:\
77TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:\
78TLS_RSA_WITH_3DES_EDE_CBC_SHA";
79
80// ---------------------------------------------------------------------------
81// Safari iOS 18 signature algorithms (10 entries, includes duplicated
82// rsa_pss_rsae_sha384 Apple quirk)
83// ---------------------------------------------------------------------------
84
85const SAFARI_IOS_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
86rsa_pss_rsae_sha256:\
87rsa_pkcs1_sha256:\
88ecdsa_secp384r1_sha384:\
89rsa_pss_rsae_sha384:\
90rsa_pss_rsae_sha384:\
91rsa_pkcs1_sha384:\
92rsa_pss_rsae_sha512:\
93rsa_pkcs1_sha512:\
94rsa_pkcs1_sha1";
95
96// ---------------------------------------------------------------------------
97// Safari iOS 18 curves — no PQ, includes P-521
98// ---------------------------------------------------------------------------
99
100const SAFARI_IOS_CURVES_LIST: &str = "X25519:P-256:P-384:P-521";
101
102// ---------------------------------------------------------------------------
103// Firefox 135 cipher suites (17 ciphers, NSS order)
104// ---------------------------------------------------------------------------
105
106const FIREFOX_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
107TLS_CHACHA20_POLY1305_SHA256:\
108TLS_AES_256_GCM_SHA384:\
109TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
110TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
111TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
112TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
113TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
114TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
115TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:\
116TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:\
117TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
118TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
119TLS_RSA_WITH_AES_128_GCM_SHA256:\
120TLS_RSA_WITH_AES_256_GCM_SHA384:\
121TLS_RSA_WITH_AES_128_CBC_SHA:\
122TLS_RSA_WITH_AES_256_CBC_SHA";
123
124// ---------------------------------------------------------------------------
125// Firefox 135 signature algorithms (11 entries, includes ecdsa_sha1 and
126// rsa_pkcs1_sha1 tail)
127// ---------------------------------------------------------------------------
128
129const FIREFOX_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
130ecdsa_secp384r1_sha384:\
131ecdsa_secp521r1_sha512:\
132rsa_pss_rsae_sha256:\
133rsa_pss_rsae_sha384:\
134rsa_pss_rsae_sha512:\
135rsa_pkcs1_sha256:\
136rsa_pkcs1_sha384:\
137rsa_pkcs1_sha512:\
138ecdsa_sha1:\
139rsa_pkcs1_sha1";
140
141// ---------------------------------------------------------------------------
142// Firefox 135 curves — MLKEM768 PQ + FFDHE groups (Firefox-only)
143// ---------------------------------------------------------------------------
144
145const FIREFOX_CURVES_LIST: &str = "X25519MLKEM768:X25519:P-256:P-384:P-521:ffdhe2048:ffdhe3072";
146
147// ---------------------------------------------------------------------------
148// Firefox 135 delegated_credentials (ext 0x22) sigalg list
149// ---------------------------------------------------------------------------
150
151const FIREFOX_DELEGATED_CREDENTIALS: &str = "ecdsa_secp256r1_sha256:\
152ecdsa_secp384r1_sha384:\
153ecdsa_secp521r1_sha512:\
154ecdsa_sha1";
155
156// ---------------------------------------------------------------------------
157// Firefox 135 record_size_limit (ext 0x1c) value: 0x4001 (16385)
158// ---------------------------------------------------------------------------
159
160const FIREFOX_RECORD_SIZE_LIMIT: u16 = 0x4001;
161
162// ---------------------------------------------------------------------------
163// Extension permutation indices
164//
165// Indices into BoringSSL's internal `BORING_SSLEXTENSION_PERMUTATION` table.
166// 0=server_name, 1=encrypted_client_hello, 2=extended_master_secret,
167// 3=renegotiate, 4=supported_groups, 5=ec_point_formats, 6=session_ticket,
168// 7=ALPN, 8=status_request, 9=signature_algorithms, 11=certificate_timestamp,
169// 14=key_share, 15=psk_key_exchange_modes, 17=supported_versions,
170// 21=cert_compression, 22=delegated_credentials, 24=application_settings_new,
171// 25=record_size_limit
172// ---------------------------------------------------------------------------
173
174/// Chrome 147 extensions (16 total). Fisher-Yates shuffled per handshake.
175const CHROME_EXTENSION_PERMUTATION: [u16; 16] = [
176    51,    // key_share
177    65037, // encrypted_client_hello
178    10,    // supported_groups
179    18,    // certificate_timestamp
180    45,    // psk_key_exchange_modes
181    23,    // extended_master_secret
182    17613, // application_settings_new
183    27,    // cert_compression
184    43,    // supported_versions
185    0,     // server_name
186    65281, // renegotiate
187    11,    // ec_point_formats
188    5,     // status_request
189    16,    // ALPN
190    35,    // session_ticket
191    13,    // signature_algorithms
192];
193
194/// Safari iOS 18 extension order — FIXED (no shuffle). 13 extensions.
195const SAFARI_IOS_EXTENSION_PERMUTATION: [u16; 13] = [
196    0,     // server_name
197    23,    // extended_master_secret
198    65281, // renegotiate
199    10,    // supported_groups
200    11,    // ec_point_formats
201    16,    // ALPN
202    5,     // status_request
203    13,    // signature_algorithms
204    18,    // certificate_timestamp
205    51,    // key_share
206    45,    // psk_key_exchange_modes
207    43,    // supported_versions
208    27,    // cert_compression
209];
210
211/// Firefox 135 extension order — FIXED (no shuffle). 15 extensions.
212const FIREFOX_EXTENSION_PERMUTATION: [u16; 15] = [
213    0,     // server_name
214    23,    // extended_master_secret
215    65281, // renegotiation_info
216    10,    // supported_groups
217    11,    // ec_point_formats
218    35,    // session_ticket
219    16,    // ALPN
220    5,     // status_request
221    34,    // delegated_credentials (0x22)
222    51,    // key_share
223    43,    // supported_versions
224    13,    // signature_algorithms
225    45,    // psk_key_exchange_modes
226    28,    // record_size_limit (0x1c)
227    65037, // encrypted_client_hello (ECH grease)
228];
229
230// ---------------------------------------------------------------------------
231// Certificate compression
232// ---------------------------------------------------------------------------
233
234/// Certificate compression algorithm identifiers (RFC 8879).
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum CertCompression {
237    Brotli,
238    Zlib,
239}
240
241// ---------------------------------------------------------------------------
242// DeviceFingerprint — per-browser TLS config
243// ---------------------------------------------------------------------------
244
245/// Per-browser TLS ClientHello fingerprint configuration.
246///
247/// Holds the raw TLS parameters needed to produce a JA3/JA4-identical
248/// ClientHello. Call [`to_tls_options`](Self::to_tls_options) to convert
249/// into hpx's [`TlsOptions`].
250#[derive(Debug, Clone)]
251pub struct DeviceFingerprint {
252    /// Colon-separated cipher suite string for BoringSSL.
253    pub cipher_list: &'static str,
254    /// Colon-separated signature algorithms string.
255    pub sigalgs_list: &'static str,
256    /// Colon-separated curves list.
257    pub curves_list: &'static str,
258    /// Extension type IDs in emission order.
259    pub extension_permutation: Vec<u16>,
260    /// Certificate compression algorithms.
261    pub cert_compression: Vec<CertCompression>,
262    /// Minimum TLS version (`"1.0"` or `"1.2"`).
263    pub min_tls_version: &'static str,
264    /// Enable ECH GREASE.
265    pub ech_grease: bool,
266    /// Enable extension permutation (Fisher-Yates shuffle).
267    pub permute_extensions: bool,
268    /// Disable session tickets (Safari iOS quirk).
269    pub no_session_ticket: bool,
270    /// GREASE enabled (disabled for Firefox).
271    pub grease_enabled: bool,
272    /// Enable OCSP stapling.
273    pub ocsp_stapling: bool,
274    /// Enable Signed Certificate Timestamps.
275    pub signed_cert_timestamps: bool,
276    /// Maximum key shares.
277    pub key_shares_limit: u8,
278    /// Delegated credentials sigalg list (Firefox-only).
279    pub delegated_credentials: Option<&'static str>,
280    /// Record size limit (Firefox-only).
281    pub record_size_limit: Option<u16>,
282    /// Use new ALPS codepoint.
283    pub alps_use_new_codepoint: bool,
284}
285
286impl DeviceFingerprint {
287    /// Convert to hpx's [`TlsOptions`].
288    ///
289    /// Note: `extension_permutation` is not set here because hpx's
290    /// `TlsOptionsBuilder` uses boring's `ExtensionType` which requires
291    /// platform-specific conversion. Use [`extension_permutation`](Self::extension_permutation)
292    /// to get the raw extension IDs.
293    pub fn to_tls_options(&self) -> TlsOptions {
294        let min_ver = match self.min_tls_version {
295            "1.0" => hpx::tls::TlsVersion::TLS_1_0,
296            _ => hpx::tls::TlsVersion::TLS_1_2,
297        };
298
299        let mut builder = TlsOptions::builder()
300            .cipher_list(self.cipher_list)
301            .sigalgs_list(self.sigalgs_list)
302            .curves_list(self.curves_list)
303            .session_ticket(!self.no_session_ticket)
304            .enable_ech_grease(self.ech_grease)
305            .permute_extensions(Some(false))
306            .grease_enabled(Some(self.grease_enabled))
307            .enable_ocsp_stapling(self.ocsp_stapling)
308            .enable_signed_cert_timestamps(self.signed_cert_timestamps)
309            .key_shares_limit(Some(self.key_shares_limit))
310            .alps_use_new_codepoint(self.alps_use_new_codepoint)
311            .min_tls_version(min_ver)
312            .max_tls_version(hpx::tls::TlsVersion::TLS_1_3);
313
314        if let Some(dc) = self.delegated_credentials {
315            builder = builder.delegated_credentials(dc);
316        }
317        if let Some(limit) = self.record_size_limit {
318            builder = builder.record_size_limit(limit);
319        }
320
321        builder.build()
322    }
323
324    /// Return the extension permutation, shuffled if this is a Chrome profile.
325    pub fn get_extension_permutation(&self) -> Vec<u16> {
326        if self.permute_extensions {
327            let mut rng = rand::rng();
328            let mut perm = self.extension_permutation.clone();
329            perm.shuffle(&mut rng);
330            perm
331        } else {
332            self.extension_permutation.clone()
333        }
334    }
335
336    /// Fisher-Yates shuffle of the Chrome extension permutation (for testing).
337    pub fn shuffled_chrome_permutation() -> Vec<u16> {
338        let mut rng = rand::rng();
339        let mut perm = CHROME_EXTENSION_PERMUTATION.to_vec();
340        perm.shuffle(&mut rng);
341        perm
342    }
343
344    /// Return the correct fingerprint for a device class and browser name.
345    pub fn for_device(device_class: DeviceClass, browser_name: &str) -> Self {
346        if browser_name.eq_ignore_ascii_case("firefox") {
347            return Self::firefox_135();
348        }
349        match device_class {
350            DeviceClass::Desktop | DeviceClass::MobileAndroid => Self::chrome_147(),
351            DeviceClass::MobileIOS => Self::safari_ios_18(),
352        }
353    }
354
355    /// Chrome 147 desktop/Android fingerprint.
356    pub fn chrome_147() -> Self {
357        Self {
358            cipher_list: CHROME_CIPHER_LIST,
359            sigalgs_list: CHROME_SIGALGS_LIST,
360            curves_list: CHROME_CURVES_LIST,
361            extension_permutation: CHROME_EXTENSION_PERMUTATION.to_vec(),
362            cert_compression: vec![CertCompression::Brotli],
363            min_tls_version: "1.2",
364            ech_grease: true,
365            permute_extensions: true,
366            no_session_ticket: false,
367            grease_enabled: true,
368            ocsp_stapling: true,
369            signed_cert_timestamps: true,
370            key_shares_limit: 2,
371            delegated_credentials: None,
372            record_size_limit: None,
373            alps_use_new_codepoint: true,
374        }
375    }
376
377    /// Safari iOS 18 fingerprint.
378    pub fn safari_ios_18() -> Self {
379        Self {
380            cipher_list: SAFARI_IOS_CIPHER_LIST,
381            sigalgs_list: SAFARI_IOS_SIGALGS_LIST,
382            curves_list: SAFARI_IOS_CURVES_LIST,
383            extension_permutation: SAFARI_IOS_EXTENSION_PERMUTATION.to_vec(),
384            cert_compression: vec![CertCompression::Zlib],
385            min_tls_version: "1.0",
386            ech_grease: false,
387            permute_extensions: false,
388            no_session_ticket: true,
389            grease_enabled: true,
390            ocsp_stapling: true,
391            signed_cert_timestamps: true,
392            key_shares_limit: 2,
393            delegated_credentials: None,
394            record_size_limit: None,
395            alps_use_new_codepoint: false,
396        }
397    }
398
399    /// Firefox 135 (NSS) fingerprint.
400    pub fn firefox_135() -> Self {
401        Self {
402            cipher_list: FIREFOX_CIPHER_LIST,
403            sigalgs_list: FIREFOX_SIGALGS_LIST,
404            curves_list: FIREFOX_CURVES_LIST,
405            extension_permutation: FIREFOX_EXTENSION_PERMUTATION.to_vec(),
406            cert_compression: vec![CertCompression::Zlib, CertCompression::Brotli],
407            min_tls_version: "1.2",
408            ech_grease: true,
409            permute_extensions: false,
410            no_session_ticket: false,
411            grease_enabled: false,
412            ocsp_stapling: true,
413            signed_cert_timestamps: true,
414            key_shares_limit: 2,
415            delegated_credentials: Some(FIREFOX_DELEGATED_CREDENTIALS),
416            record_size_limit: Some(FIREFOX_RECORD_SIZE_LIMIT),
417            alps_use_new_codepoint: false,
418        }
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn chrome_cipher_list_matches_reference() {
428        let expected = "TLS_AES_128_GCM_SHA256:\
429            TLS_AES_256_GCM_SHA384:\
430            TLS_CHACHA20_POLY1305_SHA256:\
431            TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
432            TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
433            TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
434            TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
435            TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
436            TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
437            TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
438            TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
439            TLS_RSA_WITH_AES_128_GCM_SHA256:\
440            TLS_RSA_WITH_AES_256_GCM_SHA384:\
441            TLS_RSA_WITH_AES_128_CBC_SHA:\
442            TLS_RSA_WITH_AES_256_CBC_SHA";
443        assert_eq!(CHROME_CIPHER_LIST, expected);
444    }
445
446    #[test]
447    fn chrome_sigalgs_matches_reference() {
448        let expected = "ecdsa_secp256r1_sha256:\
449            rsa_pss_rsae_sha256:\
450            rsa_pkcs1_sha256:\
451            ecdsa_secp384r1_sha384:\
452            rsa_pss_rsae_sha384:\
453            rsa_pkcs1_sha384:\
454            rsa_pss_rsae_sha512:\
455            rsa_pkcs1_sha512";
456        assert_eq!(CHROME_SIGALGS_LIST, expected);
457    }
458
459    #[test]
460    fn chrome_extension_count() {
461        assert_eq!(CHROME_EXTENSION_PERMUTATION.len(), 16);
462    }
463
464    #[test]
465    fn safari_extension_count() {
466        assert_eq!(SAFARI_IOS_EXTENSION_PERMUTATION.len(), 13);
467    }
468
469    #[test]
470    fn firefox_extension_count() {
471        assert_eq!(FIREFOX_EXTENSION_PERMUTATION.len(), 15);
472    }
473
474    #[test]
475    fn safari_has_20_ciphers() {
476        assert_eq!(SAFARI_IOS_CIPHER_LIST.matches(':').count() + 1, 20);
477    }
478
479    #[test]
480    fn firefox_has_17_ciphers() {
481        assert_eq!(FIREFOX_CIPHER_LIST.matches(':').count() + 1, 17);
482    }
483
484    #[test]
485    fn safari_sigalg_has_duplicate_rsa_pss_rsae_sha384() {
486        // Apple quirk: rsa_pss_rsae_sha384 appears twice
487        let count = SAFARI_IOS_SIGALGS_LIST
488            .split(':')
489            .filter(|s| *s == "rsa_pss_rsae_sha384")
490            .count();
491        assert_eq!(count, 2);
492    }
493
494    #[test]
495    fn firefox_curves_have_ffdhe() {
496        assert!(FIREFOX_CURVES_LIST.contains("ffdhe2048"));
497        assert!(FIREFOX_CURVES_LIST.contains("ffdhe3072"));
498    }
499
500    #[test]
501    fn chrome_curves_have_mlkem768() {
502        assert!(CHROME_CURVES_LIST.starts_with("X25519MLKEM768"));
503    }
504
505    #[test]
506    fn chrome_shuffle_preserves_set() {
507        let fp = DeviceFingerprint::chrome_147();
508        let p1 = fp.get_extension_permutation();
509        let p2 = fp.get_extension_permutation();
510
511        assert_eq!(p1.len(), 16);
512        assert_eq!(p2.len(), 16);
513
514        let mut sorted = p1.clone();
515        sorted.sort();
516        let mut expected = CHROME_EXTENSION_PERMUTATION.to_vec();
517        expected.sort();
518        assert_eq!(sorted, expected, "shuffle must preserve the set");
519        assert_ne!(p1, p2, "shuffle should be non-deterministic");
520    }
521
522    #[test]
523    fn chrome_preset_to_tls_options() {
524        let fp = DeviceFingerprint::chrome_147();
525        let opts = fp.to_tls_options();
526        assert!(opts.cipher_list.is_some());
527        assert!(opts.sigalgs_list.is_some());
528        assert!(opts.curves_list.is_some());
529        assert!(opts.session_ticket);
530        assert!(opts.enable_ech_grease);
531        assert_eq!(opts.grease_enabled, Some(true));
532        assert_eq!(opts.key_shares_limit, Some(2));
533        assert!(opts.alps_use_new_codepoint);
534        assert!(opts.delegated_credentials.is_none());
535        assert!(opts.record_size_limit.is_none());
536        // extension_permutation is available on the fingerprint directly
537        assert_eq!(fp.extension_permutation.len(), 16);
538    }
539
540    #[test]
541    fn safari_preset_to_tls_options() {
542        let fp = DeviceFingerprint::safari_ios_18();
543        let opts = fp.to_tls_options();
544        assert!(!opts.session_ticket);
545        assert!(!opts.enable_ech_grease);
546        assert_eq!(opts.min_tls_version, Some(hpx::tls::TlsVersion::TLS_1_0));
547        assert!(opts.delegated_credentials.is_none());
548    }
549
550    #[test]
551    fn firefox_preset_to_tls_options() {
552        let fp = DeviceFingerprint::firefox_135();
553        let opts = fp.to_tls_options();
554        assert_eq!(opts.grease_enabled, Some(false));
555        assert!(opts.delegated_credentials.is_some());
556        assert_eq!(opts.record_size_limit, Some(FIREFOX_RECORD_SIZE_LIMIT));
557    }
558
559    #[test]
560    fn for_device_dispatches_correctly() {
561        let chrome = DeviceFingerprint::for_device(DeviceClass::Desktop, "Chrome");
562        assert_eq!(chrome.cipher_list, CHROME_CIPHER_LIST);
563
564        let safari = DeviceFingerprint::for_device(DeviceClass::MobileIOS, "Safari");
565        assert_eq!(safari.cipher_list, SAFARI_IOS_CIPHER_LIST);
566
567        let firefox = DeviceFingerprint::for_device(DeviceClass::Desktop, "Firefox");
568        assert_eq!(firefox.cipher_list, FIREFOX_CIPHER_LIST);
569
570        // Firefox overrides device class
571        let ff_ios = DeviceFingerprint::for_device(DeviceClass::MobileIOS, "Firefox");
572        assert_eq!(ff_ios.cipher_list, FIREFOX_CIPHER_LIST);
573    }
574}