]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
output/tls: Allow logging of cl-handshake params
authorRichard McConnell <Richard_McConnell@rapid7.com>
Thu, 24 Apr 2025 10:46:47 +0000 (11:46 +0100)
committerVictor Julien <victor@inliniac.net>
Fri, 16 May 2025 19:33:54 +0000 (21:33 +0200)
Ticket: 6695

Add new custom log fields:

"client_handshake" which logs the following:
1. TLS version used during handshake
2. TLS extensions, excluding GREASE, SNI and ALPN
3. All cipher suites, excluding GREASE
4. All signature algorithms, excluding GREASE

The use-case is for logging TLS handshake parameters in order to survey
them, and so that JA4 hashes can be computed offline (in the case that
they're not already computed for the purposes of rule matching).

doc/userguide/output/eve/eve-json-format.rst
etc/schema.json
rust/src/handshake.rs
src/output-json-tls.c
suricata.yaml.in

index b5a2bf4641dc5c85eb75a393c867e2c453f5ccf1..2890b3f556b5288250c9464e160e39bcdfbb3931 100644 (file)
@@ -1053,6 +1053,9 @@ In addition to this, custom logging also allows the following fields:
 
 * "certificate": The TLS certificate base64 encoded
 * "chain": The entire TLS certificate chain base64 encoded
+* "client_handshake": structure containing "version", "ciphers" ([u16]), "exts" ([u16]), "sig_algs" ([u16]),
+  for client hello supported cipher suites, extensions, and signature algorithms,
+  respectively, in the order that they're mentioned (ie. unsorted)
 
 Examples
 ~~~~~~~~
index 791186da6448532a9cb9f594d1fdb9c986cdc609..37c09111a677a8a926d147a24101f49538d8c4f5 100644 (file)
                         "type": "string"
                     }
                 },
+                "client_handshake": {
+                    "type": "object",
+                    "properties": {
+                        "version": {
+                            "description": "TLS version in client hello",
+                            "type": "string"
+                        },
+                        "ciphers": {
+                            "description": "TLS client cipher(s)",
+                            "type": "array",
+                            "minItems": 1,
+                            "items": {
+                                "type": "integer"
+                            }
+                        },
+                        "exts": {
+                            "description": "TLS client extension(s)",
+                            "type": "array",
+                            "minItems": 1,
+                            "items": {
+                                "type": "integer"
+                            }
+                        },
+                        "sig_algs": {
+                            "description": "TLS client signature algorithm(s)",
+                            "type": "array",
+                            "minItems": 1,
+                            "items": {
+                                "type": "integer"
+                            }
+                        }
+                    }
+                },
                 "server_alpns": {
                     "description": "TLS server ALPN field(s)",
                     "type": "array",
index 1a6edead72578e9ab767c3363f36fd0dc4a0a826..df33af4dcacdf76c12b7958cf37667dd19f3ff4a 100644 (file)
@@ -24,6 +24,7 @@ use std::os::raw::c_char;
 use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion};
 
 use crate::jsonbuilder::{JsonBuilder, JsonError};
+use crate::tls_version::SCTlsVersion;
 
 #[derive(Debug, PartialEq)]
 pub struct HandshakeParams {
@@ -124,6 +125,52 @@ impl HandshakeParams {
         js.close()?;
         Ok(())
     }
+
+    fn log_version(&self, js: &mut JsonBuilder) -> Result<(), JsonError> {
+        let vers = self.tls_version.map(|v| v.0).unwrap_or_default();
+        let ver_str = SCTlsVersion::try_from(vers).map_err(|_| JsonError::InvalidState)?;
+        js.set_string("version", ver_str.as_str())?;
+        Ok(())
+    }
+
+    fn log_exts(&self, js: &mut JsonBuilder) -> Result<(), JsonError> {
+        if self.extensions.is_empty() {
+            return Ok(());
+        }
+        js.open_array("exts")?;
+
+        for v in &self.extensions {
+            js.append_uint(v.0.into())?;
+        }
+        js.close()?;
+        Ok(())
+    }
+
+    fn log_ciphers(&self, js: &mut JsonBuilder) -> Result<(), JsonError> {
+        if self.ciphersuites.is_empty() {
+            return Ok(());
+        }
+        js.open_array("ciphers")?;
+
+        for v in &self.ciphersuites {
+            js.append_uint(v.0.into())?;
+        }
+        js.close()?;
+        Ok(())
+    }
+
+    fn log_sig_algs(&self, js: &mut JsonBuilder) -> Result<(), JsonError> {
+        if self.signature_algorithms.is_empty() {
+            return Ok(());
+        }
+        js.open_array("sig_algs")?;
+
+        for v in &self.signature_algorithms {
+            js.append_uint(*v as u64)?;
+        }
+        js.close()?;
+        Ok(())
+    }
 }
 
 // Objects used to allow C to call into this struct via the below C ABI
