diff --git a/rust/src/handshake.rs b/rust/src/handshake.rs index d8687a0569..1a6edead72 100644 --- a/rust/src/handshake.rs +++ b/rust/src/handshake.rs @@ -18,11 +18,13 @@ */ -use crate::jsonbuilder::HEX; use libc::c_uchar; +use std::ffi::CStr; use std::os::raw::c_char; use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion}; +use crate::jsonbuilder::{JsonBuilder, JsonError}; + #[derive(Debug, PartialEq)] pub struct HandshakeParams { pub(crate) tls_version: Option, @@ -30,7 +32,7 @@ pub struct HandshakeParams { pub(crate) extensions: Vec, pub(crate) signature_algorithms: Vec, pub(crate) domain: bool, - pub(crate) alpn: [char; 2], + pub(crate) alpns: Vec>, pub(crate) quic: bool, } @@ -42,7 +44,7 @@ impl Default for HandshakeParams { impl HandshakeParams { #[inline] - fn is_grease(val: u16) -> bool { + pub(crate) 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, @@ -57,7 +59,7 @@ impl HandshakeParams { extensions: Vec::with_capacity(20), signature_algorithms: Vec::with_capacity(20), domain: false, - alpn: ['0', '0'], + alpns: Vec::with_capacity(4), quic: false, } } @@ -79,25 +81,11 @@ impl HandshakeParams { } } - 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 fn add_alpn(&mut self, alpn: &[u8]) { + if alpn.is_empty() { + return; } + self.alpns.push(alpn.to_vec()); } pub(crate) fn add_cipher_suite(&mut self, cipher: TlsCipherSuiteID) { @@ -123,6 +111,27 @@ impl HandshakeParams { } self.signature_algorithms.push(sigalgo); } + + fn log_alpns(&self, js: &mut JsonBuilder, key: &str) -> Result<(), JsonError> { + if self.alpns.is_empty() { + return Ok(()); + } + js.open_array(key)?; + + for v in &self.alpns { + js.append_string_from_bytes(v)?; + } + js.close()?; + Ok(()) + } +} + +// Objects used to allow C to call into this struct via the below C ABI +// that enables the return of a u8 pointer and length +#[repr(C)] +pub struct CStringData { + data: *const u8, + len: usize, } #[no_mangle] @@ -131,6 +140,11 @@ pub extern "C" fn SCTLSHandshakeNew() -> *mut HandshakeParams { Box::into_raw(hs) } +#[no_mangle] +pub unsafe extern "C" fn SCTLSHandshakeIsEmpty(hs: & HandshakeParams) -> bool { + *hs == HandshakeParams::default() +} + #[no_mangle] pub unsafe extern "C" fn SCTLSHandshakeSetTLSVersion(hs: &mut HandshakeParams, version: u16) { hs.set_tls_version(TlsVersion(version)); @@ -152,11 +166,11 @@ pub unsafe extern "C" fn SCTLSHandshakeAddSigAlgo(hs: &mut HandshakeParams, siga } #[no_mangle] -pub unsafe extern "C" fn SCTLSHandshakeSetALPN( - hs: &mut HandshakeParams, proto: *const c_char, len: u16, +pub unsafe extern "C" fn SCTLSHandshakeAddALPN( + hs: &mut HandshakeParams, alpn: *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); + let b: &[u8] = std::slice::from_raw_parts(alpn as *const c_uchar, len as usize); + hs.add_alpn(b); } #[no_mangle] @@ -165,6 +179,39 @@ pub unsafe extern "C" fn SCTLSHandshakeFree(hs: &mut HandshakeParams) { std::mem::drop(hs); } +#[no_mangle] +pub unsafe extern "C" fn SCTLSHandshakeLogALPNs( + hs: &HandshakeParams, js: *mut JsonBuilder, ptr: *const c_char +) -> bool { + if js.is_null() { + return false; + } + + if let Ok(key) = CStr::from_ptr(ptr).to_str() { + hs.log_alpns(js.as_mut().unwrap(), key).is_ok() + } else { + false + } +} + +#[no_mangle] +pub unsafe extern "C" fn SCTLSHandshakeGetALPN( + hs: &HandshakeParams, idx: u32, out: *mut CStringData, +) -> bool { + if out.is_null() { + return false; + } + if let Some(alpn) = hs.alpns.get(idx as usize) { + *out = CStringData { + data: alpn.as_ptr(), + len: alpn.len(), + }; + true + } else { + false + } +} + #[cfg(test)] mod tests { use super::*; @@ -255,25 +302,4 @@ mod tests { 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']); - } } diff --git a/rust/src/ja4.rs b/rust/src/ja4.rs index 50e3d5535c..83e6480c8f 100644 --- a/rust/src/ja4.rs +++ b/rust/src/ja4.rs @@ -18,6 +18,8 @@ */ #[cfg(feature = "ja4")] +use crate::jsonbuilder::HEX; +#[cfg(feature = "ja4")] use digest::Digest; #[cfg(feature = "ja4")] use sha2::Sha256; @@ -60,6 +62,33 @@ impl JA4 { _ => "00", } } + + fn format_alpn(alpn: Option<&Vec>) -> [char; 2] { + let mut ret = ['0', '0']; + + if let Some(alpn) = alpn { + 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 HandshakeParams::is_grease(v) { + return ret; + } + } + 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. + ret[0] = char::from(HEX[(alpn[0] >> 4) as usize]); + ret[1] = char::from(HEX[(alpn[alpn.len() - 1] & 0xF) as usize]); + } else { + ret[0] = char::from(alpn[0]); + ret[1] = char::from(alpn[alpn.len() - 1]); + } + } + } + ret + } } #[cfg(feature = "ja4")] @@ -76,7 +105,7 @@ impl JA4Impl for JA4 { }) .collect::>(); - let alpn = hs.alpn; + let alpn = Self::format_alpn(hs.alpns.first()); // Calculate JA4_a let ja4_a = format!( @@ -147,7 +176,25 @@ pub unsafe extern "C" fn SCJA4GetHash(hs: &HandshakeParams, out: &mut [u8; JA4_H #[cfg(feature = "ja4")] mod tests { use super::*; - use tls_parser::TlsCipherSuiteID; + use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion}; + + #[test] + fn test_format_alpn_ascii() { + let res = JA4::format_alpn(Some(&"http/1.1".as_bytes().to_vec())); + assert_eq!(res, ['h', '1']); + } + + #[test] + fn test_add_alpn_non_ascii_first_or_last() { + let res = JA4::format_alpn(Some(&vec![0x01, b'T', b'E', 0x7f])); // non-alphanumeric start and end + assert_eq!(res, [HEX[0x0], HEX[0xF]].map(|b| b as char)); // 0x01 -> 0, 0x7f -> f + } + + #[test] + fn test_add_alpn_grease_pair_filtered() { + let res = JA4::format_alpn(Some(&vec![0x2a, 0x2a])); // 0x2a2a GREASE + assert_eq!(res, ['0', '0']); + } #[test] fn test_hash_limit_numbers() { @@ -175,8 +222,7 @@ mod tests { #[test] fn test_short_alpn() { let mut hs = HandshakeParams::default(); - - hs.set_alpn("b".as_bytes()); + hs.add_alpn("b".as_bytes()); let mut s = JA4::try_new(&hs) .expect("JA4 create failure") .as_ref() @@ -184,7 +230,8 @@ mod tests { s.truncate(10); assert_eq!(s, "t00i0000bb"); - hs.set_alpn("h2".as_bytes()); + let mut hs = HandshakeParams::default(); + hs.add_alpn("h2".as_bytes()); let mut s = JA4::try_new(&hs) .expect("JA4 create failure") .as_ref() @@ -193,7 +240,8 @@ mod tests { assert_eq!(s, "t00i0000h2"); // from https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md#alpn-extension-value - hs.set_alpn(&[0xab]); + let mut hs = HandshakeParams::default(); + hs.add_alpn(&[0xab]); let mut s = JA4::try_new(&hs) .expect("JA4 create failure") .as_ref() @@ -201,7 +249,8 @@ mod tests { s.truncate(10); assert_eq!(s, "t00i0000ab"); - hs.set_alpn(&[0xab, 0xcd]); + let mut hs = HandshakeParams::default(); + hs.add_alpn(&[0xab, 0xcd]); let mut s = JA4::try_new(&hs) .expect("JA4 create failure") .as_ref() @@ -209,7 +258,8 @@ mod tests { s.truncate(10); assert_eq!(s, "t00i0000ad"); - hs.set_alpn(&[0x30, 0xab]); + let mut hs = HandshakeParams::default(); + hs.add_alpn(&[0x30, 0xab]); let mut s = JA4::try_new(&hs) .expect("JA4 create failure") .as_ref() @@ -217,7 +267,8 @@ mod tests { s.truncate(10); assert_eq!(s, "t00i00003b"); - hs.set_alpn(&[0x30, 0x31, 0xab, 0xcd]); + let mut hs = HandshakeParams::default(); + hs.add_alpn(&[0x30, 0x31, 0xab, 0xcd]); let mut s = JA4::try_new(&hs) .expect("JA4 create failure") .as_ref() @@ -225,7 +276,8 @@ mod tests { s.truncate(10); assert_eq!(s, "t00i00003d"); - hs.set_alpn(&[0x30, 0xab, 0xcd, 0x31]); + let mut hs = HandshakeParams::default(); + hs.add_alpn(&[0x30, 0xab, 0xcd, 0x31]); let mut s = JA4::try_new(&hs) .expect("JA4 create failure") .as_ref() @@ -263,7 +315,7 @@ mod tests { assert_eq!(s.as_ref(), "q12d000100_e3b0c44298fc_d2e2adf7177b"); // set ALPN extension, should only increase count and set end of JA4_a - hs.set_alpn(b"h3-16"); + hs.add_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"); diff --git a/rust/src/quic/frames.rs b/rust/src/quic/frames.rs index bafbdfd589..7a5717c5cc 100644 --- a/rust/src/quic/frames.rs +++ b/rust/src/quic/frames.rs @@ -295,7 +295,7 @@ fn quic_get_tls_extensions( TlsExtension::ALPN(x) => { if !x.is_empty() { if let Some(ref mut hs) = hs { - hs.set_alpn(x[0]); + hs.add_alpn(x[0]); } } for alpn in x { diff --git a/src/app-layer-ssl.c b/src/app-layer-ssl.c index 027510a77f..0aa2e6b775 100644 --- a/src/app-layer-ssl.c +++ b/src/app-layer-ssl.c @@ -1289,18 +1289,6 @@ invalid_length: return -1; } -static void StoreALPN(SSLStateConnp *connp, const uint8_t *alpn, const uint32_t size) -{ - if (size > 0) { - SSLAlpns *a = SCCalloc(1, sizeof(*a) + size); - if (a != NULL) { - memcpy(a->alpn, alpn, size); - a->size = size; - TAILQ_INSERT_TAIL(&connp->alpns, a, next); - } - } -} - static inline int TLSDecodeHSHelloExtensionALPN( SSLState *ssl_state, const uint8_t *const initial_input, const uint32_t input_len) { @@ -1336,13 +1324,7 @@ static inline int TLSDecodeHSHelloExtensionALPN( input += alpn_len - alpn_processed_len; break; } - - if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) { - if (alpn_processed_len == 1) { - SCTLSHandshakeSetALPN(ssl_state->curr_connp->hs, (const char *)input, protolen); - } - } - StoreALPN(ssl_state->curr_connp, input, protolen); + SCTLSHandshakeAddALPN(ssl_state->curr_connp->hs, (const char *)input, protolen); alpn_processed_len += protolen; input += protolen; @@ -2939,9 +2921,7 @@ static void *SSLStateAlloc(void *orig_state, AppProto proto_orig) 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); - TAILQ_INIT(&ssl_state->client_connp.alpns); return (void *)ssl_state; } @@ -3030,18 +3010,6 @@ static void SSLStateFree(void *p) } TAILQ_INIT(&ssl_state->client_connp.certs); - SSLAlpns *a; - while ((a = TAILQ_FIRST(&ssl_state->server_connp.alpns))) { - TAILQ_REMOVE(&ssl_state->server_connp.alpns, a, next); - SCFree(a); - } - TAILQ_INIT(&ssl_state->server_connp.alpns); - while ((a = TAILQ_FIRST(&ssl_state->client_connp.alpns))) { - TAILQ_REMOVE(&ssl_state->client_connp.alpns, a, next); - SCFree(a); - } - TAILQ_INIT(&ssl_state->client_connp.alpns); - SCFree(ssl_state); } diff --git a/src/app-layer-ssl.h b/src/app-layer-ssl.h index 51c43c5ce2..4dd9a4e025 100644 --- a/src/app-layer-ssl.h +++ b/src/app-layer-ssl.h @@ -231,12 +231,6 @@ typedef struct SSLCertsChain_ { TAILQ_ENTRY(SSLCertsChain_) next; } SSLCertsChain; -typedef struct SSLAlpns_ { - TAILQ_ENTRY(SSLAlpns_) next; - uint32_t size; - uint8_t alpn[]; -} SSLAlpns; - typedef struct SSLStateConnp_ { /* record length */ uint32_t record_length; @@ -272,7 +266,6 @@ typedef struct SSLStateConnp_ { char *session_id; TAILQ_HEAD(, SSLCertsChain_) certs; - TAILQ_HEAD(, SSLAlpns_) alpns; uint8_t *certs_buffer; uint32_t certs_buffer_size; diff --git a/src/detect-tls-alpn.c b/src/detect-tls-alpn.c index 71d7538e7c..3a406a824a 100644 --- a/src/detect-tls-alpn.c +++ b/src/detect-tls-alpn.c @@ -62,6 +62,7 @@ static bool TlsAlpnGetData(DetectEngineThreadCtx *det_ctx, const void *txv, cons const SSLState *ssl_state = (SSLState *)txv; const SSLStateConnp *connp; + CStringData d; if (flags & STREAM_TOSERVER) { connp = &ssl_state->client_connp; @@ -69,27 +70,13 @@ static bool TlsAlpnGetData(DetectEngineThreadCtx *det_ctx, const void *txv, cons connp = &ssl_state->server_connp; } - if (TAILQ_EMPTY(&connp->alpns)) { - return false; - } - - SSLAlpns *a; - if (idx == 0) { - a = TAILQ_FIRST(&connp->alpns); + if (SCTLSHandshakeGetALPN(connp->hs, idx, &d)) { + *buf = d.data; + *buf_len = d.len; + return true; } else { - // TODO optimize ? - a = TAILQ_FIRST(&connp->alpns); - for (uint32_t i = 0; i < idx; i++) { - a = TAILQ_NEXT(a, next); - } - } - if (a == NULL) { return false; } - - *buf = a->alpn; - *buf_len = a->size; - return true; } /** diff --git a/src/output-json-tls.c b/src/output-json-tls.c index f339155a2e..45bb5fcb8e 100644 --- a/src/output-json-tls.c +++ b/src/output-json-tls.c @@ -277,20 +277,14 @@ static void JsonTlsLogJa3S(SCJsonBuilder *js, SSLState *ssl_state) static void JsonTlsLogAlpns(SCJsonBuilder *js, SSLStateConnp *connp, const char *object) { - if (TAILQ_EMPTY(&connp->alpns)) { + if (connp->hs == NULL) { return; } - SSLAlpns *a = TAILQ_FIRST(&connp->alpns); - if (a == NULL) { + if (SCTLSHandshakeIsEmpty(connp->hs)) { return; } - - SCJbOpenArray(js, object); - TAILQ_FOREACH (a, &connp->alpns, next) { - SCJbAppendStringFromBytes(js, a->alpn, a->size); - } - SCJbClose(js); + SCTLSHandshakeLogALPNs(connp->hs, js, object); } static void JsonTlsLogCertificate(SCJsonBuilder *js, SSLStateConnp *connp)