Coverage Report

Created: 2026-09-28 07:39

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