]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
tls: Introduce HandshakeParams object for tracking
authorRichard McConnell <Richard_McConnell@rapid7.com>
Wed, 23 Apr 2025 14:44:09 +0000 (15:44 +0100)
committerVictor Julien <victor@inliniac.net>
Fri, 16 May 2025 19:33:54 +0000 (21:33 +0200)
Ticket: 6695

This introduction splits the use of the handshake parameters into their
own object, HandshakeParams, which is populated by the TLS decoder. The
JA4 object is now very simple. It's a simple String object (the JA4
Hash) which is generated during new().

This introduction is part of a larger idea, which is to enable
outputting these raw parameters without JA3/JA4. These handshake
parameters are the components used to generate the JA4 hash, thus it
makes sense for it to be a user of HandshakeParams.

13 files changed:
rust/src/handshake.rs [new file with mode: 0644]
rust/src/ja4.rs
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-ja4-hash.c
src/output-json-tls.c
src/util-ja4.h [deleted file]

diff --git a/rust/src/handshake.rs b/rust/src/handshake.rs
new file mode 100644 (file)
index 0000000..d8687a0
--- /dev/null
@@ -0,0 +1,279 @@
+/* Copyright (C) 2025 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>
+
+*/
+
+use crate::jsonbuilder::HEX;
+use libc::c_uchar;
+use std::os::raw::c_char;
+use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion};
+
+#[derive(Debug, PartialEq)]
+pub struct HandshakeParams {
+    pub(crate) tls_version: Option<TlsVersion>,
+    pub(crate) ciphersuites: Vec<TlsCipherSuiteID>,
+    pub(crate) extensions: Vec<TlsExtensionType>,
+    pub(crate) signature_algorithms: Vec<u16>,
+    pub(crate) domain: bool,
+    pub(crate) alpn: [char; 2],
+    pub(crate) quic: bool,
+}
+
+impl Default for HandshakeParams {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl HandshakeParams {
+    #[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,
+        }
+    }
+
+    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,
+        }
+    }
+
+    pub(crate) fn set_tls_version(&mut self, version: TlsVersion) {
+        if Self::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(crate) fn set_alpn(&mut self, alpn: &[u8]) {
+        if !alpn.is_empty() {
+            // If the first ALPN value is only a single character, then that character is treated as both the first and last character.
+            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 Self::is_grease(v) {
+                    return;
+                }
+            }
+            if !alpn[0].is_ascii_alphanumeric() || !alpn[alpn.len() - 1].is_ascii_alphanumeric() {
+                // 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.
+                self.alpn[0] = char::from(HEX[(alpn[0] >> 4) as usize]);
+                self.alpn[1] = char::from(HEX[(alpn[alpn.len() - 1] & 0xF) as usize]);
+                return;
+            }
+            self.alpn[0] = char::from(alpn[0]);
+            self.alpn[1] = char::from(alpn[alpn.len() - 1]);
+        }
+    }
+
+    pub(crate) fn add_cipher_suite(&mut self, cipher: TlsCipherSuiteID) {
+        if Self::is_grease(u16::from(cipher)) {
+            return;
+        }
+        self.ciphersuites.push(cipher);
+    }
+
+    pub(crate) fn add_extension(&mut self, ext: TlsExtensionType) {
+        if Self::is_grease(u16::from(ext)) {
+            return;
+        }
+        if ext == TlsExtensionType::ServerName {
+            self.domain = true;
+        }
+        self.extensions.push(ext);
+    }
+
+    pub(crate) fn add_signature_algorithm(&mut self, sigalgo: u16) {
+        if Self::is_grease(sigalgo) {
+            return;
+        }
+        self.signature_algorithms.push(sigalgo);
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn SCTLSHandshakeNew() -> *mut HandshakeParams {
+    let hs = Box::new(HandshakeParams::new());
+    Box::into_raw(hs)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeSetTLSVersion(hs: &mut HandshakeParams, version: u16) {
+    hs.set_tls_version(TlsVersion(version));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeAddCipher(hs: &mut HandshakeParams, cipher: u16) {
+    hs.add_cipher_suite(TlsCipherSuiteID(cipher));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeAddExtension(hs: &mut HandshakeParams, ext: u16) {
+    hs.add_extension(TlsExtensionType(ext));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeAddSigAlgo(hs: &mut HandshakeParams, sigalgo: u16) {
+    hs.add_signature_algorithm(sigalgo);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeSetALPN(
+    hs: &mut HandshakeParams, proto: *const c_char, len: u16,
+) {
+    let b: &[u8] = std::slice::from_raw_parts(proto as *const c_uchar, len as usize);
+    hs.set_alpn(b);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeFree(hs: &mut HandshakeParams) {
+    let hs: Box<HandshakeParams> = Box::from_raw(hs);
+    std::mem::drop(hs);
+}
+
+#[cfg(test)]
+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!(!HandshakeParams::is_grease(v));
+
+        alpn = &[0x0a, 0x0a];
+        len = alpn.len();
+        let v: u16 = ((alpn[0] as u16) << 8) | alpn[len - 1] as u16;
+        assert!(HandshakeParams::is_grease(v));
+    }
+
+    #[test]
+    fn test_tlsversion_max() {
+        let mut hs = HandshakeParams::new();
+        assert_eq!(hs.tls_version, None);
+        hs.set_tls_version(TlsVersion::Ssl30);
+        assert_eq!(hs.tls_version, Some(TlsVersion::Ssl30));
+        hs.set_tls_version(TlsVersion::Tls12);
+        assert_eq!(hs.tls_version, Some(TlsVersion::Tls12));
+        hs.set_tls_version(TlsVersion::Tls10);
+        assert_eq!(hs.tls_version, Some(TlsVersion::Tls12));
+    }
+
+    #[test]
+    fn test_add_cipher_suite_filters_grease() {
+        let mut hs = HandshakeParams::new();
+        hs.add_cipher_suite(TlsCipherSuiteID(0x1a1a)); // GREASE
+        assert_eq!(hs.ciphersuites.len(), 0);
+    }
+
+    #[test]
+    fn test_add_cipher_suite_accepts_normal() {
+        let mut hs = HandshakeParams::new();
+        hs.add_cipher_suite(TlsCipherSuiteID(0x1301));
+        assert_eq!(hs.ciphersuites, &[TlsCipherSuiteID(0x1301)]);
+    }
+
+    #[test]
+    fn test_add_cipher_suite_len_tracking() {
+        let mut hs = HandshakeParams::new();
+        hs.add_cipher_suite(TlsCipherSuiteID(0x1301));
+        hs.add_cipher_suite(TlsCipherSuiteID(0x1302));
+        hs.add_cipher_suite(TlsCipherSuiteID(0x1a1a)); // GREASE
+        assert_eq!(hs.ciphersuites.len(), 2);
+    }
+
+    #[test]
+    fn test_add_extension_sets_domain_for_server_name() {
+        let mut hs = HandshakeParams::new();
+        hs.add_extension(TlsExtensionType::ServerName);
+        assert!(hs.domain);
+    }
+
+    #[test]
+    fn test_add_extension_filters_grease() {
+        let mut hs = HandshakeParams::new();
+        hs.add_extension(TlsExtensionType(0xaaaa)); // GREASE
+        assert_eq!(hs.extensions.len(), 0);
+    }
+
+    #[test]
+    fn test_add_extension_len_tracking() {
+        let mut hs = HandshakeParams::new();
+        hs.add_extension(TlsExtensionType::ClientCertificate);
+        hs.add_extension(TlsExtensionType::ServerName);
+        hs.add_extension(TlsExtensionType(0xaaaa)); // GREASE
+        assert_eq!(hs.extensions.len(), 2);
+    }
+
+    #[test]
+    fn test_add_signature_algorithm_filters_grease() {
+        let mut hs = HandshakeParams::new();
+        hs.add_signature_algorithm(0xbaba); // GREASE
+        assert_eq!(hs.signature_algorithms.len(), 0);
+    }
+
+    #[test]
+    fn test_add_signature_algorithm_len_tracking() {
+        let mut hs = HandshakeParams::new();
+        hs.add_signature_algorithm(0x1234);
+        hs.add_signature_algorithm(0x1235);
+        hs.add_signature_algorithm(0xbaba); // GREASE
+        assert_eq!(hs.signature_algorithms.len(), 2);
+    }
+
+    #[test]
+    fn test_set_alpn_ascii() {
+        let mut hs = HandshakeParams::new();
+        hs.set_alpn(b"http/1.1");
+        assert_eq!(hs.alpn, ['h', '1']);
+    }
+
+    #[test]
+    fn test_set_alpn_non_ascii_first_or_last() {
+        let mut hs = HandshakeParams::new();
+        hs.set_alpn(&[0x01, b'T', b'E', 0x7f]); // non-alphanumeric start and end
+        assert_eq!(hs.alpn, [HEX[0x0], HEX[0xF]].map(|b| b as char)); // 0x01 -> 0, 0x7f -> f
+    }
+
+    #[test]
+    fn test_set_alpn_grease_pair_filtered() {
+        let mut hs = HandshakeParams::new();
+        hs.set_alpn(&[0x2a, 0x2a]); // 0x2a2a GREASE
+        assert_eq!(hs.alpn, ['0', '0']);
+    }
+}
index ae76186c905d5956af3afd282543da6f48047d31..50e3d5535c580cad790efa240fd5b0b45bb511a9 100644 (file)
 // 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};
 #[cfg(feature = "ja4")]
-use crate::jsonbuilder::HEX;
+use tls_parser::{TlsExtensionType, TlsVersion};
+
+use crate::handshake::HandshakeParams;
+
+pub const JA4_HEX_LEN: usize = 36;
+
+pub(crate) trait JA4Impl {
+    fn try_new(hs: &HandshakeParams) -> Option<JA4>;
+}
 
 #[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,
+    hash: String,
 }
 
-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()
+impl AsRef<str> for JA4 {
+    fn as_ref(&self) -> &str {
+        &self.hash
     }
 }
 
 #[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 {
@@ -103,105 +60,38 @@ impl JA4 {
             _ => "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.is_empty() {
-            // If the first ALPN value is only a single character, then that character is treated as both the first and last character.
-            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;
-                }
-            }
-            if !alpn[0].is_ascii_alphanumeric() || !alpn[alpn.len() - 1].is_ascii_alphanumeric() {
-                // 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.
-                self.alpn[0] = char::from(HEX[(alpn[0] >> 4) as usize]);
-                self.alpn[1] = char::from(HEX[(alpn[alpn.len() - 1] & 0xF) as usize]);
-                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);
-    }
+#[cfg(feature = "ja4")]
+impl JA4Impl for JA4 {
+    fn try_new(hs: &HandshakeParams) -> Option<Self> {
+        // All non-GREASE extensions are stored to produce a more verbose, complete output
+        // of extensions but we need to omit ALPN & SNI extensions from the JA4_a hash.
+        let mut exts = hs
+            .extensions
+            .iter()
+            .filter(|&ext| {
+                *ext != TlsExtensionType::ApplicationLayerProtocolNegotiation
+                    && *ext != TlsExtensionType::ServerName
+            })
+            .collect::<Vec<&TlsExtensionType>>();
 
-    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;
-    }
+        let alpn = hs.alpn;
 
-    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]
+            proto = if hs.quic { "q" } else { "t" },
+            version = Self::version_to_ja4code(hs.tls_version),
+            sni = if hs.domain { "d" } else { "i" },
+            nof_c = min(99, hs.ciphersuites.len()),
+            nof_e = min(99, hs.extensions.len()),
+            al1 = alpn[0],
+            al2 = alpn[1]
         );
 
         // Calculate JA4_b
-        let mut sorted_ciphers = self.ciphersuites.to_vec();
+        let mut sorted_ciphers = hs.ciphersuites.to_vec();
         sorted_ciphers.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b)));
         let sorted_cipherstrings: Vec<String> = sorted_ciphers
             .iter()
