--- /dev/null
+/* Copyright (C) 2025 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.
+
+// Author: Sascha Steinbiss <sascha@steinbiss.name>
+
+*/
+
+use crate::jsonbuilder::HEX;
+use libc::c_uchar;
+use std::os::raw::c_char;
+use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion};
+
+#[derive(Debug, PartialEq)]
+pub struct HandshakeParams {
+ pub(crate) tls_version: Option<TlsVersion>,
+ pub(crate) ciphersuites: Vec<TlsCipherSuiteID>,
+ pub(crate) extensions: Vec<TlsExtensionType>,
+ pub(crate) signature_algorithms: Vec<u16>,
+ pub(crate) domain: bool,
+ pub(crate) alpn: [char; 2],
+ pub(crate) quic: bool,
+}
+
+impl Default for HandshakeParams {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl HandshakeParams {
+ #[inline]
+ 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,
+ _ => false,
+ }
+ }
+
+ fn new() -> Self {
+ Self {
+ tls_version: None,
+ ciphersuites: Vec::with_capacity(20),
+ extensions: Vec::with_capacity(20),
+ signature_algorithms: Vec::with_capacity(20),
+ domain: false,
+ alpn: ['0', '0'],
+ quic: false,
+ }
+ }
+
+ pub(crate) fn set_tls_version(&mut self, version: TlsVersion) {
+ if Self::is_grease(u16::from(version)) {
+ return;
+ }
+ // Track maximum of seen TLS versions
+ match self.tls_version {
+ None => {
+ self.tls_version = Some(version);
+ }
+ Some(cur_version) => {
+ if u16::from(version) > u16::from(cur_version) {
+ self.tls_version = Some(version);
+ }
+ }
+ }
+ }
+
+ 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(crate) fn add_cipher_suite(&mut self, cipher: TlsCipherSuiteID) {
+ if Self::is_grease(u16::from(cipher)) {
+ return;
+ }
+ self.ciphersuites.push(cipher);
+ }
+
+ pub(crate) fn add_extension(&mut self, ext: TlsExtensionType) {
+ if Self::is_grease(u16::from(ext)) {
+ return;
+ }
+ if ext == TlsExtensionType::ServerName {
+ self.domain = true;
+ }
+ self.extensions.push(ext);
+ }
+
+ pub(crate) fn add_signature_algorithm(&mut self, sigalgo: u16) {
+ if Self::is_grease(sigalgo) {
+ return;
+ }
+ self.signature_algorithms.push(sigalgo);
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn SCTLSHandshakeNew() -> *mut HandshakeParams {
+ let hs = Box::new(HandshakeParams::new());
+ Box::into_raw(hs)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeSetTLSVersion(hs: &mut HandshakeParams, version: u16) {
+ hs.set_tls_version(TlsVersion(version));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeAddCipher(hs: &mut HandshakeParams, cipher: u16) {
+ hs.add_cipher_suite(TlsCipherSuiteID(cipher));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeAddExtension(hs: &mut HandshakeParams, ext: u16) {
+ hs.add_extension(TlsExtensionType(ext));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeAddSigAlgo(hs: &mut HandshakeParams, sigalgo: u16) {
+ hs.add_signature_algorithm(sigalgo);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeSetALPN(
+ hs: &mut HandshakeParams, proto: *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);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCTLSHandshakeFree(hs: &mut HandshakeParams) {
+ let hs: Box<HandshakeParams> = Box::from_raw(hs);
+ std::mem::drop(hs);
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_is_grease() {
+ let mut alpn = "foobar".as_bytes();
+ let mut len = alpn.len();
+ let v: u16 = ((alpn[0] as u16) << 8) | alpn[len - 1] as u16;
+ assert!(!HandshakeParams::is_grease(v));
+
+ alpn = &[0x0a, 0x0a];
+ len = alpn.len();
+ let v: u16 = ((alpn[0] as u16) << 8) | alpn[len - 1] as u16;
+ assert!(HandshakeParams::is_grease(v));
+ }
+
+ #[test]
+ fn test_tlsversion_max() {
+ let mut hs = HandshakeParams::new();
+ assert_eq!(hs.tls_version, None);
+ hs.set_tls_version(TlsVersion::Ssl30);
+ assert_eq!(hs.tls_version, Some(TlsVersion::Ssl30));
+ hs.set_tls_version(TlsVersion::Tls12);
+ assert_eq!(hs.tls_version, Some(TlsVersion::Tls12));
+ hs.set_tls_version(TlsVersion::Tls10);
+ assert_eq!(hs.tls_version, Some(TlsVersion::Tls12));
+ }
+
+ #[test]
+ fn test_add_cipher_suite_filters_grease() {
+ let mut hs = HandshakeParams::new();
+ hs.add_cipher_suite(TlsCipherSuiteID(0x1a1a)); // GREASE
+ assert_eq!(hs.ciphersuites.len(), 0);
+ }
+
+ #[test]
+ fn test_add_cipher_suite_accepts_normal() {
+ let mut hs = HandshakeParams::new();
+ hs.add_cipher_suite(TlsCipherSuiteID(0x1301));
+ assert_eq!(hs.ciphersuites, &[TlsCipherSuiteID(0x1301)]);
+ }
+
+ #[test]
+ fn test_add_cipher_suite_len_tracking() {
+ let mut hs = HandshakeParams::new();
+ hs.add_cipher_suite(TlsCipherSuiteID(0x1301));
+ hs.add_cipher_suite(TlsCipherSuiteID(0x1302));
+ hs.add_cipher_suite(TlsCipherSuiteID(0x1a1a)); // GREASE
+ assert_eq!(hs.ciphersuites.len(), 2);
+ }
+
+ #[test]
+ fn test_add_extension_sets_domain_for_server_name() {
+ let mut hs = HandshakeParams::new();
+ hs.add_extension(TlsExtensionType::ServerName);
+ assert!(hs.domain);
+ }
+
+ #[test]
+ fn test_add_extension_filters_grease() {
+ let mut hs = HandshakeParams::new();
+ hs.add_extension(TlsExtensionType(0xaaaa)); // GREASE
+ assert_eq!(hs.extensions.len(), 0);
+ }
+
+ #[test]
+ fn test_add_extension_len_tracking() {
+ let mut hs = HandshakeParams::new();
+ hs.add_extension(TlsExtensionType::ClientCertificate);
+ hs.add_extension(TlsExtensionType::ServerName);
+ hs.add_extension(TlsExtensionType(0xaaaa)); // GREASE
+ assert_eq!(hs.extensions.len(), 2);
+ }
+
+ #[test]
+ fn test_add_signature_algorithm_filters_grease() {
+ let mut hs = HandshakeParams::new();
+ hs.add_signature_algorithm(0xbaba); // GREASE
+ assert_eq!(hs.signature_algorithms.len(), 0);
+ }
+
+ #[test]
+ fn test_add_signature_algorithm_len_tracking() {
+ let mut hs = HandshakeParams::new();
+ hs.add_signature_algorithm(0x1234);
+ hs.add_signature_algorithm(0x1235);
+ 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']);
+ }
+}
// Author: Sascha Steinbiss <sascha@steinbiss.name>
*/
-
#[cfg(feature = "ja4")]
use digest::Digest;
-use libc::c_uchar;
#[cfg(feature = "ja4")]
use sha2::Sha256;
#[cfg(feature = "ja4")]
use std::cmp::min;
-use std::os::raw::c_char;
-use tls_parser::{TlsCipherSuiteID, TlsExtensionType, TlsVersion};
#[cfg(feature = "ja4")]
-use crate::jsonbuilder::HEX;
+use tls_parser::{TlsExtensionType, TlsVersion};
+
+use crate::handshake::HandshakeParams;
+
+pub const JA4_HEX_LEN: usize = 36;
+
+pub(crate) trait JA4Impl {
+ fn try_new(hs: &HandshakeParams) -> Option<JA4>;
+}
#[derive(Debug, PartialEq)]
pub struct JA4 {
- tls_version: Option<TlsVersion>,
- ciphersuites: Vec<TlsCipherSuiteID>,
- extensions: Vec<TlsExtensionType>,
- signature_algorithms: Vec<u16>,
- 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,
+ hash: String,
}
-impl Default for JA4 {
- fn default() -> Self {
- Self::new()
- }
-}
-
-// Stubs for when JA4 is disabled
-#[cfg(not(feature = "ja4"))]
-impl JA4 {
- pub fn new() -> Self {
- Self {
- tls_version: None,
- // Vec::new() does not allocate memory until filled, which we
- // will not do here.
- ciphersuites: Vec::new(),
- extensions: Vec::new(),
- signature_algorithms: Vec::new(),
- domain: false,
- alpn: ['0', '0'],
- quic: false,
- nof_exts: 0,
- }
- }
- pub fn set_quic(&mut self) {}
- pub fn set_tls_version(&mut self, _version: TlsVersion) {}
- pub fn set_alpn(&mut self, _alpn: &[u8]) {}
- 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 {
- String::new()
+impl AsRef<str> for JA4 {
+ fn as_ref(&self) -> &str {
+ &self.hash
}
}
#[cfg(feature = "ja4")]
impl JA4 {
- #[inline]
- 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,
- _ => false,
- }
- }
-
#[inline]
fn version_to_ja4code(val: Option<TlsVersion>) -> &'static str {
match val {
_ => "00",
}
}
+}
- pub fn new() -> Self {
- Self {
- tls_version: None,
- ciphersuites: Vec::with_capacity(20),
- extensions: Vec::with_capacity(20),
- signature_algorithms: Vec::with_capacity(20),
- domain: false,
- alpn: ['0', '0'],
- quic: false,
- nof_exts: 0,
- }
- }
-
- pub fn set_quic(&mut self) {
- self.quic = true;
- }
-
- pub fn set_tls_version(&mut self, version: TlsVersion) {
- if JA4::is_grease(u16::from(version)) {
- return;
- }
- // Track maximum of seen TLS versions
- match self.tls_version {
- None => {
- self.tls_version = Some(version);
- }
- Some(cur_version) => {
- if u16::from(version) > u16::from(cur_version) {
- self.tls_version = Some(version);
- }
- }
- }
- }
-
- pub 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 JA4::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_cipher_suite(&mut self, cipher: TlsCipherSuiteID) {
- if JA4::is_grease(u16::from(cipher)) {
- return;
- }
- self.ciphersuites.push(cipher);
- }
+#[cfg(feature = "ja4")]
+impl JA4Impl for JA4 {
+ fn try_new(hs: &HandshakeParams) -> Option<Self> {
+ // 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 = hs
+ .extensions
+ .iter()
+ .filter(|&ext| {
+ *ext != TlsExtensionType::ApplicationLayerProtocolNegotiation
+ && *ext != TlsExtensionType::ServerName
+ })
+ .collect::<Vec<&TlsExtensionType>>();
- pub fn add_extension(&mut self, ext: TlsExtensionType) {
- if JA4::is_grease(u16::from(ext)) {
- return;
- }
- if ext != TlsExtensionType::ApplicationLayerProtocolNegotiation
- && ext != TlsExtensionType::ServerName
- {
- self.extensions.push(ext);
- } else if ext == TlsExtensionType::ServerName {
- self.domain = true;
- }
- self.nof_exts += 1;
- }
+ let alpn = hs.alpn;
- pub fn add_signature_algorithm(&mut self, sigalgo: u16) {
- if JA4::is_grease(sigalgo) {
- return;
- }
- self.signature_algorithms.push(sigalgo);
- }
-
- pub fn get_hash(&self) -> String {
// Calculate JA4_a
let ja4_a = format!(
"{proto}{version}{sni}{nof_c:02}{nof_e:02}{al1}{al2}",
- proto = if self.quic { "q" } else { "t" },
- 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),
- al1 = self.alpn[0],
- al2 = self.alpn[1]
+ proto = if hs.quic { "q" } else { "t" },
+ version = Self::version_to_ja4code(hs.tls_version),
+ sni = if hs.domain { "d" } else { "i" },
+ nof_c = min(99, hs.ciphersuites.len()),
+ nof_e = min(99, hs.extensions.len()),
+ al1 = alpn[0],
+ al2 = alpn[1]
);
// Calculate JA4_b
- let mut sorted_ciphers = self.ciphersuites.to_vec();
+ let mut sorted_ciphers = hs.ciphersuites.to_vec();
sorted_ciphers.sort_by(|a, b| u16::from(*a).cmp(&u16::from(*b)));
let sorted_cipherstrings: Vec<String> = sorted_ciphers
.iter()
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<String> = 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<String> = exts
+ .into_iter()
+ .map(|&v| format!("{:04x}", u16::from(v)))
.collect();
let ja4_c1_raw = sorted_extstrings.join(",");
- let unsorted_sigalgostrings: Vec<String> = self
+ let unsorted_sigalgostrings: Vec<String> = hs
.signature_algorithms
.iter()
.map(|v| format!("{:04x}", (*v)))
let mut ja4_c = format!("{:x}", sha.finalize());
ja4_c.truncate(12);
- return format!("{}_{}_{}", ja4_a, ja4_b, ja4_c);
+ Some(Self {
+ hash: format!("{}_{}_{}", ja4_a, ja4_b, ja4_c),
+ })
}
}
-#[no_mangle]
-pub extern "C" fn SCJA4New() -> *mut JA4 {
- let j = Box::new(JA4::new());
- Box::into_raw(j)
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn SCJA4SetTLSVersion(j: &mut JA4, version: u16) {
- j.set_tls_version(TlsVersion(version));
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn SCJA4AddCipher(j: &mut JA4, cipher: u16) {
- j.add_cipher_suite(TlsCipherSuiteID(cipher));
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn SCJA4AddExtension(j: &mut JA4, ext: u16) {
- j.add_extension(TlsExtensionType(ext));
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn SCJA4AddSigAlgo(j: &mut JA4, sigalgo: u16) {
- j.add_signature_algorithm(sigalgo);
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn SCJA4SetALPN(j: &mut JA4, proto: *const c_char, len: u16) {
- let b: &[u8] = std::slice::from_raw_parts(proto as *const c_uchar, len as usize);
- j.set_alpn(b);
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn SCJA4GetHash(j: &mut JA4, out: &mut [u8; 36]) {
- let hash = j.get_hash();
- out[0..36].copy_from_slice(hash.as_bytes());
+#[cfg(not(feature = "ja4"))]
+impl JA4Impl for JA4 {
+ fn try_new(_hs: &HandshakeParams) -> Option<Self> {
+ None
+ }
}
+// C ABI
+#[cfg(feature = "ja4")]
#[no_mangle]
-pub unsafe extern "C" fn SCJA4Free(j: &mut JA4) {
- let ja4: Box<JA4> = Box::from_raw(j);
- std::mem::drop(ja4);
+pub unsafe extern "C" fn SCJA4GetHash(hs: &HandshakeParams, out: &mut [u8; JA4_HEX_LEN]) {
+ if let Some(ja4) = JA4::try_new(hs) {
+ out[0..JA4_HEX_LEN].copy_from_slice(ja4.as_ref().as_bytes());
+ }
}
-#[cfg(all(test, feature = "ja4"))]
+#[cfg(test)]
+#[cfg(feature = "ja4")]
mod tests {
use super::*;
+ use tls_parser::TlsCipherSuiteID;
#[test]
- fn test_is_grease() {
- let mut alpn = "foobar".as_bytes();
- let mut len = alpn.len();
- let v: u16 = ((alpn[0] as u16) << 8) | alpn[len - 1] as u16;
- assert!(!JA4::is_grease(v));
-
- alpn = &[0x0a, 0x0a];
- len = alpn.len();
- let v: u16 = ((alpn[0] as u16) << 8) | alpn[len - 1] as u16;
- assert!(JA4::is_grease(v));
- }
-
- #[test]
- fn test_tlsversion_max() {
- let mut j = JA4::new();
- assert_eq!(j.tls_version, None);
- j.set_tls_version(TlsVersion::Ssl30);
- assert_eq!(j.tls_version, Some(TlsVersion::Ssl30));
- j.set_tls_version(TlsVersion::Tls12);
- assert_eq!(j.tls_version, Some(TlsVersion::Tls12));
- j.set_tls_version(TlsVersion::Tls10);
- assert_eq!(j.tls_version, Some(TlsVersion::Tls12));
- }
-
- #[test]
- fn test_get_hash_limit_numbers() {
+ fn test_hash_limit_numbers() {
// Test whether the limitation of the extension and ciphersuite
// count to 99 is reflected correctly.
- let mut j = JA4::new();
+ let mut hs = HandshakeParams::default();
for i in 1..200 {
- j.add_cipher_suite(TlsCipherSuiteID(i));
+ hs.add_cipher_suite(TlsCipherSuiteID(i));
}
for i in 1..200 {
- j.add_extension(TlsExtensionType(i));
+ hs.add_extension(TlsExtensionType(i));
}
- let mut s = j.get_hash();
- s.truncate(10);
- assert_eq!(s, "t00i999900");
+ let ja4 = JA4::try_new(&hs).expect("JA4 create failure");
+
+ // Only testing the ja4_a portion of the hash, we we truncate to
+ // ensure we're only testing this
+ let mut ja4_hash = ja4.as_ref().to_string();
+ ja4_hash.truncate(10);
+
+ assert_eq!(ja4_hash, "t00i999900");
}
#[test]
fn test_short_alpn() {
- let mut j = JA4::new();
+ let mut hs = HandshakeParams::default();
- j.set_alpn("b".as_bytes());
- let mut s = j.get_hash();
+ hs.set_alpn("b".as_bytes());
+ let mut s = JA4::try_new(&hs)
+ .expect("JA4 create failure")
+ .as_ref()
+ .to_string();
s.truncate(10);
assert_eq!(s, "t00i0000bb");
- j.set_alpn("h2".as_bytes());
- let mut s = j.get_hash();
+ hs.set_alpn("h2".as_bytes());
+ let mut s = JA4::try_new(&hs)
+ .expect("JA4 create failure")
+ .as_ref()
+ .to_string();
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();
+ hs.set_alpn(&[0xab]);
+ let mut s = JA4::try_new(&hs)
+ .expect("JA4 create failure")
+ .as_ref()
+ .to_string();
s.truncate(10);
assert_eq!(s, "t00i0000ab");
- j.set_alpn(&[0xab, 0xcd]);
- let mut s = j.get_hash();
+ hs.set_alpn(&[0xab, 0xcd]);
+ let mut s = JA4::try_new(&hs)
+ .expect("JA4 create failure")
+ .as_ref()
+ .to_string();
s.truncate(10);
assert_eq!(s, "t00i0000ad");
- j.set_alpn(&[0x30, 0xab]);
- let mut s = j.get_hash();
+ hs.set_alpn(&[0x30, 0xab]);
+ let mut s = JA4::try_new(&hs)
+ .expect("JA4 create failure")
+ .as_ref()
+ .to_string();
s.truncate(10);
assert_eq!(s, "t00i00003b");
- j.set_alpn(&[0x30, 0x31, 0xab, 0xcd]);
- let mut s = j.get_hash();
+ hs.set_alpn(&[0x30, 0x31, 0xab, 0xcd]);
+ let mut s = JA4::try_new(&hs)
+ .expect("JA4 create failure")
+ .as_ref()
+ .to_string();
s.truncate(10);
assert_eq!(s, "t00i00003d");
- j.set_alpn(&[0x30, 0xab, 0xcd, 0x31]);
- let mut s = j.get_hash();
+ hs.set_alpn(&[0x30, 0xab, 0xcd, 0x31]);
+ let mut s = JA4::try_new(&hs)
+ .expect("JA4 create failure")
+ .as_ref()
+ .to_string();
s.truncate(10);
assert_eq!(s, "t00i000001");
}
#[test]
fn test_get_hash() {
- let mut j = JA4::new();
+ let mut hs = HandshakeParams::default();
// the empty JA4 hash
- let s = j.get_hash();
- assert_eq!(s, "t00i000000_e3b0c44298fc_d2e2adf7177b");
+ let s = JA4::try_new(&hs).expect("JA4 create failure");
+ assert_eq!(s.as_ref(), "t00i000000_e3b0c44298fc_d2e2adf7177b");
// set TLS version
- j.set_tls_version(TlsVersion::Tls12);
- let s = j.get_hash();
- assert_eq!(s, "t12i000000_e3b0c44298fc_d2e2adf7177b");
+ hs.set_tls_version(TlsVersion::Tls12);
+ let s = JA4::try_new(&hs).expect("JA4 create failure");
+ assert_eq!(s.as_ref(), "t12i000000_e3b0c44298fc_d2e2adf7177b");
// set QUIC
- j.set_quic();
- let s = j.get_hash();
- assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b");
+ hs.quic = true;
+ let s = JA4::try_new(&hs).expect("JA4 create failure");
+ assert_eq!(s.as_ref(), "q12i000000_e3b0c44298fc_d2e2adf7177b");
// set GREASE extension, should be ignored
- j.add_extension(TlsExtensionType(0x0a0a));
- let s = j.get_hash();
- assert_eq!(s, "q12i000000_e3b0c44298fc_d2e2adf7177b");
+ hs.add_extension(TlsExtensionType(0x0a0a));
+ let s = JA4::try_new(&hs).expect("JA4 create failure");
+ assert_eq!(s.as_ref(), "q12i000000_e3b0c44298fc_d2e2adf7177b");
// set SNI extension, should only increase count and change i->d
- j.add_extension(TlsExtensionType(0x0000));
- let s = j.get_hash();
- assert_eq!(s, "q12d000100_e3b0c44298fc_d2e2adf7177b");
+ hs.add_extension(TlsExtensionType(0x0000));
+ let s = JA4::try_new(&hs).expect("JA4 create failure");
+ assert_eq!(s.as_ref(), "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();
- assert_eq!(s, "q12d0002h6_e3b0c44298fc_d2e2adf7177b");
+ hs.set_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");
// 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();
- assert_eq!(s, "q12d0302h6_f500716053f9_d2e2adf7177b");
+ hs.add_cipher_suite(TlsCipherSuiteID(0x1111));
+ hs.add_cipher_suite(TlsCipherSuiteID(0x0a20));
+ hs.add_cipher_suite(TlsCipherSuiteID(0xbada));
+ let s = JA4::try_new(&hs).expect("JA4 create failure");
+ assert_eq!(s.as_ref(), "q12d0302h6_f500716053f9_d2e2adf7177b");
// set some extensions and signature algorithms
- j.add_extension(TlsExtensionType(0xface));
- j.add_extension(TlsExtensionType(0x0121));
- j.add_extension(TlsExtensionType(0x1234));
- j.add_signature_algorithm(0x6666);
- let s = j.get_hash();
- assert_eq!(s, "q12d0305h6_f500716053f9_2debc8880bae");
+ hs.add_extension(TlsExtensionType(0xface));
+ hs.add_extension(TlsExtensionType(0x0121));
+ hs.add_extension(TlsExtensionType(0x1234));
+ hs.add_signature_algorithm(0x6666);
+ let s = JA4::try_new(&hs).expect("JA4 create failure");
+ assert_eq!(s.as_ref(), "q12d0305h6_f500716053f9_2debc8880bae");
}
}
pub mod utils;
pub mod ja4;
+pub mod handshake;
pub mod lua;
tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
) -> u8 {
if let Some(ja4) = &tx.ja4 {
- *buffer = ja4.as_ptr();
- *buffer_len = ja4.len() as u32;
+ *buffer = ja4.as_ref().as_ptr();
+ *buffer_len = ja4.as_ref().len() as u32;
1
} else {
*buffer = ptr::null();
use super::error::QuicError;
use super::quic::QUIC_MAX_CRYPTO_FRAG_LEN;
-use crate::ja4::*;
+use crate::handshake::HandshakeParams;
use crate::quic::parser::quic_var_uint;
use nom7::bytes::complete::take;
use nom7::combinator::{all_consuming, complete};
// the lifetime of TlsExtension due to references to the slice used for parsing
pub extv: Vec<QuicTlsExtension>,
pub ja3: Option<String>,
- pub ja4: Option<JA4>,
+ pub hs: Option<HandshakeParams>,
}
#[derive(Debug, PartialEq)]
// get interesting stuff out of parsed tls extensions
fn quic_get_tls_extensions(
- input: Option<&[u8]>, ja3: &mut String, mut ja4: Option<&mut JA4>, client: bool,
+ input: Option<&[u8]>, ja3: &mut String, mut hs: Option<&mut HandshakeParams>, client: bool,
) -> Vec<QuicTlsExtension> {
let mut extv = Vec::new();
if let Some(extr) = input {
dash = true;
}
ja3.push_str(&u16::from(etype).to_string());
- if let Some(ref mut ja4) = ja4 {
- ja4.add_extension(etype)
+ if let Some(ref mut hs) = hs {
+ hs.add_extension(etype)
}
let mut values = Vec::new();
match e {
let mut value = Vec::new();
value.extend_from_slice(version.to_string().as_bytes());
values.push(value);
- if let Some(ref mut ja4) = ja4 {
- ja4.set_tls_version(*version);
+ if let Some(ref mut hs) = hs {
+ hs.set_tls_version(*version);
}
}
}
let mut value = Vec::new();
value.extend_from_slice(sigalgo.to_string().as_bytes());
values.push(value);
- if let Some(ref mut ja4) = ja4 {
- ja4.add_signature_algorithm(*sigalgo)
+ if let Some(ref mut hs) = hs {
+ hs.add_signature_algorithm(*sigalgo)
}
}
}
TlsExtension::ALPN(x) => {
if !x.is_empty() {
- if let Some(ref mut ja4) = ja4 {
- ja4.set_alpn(x[0]);
+ if let Some(ref mut hs) = hs {
+ hs.set_alpn(x[0]);
}
}
for alpn in x {
let mut ja3 = String::with_capacity(256);
ja3.push_str(&u16::from(ch.version).to_string());
ja3.push(',');
- let mut ja4 = JA4::new();
- ja4.set_quic();
+ let mut hs = HandshakeParams { quic: true, ..Default::default() };
let mut dash = false;
for c in &ch.ciphers {
if dash {
dash = true;
}
ja3.push_str(&u16::from(*c).to_string());
- ja4.add_cipher_suite(*c);
+ hs.add_cipher_suite(*c);
}
ja3.push(',');
let ciphers = ch.ciphers;
- let extv = quic_get_tls_extensions(ch.ext, &mut ja3, Some(&mut ja4), true);
+ let extv = quic_get_tls_extensions(ch.ext, &mut ja3, Some(&mut hs), true);
return Some(Frame::Crypto(Crypto {
ciphers,
extv,
} else {
None
},
- ja4: if cfg!(feature = "ja4") {
- Some(ja4)
+ hs: if cfg!(feature = "ja4") {
+ Some(hs)
} else {
None
},
} else {
None
},
- ja4: None,
+ hs: None,
}));
}
_ => {}
}
if let Some(ref ja4) = &tx.ja4 {
- js.set_string("ja4", ja4)?;
+ js.set_string("ja4", ja4.as_ref())?;
}
if !tx.extv.is_empty() {
frames::{Frame, QuicTlsExtension, StreamTag},
parser::{quic_pkt_num, QuicData, QuicHeader, QuicType},
};
-use crate::core::{ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP};
use crate::{
applayer::{self, *},
direction::Direction,
flow::Flow,
+ ja4::JA4,
+};
+use crate::{
+ core::{ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP},
+ ja4::JA4Impl,
};
use std::collections::VecDeque;
use std::ffi::CString;
pub ua: Option<Vec<u8>>,
pub extv: Vec<QuicTlsExtension>,
pub ja3: Option<String>,
- pub ja4: Option<String>,
+ pub ja4: Option<JA4>,
pub client: bool,
tx_data: AppLayerTxData,
}
impl QuicTransaction {
fn new(
header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>,
- extv: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<String>, client: bool,
+ extv: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<JA4>, client: bool,
) -> Self {
let direction = if client {
Direction::ToServer
fn new_tx(
&mut self, header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>,
- extb: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<String>, client: bool,
+ extb: Vec<QuicTlsExtension>, ja3: Option<String>, ja4: Option<JA4>, client: bool,
frag_long: bool,
) {
let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, ja4, client);
let mut sni: Option<Vec<u8>> = None;
let mut ua: Option<Vec<u8>> = None;
let mut ja3: Option<String> = None;
- let mut ja4: Option<String> = None;
+ let mut ja4: Option<JA4> = None;
let mut extv: Vec<QuicTlsExtension> = Vec::new();
let mut frag_long = false;
for frame in &data.frames {
if to_server {
// our hash is complete, let's only use strings from
// now on
- if let Some(ref rja4) = c.ja4 {
- ja4 = Some(rja4.get_hash());
+ if let Some(ref rja4) = c.hs {
+ ja4 = JA4::try_new(rja4);
}
}
for e in &c.extv {
util-ioctl.h \
util-ip.h \
util-ja3.h \
- util-ja4.h \
util-landlock.h \
util-log-redis.h \
util-logopenfile.h \
uint16_t version = (uint16_t)(*input << 8) | *(input + 1);
ssl_state->curr_connp->version = version;
- if (ssl_state->curr_connp->ja4 != NULL &&
- ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
- SCJA4SetTLSVersion(ssl_state->curr_connp->ja4, version);
+ if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+ SCTLSHandshakeSetTLSVersion(ssl_state->curr_connp->hs, version);
}
/* TLSv1.3 draft1 to draft21 use the version field as earlier TLS
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)) {
- JA3Buffer *ja3_cipher_suites = NULL;
+ JA3Buffer *ja3_cipher_suites = NULL;
- if (enable_ja3) {
- ja3_cipher_suites = Ja3BufferInit();
- if (ja3_cipher_suites == NULL)
- return -1;
- }
+ if (enable_ja3) {
+ ja3_cipher_suites = Ja3BufferInit();
+ if (ja3_cipher_suites == NULL)
+ return -1;
+ }
- uint16_t processed_len = 0;
- /* coverity[tainted_data] */
- while (processed_len < cipher_suites_length)
- {
- if (!(HAS_SPACE(2))) {
- if (enable_ja3) {
- Ja3BufferFree(&ja3_cipher_suites);
- }
- goto invalid_length;
+ uint16_t processed_len = 0;
+ /* coverity[tainted_data] */
+ while (processed_len < cipher_suites_length) {
+ if (!(HAS_SPACE(2))) {
+ if (enable_ja3) {
+ Ja3BufferFree(&ja3_cipher_suites);
}
+ goto invalid_length;
+ }
- uint16_t cipher_suite = (uint16_t)(*input << 8) | *(input + 1);
- input += 2;
+ uint16_t cipher_suite = (uint16_t)(*input << 8) | *(input + 1);
+ input += 2;
- if (TLSDecodeValueIsGREASE(cipher_suite) != 1) {
- if (ssl_state->curr_connp->ja4 != NULL &&
- ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
- SCJA4AddCipher(ssl_state->curr_connp->ja4, cipher_suite);
- }
- if (enable_ja3) {
- int rc = Ja3BufferAddValue(&ja3_cipher_suites, cipher_suite);
- if (rc != 0) {
- return -1;
- }
+ if (TLSDecodeValueIsGREASE(cipher_suite) != 1) {
+ if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+ SCTLSHandshakeAddCipher(ssl_state->curr_connp->hs, cipher_suite);
+ }
+ if (enable_ja3) {
+ int rc = Ja3BufferAddValue(&ja3_cipher_suites, cipher_suite);
+ if (rc != 0) {
+ return -1;
}
}
- processed_len += 2;
}
+ processed_len += 2;
+ }
- if (enable_ja3) {
- int rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, &ja3_cipher_suites);
- if (rc == -1) {
- return -1;
- }
+ if (enable_ja3) {
+ int rc = Ja3BufferAppendBuffer(&ssl_state->curr_connp->ja3_str, &ja3_cipher_suites);
+ if (rc == -1) {
+ return -1;
}
-
- } else {
- /* Skip cipher suites */
- input += cipher_suites_length;
}
return (int)(input - initial_input);
uint16_t ver = (uint16_t)(input[i] << 8) | input[i + 1];
if (TLSVersionValid(ver)) {
ssl_state->curr_connp->version = ver;
- if (ssl_state->curr_connp->ja4 != NULL &&
- ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
- SCJA4SetTLSVersion(ssl_state->curr_connp->ja4, ver);
- }
+ SCTLSHandshakeSetTLSVersion(ssl_state->curr_connp->hs, ver);
break;
}
i += 2;
if (!(HAS_SPACE(sigalgo_len)))
goto invalid_length;
- if (ssl_state->curr_connp->ja4 != NULL &&
- ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+ if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
uint16_t sigalgo_processed_len = 0;
while (sigalgo_processed_len < sigalgo_len) {
uint16_t sigalgo = (uint16_t)(*input << 8) | *(input + 1);
input += 2;
sigalgo_processed_len += 2;
- SCJA4AddSigAlgo(ssl_state->curr_connp->ja4, sigalgo);
+ SCTLSHandshakeAddSigAlgo(ssl_state->curr_connp->hs, sigalgo);
}
} else {
/* Skip signature algorithms */
break;
}
- /* Only record the first value for JA4 */
- if (ssl_state->curr_connp->ja4 != NULL &&
- ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+ if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
if (alpn_processed_len == 1) {
- SCJA4SetALPN(ssl_state->curr_connp->ja4, (const char *)input, protolen);
+ SCTLSHandshakeSetALPN(ssl_state->curr_connp->hs, (const char *)input, protolen);
}
}
StoreALPN(ssl_state->curr_connp, input, protolen);
}
}
- if (ssl_state->curr_connp->ja4 != NULL &&
- ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
+ if (ssl_state->current_flags & SSL_AL_FLAG_STATE_CLIENT_HELLO) {
if (TLSDecodeValueIsGREASE(ext_type) != 1) {
- SCJA4AddExtension(ssl_state->curr_connp->ja4, ext_type);
+ SCTLSHandshakeAddExtension(ssl_state->curr_connp->hs, ext_type);
}
}
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 &&
- ssl_state->curr_connp->ja4 == NULL) {
- ssl_state->curr_connp->ja4 = SCJA4New();
- }
-
ret = TLSDecodeHSHelloVersion(ssl_state, input, input_len);
if (ret < 0)
goto end;
ssl_state->server_connp.cert_log_flag = 0;
memset(ssl_state->client_connp.random, 0, TLS_RANDOM_LEN);
memset(ssl_state->server_connp.random, 0, TLS_RANDOM_LEN);
+ 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);
if (ssl_state->server_connp.session_id)
SCFree(ssl_state->server_connp.session_id);
- if (ssl_state->client_connp.ja4)
- SCJA4Free(ssl_state->client_connp.ja4);
+ if (ssl_state->client_connp.hs)
+ SCTLSHandshakeFree(ssl_state->client_connp.hs);
if (ssl_state->client_connp.ja3_str)
Ja3BufferFree(&ssl_state->client_connp.ja3_str);
if (ssl_state->client_connp.ja3_hash)
SCFree(ssl_state->client_connp.ja3_hash);
+ if (ssl_state->server_connp.hs)
+ SCTLSHandshakeFree(ssl_state->server_connp.hs);
if (ssl_state->server_connp.ja3_str)
Ja3BufferFree(&ssl_state->server_connp.ja3_str);
if (ssl_state->server_connp.ja3_hash)
JA3Buffer *ja3_str;
char *ja3_hash;
- JA4 *ja4;
+ HandshakeParams *hs;
/* handshake tls fragmentation buffer. Handshake messages can be fragmented over multiple
* TLS records. */
#include "detect-engine-prefilter.h"
#include "detect-ja4-hash.h"
-#include "util-ja4.h"
-
#include "app-layer-ssl.h"
#ifndef HAVE_JA4
if (buffer->inspect == NULL) {
const SSLState *ssl_state = (SSLState *)f->alstate;
- if (ssl_state->client_connp.ja4 == NULL) {
+ if (ssl_state->client_connp.hs == NULL) {
return NULL;
}
uint8_t data[JA4_HEX_LEN];
- SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])data);
+ SCJA4GetHash(ssl_state->client_connp.hs, (uint8_t(*)[JA4_HEX_LEN])data);
InspectionBufferSetup(det_ctx, list_id, buffer, data, 0);
InspectionBufferCopy(buffer, data, JA4_HEX_LEN);
#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)
static void JsonTlsLogSCJA4(SCJsonBuilder *js, SSLState *ssl_state)
{
- if (ssl_state->client_connp.ja4 != NULL) {
+#ifdef HAVE_JA4
+ if (ssl_state->client_connp.hs != NULL) {
uint8_t buffer[JA4_HEX_LEN];
/* JA4 hash has 36 characters */
- SCJA4GetHash(ssl_state->client_connp.ja4, (uint8_t(*)[JA4_HEX_LEN])buffer);
- SCJbSetStringFromBytes(js, "ja4", buffer, 36);
+ SCJA4GetHash(ssl_state->client_connp.hs, (uint8_t(*)[JA4_HEX_LEN])buffer);
+ SCJbSetStringFromBytes(js, "ja4", buffer, JA4_HEX_LEN);
}
+#endif
}
static void JsonTlsLogJa3SHash(SCJsonBuilder *js, SSLState *ssl_state)
+++ /dev/null
-/* 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 <sascha@steinbiss.name>
- */
-
-#ifndef SURICATA_UTIL_JA4_H
-#define SURICATA_UTIL_JA4_H
-
-#define JA4_HEX_LEN 36
-
-#endif /* SURICATA_UTIL_JA4_H */