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;