@@ -214,14 +104,13 @@ impl JA4 {
         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)))
+        exts.sort_by(|&a, &b| u16::from(*a).cmp(&u16::from(*b)));
+        let sorted_extstrings: Vec<String> = exts
+            .into_iter()
+            .map(|&v| format!("{:04x}", u16::from(v)))
             .collect();
         let ja4_c1_raw = sorted_extstrings.join(",");
-        let unsorted_sigalgostrings: Vec<String> = self
+        let unsorted_sigalgostrings: Vec<String> = hs
             .signature_algorithms
             .iter()
             .map(|v| format!("{:04x}", (*v)))
@@ -232,189 +121,166 @@ impl JA4 {
         let mut ja4_c = format!("{:x}", sha.finalize());
         ja4_c.truncate(12);
 
-        return format!("{}_{}_{}", ja4_a, ja4_b, ja4_c);
+        Some(Self {
+            hash: 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());
+#[cfg(not(feature = "ja4"))]
+impl JA4Impl for JA4 {
+    fn try_new(_hs: &HandshakeParams) -> Option<Self> {
+        None
+    }
 }
 
+// C ABI
+#[cfg(feature = "ja4")]
 #[no_mangle]
-pub unsafe extern "C" fn SCJA4Free(j: &mut JA4) {
-    let ja4: Box<JA4> = Box::from_raw(j);
-    std::mem::drop(ja4);
+pub unsafe extern "C" fn SCJA4GetHash(hs: &HandshakeParams, out: &mut [u8; JA4_HEX_LEN]) {
+    if let Some(ja4) = JA4::try_new(hs) {
+        out[0..JA4_HEX_LEN].copy_from_slice(ja4.as_ref().as_bytes());
+    }
 }
 
-#[cfg(all(test, feature = "ja4"))]
+#[cfg(test)]
+#[cfg(feature = "ja4")]
 mod tests {
     use super::*;
+    use tls_parser::TlsCipherSuiteID;
 
     #[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() {
+    fn test_hash_limit_numbers() {
         // Test whether the limitation of the extension and ciphersuite
         // count to 99 is reflected correctly.
-        let mut j = JA4::new();
+        let mut hs = HandshakeParams::default();
 
         for i in 1..200 {
-            j.add_cipher_suite(TlsCipherSuiteID(i));
+            hs.add_cipher_suite(TlsCipherSuiteID(i));
         }
         for i in 1..200 {
-            j.add_extension(TlsExtensionType(i));
+            hs.add_extension(TlsExtensionType(i));
         }
 
-        let mut s = j.get_hash();
-        s.truncate(10);
-        assert_eq!(s, "t00i999900");
+        let ja4 = JA4::try_new(&hs).expect("JA4 create failure");
+
+        // Only testing the ja4_a portion of the hash, we we truncate to
+        // ensure we're only testing this
+        let mut ja4_hash = ja4.as_ref().to_string();
+        ja4_hash.truncate(10);
+
+        assert_eq!(ja4_hash, "t00i999900");
     }
 
     #[test]
     fn test_short_alpn() {
-        let mut j = JA4::new();
+        let mut hs = HandshakeParams::default();
 
-        j.set_alpn("b".as_bytes());
-        let mut s = j.get_hash();
+        hs.set_alpn("b".as_bytes());
+        let mut s = JA4::try_new(&hs)
+            .expect("JA4 create failure")
+            .as_ref()
+            .to_string();
         s.truncate(10);
         assert_eq!(s, "t00i0000bb");
 
-        j.set_alpn("h2".as_bytes());
-        let mut s = j.get_hash();
+        hs.set_alpn("h2".as_bytes());
+        let mut s = JA4::try_new(&hs)
+            .expect("JA4 create failure")
+            .as_ref()
+            .to_string();
         s.truncate(10);
         assert_eq!(s, "t00i0000h2");
 
         // from https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md#alpn-extension-value
-        j.set_alpn(&[0xab]);
-        let mut s = j.get_hash();
+        hs.set_alpn(&[0xab]);
+        let mut s = JA4::try_new(&hs)
+            .expect("JA4 create failure")
+            .as_ref()
+            .to_string();
         s.truncate(10);
         assert_eq!(s, "t00i0000ab");
 
-        j.set_alpn(&[0xab, 0xcd]);
-        let mut s = j.get_hash();
+        hs.set_alpn(&[0xab, 0xcd]);
+        let mut s = JA4::try_new(&hs)
+            .expect("JA4 create failure")
+            .as_ref()
+            .to_string();
         s.truncate(10);
         assert_eq!(s, "t00i0000ad");
 
-        j.set_alpn(&[0x30, 0xab]);
-        let mut s = j.get_hash();
+        hs.set_alpn(&[0x30, 0xab]);
+        let mut s = JA4::try_new(&hs)
+            .expect("JA4 create failure")
+            .as_ref()
+            .to_string();
         s.truncate(10);
         assert_eq!(s, "t00i00003b");
 
-        j.set_alpn(&[0x30, 0x31, 0xab, 0xcd]);
-        let mut s = j.get_hash();
+        hs.set_alpn(&[0x30, 0x31, 0xab, 0xcd]);
+        let mut s = JA4::try_new(&hs)
+            .expect("JA4 create failure")
+            .as_ref()
+            .to_string();
         s.truncate(10);
         assert_eq!(s, "t00i00003d");
 
-        j.set_alpn(&[0x30, 0xab, 0xcd, 0x31]);
-        let mut s = j.get_hash();
+        hs.set_alpn(&[0x30, 0xab, 0xcd, 0x31]);
+        let mut s = JA4::try_new(&hs)
+            .expect("JA4 create failure")
+            .as_ref()
+            .to_string();
         s.truncate(10);
         assert_eq!(s, "t00i000001");
     }
 
     #[test]
     fn test_get_hash() {
-        let mut j = JA4::new();
+        let mut hs = HandshakeParams::default();
 
         // the empty JA4 hash
-        let s = j.get_hash();
-        assert_eq!(s, "t00i000000_e3b0c44298fc_d2e2adf7177b");
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "t00i000000_e3b0c44298fc_d2e2adf7177b");
 
         // set TLS version
-        j.set_tls_version(TlsVersion::Tls12);
-        let s = j.get_hash();
-        assert_eq!(s, "t12i000000_e3b0c44298fc_d2e2adf7177b");
+        hs.set_tls_version(TlsVersion::Tls12);
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "t12i000000_e3b0c44298fc_d2e2adf7177b");
 
         // set QUIC
-        j.set_quic();
-        let s = j.get_hash();
-        assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b");
+        hs.quic = true;
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "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");
+        hs.add_extension(TlsExtensionType(0x0a0a));
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "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");
+        hs.add_extension(TlsExtensionType(0x0000));
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "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");
+        hs.set_alpn(b"h3-16");
+        hs.add_extension(TlsExtensionType::ApplicationLayerProtocolNegotiation);
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "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");
+        hs.add_cipher_suite(TlsCipherSuiteID(0x1111));
+        hs.add_cipher_suite(TlsCipherSuiteID(0x0a20));
+        hs.add_cipher_suite(TlsCipherSuiteID(0xbada));
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "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");
+        hs.add_extension(TlsExtensionType(0xface));
+        hs.add_extension(TlsExtensionType(0x0121));
+        hs.add_extension(TlsExtensionType(0x1234));
+        hs.add_signature_algorithm(0x6666);
+        let s = JA4::try_new(&hs).expect("JA4 create failure");
+        assert_eq!(s.as_ref(), "q12d0305h6_f500716053f9_2debc8880bae");
     }
 }
