/src/suricata8/rust/src/ja4.rs
Line | Count | Source |
1 | | /* Copyright (C) 2023-2024 Open Information Security Foundation |
2 | | * |
3 | | * You can copy, redistribute or modify this Program under the terms of |
4 | | * the GNU General Public License version 2 as published by the Free |
5 | | * Software Foundation. |
6 | | * |
7 | | * This program is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | | * GNU General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU General Public License |
13 | | * version 2 along with this program; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
15 | | * 02110-1301, USA. |
16 | | |
17 | | // Author: Sascha Steinbiss <sascha@steinbiss.name> |
18 | | |
19 | | */ |
20 | | #[cfg(feature = "ja4")] |
21 | | use crate::jsonbuilder::HEX; |
22 | | #[cfg(feature = "ja4")] |
23 | | use digest::Digest; |
24 | | #[cfg(feature = "ja4")] |
25 | | use sha2::Sha256; |
26 | | #[cfg(feature = "ja4")] |
27 | | use std::cmp::min; |
28 | | #[cfg(feature = "ja4")] |
29 | | use tls_parser::{TlsExtensionType, TlsVersion}; |
30 | | |
31 | | use crate::handshake::HandshakeParams; |
32 | | |
33 | | pub const JA4_HEX_LEN: usize = 36; |
34 | | |
35 | | pub(crate) trait JA4Impl { |
36 | | fn try_new(hs: &HandshakeParams) -> Option<JA4>; |
37 | | } |
38 | | |
39 | | #[derive(Debug, PartialEq)] |
40 | | pub struct JA4 { |
41 | | hash: String, |
42 | | } |
43 | | |
44 | | impl AsRef<str> for JA4 { |
45 | 252 | fn as_ref(&self) -> &str { |
46 | 252 | &self.hash |
47 | 252 | } |
48 | | } |
49 | | |
50 | | #[cfg(feature = "ja4")] |
51 | | impl JA4 { |
52 | | #[inline] |
53 | 6.14k | fn version_to_ja4code(val: Option<TlsVersion>) -> &'static str { |
54 | 6.14k | match val { |
55 | 0 | Some(TlsVersion::Tls13) => "13", |
56 | 0 | Some(TlsVersion::Tls12) => "12", |
57 | 0 | Some(TlsVersion::Tls11) => "11", |
58 | 2 | Some(TlsVersion::Tls10) => "10", |
59 | 0 | Some(TlsVersion::Ssl30) => "s3", |
60 | | // the TLS parser does not support SSL 1.0 and 2.0 hence no |
61 | | // support for "s1"/"s2" |
62 | 6.13k | _ => "00", |
63 | | } |
64 | 6.14k | } |
65 | | |
66 | 6.14k | fn format_alpn(alpn: Option<&Vec<u8>>) -> [char; 2] { |
67 | 6.14k | let mut ret = ['0', '0']; |
68 | | |
69 | 6.14k | if let Some(alpn) = alpn { |
70 | 60 | if !alpn.is_empty() { |
71 | | // If the first ALPN value is only a single character, then that character is treated as both the first and last character. |
72 | 60 | if alpn.len() == 2 { |
73 | | // GREASE values are 2 bytes, so this could be one -- check |
74 | 17 | let v: u16 = ((alpn[0] as u16) << 8) | alpn[alpn.len() - 1] as u16; |
75 | 17 | if HandshakeParams::is_grease(v) { |
76 | 1 | return ret; |
77 | 16 | } |
78 | 43 | } |
79 | 59 | if !alpn[0].is_ascii_alphanumeric() || !alpn[alpn.len() - 1].is_ascii_alphanumeric() |
80 | 38 | { |
81 | 38 | // If the first or last byte of the first ALPN is non-alphanumeric (meaning not 0x30-0x39, 0x41-0x5A, or 0x61-0x7A), then we print the first and last characters of the hex representation of the first ALPN instead. |
82 | 38 | ret[0] = char::from(HEX[(alpn[0] >> 4) as usize]); |
83 | 38 | ret[1] = char::from(HEX[(alpn[alpn.len() - 1] & 0xF) as usize]); |
84 | 38 | } else { |
85 | 21 | ret[0] = char::from(alpn[0]); |
86 | 21 | ret[1] = char::from(alpn[alpn.len() - 1]); |
87 | 21 | } |
88 | 0 | } |
89 | 6.08k | } |
90 | 6.13k | ret |
91 | 6.14k | } |
92 | | } |
93 | | |
94 | | #[cfg(feature = "ja4")] |
95 | | impl JA4Impl for JA4 { |
96 | 6.14k | fn try_new(hs: &HandshakeParams) -> Option<Self> { |
97 | | // All non-GREASE extensions are stored to produce a more verbose, complete output |
98 | | // of extensions but we need to omit ALPN & SNI extensions from the JA4_a hash. |
99 | 6.14k | let mut exts = hs |
100 | 6.14k | .extensions |
101 | 6.14k | .iter() |
102 | 710k | .filter(|&ext| { |
103 | 710k | *ext != TlsExtensionType::ApplicationLayerProtocolNegotiation |
104 | 709k | && *ext != TlsExtensionType::ServerName |
105 | 710k | }) |
106 | 6.14k | .collect::<Vec<&TlsExtensionType>>(); |
107 | | |
108 | 6.14k | let alpn = Self::format_alpn(hs.alpns.first()); |
109 | | |
110 | | // Calculate JA4_a |
111 | 6.14k | let ja4_a = format!( |
112 | 6.14k | "{proto}{version}{sni}{nof_c:02}{nof_e:02}{al1}{al2}", |
113 | 6.14k | proto = if hs.quic { "q" } else { "t" }, |
114 | 6.14k | version = Self::version_to_ja4code(hs.tls_version), |
115 | 6.14k | sni = if hs.domain { "d" } else { "i" }, |
116 | 6.14k | nof_c = min(99, hs.ciphersuites.len()), |
117 | 6.14k | nof_e = min(99, hs.extensions.len()), |
118 | 6.14k | al1 = alpn[0], |
119 | 6.14k | al2 = alpn[1] |
120 | | ); |
121 | | |
122 | | // Calculate JA4_b |
123 | 6.14k | let mut sorted_ciphers = hs.ciphersuites.to_vec(); |
124 | 9.56M | sorted_ciphers.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b))); |
125 | 6.14k | let sorted_cipherstrings: Vec<String> = sorted_ciphers |
126 | 6.14k | .iter() |
127 | 2.20M | .map(|v| format!("{:04x}", u16::from(*v))) |
128 | 6.14k | .collect(); |
129 | 6.14k | let mut sha = Sha256::new(); |
130 | 6.14k | let ja4_b_raw = sorted_cipherstrings.join(","); |
131 | 6.14k | sha.update(&ja4_b_raw); |
132 | 6.14k | let mut ja4_b = format!("{:x}", sha.finalize_reset()); |
133 | 6.14k | ja4_b.truncate(12); |
134 | | |
135 | | // Calculate JA4_c |
136 | 405k | exts.sort_by(|&a, &b| u16::from(*a).cmp(&u16::from(*b))); |
137 | 6.14k | let sorted_extstrings: Vec<String> = exts |
138 | 6.14k | .into_iter() |
139 | 107k | .map(|&v| format!("{:04x}", u16::from(v))) |
140 | 6.14k | .collect(); |
141 | 6.14k | let ja4_c1_raw = sorted_extstrings.join(","); |
142 | 6.14k | let unsorted_sigalgostrings: Vec<String> = hs |
143 | 6.14k | .signature_algorithms |
144 | 6.14k | .iter() |
145 | 123k | .map(|v| format!("{:04x}", (*v))) |
146 | 6.14k | .collect(); |
147 | 6.14k | let ja4_c2_raw = unsorted_sigalgostrings.join(","); |
148 | 6.14k | let ja4_c_raw = format!("{}_{}", ja4_c1_raw, ja4_c2_raw); |
149 | 6.14k | sha.update(&ja4_c_raw); |
150 | 6.14k | let mut ja4_c = format!("{:x}", sha.finalize()); |
151 | 6.14k | ja4_c.truncate(12); |
152 | | |
153 | 6.14k | Some(Self { |
154 | 6.14k | hash: format!("{}_{}_{}", ja4_a, ja4_b, ja4_c), |
155 | 6.14k | }) |
156 | 6.14k | } |
157 | | } |
158 | | |
159 | | #[cfg(not(feature = "ja4"))] |
160 | | impl JA4Impl for JA4 { |
161 | | fn try_new(_hs: &HandshakeParams) -> Option<Self> { |
162 | | None |
163 | | } |
164 | | } |
165 | | |
166 | | // C ABI |
167 | | #[cfg(feature = "ja4")] |
168 | | #[no_mangle] |
169 | 252 | pub unsafe extern "C" fn SCJA4GetHash(hs: &HandshakeParams, out: &mut [u8; JA4_HEX_LEN]) { |
170 | 252 | if let Some(ja4) = JA4::try_new(hs) { |
171 | 252 | out[0..JA4_HEX_LEN].copy_from_slice(ja4.as_ref().as_bytes()); |
172 | 252 | } |
173 | 252 | } |
174 | | |
175 | | #[cfg(test)] |
176 | | #[cfg(feature = "ja4")] |
177 | | mod tests { |
178 | | use super::*; |
179 | | use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion}; |
180 | | |
181 | | #[test] |
182 | | fn test_format_alpn_ascii() { |
183 | | let res = JA4::format_alpn(Some(&"http/1.1".as_bytes().to_vec())); |
184 | | assert_eq!(res, ['h', '1']); |
185 | | } |
186 | | |
187 | | #[test] |
188 | | fn test_add_alpn_non_ascii_first_or_last() { |
189 | | let res = JA4::format_alpn(Some(&vec![0x01, b'T', b'E', 0x7f])); // non-alphanumeric start and end |
190 | | assert_eq!(res, [HEX[0x0], HEX[0xF]].map(|b| b as char)); // 0x01 -> 0, 0x7f -> f |
191 | | } |
192 | | |
193 | | #[test] |
194 | | fn test_add_alpn_grease_pair_filtered() { |
195 | | let res = JA4::format_alpn(Some(&vec![0x2a, 0x2a])); // 0x2a2a GREASE |
196 | | assert_eq!(res, ['0', '0']); |
197 | | } |
198 | | |
199 | | #[test] |
200 | | fn test_hash_limit_numbers() { |
201 | | // Test whether the limitation of the extension and ciphersuite |
202 | | // count to 99 is reflected correctly. |
203 | | let mut hs = HandshakeParams::default(); |
204 | | |
205 | | for i in 1..200 { |
206 | | hs.add_cipher_suite(TlsCipherSuiteID(i)); |
207 | | } |
208 | | for i in 1..200 { |
209 | | hs.add_extension(TlsExtensionType(i)); |
210 | | } |
211 | | |
212 | | let ja4 = JA4::try_new(&hs).expect("JA4 create failure"); |
213 | | |
214 | | // Only testing the ja4_a portion of the hash, we we truncate to |
215 | | // ensure we're only testing this |
216 | | let mut ja4_hash = ja4.as_ref().to_string(); |
217 | | ja4_hash.truncate(10); |
218 | | |
219 | | assert_eq!(ja4_hash, "t00i999900"); |
220 | | } |
221 | | |
222 | | #[test] |
223 | | fn test_short_alpn() { |
224 | | let mut hs = HandshakeParams::default(); |
225 | | hs.add_alpn("b".as_bytes()); |
226 | | let mut s = JA4::try_new(&hs) |
227 | | .expect("JA4 create failure") |
228 | | .as_ref() |
229 | | .to_string(); |
230 | | s.truncate(10); |
231 | | assert_eq!(s, "t00i0000bb"); |
232 | | |
233 | | let mut hs = HandshakeParams::default(); |
234 | | hs.add_alpn("h2".as_bytes()); |
235 | | let mut s = JA4::try_new(&hs) |
236 | | .expect("JA4 create failure") |
237 | | .as_ref() |
238 | | .to_string(); |
239 | | s.truncate(10); |
240 | | assert_eq!(s, "t00i0000h2"); |
241 | | |
242 | | // from https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md#alpn-extension-value |
243 | | let mut hs = HandshakeParams::default(); |
244 | | hs.add_alpn(&[0xab]); |
245 | | let mut s = JA4::try_new(&hs) |
246 | | .expect("JA4 create failure") |
247 | | .as_ref() |
248 | | .to_string(); |
249 | | s.truncate(10); |
250 | | assert_eq!(s, "t00i0000ab"); |
251 | | |
252 | | let mut hs = HandshakeParams::default(); |
253 | | hs.add_alpn(&[0xab, 0xcd]); |
254 | | let mut s = JA4::try_new(&hs) |
255 | | .expect("JA4 create failure") |
256 | | .as_ref() |
257 | | .to_string(); |
258 | | s.truncate(10); |
259 | | assert_eq!(s, "t00i0000ad"); |
260 | | |
261 | | let mut hs = HandshakeParams::default(); |
262 | | hs.add_alpn(&[0x30, 0xab]); |
263 | | let mut s = JA4::try_new(&hs) |
264 | | .expect("JA4 create failure") |
265 | | .as_ref() |
266 | | .to_string(); |
267 | | s.truncate(10); |
268 | | assert_eq!(s, "t00i00003b"); |
269 | | |
270 | | let mut hs = HandshakeParams::default(); |
271 | | hs.add_alpn(&[0x30, 0x31, 0xab, 0xcd]); |
272 | | let mut s = JA4::try_new(&hs) |
273 | | .expect("JA4 create failure") |
274 | | .as_ref() |
275 | | .to_string(); |
276 | | s.truncate(10); |
277 | | assert_eq!(s, "t00i00003d"); |
278 | | |
279 | | let mut hs = HandshakeParams::default(); |
280 | | hs.add_alpn(&[0x30, 0xab, 0xcd, 0x31]); |
281 | | let mut s = JA4::try_new(&hs) |
282 | | .expect("JA4 create failure") |
283 | | .as_ref() |
284 | | .to_string(); |
285 | | s.truncate(10); |
286 | | assert_eq!(s, "t00i000001"); |
287 | | } |
288 | | |
289 | | #[test] |
290 | | fn test_get_hash() { |
291 | | let mut hs = HandshakeParams::default(); |
292 | | |
293 | | // the empty JA4 hash |
294 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
295 | | assert_eq!(s.as_ref(), "t00i000000_e3b0c44298fc_d2e2adf7177b"); |
296 | | |
297 | | // set TLS version |
298 | | hs.set_tls_version(TlsVersion::Tls12); |
299 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
300 | | assert_eq!(s.as_ref(), "t12i000000_e3b0c44298fc_d2e2adf7177b"); |
301 | | |
302 | | // set QUIC |
303 | | hs.quic = true; |
304 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
305 | | assert_eq!(s.as_ref(), "q12i000000_e3b0c44298fc_d2e2adf7177b"); |
306 | | |
307 | | // set GREASE extension, should be ignored |
308 | | hs.add_extension(TlsExtensionType(0x0a0a)); |
309 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
310 | | assert_eq!(s.as_ref(), "q12i000000_e3b0c44298fc_d2e2adf7177b"); |
311 | | |
312 | | // set SNI extension, should only increase count and change i->d |
313 | | hs.add_extension(TlsExtensionType(0x0000)); |
314 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
315 | | assert_eq!(s.as_ref(), "q12d000100_e3b0c44298fc_d2e2adf7177b"); |
316 | | |
317 | | // set ALPN extension, should only increase count and set end of JA4_a |
318 | | hs.add_alpn(b"h3-16"); |
319 | | hs.add_extension(TlsExtensionType::ApplicationLayerProtocolNegotiation); |
320 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
321 | | assert_eq!(s.as_ref(), "q12d0002h6_e3b0c44298fc_d2e2adf7177b"); |
322 | | |
323 | | // set some ciphers |
324 | | hs.add_cipher_suite(TlsCipherSuiteID(0x1111)); |
325 | | hs.add_cipher_suite(TlsCipherSuiteID(0x0a20)); |
326 | | hs.add_cipher_suite(TlsCipherSuiteID(0xbada)); |
327 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
328 | | assert_eq!(s.as_ref(), "q12d0302h6_f500716053f9_d2e2adf7177b"); |
329 | | |
330 | | // set some extensions and signature algorithms |
331 | | hs.add_extension(TlsExtensionType(0xface)); |
332 | | hs.add_extension(TlsExtensionType(0x0121)); |
333 | | hs.add_extension(TlsExtensionType(0x1234)); |
334 | | hs.add_signature_algorithm(0x6666); |
335 | | let s = JA4::try_new(&hs).expect("JA4 create failure"); |
336 | | assert_eq!(s.as_ref(), "q12d0305h6_f500716053f9_2debc8880bae"); |
337 | | } |
338 | | } |