Skip to main content

leyline/audit/
ja4.rs

1use crate::audit::{hash12, non_grease_cipher_ids, non_grease_ext_ids};
2use crate::iana::sigalg_id;
3
4#[derive(Debug)]
5pub struct Ja4Input<'a> {
6    pub ciphers: &'a [String],
7    pub sigalgs: &'a [String],
8    pub curves: &'a [String],
9    pub extension_ids: &'a [u16],
10    pub tls_version: &'a str,
11    pub has_sni: bool,
12    pub alpn: &'a str,
13}
14
15pub fn compute_ja4(input: &Ja4Input<'_>) -> String {
16    let section_a = compute_section_a(input);
17    let section_b = compute_section_b(input);
18    let section_c = compute_section_c(input);
19    format!("{section_a}_{section_b}_{section_c}")
20}
21
22fn compute_section_a(input: &Ja4Input<'_>) -> String {
23    let proto = "t";
24    let version = match input.tls_version {
25        "1.3" => "13",
26        "1.2" => "12",
27        "1.1" => "11",
28        "1.0" => "10",
29        _ => "00",
30    };
31
32    let sni = if input.has_sni { "d" } else { "i" };
33
34    let cipher_count = non_grease_cipher_ids(input.ciphers).len().min(99);
35
36    let ext_count = non_grease_ext_ids(input.extension_ids).len().min(99);
37
38    let alpn = if input.alpn.is_empty() {
39        "00".to_string()
40    } else {
41        let bytes = input.alpn.as_bytes();
42        let first = bytes[0] as char;
43        let last = bytes[bytes.len() - 1] as char;
44        format!("{first}{last}")
45    };
46
47    format!("{proto}{version}{sni}{cipher_count:02}{ext_count:02}{alpn}")
48}
49
50fn compute_section_b(input: &Ja4Input<'_>) -> String {
51    let mut ids = non_grease_cipher_ids(input.ciphers);
52
53    ids.sort();
54
55    let s: String = ids
56        .iter()
57        .map(|id| format!("{id:04x}"))
58        .collect::<Vec<_>>()
59        .join(",");
60
61    hash12(&s)
62}
63
64fn compute_section_c(input: &Ja4Input<'_>) -> String {
65    let mut ext_ids: Vec<u16> = non_grease_ext_ids(input.extension_ids)
66        .into_iter()
67        .filter(|id| *id != 0x0000 && *id != 0x0010)
68        .collect();
69    ext_ids.sort();
70
71    let ext_str: String = ext_ids
72        .iter()
73        .map(|id| format!("{id:04x}"))
74        .collect::<Vec<_>>()
75        .join(",");
76
77    let sigalg_str: String = input
78        .sigalgs
79        .iter()
80        .filter_map(|s| sigalg_id(s))
81        .map(|id| format!("{id:04x}"))
82        .collect::<Vec<_>>()
83        .join(",");
84
85    let combined = format!("{ext_str}_{sigalg_str}");
86    hash12(&combined)
87}
88
89#[cfg(test)]
90mod tests;