index 4c393383a6a78247c731f066a687694c4881519a..6ed5a2cb67ea6bf5f251a11f841cd9dfe1b96720 100644 (file)
@@ -95,6 +95,7 @@ pub mod detect;
 pub mod utils;
 
 pub mod ja4;
+pub mod handshake;
 
 pub mod lua;
 
index 11902b71c966e19a3eebbd4aaebf80534babeaf8..4d23603e952a8d7ea6edebd06b9a33198245a592 100644 (file)
@@ -75,8 +75,8 @@ pub unsafe extern "C" fn SCQuicTxGetJa4(
     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;
+        *buffer = ja4.as_ref().as_ptr();
+        *buffer_len = ja4.as_ref().len() as u32;
         1
     } else {
         *buffer = ptr::null();
index a4010f4488a989bc5644a9ad6f04eff657df2157..bafbdfd5896c1fcfc3d9e3a22638083d20478a38 100644 (file)
@@ -17,7 +17,7 @@
 
 use super::error::QuicError;
 use super::quic::QUIC_MAX_CRYPTO_FRAG_LEN;
-use crate::ja4::*;
+use crate::handshake::HandshakeParams;
 use crate::quic::parser::quic_var_uint;
 use nom7::bytes::complete::take;
 use nom7::combinator::{all_consuming, complete};
@@ -139,7 +139,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>,
+    pub hs: Option<HandshakeParams>,
 }
 
 #[derive(Debug, PartialEq)]
