Coverage Report

Created: 2026-01-16 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}