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