@@ -246,7 +246,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, mut ja4: Option<&mut JA4>, client: bool,
+    input: Option<&[u8]>, ja3: &mut String, mut hs: Option<&mut HandshakeParams>, client: bool,
 ) -> Vec<QuicTlsExtension> {
     let mut extv = Vec::new();
     if let Some(extr) = input {
@@ -260,8 +260,8 @@ 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)
+                if let Some(ref mut hs) = hs {
+                    hs.add_extension(etype)
                 }
                 let mut values = Vec::new();
                 match e {
@@ -270,8 +270,8 @@ fn quic_get_tls_extensions(
                             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);
+                            if let Some(ref mut hs) = hs {
+                                hs.set_tls_version(*version);
                             }
                         }
                     }
@@ -287,15 +287,15 @@ fn quic_get_tls_extensions(
                             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)
+                            if let Some(ref mut hs) = hs {
+                                hs.add_signature_algorithm(*sigalgo)
                             }
                         }
                     }
                     TlsExtension::ALPN(x) => {
                         if !x.is_empty() {
-                            if let Some(ref mut ja4) = ja4 {
-                                ja4.set_alpn(x[0]);
+                            if let Some(ref mut hs) = hs {
+                                hs.set_alpn(x[0]);
                             }
                         }
                         for alpn in x {
@@ -323,8 +323,7 @@ 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 hs = HandshakeParams { quic: true, ..Default::default() };
                 let mut dash = false;
                 for c in &ch.ciphers {
                     if dash {
@@ -333,11 +332,11 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                         dash = true;
                     }
                     ja3.push_str(&u16::from(*c).to_string());
-                    ja4.add_cipher_suite(*c);
+                    hs.add_cipher_suite(*c);
                 }
                 ja3.push(',');
                 let ciphers = ch.ciphers;
-                let extv = quic_get_tls_extensions(ch.ext, &mut ja3, Some(&mut ja4), true);
+                let extv = quic_get_tls_extensions(ch.ext, &mut ja3, Some(&mut hs), true);
                 return Some(Frame::Crypto(Crypto {
                     ciphers,
                     extv,
@@ -346,8 +345,8 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                     } else {
                         None
                     },
-                    ja4: if cfg!(feature = "ja4") {
-                        Some(ja4)
+                    hs: if cfg!(feature = "ja4") {
+                        Some(hs)
                     } else {
                         None
                     },
@@ -369,7 +368,7 @@ fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
                     } else {
                         None
                     },
-                    ja4: None,
+                    hs: None,
                 }));
             }
             _ => {}
index 1ba2afbb66c15d601cefd254b3810f2a0e939a0b..fc21b383724580d92304f9ea31aa82574fa4a1c9 100644 (file)
@@ -124,7 +124,7 @@ fn log_quic(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonError>
     }
 
     if let Some(ref ja4) = &tx.ja4 {
-        js.set_string("ja4", ja4)?;
+        js.set_string("ja4", ja4.as_ref())?;
     }
 
     if !tx.extv.is_empty() {
index 886c14e75265195c6a44db6668b0ce8abb472b9d..72872f806eb3d103db5a85ee0f3115f3d024efd2 100644 (file)
@@ -21,11 +21,15 @@ use super::{
     frames::{Frame, QuicTlsExtension, StreamTag},
     parser::{quic_pkt_num, QuicData, QuicHeader, QuicType},
 };
-use crate::core::{ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP};
 use crate::{
     applayer::{self, *},
     direction::Direction,
     flow::Flow,
+    ja4::JA4,
+};
+use crate::{
+    core::{ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP},
+    ja4::JA4Impl,
 };
 use std::collections::VecDeque;
 use std::ffi::CString;
@@ -55,7 +59,7 @@ pub struct QuicTransaction {
     pub ua: Option<Vec<u8>>,
     pub extv: Vec<QuicTlsExtension>,
     pub ja3: Option<String>,
-    pub ja4: Option<String>,
+    pub ja4: Option<JA4>,
     pub client: bool,
     tx_data: AppLayerTxData,
 }
@@ -63,7 +67,7 @@ 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>, ja4: Option<String>, client: bool,
+        extv: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<JA4>, client: bool,
     ) -> Self {
         let direction = if client {
             Direction::ToServer
@@ -164,7 +168,7 @@ 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>, ja4: Option<String>, client: bool,
+        extb: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<JA4>, client: bool,
         frag_long: bool,
     ) {
         let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, ja4, client);
@@ -248,7 +252,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 ja4: Option<JA4> = None;
         let mut extv: Vec<QuicTlsExtension> = Vec::new();
         let mut frag_long = false;
         for frame in &data.frames {
@@ -293,8 +297,8 @@ impl QuicState {
                     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());
+                        if let Some(ref rja4) = c.hs {
+                            ja4 = JA4::try_new(rja4);
                         }
                     }
                     for e in &c.extv {
index 47c123798ba81a0218686876712a06ba2f7ce67e..d5c302bd3c829d2da4b2f5a14ddec78e325ba0c2 100755 (executable)
@@ -525,7 +525,6 @@ noinst_HEADERS = \
        util-ioctl.h \
        util-ip.h \
        util-ja3.h \
-       util-ja4.h \
        util-landlock.h \
        util-log-redis.h \
        util-logopenfile.h \
index ff8a11e3fae639abad2b5d16e4baa041e1d12c99..027510a77f8a7ec07a68b7bffbd656fa3f461fc4 100644 (file)
@@ -753,9 +753,8 @@ 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);
+    if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+        SCTLSHandshakeSetTLSVersion(ssl_state->curr_connp->hs, version);
     }
 
     /* TLSv1.3 draft1 to draft21 use the version field as earlier TLS
@@ -904,54 +903,46 @@ static inline int TLSDecodeHSHelloCipherSuites(SSLState *ssl_state,
     const bool enable_ja3 =
             SC_ATOMIC_GET(ssl_config.enable_ja3) && ssl_state->curr_connp->ja3_hash == NULL;
 
-    if (enable_ja3 || SC_ATOMIC_GET(ssl_config.enable_ja4)) {
-        JA3Buffer *ja3_cipher_suites = NULL;
+    JA3Buffer *ja3_cipher_suites = NULL;
 
-        if (enable_ja3) {
-            ja3_cipher_suites = Ja3BufferInit();
-            if (ja3_cipher_suites == NULL)
-                return -1;
-        }
+    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))) {
-                if (enable_ja3) {
-                    Ja3BufferFree(&ja3_cipher_suites);
-                }
-                goto invalid_length;
+    uint16_t processed_len = 0;
+    /* coverity[tainted_data] */
+    while (processed_len < cipher_suites_length) {
+        if (!(HAS_SPACE(2))) {
+            if (enable_ja3) {
+                Ja3BufferFree(&ja3_cipher_suites);
             }
+            goto invalid_length;
+        }
 
-            uint16_t cipher_suite = (uint16_t)(*input << 8) | *(input + 1);
-            input += 2;
+        uint16_t cipher_suite = (uint16_t)(*input << 8) | *(input + 1);
+        input += 2;
 
-            if (TLSDecodeValueIsGREASE(cipher_suite) != 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;
-                    }
+        if (TLSDecodeValueIsGREASE(cipher_suite) != 1) {
+            if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+                SCTLSHandshakeAddCipher(ssl_state->curr_connp->hs, cipher_suite);
+            }
+            if (enable_ja3) {
+                int rc = Ja3BufferAddValue(&ja3_cipher_suites, cipher_suite);
+                if (rc != 0) {
+                    return -1;
                 }
             }
-            processed_len += 2;
         }
+        processed_len += 2;
+    }
 
