diff --git a/doc/userguide/output/eve/eve-json-format.rst b/doc/userguide/output/eve/eve-json-format.rst index 952945dffc98..89b9ed97833e 100644 --- a/doc/userguide/output/eve/eve-json-format.rst +++ b/doc/userguide/output/eve/eve-json-format.rst @@ -1041,12 +1041,17 @@ If extended logging is enabled the following fields are also included: * "client_alpns": array of strings with ALPN values * "server_alpns": array of strings with ALPN values -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'). +JA3 and JA4 fingerprints can 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: * "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) +* "server_handshake": structure containing "version", "chosen cipher", "exts" ([u16]), for server hello + in the order that they're mentioned (ie. unsorted) Examples ~~~~~~~~ diff --git a/etc/schema.json b/etc/schema.json index 3a877aabb96e..77c15bd521c0 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -6729,6 +6729,54 @@ "type": "string" } }, + "client_handshake": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ciphers": { + "description": "TLS client cipher(s)", + "type": "array", + "items": { + "type": "integer" + } + }, + "exts": { + "description": "TLS client extension(s)", + "type": "array", + "items": { + "type": "integer" + } + }, + "sig_algs": { + "description": "TLS client signature algorithm(s)", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "server_handshake": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "cipher": { + "description": "TLS server's chosen cipher", + "type": "integer" + }, + "exts": { + "description": "TLS server extension(s)", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, "server_alpns": { "description": "TLS server ALPN field(s)", "type": "array", diff --git a/rust/src/ja4.rs b/rust/src/ja4.rs index 4660f2330227..966e646c17dc 100644 --- a/rust/src/ja4.rs +++ b/rust/src/ja4.rs @@ -30,6 +30,8 @@ use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion}; #[cfg(feature = "ja4")] use crate::jsonbuilder::HEX; +pub const JA4_HEX_LEN: usize = 36; + #[derive(Debug, PartialEq)] pub struct JA4 { tls_version: Option, @@ -39,10 +41,6 @@ pub struct JA4 { 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 { @@ -65,7 +63,6 @@ impl JA4 { domain: false, alpn: ['0', '0'], quic: false, - nof_exts: 0, } } pub fn set_quic(&mut self) {} @@ -113,7 +110,6 @@ impl JA4 { domain: false, alpn: ['0', '0'], quic: false, - nof_exts: 0, } } @@ -170,14 +166,10 @@ impl JA4 { if JA4::is_grease(u16::from(ext)) { return; } - if ext != TlsExtensionType::ApplicationLayerProtocolNegotiation - && ext != TlsExtensionType::ServerName - { - self.extensions.push(ext); - } else if ext == TlsExtensionType::ServerName { + if ext == TlsExtensionType::ServerName { self.domain = true; } - self.nof_exts += 1; + self.extensions.push(ext); } pub fn add_signature_algorithm(&mut self, sigalgo: u16) { @@ -188,6 +180,17 @@ impl JA4 { } pub fn get_hash(&self) -> String { + // 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 = self + .extensions + .iter() + .filter(|&ext| { + *ext != TlsExtensionType::ApplicationLayerProtocolNegotiation + && *ext != TlsExtensionType::ServerName + }) + .collect::>(); + // Calculate JA4_a let ja4_a = format!( "{proto}{version}{sni}{nof_c:02}{nof_e:02}{al1}{al2}", @@ -195,7 +198,7 @@ impl JA4 { 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), + nof_e = min(99, self.extensions.len()), al1 = self.alpn[0], al2 = self.alpn[1] ); @@ -214,11 +217,10 @@ 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 = 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 = exts + .into_iter() + .map(|&v| format!("{:04x}", u16::from(v))) .collect(); let ja4_c1_raw = sorted_extstrings.join(","); let unsorted_sigalgostrings: Vec = self @@ -269,9 +271,37 @@ pub unsafe extern "C" fn SCJA4SetALPN(j: &mut JA4, proto: *const c_char, len: u1 } #[no_mangle] -pub unsafe extern "C" fn SCJA4GetHash(j: &mut JA4, out: &mut [u8; 36]) { +pub unsafe extern "C" fn SCJA4GetVersion(j: &JA4) -> u16 { + u16::from(j.tls_version.unwrap_or(TlsVersion(0))) +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4GetHash(j: &mut JA4, out: &mut [u8; JA4_HEX_LEN]) { let hash = j.get_hash(); - out[0..36].copy_from_slice(hash.as_bytes()); + out[0..JA4_HEX_LEN].copy_from_slice(hash.as_bytes()); +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4GetCiphers(j: &mut JA4, out: *mut usize) -> *const u16 { + *out = j.ciphersuites.len(); + j.ciphersuites.as_ptr() as *const u16 +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4GetFirstCipher(j: &mut JA4) -> u16 { + j.ciphersuites.first().map(|&v| *v).unwrap_or(0) +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4GetExtensions(j: &mut JA4, out: *mut usize) -> *const u16 { + *out = j.extensions.len(); + j.extensions.as_ptr() as *const u16 +} + +#[no_mangle] +pub unsafe extern "C" fn SCJA4GetSigAlgs(j: &mut JA4, out: *mut usize) -> *const u16 { + *out = j.signature_algorithms.len(); + j.signature_algorithms.as_ptr() } #[no_mangle] diff --git a/src/Makefile.am b/src/Makefile.am index c3b1e9d237f9..54a872ee3dd6 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -507,7 +507,6 @@ noinst_HEADERS = \ util-ioctl.h \ util-ip.h \ util-ja3.h \ - util-ja4.h \ util-landlock.h \ util-logopenfile.h \ util-log-redis.h \ diff --git a/src/app-layer-ssl.c b/src/app-layer-ssl.c index 8da3617acffc..3ff2c2e1dcd3 100644 --- a/src/app-layer-ssl.c +++ b/src/app-layer-ssl.c @@ -695,7 +695,8 @@ static inline int TLSDecodeHSHelloVersion(SSLState *ssl_state, ssl_state->curr_connp->version = version; if (ssl_state->curr_connp->ja4 != NULL && - ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + ssl_state->current_flags & + (SSL_AL_FLAG_STATE_CLIENT_HELLO | SSL_AL_FLAG_STATE_SERVER_HELLO)) { SCJA4SetTLSVersion(ssl_state->curr_connp->ja4, version); } @@ -845,7 +846,7 @@ 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)) { + if (enable_ja3 || ssl_state->curr_connp->ja4 != NULL) { JA3Buffer *ja3_cipher_suites = NULL; if (enable_ja3) { @@ -870,7 +871,8 @@ static inline int TLSDecodeHSHelloCipherSuites(SSLState *ssl_state, if (TLSDecodeValueIsGREASE(cipher_suite) != 1) { if (ssl_state->curr_connp->ja4 != NULL && - ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + ssl_state->current_flags & + (SSL_AL_FLAG_STATE_CLIENT_HELLO | SSL_AL_FLAG_STATE_SERVER_HELLO)) { SCJA4AddCipher(ssl_state->curr_connp->ja4, cipher_suite); } if (enable_ja3) { @@ -1293,7 +1295,8 @@ static inline int TLSDecodeHSHelloExtensionALPN( /* Only record the first value for JA4 */ if (ssl_state->curr_connp->ja4 != NULL && - ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + ssl_state->current_flags & + (SSL_AL_FLAG_STATE_CLIENT_HELLO | SSL_AL_FLAG_STATE_SERVER_HELLO)) { if (alpn_processed_len == 1) { SCJA4SetALPN(ssl_state->curr_connp->ja4, (const char *)input, protolen); } @@ -1492,7 +1495,8 @@ static inline int TLSDecodeHSHelloExtensions(SSLState *ssl_state, } if (ssl_state->curr_connp->ja4 != NULL && - ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { + ssl_state->current_flags & + (SSL_AL_FLAG_STATE_CLIENT_HELLO | SSL_AL_FLAG_STATE_SERVER_HELLO)) { if (TLSDecodeValueIsGREASE(ext_type) != 1) { SCJA4AddExtension(ssl_state->curr_connp->ja4, ext_type); } @@ -1546,11 +1550,11 @@ 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 && + /* Ensure that we have a JA4 state defined by now, we are in a client/server hello + and we don't have such a state yet (to avoid leaking memory in case this function + is entered more than once). */ + if (ssl_state->current_flags & + (SSL_AL_FLAG_STATE_CLIENT_HELLO | SSL_AL_FLAG_STATE_SERVER_HELLO) && ssl_state->curr_connp->ja4 == NULL) { ssl_state->curr_connp->ja4 = SCJA4New(); } @@ -2894,6 +2898,8 @@ static void SSLStateFree(void *p) if (ssl_state->client_connp.ja4) SCJA4Free(ssl_state->client_connp.ja4); + if (ssl_state->server_connp.ja4) + SCJA4Free(ssl_state->server_connp.ja4); if (ssl_state->client_connp.ja3_str) Ja3BufferFree(&ssl_state->client_connp.ja3_str); if (ssl_state->client_connp.ja3_hash) diff --git a/src/detect-ja4-hash.c b/src/detect-ja4-hash.c index 78a9367fdaa5..1cd262782251 100644 --- a/src/detect-ja4-hash.c +++ b/src/detect-ja4-hash.c @@ -34,8 +34,6 @@ #include "detect-engine-prefilter.h" #include "detect-ja4-hash.h" -#include "util-ja4.h" - #include "app-layer-ssl.h" #ifndef HAVE_JA4 diff --git a/src/output-json-tls.c b/src/output-json-tls.c index c4ba0e249e62..9b382a55ec0d 100644 --- a/src/output-json-tls.c +++ b/src/output-json-tls.c @@ -35,29 +35,30 @@ #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) -#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) +#define LOG_TLS_FIELD_SERVER_HANDSHAKE BIT_U64(21) typedef struct { const char *name; @@ -86,6 +87,8 @@ 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 }, + { "server_handshake", LOG_TLS_FIELD_SERVER_HANDSHAKE }, { NULL, -1 }, // clang-format on }; @@ -190,10 +193,10 @@ static void JsonTlsLogSerial(JsonBuilder *js, SSLState *ssl_state) } } -static void JsonTlsLogVersion(JsonBuilder *js, SSLState *ssl_state) +static void JsonTlsLogVersion(JsonBuilder *js, const uint16_t version) { char ssl_version[SSL_VERSION_MAX_STRLEN]; - SSLVersionToString(ssl_state->server_connp.version, ssl_version); + SSLVersionToString(version, ssl_version); jb_set_string(js, "version", ssl_version); } @@ -244,7 +247,7 @@ static void JsonTlsLogJa3(JsonBuilder *js, SSLState *ssl_state) static void JsonTlsLogSCJA4(JsonBuilder *js, SSLState *ssl_state) { - if (ssl_state->client_connp.ja4 != NULL) { + if (SSLJA4IsEnabled() && 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); @@ -374,6 +377,71 @@ static void JsonTlsLogClientCert( } } +static void JsonTlsLogClientHandshake(JsonBuilder *js, SSLState *ssl_state) +{ + const uint16_t *val; + uintptr_t i, nr; + + if (ssl_state->client_connp.ja4 == NULL) { + return; + } + + jb_open_object(js, "client_handshake"); + + const uint16_t vers = SCJA4GetVersion(ssl_state->client_connp.ja4); + JsonTlsLogVersion(js, vers); + + val = SCJA4GetCiphers(ssl_state->client_connp.ja4, &nr); + jb_open_array(js, "ciphers"); + for (i = 0; i < nr; i++) { + jb_append_uint(js, val[i]); + } + jb_close(js); + + val = SCJA4GetExtensions(ssl_state->client_connp.ja4, &nr); + jb_open_array(js, "exts"); + for (i = 0; i < nr; i++) { + jb_append_uint(js, val[i]); + } + jb_close(js); + + val = SCJA4GetSigAlgs(ssl_state->client_connp.ja4, &nr); + jb_open_array(js, "sig_algs"); + for (i = 0; i < nr; i++) { + jb_append_uint(js, val[i]); + } + jb_close(js); + + jb_close(js); +} + +static void JsonTlsLogServerHandshake(JsonBuilder *js, SSLState *ssl_state) +{ + const uint16_t *val; + uintptr_t i, nr; + + if (ssl_state->server_connp.ja4 == NULL) { + return; + } + + jb_open_object(js, "server_handshake"); + + const uint16_t vers = SCJA4GetVersion(ssl_state->server_connp.ja4); + JsonTlsLogVersion(js, vers); + + const uint16_t choosen_cipher = SCJA4GetFirstCipher(ssl_state->server_connp.ja4); + jb_set_uint(js, "cipher", choosen_cipher); + + val = SCJA4GetExtensions(ssl_state->server_connp.ja4, &nr); + jb_open_array(js, "exts"); + for (i = 0; i < nr; i++) { + jb_append_uint(js, val[i]); + } + jb_close(js); + + jb_close(js); +} + static void JsonTlsLogFields(JsonBuilder *js, SSLState *ssl_state, uint64_t fields) { /* tls subject */ @@ -406,7 +474,7 @@ static void JsonTlsLogFields(JsonBuilder *js, SSLState *ssl_state, uint64_t fiel /* tls version */ if (fields & LOG_TLS_FIELD_VERSION) - JsonTlsLogVersion(js, ssl_state); + JsonTlsLogVersion(js, ssl_state->server_connp.version); /* tls notbefore */ if (fields & LOG_TLS_FIELD_NOTBEFORE) @@ -453,6 +521,14 @@ static void JsonTlsLogFields(JsonBuilder *js, SSLState *ssl_state, uint64_t fiel jb_close(js); } } + + /* tls client handshake parameters */ + if (fields & LOG_TLS_FIELD_CLIENT_HANDSHAKE) + JsonTlsLogClientHandshake(js, ssl_state); + + /* tls server handshake parameters */ + if (fields & LOG_TLS_FIELD_SERVER_HANDSHAKE) + JsonTlsLogServerHandshake(js, ssl_state); } bool JsonTlsLogJSONExtended(void *vtx, JsonBuilder *tjs) diff --git a/src/util-ja4.h b/src/util-ja4.h deleted file mode 100644 index 769e089652d8..000000000000 --- a/src/util-ja4.h +++ /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 - */ - -#ifndef SURICATA_UTIL_JA4_H -#define SURICATA_UTIL_JA4_H - -#define JA4_HEX_LEN 36 - -#endif /* SURICATA_UTIL_JA4_H */