pub mod eras;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumericEntry {
Greased,
Value(u16),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtensionEntry {
Greased,
Named { id: u16, name: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Browser {
Chrome,
Chromium,
Firefox,
Edge,
Safari,
Brave,
Opera,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum BrowserOs {
Linux,
Windows,
MacOs,
Android,
Other,
}
#[derive(Debug, Clone, Copy)]
pub struct TlsFingerprint {
pub name: &'static str,
pub browser: Browser,
pub browser_name: &'static str,
pub major: u16,
pub version: &'static str,
pub os: BrowserOs,
pub os_name: &'static str,
pub record_version: u16,
pub handshake_version: u16,
pub session_id_length: u32,
pub ciphersuites: &'static [NumericEntry],
pub comp_methods: &'static [u8],
pub extensions: &'static [ExtensionEntry],
pub alpn: &'static [&'static str],
pub alps_alpn: &'static [&'static str],
pub sig_hash_algs: &'static [u16],
pub supported_groups: &'static [NumericEntry],
pub ec_point_formats: &'static [u8],
pub supported_versions: &'static [NumericEntry],
pub cert_compress_algs: &'static [u16],
pub psk_ke_modes: &'static [u8],
pub key_share_groups: &'static [NumericEntry],
pub has_status_request: bool,
pub has_extended_master_secret: bool,
pub has_renegotiation_info: bool,
pub has_session_ticket: bool,
pub has_signed_certificate_timestamp: bool,
pub has_padding: bool,
pub has_ech_grease: bool,
}
impl TlsFingerprint {
pub fn ciphers_no_grease(&self) -> Vec<u16> {
self.ciphersuites
.iter()
.filter_map(|e| match e {
NumericEntry::Value(v) => Some(*v),
NumericEntry::Greased => None,
})
.collect()
}
pub fn extension_ids_no_grease(&self) -> Vec<u16> {
self.extensions
.iter()
.filter_map(|e| match e {
ExtensionEntry::Named { id, .. } => Some(*id),
ExtensionEntry::Greased => None,
})
.collect()
}
pub fn supported_groups_no_grease(&self) -> Vec<u16> {
self.supported_groups
.iter()
.filter_map(|e| match e {
NumericEntry::Value(v) => Some(*v),
NumericEntry::Greased => None,
})
.collect()
}
pub fn ja3_string(&self) -> String {
let join = |xs: &[u16]| {
xs.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("-")
};
let ciphers = self.ciphers_no_grease();
let exts = self.extension_ids_no_grease();
let groups = self.supported_groups_no_grease();
let fmts: Vec<u16> = self.ec_point_formats.iter().map(|n| *n as u16).collect();
format!(
"{},{},{},{},{}",
self.handshake_version,
join(&ciphers),
join(&exts),
join(&groups),
join(&fmts)
)
}
}
include!(concat!(env!("OUT_DIR"), "/tls_catalog_generated.rs"));
pub fn lookup(name: &str) -> Option<&'static TlsFingerprint> {
CATALOG.iter().find(|(n, _)| *n == name).map(|(_, fp)| *fp)
}
pub fn all() -> impl Iterator<Item = &'static TlsFingerprint> {
CATALOG.iter().map(|(_, fp)| *fp)
}
pub fn cipher_id_to_openssl_name(id: u16) -> Option<&'static str> {
Some(match id {
0x1301 => "TLS_AES_128_GCM_SHA256",
0x1302 => "TLS_AES_256_GCM_SHA384",
0x1303 => "TLS_CHACHA20_POLY1305_SHA256",
0x1304 => "TLS_AES_128_CCM_SHA256",
0x1305 => "TLS_AES_128_CCM_8_SHA256",
0xc02b => "ECDHE-ECDSA-AES128-GCM-SHA256",
0xc02c => "ECDHE-ECDSA-AES256-GCM-SHA384",
0xcca9 => "ECDHE-ECDSA-CHACHA20-POLY1305",
0xc02f => "ECDHE-RSA-AES128-GCM-SHA256",
0xc030 => "ECDHE-RSA-AES256-GCM-SHA384",
0xcca8 => "ECDHE-RSA-CHACHA20-POLY1305",
0xc013 => "ECDHE-RSA-AES128-SHA",
0xc014 => "ECDHE-RSA-AES256-SHA",
0xc009 => "ECDHE-ECDSA-AES128-SHA",
0xc00a => "ECDHE-ECDSA-AES256-SHA",
0x009c => "AES128-GCM-SHA256",
0x009d => "AES256-GCM-SHA384",
0x002f => "AES128-SHA",
0x0035 => "AES256-SHA",
_ => return None,
})
}
pub fn group_id_to_openssl_name(id: u16) -> Option<&'static str> {
Some(match id {
0x0017 => "P-256",
0x0018 => "P-384",
0x0019 => "P-521",
0x001d => "X25519",
0x001e => "X448",
0x6399 => "X25519Kyber768Draft00",
0x11ec => "X25519MLKEM768",
_ => return None,
})
}
pub fn sigalg_id_to_openssl_name(id: u16) -> Option<&'static str> {
Some(match id {
0x0401 => "rsa_pkcs1_sha256",
0x0501 => "rsa_pkcs1_sha384",
0x0601 => "rsa_pkcs1_sha512",
0x0403 => "ecdsa_secp256r1_sha256",
0x0503 => "ecdsa_secp384r1_sha384",
0x0603 => "ecdsa_secp521r1_sha512",
0x0804 => "rsa_pss_rsae_sha256",
0x0805 => "rsa_pss_rsae_sha384",
0x0806 => "rsa_pss_rsae_sha512",
0x0807 => "ed25519",
0x0808 => "ed448",
0x0809 => "rsa_pss_pss_sha256",
0x080a => "rsa_pss_pss_sha384",
0x080b => "rsa_pss_pss_sha512",
_ => return None,
})
}
pub fn render_cipher_list(fp: &TlsFingerprint) -> String {
fp.ciphers_no_grease()
.into_iter()
.filter_map(|id| match cipher_id_to_openssl_name(id) {
Some(name) => Some(name),
None => {
tracing::warn!(
target: "crawlex::impersonate::catalog",
cipher_id = format!("0x{:04x}", id),
profile = fp.name,
"unknown cipher ID — dropping from BoringSSL list"
);
None
}
})
.collect::<Vec<_>>()
.join(":")
}
pub fn render_curves_list(fp: &TlsFingerprint) -> String {
fp.supported_groups_no_grease()
.into_iter()
.filter_map(|id| match group_id_to_openssl_name(id) {
Some(name) => Some(name),
None => {
tracing::warn!(
target: "crawlex::impersonate::catalog",
group_id = format!("0x{:04x}", id),
profile = fp.name,
"unknown supported_group ID — dropping from BoringSSL list"
);
None
}
})
.collect::<Vec<_>>()
.join(":")
}
pub fn render_sigalgs_list(fp: &TlsFingerprint) -> String {
fp.sig_hash_algs
.iter()
.copied()
.filter_map(|id| match sigalg_id_to_openssl_name(id) {
Some(name) => Some(name),
None => {
tracing::warn!(
target: "crawlex::impersonate::catalog",
sigalg_id = format!("0x{:04x}", id),
profile = fp.name,
"unknown sigalg ID — dropping from BoringSSL list"
);
None
}
})
.collect::<Vec<_>>()
.join(":")
}
pub fn encode_alpn_wire(alpn: &[&str]) -> Vec<u8> {
let mut out = Vec::with_capacity(alpn.iter().map(|s| s.len() + 1).sum());
for proto in alpn {
if proto.len() > u8::MAX as usize {
continue;
}
out.push(proto.len() as u8);
out.extend_from_slice(proto.as_bytes());
}
out
}