-        if (enable_ja3) {
-            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 {
-        /* Skip cipher suites */
-        input += cipher_suites_length;
     }
 
     return (int)(input - initial_input);
@@ -1107,10 +1098,7 @@ 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);
-                }
+                SCTLSHandshakeSetTLSVersion(ssl_state->curr_connp->hs, ver);
                 break;
             }
             i += 2;
@@ -1278,15 +1266,14 @@ static inline int TLSDecodeHSHelloExtensionSigAlgorithms(
     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) {
+    if (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);
+            SCTLSHandshakeAddSigAlgo(ssl_state->curr_connp->hs, sigalgo);
         }
     } else {
         /* Skip signature algorithms */
@@ -1350,11 +1337,9 @@ static inline int TLSDecodeHSHelloExtensionALPN(
             break;
         }
 
-        /* Only record the first value for JA4 */
-        if (ssl_state->curr_connp->ja4 != NULL &&
-                ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+        if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
             if (alpn_processed_len == 1) {
-                SCJA4SetALPN(ssl_state->curr_connp->ja4, (const char *)input, protolen);
+                SCTLSHandshakeSetALPN(ssl_state->curr_connp->hs, (const char *)input, protolen);
             }
         }
         StoreALPN(ssl_state->curr_connp, input, protolen);
@@ -1550,10 +1535,9 @@ 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 (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
             if (TLSDecodeValueIsGREASE(ext_type) != 1) {
-                SCJA4AddExtension(ssl_state->curr_connp->ja4, ext_type);
+                SCTLSHandshakeAddExtension(ssl_state->curr_connp->hs, ext_type);
             }
         }
 
@@ -1605,15 +1589,6 @@ 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;
@@ -2961,6 +2936,8 @@ static void *SSLStateAlloc(void *orig_state, AppProto proto_orig)
     ssl_state->server_connp.cert_log_flag = 0;
     memset(ssl_state->client_connp.random, 0, TLS_RANDOM_LEN);
     memset(ssl_state->server_connp.random, 0, TLS_RANDOM_LEN);
+    ssl_state->client_connp.hs = SCTLSHandshakeNew();
+    ssl_state->server_connp.hs = SCTLSHandshakeNew();
     TAILQ_INIT(&ssl_state->server_connp.certs);
     TAILQ_INIT(&ssl_state->server_connp.alpns);
     TAILQ_INIT(&ssl_state->client_connp.certs);
@@ -3016,12 +2993,14 @@ 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.hs)
+        SCTLSHandshakeFree(ssl_state->client_connp.hs);
     if (ssl_state->client_connp.ja3_str)
         Ja3BufferFree(&ssl_state->client_connp.ja3_str);
     if (ssl_state->client_connp.ja3_hash)
         SCFree(ssl_state->client_connp.ja3_hash);
