1use sha2::{Digest, Sha256};
26use std::fmt;
27
28pub const MAX_CLIENT_HELLO_SIZE: usize = 16 * 1024;
32
33const RECORD_HEADER_LEN: usize = 5;
34const HANDSHAKE_HEADER_LEN: usize = 4;
35const CONTENT_TYPE_HANDSHAKE: u8 = 0x16;
36const HANDSHAKE_TYPE_CLIENT_HELLO: u8 = 0x01;
37const MAX_RECORD_LEN: usize = 1 << 14;
39
40const EXT_SERVER_NAME: u16 = 0x0000;
41const EXT_SIGNATURE_ALGORITHMS: u16 = 0x000d;
42const EXT_ALPN: u16 = 0x0010;
43const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
44
45const EMPTY_HASH: &str = "000000000000";
47const HEX: &[u8; 16] = b"0123456789abcdef";
48
49#[inline]
52pub fn is_grease(value: u16) -> bool {
53 value & 0x0f0f == 0x0a0a && value >> 8 == value & 0x00ff
54}
55
56#[derive(Debug, PartialEq, Eq)]
58pub enum ClientHelloStatus {
59 Incomplete,
61 Complete(Vec<u8>),
64 Invalid,
66}
67
68pub fn read_client_hello(buf: &[u8]) -> ClientHelloStatus {
75 let mut message: Vec<u8> = Vec::new();
76 let mut expected: Option<usize> = None;
77 let mut pos = 0;
78 while buf.len() - pos >= RECORD_HEADER_LEN {
79 let header = &buf[pos..pos + RECORD_HEADER_LEN];
80 if header[0] != CONTENT_TYPE_HANDSHAKE {
81 return ClientHelloStatus::Invalid;
82 }
83 let len = usize::from(u16::from_be_bytes([header[3], header[4]]));
84 if len == 0 || len > MAX_RECORD_LEN {
85 return ClientHelloStatus::Invalid;
86 }
87 let start = pos + RECORD_HEADER_LEN;
88 let Some(fragment) = buf.get(start..start + len) else {
89 return ClientHelloStatus::Incomplete;
90 };
91 message.extend_from_slice(fragment);
92 pos = start + len;
93
94 if expected.is_none() && message.len() >= HANDSHAKE_HEADER_LEN {
95 if message[0] != HANDSHAKE_TYPE_CLIENT_HELLO {
96 return ClientHelloStatus::Invalid;
97 }
98 let body_len = usize::from(message[1]) << 16
99 | usize::from(message[2]) << 8
100 | usize::from(message[3]);
101 if body_len > MAX_CLIENT_HELLO_SIZE {
102 return ClientHelloStatus::Invalid;
103 }
104 expected = Some(HANDSHAKE_HEADER_LEN + body_len);
105 }
106 if let Some(total) = expected
107 && message.len() >= total
108 {
109 message.truncate(total);
110 message.drain(..HANDSHAKE_HEADER_LEN);
111 return ClientHelloStatus::Complete(message);
112 }
113 }
114 ClientHelloStatus::Incomplete
115}
116
117struct Reader<'a> {
119 data: &'a [u8],
120}
121
122impl<'a> Reader<'a> {
123 fn new(data: &'a [u8]) -> Self {
124 Self { data }
125 }
126 fn is_empty(&self) -> bool {
127 self.data.is_empty()
128 }
129 fn take(&mut self, n: usize) -> Option<&'a [u8]> {
130 if self.data.len() < n {
131 return None;
132 }
133 let (head, tail) = self.data.split_at(n);
134 self.data = tail;
135 Some(head)
136 }
137 fn u8(&mut self) -> Option<u8> {
138 self.take(1).map(|b| b[0])
139 }
140 fn u16(&mut self) -> Option<u16> {
141 self.take(2).map(|b| u16::from_be_bytes([b[0], b[1]]))
142 }
143 fn vec8(&mut self) -> Option<&'a [u8]> {
145 let n = usize::from(self.u8()?);
146 self.take(n)
147 }
148 fn vec16(&mut self) -> Option<&'a [u8]> {
150 let n = usize::from(self.u16()?);
151 self.take(n)
152 }
153}
154
155fn u16_list(data: &[u8]) -> Option<Vec<u16>> {
157 let (pairs, rest) = data.as_chunks::<2>();
158 if !rest.is_empty() {
159 return None;
160 }
161 Some(
162 pairs
163 .iter()
164 .map(|pair| u16::from_be_bytes(*pair))
165 .filter(|value| !is_grease(*value))
166 .collect(),
167 )
168}
169
170fn first_alpn(data: &[u8]) -> Option<Option<&[u8]>> {
173 let mut reader = Reader::new(data);
174 let list = reader.vec16()?;
175 if !reader.is_empty() {
176 return None;
177 }
178 let mut list = Reader::new(list);
179 if list.is_empty() {
180 return Some(None);
181 }
182 Some(Some(list.vec8()?))
183}
184
185fn highest_version(data: &[u8]) -> Option<Option<u16>> {
188 let mut reader = Reader::new(data);
189 let versions = u16_list(reader.vec8()?)?;
190 if !reader.is_empty() {
191 return None;
192 }
193 Some(versions.into_iter().max())
194}
195
196fn version_code(version: u16) -> &'static str {
198 match version {
199 0x0304 => "13",
200 0x0303 => "12",
201 0x0302 => "11",
202 0x0301 => "10",
203 0x0300 => "s3",
204 0x0002 => "s2",
205 0xfeff => "d1",
206 0xfefd => "d2",
207 0xfefc => "d3",
208 _ => "00",
209 }
210}
211
212fn alpn_code(protocol: Option<&[u8]>) -> [u8; 2] {
216 match protocol {
217 Some(value) if !value.is_empty() => {
218 let (first, last) = (value[0], value[value.len() - 1]);
219 if first.is_ascii_alphanumeric() && last.is_ascii_alphanumeric() {
220 [first, last]
221 } else {
222 [HEX[usize::from(first >> 4)], HEX[usize::from(last & 0x0f)]]
223 }
224 },
225 _ => *b"00",
226 }
227}
228
229fn push_count(out: &mut String, count: usize) {
231 let count = count.min(99);
232 out.push(char::from(b'0' + (count / 10) as u8));
233 out.push(char::from(b'0' + (count % 10) as u8));
234}
235
236fn join_hex(values: &[u16]) -> String {
238 let mut out = String::with_capacity(values.len() * 5);
239 for (i, value) in values.iter().enumerate() {
240 if i > 0 {
241 out.push(',');
242 }
243 for shift in [12, 8, 4, 0] {
244 out.push(char::from(HEX[usize::from((value >> shift) & 0x0f)]));
245 }
246 }
247 out
248}
249
250fn hash12(input: &str) -> String {
252 let digest = Sha256::digest(input.as_bytes());
253 let mut out = String::with_capacity(12);
254 for byte in &digest[..6] {
255 out.push(char::from(HEX[usize::from(byte >> 4)]));
256 out.push(char::from(HEX[usize::from(byte & 0x0f)]));
257 }
258 out
259}
260
261fn extension_part(extensions: &[u16], signature_algorithms: &[u16]) -> String {
265 let mut out = join_hex(extensions);
266 if !signature_algorithms.is_empty() {
267 out.push('_');
268 out.push_str(&join_hex(signature_algorithms));
269 }
270 out
271}
272
273fn hashed(raw: &str, list_is_empty: bool) -> String {
274 if list_is_empty {
275 EMPTY_HASH.to_string()
276 } else {
277 hash12(raw)
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct Ja4Fingerprint {
288 prefix: String,
290 ciphers: Box<[u16]>,
292 extensions: Box<[u16]>,
294 signature_algorithms: Box<[u16]>,
297 ja4: Box<str>,
298}
299
300impl Ja4Fingerprint {
301 pub fn from_client_hello(body: &[u8]) -> Option<Self> {
305 let mut reader = Reader::new(body);
306 let legacy_version = reader.u16()?;
307 reader.take(32)?;
309 let session_id = reader.vec8()?;
310 if session_id.len() > 32 {
311 return None;
312 }
313 let ciphers = u16_list(reader.vec16()?)?;
314 reader.vec8()?;
316
317 let mut extensions = Vec::new();
318 let mut signature_algorithms = Vec::new();
319 let mut has_sni = false;
320 let mut alpn = None;
321 let mut supported_version = None;
322 if !reader.is_empty() {
323 let mut list = Reader::new(reader.vec16()?);
324 if !reader.is_empty() {
325 return None;
326 }
327 while !list.is_empty() {
328 let ext_type = list.u16()?;
329 let data = list.vec16()?;
330 if is_grease(ext_type) {
331 continue;
332 }
333 extensions.push(ext_type);
334 match ext_type {
335 EXT_SERVER_NAME => has_sni = true,
336 EXT_ALPN => alpn = first_alpn(data)?,
337 EXT_SUPPORTED_VERSIONS => {
338 supported_version = highest_version(data)?
339 },
340 EXT_SIGNATURE_ALGORITHMS => {
341 let mut reader = Reader::new(data);
342 signature_algorithms = u16_list(reader.vec16()?)?;
343 if !reader.is_empty() {
344 return None;
345 }
346 },
347 _ => {},
348 }
349 }
350 }
351
352 let mut prefix = String::with_capacity(10);
353 prefix.push('t');
354 prefix.push_str(version_code(
355 supported_version.unwrap_or(legacy_version),
356 ));
357 prefix.push(if has_sni { 'd' } else { 'i' });
358 push_count(&mut prefix, ciphers.len());
359 push_count(&mut prefix, extensions.len());
360 let [first, last] = alpn_code(alpn);
361 prefix.push(char::from(first));
362 prefix.push(char::from(last));
363
364 let mut fingerprint = Self {
365 prefix,
366 ciphers: ciphers.into(),
367 extensions: extensions.into(),
368 signature_algorithms: signature_algorithms.into(),
369 ja4: Box::default(),
370 };
371 let (ciphers, extensions) = fingerprint.sorted_parts();
372 fingerprint.ja4 = format!(
373 "{}_{}_{}",
374 fingerprint.prefix,
375 hashed(&ciphers, fingerprint.ciphers.is_empty()),
376 hashed(&extensions, extensions.is_empty())
377 )
378 .into();
379 Some(fingerprint)
380 }
381
382 fn sorted_parts(&self) -> (String, String) {
385 let mut ciphers = self.ciphers.to_vec();
386 ciphers.sort_unstable();
387 let mut extensions: Vec<u16> = self
388 .extensions
389 .iter()
390 .copied()
391 .filter(|ext| *ext != EXT_SERVER_NAME && *ext != EXT_ALPN)
392 .collect();
393 extensions.sort_unstable();
394 let extensions = if extensions.is_empty() {
395 String::new()
396 } else {
397 extension_part(&extensions, &self.signature_algorithms)
398 };
399 (join_hex(&ciphers), extensions)
400 }
401
402 fn original_parts(&self) -> (String, String) {
405 let extensions = if self.extensions.is_empty() {
406 String::new()
407 } else {
408 extension_part(&self.extensions, &self.signature_algorithms)
409 };
410 (join_hex(&self.ciphers), extensions)
411 }
412
413 pub fn ja4(&self) -> &str {
415 &self.ja4
416 }
417
418 pub fn ja4_r(&self) -> String {
420 let (ciphers, extensions) = self.sorted_parts();
421 format!("{}_{ciphers}_{extensions}", self.prefix)
422 }
423
424 pub fn ja4_o(&self) -> String {
427 let (ciphers, extensions) = self.original_parts();
428 format!(
429 "{}_{}_{}",
430 self.prefix,
431 hashed(&ciphers, self.ciphers.is_empty()),
432 hashed(&extensions, self.extensions.is_empty())
433 )
434 }
435
436 pub fn ja4_ro(&self) -> String {
438 let (ciphers, extensions) = self.original_parts();
439 format!("{}_{ciphers}_{extensions}", self.prefix)
440 }
441}
442
443impl fmt::Display for Ja4Fingerprint {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 f.write_str(&self.ja4)
446 }
447}
448
449#[doc(hidden)]
452pub mod testing {
453 pub fn sni(host: &str) -> Vec<u8> {
455 let name = host.as_bytes();
456 let mut entry = vec![0u8];
457 entry.extend_from_slice(&(name.len() as u16).to_be_bytes());
458 entry.extend_from_slice(name);
459 let mut out = (entry.len() as u16).to_be_bytes().to_vec();
460 out.extend(entry);
461 out
462 }
463
464 pub fn alpn(protocols: &[&[u8]]) -> Vec<u8> {
466 let mut list = Vec::new();
467 for protocol in protocols {
468 list.push(protocol.len() as u8);
469 list.extend_from_slice(protocol);
470 }
471 let mut out = (list.len() as u16).to_be_bytes().to_vec();
472 out.extend(list);
473 out
474 }
475
476 pub fn supported_versions(versions: &[u16]) -> Vec<u8> {
478 let mut out = vec![(versions.len() * 2) as u8];
479 for version in versions {
480 out.extend_from_slice(&version.to_be_bytes());
481 }
482 out
483 }
484
485 pub fn signature_algorithms(algorithms: &[u16]) -> Vec<u8> {
487 let mut out = ((algorithms.len() * 2) as u16).to_be_bytes().to_vec();
488 for algorithm in algorithms {
489 out.extend_from_slice(&algorithm.to_be_bytes());
490 }
491 out
492 }
493
494 pub fn client_hello_body(
497 legacy_version: u16,
498 ciphers: &[u16],
499 extensions: Option<&[(u16, Vec<u8>)]>,
500 ) -> Vec<u8> {
501 let mut body = legacy_version.to_be_bytes().to_vec();
502 body.extend_from_slice(&[0x11; 32]);
503 body.push(32);
504 body.extend_from_slice(&[0x22; 32]);
505 body.extend_from_slice(&((ciphers.len() * 2) as u16).to_be_bytes());
506 for cipher in ciphers {
507 body.extend_from_slice(&cipher.to_be_bytes());
508 }
509 body.extend_from_slice(&[1, 0]);
510 if let Some(extensions) = extensions {
511 let mut block = Vec::new();
512 for (ext_type, data) in extensions {
513 block.extend_from_slice(&ext_type.to_be_bytes());
514 block.extend_from_slice(&(data.len() as u16).to_be_bytes());
515 block.extend_from_slice(data);
516 }
517 body.extend_from_slice(&(block.len() as u16).to_be_bytes());
518 body.extend(block);
519 }
520 body
521 }
522
523 pub fn client_hello_records(body: &[u8], record_size: usize) -> Vec<u8> {
526 let mut message = vec![0x01];
527 message.extend_from_slice(&(body.len() as u32).to_be_bytes()[1..]);
528 message.extend_from_slice(body);
529 let mut out = Vec::new();
530 for chunk in message.chunks(record_size.max(1)) {
531 out.extend_from_slice(&[0x16, 0x03, 0x01]);
532 out.extend_from_slice(&(chunk.len() as u16).to_be_bytes());
533 out.extend_from_slice(chunk);
534 }
535 out
536 }
537
538 pub fn spec_example_body() -> Vec<u8> {
543 let ciphers = [
544 0x2a2a, 0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
545 0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035,
546 ];
547 let sig_algs = [
548 0x0403, 0x0804, 0x0401, 0x0503, 0x0805, 0x0501, 0x0806, 0x0601,
549 ];
550 let extensions: Vec<(u16, Vec<u8>)> = vec![
551 (0x0a0a, vec![]),
552 (0x001b, vec![2, 0, 2]),
553 (0x0000, sni("example.com")),
554 (0x0033, vec![0, 0]),
555 (0x0010, alpn(&[b"h2", b"http/1.1"])),
556 (0x4469, vec![0, 3, 2, b'h', b'2']),
557 (0x0017, vec![]),
558 (0x002d, vec![1, 1]),
559 (0x000d, signature_algorithms(&sig_algs)),
560 (0x0005, vec![1, 0, 0, 0, 0]),
561 (0x0023, vec![]),
562 (0x0012, vec![]),
563 (0x002b, supported_versions(&[0x3a3a, 0x0304, 0x0303])),
564 (0xff01, vec![0]),
565 (0x000b, vec![1, 0]),
566 (0x000a, vec![0, 4, 0x3a, 0x3a, 0, 0x1d]),
567 (0x0015, vec![0; 16]),
568 (0x1a1a, vec![0]),
569 ];
570 client_hello_body(0x0303, &ciphers, Some(&extensions))
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::testing::*;
577 use super::*;
578 use pretty_assertions::assert_eq;
579
580 fn fingerprint(body: &[u8]) -> Ja4Fingerprint {
581 Ja4Fingerprint::from_client_hello(body).expect("valid client hello")
582 }
583
584 #[test]
585 fn test_is_grease() {
586 for value in [0x0a0a, 0x1a1a, 0x2a2a, 0xaaaa, 0xeaea, 0xfafa] {
587 assert_eq!(true, is_grease(value), "{value:04x}");
588 }
589 for value in [0x0a1a, 0x0000, 0x1301, 0xabab, 0x0a0b] {
590 assert_eq!(false, is_grease(value), "{value:04x}");
591 }
592 }
593
594 #[test]
596 fn test_spec_hashes() {
597 assert_eq!(
598 "8daaf6152771",
599 hash12(
600 "002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9"
601 )
602 );
603 assert_eq!(
604 "e5627efa2ab1",
605 hash12(
606 "0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601"
607 )
608 );
609 assert_eq!(
611 "6d807ffa2a79",
612 hash12(
613 "0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01"
614 )
615 );
616 }
617
618 #[test]
620 fn test_spec_example() {
621 let fp = fingerprint(&spec_example_body());
622 assert_eq!("t13d1516h2_8daaf6152771_e5627efa2ab1", fp.ja4());
623 assert_eq!("t13d1516h2_8daaf6152771_e5627efa2ab1", fp.to_string());
624 assert_eq!(
625 "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
626 fp.ja4_r()
627 );
628 assert_eq!("t13d1516h2_acb858a92679_18f69afefd3d", fp.ja4_o());
629 assert_eq!(
630 "t13d1516h2_1301,1302,1303,c02b,c02f,c02c,c030,cca9,cca8,c013,c014,009c,009d,002f,0035_001b,0000,0033,0010,4469,0017,002d,000d,0005,0023,0012,002b,ff01,000b,000a,0015_0403,0804,0401,0503,0805,0501,0806,0601",
631 fp.ja4_ro()
632 );
633 }
634
635 #[test]
639 fn test_no_extensions() {
640 let fp =
641 fingerprint(&client_hello_body(0x0303, &[0xc02f, 0x009c], None));
642 assert_eq!(
643 format!("t12i020000_{}_000000000000", hash12("009c,c02f")),
644 fp.ja4()
645 );
646 assert_eq!("t12i020000_009c,c02f_", fp.ja4_r());
647 }
648
649 #[test]
650 fn test_prefix_rules() {
651 let prefix = |extensions: &[(u16, Vec<u8>)]| {
652 fingerprint(&client_hello_body(0x0301, &[0x1301], Some(extensions)))
653 .ja4()[..10]
654 .to_string()
655 };
656 assert_eq!(
658 "t13i0101",
659 &prefix(&[(0x002b, supported_versions(&[0x0303, 0x0304, 0xfafa]))])
660 [..8]
661 );
662 assert_eq!(
664 "t10i0101",
665 &prefix(&[(0x002b, supported_versions(&[0x2a2a]))])[..8]
666 );
667 let fp = fingerprint(&client_hello_body(0x9999, &[], Some(&[])));
669 assert_eq!("t00i000000_000000000000_000000000000", fp.ja4());
670 assert_eq!(
672 "t10i0101h1",
673 prefix(&[(0x0010, alpn(&[b"http/1.1", b"h2"]))])
674 );
675 assert_eq!("t10i0101hh", prefix(&[(0x0010, alpn(&[b"h"]))]));
676 assert_eq!("t10i0101ab", prefix(&[(0x0010, alpn(&[&[0xab]]))]));
678 assert_eq!("t10i010120", prefix(&[(0x0010, alpn(&[b" "]))]));
679 assert_eq!("t10i0101ad", prefix(&[(0x0010, alpn(&[&[0xab, 0xcd]]))]));
680 assert_eq!("t10i010100", prefix(&[(0x0010, alpn(&[b""]))]));
681 assert_eq!("t10i010100", prefix(&[(0x0010, alpn(&[]))]));
682 assert_eq!("t10d0101", &prefix(&[(0x0000, sni("a.b"))])[..8]);
684 }
685
686 #[test]
688 fn test_counts() {
689 let ciphers: Vec<u16> = (0..120)
690 .map(|i| 0x0100 + i)
691 .chain([0x0a0a, 0x1a1a])
692 .collect();
693 let fp = fingerprint(&client_hello_body(0x0303, &ciphers, None));
694 assert_eq!("t12i9900", &fp.ja4()[..8]);
695 }
696
697 #[test]
700 fn test_sni_and_alpn_in_hashes() {
701 let body = client_hello_body(
702 0x0303,
703 &[0x1301],
704 Some(&[
705 (0x0000, sni("example.com")),
706 (0x0010, alpn(&[b"h2"])),
707 (0x0017, vec![]),
708 ]),
709 );
710 let fp = fingerprint(&body);
711 assert_eq!(
712 format!("t12d0103h2_{}_{}", hash12("1301"), hash12("0017")),
713 fp.ja4()
714 );
715 assert_eq!(
716 format!(
717 "t12d0103h2_{}_{}",
718 hash12("1301"),
719 hash12("0000,0010,0017")
720 ),
721 fp.ja4_o()
722 );
723 }
724
725 #[test]
726 fn test_malformed_client_hello() {
727 let body = spec_example_body();
728 for len in [0, 1, 34, 40, body.len() - 1] {
729 assert_eq!(
730 None,
731 Ja4Fingerprint::from_client_hello(&body[..len]),
732 "{len}"
733 );
734 }
735 let mut trailing = body.clone();
736 trailing.push(0);
737 assert_eq!(None, Ja4Fingerprint::from_client_hello(&trailing));
738 let mut odd = client_hello_body(0x0303, &[0x1301], None);
741 odd[68] = 3;
742 assert_eq!(None, Ja4Fingerprint::from_client_hello(&odd));
743 }
744
745 #[test]
746 fn test_read_client_hello() {
747 let body = spec_example_body();
748 let records = client_hello_records(&body, 16 * 1024);
749 assert_eq!(
750 ClientHelloStatus::Complete(body.clone()),
751 read_client_hello(&records)
752 );
753 let mut split = client_hello_records(&body, 100);
755 assert_eq!(true, split.len() > records.len());
756 split.extend_from_slice(&[0x17, 0x03, 0x03, 0x00, 0x01, 0xff]);
757 assert_eq!(
758 ClientHelloStatus::Complete(body.clone()),
759 read_client_hello(&split)
760 );
761 for len in 0..records.len() {
763 assert_eq!(
764 ClientHelloStatus::Incomplete,
765 read_client_hello(&records[..len]),
766 "{len}"
767 );
768 }
769 assert_eq!(
771 ClientHelloStatus::Invalid,
772 read_client_hello(b"GET / HTTP/1.1\r\n")
773 );
774 let mut server_hello = records.clone();
775 server_hello[5] = 0x02;
776 assert_eq!(
777 ClientHelloStatus::Invalid,
778 read_client_hello(&server_hello)
779 );
780 let huge = [0x16, 0x03, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x00];
781 assert_eq!(ClientHelloStatus::Invalid, read_client_hello(&huge));
782 assert_eq!(
783 ClientHelloStatus::Invalid,
784 read_client_hello(&[0x16, 0x03, 0x01, 0x00, 0x00])
785 );
786 }
787}