#![no_std]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)]
use sha2::{Digest, Sha256};
#[inline(always)]
fn is_grease(val: u16) -> bool {
(val & 0x0F0F) == 0x0A0A && ((val >> 8) == (val & 0xFF))
}
#[inline(always)]
fn write_u16_hex(val: u16, buf: &mut [u8]) {
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
buf[0] = HEX_CHARS[((val >> 12) & 0xF) as usize];
buf[1] = HEX_CHARS[((val >> 8) & 0xF) as usize];
buf[2] = HEX_CHARS[((val >> 4) & 0xF) as usize];
buf[3] = HEX_CHARS[(val & 0xF) as usize];
}
#[inline(always)]
fn read_u8(data: &mut &[u8]) -> Option<u8> {
let (&first, rest) = data.split_first()?;
*data = rest;
Some(first)
}
#[inline(always)]
fn read_u16(data: &mut &[u8]) -> Option<u16> {
if data.len() < 2 {
return None;
}
let val = u16::from_be_bytes([data[0], data[1]]);
*data = &data[2..];
Some(val)
}
#[inline(always)]
fn read_slice<'a>(data: &mut &'a [u8], len: usize) -> Option<&'a [u8]> {
if data.len() < len {
return None;
}
let (left, right) = data.split_at(len);
*data = right;
Some(left)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Ja4Result {
pub fingerprint: [u8; 36],
}
impl Ja4Result {
#[inline]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.fingerprint).unwrap_or("0000000000_000000000000_000000000000")
}
}
impl core::fmt::Display for Ja4Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
pub fn parse_ja4(packet: &[u8], is_quic: bool) -> Option<Ja4Result> {
let mut data = packet;
if !is_quic {
let record_type = read_u8(&mut data)?;
if record_type != 0x16 {
return None;
}
let _record_version = read_u16(&mut data)?;
let record_len = read_u16(&mut data)? as usize;
if data.len() < record_len {
return None;
}
data = &data[..record_len];
}
let msg_type = read_u8(&mut data)?;
if msg_type != 0x01 {
return None;
}
if data.len() < 3 {
return None;
}
let msg_len = ((data[0] as usize) << 16)
| ((data[1] as usize) << 8)
| (data[2] as usize);
data = &data[3..];
if data.len() < msg_len {
return None;
}
data = &data[..msg_len];
let client_version = read_u16(&mut data)?;
read_slice(&mut data, 32)?;
let session_id_len = read_u8(&mut data)? as usize;
read_slice(&mut data, session_id_len)?;
let ciphers_len = read_u16(&mut data)? as usize;
if !ciphers_len.is_multiple_of(2) {
return None;
}
let mut ciphers_data = read_slice(&mut data, ciphers_len)?;
let mut ciphers = [0u16; 128];
let mut ciphers_count = 0;
while !ciphers_data.is_empty() {
let cipher = read_u16(&mut ciphers_data)?;
if !is_grease(cipher) && ciphers_count < 128 {
ciphers[ciphers_count] = cipher;
ciphers_count += 1;
}
}
let comp_len = read_u8(&mut data)? as usize;
read_slice(&mut data, comp_len)?;
let mut extensions = [0u16; 128];
let mut extensions_count = 0;
let mut total_ext_count = 0;
let mut has_sni = false;
let mut alpn = *b"00";
let mut sig_algs = [0u16; 128];
let mut sig_algs_count = 0;
let mut highest_supported_version = None;
if !data.is_empty() {
if data.len() < 2 {
return None;
}
let ext_total_len = read_u16(&mut data)? as usize;
if data.len() < ext_total_len {
return None;
}
let mut ext_data = &data[..ext_total_len];
while !ext_data.is_empty() {
let ext_type = read_u16(&mut ext_data)?;
let ext_len = read_u16(&mut ext_data)? as usize;
let mut ext_payload = read_slice(&mut ext_data, ext_len)?;
if !is_grease(ext_type) {
total_ext_count += 1;
if ext_type == 0x0000 {
has_sni = true;
} else if ext_type == 0x0010 {
if ext_payload.len() >= 2 {
let alpn_list_len = read_u16(&mut ext_payload)? as usize;
if ext_payload.len() >= alpn_list_len && alpn_list_len > 0 {
let mut alpn_payload = &ext_payload[..alpn_list_len];
if !alpn_payload.is_empty() {
let first_alpn_len = read_u8(&mut alpn_payload)? as usize;
if alpn_payload.len() >= first_alpn_len && first_alpn_len > 0 {
let first_alpn = &alpn_payload[..first_alpn_len];
let first_byte = first_alpn[0];
let last_byte = first_alpn[first_alpn_len - 1];
if first_byte.is_ascii_alphanumeric() && last_byte.is_ascii_alphanumeric() {
let mut first_char = 0;
let mut last_char = 0;
let mut char_count = 0;
for &b in first_alpn {
if b.is_ascii_alphanumeric() {
char_count += 1;
if char_count == 1 {
first_char = b;
}
last_char = b;
}
}
if char_count > 0 {
if char_count == 1 {
alpn[0] = first_char.to_ascii_lowercase();
alpn[1] = b'0';
} else {
alpn[0] = first_char.to_ascii_lowercase();
alpn[1] = last_char.to_ascii_lowercase();
}
}
} else {
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
let f_char = if first_byte.is_ascii_alphanumeric() {
first_byte.to_ascii_lowercase()
} else {
HEX_CHARS[((first_byte >> 4) & 0xF) as usize]
};
let l_char = if last_byte.is_ascii_alphanumeric() {
last_byte.to_ascii_lowercase()
} else {
HEX_CHARS[(last_byte & 0xF) as usize]
};
alpn[0] = f_char;
alpn[1] = l_char;
}
}
}
}
}
} else if ext_type == 0x000d {
if ext_payload.len() >= 2 {
let sig_list_len = read_u16(&mut ext_payload)? as usize;
if ext_payload.len() >= sig_list_len && sig_list_len > 0 {
let mut sig_payload = &ext_payload[..sig_list_len];
while !sig_payload.is_empty() && sig_algs_count < 128 {
let sig = read_u16(&mut sig_payload)?;
sig_algs[sig_algs_count] = sig;
sig_algs_count += 1;
}
}
}
} else if ext_type == 0x002b {
if !ext_payload.is_empty() {
let ver_list_len = read_u8(&mut ext_payload)? as usize;
if ext_payload.len() >= ver_list_len && ver_list_len > 0 {
let mut ver_payload = &ext_payload[..ver_list_len];
while !ver_payload.is_empty() {
let ver = read_u16(&mut ver_payload)?;
if !is_grease(ver) {
match highest_supported_version {
None => highest_supported_version = Some(ver),
Some(highest) if ver > highest => highest_supported_version = Some(ver),
_ => {}
}
}
}
}
}
}
if ext_type != 0x0000 && ext_type != 0x0010 && extensions_count < 128 {
extensions[extensions_count] = ext_type;
extensions_count += 1;
}
}
}
}
let ciphers_slice = &mut ciphers[..ciphers_count];
ciphers_slice.sort_unstable();
let extensions_slice = &mut extensions[..extensions_count];
extensions_slice.sort_unstable();
let protocol = if is_quic { b'q' } else { b't' };
let chosen_version = highest_supported_version.unwrap_or(client_version);
let version_bytes = match chosen_version {
0x0304 => b"13",
0x0303 => b"12",
0x0302 => b"11",
0x0301 => b"10",
0x0300 => b"s3",
0x0200 => b"s2",
0x0100 => b"s1",
0x0002 => b"s2",
_ => b"00",
};
let sni = if has_sni { b'd' } else { b'i' };
let mut fp = [0u8; 36];
fp[0] = protocol;
fp[1] = version_bytes[0];
fp[2] = version_bytes[1];
fp[3] = sni;
let cc = ciphers_count.min(99);
fp[4] = b'0' + (cc / 10) as u8;
fp[5] = b'0' + (cc % 10) as u8;
let ec = total_ext_count.min(99);
fp[6] = b'0' + (ec / 10) as u8;
fp[7] = b'0' + (ec % 10) as u8;
fp[8] = alpn[0];
fp[9] = alpn[1];
fp[10] = b'_';
if ciphers_count == 0 {
fp[11..23].copy_from_slice(b"000000000000");
} else {
let mut hasher = Sha256::new();
for (i, &c) in ciphers_slice.iter().enumerate() {
if i > 0 {
hasher.update(b",");
}
let mut hex = [0u8; 4];
write_u16_hex(c, &mut hex);
hasher.update(&hex);
}
let hash = hasher.finalize();
let hex_chars = b"0123456789abcdef";
for i in 0..6 {
let b = hash[i];
fp[11 + i * 2] = hex_chars[(b >> 4) as usize];
fp[11 + i * 2 + 1] = hex_chars[(b & 0x0F) as usize];
}
}
fp[23] = b'_';
if extensions_count == 0 && sig_algs_count == 0 {
fp[24..36].copy_from_slice(b"000000000000");
} else {
let mut hasher = Sha256::new();
if extensions_count > 0 {
for (i, &e) in extensions_slice.iter().enumerate() {
if i > 0 {
hasher.update(b",");
}
let mut hex = [0u8; 4];
write_u16_hex(e, &mut hex);
hasher.update(&hex);
}
}
if extensions_count > 0 && sig_algs_count > 0 {
hasher.update(b"_");
}
if sig_algs_count > 0 {
for (i, &s) in sig_algs[..sig_algs_count].iter().enumerate() {
if i > 0 {
hasher.update(b",");
}
let mut hex = [0u8; 4];
write_u16_hex(s, &mut hex);
hasher.update(&hex);
}
}
let hash = hasher.finalize();
let hex_chars = b"0123456789abcdef";
for i in 0..6 {
let b = hash[i];
fp[24 + i * 2] = hex_chars[(b >> 4) as usize];
fp[24 + i * 2 + 1] = hex_chars[(b & 0x0F) as usize];
}
}
Some(Ja4Result { fingerprint: fp })
}
#[cfg(test)]
mod tests {
extern crate std;
use std::prelude::v1::*;
use super::*;
#[test]
fn test_is_grease() {
assert!(is_grease(0x0A0A));
assert!(is_grease(0x1A1A));
assert!(is_grease(0x3A3A));
assert!(is_grease(0xFAFA));
assert!(!is_grease(0x0A0B));
assert!(!is_grease(0x1111));
}
#[test]
fn test_write_u16_hex() {
let mut buf = [0u8; 4];
write_u16_hex(0x1a2f, &mut buf);
assert_eq!(&buf, b"1a2f");
write_u16_hex(0x0000, &mut buf);
assert_eq!(&buf, b"0000");
write_u16_hex(0xffff, &mut buf);
assert_eq!(&buf, b"ffff");
}
#[test]
fn test_empty_or_too_short_packet() {
assert!(parse_ja4(&[], false).is_none());
assert!(parse_ja4(&[0x16], false).is_none());
assert!(parse_ja4(&[0x16, 0x03, 0x01, 0x00], false).is_none());
}
#[test]
fn test_minimal_valid_handshake() {
let mut packet = vec![
0x16, 0x03, 0x01, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x2b, 0x03, 0x03, ];
packet.extend_from_slice(&[0u8; 32]);
packet.push(0x00);
packet.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]);
packet.extend_from_slice(&[0x01, 0x00]);
packet.extend_from_slice(&[0x00, 0x00]);
let res = parse_ja4(&packet, false).expect("Parsing should succeed");
let fp_str = res.as_str();
assert!(fp_str.starts_with("t12i010000_"));
assert_eq!(fp_str.len(), 36);
assert_eq!(res.to_string(), fp_str);
}
#[test]
fn test_quic_handshake() {
let mut packet = vec![
0x01, 0x00, 0x00, 0x2b, 0x03, 0x03, ];
packet.extend_from_slice(&[0u8; 32]);
packet.push(0x00);
packet.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]);
packet.extend_from_slice(&[0x01, 0x00]);
packet.extend_from_slice(&[0x00, 0x00]);
let res = parse_ja4(&packet, true).expect("Parsing QUIC should succeed");
let fp_str = res.as_str();
assert!(fp_str.starts_with("q12i010000_"));
}
#[test]
fn test_handshake_with_extensions() {
let mut handshake = vec![
0x03, 0x03, ];
handshake.extend_from_slice(&[0u8; 32]);
handshake.push(0x00);
handshake.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]);
handshake.extend_from_slice(&[0x01, 0x00]);
let mut extensions_payload = Vec::new();
extensions_payload.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
extensions_payload.extend_from_slice(&[0x00, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2']);
extensions_payload.extend_from_slice(&[0x00, 0x0d, 0x00, 0x06, 0x00, 0x04, 0x04, 0x03, 0x08, 0x04]);
extensions_payload.extend_from_slice(&[0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04]);
let ext_len = extensions_payload.len();
handshake.extend_from_slice(&(ext_len as u16).to_be_bytes());
handshake.extend_from_slice(&extensions_payload);
let mut packet = vec![
0x01, ((handshake.len() >> 16) & 0xFF) as u8,
((handshake.len() >> 8) & 0xFF) as u8,
(handshake.len() & 0xFF) as u8,
];
packet.extend_from_slice(&handshake);
let res = parse_ja4(&packet, true).expect("Parsing with extensions should succeed");
let fp_str = res.as_str();
assert!(fp_str.starts_with("q13d0104h2_"));
}
#[test]
fn test_alpn_non_alphanumeric_hex_fallback() {
let make_packet_with_alpn = |alpn_bytes: &[u8]| -> Vec<u8> {
let mut handshake = vec![
0x03, 0x03, ];
handshake.extend_from_slice(&[0u8; 32]); handshake.push(0x00); handshake.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]); handshake.extend_from_slice(&[0x01, 0x00]);
let mut ext_payload = Vec::new();
let list_len = 1 + alpn_bytes.len();
let ext_len = 2 + list_len;
ext_payload.extend_from_slice(&[0x00, 0x10]); ext_payload.extend_from_slice(&(ext_len as u16).to_be_bytes());
ext_payload.extend_from_slice(&(list_len as u16).to_be_bytes());
ext_payload.push(alpn_bytes.len() as u8);
ext_payload.extend_from_slice(alpn_bytes);
handshake.extend_from_slice(&(ext_payload.len() as u16).to_be_bytes());
handshake.extend_from_slice(&ext_payload);
let mut packet = vec![
0x01,
((handshake.len() >> 16) & 0xFF) as u8,
((handshake.len() >> 8) & 0xFF) as u8,
(handshake.len() & 0xFF) as u8,
];
packet.extend_from_slice(&handshake);
packet
};
let p1 = make_packet_with_alpn(&[0xab]);
let res1 = parse_ja4(&p1, true).unwrap();
assert_eq!(&res1.as_str()[8..10], "ab");
let p2 = make_packet_with_alpn(&[0x20]);
let res2 = parse_ja4(&p2, true).unwrap();
assert_eq!(&res2.as_str()[8..10], "20");
let p3 = make_packet_with_alpn(&[0xab, 0xcd]);
let res3 = parse_ja4(&p3, true).unwrap();
assert_eq!(&res3.as_str()[8..10], "ad");
let p4 = make_packet_with_alpn(&[0x20, 0x61]);
let res4 = parse_ja4(&p4, true).unwrap();
assert_eq!(&res4.as_str()[8..10], "2a");
}
#[test]
fn test_full_fingerprint_hash_verification() {
let mut handshake = vec![
0x03, 0x03, ];
handshake.extend_from_slice(&[0u8; 32]);
handshake.push(0x00);
handshake.extend_from_slice(&[0x00, 0x04, 0x13, 0x02, 0x13, 0x01]);
handshake.extend_from_slice(&[0x01, 0x00]);
let mut extensions_payload = Vec::new();
extensions_payload.extend_from_slice(&[0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04]);
extensions_payload.extend_from_slice(&[0x00, 0x0d, 0x00, 0x06, 0x00, 0x04, 0x04, 0x03, 0x08, 0x04]);
let ext_len = extensions_payload.len();
handshake.extend_from_slice(&(ext_len as u16).to_be_bytes());
handshake.extend_from_slice(&extensions_payload);
let mut handshake_packet = vec![
0x01, ((handshake.len() >> 16) & 0xFF) as u8,
((handshake.len() >> 8) & 0xFF) as u8,
(handshake.len() & 0xFF) as u8,
];
handshake_packet.extend_from_slice(&handshake);
let mut packet = vec![
0x16, 0x03, 0x01, ];
packet.extend_from_slice(&(handshake_packet.len() as u16).to_be_bytes());
packet.extend_from_slice(&handshake_packet);
let res = parse_ja4(&packet, false).expect("Parsing full handshake should succeed");
let fp_str = res.as_str();
assert_eq!(fp_str, "t13i020200_62ed6f6ca7ad_ef5f37ab036a");
}
#[test]
fn test_overflow_limits() {
let mut handshake = vec![
0x03, 0x03, ];
handshake.extend_from_slice(&[0u8; 32]); handshake.push(0x00);
handshake.extend_from_slice(&[0x01, 0x04]); for i in 0..130u16 {
handshake.extend_from_slice(&i.to_be_bytes());
}
handshake.extend_from_slice(&[0x01, 0x00]);
handshake.extend_from_slice(&[0x02, 0x08]); for i in 0..130u16 {
let ext_type = if is_grease(i) { 0x0001 } else { i };
handshake.extend_from_slice(&ext_type.to_be_bytes());
handshake.extend_from_slice(&[0x00, 0x00]); }
let mut packet = vec![
0x01,
((handshake.len() >> 16) & 0xFF) as u8,
((handshake.len() >> 8) & 0xFF) as u8,
(handshake.len() & 0xFF) as u8,
];
packet.extend_from_slice(&handshake);
let res = parse_ja4(&packet, true).unwrap();
let fp = res.as_str();
println!("Fingerprint: {}", fp);
assert!(fp.starts_with("q12d999900_"));
}
#[test]
fn test_truncated_packet() {
let mut packet = vec![
0x16, 0x03, 0x01, 0x00, 0x2f,
0x01, 0x00, 0x00, 0x2b,
0x03, 0x03,
];
packet.extend_from_slice(&[0u8; 32]);
packet.push(0x00);
packet.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]);
packet.extend_from_slice(&[0x01, 0x00]);
packet.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]);
for i in 1..packet.len() {
assert!(parse_ja4(&packet[..i], false).is_none());
}
}
}