+    if (ssl_state->server_connp.hs)
+        SCTLSHandshakeFree(ssl_state->server_connp.hs);
     if (ssl_state->server_connp.ja3_str)
         Ja3BufferFree(&ssl_state->server_connp.ja3_str);
     if (ssl_state->server_connp.ja3_hash)
index 2d136f6f01cfb4e8cc2a3faecccc219d6e93f2d8..51c43c5ce2990d78795c3e0fa57c42fb0c31ed9c 100644 (file)
@@ -282,7 +282,7 @@ typedef struct SSLStateConnp_ {
     JA3Buffer *ja3_str;
     char *ja3_hash;
 
-    JA4 *ja4;
+    HandshakeParams *hs;
 
     /* handshake tls fragmentation buffer. Handshake messages can be fragmented over multiple
      * TLS records. */
index b1f79906bdcffa0957afc5692e035e32a863cc03..2e879abee6e0bfdf7173d5ba7397cec833007a3c 100644 (file)
@@ -35,8 +35,6 @@
 #include "detect-engine-prefilter.h"
 #include "detect-ja4-hash.h"
 
-#include "util-ja4.h"
-
 #include "app-layer-ssl.h"
 
 #ifndef HAVE_JA4
@@ -146,12 +144,12 @@ static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx,
     if (buffer->inspect == NULL) {
         const SSLState *ssl_state = (SSLState *)f->alstate;
 
-        if (ssl_state->client_connp.ja4 == NULL) {
+        if (ssl_state->client_connp.hs == NULL) {
             return NULL;
         }
 
         uint8_t data[JA4_HEX_LEN];
-        SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])data);
+        SCJA4GetHash(ssl_state->client_connp.hs, (uint8_t(*)[JA4_HEX_LEN])data);
 
         InspectionBufferSetup(det_ctx, list_id, buffer, data, 0);
         InspectionBufferCopy(buffer, data, JA4_HEX_LEN);
