/src/suricata7/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 | | |
21 | | #[cfg(feature = "ja4")] |
22 | | use digest::Digest; |
23 | | use libc::c_uchar; |
24 | | #[cfg(feature = "ja4")] |
25 | | use sha2::Sha256; |
26 | | #[cfg(feature = "ja4")] |
27 | | use std::cmp::min; |
28 | | use std::os::raw::c_char; |
29 | | use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion}; |
30 | | #[cfg(feature = "ja4")] |
31 | | use crate::jsonbuilder::HEX; |
32 | | |
33 | | #[derive(Debug, PartialEq)] |
34 | | pub struct JA4 { |
35 | | tls_version: Option<TlsVersion>, |
36 | | ciphersuites: Vec<TlsCipherSuiteID>, |
37 | | extensions: Vec<TlsExtensionType>, |
38 | | signature_algorithms: Vec<u16>, |
39 | | domain: bool, |
40 | | alpn: [char; 2], |
41 | | quic: bool, |
42 | | // Some extensions contribute to the total count component of the |
43 | | // fingerprint, yet are not to be included in the SHA256 hash component. |
44 | | // Let's track the count separately. |
45 | | nof_exts: u16, |
46 | | } |
47 | | |
48 | | impl Default for JA4 { |
49 | 0 | fn default() -> Self { |
50 | 0 | Self::new() |
51 | 0 | } |
52 | | } |
53 | | |
54 | | // Stubs for when JA4 is disabled |
55 | | #[cfg(not(feature = "ja4"))] |
56 | | impl JA4 { |
57 | | pub fn new() -> Self { |
58 | | Self { |
59 | | tls_version: None, |
60 | | // Vec::new() does not allocate memory until filled, which we |
61 | | // will not do here. |
62 | | ciphersuites: Vec::new(), |
63 | | extensions: Vec::new(), |
64 | | signature_algorithms: Vec::new(), |
65 | | domain: false, |
66 | | alpn: ['0', '0'], |
67 | | quic: false, |
68 | | nof_exts: 0, |
69 | | } |
70 | | } |
71 | | pub fn set_quic(&mut self) {} |
72 | | pub fn set_tls_version(&mut self, _version: TlsVersion) {} |
73 | | pub fn set_alpn(&mut self, _alpn: &[u8]) {} |
74 | | pub fn add_cipher_suite(&mut self, _cipher: TlsCipherSuiteID) {} |
75 | | pub fn add_extension(&mut self, _ext: TlsExtensionType) {} |
76 | | pub fn add_signature_algorithm(&mut self, _sigalgo: u16) {} |
77 | | pub fn get_hash(&self) -> String { |
78 | | String::new() |
79 | | } |
80 | | } |
81 | | |
82 | | #[cfg(feature = "ja4")] |
83 | | impl JA4 { |
84 | | #[inline] |
85 | 7.64M | fn is_grease(val: u16) -> bool { |
86 | 7.64M | match val { |
87 | | 0x0a0a | 0x1a1a | 0x2a2a | 0x3a3a | 0x4a4a | 0x5a5a | 0x6a6a | 0x7a7a | 0x8a8a |
88 | 59.4k | | 0x9a9a | 0xaaaa | 0xbaba | 0xcaca | 0xdada | 0xeaea | 0xfafa => true, |
89 | 7.58M | _ => false, |
90 | | } |
91 | 7.64M | } |
92 | | |
93 | | #[inline] |
94 | 36.0k | fn version_to_ja4code(val: Option<TlsVersion>) -> &'static str { |
95 | 36.0k | match val { |
96 | 57 | Some(TlsVersion::Tls13) => "13", |
97 | 5 | Some(TlsVersion::Tls12) => "12", |
98 | 5 | Some(TlsVersion::Tls11) => "11", |
99 | 307 | Some(TlsVersion::Tls10) => "10", |
100 | 7 | Some(TlsVersion::Ssl30) => "s3", |
101 | | // the TLS parser does not support SSL 1.0 and 2.0 hence no |
102 | | // support for "s1"/"s2" |
103 | 35.6k | _ => "00", |
104 | | } |
105 | 36.0k | } |
106 | | |
107 | 52.5k | pub fn new() -> Self { |
108 | 52.5k | Self { |
109 | 52.5k | tls_version: None, |
110 | 52.5k | ciphersuites: Vec::with_capacity(20), |
111 | 52.5k | extensions: Vec::with_capacity(20), |
112 | 52.5k | signature_algorithms: Vec::with_capacity(20), |
113 | 52.5k | domain: false, |
114 | 52.5k | alpn: ['0', '0'], |
115 | 52.5k | quic: false, |
116 | 52.5k | nof_exts: 0, |
117 | 52.5k | } |
118 | 52.5k | } |
119 | | |
120 | 47.0k | pub fn set_quic(&mut self) { |
121 | 47.0k | self.quic = true; |
122 | 47.0k | } |
123 | | |
124 | 134k | pub fn set_tls_version(&mut self, version: TlsVersion) { |
125 | 134k | if JA4::is_grease(u16::from(version)) { |
126 | 5.27k | return; |
127 | 129k | } |
128 | | // Track maximum of seen TLS versions |
129 | 129k | match self.tls_version { |
130 | 6.92k | None => { |
131 | 6.92k | self.tls_version = Some(version); |
132 | 6.92k | } |
133 | 122k | Some(cur_version) => { |
134 | 122k | if u16::from(version) > u16::from(cur_version) { |
135 | 5.38k | self.tls_version = Some(version); |
136 | 116k | } |
137 | | } |
138 | | } |
139 | 134k | } |
140 | | |
141 | 7.36k | pub fn set_alpn(&mut self, alpn: &[u8]) { |
142 | 7.36k | if !alpn.is_empty() { |
143 | | // If the first ALPN value is only a single character, then that character is treated as both the first and last character. |
144 | 7.03k | if alpn.len() == 2 { |
145 | | // GREASE values are 2 bytes, so this could be one -- check |
146 | 6.32k | let v: u16 = ((alpn[0] as u16) << 8) | alpn[alpn.len() - 1] as u16; |
147 | 6.32k | if JA4::is_grease(v) { |
148 | 1.69k | return; |
149 | 4.62k | } |
150 | 711 | } |
151 | 5.33k | if !alpn[0].is_ascii_alphanumeric() || !alpn[alpn.len() - 1].is_ascii_alphanumeric() { |
152 | | // 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. |
153 | 875 | self.alpn[0] = char::from(HEX[(alpn[0] >> 4) as usize]); |
154 | 875 | self.alpn[1] = char::from(HEX[(alpn[alpn.len() - 1] & 0xF) as usize]); |
155 | 875 | return |
156 | 4.45k | } |
157 | 4.45k | self.alpn[0] = char::from(alpn[0]); |
158 | 4.45k | self.alpn[1] = char::from(alpn[alpn.len() - 1]); |
159 | 338 | } |
160 | 7.36k | } |
161 | | |
162 | 5.40M | pub fn add_cipher_suite(&mut self, cipher: TlsCipherSuiteID) { |
163 | 5.40M | if JA4::is_grease(u16::from(cipher)) { |
164 | 43.0k | return; |
165 | 5.36M | } |
166 | 5.36M | self.ciphersuites.push(cipher); |
167 | 5.40M | } |
168 | | |
169 | 1.67M | pub fn add_extension(&mut self, ext: TlsExtensionType) { |
170 | 1.67M | if JA4::is_grease(u16::from(ext)) { |
171 | 309 | return; |
172 | 1.67M | } |
173 | 1.67M | if ext != TlsExtensionType::ApplicationLayerProtocolNegotiation |
174 | 1.66M | && ext != TlsExtensionType::ServerName |
175 | 592k | { |
176 | 592k | self.extensions.push(ext); |
177 | 1.08M | } else if ext == TlsExtensionType::ServerName { |
178 | 1.07M | self.domain = true; |
179 | 1.07M | } |
180 | 1.67M | self.nof_exts += 1; |
181 | 1.67M | } |
182 | | |
183 | 421k | pub fn add_signature_algorithm(&mut self, sigalgo: u16) { |
184 | 421k | if JA4::is_grease(sigalgo) { |
185 | 9.06k | return; |
186 | 411k | } |
187 | 411k | self.signature_algorithms.push(sigalgo); |
188 | 421k | } |
189 | | |
190 | 36.0k | pub fn get_hash(&self) -> String { |
191 | | // Calculate JA4_a |
192 | 36.0k | let ja4_a = format!( |
193 | 36.0k | "{proto}{version}{sni}{nof_c:02}{nof_e:02}{al1}{al2}", |
194 | 36.0k | proto = if self.quic { "q" } else { "t" }, |
195 | 36.0k | version = JA4::version_to_ja4code(self.tls_version), |
196 | 36.0k | sni = if self.domain { "d" } else { "i" }, |
197 | 36.0k | nof_c = min(99, self.ciphersuites.len()), |
198 | 36.0k | nof_e = min(99, self.nof_exts), |
199 | 36.0k | al1 = self.alpn[0], |
200 | 36.0k | al2 = self.alpn[1] |
201 | | ); |
202 | | |
203 | | // Calculate JA4_b |
204 | 36.0k | let mut sorted_ciphers = self.ciphersuites.to_vec(); |
205 | 19.5M | sorted_ciphers.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b))); |
206 | 36.0k | let sorted_cipherstrings: Vec<String> = sorted_ciphers |
207 | 36.0k | .iter() |
208 | 5.19M | .map(|v| format!("{:04x}", u16::from(*v))) |
209 | 36.0k | .collect(); |
210 | 36.0k | let mut sha = Sha256::new(); |
211 | 36.0k | let ja4_b_raw = sorted_cipherstrings.join(","); |
212 | 36.0k | sha.update(&ja4_b_raw); |
213 | 36.0k | let mut ja4_b = format!("{:x}", sha.finalize_reset()); |
214 | 36.0k | ja4_b.truncate(12); |
215 | | |
216 | | // Calculate JA4_c |
217 | 36.0k | let mut sorted_exts = self.extensions.to_vec(); |
218 | 2.31M | sorted_exts.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b))); |
219 | 36.0k | let sorted_extstrings: Vec<String> = sorted_exts |
220 | 36.0k | .iter() |
221 | 535k | .map(|v| format!("{:04x}", u16::from(*v))) |
222 | 36.0k | .collect(); |
223 | 36.0k | let ja4_c1_raw = sorted_extstrings.join(","); |
224 | 36.0k | let unsorted_sigalgostrings: Vec<String> = self |
225 | 36.0k | .signature_algorithms |
226 | 36.0k | .iter() |
227 | 359k | .map(|v| format!("{:04x}", (*v))) |
228 | 36.0k | .collect(); |
229 | 36.0k | let ja4_c2_raw = unsorted_sigalgostrings.join(","); |
230 | 36.0k | let ja4_c_raw = format!("{}_{}", ja4_c1_raw, ja4_c2_raw); |
231 | 36.0k | sha.update(&ja4_c_raw); |
232 | 36.0k | let mut ja4_c = format!("{:x}", sha.finalize()); |
233 | 36.0k | ja4_c.truncate(12); |
234 | | |
235 | 36.0k | return format!("{}_{}_{}", ja4_a, ja4_b, ja4_c); |
236 | 36.0k | } |
237 | | } |
238 | | |
239 | | #[no_mangle] |
240 | 5.54k | pub extern "C" fn SCJA4New() -> *mut JA4 { |
241 | 5.54k | let j = Box::new(JA4::new()); |
242 | 5.54k | Box::into_raw(j) |
243 | 5.54k | } |
244 | | |
245 | | #[no_mangle] |
246 | 9.17k | pub unsafe extern "C" fn SCJA4SetTLSVersion(j: &mut JA4, version: u16) { |
247 | 9.17k | j.set_tls_version(TlsVersion(version)); |
248 | 9.17k | } |
249 | | |
250 | | #[no_mangle] |
251 | 89.9k | pub unsafe extern "C" fn SCJA4AddCipher(j: &mut JA4, cipher: u16) { |
252 | 89.9k | j.add_cipher_suite(TlsCipherSuiteID(cipher)); |
253 | 89.9k | } |
254 | | |
255 | | #[no_mangle] |
256 | 69.1k | pub unsafe extern "C" fn SCJA4AddExtension(j: &mut JA4, ext: u16) { |
257 | 69.1k | j.add_extension(TlsExtensionType(ext)); |
258 | 69.1k | } |
259 | | |
260 | | #[no_mangle] |
261 | 39.8k | pub unsafe extern "C" fn SCJA4AddSigAlgo(j: &mut JA4, sigalgo: u16) { |
262 | 39.8k | j.add_signature_algorithm(sigalgo); |
263 | 39.8k | } |
264 | | |
265 | | #[no_mangle] |
266 | 4.09k | pub unsafe extern "C" fn SCJA4SetALPN(j: &mut JA4, proto: *const c_char, len: u16) { |
267 | 4.09k | let b: &[u8] = std::slice::from_raw_parts(proto as *const c_uchar, len as usize); |
268 | 4.09k | j.set_alpn(b); |
269 | 4.09k | } |
270 | | |
271 | | #[no_mangle] |
272 | 12 | pub unsafe extern "C" fn SCJA4GetHash(j: &mut JA4, out: &mut [u8; 36]) { |
273 | 12 | let hash = j.get_hash(); |
274 | 12 | out[0..36].copy_from_slice(hash.as_bytes()); |
275 | 12 | } |
276 | | |
277 | | #[no_mangle] |
278 | 5.54k | pub unsafe extern "C" fn SCJA4Free(j: &mut JA4) { |
279 | 5.54k | let ja4: Box<JA4> = Box::from_raw(j); |
280 | 5.54k | std::mem::drop(ja4); |
281 | 5.54k | } |
282 | | |
283 | | #[cfg(all(test, feature = "ja4"))] |
284 | | mod tests { |
285 | | use super::*; |
286 | | |
287 | | #[test] |
288 | | fn test_is_grease() { |
289 | | let mut alpn = "foobar".as_bytes(); |
290 | | let mut len = alpn.len(); |
291 | | let v: u16 = ((alpn[0] as u16) << 8) | alpn[len - 1] as u16; |
292 | | assert!(!JA4::is_grease(v)); |
293 | | |
294 | | alpn = &[0x0a, 0x0a]; |
295 | | len = alpn.len(); |
296 | | let v: u16 = ((alpn[0] as u16) << 8) | alpn[len - 1] as u16; |
297 | | assert!(JA4::is_grease(v)); |
298 | | } |
299 | | |
300 | | #[test] |
301 | | fn test_tlsversion_max() { |
302 | | let mut j = JA4::new(); |
303 | | assert_eq!(j.tls_version, None); |
304 | | j.set_tls_version(TlsVersion::Ssl30); |
305 | | assert_eq!(j.tls_version, Some(TlsVersion::Ssl30)); |
306 | | j.set_tls_version(TlsVersion::Tls12); |
307 | | assert_eq!(j.tls_version, Some(TlsVersion::Tls12)); |
308 | | j.set_tls_version(TlsVersion::Tls10); |
309 | | assert_eq!(j.tls_version, Some(TlsVersion::Tls12)); |
310 | | } |
311 | | |
312 | | #[test] |
313 | | fn test_get_hash_limit_numbers() { |
314 | | // Test whether the limitation of the extension and ciphersuite |
315 | | // count to 99 is reflected correctly. |
316 | | let mut j = JA4::new(); |
317 | | |
318 | | for i in 1..200 { |
319 | | j.add_cipher_suite(TlsCipherSuiteID(i)); |
320 | | } |
321 | | for i in 1..200 { |
322 | | j.add_extension(TlsExtensionType(i)); |
323 | | } |
324 | | |
325 | | let mut s = j.get_hash(); |
326 | | s.truncate(10); |
327 | | assert_eq!(s, "t00i999900"); |
328 | | } |
329 | | |
330 | | #[test] |
331 | | fn test_short_alpn() { |
332 | | let mut j = JA4::new(); |
333 | | |
334 | | j.set_alpn("b".as_bytes()); |
335 | | let mut s = j.get_hash(); |
336 | | s.truncate(10); |
337 | | assert_eq!(s, "t00i0000bb"); |
338 | | |
339 | | j.set_alpn("h2".as_bytes()); |
340 | | let mut s = j.get_hash(); |
341 | | s.truncate(10); |
342 | | assert_eq!(s, "t00i0000h2"); |
343 | | |
344 | | // from https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md#alpn-extension-value |
345 | | j.set_alpn(&[0xab]); |
346 | | let mut s = j.get_hash(); |
347 | | s.truncate(10); |
348 | | assert_eq!(s, "t00i0000ab"); |
349 | | |
350 | | j.set_alpn(&[0xab, 0xcd]); |
351 | | let mut s = j.get_hash(); |
352 | | s.truncate(10); |
353 | | assert_eq!(s, "t00i0000ad"); |
354 | | |
355 | | j.set_alpn(&[0x30, 0xab]); |
356 | | let mut s = j.get_hash(); |
357 | | s.truncate(10); |
358 | | assert_eq!(s, "t00i00003b"); |
359 | | |
360 | | j.set_alpn(&[0x30, 0x31, 0xab, 0xcd]); |
361 | | let mut s = j.get_hash(); |
362 | | s.truncate(10); |
363 | | assert_eq!(s, "t00i00003d"); |
364 | | |
365 | | j.set_alpn(&[0x30, 0xab, 0xcd, 0x31]); |
366 | | let mut s = j.get_hash(); |
367 | | s.truncate(10); |
368 | | assert_eq!(s, "t00i000001"); |
369 | | } |
370 | | |
371 | | #[test] |
372 | | fn test_get_hash() { |
373 | | let mut j = JA4::new(); |
374 | | |
375 | | // the empty JA4 hash |
376 | | let s = j.get_hash(); |
377 | | assert_eq!(s, "t00i000000_e3b0c44298fc_d2e2adf7177b"); |
378 | | |
379 | | // set TLS version |
380 | | j.set_tls_version(TlsVersion::Tls12); |
381 | | let s = j.get_hash(); |
382 | | assert_eq!(s, "t12i000000_e3b0c44298fc_d2e2adf7177b"); |
383 | | |
384 | | // set QUIC |
385 | | j.set_quic(); |
386 | | let s = j.get_hash(); |
387 | | assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b"); |
388 | | |
389 | | // set GREASE extension, should be ignored |
390 | | j.add_extension(TlsExtensionType(0x0a0a)); |
391 | | let s = j.get_hash(); |
392 | | assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b"); |
393 | | |
394 | | // set SNI extension, should only increase count and change i->d |
395 | | j.add_extension(TlsExtensionType(0x0000)); |
396 | | let s = j.get_hash(); |
397 | | assert_eq!(s, "q12d000100_e3b0c44298fc_d2e2adf7177b"); |
398 | | |
399 | | // set ALPN extension, should only increase count and set end of JA4_a |
400 | | j.set_alpn(b"h3-16"); |
401 | | j.add_extension(TlsExtensionType::ApplicationLayerProtocolNegotiation); |
402 | | let s = j.get_hash(); |
403 | | assert_eq!(s, "q12d0002h6_e3b0c44298fc_d2e2adf7177b"); |
404 | | |
405 | | // set some ciphers |
406 | | j.add_cipher_suite(TlsCipherSuiteID(0x1111)); |
407 | | j.add_cipher_suite(TlsCipherSuiteID(0x0a20)); |
408 | | j.add_cipher_suite(TlsCipherSuiteID(0xbada)); |
409 | | let s = j.get_hash(); |
410 | | assert_eq!(s, "q12d0302h6_f500716053f9_d2e2adf7177b"); |
411 | | |
412 | | // set some extensions and signature algorithms |
413 | | j.add_extension(TlsExtensionType(0xface)); |
414 | | j.add_extension(TlsExtensionType(0x0121)); |
415 | | j.add_extension(TlsExtensionType(0x1234)); |
416 | | j.add_signature_algorithm(0x6666); |
417 | | let s = j.get_hash(); |
418 | | assert_eq!(s, "q12d0305h6_f500716053f9_2debc8880bae"); |
419 | | } |
420 | | } |