#![allow(dead_code)]
use std::fmt::Write;
fn is_grease(v: u16) -> bool {
let high = (v >> 8) & 0xff;
let low = v & 0xff;
high == low && (low & 0x0f) == 0x0a
}
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("short read at offset {offset}: need {need} bytes, have {have}")]
ShortRead {
offset: usize,
need: usize,
have: usize,
},
#[error("not a TLS handshake record (byte0={0:#04x})")]
NotHandshake(u8),
#[error("not a ClientHello (handshake type={0:#04x})")]
NotClientHello(u8),
#[error("malformed extension: type={ext_type:#06x}, declared length {declared}, remaining {remaining}")]
BadExtensionLen {
ext_type: u16,
declared: usize,
remaining: usize,
},
}
#[derive(Debug, Clone, Default)]
pub struct ClientHello {
pub legacy_version: u16,
pub cipher_suites_raw: Vec<u16>,
pub cipher_suites: Vec<u16>,
pub extensions_raw: Vec<u16>,
pub extensions: Vec<u16>,
pub extensions_sorted: Vec<u16>,
pub supported_groups: Vec<u16>,
pub ec_point_formats: Vec<u8>,
pub signature_algorithms: Vec<u16>,
pub supported_versions: Vec<u16>,
pub alpn: Vec<String>,
pub psk_key_exchange_modes: Vec<u8>,
pub key_share_groups: Vec<u16>,
pub cert_compression_algs: Vec<u16>,
pub alps_payload_by_proto: Vec<(String, Vec<u8>)>,
pub has_ech_ext: bool,
pub ech_payload_len: usize,
pub sni: Option<String>,
pub has_renegotiation_info: bool,
pub has_extended_master_secret: bool,
pub has_session_ticket: bool,
pub raw: Vec<u8>,
}
impl ClientHello {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
let mut ch = ClientHello {
raw: data.to_vec(),
..Default::default()
};
let mut o = 0usize;
need(data, o, 5)?;
if data[o] != 0x16 {
return Err(ParseError::NotHandshake(data[o]));
}
o += 5;
need(data, o, 4)?;
if data[o] != 0x01 {
return Err(ParseError::NotClientHello(data[o]));
}
o += 4;
need(data, o, 2)?;
ch.legacy_version = u16::from_be_bytes([data[o], data[o + 1]]);
o += 2;
need(data, o, 32)?;
o += 32;
need(data, o, 1)?;
let sid_len = data[o] as usize;
o += 1;
need(data, o, sid_len)?;
o += sid_len;
need(data, o, 2)?;
let cipher_len = u16::from_be_bytes([data[o], data[o + 1]]) as usize;
o += 2;
need(data, o, cipher_len)?;
for i in (0..cipher_len).step_by(2) {
let c = u16::from_be_bytes([data[o + i], data[o + i + 1]]);
ch.cipher_suites_raw.push(c);
if !is_grease(c) {
ch.cipher_suites.push(c);
}
}
o += cipher_len;
need(data, o, 1)?;
let comp_len = data[o] as usize;
o += 1 + comp_len;
need(data, o, 2)?;
let ext_total = u16::from_be_bytes([data[o], data[o + 1]]) as usize;
o += 2;
let ext_end = o + ext_total;
need(data, ext_end, 0)?;
while o + 4 <= ext_end {
let t = u16::from_be_bytes([data[o], data[o + 1]]);
let l = u16::from_be_bytes([data[o + 2], data[o + 3]]) as usize;
o += 4;
if o + l > ext_end {
return Err(ParseError::BadExtensionLen {
ext_type: t,
declared: l,
remaining: ext_end - o,
});
}
let payload = &data[o..o + l];
ch.extensions_raw.push(t);
if !is_grease(t) {
ch.extensions.push(t);
}
parse_extension(&mut ch, t, payload)?;
o += l;
}
ch.extensions_sorted = ch.extensions.clone();
ch.extensions_sorted.sort_unstable();
Ok(ch)
}
pub fn ja3_string(&self) -> String {
let mut s = String::with_capacity(256);
let _ = write!(s, "{}", self.legacy_version);
s.push(',');
join_u16(&mut s, &self.cipher_suites, '-');
s.push(',');
join_u16(&mut s, &self.extensions, '-');
s.push(',');
join_u16(&mut s, &self.supported_groups, '-');
s.push(',');
let fmts: Vec<u16> = self.ec_point_formats.iter().map(|&b| b as u16).collect();
join_u16(&mut s, &fmts, '-');
s
}
pub fn ja4_a(&self) -> String {
let tls_code = self
.supported_versions
.iter()
.copied()
.filter(|v| !is_grease(*v))
.max()
.unwrap_or(self.legacy_version);
let tls_pair = match tls_code {
0x0304 => "13",
0x0303 => "12",
0x0302 => "11",
0x0301 => "10",
_ => "00",
};
let sni = if self.sni.is_some() { 'd' } else { 'i' };
let proto = 't'; let cipher_count = self.cipher_suites.len().min(99);
let ext_count = self.extensions.len().min(99);
let alpn = match self.alpn.first().map(|s| s.as_str()) {
Some("h2") => "h2".to_string(),
Some("http/1.1") => "h1".to_string(),
Some(other) if other.len() >= 2 => {
let bytes = other.as_bytes();
format!("{}{}", bytes[0] as char, bytes[bytes.len() - 1] as char)
}
_ => "00".into(),
};
format!("{proto}{tls_pair}{sni}{cipher_count:02}{ext_count:02}{alpn}")
}
pub fn summary(&self) -> String {
let mut s = String::with_capacity(1024);
let _ = writeln!(s, "JA3 = {}", self.ja3_string());
let _ = writeln!(s, "JA4_a = {}", self.ja4_a());
let _ = writeln!(
s,
"cipher_suites[{}] = {:?}",
self.cipher_suites.len(),
self.cipher_suites
);
let _ = writeln!(
s,
"extensions_sorted[{}] = {:?}",
self.extensions_sorted.len(),
self.extensions_sorted
);
let _ = writeln!(s, "supported_groups = {:?}", self.supported_groups);
let _ = writeln!(s, "signature_algos = {:?}", self.signature_algorithms);
let _ = writeln!(s, "supported_versions = {:?}", self.supported_versions);
let _ = writeln!(s, "alpn = {:?}", self.alpn);
let _ = writeln!(s, "key_share_groups = {:?}", self.key_share_groups);
let _ = writeln!(
s,
"cert_compression_algs = {:?}",
self.cert_compression_algs
);
let _ = writeln!(
s,
"psk_key_exchange_modes = {:?}",
self.psk_key_exchange_modes
);
let _ = writeln!(
s,
"alps = {:?}",
self.alps_payload_by_proto
.iter()
.map(|(p, b)| (p.clone(), b.len()))
.collect::<Vec<_>>()
);
let _ = writeln!(
s,
"ech_ext present={} payload_len={}",
self.has_ech_ext, self.ech_payload_len
);
let _ = writeln!(
s,
"ems={} renego_info={} session_ticket={} sni={:?}",
self.has_extended_master_secret,
self.has_renegotiation_info,
self.has_session_ticket,
self.sni
);
s
}
}
fn join_u16(s: &mut String, xs: &[u16], sep: char) {
for (i, x) in xs.iter().enumerate() {
if i > 0 {
s.push(sep);
}
let _ = write!(s, "{x}");
}
}
fn need(data: &[u8], offset: usize, n: usize) -> Result<(), ParseError> {
if offset + n > data.len() {
return Err(ParseError::ShortRead {
offset,
need: n,
have: data.len(),
});
}
Ok(())
}
fn parse_extension(ch: &mut ClientHello, t: u16, p: &[u8]) -> Result<(), ParseError> {
match t {
0 => {
if p.len() >= 5 {
let name_type = p[2];
if name_type == 0 {
let nlen = u16::from_be_bytes([p[3], p[4]]) as usize;
if p.len() >= 5 + nlen {
ch.sni = Some(String::from_utf8_lossy(&p[5..5 + nlen]).into_owned());
}
}
}
}
10 => {
if p.len() >= 2 {
let l = u16::from_be_bytes([p[0], p[1]]) as usize;
let mut i = 2;
while i + 2 <= 2 + l && i + 2 <= p.len() {
let g = u16::from_be_bytes([p[i], p[i + 1]]);
if !is_grease(g) {
ch.supported_groups.push(g);
}
i += 2;
}
}
}
11 => {
if !p.is_empty() {
let l = p[0] as usize;
for i in 0..l {
if 1 + i < p.len() {
ch.ec_point_formats.push(p[1 + i]);
}
}
}
}
13 => {
if p.len() >= 2 {
let l = u16::from_be_bytes([p[0], p[1]]) as usize;
let mut i = 2;
while i + 2 <= 2 + l && i + 2 <= p.len() {
ch.signature_algorithms
.push(u16::from_be_bytes([p[i], p[i + 1]]));
i += 2;
}
}
}
16 => {
if p.len() >= 2 {
let l = u16::from_be_bytes([p[0], p[1]]) as usize;
let mut i = 2;
while i < 2 + l && i < p.len() {
let item_len = p[i] as usize;
i += 1;
if i + item_len <= p.len() {
ch.alpn
.push(String::from_utf8_lossy(&p[i..i + item_len]).into_owned());
}
i += item_len;
}
}
}
23 => ch.has_extended_master_secret = true,
27 => {
if !p.is_empty() {
let l = p[0] as usize;
let mut i = 1;
while i + 2 <= 1 + l && i + 2 <= p.len() {
ch.cert_compression_algs
.push(u16::from_be_bytes([p[i], p[i + 1]]));
i += 2;
}
}
}
35 => ch.has_session_ticket = true,
43 => {
if !p.is_empty() {
let l = p[0] as usize;
let mut i = 1;
while i + 2 <= 1 + l && i + 2 <= p.len() {
let v = u16::from_be_bytes([p[i], p[i + 1]]);
ch.supported_versions.push(v);
i += 2;
}
}
}
45 => {
if !p.is_empty() {
let l = p[0] as usize;
for i in 0..l {
if 1 + i < p.len() {
ch.psk_key_exchange_modes.push(p[1 + i]);
}
}
}
}
51 => {
if p.len() >= 2 {
let l = u16::from_be_bytes([p[0], p[1]]) as usize;
let mut i = 2;
while i + 4 <= 2 + l && i + 4 <= p.len() {
let g = u16::from_be_bytes([p[i], p[i + 1]]);
let kl = u16::from_be_bytes([p[i + 2], p[i + 3]]) as usize;
if !is_grease(g) {
ch.key_share_groups.push(g);
}
i += 4 + kl;
}
}
}
17513 | 17613 => {
if p.len() >= 2 {
let list_len = u16::from_be_bytes([p[0], p[1]]) as usize;
let end = (2 + list_len).min(p.len());
let mut i = 2;
while i < end {
let plen = p[i] as usize;
i += 1;
if i + plen > end {
break;
}
let proto = String::from_utf8_lossy(&p[i..i + plen]).into_owned();
i += plen;
ch.alps_payload_by_proto.push((proto, Vec::new()));
}
}
}
65037 => {
ch.has_ech_ext = true;
ch.ech_payload_len = p.len();
}
65281 => ch.has_renegotiation_info = true,
_ => {}
}
Ok(())
}
pub fn current_chrome_fingerprint_summary(profile: crate::impersonate::Profile) -> &'static str {
use crate::impersonate::catalog::Browser;
let (browser, major, _) = profile.parts();
match (browser, major) {
(Browser::Chrome | Browser::Chromium, m) if m >= 132 => {
"t13i1113h2|ciphers=11|pq=X25519MLKEM768|cert_comp=[2,1,3]|ech=1"
}
(Browser::Chrome | Browser::Chromium, _) => {
"t13i1113h2|ciphers=11|pq=X25519Kyber768|cert_comp=[2,1,3]|ech=1"
}
(Browser::Firefox, _) => "t13i1014h2|ciphers=12|pq=none|cert_comp=[]|ech=0",
(Browser::Edge, _) => "t13i1113h2|ciphers=11|pq=X25519MLKEM768|cert_comp=[2,1,3]|ech=1",
_ => "t13i????h?|ciphers=?|pq=?|cert_comp=?|ech=?",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grease_detection_rfc8701() {
for &g in &[0x0a0au16, 0x1a1a, 0x2a2a, 0xcaca, 0xfafa] {
assert!(is_grease(g), "{g:#06x}");
}
for &g in &[0x0000u16, 0x1301, 0x1a1b, 0x1b1a] {
assert!(!is_grease(g), "{g:#06x}");
}
}
fn tiny_ch() -> Vec<u8> {
let body_cipher = [0x00, 0x02, 0x13, 0x01];
let random = [0u8; 32];
let sid = [0u8; 1];
let compression = [0x01, 0x00];
let name = b"example.com";
let mut sn_ext = Vec::new();
let list_len = 1 + 2 + name.len();
sn_ext.extend_from_slice(&(list_len as u16).to_be_bytes());
sn_ext.push(0);
sn_ext.extend_from_slice(&(name.len() as u16).to_be_bytes());
sn_ext.extend_from_slice(name);
let mut extensions = Vec::new();
extensions.extend_from_slice(&0u16.to_be_bytes()); extensions.extend_from_slice(&(sn_ext.len() as u16).to_be_bytes());
extensions.extend_from_slice(&sn_ext);
let mut body = Vec::new();
body.extend_from_slice(&[0x03, 0x03]); body.extend_from_slice(&random);
body.extend_from_slice(&sid);
body.extend_from_slice(&body_cipher);
body.extend_from_slice(&compression);
body.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
body.extend_from_slice(&extensions);
let mut hs = Vec::new();
hs.push(0x01);
let body_len = body.len() as u32;
hs.push(((body_len >> 16) & 0xff) as u8);
hs.push(((body_len >> 8) & 0xff) as u8);
hs.push((body_len & 0xff) as u8);
hs.extend_from_slice(&body);
let mut rec = Vec::new();
rec.push(0x16);
rec.extend_from_slice(&[0x03, 0x01]);
rec.extend_from_slice(&(hs.len() as u16).to_be_bytes());
rec.extend_from_slice(&hs);
rec
}
#[test]
fn parse_tiny_hello_roundtrip() {
let buf = tiny_ch();
let ch = ClientHello::parse(&buf).expect("parse");
assert_eq!(ch.legacy_version, 0x0303);
assert_eq!(ch.cipher_suites, vec![0x1301]);
assert_eq!(ch.extensions, vec![0]);
assert_eq!(ch.sni.as_deref(), Some("example.com"));
}
#[test]
fn parse_rejects_non_handshake() {
let mut buf = tiny_ch();
buf[0] = 0x17; assert!(matches!(
ClientHello::parse(&buf),
Err(ParseError::NotHandshake(0x17))
));
}
}