]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
ja4: implement for TLS and QUIC
authorSascha Steinbiss <satta@debian.org>
Sat, 24 Feb 2024 10:10:47 +0000 (11:10 +0100)
committerJeff Lucovsky <jlucovsky@oisf.net>
Sat, 8 Jun 2024 13:36:46 +0000 (09:36 -0400)
Ticket: OISF#6379
(cherry picked from commit 120313f4daf2b2501f7ab098954e91f550413c96)

32 files changed:
configure.ac
doc/userguide/output/eve/eve-json-format.rst
doc/userguide/output/eve/eve-json-output.rst
doc/userguide/rules/index.rst
doc/userguide/rules/ja-keywords.rst [moved from doc/userguide/rules/ja3-keywords.rst with 60% similarity]
etc/schema.json
rust/Cargo.toml.in
rust/Makefile.am
rust/src/ja4.rs [new file with mode: 0644]
rust/src/lib.rs
rust/src/quic/detect.rs
rust/src/quic/frames.rs
rust/src/quic/logger.rs
rust/src/quic/quic.rs
src/Makefile.am
src/app-layer-ssl.c
src/app-layer-ssl.h
src/detect-engine-register.c
src/detect-engine-register.h
src/detect-ja4-hash.c [new file with mode: 0644]
src/detect-ja4-hash.h [new file with mode: 0644]
src/detect-parse.c
src/detect-tls-ja3-hash.c
src/detect-tls-ja3-string.c
src/detect-tls-ja3s-hash.c
src/detect-tls-ja3s-string.c
src/detect.h
src/feature.h
src/output-json-tls.c
src/suricata.c
src/util-ja4.h [new file with mode: 0644]
suricata.yaml.in

index 74ab6fd38329a7b9f9769313abdef3dc7d8a70c2..91c798fc0fbf03b4016b08e5b71622e1218b0283 100644 (file)
     fi
     AM_CONDITIONAL([HAVE_JA3], [test "x$enable_ja3" != "xno"])
 
+    AC_ARG_ENABLE(ja4,
+           AS_HELP_STRING([--disable-ja4], [Disable JA4 support]),
+           [enable_ja4="$enableval"],
+           [enable_ja4=yes])
+    if test "$enable_ja4" = "yes"; then
+        AC_DEFINE([HAVE_JA4],[1],[JA4 enabled])
+        enable_ja4="yes"
+    fi
+    AM_CONDITIONAL([HAVE_JA4], [test "x$enable_ja4" != "xno"])
+
+
 # Check for lz4
 enable_liblz4="yes"
 AC_CHECK_LIB(lz4, LZ4F_createCompressionContext, , enable_liblz4="no")
@@ -2668,6 +2679,7 @@ SURICATA_BUILD_CONF="Suricata Configuration:
   libluajit:                               ${enable_luajit}
   GeoIP2 support:                          ${enable_geoip}
   JA3 support:                             ${enable_ja3}
+  JA4 support:                             ${enable_ja4}
   Non-bundled htp:                         ${enable_non_bundled_htp}
   Hyperscan support:                       ${enable_hyperscan}
   Libnet support:                          ${enable_libnet}
index 3184426b30ccbde1d03b62b20a648b1dec1a4f4d..a2140149eb04ede598a022563cbd37ebb20124a9 100644 (file)
@@ -1045,8 +1045,9 @@ If extended logging is enabled the following fields are also included:
 * "notafter": The NotAfter field from the TLS certificate
 * "ja3": The JA3 fingerprint consisting of both a JA3 hash and a JA3 string
 * "ja3s": The JA3S fingerprint consisting of both a JA3 hash and a JA3 string
+* "ja4": The JA4 client fingerprint for TLS
 
-JA3 must be enabled in the Suricata config file (set 'app-layer.protocols.tls.ja3-fingerprints' to 'yes').
+JA3 and JA4 must be enabled in the Suricata config file (set 'app-layer.protocols.tls.ja3-fingerprints'/'app-layer.protocols.tls.ja4-fingerprints' to 'yes').
 
 In addition to this, custom logging also allows the following fields:
 
@@ -2915,11 +2916,14 @@ Fields
 * "cyu": List of found CYUs in the packet
 * "cyu[].hash": CYU hash
 * "cyu[].string": CYU string
+* "ja3": The JA3 fingerprint consisting of both a JA3 hash and a JA3 string
+* "ja3s": The JA3S fingerprint consisting of both a JA3 hash and a JA3 string
+* "ja4": The JA4 client fingerprint for QUIC
 
 Examples
 ~~~~~~~~
 
-Example of QUIC logging with a CYU hash:
+Example of QUIC logging with CYU, JA3 and JA4 hashes (note that the JA4 hash is only an example to illustrate the format and does not correlate with the others):
 
 ::
 
@@ -2931,7 +2935,12 @@ Example of QUIC logging with a CYU hash:
             "hash": "7b3ceb1adc974ad360cfa634e8d0a730",
             "string": "46,PAD-SNI-STK-SNO-VER-CCS-NONC-AEAD-UAID-SCID-TCID-PDMD-SMHL-ICSL-NONP-PUBS-MIDS-SCLS-KEXS-XLCT-CSCT-COPT-CCRT-IRTT-CFCW-SFCW"
         }
-    ]
+    ],
+    "ja3": {
+        "hash": "324f8c50e267adba4b5dd06c964faf67",
+        "string": "771,4865-4866-4867,51-43-13-27-17513-16-45-0-10-57,29-23-24,"
+    },
+    "ja4": "q13d0310h3_55b375c5d22e_cd85d2d88918"
   }
 
 Event type: DHCP
index 2730f543bbf5b6b0a4615d59741aa271d43166a9..364a80418d3998545de68ab66d9a6286a88bd3bd 100644 (file)
@@ -259,7 +259,7 @@ YAML::
             extended: yes     # enable this for extended logging information
             # custom allows to control which tls fields that are included
             # in eve-log
-            #custom: [subject, issuer, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s]
+            #custom: [subject, issuer, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s, ja4]
 
 The default is to log certificate subject and issuer. If ``extended`` is
 enabled, then the log gets more verbose.
index 76266b33458100a6f4f802dc62a96434d979ef7a..1a6e9fbc155afd1a799716467a9debbb3c923ee6 100644 (file)
@@ -16,7 +16,7 @@ Suricata Rules
    dns-keywords
    tls-keywords
    ssh-keywords
-   ja3-keywords
+   ja-keywords
    modbus-keyword
    dcerpc-keywords
    dhcp-keywords
similarity index 60%
rename from doc/userguide/rules/ja3-keywords.rst
rename to doc/userguide/rules/ja-keywords.rst
index c77b9f390635fe44daeb423a143aa60ab1451b7b..6e80efd2553ec48dc6cc8ecb7e572aac3222cad6 100644 (file)
@@ -1,9 +1,16 @@
-JA3 Keywords
-============
+JA3/JA4 Keywords
+================
 
