diff --git a/rustls/src/client/client_emulator.rs b/rustls/src/client/client_emulator.rs deleted file mode 100644 index f588588271b..00000000000 --- a/rustls/src/client/client_emulator.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[derive(Clone, Debug)] -#[allow(missing_docs, clippy::exhaustive_structs)] -pub struct BrowserEmulator { - pub browser_type: BrowserType, - pub version: BrowserVersion, -} - -#[derive(Clone, Debug)] -#[allow(missing_docs, clippy::exhaustive_structs)] -pub struct BrowserVersion { - pub major: u8, - pub minor: u8, - pub patch: u8, -} - -#[derive(Clone, Debug)] -#[allow(clippy::exhaustive_enums, missing_docs)] -pub enum BrowserType { - Chrome, - Firefox, -} diff --git a/rustls/src/client/config.rs b/rustls/src/client/config.rs index d20632b98c6..87555b55e86 100644 --- a/rustls/src/client/config.rs +++ b/rustls/src/client/config.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "impit")] +use alloc::vec; use alloc::vec::Vec; use core::fmt; use core::marker::PhantomData; @@ -9,10 +11,10 @@ use super::ech::EchMode; use super::handy::ClientSessionMemoryCache; use super::handy::{FailResolveClientCert, NoClientSessionStorage}; use crate::builder::{ConfigBuilder, WantsVerifier}; -#[cfg(feature = "impit")] -use crate::client::client_emulator::BrowserEmulator; #[cfg(doc)] use crate::crypto; +#[cfg(feature = "impit")] +use crate::crypto::emulation::TlsFingerprint; use crate::crypto::kx::NamedGroup; use crate::crypto::{ CipherSuite, Credentials, CryptoProvider, Identity, SelectedCredential, SignatureScheme, @@ -60,13 +62,12 @@ use crate::{DistinguishedName, KeyLog, compress, verify}; /// [`RootCertStore`]: crate::RootCertStore #[derive(Clone, Debug)] pub struct ClientConfig { - /// Whether this client is using browser-emulated settings. - /// This is used by the retch_rust project to emulate browsers' JA4 fingerprints. + /// TLS fingerprint configuration for browser emulation. + /// This allows fine-grained control over TLS parameters to match specific browser fingerprints. /// - /// Note that this can be only set by the builder's `with_browser_emulation` method. - /// Setting this field directly won't work correctly and might cause inconsistencies in your JA4 fingerprints. + /// Note that this should be set via the builder's `with_tls_fingerprint` method. #[cfg(feature = "impit")] - pub browser_emulation: Option, + pub tls_fingerprint: Option, /// Which ALPN protocols we include in our client hello. /// If empty, no ALPN extension is sent. @@ -557,6 +558,24 @@ pub struct WantsClientCert { } impl ConfigBuilder { + /// Enable TLS fingerprinting with a custom fingerprint. + #[cfg(feature = "impit")] + pub fn with_tls_fingerprint( + self, + fingerprint: TlsFingerprint, + ) -> ConfigBuilder { + ConfigBuilder { + state: WantsClientCertWithTlsFingerprint { + verifier: self.state.verifier, + client_ech_mode: self.state.client_ech_mode, + tls_fingerprint: fingerprint, + }, + provider: self.provider, + time_provider: self.time_provider, + side: PhantomData, + } + } + /// Sets a single certificate chain and matching private key for use /// in client authentication. /// @@ -607,7 +626,7 @@ impl ConfigBuilder { alpn_protocols: Vec::new(), resumption: Resumption::default(), #[cfg(feature = "impit")] - browser_emulation: None, + tls_fingerprint: None, max_fragment_size: None, client_auth_cert_resolver, enable_sni: true, @@ -625,6 +644,96 @@ impl ConfigBuilder { } } +/// A config builder state where the caller needs to supply whether and how to provide a client +/// certificate, with TLS fingerprint enabled. +/// +/// For more information, see the [`ConfigBuilder`] documentation. +#[cfg(feature = "impit")] +#[derive(Clone)] +pub struct WantsClientCertWithTlsFingerprint { + verifier: Arc, + client_ech_mode: Option, + tls_fingerprint: TlsFingerprint, +} + +#[cfg(feature = "impit")] +impl ConfigBuilder { + /// Sets a single certificate chain and matching private key for use + /// in client authentication. + pub fn with_client_auth_cert( + self, + identity: Arc>, + key_der: PrivateKeyDer<'static>, + ) -> Result { + let credentials = Credentials::from_der(identity, key_der, &self.provider)?; + self.with_client_credential_resolver(Arc::new(SingleCredential::from(credentials))) + } + + /// Do not support client auth. + pub fn with_no_client_auth(self) -> Result { + self.with_client_credential_resolver(Arc::new(FailResolveClientCert {})) + } + + /// Sets a custom [`ClientCredentialResolver`]. + pub fn with_client_credential_resolver( + self, + client_auth_cert_resolver: Arc, + ) -> Result { + use crate::crypto::emulation::FingerprintCertCompressionAlgorithm; + + self.provider.consistency_check()?; + + // Determine cert compression based on fingerprint + let (cert_compressors, cert_decompressors) = if let Some(ref compression) = self + .state + .tls_fingerprint + .cert_compression + { + let compressors: Vec<_> = compression + .iter() + .filter_map(|alg| match alg { + FingerprintCertCompressionAlgorithm::Brotli => { + Some(compress::BROTLI_COMPRESSOR) + } + _ => None, // Only Brotli is supported for now + }) + .collect(); + let decompressors: Vec<_> = compression + .iter() + .filter_map(|alg| match alg { + FingerprintCertCompressionAlgorithm::Brotli => { + Some(compress::BROTLI_DECOMPRESSOR) + } + _ => None, + }) + .collect(); + (compressors, decompressors) + } else { + (vec![], vec![]) + }; + + Ok(ClientConfig { + tls_fingerprint: Some(self.state.tls_fingerprint), + provider: self.provider, + alpn_protocols: Vec::new(), + resumption: Resumption::default(), + max_fragment_size: None, + client_auth_cert_resolver, + enable_sni: true, + verifier: self.state.verifier, + key_log: Arc::new(crate::KeyLogFile::new()), + enable_secret_extraction: false, + enable_early_data: false, + require_ems: cfg!(feature = "fips"), + time_provider: self.time_provider, + cert_compressors, + cert_compression_cache: Arc::new(compress::CompressionCache::default()), + cert_decompressors, + ech_mode: self.state.client_ech_mode, + }) + } +} + /// Container for unsafe APIs pub(super) mod danger { use core::marker::PhantomData; diff --git a/rustls/src/client/hs.rs b/rustls/src/client/hs.rs index a6b28bca158..f62db9764aa 100644 --- a/rustls/src/client/hs.rs +++ b/rustls/src/client/hs.rs @@ -13,8 +13,6 @@ use super::connection::ClientConnectionData; use super::ech::{EchMode, EchState, EchStatus}; use super::{ClientHelloDetails, tls13}; use crate::check::inappropriate_handshake_message; -#[cfg(feature = "impit")] -use crate::client::client_emulator::{BrowserEmulator, BrowserType}; use crate::common_state::{CommonState, HandshakeKind, KxState, State}; use crate::crypto::cipher::Payload; use crate::crypto::kx::{KeyExchangeAlgorithm, NamedGroup, StartedKeyExchange}; @@ -583,14 +581,16 @@ fn emit_client_hello_for_retry( .collect(); #[cfg(feature = "impit")] - if let Some(BrowserEmulator { - browser_type: BrowserType::Chrome, - version: _, - }) = config.browser_emulation - { - offered_groups.push(NamedGroup::GREASE); - // offered_groups.push(NamedGroup::X25519Kyber768Draft00); - } + let offered_groups: Vec = if let Some(ref fingerprint) = config.tls_fingerprint { + // Use key exchange groups from TLS fingerprint + fingerprint + .key_exchange_groups + .iter() + .map(|g| g.to_named_group()) + .collect() + } else { + offered_groups + }; let mut exts = Box::new(ClientExtensions { supported_versions: Some(supported_versions), @@ -602,43 +602,52 @@ fn emit_client_hello_for_retry( .supported_verify_schemes(), ), extended_master_secret_request: Some(()), - certificate_status_request: match config.verifier.request_ocsp_response() { - true => Some(CertificateStatusRequest::build_ocsp()), - false => None, - }, + certificate_status_request: Some(CertificateStatusRequest::build_ocsp()), protocols: extra_exts.protocols.clone(), ..Default::default() }); #[cfg(feature = "impit")] - match config.browser_emulation { - Some(BrowserEmulator { - browser_type: BrowserType::Chrome, - version: _, - }) => { + if let Some(ref fingerprint) = config.tls_fingerprint { + // Apply TLS fingerprint extensions configuration + let ext_config = &fingerprint.extensions; + + if ext_config.grease { + exts.reserved_grease = Some(()); + } + + if ext_config.signed_certificate_timestamp { + exts.signed_certificate_timestamp = Some(()); + } + + if ext_config.application_settings { // hack - to avoid `Unexpected Message` when communicating with BoringSSL-based servers, // we cannot send an actual ALPN protocol name list let application_settings: PayloadU16 = PayloadU16::new(vec![0x05, 0x69, 0x6d, 0x70, 0x69, 0x74]); - - exts.reserved_grease = Some(()); - exts.signed_certificate_timestamp = Some(()); - exts.application_settings = Some(application_settings); - exts.renegotiation_info = Some(PayloadU8::empty()); + if ext_config.use_new_alps_codepoint { + // Use new ALPS codepoint (17613 / 0x44cd) for Chrome 136+ + exts.application_settings_new = Some(application_settings); + } else { + // Use old ALPS codepoint (17513 / 0x4469) + exts.application_settings = Some(application_settings); + } } - Some(BrowserEmulator { - browser_type: BrowserType::Firefox, - version: _, - }) => { + + if ext_config.delegated_credentials { // TODO: We don't really support the delegated credentials extension yet, just sending it in the client hello message let delegated_credentials_signature_algos = PayloadU16::new(vec![0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x02, 0x03]); - exts.delegated_credentials = Some(delegated_credentials_signature_algos); + } + + if ext_config.renegotiation_info { exts.renegotiation_info = Some(PayloadU8::empty()); - exts.record_size_limit = Some(16385); } - _ => {} + + if let Some(record_size_limit) = ext_config.record_size_limit { + exts.record_size_limit = Some(record_size_limit); + } } if let Some(TransportParameters::Quic(v)) = &extra_exts.transport_parameters { @@ -766,6 +775,28 @@ fn emit_client_hello_for_retry( // but they also need to keep the same order as the previous ClientHello exts.order_seed = input.hello.extension_order_seed; + #[cfg(feature = "impit")] + if let Some(ref fingerprint) = config.tls_fingerprint { + if !fingerprint + .extensions + .extension_order + .is_empty() + { + exts.contiguous_extensions = fingerprint + .extensions + .extension_order + .clone(); + } + + if !fingerprint + .extensions + .supported_versions + { + exts.supported_versions = None; + } + } + + #[cfg(not(feature = "impit"))] let mut cipher_suites: Vec<_> = config .provider .iter_cipher_suites() @@ -775,31 +806,42 @@ fn emit_client_hello_for_retry( }) .collect(); + #[cfg(feature = "impit")] + let mut cipher_suites: Vec<_> = if let Some(ref fingerprint) = config.tls_fingerprint { + // Use cipher suites from TLS fingerprint with correct codes for advertising + fingerprint + .cipher_suites + .iter() + .map(|cs| cs.to_cipher_suite()) + .collect() + } else { + config + .provider + .iter_cipher_suites() + .filter_map(|cs| match cs.usable_for_protocol(cx.common.protocol) { + true => Some(cs.suite()), + false => None, + }) + .collect() + }; + #[cfg(not(feature = "impit"))] // We don't do renegotiation at all, in fact. if supported_versions.tls12 { - // We don't do renegotiation at all, in fact. cipher_suites.push(CipherSuite::TLS_EMPTY_RENEGOTIATION_INFO_SCSV); } #[cfg(feature = "impit")] - match config.browser_emulation { - // Chrome doesn't send this cipher suite. - Some(BrowserEmulator { - browser_type: BrowserType::Chrome, - version: _, - }) => {} - // Firefox also doesn't seem to send this cipher suite? - Some(BrowserEmulator { - browser_type: BrowserType::Firefox, - version: _, - }) => {} - _ => { - // We don't do renegotiation at all, in fact. - if supported_versions.tls12 { - // We don't do renegotiation at all, in fact. - cipher_suites.push(CipherSuite::TLS_EMPTY_RENEGOTIATION_INFO_SCSV); - } + // Skip adding TLS_EMPTY_RENEGOTIATION_INFO_SCSV when using tls_fingerprint + if config.tls_fingerprint.is_none() && supported_versions.tls12 { + cipher_suites.push(CipherSuite::TLS_EMPTY_RENEGOTIATION_INFO_SCSV); + } + + // Add padding extension if configured (RFC7685) + #[cfg(feature = "impit")] + if let Some(ref fingerprint) = config.tls_fingerprint { + if fingerprint.extensions.padding { + exts.padding = Some(Payload::Borrowed(&[])); } } diff --git a/rustls/src/client/mod.rs b/rustls/src/client/mod.rs index a796eb9dc63..2e4eb6cf221 100644 --- a/rustls/src/client/mod.rs +++ b/rustls/src/client/mod.rs @@ -14,6 +14,8 @@ pub use crate::webpki::{ }; mod config; +#[cfg(feature = "impit")] +pub use config::WantsClientCertWithTlsFingerprint; pub use config::{ ClientConfig, ClientCredentialResolver, ClientSessionStore, CredentialRequest, Resumption, Tls12Resumption, WantsClientCert, @@ -42,9 +44,6 @@ pub(crate) use tls12::TLS12_HANDLER; mod tls13; pub(crate) use tls13::TLS13_HANDLER; -#[allow(missing_docs)] -pub mod client_emulator; - /// Dangerous configuration that should be audited and used with extreme care. pub mod danger { pub use super::config::danger::{DangerousClientConfig, DangerousClientConfigBuilder}; diff --git a/rustls/src/client/tls13.rs b/rustls/src/client/tls13.rs index 085420a5b9d..a0580f27b9b 100644 --- a/rustls/src/client/tls13.rs +++ b/rustls/src/client/tls13.rs @@ -329,6 +329,45 @@ pub(super) fn initial_key_share( server_name: &ServerName<'_>, kx_state: &mut KxState, ) -> Result { + // When fingerprinting is enabled, use the first non-GREASE group from the fingerprint + #[cfg(feature = "impit")] + let group = if let Some(ref fingerprint) = config.tls_fingerprint { + use crate::crypto::emulation::FingerprintKeyExchangeGroup; + + // Find the first non-GREASE group from the fingerprint + let first_real_group = fingerprint + .key_exchange_groups + .iter() + .find(|g| !matches!(g, FingerprintKeyExchangeGroup::Grease)) + .map(|g| g.to_named_group()) + .expect("No non-GREASE key exchange groups in fingerprint"); + + config + .provider + .find_kx_group(first_real_group, ProtocolVersion::TLSv1_3) + .expect("Fingerprint key exchange group not supported by provider") + } else { + config + .resumption + .store + .kx_hint(server_name) + .and_then(|group_name| { + config + .provider + .find_kx_group(group_name, ProtocolVersion::TLSv1_3) + }) + .unwrap_or_else(|| { + config + .provider + .kx_groups + .iter() + .copied() + .next() + .expect("No kx groups configured") + }) + }; + + #[cfg(not(feature = "impit"))] let group = config .resumption .store diff --git a/rustls/src/crypto/emulation/mod.rs b/rustls/src/crypto/emulation/mod.rs index 2ba07319fbf..74f1744b6b5 100644 --- a/rustls/src/crypto/emulation/mod.rs +++ b/rustls/src/crypto/emulation/mod.rs @@ -1,63 +1,313 @@ +#![allow(missing_docs, non_camel_case_types)] #![cfg(feature = "impit")] -use webpki::aws_lc_rs as webpki_algs_aws; +use alloc::vec::Vec; + +use crate::crypto::kx::NamedGroup; +use crate::crypto::{SignatureScheme, SupportedCipherSuite}; +use crate::msgs::enums::ExtensionType; use super::{WebPkiSupportedAlgorithms, aws_lc_rs}; -use crate::crypto::SignatureScheme; -use crate::{Tls12CipherSuite, Tls13CipherSuite}; - -/// The cipher suites supported by Google Chrome. -/// Note that some of these are not real cipher suites and their implementation doesn't match the specification. -pub static CHROME_TLS13_CIPHER_SUITES: [&Tls13CipherSuite; 10] = [ - aws_lc_rs::cipher_suite::TLS13_RESERVED_GREASE, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256, - aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384, - aws_lc_rs::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_128_GCM_SHA256, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_256_GCM_SHA384, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_128_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_256_CBC_SHA, // fake cipher suite from the patch -]; - -/// The TLS 1.2 cipher suites supported by Google Chrome. -pub static CHROME_TLS12_CIPHER_SUITES: [&Tls12CipherSuite; 6] = [ - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, -]; - -/// The cipher suites supported by Firefox. -/// Note that some of these are not real cipher suites and their implementation doesn't match the specification. -pub static FIREFOX_TLS13_CIPHER_SUITES: [&Tls13CipherSuite; 11] = [ - aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256, - aws_lc_rs::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, - aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384, - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_128_GCM_SHA256, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_256_GCM_SHA384, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_128_CBC_SHA, // fake cipher suite from the patch - aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_256_CBC_SHA, // fake cipher suite from the patch -]; - -/// The TLS 1.2 cipher suites supported by Firefox. -pub static FIREFOX_TLS12_CIPHER_SUITES: [&Tls12CipherSuite; 6] = [ - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, -]; - -/// The signature verification algorithms supported by Google Chrome. -pub static CHROME_SIGNATURE_VERIFICATION_ALGOS: WebPkiSupportedAlgorithms = +use webpki::aws_lc_rs as webpki_algs_aws; + +/// TLS fingerprint configuration for browser emulation. +/// +/// This struct allows fine-grained control over TLS parameters +/// to match specific browser fingerprints. +#[derive(Clone, Debug)] +pub struct TlsFingerprint { + /// Cipher suites in preference order + pub cipher_suites: Vec, + /// Key exchange groups in preference order + pub key_exchange_groups: Vec, + /// Signature algorithms in preference order + pub signature_algorithms: Vec, + /// TLS extensions configuration + pub extensions: TlsExtensionsConfig, + /// ALPN protocols in preference order + pub alpn_protocols: Vec>, + /// Certificate compression algorithms + pub cert_compression: Option>, +} + +impl TlsFingerprint { + /// Creates a new TLS fingerprint with the given configuration. + pub fn new( + cipher_suites: Vec, + key_exchange_groups: Vec, + signature_algorithms: Vec, + extensions: TlsExtensionsConfig, + alpn_protocols: Vec>, + cert_compression: Option>, + ) -> Self { + Self { + cipher_suites, + key_exchange_groups, + signature_algorithms, + extensions, + alpn_protocols, + cert_compression, + } + } +} + +/// TLS cipher suites for fingerprinting. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FingerprintCipherSuite { + // TLS 1.3 cipher suites + TLS13_AES_128_GCM_SHA256, + TLS13_AES_256_GCM_SHA384, + TLS13_CHACHA20_POLY1305_SHA256, + // TLS 1.2 cipher suites + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + /// GREASE cipher suite + Grease, +} + +impl FingerprintCipherSuite { + /// Returns the CipherSuite code to advertise in the ClientHello. + /// This returns the actual cipher suite code, even for cipher suites + /// that are not implemented (like 3DES). + pub fn to_cipher_suite(&self) -> crate::crypto::CipherSuite { + use crate::crypto::CipherSuite; + match self { + Self::TLS13_AES_128_GCM_SHA256 => CipherSuite::TLS13_AES_128_GCM_SHA256, + Self::TLS13_AES_256_GCM_SHA384 => CipherSuite::TLS13_AES_256_GCM_SHA384, + Self::TLS13_CHACHA20_POLY1305_SHA256 => CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + Self::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => { + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + } + Self::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => { + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + } + Self::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => { + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + } + Self::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => { + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + } + Self::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => { + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + } + Self::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => { + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + } + Self::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA => { + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + } + Self::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA => { + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + } + Self::TLS_RSA_WITH_AES_128_GCM_SHA256 => CipherSuite::TLS_RSA_WITH_AES_128_GCM_SHA256, + Self::TLS_RSA_WITH_AES_256_GCM_SHA384 => CipherSuite::TLS_RSA_WITH_AES_256_GCM_SHA384, + Self::TLS_RSA_WITH_AES_128_CBC_SHA => CipherSuite::TLS_RSA_WITH_AES_128_CBC_SHA, + Self::TLS_RSA_WITH_AES_256_CBC_SHA => CipherSuite::TLS_RSA_WITH_AES_256_CBC_SHA, + Self::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA => { + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + } + Self::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA => { + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + } + Self::Grease => CipherSuite::TLS_RESERVED_GREASE, + } + } + + /// Converts the fingerprint cipher suite to rustls's SupportedCipherSuite. + pub fn to_supported_cipher_suite(&self) -> SupportedCipherSuite { + match self { + Self::TLS13_AES_128_GCM_SHA256 => { + SupportedCipherSuite::Tls13(aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256) + } + Self::TLS13_AES_256_GCM_SHA384 => { + SupportedCipherSuite::Tls13(aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384) + } + Self::TLS13_CHACHA20_POLY1305_SHA256 => { + SupportedCipherSuite::Tls13(aws_lc_rs::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256) + } + Self::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => SupportedCipherSuite::Tls12( + aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + ), + Self::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => SupportedCipherSuite::Tls12( + aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + ), + Self::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => SupportedCipherSuite::Tls12( + aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + ), + Self::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => SupportedCipherSuite::Tls12( + aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + ), + Self::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => SupportedCipherSuite::Tls12( + aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + ), + Self::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => SupportedCipherSuite::Tls12( + aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + ), + // These CBC/RSA cipher suites are fake TLS 1.3 cipher suites from the impit patch + Self::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA => SupportedCipherSuite::Tls13( + aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + ), + Self::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA => SupportedCipherSuite::Tls13( + aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + ), + Self::TLS_RSA_WITH_AES_128_GCM_SHA256 => SupportedCipherSuite::Tls13( + aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_128_GCM_SHA256, + ), + Self::TLS_RSA_WITH_AES_256_GCM_SHA384 => SupportedCipherSuite::Tls13( + aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_256_GCM_SHA384, + ), + Self::TLS_RSA_WITH_AES_128_CBC_SHA => { + SupportedCipherSuite::Tls13(aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_128_CBC_SHA) + } + Self::TLS_RSA_WITH_AES_256_CBC_SHA => { + SupportedCipherSuite::Tls13(aws_lc_rs::cipher_suite::TLS_RSA_WITH_AES_256_CBC_SHA) + } + Self::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA => SupportedCipherSuite::Tls13( + aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + ), + Self::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA => SupportedCipherSuite::Tls13( + aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + ), + Self::Grease => { + SupportedCipherSuite::Tls13(aws_lc_rs::cipher_suite::TLS13_RESERVED_GREASE) + } + } + } +} + +/// Key exchange groups for fingerprinting. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FingerprintKeyExchangeGroup { + X25519, + /// X25519 with MLKEM768 (post-quantum hybrid) + X25519MLKEM768, + Secp256r1, + Secp384r1, + Secp521r1, + Ffdhe2048, + Ffdhe3072, + Ffdhe4096, + Ffdhe6144, + Ffdhe8192, + /// GREASE key exchange group + Grease, +} + +impl FingerprintKeyExchangeGroup { + /// Converts the fingerprint key exchange group to rustls's NamedGroup. + pub fn to_named_group(&self) -> NamedGroup { + match self { + Self::X25519 => NamedGroup::X25519, + Self::X25519MLKEM768 => NamedGroup::X25519MLKEM768, + Self::Secp256r1 => NamedGroup::secp256r1, + Self::Secp384r1 => NamedGroup::secp384r1, + Self::Secp521r1 => NamedGroup::secp521r1, + Self::Ffdhe2048 => NamedGroup::FFDHE2048, + Self::Ffdhe3072 => NamedGroup::FFDHE3072, + Self::Ffdhe4096 => NamedGroup::FFDHE4096, + Self::Ffdhe6144 => NamedGroup::FFDHE6144, + Self::Ffdhe8192 => NamedGroup::FFDHE8192, + Self::Grease => NamedGroup::GREASE, + } + } +} + +/// Signature algorithms for fingerprinting. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum FingerprintSignatureAlgorithm { + // ECDSA algorithms + EcdsaSecp256r1Sha256, + EcdsaSecp384r1Sha384, + EcdsaSecp521r1Sha512, + // RSA PSS algorithms + RsaPssRsaSha256, + RsaPssRsaSha384, + RsaPssRsaSha512, + // RSA PKCS#1 algorithms + RsaPkcs1Sha256, + RsaPkcs1Sha384, + RsaPkcs1Sha512, + RsaPkcs1Sha1, + // EdDSA algorithms + Ed25519, + Ed448, + // Legacy + EcdsaSha1Legacy, +} + +impl FingerprintSignatureAlgorithm { + /// Converts the fingerprint signature algorithm to rustls's SignatureScheme. + pub fn to_signature_scheme(&self) -> SignatureScheme { + match self { + Self::EcdsaSecp256r1Sha256 => SignatureScheme::ECDSA_NISTP256_SHA256, + Self::EcdsaSecp384r1Sha384 => SignatureScheme::ECDSA_NISTP384_SHA384, + Self::EcdsaSecp521r1Sha512 => SignatureScheme::ECDSA_NISTP521_SHA512, + Self::RsaPssRsaSha256 => SignatureScheme::RSA_PSS_SHA256, + Self::RsaPssRsaSha384 => SignatureScheme::RSA_PSS_SHA384, + Self::RsaPssRsaSha512 => SignatureScheme::RSA_PSS_SHA512, + Self::RsaPkcs1Sha256 => SignatureScheme::RSA_PKCS1_SHA256, + Self::RsaPkcs1Sha384 => SignatureScheme::RSA_PKCS1_SHA384, + Self::RsaPkcs1Sha512 => SignatureScheme::RSA_PKCS1_SHA512, + Self::RsaPkcs1Sha1 => SignatureScheme::RSA_PKCS1_SHA1, + Self::Ed25519 => SignatureScheme::ED25519, + Self::Ed448 => SignatureScheme::ED448, + Self::EcdsaSha1Legacy => SignatureScheme::ECDSA_SHA1_Legacy, + } + } +} + +/// Certificate compression algorithms for fingerprinting. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FingerprintCertCompressionAlgorithm { + Zlib, + Brotli, + Zstd, +} + +/// TLS extensions configuration for fingerprinting. +#[derive(Clone, Debug, Default)] +pub struct TlsExtensionsConfig { + /// Whether to send GREASE extensions + pub grease: bool, + /// Whether to send signed_certificate_timestamp extension + pub signed_certificate_timestamp: bool, + /// Whether to send application_settings extension + pub application_settings: bool, + /// Whether to use new ALPS codepoint (17613) instead of old (17513) + /// Chrome 136+ uses the new codepoint + pub use_new_alps_codepoint: bool, + /// Whether to send delegated_credentials extension + pub delegated_credentials: bool, + /// Whether to send record_size_limit extension + pub record_size_limit: Option, + /// Whether to send renegotiation_info extension + pub renegotiation_info: bool, + /// Whether to send padding extension (RFC7685) + pub padding: bool, + /// Whether to send supported_versions extension. + /// Defaults to true. Set to false for TLS 1.2-only fingerprints (e.g. + /// OkHttp 3) where the real client never advertises TLS 1.3 support. + pub supported_versions: bool, + /// Explicit extension order for fingerprinting. + /// When non-empty, all listed extensions are emitted in this exact order + /// via contiguous_extensions, bypassing randomization. + pub extension_order: Vec, +} + +/// Default signature verification algorithms. +/// Based on common browser implementations. +pub static DEFAULT_SIGNATURE_VERIFICATION_ALGOS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { all: &[ webpki_algs_aws::ECDSA_P256_SHA256, @@ -105,106 +355,222 @@ pub static CHROME_SIGNATURE_VERIFICATION_ALGOS: WebPkiSupportedAlgorithms = ], }; -/// The signature schemes supported by Google Chrome. -pub static CHROME_SIGNATURE_SCHEMES: &[SignatureScheme; 8] = &[ - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::RSA_PKCS1_SHA512, -]; - -/// The signature verification algorithms supported by Firefox. -pub static FIREFOX_SIGNATURE_VERIFICATION_ALGOS: WebPkiSupportedAlgorithms = - WebPkiSupportedAlgorithms { - all: &[ +impl FingerprintSignatureAlgorithm { + /// Returns the webpki signature verification algorithms for this fingerprint algorithm. + /// Returns an empty slice for algorithms that are not supported for verification (e.g., Ed448). + fn to_webpki_algs(&self) -> &'static [&'static dyn pki_types::SignatureVerificationAlgorithm] { + // Static arrays for algorithms used in the 'all' list + static ECDSA_P256_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = &[ webpki_algs_aws::ECDSA_P256_SHA256, webpki_algs_aws::ECDSA_P256_SHA384, + ]; + static ECDSA_P384_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = &[ webpki_algs_aws::ECDSA_P384_SHA256, webpki_algs_aws::ECDSA_P384_SHA384, - webpki_algs_aws::ECDSA_P384_SHA384, + ]; + static ECDSA_P521_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = &[ webpki_algs_aws::ECDSA_P521_SHA256, webpki_algs_aws::ECDSA_P521_SHA384, webpki_algs_aws::ECDSA_P521_SHA512, - webpki_algs_aws::ED25519, - webpki_algs_aws::RSA_PSS_2048_8192_SHA256_LEGACY_KEY, - webpki_algs_aws::RSA_PSS_2048_8192_SHA384_LEGACY_KEY, - webpki_algs_aws::RSA_PSS_2048_8192_SHA512_LEGACY_KEY, - webpki_algs_aws::RSA_PKCS1_2048_8192_SHA256, + ]; + static RSA_PSS_256_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PSS_2048_8192_SHA256_LEGACY_KEY]; + static RSA_PSS_384_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PSS_2048_8192_SHA384_LEGACY_KEY]; + static RSA_PSS_512_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PSS_2048_8192_SHA512_LEGACY_KEY]; + static RSA_PKCS1_256_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA256]; + static RSA_PKCS1_384_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = &[ webpki_algs_aws::RSA_PKCS1_2048_8192_SHA384, - webpki_algs_aws::RSA_PKCS1_2048_8192_SHA512, webpki_algs_aws::RSA_PKCS1_3072_8192_SHA384, - ], - mapping: &[ - ( - SignatureScheme::ECDSA_NISTP256_SHA256, - &[ - webpki_algs_aws::ECDSA_P256_SHA256, - webpki_algs_aws::ECDSA_P384_SHA256, - webpki_algs_aws::ECDSA_P521_SHA256, - ], - ), - ( - SignatureScheme::ECDSA_NISTP384_SHA384, - &[ - webpki_algs_aws::ECDSA_P384_SHA384, - webpki_algs_aws::ECDSA_P256_SHA384, - webpki_algs_aws::ECDSA_P521_SHA384, - ], - ), - ( - SignatureScheme::ECDSA_NISTP521_SHA512, - &[webpki_algs_aws::ECDSA_P521_SHA512], - ), - ( - SignatureScheme::RSA_PSS_SHA256, - &[webpki_algs_aws::RSA_PSS_2048_8192_SHA256_LEGACY_KEY], - ), - ( - SignatureScheme::RSA_PSS_SHA384, - &[webpki_algs_aws::RSA_PSS_2048_8192_SHA384_LEGACY_KEY], - ), - ( - SignatureScheme::RSA_PSS_SHA512, - &[webpki_algs_aws::RSA_PSS_2048_8192_SHA512_LEGACY_KEY], - ), - ( - SignatureScheme::RSA_PKCS1_SHA256, - &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA256], - ), - ( - SignatureScheme::RSA_PKCS1_SHA384, - &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA384], - ), - ( - SignatureScheme::RSA_PKCS1_SHA512, - &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA512], - ), - ( - SignatureScheme::ECDSA_SHA1_Legacy, - &[webpki_algs_aws::ECDSA_P256_SHA256], // fake signature scheme from the patch - ), - ( - SignatureScheme::RSA_PKCS1_SHA1, - &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA256], // fake signature scheme from the patch - ), - ], - }; + ]; + static RSA_PKCS1_512_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA512]; + static ED25519_ALGS: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::ED25519]; + static EMPTY: &[&dyn pki_types::SignatureVerificationAlgorithm] = &[]; + + match self { + Self::EcdsaSecp256r1Sha256 => ECDSA_P256_ALGS, + Self::EcdsaSecp384r1Sha384 => ECDSA_P384_ALGS, + Self::EcdsaSecp521r1Sha512 => ECDSA_P521_ALGS, + Self::RsaPssRsaSha256 => RSA_PSS_256_ALGS, + Self::RsaPssRsaSha384 => RSA_PSS_384_ALGS, + Self::RsaPssRsaSha512 => RSA_PSS_512_ALGS, + Self::RsaPkcs1Sha256 => RSA_PKCS1_256_ALGS, + Self::RsaPkcs1Sha384 => RSA_PKCS1_384_ALGS, + Self::RsaPkcs1Sha512 => RSA_PKCS1_512_ALGS, + Self::Ed25519 => ED25519_ALGS, + // Ed448 is not supported by webpki, SHA1 legacy uses fallback in mapping + Self::Ed448 | Self::RsaPkcs1Sha1 | Self::EcdsaSha1Legacy => EMPTY, + } + } + + /// Returns the mapping entry for this algorithm (SignatureScheme -> webpki algs). + fn to_mapping_entry( + &self, + ) -> Option<( + SignatureScheme, + &'static [&'static dyn pki_types::SignatureVerificationAlgorithm], + )> { + // Static arrays for each algorithm type - includes multiple curves for ECDSA + static ECDSA_P256_MAPPING: &[&dyn pki_types::SignatureVerificationAlgorithm] = &[ + webpki_algs_aws::ECDSA_P256_SHA256, + webpki_algs_aws::ECDSA_P384_SHA256, + webpki_algs_aws::ECDSA_P521_SHA256, + ]; + static ECDSA_P384_MAPPING: &[&dyn pki_types::SignatureVerificationAlgorithm] = &[ + webpki_algs_aws::ECDSA_P384_SHA384, + webpki_algs_aws::ECDSA_P256_SHA384, + webpki_algs_aws::ECDSA_P521_SHA384, + ]; + static ECDSA_P521_MAPPING: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::ECDSA_P521_SHA512]; + static RSA_PSS_256: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PSS_2048_8192_SHA256_LEGACY_KEY]; + static RSA_PSS_384: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PSS_2048_8192_SHA384_LEGACY_KEY]; + static RSA_PSS_512: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PSS_2048_8192_SHA512_LEGACY_KEY]; + static RSA_PKCS1_256: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA256]; + static RSA_PKCS1_384: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA384]; + static RSA_PKCS1_512: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA512]; + // Legacy SHA1 algorithms fall back to SHA256 (fake signature scheme from the patch) + static RSA_PKCS1_SHA1_FALLBACK: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::RSA_PKCS1_2048_8192_SHA256]; + static ECDSA_SHA1_FALLBACK: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::ECDSA_P256_SHA256]; + static ED25519: &[&dyn pki_types::SignatureVerificationAlgorithm] = + &[webpki_algs_aws::ED25519]; + + match self { + Self::EcdsaSecp256r1Sha256 => { + Some((SignatureScheme::ECDSA_NISTP256_SHA256, ECDSA_P256_MAPPING)) + } + Self::EcdsaSecp384r1Sha384 => { + Some((SignatureScheme::ECDSA_NISTP384_SHA384, ECDSA_P384_MAPPING)) + } + Self::EcdsaSecp521r1Sha512 => { + Some((SignatureScheme::ECDSA_NISTP521_SHA512, ECDSA_P521_MAPPING)) + } + Self::RsaPssRsaSha256 => Some((SignatureScheme::RSA_PSS_SHA256, RSA_PSS_256)), + Self::RsaPssRsaSha384 => Some((SignatureScheme::RSA_PSS_SHA384, RSA_PSS_384)), + Self::RsaPssRsaSha512 => Some((SignatureScheme::RSA_PSS_SHA512, RSA_PSS_512)), + Self::RsaPkcs1Sha256 => Some((SignatureScheme::RSA_PKCS1_SHA256, RSA_PKCS1_256)), + Self::RsaPkcs1Sha384 => Some((SignatureScheme::RSA_PKCS1_SHA384, RSA_PKCS1_384)), + Self::RsaPkcs1Sha512 => Some((SignatureScheme::RSA_PKCS1_SHA512, RSA_PKCS1_512)), + Self::RsaPkcs1Sha1 => Some((SignatureScheme::RSA_PKCS1_SHA1, RSA_PKCS1_SHA1_FALLBACK)), + Self::EcdsaSha1Legacy => { + Some((SignatureScheme::ECDSA_SHA1_Legacy, ECDSA_SHA1_FALLBACK)) + } + Self::Ed25519 => Some((SignatureScheme::ED25519, ED25519)), + // Ed448 is not supported + Self::Ed448 => None, + } + } +} + +/// Global cache for `WebPkiSupportedAlgorithms` to avoid memory leaks from repeated `Box::leak` calls. +/// Each unique signature algorithm configuration is only leaked once. +mod sig_alg_cache { + use super::{FingerprintSignatureAlgorithm, WebPkiSupportedAlgorithms}; + use alloc::boxed::Box; + use alloc::collections::BTreeSet; + use alloc::vec::Vec; + use std::collections::HashMap; + use std::sync::{Mutex, OnceLock}; + + static CACHE: OnceLock< + Mutex, WebPkiSupportedAlgorithms>>, + > = OnceLock::new(); + + fn get_cache() + -> &'static Mutex, WebPkiSupportedAlgorithms>> { + CACHE.get_or_init(|| Mutex::new(HashMap::new())) + } + + pub(super) fn get_or_create( + signature_algorithms: &[FingerprintSignatureAlgorithm], + ) -> WebPkiSupportedAlgorithms { + let cache = get_cache(); + + // Check if we already have this configuration cached + { + let guard = cache.lock().unwrap(); + if let Some(cached) = guard.get(signature_algorithms) { + return *cached; + } + } + + // Build the algorithms (will leak, but only once per unique configuration) + let algorithms = build_algorithms(signature_algorithms); + + // Store in cache + { + let mut guard = cache.lock().unwrap(); + // Double-check in case another thread added it while we were building + if let Some(cached) = guard.get(signature_algorithms) { + return *cached; + } + guard.insert(signature_algorithms.to_vec(), algorithms); + } + + algorithms + } + + fn build_algorithms( + signature_algorithms: &[FingerprintSignatureAlgorithm], + ) -> WebPkiSupportedAlgorithms { + // Collect all unique webpki algorithms (using pointer address for dedup) + let mut seen: BTreeSet = BTreeSet::new(); + let all_algs: Vec<&'static dyn pki_types::SignatureVerificationAlgorithm> = + signature_algorithms + .iter() + .flat_map(|sa| sa.to_webpki_algs().iter().copied()) + .filter(|alg| { + let ptr: *const dyn pki_types::SignatureVerificationAlgorithm = *alg; + seen.insert(ptr as *const () as usize) + }) + .collect(); + + // Collect mapping entries in fingerprint order + let mapping_entries: Vec<( + crate::crypto::SignatureScheme, + &'static [&'static dyn pki_types::SignatureVerificationAlgorithm], + )> = signature_algorithms + .iter() + .filter_map(|sa| sa.to_mapping_entry()) + .collect(); + + // Leak the vectors to get 'static references + // This only happens once per unique configuration due to caching + let all_static: &'static [&'static dyn pki_types::SignatureVerificationAlgorithm] = + Box::leak(all_algs.into_boxed_slice()); + let mapping_static: &'static [( + crate::crypto::SignatureScheme, + &'static [&'static dyn pki_types::SignatureVerificationAlgorithm], + )] = Box::leak(mapping_entries.into_boxed_slice()); + + WebPkiSupportedAlgorithms { + all: all_static, + mapping: mapping_static, + } + } +} -/// The signature schemes supported by Firefox. -pub static FIREFOX_SIGNATURE_SCHEMES: &[SignatureScheme; 11] = &[ - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::ECDSA_NISTP521_SHA512, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - SignatureScheme::ECDSA_SHA1_Legacy, - SignatureScheme::RSA_PKCS1_SHA1, -]; +impl TlsFingerprint { + /// Builds a `WebPkiSupportedAlgorithms` from this fingerprint's signature algorithms. + /// + /// The order of algorithms in the mapping reflects the fingerprint's preference order, + /// which is important for TLS fingerprinting. + /// + /// Results are cached globally to avoid memory leaks from repeated allocations. + /// Each unique signature algorithm configuration is only allocated once. + pub fn to_signature_verification_algorithms(&self) -> WebPkiSupportedAlgorithms { + sig_alg_cache::get_or_create(&self.signature_algorithms) + } +} diff --git a/rustls/src/crypto/mod.rs b/rustls/src/crypto/mod.rs index 526390402db..43da3df6b5d 100644 --- a/rustls/src/crypto/mod.rs +++ b/rustls/src/crypto/mod.rs @@ -7,8 +7,6 @@ use core::time::Duration; use pki_types::PrivateKeyDer; -#[cfg(feature = "impit")] -use crate::client::client_emulator::BrowserEmulator; use crate::enums::ProtocolVersion; use crate::error::{ApiMisuse, Error}; use crate::msgs::handshake::ALL_KEY_EXCHANGE_ALGORITHMS; @@ -235,60 +233,47 @@ pub struct CryptoProvider { /// Convenience builder for `CryptoProvider`. #[cfg(feature = "impit")] pub struct CryptoProviderBuilder { - browser_emulator: Option, + tls_fingerprint: Option, } #[cfg(feature = "impit")] impl CryptoProviderBuilder { - /// Sets the browser emulator to use for this provider. - pub fn with_browser_emulator(mut self, browser_emulator: &BrowserEmulator) -> Self { - self.browser_emulator = Some(browser_emulator.clone()); + /// Sets the TLS fingerprint to use for this provider. + pub fn with_tls_fingerprint(mut self, fingerprint: &emulation::TlsFingerprint) -> Self { + self.tls_fingerprint = Some(fingerprint.clone()); self } /// Builds the `CryptoProvider`. pub fn build(self) -> CryptoProvider { - use crate::client::client_emulator::{BrowserEmulator, BrowserType}; use crate::crypto::aws_lc_rs::DEFAULT_PROVIDER; - match self.browser_emulator { - Some(BrowserEmulator { - browser_type: BrowserType::Chrome, - version: _, - }) => { - use crate::crypto::aws_lc_rs::DEFAULT_PROVIDER; - use crate::crypto::emulation::{ - CHROME_SIGNATURE_VERIFICATION_ALGOS, CHROME_TLS12_CIPHER_SUITES, - CHROME_TLS13_CIPHER_SUITES, - }; - - let provider = CryptoProvider { - tls13_cipher_suites: Cow::Borrowed(&CHROME_TLS13_CIPHER_SUITES), - tls12_cipher_suites: Cow::Borrowed(&CHROME_TLS12_CIPHER_SUITES), - signature_verification_algorithms: CHROME_SIGNATURE_VERIFICATION_ALGOS, - ..DEFAULT_PROVIDER - }; - - provider - } - Some(BrowserEmulator { - browser_type: BrowserType::Firefox, - version: _, - }) => { - use crate::crypto::aws_lc_rs::DEFAULT_PROVIDER; - use crate::crypto::emulation::{ - FIREFOX_SIGNATURE_VERIFICATION_ALGOS, FIREFOX_TLS12_CIPHER_SUITES, - FIREFOX_TLS13_CIPHER_SUITES, - }; - - let provider = CryptoProvider { - tls13_cipher_suites: Cow::Borrowed(&FIREFOX_TLS13_CIPHER_SUITES), - tls12_cipher_suites: Cow::Borrowed(&FIREFOX_TLS12_CIPHER_SUITES), - signature_verification_algorithms: FIREFOX_SIGNATURE_VERIFICATION_ALGOS, + match self.tls_fingerprint { + Some(ref fingerprint) => { + let tls13: Vec<_> = fingerprint + .cipher_suites + .iter() + .filter_map(|cs| match cs.to_supported_cipher_suite() { + SupportedCipherSuite::Tls13(s) => Some(s), + _ => None, + }) + .collect(); + let tls12: Vec<_> = fingerprint + .cipher_suites + .iter() + .filter_map(|cs| match cs.to_supported_cipher_suite() { + SupportedCipherSuite::Tls12(s) => Some(s), + _ => None, + }) + .collect(); + let sig_algs = fingerprint.to_signature_verification_algorithms(); + + CryptoProvider { + tls13_cipher_suites: Cow::Owned(tls13), + tls12_cipher_suites: Cow::Owned(tls12), + signature_verification_algorithms: sig_algs, ..DEFAULT_PROVIDER - }; - - provider + } } None => DEFAULT_PROVIDER, } @@ -300,7 +285,7 @@ impl CryptoProvider { #[cfg(feature = "impit")] pub fn builder() -> CryptoProviderBuilder { CryptoProviderBuilder { - browser_emulator: None, + tls_fingerprint: None, } } diff --git a/rustls/src/lib.rs b/rustls/src/lib.rs index 1d84f7fec2c..39194b6597c 100644 --- a/rustls/src/lib.rs +++ b/rustls/src/lib.rs @@ -473,10 +473,17 @@ pub mod unbuffered { // The public interface is: pub use crate::builder::{ConfigBuilder, ConfigSide, WantsVerifier}; +#[cfg(feature = "impit")] +pub use crate::client::WantsClientCertWithTlsFingerprint; pub use crate::common_state::{CommonState, HandshakeKind, IoState, Side}; #[cfg(feature = "std")] pub use crate::conn::{Connection, Reader, Writer}; pub use crate::conn::{ConnectionCommon, KeyingMaterialExporter, SideData, kernel}; +#[cfg(feature = "impit")] +pub use crate::crypto::emulation::{ + FingerprintCertCompressionAlgorithm, FingerprintCipherSuite, FingerprintKeyExchangeGroup, + FingerprintSignatureAlgorithm, TlsExtensionsConfig, TlsFingerprint, +}; pub use crate::error::Error; pub use crate::key_log::{KeyLog, NoKeyLog}; #[cfg(feature = "std")] diff --git a/rustls/src/msgs/enums.rs b/rustls/src/msgs/enums.rs index bdb57454811..0aa563bcf02 100644 --- a/rustls/src/msgs/enums.rs +++ b/rustls/src/msgs/enums.rs @@ -105,6 +105,7 @@ enum_builder! { DelegatedCredentials => 0x0022, RecordSizeLimit => 0x001c, ApplicationSettings => 0x4469, + ApplicationSettingsNew => 0x44cd, } } diff --git a/rustls/src/msgs/handshake.rs b/rustls/src/msgs/handshake.rs index 6bbecf1f3da..d649efa7be3 100644 --- a/rustls/src/msgs/handshake.rs +++ b/rustls/src/msgs/handshake.rs @@ -943,6 +943,15 @@ extension_struct! { ExtensionType::ApplicationSettings => pub(crate) application_settings: Option, + /// ALPS extension (new codepoint 17613, used by Chrome 136+) + ExtensionType::ApplicationSettingsNew => + pub(crate) application_settings_new: Option, + + /// Padding extension (RFC7685) - used to pad ClientHello to avoid triggering + /// bugs in some middleboxes. Contains zero bytes. + ExtensionType::Padding => + pub(crate) padding: Option>, + /// Encrypted client hello outer extensions (draft-ietf-tls-esni) ExtensionType::EncryptedClientHelloOuterExtensions => pub(crate) encrypted_client_hello_outer: Option>, @@ -982,6 +991,8 @@ impl ClientExtensions<'_> { encrypted_client_hello_outer, order_seed, application_settings, + application_settings_new, + padding, reserved_grease, signed_certificate_timestamp, delegated_credentials, @@ -1014,6 +1025,8 @@ impl ClientExtensions<'_> { order_seed, contiguous_extensions, application_settings, + application_settings_new, + padding: padding.map(|x| x.into_owned()), reserved_grease, signed_certificate_timestamp, delegated_credentials, diff --git a/rustls/src/verify.rs b/rustls/src/verify.rs index a2cfcead85d..2c565ca2f9a 100644 --- a/rustls/src/verify.rs +++ b/rustls/src/verify.rs @@ -1,12 +1,9 @@ +#[cfg(feature = "impit")] +use alloc::vec; use alloc::vec::Vec; use core::fmt::Debug; -#[cfg(feature = "impit")] -use std::vec; - use pki_types::{CertificateDer, ServerName, SubjectPublicKeyInfoDer, UnixTime}; -#[cfg(feature = "impit")] -use crate::client::client_emulator::BrowserEmulator; use crate::crypto::{Identity, SignatureScheme}; use crate::enums::CertificateType; use crate::error::{Error, InvalidMessage}; @@ -31,13 +28,36 @@ use crate::x509::wrap_in_sequence; /// Used for the `ignore_tls_errors` option in `impit`. #[cfg(feature = "impit")] #[derive(Debug)] -pub struct NoVerifier(Option); +pub struct NoVerifier { + signature_schemes: Vec, +} #[cfg(feature = "impit")] impl NoVerifier { - /// Create a new `NoVerifier` instance. - pub fn new(browser_emulator: Option) -> Self { - Self(browser_emulator) + /// Create a new `NoVerifier` instance with the given signature schemes. + pub fn new(signature_schemes: Vec) -> Self { + Self { signature_schemes } + } + + /// Create a new `NoVerifier` instance with default signature schemes. + pub fn with_default_schemes() -> Self { + Self { + signature_schemes: vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ], + } } } @@ -62,30 +82,7 @@ impl ServerVerifier for NoVerifier { } fn supported_verify_schemes(&self) -> Vec { - use crate::client::client_emulator::BrowserType; - use crate::crypto::emulation::{CHROME_SIGNATURE_SCHEMES, FIREFOX_SIGNATURE_SCHEMES}; - - match &self.0 { - Some(browser_emulator) => match browser_emulator.browser_type { - BrowserType::Chrome => CHROME_SIGNATURE_SCHEMES.to_vec(), - BrowserType::Firefox => FIREFOX_SIGNATURE_SCHEMES.to_vec(), - }, - None => vec![ - SignatureScheme::RSA_PKCS1_SHA1, - SignatureScheme::ECDSA_SHA1_Legacy, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - SignatureScheme::ECDSA_NISTP521_SHA512, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::ED25519, - SignatureScheme::ED448, - ], - } + self.signature_schemes.clone() } fn request_ocsp_response(&self) -> bool {