From 94c8be22d4084ec33b28666649f929f345056da5 Mon Sep 17 00:00:00 2001 From: Richard McConnell Date: Thu, 24 Apr 2025 11:46:47 +0100 Subject: [PATCH] output/tls: Allow logging of cl-handshake params 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 | 3 + etc/schema.json | 33 ++++++++ rust/src/handshake.rs | 79 ++++++++++++++++++++ src/output-json-tls.c | 70 +++++++++++------ suricata.yaml.in | 2 +- 5 files changed, 165 insertions(+), 22 deletions(-) diff --git a/doc/userguide/output/eve/eve-json-format.rst b/doc/userguide/output/eve/eve-json-format.rst index b5a2bf4641..2890b3f556 100644 --- a/doc/userguide/output/eve/eve-json-format.rst +++ b/doc/userguide/output/eve/eve-json-format.rst @@ -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 ~~~~~~~~ diff --git a/etc/schema.json b/etc/schema.json index 791186da64..37c09111a6 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -7069,6 +7069,39 @@ "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", diff --git a/rust/src/handshake.rs b/rust/src/handshake.rs index 1a6edead72..df33af4dca 100644 --- a/rust/src/handshake.rs +++ b/rust/src/handshake.rs @@ -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 diff --git a/src/output-json-tls.c b/src/output-json-tls.c index 45bb5fcb8e..3fc07e826c 100644 --- a/src/output-json-tls.c +++ b/src/output-json-tls.c @@ -37,26 +37,27 @@ #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; diff --git a/suricata.yaml.in b/suricata.yaml.in index 972de0687e..b6506b11f0 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -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,