-Suricata comes with a JA3 integration (https://github.com/salesforce/ja3). JA3 is used to fingerprint TLS clients.
+Suricata comes with JA3 (https://github.com/salesforce/ja3) and 
+JA4 (https://github.com/FoxIO-LLC/ja4) integration.
+JA3 and JA4 are used to fingerprint TLS and QUIC clients.
 
-JA3 must be enabled in the Suricata config file (set 'app-layer.protocols.tls.ja3-fingerprints' to 'yes').
+Support must be enabled in the Suricata config file (set
+``app-layer.protocols.tls.ja{3,4}-fingerprints`` to ``yes``). If it is not
+explicitly disabled (``no``) , it will be enabled if a loaded rule requires it.
+Note that JA3/JA4 support can also be disabled at compile time; it is possible to
+use the ``requires: feature ja{3,4};`` keyword to skip rules if no JA3/JA4 support is
+present.
 
 ja3.hash
 --------
@@ -71,3 +78,19 @@ Example::
 ``ja3s.string`` is a 'sticky buffer'.
 
 ``ja3s.string`` can be used as ``fast_pattern``.
+
+ja4.hash
+--------
+
+Match on JA4 hash (e.g. ``q13d0310h3_55b375c5d22e_cd85d2d88918``).
+
+Example::
+
+  alert quic any any -> any any (msg:"match JA4 hash"; \
+      ja4.hash; content:"q13d0310h3_55b375c5d22e_cd85d2d88918"; \
+      sid:100001;)
+
+``ja4.hash`` is a 'sticky buffer'.
+
+``ja4.hash`` can be used as ``fast_pattern``.
+
index 960de2136fa40f5c0c67208dcc441aecda7d9759..f733627042b0cb7202d5b3db8f596cafd07bceb8 100644 (file)
                 "renewal_time": {
                     "type": "integer"
                 },
-                "requested_ip":{
+                "requested_ip": {
                     "type": "string"
                 },
                 "subnet_mask": {
                 "type": {
                     "type": "string"
                 },
-                "vendor_class_identifier":{
+                "vendor_class_identifier": {
                     "type": "string"
                 },
                 "dns_servers": {
                     },
                     "additionalProperties": false
                 },
+                "ja4": {
+                    "type": "string"
+                },
                 "sni": {
                     "type": "string"
                 },
                         }
                     },
                     "additionalProperties": false
+                },
+                "ja4": {
+                    "type": "string"
                 }
             },
             "additionalProperties": false
index 9c0d3b82bef76d661ca7d25fcaccb3e51c61100f..1d33dca58978f221dd3bf6695113c58a22222e16 100644 (file)
@@ -22,6 +22,7 @@ strict = []
 debug = []
 debug-validate = []
 ja3 = []
+ja4 = []
 
 [dependencies]
 nom7 = { version="7.0", package="nom" }
index fb172f993ee257c98c2ea3b1427f94349aa030bf..665077b3e5fc636b3d2cf9e693097757ae723e8e 100644 (file)
@@ -20,6 +20,10 @@ if HAVE_JA3
 RUST_FEATURES +=       ja3
 endif
 
+if HAVE_JA4
+RUST_FEATURES +=       ja4
+endif
+
 if DEBUG
 RUST_FEATURES +=       debug
 endif
diff --git a/rust/src/ja4.rs b/rust/src/ja4.rs
new file mode 100644 (file)
index 0000000..883c00e
--- /dev/null
@@ -0,0 +1,385 @@
+/* Copyright (C) 2023-2024 Open Information Security Foundation
+*
+* You can copy, redistribute or modify this Program under the terms of
+* the GNU General Public License version 2 as published by the Free
+* Software Foundation.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* version 2 along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+* 02110-1301, USA.
+
+// Author: Sascha Steinbiss <sascha@steinbiss.name>
+
+*/
+
+#[cfg(feature = "ja4")]
+use digest::Digest;
+use libc::c_uchar;
+#[cfg(feature = "ja4")]
+use sha2::Sha256;
+#[cfg(feature = "ja4")]
+use std::cmp::min;
+use std::os::raw::c_char;
+use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion};
+
+#[derive(Debug, PartialEq)]
+pub struct JA4 {
+    tls_version: Option<TlsVersion>,
+    ciphersuites: Vec<TlsCipherSuiteID>,
+    extensions: Vec<TlsExtensionType>,
+    signature_algorithms: Vec<u16>,
+    domain: bool,
+    alpn: [char; 2],
+    quic: bool,
+    // Some extensions contribute to the total count component of the
+    // fingerprint, yet are not to be included in the SHA256 hash component.
+    // Let's track the count separately.
+    nof_exts: u16,
+}
+
+impl Default for JA4 {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+// Stubs for when JA4 is disabled
+#[cfg(not(feature = "ja4"))]
+impl JA4 {
+    pub fn new() -> Self {
+        Self {
+            tls_version: None,
+            // Vec::new() does not allocate memory until filled, which we
+            // will not do here.
+            ciphersuites: Vec::new(),
+            extensions: Vec::new(),
+            signature_algorithms: Vec::new(),
+            domain: false,
+            alpn: ['0', '0'],
+            quic: false,
+            nof_exts: 0,
+        }
+    }
+    pub fn set_quic(&mut self) {}
+    pub fn set_tls_version(&mut self, _version: TlsVersion) {}
+    pub fn set_alpn(&mut self, _alpn: &[u8]) {}
+    pub fn add_cipher_suite(&mut self, _cipher: TlsCipherSuiteID) {}
+    pub fn add_extension(&mut self, _ext: TlsExtensionType) {}
+    pub fn add_signature_algorithm(&mut self, _sigalgo: u16) {}
+    pub fn get_hash(&self) -> String {
+        String::new()
+    }
+}
+
+#[cfg(feature = "ja4")]
+impl JA4 {
+    #[inline]
+    fn is_grease(val: u16) -> bool {
+        match val {
+            0x0a0a | 0x1a1a | 0x2a2a | 0x3a3a | 0x4a4a | 0x5a5a | 0x6a6a | 0x7a7a | 0x8a8a
+            | 0x9a9a | 0xaaaa | 0xbaba | 0xcaca | 0xdada | 0xeaea | 0xfafa => true,
+            _ => false,
+        }
+    }
+
+    #[inline]
+    fn version_to_ja4code(val: Option<TlsVersion>) -> &'static str {
+        match val {
+            Some(TlsVersion::Tls13) => "13",
+            Some(TlsVersion::Tls12) => "12",
+            Some(TlsVersion::Tls11) => "11",
+            Some(TlsVersion::Tls10) => "10",
+            Some(TlsVersion::Ssl30) => "s3",
+            // the TLS parser does not support SSL 1.0 and 2.0 hence no
+            // support for "s1"/"s2"
+            _ => "00",
+        }
+    }
+
+    pub fn new() -> Self {
+        Self {
+            tls_version: None,
+            ciphersuites: Vec::with_capacity(20),
+            extensions: Vec::with_capacity(20),
+            signature_algorithms: Vec::with_capacity(20),
+            domain: false,
+            alpn: ['0', '0'],
+            quic: false,
+            nof_exts: 0,
+        }
+    }
+
+    pub fn set_quic(&mut self) {
+        self.quic = true;
+    }
+
+    pub fn set_tls_version(&mut self, version: TlsVersion) {
+        if JA4::is_grease(u16::from(version)) {
+            return;
+        }
+        // Track maximum of seen TLS versions
+        match self.tls_version {
+            None => {
+                self.tls_version = Some(version);
+            }
+            Some(cur_version) => {
+                if u16::from(version) > u16::from(cur_version) {
+                    self.tls_version = Some(version);
+                }
+            }
+        }
+    }
+
+    pub fn set_alpn(&mut self, alpn: &[u8]) {
+        if alpn.len() > 1 {
+            if alpn.len() == 2 {
+                // GREASE values are 2 bytes, so this could be one -- check
+                let v: u16 = (alpn[0] as u16) << 8 | alpn[alpn.len() - 1] as u16;
+                if JA4::is_grease(v) {
+                    return;
+                }
+            }
+            self.alpn[0] = char::from(alpn[0]);
+            self.alpn[1] = char::from(alpn[alpn.len() - 1]);
+        }
+    }
+
+    pub fn add_cipher_suite(&mut self, cipher: TlsCipherSuiteID) {
+        if JA4::is_grease(u16::from(cipher)) {
+            return;
+        }
+        self.ciphersuites.push(cipher);
+    }
+
+    pub fn add_extension(&mut self, ext: TlsExtensionType) {
+        if JA4::is_grease(u16::from(ext)) {
+            return;
+        }
+        if ext != TlsExtensionType::ApplicationLayerProtocolNegotiation
+            && ext != TlsExtensionType::ServerName
+        {
+            self.extensions.push(ext);
+        } else if ext == TlsExtensionType::ServerName {
+            self.domain = true;
+        }
+        self.nof_exts += 1;
+    }
+
+    pub fn add_signature_algorithm(&mut self, sigalgo: u16) {
+        if JA4::is_grease(sigalgo) {
+            return;
+        }
+        self.signature_algorithms.push(sigalgo);
+    }
+
+    pub fn get_hash(&self) -> String {
+        // Calculate JA4_a
+        let ja4_a = format!(
+            "{proto}{version}{sni}{nof_c:02}{nof_e:02}{al1}{al2}",
+            proto = if self.quic { "q" } else { "t" },
+            version = JA4::version_to_ja4code(self.tls_version),
+            sni = if self.domain { "d" } else { "i" },
+            nof_c = min(99, self.ciphersuites.len()),
+            nof_e = min(99, self.nof_exts),
+            al1 = self.alpn[0],
+            al2 = self.alpn[1]
+        );
+
+        // Calculate JA4_b
+        let mut sorted_ciphers = self.ciphersuites.to_vec();
+        sorted_ciphers.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b)));
+        let sorted_cipherstrings: Vec<String> = sorted_ciphers
+            .iter()
+            .map(|v| format!("{:04x}", u16::from(*v)))
+            .collect();
+        let mut sha = Sha256::new();
+        let ja4_b_raw = sorted_cipherstrings.join(",");
+        sha.update(&ja4_b_raw);
+        let mut ja4_b = format!("{:x}", sha.finalize_reset());
+        ja4_b.truncate(12);
+
+        // Calculate JA4_c
+        let mut sorted_exts = self.extensions.to_vec();
+        sorted_exts.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b)));
+        let sorted_extstrings: Vec<String> = sorted_exts
+            .iter()
+            .map(|v| format!("{:04x}", u16::from(*v)))
+            .collect();
+        let ja4_c1_raw = sorted_extstrings.join(",");
+        let unsorted_sigalgostrings: Vec<String> = self
+            .signature_algorithms
+            .iter()
+            .map(|v| format!("{:04x}", (*v)))
+            .collect();
+        let ja4_c2_raw = unsorted_sigalgostrings.join(",");
+        let ja4_c_raw = format!("{}_{}", ja4_c1_raw, ja4_c2_raw);
+        sha.update(&ja4_c_raw);
+        let mut ja4_c = format!("{:x}", sha.finalize());
+        ja4_c.truncate(12);
+
+        return format!("{}_{}_{}", ja4_a, ja4_b, ja4_c);
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn SCJA4New() -> *mut JA4 {
+    let j = Box::new(JA4::new());
+    Box::into_raw(j)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4SetTLSVersion(j: &mut JA4, version: u16) {
+    j.set_tls_version(TlsVersion(version));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4AddCipher(j: &mut JA4, cipher: u16) {
+    j.add_cipher_suite(TlsCipherSuiteID(cipher));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4AddExtension(j: &mut JA4, ext: u16) {
+    j.add_extension(TlsExtensionType(ext));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4AddSigAlgo(j: &mut JA4, sigalgo: u16) {
+    j.add_signature_algorithm(sigalgo);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4SetALPN(j: &mut JA4, proto: *const c_char, len: u16) {
+    let b: &[u8] = std::slice::from_raw_parts(proto as *const c_uchar, len as usize);
+    j.set_alpn(b);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4GetHash(j: &mut JA4, out: &mut [u8; 36]) {
+    let hash = j.get_hash();
+    out[0..36].copy_from_slice(hash.as_bytes());
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCJA4Free(j: &mut JA4) {
+    let ja4: Box<JA4> = Box::from_raw(j);
+    std::mem::drop(ja4);
+}
+
+#[cfg(all(test, feature = "ja4"))]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_is_grease() {
+        let mut alpn = "foobar".as_bytes();
+        let mut len = alpn.len();
+        let v: u16 = (alpn[0] as u16) << 8 | alpn[len - 1] as u16;
+        assert!(!JA4::is_grease(v));
+
+        alpn = &[0x0a, 0x0a];
+        len = alpn.len();
+        let v: u16 = (alpn[0] as u16) << 8 | alpn[len - 1] as u16;
+        assert!(JA4::is_grease(v));
+    }
+
+    #[test]
+    fn test_tlsversion_max() {
+        let mut j = JA4::new();
+        assert_eq!(j.tls_version, None);
+        j.set_tls_version(TlsVersion::Ssl30);
+        assert_eq!(j.tls_version, Some(TlsVersion::Ssl30));
+        j.set_tls_version(TlsVersion::Tls12);
+        assert_eq!(j.tls_version, Some(TlsVersion::Tls12));
+        j.set_tls_version(TlsVersion::Tls10);
+        assert_eq!(j.tls_version, Some(TlsVersion::Tls12));
+    }
+
+    #[test]
+    fn test_get_hash_limit_numbers() {
+        // Test whether the limitation of the extension and ciphersuite
+        // count to 99 is reflected correctly.
+        let mut j = JA4::new();
+
+        for i in 1..200 {
+            j.add_cipher_suite(TlsCipherSuiteID(i));
+        }
+        for i in 1..200 {
+            j.add_extension(TlsExtensionType(i));
+        }
+
+        let mut s = j.get_hash();
+        s.truncate(10);
+        assert_eq!(s, "t00i999900");
+    }
+
+    #[test]
+    fn test_short_alpn() {
+        let mut j = JA4::new();
+
+        j.set_alpn("a".as_bytes());
+        let mut s = j.get_hash();
+        s.truncate(10);
+        assert_eq!(s, "t00i000000");
+
+        j.set_alpn("aa".as_bytes());
+        let mut s = j.get_hash();
+        s.truncate(10);
+        assert_eq!(s, "t00i0000aa");
+    }
+
+    #[test]
+    fn test_get_hash() {
+        let mut j = JA4::new();
+
+        // the empty JA4 hash
+        let s = j.get_hash();
+        assert_eq!(s, "t00i000000_e3b0c44298fc_d2e2adf7177b");
+
+        // set TLS version
+        j.set_tls_version(TlsVersion::Tls12);
+        let s = j.get_hash();
+        assert_eq!(s, "t12i000000_e3b0c44298fc_d2e2adf7177b");
+
+        // set QUIC
+        j.set_quic();
+        let s = j.get_hash();
+        assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b");
+
+        // set GREASE extension, should be ignored
+        j.add_extension(TlsExtensionType(0x0a0a));
+        let s = j.get_hash();
+        assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b");
+
+        // set SNI extension, should only increase count and change i->d
+        j.add_extension(TlsExtensionType(0x0000));
+        let s = j.get_hash();
+        assert_eq!(s, "q12d000100_e3b0c44298fc_d2e2adf7177b");
+
+        // set ALPN extension, should only increase count and set end of JA4_a
+        j.set_alpn(b"h3-16");
+        j.add_extension(TlsExtensionType::ApplicationLayerProtocolNegotiation);
+        let s = j.get_hash();
+        assert_eq!(s, "q12d0002h6_e3b0c44298fc_d2e2adf7177b");
+
+        // set some ciphers
+        j.add_cipher_suite(TlsCipherSuiteID(0x1111));
+        j.add_cipher_suite(TlsCipherSuiteID(0x0a20));
+        j.add_cipher_suite(TlsCipherSuiteID(0xbada));
+        let s = j.get_hash();
+        assert_eq!(s, "q12d0302h6_f500716053f9_d2e2adf7177b");
+
+        // set some extensions and signature algorithms
+        j.add_extension(TlsExtensionType(0xface));
+        j.add_extension(TlsExtensionType(0x0121));
+        j.add_extension(TlsExtensionType(0x1234));
+        j.add_signature_algorithm(0x6666);
+        let s = j.get_hash();
+        assert_eq!(s, "q12d0305h6_f500716053f9_2debc8880bae");
+    }
+}
index 15e21c4057d1d4505ed8d4f6d6555b8bd720db55..9e58d4d805e8a22f64e536c4c047880a95d25aca 100644 (file)
@@ -84,6 +84,8 @@ pub mod filetracker;
 pub mod kerberos;
 pub mod detect;
 
+pub mod ja4;
+
 #[cfg(feature = "lua")]
 pub mod lua;
 
index 7e9019bef0047997616d11b7fe0440fb9f4aa4fd..cd88120646ab8457f79afd891849243a5c9f06eb 100644 (file)
@@ -63,6 +63,21 @@ pub unsafe extern "C" fn rs_quic_tx_get_ja3(
     }
 }
 
+#[no_mangle]
+pub unsafe extern "C" fn rs_quic_tx_get_ja4(
+    tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
+) -> u8 {
+    if let Some(ja4) = &tx.ja4 {
+        *buffer = ja4.as_ptr();
+        *buffer_len = ja4.len() as u32;
+        1
+    } else {
+        *buffer = ptr::null();
+        *buffer_len = 0;
+        0
+    }
+}
+
 #[no_mangle]
 pub unsafe extern "C" fn rs_quic_tx_get_version(
     tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
index d432c79d12531475a551d3e248ab0985d230fb95..0266850fd38ad84555150622f2b353b51e367827 100644 (file)
@@ -16,6 +16,7 @@
  */
 
 use super::error::QuicError;
+use crate::ja4::*;
 use crate::quic::parser::quic_var_uint;
 use nom7::bytes::complete::take;
 use nom7::combinator::{all_consuming, complete};
@@ -137,6 +138,7 @@ pub(crate) struct Crypto {
     // the lifetime of TlsExtension due to references to the slice used for parsing
     pub extv: Vec<QuicTlsExtension>,
     pub ja3: Option<String>,
+    pub ja4: Option<JA4>,
 }
 
 #[derive(Debug, PartialEq)]
@@ -235,7 +237,7 @@ fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec<TlsExtension>) {
 
 // get interesting stuff out of parsed tls extensions
 fn quic_get_tls_extensions(
-    input: Option<&[u8]>, ja3: &mut String, client: bool,
+    input: Option<&[u8]>, ja3: &mut String, mut ja4: Option<&mut JA4>, client: bool,
 ) -> Vec<QuicTlsExtension> {
     let mut extv = Vec::new();
     if let Some(extr) = input {
@@ -249,8 +251,21 @@ fn quic_get_tls_extensions(
                     dash = true;
                 }
                 ja3.push_str(&u16::from(etype).to_string());
+                if let Some(ref mut ja4) = ja4 {
+                    ja4.add_extension(etype)
+                }
                 let mut values = Vec::new();
                 match e {
+                    TlsExtension::SupportedVersions(x) => {
+                        for version in x {
+                            let mut value = Vec::new();
+                            value.extend_from_slice(version.to_string().as_bytes());
+                            values.push(value);
+                            if let Some(ref mut ja4) = ja4 {
+                                ja4.set_tls_version(*version);
+                            }
+                        }
+                    }
                     TlsExtension::SNI(x) => {
                         for sni in x {
                             let mut value = Vec::new();
@@ -258,7 +273,22 @@ fn quic_get_tls_extensions(
                             values.push(value);
                         }
                     }
+                    TlsExtension::SignatureAlgorithms(x) => {
+                        for sigalgo in x {
+                            let mut value = Vec::new();
+                            value.extend_from_slice(sigalgo.to_string().as_bytes());
+                            values.push(value);
+                            if let Some(ref mut ja4) = ja4 {
+                                ja4.add_signature_algorithm(*sigalgo)
+                            }
+                        }
+                    }
                     TlsExtension::ALPN(x) => {
+                        if !x.is_empty() {
+                            if let Some(ref mut ja4) = ja4 {
+                                ja4.set_alpn(x[0]);
+                            }
+                        }
                         for alpn in x {
                             let mut value = Vec::new();
                             value.extend_from_slice(alpn);
@@ -284,6 +314,8 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                 let mut ja3 = String::with_capacity(256);
                 ja3.push_str(&u16::from(ch.version).to_string());
                 ja3.push(',');
+                let mut ja4 = JA4::new();
+                ja4.set_quic();
                 let mut dash = false;
                 for c in &ch.ciphers {
                     if dash {
@@ -292,10 +324,11 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                         dash = true;
                     }
                     ja3.push_str(&u16::from(*c).to_string());
+                    ja4.add_cipher_suite(*c);
                 }
                 ja3.push(',');
                 let ciphers = ch.ciphers;
-                let extv = quic_get_tls_extensions(ch.ext, &mut ja3, true);
+                let extv = quic_get_tls_extensions(ch.ext, &mut ja3, Some(&mut ja4), true);
                 return Some(Frame::Crypto(Crypto {
                     ciphers,
                     extv,
@@ -304,6 +337,11 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                     } else {
                         None
                     },
+                    ja4: if cfg!(feature = "ja4") {
+                        Some(ja4)
+                    } else {
+                        None
+                    },
                 }));
             }
             ServerHello(sh) => {
@@ -313,7 +351,7 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                 ja3.push_str(&u16::from(sh.cipher).to_string());
                 ja3.push(',');
                 let ciphers = vec![sh.cipher];
-                let extv = quic_get_tls_extensions(sh.ext, &mut ja3, false);
+                let extv = quic_get_tls_extensions(sh.ext, &mut ja3, None, false);
                 return Some(Frame::Crypto(Crypto {
                     ciphers,
                     extv,
@@ -322,6 +360,7 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                     } else {
                         None
                     },
+                    ja4: None,
                 }));
             }
             _ => {}
@@ -520,8 +559,7 @@ impl Frame {
             let mut d = vec![0; crypto_max_size as usize];
             for f in &frames {
                 if let Frame::CryptoFrag(c) = f {
-                    d[c.offset as usize..(c.offset + c.length) as usize]
-                        .clone_from_slice(&c.data);
+                    d[c.offset as usize..(c.offset + c.length) as usize].clone_from_slice(&c.data);
                 }
             }
             if let Ok((_, msg)) = parse_tls_message_handshake(&d) {
index e03ebdd6bf2174caecef5c4ca03960546aa8bda3..fc2e85c88a75e344aedb8c4a11561df8be568084 100644 (file)
@@ -122,6 +122,11 @@ fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonEr
         js.set_string("string", ja3)?;
         js.close()?;
     }
+
+    if let Some(ref ja4) = &tx.ja4 {
+        js.set_string("ja4", ja4)?;
+    }
+
     if !tx.extv.is_empty() {
         js.open_array("extensions")?;
         for e in &tx.extv {
index 147bcf0416b8ba1971b2f814f87ce3622a1d33b2..d0bff24cec67493306e32d41033db0e78eca8042 100644 (file)
@@ -22,7 +22,7 @@ use super::{
     parser::{quic_pkt_num, QuicData, QuicHeader, QuicType},
 };
 use crate::applayer::{self, *};
-use crate::core::{AppProto, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP, Direction};
+use crate::core::{AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP};
 use std::collections::VecDeque;
 use std::ffi::CString;
 use tls_parser::TlsExtensionType;
@@ -48,6 +48,7 @@ pub struct QuicTransaction {
     pub ua: Option<Vec<u8>>,
     pub extv: Vec<QuicTlsExtension>,
     pub ja3: Option<String>,
+    pub ja4: Option<String>,
     pub client: bool,
     tx_data: AppLayerTxData,
 }
@@ -55,9 +56,13 @@ pub struct QuicTransaction {
 impl QuicTransaction {
     fn new(
         header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>,
-        extv: Vec<QuicTlsExtension>, ja3: Option<String>, client: bool,
+        extv: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<String>, client: bool,
     ) -> Self {
-       let direction = if client { Direction::ToServer } else { Direction::ToClient };
+        let direction = if client {
+            Direction::ToServer
+        } else {
+            Direction::ToClient
+        };
         let cyu = Cyu::generate(&header, &data.frames);
         QuicTransaction {
             tx_id: 0,
@@ -67,13 +72,18 @@ impl QuicTransaction {
             ua,
             extv,
             ja3,
+            ja4,
             client,
             tx_data: AppLayerTxData::for_direction(direction),
         }
     }
 
     fn new_empty(client: bool, header: QuicHeader) -> Self {
-       let direction = if client { Direction::ToServer } else { Direction::ToClient };
+        let direction = if client {
+            Direction::ToServer
+        } else {
+            Direction::ToClient
+        };
         QuicTransaction {
             tx_id: 0,
             header,
@@ -82,6 +92,7 @@ impl QuicTransaction {
             ua: None,
             extv: Vec::new(),
             ja3: None,
+            ja4: None,
             client,
             tx_data: AppLayerTxData::for_direction(direction),
         }
@@ -132,9 +143,9 @@ impl QuicState {
 
     fn new_tx(
         &mut self, header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>,
-        extb: Vec<QuicTlsExtension>, ja3: Option<String>, client: bool,
+        extb: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<String>, client: bool,
     ) {
-        let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, client);
+        let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, ja4, client);
         self.max_tx_id += 1;
         tx.tx_id = self.max_tx_id;
         self.transactions.push_back(tx);
@@ -212,6 +223,7 @@ impl QuicState {
         let mut sni: Option<Vec<u8>> = None;
         let mut ua: Option<Vec<u8>> = None;
         let mut ja3: Option<String> = None;
+        let mut ja4: Option<String> = None;
         let mut extv: Vec<QuicTlsExtension> = Vec::new();
         for frame in &data.frames {
             match frame {
@@ -233,6 +245,14 @@ impl QuicState {
                     if let Some(ja3str) = &c.ja3 {
                         ja3 = Some(ja3str.clone());
                     }
+                    // we only do client fingerprints for now
+                    if to_server {
+                        // our hash is complete, let's only use strings from
+                        // now on
+                        if let Some(ref rja4) = c.ja4 {
+                            ja4 = Some(rja4.get_hash());
+                        }
+                    }
                     for e in &c.extv {
                         if e.etype == TlsExtensionType::ServerName && !e.values.is_empty() {
                             sni = Some(e.values[0].to_vec());
@@ -248,7 +268,7 @@ impl QuicState {
                 _ => {}
             }
         }
-        self.new_tx(header, data, sni, ua, extv, ja3, to_server);
+        self.new_tx(header, data, sni, ua, extv, ja3, ja4, to_server);
     }
 
     fn set_event_notx(&mut self, event: QuicEvent, header: QuicHeader, client: bool) {
@@ -305,6 +325,7 @@ impl QuicState {
                             None,
                             Vec::new(),
                             None,
+                            None,
                             to_server,
                         );
                         continue;
index d885a8b7ea6abccae61edc118c09212ee8de2417..582be4585c2b832231389a3b56164fe9ce6990ce 100755 (executable)
@@ -229,6 +229,7 @@ noinst_HEADERS = \
        detect-ipv6hdr.h \
        detect-isdataat.h \
        detect-itype.h \
+       detect-ja4-hash.h \
        detect-krb5-cname.h \
        detect-krb5-errcode.h \
        detect-krb5-msgtype.h \
@@ -557,6 +558,7 @@ noinst_HEADERS = \
        util-ioctl.h \
        util-ip.h \
        util-ja3.h \
+       util-ja4.h \
        util-landlock.h \
        util-logopenfile.h \
        util-log-redis.h \
@@ -845,6 +847,7 @@ libsuricata_c_a_SOURCES = \
        detect-ipv6hdr.c \
        detect-isdataat.c \
        detect-itype.c \
+       detect-ja4-hash.c \
        detect-krb5-cname.c \
        detect-krb5-errcode.c \
        detect-krb5-msgtype.c \
index 2bde7de096e27d6b8155f539326e066abdc9b71a..e5c1ed1eb0fbeb7f6241f9ab8803c7cada77d321 100644 (file)
@@ -145,8 +145,9 @@ enum {
     ERR_EXTRACT_VALIDITY,
 };
 
-/* JA3 fingerprints are disabled by default */
+/* JA3 and JA4 fingerprints are disabled by default */
 #define SSL_CONFIG_DEFAULT_JA3 0
+#define SSL_CONFIG_DEFAULT_JA4 0
 
 enum SslConfigEncryptHandling {
     SSL_CNF_ENC_HANDLE_DEFAULT = 0, /**< disable raw content, continue tracking */
@@ -156,10 +157,12 @@ enum SslConfigEncryptHandling {
 
 typedef struct SslConfig_ {
     enum SslConfigEncryptHandling encrypt_mode;
-    /** dynamic setting for ja3: can be enabled on demand if not explicitly
-     *  disabled. */
+    /** dynamic setting for ja3 and ja4: can be enabled on demand if not
+     *  explicitly disabled. */
     SC_ATOMIC_DECLARE(int, enable_ja3);
     bool disable_ja3; /**< ja3 explicitly disabled. Don't enable on demand. */
+    SC_ATOMIC_DECLARE(int, enable_ja4);
+    bool disable_ja4; /**< ja4 explicitly disabled. Don't enable on demand. */
 } SslConfig;
 
 SslConfig ssl_config;
@@ -693,6 +696,11 @@ static inline int TLSDecodeHSHelloVersion(SSLState *ssl_state,
     uint16_t version = (uint16_t)(*input << 8) | *(input + 1);
     ssl_state->curr_connp->version = version;
 
+    if (ssl_state->curr_connp->ja4 != NULL &&
+            ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+        SCJA4SetTLSVersion(ssl_state->curr_connp->ja4, version);
+    }
+
     /* TLSv1.3 draft1 to draft21 use the version field as earlier TLS
        versions, instead of using the supported versions extension. */
     if ((ssl_state->current_flags & SSL_AL_FLAG_STATE_SERVER_HELLO) &&
@@ -836,17 +844,25 @@ static inline int TLSDecodeHSHelloCipherSuites(SSLState *ssl_state,
         goto invalid_length;
     }
 
-    if (SC_ATOMIC_GET(ssl_config.enable_ja3)) {
-        JA3Buffer *ja3_cipher_suites = Ja3BufferInit();
-        if (ja3_cipher_suites == NULL)
-            return -1;
+    const bool enable_ja3 = SC_ATOMIC_GET(ssl_config.enable_ja3);
+
+    if (enable_ja3 || SC_ATOMIC_GET(ssl_config.enable_ja4)) {
+        JA3Buffer *ja3_cipher_suites = NULL;
+
+        if (enable_ja3) {
+            ja3_cipher_suites = Ja3BufferInit();
+            if (ja3_cipher_suites == NULL)
+                return -1;
+        }
 
         uint16_t processed_len = 0;
         /* coverity[tainted_data] */
         while (processed_len < cipher_suites_length)
         {
             if (!(HAS_SPACE(2))) {
-                Ja3BufferFree(&ja3_cipher_suites);
+                if (enable_ja3) {
+                    Ja3BufferFree(&ja3_cipher_suites);
+                }
                 goto invalid_length;
             }
 
@@ -854,19 +870,25 @@ static inline int TLSDecodeHSHelloCipherSuites(SSLState *ssl_state,
             input += 2;
 
             if (TLSDecodeValueIsGREASE(cipher_suite) != 1) {
-                int rc = Ja3BufferAddValue(&ja3_cipher_suites, cipher_suite);
-                if (rc != 0) {
-                    return -1;
+                if (ssl_state->curr_connp->ja4 != NULL &&
+                        ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+                    SCJA4AddCipher(ssl_state->curr_connp->ja4, cipher_suite);
+                }
+                if (enable_ja3) {
+                    int rc = Ja3BufferAddValue(&ja3_cipher_suites, cipher_suite);
+                    if (rc != 0) {
+                        return -1;
+                    }
                 }
             }
-
             processed_len += 2;
         }
 
-        int rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str,
-                                   &ja3_cipher_suites);
-        if (rc == -1) {
-            return -1;
+        if (enable_ja3) {
+            int rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, &ja3_cipher_suites);
+            if (rc == -1) {
+                return -1;
+            }
         }
 
     } else {
@@ -1027,6 +1049,10 @@ static inline int TLSDecodeHSHelloExtensionSupportedVersions(SSLState *ssl_state
             uint16_t ver = (uint16_t)(input[i] << 8) | input[i + 1];
             if (TLSVersionValid(ver)) {
                 ssl_state->curr_connp->version = ver;
+                if (ssl_state->curr_connp->ja4 != NULL &&
+                        ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+                    SCJA4SetTLSVersion(ssl_state->curr_connp->ja4, ver);
+                }
                 break;
             }
             i += 2;
@@ -1173,6 +1199,113 @@ invalid_length:
     return -1;
 }
 
+static inline int TLSDecodeHSHelloExtensionSigAlgorithms(
+        SSLState *ssl_state, const uint8_t *const initial_input, const uint32_t input_len)
+{
+    const uint8_t *input = initial_input;
+
+    /* Empty extension */
+    if (input_len == 0)
+        return 0;
+
+    if (!(HAS_SPACE(2)))
+        goto invalid_length;
+
+    uint16_t sigalgo_len = (uint16_t)(*input << 8) | *(input + 1);
+    input += 2;
+
+    /* Signature algorithms length should always be divisible by 2 */
+    if ((sigalgo_len % 2) != 0) {
+        goto invalid_length;
+    }
+
+    if (!(HAS_SPACE(sigalgo_len)))
+        goto invalid_length;
+
+    if (ssl_state->curr_connp->ja4 != NULL &&
+            ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+        uint16_t sigalgo_processed_len = 0;
+        while (sigalgo_processed_len < sigalgo_len) {
+            uint16_t sigalgo = (uint16_t)(*input << 8) | *(input + 1);
+            input += 2;
+            sigalgo_processed_len += 2;
+
+            SCJA4AddSigAlgo(ssl_state->curr_connp->ja4, sigalgo);
+        }
+    } else {
+        /* Skip signature algorithms */
+        input += sigalgo_len;
+    }
+
+    return (input - initial_input);
+
+invalid_length:
+    SCLogDebug("Signature algorithm list invalid length");
+    SSLSetEvent(ssl_state, TLS_DECODER_EVENT_HANDSHAKE_INVALID_LENGTH);
+
+    return -1;
+}
+
+static inline int TLSDecodeHSHelloExtensionALPN(
+        SSLState *ssl_state, const uint8_t *const initial_input, const uint32_t input_len)
+{
+    const uint8_t *input = initial_input;
+
+    /* Empty extension */
+    if (input_len == 0)
+        return 0;
+
+    if (!(HAS_SPACE(2)))
+        goto invalid_length;
+
+    uint16_t alpn_len = (uint16_t)(*input << 8) | *(input + 1);
+    input += 2;
+
+    if (!(HAS_SPACE(alpn_len)))
+        goto invalid_length;
+
+    if (ssl_state->curr_connp->ja4 != NULL &&
+            ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+        /* We use 32 bits here to avoid potentially overflowing a value that
+           needs to be compared to an unsigned 16-bit value. */
+        uint32_t alpn_processed_len = 0;
+        while (alpn_processed_len < alpn_len) {
+            uint8_t protolen = *input;
+            input += 1;
+            alpn_processed_len += 1;
+
+            if (!(HAS_SPACE(protolen)))
+                goto invalid_length;
+
+            /* Check if reading another protolen bytes would exceed the
+               overall ALPN length; if so, skip and continue */
+            if (alpn_processed_len + protolen > ((uint32_t)alpn_len)) {
+                input += alpn_len - alpn_processed_len;
+                break;
+            }
+
+            /* Only record the first value for JA4 */
+            if (alpn_processed_len == 1) {
+                SCJA4SetALPN(ssl_state->curr_connp->ja4, (const char *)input, protolen);
+            }
+
+            alpn_processed_len += protolen;
+            input += protolen;
+        }
+    } else {
+        /* Skip ALPN protocols */
+        input += alpn_len;
+    }
+
+    return (input - initial_input);
+
+invalid_length:
+    SCLogDebug("ALPN list invalid length");
+    SSLSetEvent(ssl_state, TLS_DECODER_EVENT_HANDSHAKE_INVALID_LENGTH);
+
+    return -1;
+}
+
 static inline int TLSDecodeHSHelloExtensions(SSLState *ssl_state,
                                          const uint8_t * const initial_input,
                                          const uint32_t input_len)
@@ -1274,6 +1407,28 @@ static inline int TLSDecodeHSHelloExtensions(SSLState *ssl_state,
                 break;
             }
 
+            case SSL_EXTENSION_SIGNATURE_ALGORITHMS: {
+                /* coverity[tainted_data] */
+                ret = TLSDecodeHSHelloExtensionSigAlgorithms(ssl_state, input, ext_len);
+                if (ret < 0)
+                    goto end;
+
+                input += ret;
+
+                break;
+            }
+
+            case SSL_EXTENSION_ALPN: {
+                /* coverity[tainted_data] */
+                ret = TLSDecodeHSHelloExtensionALPN(ssl_state, input, ext_len);
+                if (ret < 0)
+                    goto end;
+
+                input += ext_len;
+
+                break;
+            }
+
             case SSL_EXTENSION_EARLY_DATA:
             {
                 if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
@@ -1327,6 +1482,13 @@ static inline int TLSDecodeHSHelloExtensions(SSLState *ssl_state,
             }
         }
 
+        if (ssl_state->curr_connp->ja4 != NULL &&
+                ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+            if (TLSDecodeValueIsGREASE(ext_type) != 1) {
+                SCJA4AddExtension(ssl_state->curr_connp->ja4, ext_type);
+            }
+        }
+
         processed_len += ext_len + 4;
     }
 
@@ -1375,6 +1537,15 @@ static int TLSDecodeHandshakeHello(SSLState *ssl_state,
     int ret;
     uint32_t parsed = 0;
 
+    /* Ensure that we have a JA4 state defined by now if we have JA4 enabled,
+       we are in a client hello and we don't have such a state yet (to avoid
+       leaking memory in case this function is entered more than once). */
+    if (SC_ATOMIC_GET(ssl_config.enable_ja4) &&
+            ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO &&
+            ssl_state->curr_connp->ja4 == NULL) {
+        ssl_state->curr_connp->ja4 = SCJA4New();
+    }
+
     ret = TLSDecodeHSHelloVersion(ssl_state, input, input_len);
     if (ret < 0)
         goto end;
@@ -2699,6 +2870,8 @@ static void SSLStateFree(void *p)
     if (ssl_state->server_connp.session_id)
         SCFree(ssl_state->server_connp.session_id);
 
+    if (ssl_state->client_connp.ja4)
+        SCJA4Free(ssl_state->client_connp.ja4);
     if (ssl_state->client_connp.ja3_str)
         Ja3BufferFree(&ssl_state->client_connp.ja3_str);
     if (ssl_state->client_connp.ja3_hash)
@@ -2971,12 +3144,37 @@ static void CheckJA3Enabled(void)
     SC_ATOMIC_SET(ssl_config.enable_ja3, enable_ja3);
     if (!ssl_config.disable_ja3 && !g_disable_hashing) {
         /* The feature is available, i.e. _could_ be activated by a rule or
-           even is enabled in the configuration. */
+            even is enabled in the configuration. */
         ProvidesFeature(FEATURE_JA3);
     }
 }
 #endif /* HAVE_JA3 */
 
+#ifdef HAVE_JA4
+static void CheckJA4Enabled(void)
+{
+    const char *strval = NULL;
+    /* Check if we should generate JA4 fingerprints */
+    int enable_ja4 = SSL_CONFIG_DEFAULT_JA4;
+    if (ConfGet("app-layer.protocols.tls.ja4-fingerprints", &strval) != 1) {
+        enable_ja4 = SSL_CONFIG_DEFAULT_JA4;
+    } else if (strcmp(strval, "auto") == 0) {
+        enable_ja4 = SSL_CONFIG_DEFAULT_JA4;
+    } else if (ConfValIsFalse(strval)) {
+        enable_ja4 = 0;
+        ssl_config.disable_ja4 = true;
+    } else if (ConfValIsTrue(strval)) {
+        enable_ja4 = true;
+    }
+    SC_ATOMIC_SET(ssl_config.enable_ja4, enable_ja4);
+    if (!ssl_config.disable_ja4 && !g_disable_hashing) {
+        /* The feature is available, i.e. _could_ be activated by a rule or
+            even is enabled in the configuration. */
+        ProvidesFeature(FEATURE_JA4);
+    }
+}
+#endif /* HAVE_JA4 */
+
 /**
  * \brief Function to register the SSL protocol parser and other functions
  */
@@ -3079,17 +3277,27 @@ void RegisterSSLParsers(void)
 #ifdef HAVE_JA3
         CheckJA3Enabled();
 #endif /* HAVE_JA3 */
+#ifdef HAVE_JA4
+        CheckJA4Enabled();
+#endif /* HAVE_JA4 */
 
         if (g_disable_hashing) {
             if (SC_ATOMIC_GET(ssl_config.enable_ja3)) {
                 SCLogWarning("MD5 calculation has been disabled, disabling JA3");
                 SC_ATOMIC_SET(ssl_config.enable_ja3, 0);
             }
+            if (SC_ATOMIC_GET(ssl_config.enable_ja4)) {
+                SCLogWarning("Hashing has been disabled, disabling JA4");
+                SC_ATOMIC_SET(ssl_config.enable_ja4, 0);
+            }
         } else {
             if (RunmodeIsUnittests()) {
 #ifdef HAVE_JA3
                 SC_ATOMIC_SET(ssl_config.enable_ja3, 1);
 #endif /* HAVE_JA3 */
+#ifdef HAVE_JA4
+                SC_ATOMIC_SET(ssl_config.enable_ja4, 1);
+#endif /* HAVE_JA4 */
             }
         }
     } else {
@@ -3117,10 +3325,45 @@ void SSLEnableJA3(void)
     SC_ATOMIC_SET(ssl_config.enable_ja3, 1);
 }
 
-bool SSLJA3IsEnabled(void)
+/**
+ * \brief if not explicitly disabled in config, enable ja4 support
+ *
+ * Implemented using atomic to allow rule reloads to do this at
+ * runtime.
+ */
+void SSLEnableJA4(void)
 {
-    if (SC_ATOMIC_GET(ssl_config.enable_ja3)) {
-        return true;
+    if (g_disable_hashing || ssl_config.disable_ja4) {
+        return;
     }
-    return false;
+    if (SC_ATOMIC_GET(ssl_config.enable_ja4)) {
+        return;
+    }
+    SC_ATOMIC_SET(ssl_config.enable_ja4, 1);
+}
+
+/**
+ * \brief return whether ja3 is effectively enabled
+ *
+ * This means that it either has been enabled explicitly or has been
+ * enabled by having loaded a rule while not being explicitly disabled.
+ *
+ * \retval true if enabled, false otherwise
+ */
+bool SSLJA3IsEnabled(void)
+{
+    return SC_ATOMIC_GET(ssl_config.enable_ja3);
+}
+
+/**
+ * \brief return whether ja4 is effectively enabled
+ *
+ * This means that it either has been enabled explicitly or has been
+ * enabled by having loaded a rule while not being explicitly disabled.
+ *
+ * \retval true if enabled, false otherwise
+ */
+bool SSLJA4IsEnabled(void)
+{
+    return SC_ATOMIC_GET(ssl_config.enable_ja4);
 }
index f2e42622308e1bff40214f3d124b929b27774e9e..09d975ae55d73b707042f5f2391063cf9d4d9e55 100644 (file)
@@ -141,6 +141,8 @@ enum {
 #define SSL_EXTENSION_SNI                       0x0000
 #define SSL_EXTENSION_ELLIPTIC_CURVES           0x000a
 #define SSL_EXTENSION_EC_POINT_FORMATS          0x000b
+#define SSL_EXTENSION_SIGNATURE_ALGORITHMS      0x000d
+#define SSL_EXTENSION_ALPN                      0x0010
 #define SSL_EXTENSION_SESSION_TICKET            0x0023
 #define SSL_EXTENSION_EARLY_DATA                0x002a
 #define SSL_EXTENSION_SUPPORTED_VERSIONS        0x002b
@@ -267,6 +269,8 @@ typedef struct SSLStateConnp_ {
     JA3Buffer *ja3_str;
     char *ja3_hash;
 
+    JA4 *ja4;
+
     /* handshake tls fragmentation buffer. Handshake messages can be fragmented over multiple
      * TLS records. */
     uint8_t *hs_buffer;
@@ -307,5 +311,7 @@ void RegisterSSLParsers(void);
 void SSLVersionToString(uint16_t, char *);
 void SSLEnableJA3(void);
 bool SSLJA3IsEnabled(void);
+void SSLEnableJA4(void);
+bool SSLJA4IsEnabled(void);
 
 #endif /* __APP_LAYER_SSL_H__ */
index 1437f1a0a7056a2f52993d530955950d7ec764b0..62e314cc3f5ff07c2b6c3d4ad0aa9e88ae7669d3 100644 (file)
 #include "detect-quic-version.h"
 #include "detect-quic-cyu-hash.h"
 #include "detect-quic-cyu-string.h"
+#include "detect-ja4-hash.h"
 
 #include "detect-bypass.h"
 #include "detect-ftpdata.h"
@@ -688,6 +689,7 @@ void SigTableSetup(void)
     DetectQuicVersionRegister();
     DetectQuicCyuHashRegister();
     DetectQuicCyuStringRegister();
+    DetectJa4HashRegister();
 
     DetectBypassRegister();
     DetectConfigRegister();
index df0afcc98abd2389e8e77dd39772d8598c571557..f7d959151d5d8ff2143d7ad8966c3880b550c1c8 100644 (file)
@@ -338,6 +338,8 @@ enum DetectKeywordId {
     DETECT_AL_IKE_NONCE,
     DETECT_AL_IKE_KEY_EXCHANGE,
 
+    DETECT_AL_JA4_HASH,
+
     /* make sure this stays last */
     DETECT_TBLSIZE,
 };
diff --git a/src/detect-ja4-hash.c b/src/detect-ja4-hash.c
new file mode 100644 (file)
index 0000000..c029b6d
--- /dev/null
@@ -0,0 +1,173 @@
+/* Copyright (C) 2023 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+/**
+ * \file
+ *
+ * \author Sascha Steinbiss <sascha@steinbiss.name>
+ *
+ * Implements support for ja4.hash keyword.
+ */
+
+#include "suricata-common.h"
+#include "threads.h"
+#include "decode.h"
+#include "detect.h"
+
+#include "detect-parse.h"
+#include "detect-engine.h"
+#include "detect-engine-mpm.h"
+#include "detect-engine-prefilter.h"
+#include "detect-ja4-hash.h"
+
+#include "util-ja4.h"
+
+#include "app-layer-ssl.h"
+
+#ifndef HAVE_JA4
+static int DetectJA4SetupNoSupport(DetectEngineCtx *a, Signature *b, const char *c)
+{
+    SCLogError("no JA4 support built in");
+    return -1;
+}
+#endif /* HAVE_JA4 */
+
+static int DetectJa4HashSetup(DetectEngineCtx *, Signature *, const char *);
+static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx,
+        const DetectEngineTransforms *transforms, Flow *f, const uint8_t flow_flags, void *txv,
+        const int list_id);
+int Ja4IsDisabled(const char *type);
+static InspectionBuffer *Ja4DetectGetHash(DetectEngineThreadCtx *det_ctx,
+        const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv,
+        const int list_id);
+
+static int g_ja4_hash_buffer_id = 0;
+
+/**
+ * \brief Registration function for keyword: ja4.hash
+ */
+void DetectJa4HashRegister(void)
+{
+    sigmatch_table[DETECT_AL_JA4_HASH].name = "ja4.hash";
+    sigmatch_table[DETECT_AL_JA4_HASH].alias = "ja4_hash";
+    sigmatch_table[DETECT_AL_JA4_HASH].desc = "sticky buffer to match the JA4 hash buffer";
+    sigmatch_table[DETECT_AL_JA4_HASH].url = "/rules/ja4-keywords.html#ja4-hash";
+#ifdef HAVE_JA4
+    sigmatch_table[DETECT_AL_JA4_HASH].Setup = DetectJa4HashSetup;
+#else  /* HAVE_JA4 */
+    sigmatch_table[DETECT_AL_JA4_HASH].Setup = DetectJA4SetupNoSupport;
+#endif /* HAVE_JA4 */
+    sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_NOOPT;
+    sigmatch_table[DETECT_AL_JA4_HASH].flags |= SIGMATCH_INFO_STICKY_BUFFER;
+
+#ifdef HAVE_JA4
+    DetectAppLayerInspectEngineRegister2("ja4.hash", ALPROTO_TLS, SIG_FLAG_TOSERVER, 0,
+            DetectEngineInspectBufferGeneric, GetData);
+
+    DetectAppLayerMpmRegister2(
+            "ja4.hash", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister, GetData, ALPROTO_TLS, 0);
+
+    DetectAppLayerMpmRegister2("ja4.hash", SIG_FLAG_TOSERVER, 2, PrefilterGenericMpmRegister,
+            Ja4DetectGetHash, ALPROTO_QUIC, 1);
+
+    DetectAppLayerInspectEngineRegister2("ja4.hash", ALPROTO_QUIC, SIG_FLAG_TOSERVER, 1,
+            DetectEngineInspectBufferGeneric, Ja4DetectGetHash);
+
+    DetectBufferTypeSetDescriptionByName("ja4.hash", "TLS JA4 hash");
+
+    g_ja4_hash_buffer_id = DetectBufferTypeGetByName("ja4.hash");
+#endif /* HAVE_JA4 */
+}
+
+/**
+ * \brief this function setup the ja4.hash modifier keyword used in the rule
+ *
+ * \param de_ctx Pointer to the Detection Engine Context
+ * \param s      Pointer to the Signature to which the current keyword belongs
+ * \param str    Should hold an empty string always
+ *
+ * \retval 0  On success
+ * \retval -1 On failure
+ */
+static int DetectJa4HashSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str)
+{
+    if (DetectBufferSetActiveList(de_ctx, s, g_ja4_hash_buffer_id) < 0)
+        return -1;
+
+    if (s->alproto != ALPROTO_UNKNOWN && s->alproto != ALPROTO_TLS && s->alproto != ALPROTO_QUIC) {
+        SCLogError("rule contains conflicting protocols.");
+        return -1;
+    }
+
+    /* try to enable JA4 */
+    SSLEnableJA4();
+
+    /* check if JA4 enabling had an effect */
+    if (!RunmodeIsUnittests() && !SSLJA4IsEnabled()) {
+        if (!SigMatchSilentErrorEnabled(de_ctx, DETECT_AL_JA4_HASH)) {
+            SCLogError("JA4 support is not enabled");
+        }
+        return -2;
+    }
+    s->init_data->init_flags |= SIG_FLAG_INIT_JA;
+
+    return 0;
+}
+
+static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx,
+        const DetectEngineTransforms *transforms, Flow *f, const uint8_t flow_flags, void *txv,
+        const int list_id)
+{
+    InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id);
+    if (buffer->inspect == NULL) {
+        const SSLState *ssl_state = (SSLState *)f->alstate;
+
+        if (ssl_state->client_connp.ja4 == NULL) {
+            return NULL;
+        }
+
+        uint8_t data[JA4_HEX_LEN];
+        SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])data);
+
+        InspectionBufferSetup(det_ctx, list_id, buffer, data, 0);
+        InspectionBufferCopy(buffer, data, JA4_HEX_LEN);
+        InspectionBufferApplyTransforms(buffer, transforms);
+    }
+
+    return buffer;
+}
+
+static InspectionBuffer *Ja4DetectGetHash(DetectEngineThreadCtx *det_ctx,
+        const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv,
+        const int list_id)
+{
+    InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id);
+    if (buffer->inspect == NULL) {
+        uint32_t b_len = 0;
+        const uint8_t *b = NULL;
+
+        if (rs_quic_tx_get_ja4(txv, &b, &b_len) != 1)
+            return NULL;
+        if (b == NULL || b_len == 0)
+            return NULL;
+
+        InspectionBufferSetup(det_ctx, list_id, buffer, NULL, 0);
+        InspectionBufferCopy(buffer, (uint8_t *)b, JA4_HEX_LEN);
+        InspectionBufferApplyTransforms(buffer, transforms);
+    }
+    return buffer;
+}
diff --git a/src/detect-ja4-hash.h b/src/detect-ja4-hash.h
new file mode 100644 (file)
index 0000000..f3a5782
--- /dev/null
@@ -0,0 +1,30 @@
+/* Copyright (C) 2023 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+/**
+ * \file
+ *
+ * \author Sascha Steinbiss <sascha@steinbiss.name>
+ */
+
+#ifndef __DETECT_JA4_HASH_H__
+#define __DETECT_JA4_HASH_H__
+
+/* Prototypes */
+void DetectJa4HashRegister(void);
+
+#endif /* __DETECT_JA4_HASH_H__ */
index 5dee7e6bc2c70310eec2c26d0d793fb1f1e3e896..a2e7158bda142648a1e52768da5f44563c7cf547 100644 (file)
@@ -2114,9 +2114,9 @@ static int SigValidate(DetectEngineCtx *de_ctx, Signature *s)
     DetectLuaPostSetup(s);
 #endif
 
-    if (s->init_data->init_flags & SIG_FLAG_INIT_JA3 && s->alproto != ALPROTO_UNKNOWN &&
+    if ((s->init_data->init_flags & SIG_FLAG_INIT_JA) && s->alproto != ALPROTO_UNKNOWN &&
             s->alproto != ALPROTO_TLS && s->alproto != ALPROTO_QUIC) {
-        SCLogError("Cannot have ja3 with protocol %s.", AppProtoToString(s->alproto));
+        SCLogError("Cannot have ja3/ja4 with protocol %s.", AppProtoToString(s->alproto));
         SCReturnInt(0);
     }
     if ((s->flags & SIG_FLAG_FILESTORE) || s->file_flags != 0 ||
index 8ac08a0807ea467f47916be8db30ce2388587d87..2270896c915ddb25ca447d83e18209ea8fb73fd6 100644 (file)
@@ -148,7 +148,7 @@ static int DetectTlsJa3HashSetup(DetectEngineCtx *de_ctx, Signature *s, const ch
         }
         return -2;
     }
-    s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+    s->init_data->init_flags |= SIG_FLAG_INIT_JA;
 
     return 0;
 }
index 5f718ac352e7f931d3e09aaf9312eb8c86827904..0b0050f9d66f1f753a3b2ab29749117d74a6f9ab 100644 (file)
@@ -137,7 +137,7 @@ static int DetectTlsJa3StringSetup(DetectEngineCtx *de_ctx, Signature *s, const
         }
         return -2;
     }
-    s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+    s->init_data->init_flags |= SIG_FLAG_INIT_JA;
 
     return 0;
 }
index 4e352c0c9d270d843c22ec749e926b8e9cff8a4b..cecc98a2c1b23d061199d5f78b442070e72f556c 100644 (file)
@@ -146,7 +146,7 @@ static int DetectTlsJa3SHashSetup(DetectEngineCtx *de_ctx, Signature *s, const c
         }
         return -2;
     }
-    s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+    s->init_data->init_flags |= SIG_FLAG_INIT_JA;
 
     return 0;
 }
index bd4ceb556a9d26206966729e53a547bb4ef507af..d1d8e243c6c7947016d3966fb6e22352758725ac 100644 (file)
@@ -137,7 +137,7 @@ static int DetectTlsJa3SStringSetup(DetectEngineCtx *de_ctx, Signature *s, const
         }
         return -2;
     }
-    s->init_data->init_flags |= SIG_FLAG_INIT_JA3;
+    s->init_data->init_flags |= SIG_FLAG_INIT_JA;
 
     return 0;
 }
index c26cdeaeb97edc568c3c741d9184407e4afcc7e5..4ea495b254ea1f2072199acdb9e46126cae1e2ad 100644 (file)
@@ -289,7 +289,7 @@ typedef struct DetectPort_ {
 #define SIG_FLAG_INIT_PRIO_EXPLICIT                                                                \
     BIT_U32(8) /**< priority is explicitly set by the priority keyword */
 #define SIG_FLAG_INIT_FILEDATA              BIT_U32(9)  /**< signature has filedata keyword */
-#define SIG_FLAG_INIT_JA3                   BIT_U32(10) /**< signature has ja3 keyword */
+#define SIG_FLAG_INIT_JA                    BIT_U32(10) /**< signature has ja3/ja4 keyword */
 #define SIG_FLAG_INIT_OVERFLOW              BIT_U32(11) /**< signature has overflown buffers */
 
 /* signature mask flags */
index 40d6736718b67a4bfbfb14718192ec98e76dcf12..a1420f4a73ab35649d3bb72d43246ffe748f8d28 100644 (file)
@@ -27,6 +27,7 @@
 /* Provided feature names */
 #define FEATURE_OUTPUT_FILESTORE    "output::file-store"
 #define FEATURE_JA3                 "ja3"
+#define FEATURE_JA4                 "ja4"
 
 void ProvidesFeature(const char *);
 bool RequiresFeature(const char *);
index 9771f4d1cd7c0824e08dbc99e3c8e4f47d17248e..f1134633c394a05994f938f2fc118084684c2cb7 100644 (file)
@@ -46,6 +46,7 @@
 
 #include "util-logopenfile.h"
 #include "util-ja3.h"
+#include "util-ja4.h"
 
 #include "output-json.h"
 #include "output-json-tls.h"
@@ -76,6 +77,7 @@ SC_ATOMIC_EXTERN(unsigned int, cert_id);
 #define LOG_TLS_FIELD_CLIENT            (1 << 13) /**< client fields (issuer, subject, etc) */
 #define LOG_TLS_FIELD_CLIENT_CERT       (1 << 14)
 #define LOG_TLS_FIELD_CLIENT_CHAIN      (1 << 15)
+#define LOG_TLS_FIELD_JA4               (1 << 16)
 
 typedef struct {
     const char *name;
@@ -90,7 +92,7 @@ TlsFields tls_fields[] = { { "version", LOG_TLS_FIELD_VERSION },
     { "chain", LOG_TLS_FIELD_CHAIN }, { "session_resumed", LOG_TLS_FIELD_SESSION_RESUMED },
     { "ja3", LOG_TLS_FIELD_JA3 }, { "ja3s", LOG_TLS_FIELD_JA3S },
     { "client", LOG_TLS_FIELD_CLIENT }, { "client_certificate", LOG_TLS_FIELD_CLIENT_CERT },
-    { "client_chain", LOG_TLS_FIELD_CLIENT_CHAIN }, { NULL, -1 } };
+    { "client_chain", LOG_TLS_FIELD_CLIENT_CHAIN }, { "ja4", LOG_TLS_FIELD_JA4 }, { NULL, -1 } };
 
 typedef struct OutputTlsCtx_ {
     uint32_t flags;  /** Store mode */
@@ -210,6 +212,16 @@ static void JsonTlsLogJa3(JsonBuilder *js, SSLState *ssl_state)
     }
 }
 
+static void JsonTlsLogSCJA4(JsonBuilder *js, SSLState *ssl_state)
+{
+    if (ssl_state->client_connp.ja4 != NULL) {
+        uint8_t buffer[JA4_HEX_LEN];
+        /* JA4 hash has 36 characters */
+        SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])buffer);
+        jb_set_string_from_bytes(js, "ja4", buffer, 36);
+    }
+}
+
 static void JsonTlsLogJa3SHash(JsonBuilder *js, SSLState *ssl_state)
 {
     if (ssl_state->server_connp.ja3_hash != NULL) {
@@ -381,6 +393,10 @@ static void JsonTlsLogJSONCustom(OutputTlsCtx *tls_ctx, JsonBuilder *js,
     if (tls_ctx->fields & LOG_TLS_FIELD_JA3S)
         JsonTlsLogJa3S(js, ssl_state);
 
+    /* tls ja4 */
+    if (tls_ctx->fields & LOG_TLS_FIELD_JA4)
+        JsonTlsLogSCJA4(js, ssl_state);
+
     if (tls_ctx->fields & LOG_TLS_FIELD_CLIENT) {
         const bool log_cert = (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CERT) != 0;
         const bool log_chain = (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CHAIN) != 0;
@@ -420,6 +436,9 @@ void JsonTlsLogJSONExtended(JsonBuilder *tjs, SSLState * state)
     /* tls ja3s */
     JsonTlsLogJa3S(tjs, state);
 
+    /* tls ja4 */
+    JsonTlsLogSCJA4(tjs, state);
+
     if (HasClientCert(&state->client_connp)) {
         jb_open_object(tjs, "client");
         JsonTlsLogClientCert(tjs, &state->client_connp, false, false);
index c0b7e942c969a6c726a9ab379d30b654054af86f..5c8c7cfafeedb7c3fef1ae9292f59c06a52ce373 100644 (file)
@@ -746,6 +746,9 @@ static void PrintBuildInfo(void)
 #ifdef HAVE_JA3
     strlcat(features, "HAVE_JA3 ", sizeof(features));
 #endif
+#ifdef HAVE_JA4
+    strlcat(features, "HAVE_JA4 ", sizeof(features));
+#endif
 #ifdef HAVE_LUAJIT
     strlcat(features, "HAVE_LUAJIT ", sizeof(features));
 #endif
diff --git a/src/util-ja4.h b/src/util-ja4.h
new file mode 100644 (file)
index 0000000..769e089
--- /dev/null
@@ -0,0 +1,29 @@
+/* Copyright (C) 2024 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+/**
+ * \file
+ *
+ * \author Sascha Steinbiss <sascha@steinbiss.name>
+ */
+
+#ifndef SURICATA_UTIL_JA4_H
+#define SURICATA_UTIL_JA4_H
+
+#define JA4_HEX_LEN 36
+
+#endif /* SURICATA_UTIL_JA4_H */
index 3ead5f53dc39be84dab58256c06f24559c7a2bbc..7c8d62a6cdc798311e81a78dd788fe810483048b 100644 (file)
@@ -252,7 +252,7 @@ outputs:
             # session id
             #session-resumption: no
             # custom controls which TLS fields that are included in eve-log
-            #custom: [subject, issuer, session_resumed, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s]
+            #custom: [subject, issuer, session_resumed, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s, ja4]
         - files:
             force-magic: no   # force logging magic on all logged files
             # force logging of checksums, available hash functions are md5,
@@ -893,9 +893,10 @@ app-layer:
       detection-ports:
         dp: 443
 
-      # Generate JA3 fingerprint from client hello. If not specified it
+      # Generate JA3/JA4 fingerprints from client hello. If not specified it
       # will be disabled by default, but enabled if rules require it.
       #ja3-fingerprints: auto
+      #ja4-fingerprints: auto
 
       # What to do when the encrypted communications start:
       # - default: keep tracking TLS session, check for protocol anomalies,