@@ -179,6 +226,38 @@ pub unsafe extern "C" fn SCTLSHandshakeFree(hs: &mut HandshakeParams) {
     std::mem::drop(hs);
 }
 
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeLogVersion(hs: &HandshakeParams, js: *mut JsonBuilder) -> bool {
+    if js.is_null() {
+        return false;
+    }
+    return hs.log_version(js.as_mut().unwrap()).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeLogCiphers(hs: &HandshakeParams, js: *mut JsonBuilder) -> bool {
+    if js.is_null() {
+        return false;
+    }
+    return hs.log_ciphers(js.as_mut().unwrap()).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeLogExtensions(hs: &HandshakeParams, js: *mut JsonBuilder) -> bool {
+    if js.is_null() {
+        return false;
+    }
+    return hs.log_exts(js.as_mut().unwrap()).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeLogSigAlgs(hs: &HandshakeParams, js: *mut JsonBuilder) -> bool {
+    if js.is_null() {
+        return false;
+    }
+    return hs.log_sig_algs(js.as_mut().unwrap()).is_ok()
+}
+
 #[no_mangle]
 pub unsafe extern "C" fn SCTLSHandshakeLogALPNs(
     hs: &HandshakeParams, js: *mut JsonBuilder, ptr: *const c_char
index 45bb5fcb8e72ffa0b6bcc1dfa8c2d56f9958ab5b..3fc07e826cd5ee672aabe3b5f26d85ac5120569b 100644 (file)
 #include "util-ja3.h"
 #include "util-time.h"
 
-#define LOG_TLS_FIELD_VERSION         BIT_U64(0)
-#define LOG_TLS_FIELD_SUBJECT         BIT_U64(1)
-#define LOG_TLS_FIELD_ISSUER          BIT_U64(2)
-#define LOG_TLS_FIELD_SERIAL          BIT_U64(3)
-#define LOG_TLS_FIELD_FINGERPRINT     BIT_U64(4)
-#define LOG_TLS_FIELD_NOTBEFORE       BIT_U64(5)
-#define LOG_TLS_FIELD_NOTAFTER        BIT_U64(6)
-#define LOG_TLS_FIELD_SNI             BIT_U64(7)
-#define LOG_TLS_FIELD_CERTIFICATE     BIT_U64(8)
-#define LOG_TLS_FIELD_CHAIN           BIT_U64(9)
-#define LOG_TLS_FIELD_SESSION_RESUMED BIT_U64(10)
-#define LOG_TLS_FIELD_JA3             BIT_U64(11)
-#define LOG_TLS_FIELD_JA3S            BIT_U64(12)
-#define LOG_TLS_FIELD_CLIENT          BIT_U64(13) /**< client fields (issuer, subject, etc) */
-#define LOG_TLS_FIELD_CLIENT_CERT     BIT_U64(14)
-#define LOG_TLS_FIELD_CLIENT_CHAIN    BIT_U64(15)
-#define LOG_TLS_FIELD_JA4             BIT_U64(16)
-#define LOG_TLS_FIELD_SUBJECTALTNAME  BIT_U64(17)
-#define LOG_TLS_FIELD_CLIENT_ALPNS    BIT_U64(18)
-#define LOG_TLS_FIELD_SERVER_ALPNS    BIT_U64(19)
+#define LOG_TLS_FIELD_VERSION          BIT_U64(0)
+#define LOG_TLS_FIELD_SUBJECT          BIT_U64(1)
+#define LOG_TLS_FIELD_ISSUER           BIT_U64(2)
+#define LOG_TLS_FIELD_SERIAL           BIT_U64(3)
+#define LOG_TLS_FIELD_FINGERPRINT      BIT_U64(4)
+#define LOG_TLS_FIELD_NOTBEFORE        BIT_U64(5)
+#define LOG_TLS_FIELD_NOTAFTER         BIT_U64(6)
+#define LOG_TLS_FIELD_SNI              BIT_U64(7)
+#define LOG_TLS_FIELD_CERTIFICATE      BIT_U64(8)
+#define LOG_TLS_FIELD_CHAIN            BIT_U64(9)
+#define LOG_TLS_FIELD_SESSION_RESUMED  BIT_U64(10)
+#define LOG_TLS_FIELD_JA3              BIT_U64(11)
+#define LOG_TLS_FIELD_JA3S             BIT_U64(12)
+#define LOG_TLS_FIELD_CLIENT           BIT_U64(13) /**< client fields (issuer, subject, etc) */
+#define LOG_TLS_FIELD_CLIENT_CERT      BIT_U64(14)
+#define LOG_TLS_FIELD_CLIENT_CHAIN     BIT_U64(15)
+#define LOG_TLS_FIELD_JA4              BIT_U64(16)
+#define LOG_TLS_FIELD_SUBJECTALTNAME   BIT_U64(17)
+#define LOG_TLS_FIELD_CLIENT_ALPNS     BIT_U64(18)
+#define LOG_TLS_FIELD_SERVER_ALPNS     BIT_U64(19)
+#define LOG_TLS_FIELD_CLIENT_HANDSHAKE BIT_U64(20)
 
 typedef struct {
     const char *name;
@@ -85,6 +86,7 @@ TlsFields tls_fields[] = {
     { "subjectaltname", LOG_TLS_FIELD_SUBJECTALTNAME },
     { "client_alpns", LOG_TLS_FIELD_CLIENT_ALPNS },
     { "server_alpns", LOG_TLS_FIELD_SERVER_ALPNS },
+    { "client_handshake", LOG_TLS_FIELD_CLIENT_HANDSHAKE },
     { NULL, -1 },
     // clang-format on
 };
@@ -360,6 +362,27 @@ static void JsonTlsLogClientCert(
     }
 }
 
+static void JsonTlsLogClientHandshake(SCJsonBuilder *js, SSLState *ssl_state)
+{
+    if (ssl_state->client_connp.hs == NULL) {
+        return;
+    }
+
+    // Don't write an empty handshake
+    if (SCTLSHandshakeIsEmpty(ssl_state->client_connp.hs)) {
+        return;
+    }
+
+    SCJbOpenObject(js, "client_handshake");
+
+    SCTLSHandshakeLogVersion(ssl_state->client_connp.hs, js);
+    SCTLSHandshakeLogCiphers(ssl_state->client_connp.hs, js);
+    SCTLSHandshakeLogExtensions(ssl_state->client_connp.hs, js);
+    SCTLSHandshakeLogSigAlgs(ssl_state->client_connp.hs, js);
+
+    SCJbClose(js);
+}
+
 static void JsonTlsLogFields(SCJsonBuilder *js, SSLState *ssl_state, uint64_t fields)
 {
     /* tls subject */
@@ -391,8 +414,9 @@ static void JsonTlsLogFields(SCJsonBuilder *js, SSLState *ssl_state, uint64_t fi
         JsonTlsLogSni(js, ssl_state);
 
     /* tls version */
-    if (fields & LOG_TLS_FIELD_VERSION)
+    if (fields & LOG_TLS_FIELD_VERSION) {
         JsonTlsLogVersion(js, ssl_state);
+    }
 
     /* tls notbefore */
     if (fields & LOG_TLS_FIELD_NOTBEFORE)
@@ -430,6 +454,10 @@ static void JsonTlsLogFields(SCJsonBuilder *js, SSLState *ssl_state, uint64_t fi
         JsonTlsLogAlpns(js, &ssl_state->server_connp, "server_alpns");
     }
 
+    /* tls client handshake parameters */
+    if (fields & LOG_TLS_FIELD_CLIENT_HANDSHAKE)
+        JsonTlsLogClientHandshake(js, ssl_state);
+
     if (fields & LOG_TLS_FIELD_CLIENT) {
         const bool log_cert = (fields & LOG_TLS_FIELD_CLIENT_CERT) != 0;
         const bool log_chain = (fields & LOG_TLS_FIELD_CLIENT_CHAIN) != 0;
index 972de0687eeeed203d46f5dd8c5e676aa15bc0e6..b6506b11f0e2bf84f770782db3a938941f6d54ce 100644 (file)
@@ -288,7 +288,7 @@ outputs:
             #session-resumption: no
             # custom controls which TLS fields that are included in eve-log
             # WARNING: enabling custom disables extended logging.
-            #custom: [subject, issuer, session_resumed, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s, ja4, subjectaltname, client, client_certificate, client_chain, client_alpns, server_alpns]
+            #custom: [subject, issuer, session_resumed, serial, fingerprint, sni, version, not_before, not_after, certificate, chain, ja3, ja3s, ja4, subjectaltname, client, client_certificate, client_chain, client_alpns, server_alpns, client_handshake]
         - files:
             force-magic: no   # force logging magic on all logged files
             # force logging of checksums, available hash functions are md5,