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;