index 72d60b19e80906c0299e98051225a7e7b35be61c..f339155a2ed40c98311ba21f0d3f659f2507a3f1 100644 (file)
@@ -35,7 +35,6 @@
 #include "threadvars.h"
 #include "util-debug.h"
 #include "util-ja3.h"
-#include "util-ja4.h"
 #include "util-time.h"
 
 #define LOG_TLS_FIELD_VERSION         BIT_U64(0)
@@ -237,12 +236,14 @@ static void JsonTlsLogJa3(SCJsonBuilder *js, SSLState *ssl_state)
 
 static void JsonTlsLogSCJA4(SCJsonBuilder *js, SSLState *ssl_state)
 {
-    if (ssl_state->client_connp.ja4 != NULL) {
+#ifdef HAVE_JA4
+    if (ssl_state->client_connp.hs != NULL) {
         uint8_t buffer[JA4_HEX_LEN];
         /* JA4 hash has 36 characters */
-        SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])buffer);
-        SCJbSetStringFromBytes(js, "ja4", buffer, 36);
+        SCJA4GetHash(ssl_state->client_connp.hs, (uint8_t(*)[JA4_HEX_LEN])buffer);
+        SCJbSetStringFromBytes(js, "ja4", buffer, JA4_HEX_LEN);
     }
+#endif
 }
 
 static void JsonTlsLogJa3SHash(SCJsonBuilder *js, SSLState *ssl_state)
diff --git a/src/util-ja4.h b/src/util-ja4.h
deleted file mode 100644 (file)
index 769e089..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/* 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 */