tls: Integrate ALPNs into HandshakeParams object

Ticket: 6695

With the introduction of the HandshakeParams object we're able to
utilise the theory further by using it as the object to track the ALPNs.

The HandshakeParams object is now responsible for holding all ALPNS. The
user of this HandshakeParams object i.e. JA4, can use whichever fields
are needed. So only when we generate a JA4 hash do we use the first ALPN
and require to format it. Other users of HandshakeParams may opt to use
all ALPN's i.e. during TlsAlpnGetData().
pull/13251/head
Richard McConnell 4 months ago committed by Victor Julien
parent 468a037daa
commit 6c1238b7bd

@ -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<TlsVersion>,
@ -30,7 +32,7 @@ pub struct HandshakeParams {
pub(crate) extensions: Vec<TlsExtensionType>,
pub(crate) signature_algorithms: Vec<u16>,
pub(crate) domain: bool,
pub(crate) alpn: [char; 2],
pub(crate) alpns: Vec<Vec<u8>>,
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']);
}
}

@ -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<u8>>) -> [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::<Vec<&TlsExtensionType>>();
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");

@ -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 {

@ -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);
}

@ -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;

@ -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;
}
/**

@ -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)

Loading…
Cancel
Save