From c2df226515b084bdba135c47604e37ffae801bdf Mon Sep 17 00:00:00 2001 From: Gianni Tedesco Date: Fri, 9 Aug 2024 19:14:00 +0900 Subject: [PATCH] Output/TLS: Allow logging of client/server handshake parameters 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 - "server_handshake" which logs the following: 1. TLS version used during handshake 2. The chosen cipher suite, excluding GREASE 3. TLS extensions, excluding GREASE The use-case is for logging TLS handshake parameters in order to survey them, and so that JA4(S) 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 | 5 + etc/schema.json | 51 ++++++ rust/src/ja4.rs | 181 +++++++++++++++---- rust/src/quic/quic.rs | 2 +- src/app-layer-ssl.c | 26 ++- src/output-json-tls.c | 138 +++++++++++--- src/util-ja4.h | 3 +- 7 files changed, 333 insertions(+), 73 deletions(-) diff --git a/doc/userguide/output/eve/eve-json-format.rst b/doc/userguide/output/eve/eve-json-format.rst index 952945dffc98..bc82a5eff63c 100644 --- a/doc/userguide/output/eve/eve-json-format.rst +++ b/doc/userguide/output/eve/eve-json-format.rst @@ -1040,6 +1040,11 @@ If extended logging is enabled the following fields are also included: * "ja4": The JA4 client fingerprint for TLS * "client_alpns": array of strings with ALPN values * "server_alpns": array of strings with ALPN values +* "client_handshake": structure containing "version", "ciphers", "exts", "sig_algs", 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", for server hello + in the order that they're mentioned (ie. unsorted) 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'). diff --git a/etc/schema.json b/etc/schema.json index 818ad8a24dc8..83300da33bf0 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -6722,6 +6722,35 @@ "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_alpns": { "description": "TLS server ALPN field(s)", "type": "array", @@ -6729,6 +6758,25 @@ "type": "string" } }, + "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" + } + } + } + }, "fingerprint": { "type": "string" }, @@ -6792,6 +6840,9 @@ }, "ja4": { "type": "string" + }, + "ja4s": { + "type": "string" } }, "additionalProperties": false diff --git a/rust/src/ja4.rs b/rust/src/ja4.rs index 4660f2330227..458c012a5540 100644 --- a/rust/src/ja4.rs +++ b/rust/src/ja4.rs @@ -39,10 +39,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 +61,6 @@ impl JA4 { domain: false, alpn: ['0', '0'], quic: false, - nof_exts: 0, } } pub fn set_quic(&mut self) {} @@ -74,7 +69,10 @@ impl JA4 { 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 { + pub fn get_ja4_hash(&self) -> String { + String::new() + } + pub fn get_ja4s_hash(&self) -> String { String::new() } } @@ -113,7 +111,6 @@ impl JA4 { domain: false, alpn: ['0', '0'], quic: false, - nof_exts: 0, } } @@ -170,14 +167,11 @@ 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) { @@ -187,7 +181,18 @@ impl JA4 { self.signature_algorithms.push(sigalgo); } - pub fn get_hash(&self) -> String { + pub fn get_ja4_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 +200,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 +219,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 @@ -234,6 +238,33 @@ impl JA4 { return format!("{}_{}_{}", ja4_a, ja4_b, ja4_c); } + + pub fn get_ja4s_hash(&self) -> String { + // Calculate JA4_a + let ja4_a = format!( + "{proto}{version}{nof_e:02}{al1}{al2}", + proto = if self.quic { "q" } else { "t" }, + version = JA4::version_to_ja4code(self.tls_version), + nof_e = min(99, self.extensions.len()), + al1 = self.alpn[0], + al2 = self.alpn[1] + ); + + let ja4_b = self.ciphersuites.first().map(|&v| *v).unwrap_or(0); + + let mut sha = Sha256::new(); + let sorted_extstrings = self.extensions + .iter() + .map(|&v| format!("{:04x}", u16::from(v))) + .collect::>() + .join(","); + + sha.update(sorted_extstrings); + let mut ja4_c = format!("{:x}", sha.finalize()); + ja4_c.truncate(12); + + format!("{}_{:04x}_{}", ja4_a, ja4_b, ja4_c) + } } #[no_mangle] @@ -268,12 +299,46 @@ pub unsafe extern "C" fn SCJA4SetALPN(j: &mut JA4, proto: *const c_char, len: u1 j.set_alpn(b); } +#[no_mangle] +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; 36]) { - let hash = j.get_hash(); + let hash = j.get_ja4_hash(); out[0..36].copy_from_slice(hash.as_bytes()); } +#[no_mangle] +pub unsafe extern "C" fn SCJA4SGetHash(j: &mut JA4, out: &mut [u8; 25]) { + let hash = j.get_ja4s_hash(); + out[0..25].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] pub unsafe extern "C" fn SCJA4Free(j: &mut JA4) { let ja4: Box = Box::from_raw(j); @@ -322,7 +387,7 @@ mod tests { j.add_extension(TlsExtensionType(i)); } - let mut s = j.get_hash(); + let mut s = j.get_ja4_hash(); s.truncate(10); assert_eq!(s, "t00i999900"); } @@ -332,81 +397,81 @@ mod tests { let mut j = JA4::new(); j.set_alpn("b".as_bytes()); - let mut s = j.get_hash(); + let mut s = j.get_ja4_hash(); s.truncate(10); assert_eq!(s, "t00i0000bb"); j.set_alpn("h2".as_bytes()); - let mut s = j.get_hash(); + let mut s = j.get_ja4_hash(); 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(); + let mut s = j.get_ja4_hash(); s.truncate(10); assert_eq!(s, "t00i0000ab"); j.set_alpn(&[0xab, 0xcd]); - let mut s = j.get_hash(); + let mut s = j.get_ja4_hash(); s.truncate(10); assert_eq!(s, "t00i0000ad"); j.set_alpn(&[0x30, 0xab]); - let mut s = j.get_hash(); + let mut s = j.get_ja4_hash(); s.truncate(10); assert_eq!(s, "t00i00003b"); j.set_alpn(&[0x30, 0x31, 0xab, 0xcd]); - let mut s = j.get_hash(); + let mut s = j.get_ja4_hash(); s.truncate(10); assert_eq!(s, "t00i00003d"); j.set_alpn(&[0x30, 0xab, 0xcd, 0x31]); - let mut s = j.get_hash(); + let mut s = j.get_ja4_hash(); s.truncate(10); assert_eq!(s, "t00i000001"); } #[test] - fn test_get_hash() { + fn test_get_ja4_hash() { let mut j = JA4::new(); // the empty JA4 hash - let s = j.get_hash(); + let s = j.get_ja4_hash(); assert_eq!(s, "t00i000000_e3b0c44298fc_d2e2adf7177b"); // set TLS version j.set_tls_version(TlsVersion::Tls12); - let s = j.get_hash(); + let s = j.get_ja4_hash(); assert_eq!(s, "t12i000000_e3b0c44298fc_d2e2adf7177b"); // set QUIC j.set_quic(); - let s = j.get_hash(); + let s = j.get_ja4_hash(); assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b"); // set GREASE extension, should be ignored j.add_extension(TlsExtensionType(0x0a0a)); - let s = j.get_hash(); + let s = j.get_ja4_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(); + let s = j.get_ja4_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(); + let s = j.get_ja4_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(); + let s = j.get_ja4_hash(); assert_eq!(s, "q12d0302h6_f500716053f9_d2e2adf7177b"); // set some extensions and signature algorithms @@ -414,7 +479,47 @@ mod tests { j.add_extension(TlsExtensionType(0x0121)); j.add_extension(TlsExtensionType(0x1234)); j.add_signature_algorithm(0x6666); - let s = j.get_hash(); + let s = j.get_ja4_hash(); assert_eq!(s, "q12d0305h6_f500716053f9_2debc8880bae"); } + + #[test] + fn test_get_ja4s_hash() { + let mut j = JA4::new(); + + // the empty JA4S hash + let s = j.get_ja4s_hash(); + assert_eq!(s, "t000000_0000_e3b0c44298fc"); + + // set TLS version + j.set_tls_version(TlsVersion::Tls12); + let s = j.get_ja4s_hash(); + assert_eq!(s, "t120000_0000_e3b0c44298fc"); + + // set QUIC + j.set_quic(); + let s = j.get_ja4s_hash(); + assert_eq!(s, "q120000_0000_e3b0c44298fc"); + + // set GREASE extension, should be ignored + j.add_extension(TlsExtensionType(0x0a0a)); + let s = j.get_ja4s_hash(); + assert_eq!(s, "q120000_0000_e3b0c44298fc"); + + // set SNI extension, should increase ext-count + change JA4S_c + j.add_extension(TlsExtensionType(0x0000)); + let s = j.get_ja4s_hash(); + assert_eq!(s, "q120100_0000_9af15b336e6a"); + + // set ALPN extension, should increase count, set end of JA4S_a + change JA4S_c + j.set_alpn(b"h3-16"); + j.add_extension(TlsExtensionType::ApplicationLayerProtocolNegotiation); + let s = j.get_ja4s_hash(); + assert_eq!(s, "q1202h6_0000_a2cf1a00988d"); + + // set chosen cipher suite + j.add_cipher_suite(TlsCipherSuiteID(0xc027)); + let s = j.get_ja4s_hash(); + assert_eq!(s, "q1202h6_c027_a2cf1a00988d"); + } } diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 442cfc944a65..82c6bf6e9d06 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -250,7 +250,7 @@ impl QuicState { // our hash is complete, let's only use strings from // now on if let Some(ref rja4) = c.ja4 { - ja4 = Some(rja4.get_hash()); + ja4 = Some(rja4.get_ja4_hash()); } } for e in &c.extv { diff --git a/src/app-layer-ssl.c b/src/app-layer-ssl.c index e387c6cc46c4..31e2cdc9de93 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/output-json-tls.c b/src/output-json-tls.c index c4ba0e249e62..4228b2f443f2 100644 --- a/src/output-json-tls.c +++ b/src/output-json-tls.c @@ -38,26 +38,29 @@ #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_JA4S BIT_U64(20) +#define LOG_TLS_FIELD_CLIENT_HANDSHAKE BIT_U64(21) +#define LOG_TLS_FIELD_SERVER_HANDSHAKE BIT_U64(22) typedef struct { const char *name; @@ -86,6 +89,9 @@ TlsFields tls_fields[] = { { "subjectaltname", LOG_TLS_FIELD_SUBJECTALTNAME }, { "client_alpns", LOG_TLS_FIELD_CLIENT_ALPNS }, { "server_alpns", LOG_TLS_FIELD_SERVER_ALPNS }, + { "ja4s", LOG_TLS_FIELD_JA4S }, + { "client_handshake", LOG_TLS_FIELD_CLIENT_HANDSHAKE }, + { "server_handshake", LOG_TLS_FIELD_SERVER_HANDSHAKE }, { NULL, -1 }, // clang-format on }; @@ -190,10 +196,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); } @@ -252,6 +258,15 @@ static void JsonTlsLogSCJA4(JsonBuilder *js, SSLState *ssl_state) } } +static void JsonTlsLogSCJA4S(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.ja4 != NULL) { + uint8_t buffer[JA4S_HEX_LEN]; + SCJA4SGetHash(ssl_state->server_connp.ja4, (uint8_t(*)[JA4S_HEX_LEN])buffer); + jb_set_string_from_bytes(js, "ja4s", buffer, JA4S_HEX_LEN); + } +} + static void JsonTlsLogJa3SHash(JsonBuilder *js, SSLState *ssl_state) { if (ssl_state->server_connp.ja3_hash != NULL) { @@ -301,6 +316,71 @@ static void JsonTlsLogAlpns(JsonBuilder *js, SSLStateConnp *connp, const char *o jb_close(js); } +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 JsonTlsLogCertificate(JsonBuilder *js, SSLStateConnp *connp) { if (TAILQ_EMPTY(&connp->certs)) { @@ -406,7 +486,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) @@ -436,6 +516,10 @@ static void JsonTlsLogFields(JsonBuilder *js, SSLState *ssl_state, uint64_t fiel if (fields & LOG_TLS_FIELD_JA4) JsonTlsLogSCJA4(js, ssl_state); + /* tls ja4s */ + if (fields & LOG_TLS_FIELD_JA4S) + JsonTlsLogSCJA4S(js, ssl_state); + if (fields & LOG_TLS_FIELD_CLIENT_ALPNS) { JsonTlsLogAlpns(js, &ssl_state->client_connp, "client_alpns"); } @@ -453,6 +537,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 index 769e089652d8..aa7a72c14977 100644 --- a/src/util-ja4.h +++ b/src/util-ja4.h @@ -24,6 +24,7 @@ #ifndef SURICATA_UTIL_JA4_H #define SURICATA_UTIL_JA4_H -#define JA4_HEX_LEN 36 +#define JA4_HEX_LEN 36 +#define JA4S_HEX_LEN 25 #endif /* SURICATA_UTIL_JA4_H */