Skip to main content

pingap_core/
ja4.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! JA4 TLS client fingerprints, computed from the raw ClientHello the
16//! client sent.
17//!
18//! This follows FoxIO's specification of JA4 (the TLS client fingerprint,
19//! BSD 3-Clause), including its raw and original-order forms. Only JA4 is
20//! implemented; the other members of JA4+ are under a different licence.
21//!
22//! The input is the bytes on the wire, read before the TLS library sees
23//! them, so the result does not depend on the TLS backend.
24
25use sha2::{Digest, Sha256};
26use std::fmt;
27
28/// The largest ClientHello (handshake message body) that is fingerprinted.
29/// A real one is a few KiB even with post-quantum key shares; anything
30/// larger is reported as [`ClientHelloStatus::Invalid`] rather than read on.
31pub 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;
37/// RFC 8446 §5.1: a plaintext record carries at most 2^14 bytes.
38const 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
45/// The value of a hashed section whose list is empty.
46const EMPTY_HASH: &str = "000000000000";
47const HEX: &[u8; 16] = b"0123456789abcdef";
48
49/// Whether `value` is a GREASE value (RFC 8701): `0x0a0a`, `0x1a1a`, ...
50/// `0xfafa`. JA4 ignores them wherever they appear.
51#[inline]
52pub fn is_grease(value: u16) -> bool {
53    value & 0x0f0f == 0x0a0a && value >> 8 == value & 0x00ff
54}
55
56/// Where reading a ClientHello from the start of a connection stands.
57#[derive(Debug, PartialEq, Eq)]
58pub enum ClientHelloStatus {
59    /// The bytes so far are a valid beginning; more are needed.
60    Incomplete,
61    /// The whole ClientHello, as the handshake message body (without the
62    /// handshake header), reassembled from however many records carried it.
63    Complete(Vec<u8>),
64    /// Not a TLS ClientHello, or larger than [`MAX_CLIENT_HELLO_SIZE`].
65    Invalid,
66}
67
68/// Reads the ClientHello from the first bytes of a TLS connection.
69///
70/// The message may be split over several handshake records (RFC 8446 §5.1
71/// allows it); the fragments are reassembled. Anything that is not a
72/// handshake record carrying a ClientHello is [`ClientHelloStatus::Invalid`]
73/// as soon as it can be told apart.
74pub 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
117/// A cursor over TLS wire data; every read is bounds checked.
118struct 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    /// A vector with a one-byte length prefix.
144    fn vec8(&mut self) -> Option<&'a [u8]> {
145        let n = usize::from(self.u8()?);
146        self.take(n)
147    }
148    /// A vector with a two-byte length prefix.
149    fn vec16(&mut self) -> Option<&'a [u8]> {
150        let n = usize::from(self.u16()?);
151        self.take(n)
152    }
153}
154
155/// The `u16` values of a list, GREASE removed; `None` for an odd length.
156fn 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
170/// The first protocol of an ALPN extension, `Some(None)` when the list is
171/// empty, `None` when the extension is malformed.
172fn 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
185/// The highest version a supported_versions extension offers, GREASE
186/// ignored; `Some(None)` when it offers none, `None` when malformed.
187fn 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
196/// The two characters JA4 uses for a TLS version.
197fn 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
212/// The two characters JA4 uses for the first ALPN protocol: its first and
213/// last characters when both are ASCII alphanumeric, otherwise the first
214/// and last characters of its hex form; `00` when there is none.
215fn 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
229/// A count as the two digits JA4 prints, capped at 99.
230fn 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
236/// `values` as comma separated four-digit lowercase hex.
237fn 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
250/// The first 12 lowercase hex characters of the SHA-256 of `input`.
251fn 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
261/// The part of a fingerprint built from the extensions: the list, then,
262/// when there are any, `_` and the signature algorithms in their original
263/// order.
264fn 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/// The JA4 fingerprint of one ClientHello.
282///
283/// [`Ja4Fingerprint::ja4`] is computed up front, since it is the form that
284/// is normally logged; the raw and original-order forms are built when
285/// asked for.
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct Ja4Fingerprint {
288    /// The first section, e.g. `t13d1516h2`.
289    prefix: String,
290    /// Cipher suites in the order the client sent them, GREASE removed.
291    ciphers: Box<[u16]>,
292    /// Extensions in the order the client sent them, GREASE removed.
293    extensions: Box<[u16]>,
294    /// Signature algorithms in the order the client sent them, GREASE
295    /// removed.
296    signature_algorithms: Box<[u16]>,
297    ja4: Box<str>,
298}
299
300impl Ja4Fingerprint {
301    /// Fingerprints a ClientHello message body, as returned by
302    /// [`read_client_hello`]. `None` when it is malformed: a fingerprint of
303    /// a broken hello would not match what any other tool computes.
304    pub fn from_client_hello(body: &[u8]) -> Option<Self> {
305        let mut reader = Reader::new(body);
306        let legacy_version = reader.u16()?;
307        // random
308        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        // compression methods
315        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    /// The raw ciphers and extensions of the sorted forms: both sorted,
383    /// SNI and ALPN left out of the extensions (the prefix covers them).
384    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    /// The raw ciphers and extensions of the original-order forms: as sent,
403    /// SNI and ALPN included.
404    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    /// `JA4`, e.g. `t13d1516h2_8daaf6152771_e5627efa2ab1`.
414    pub fn ja4(&self) -> &str {
415        &self.ja4
416    }
417
418    /// `JA4_r`: the sorted lists themselves instead of their hashes.
419    pub fn ja4_r(&self) -> String {
420        let (ciphers, extensions) = self.sorted_parts();
421        format!("{}_{ciphers}_{extensions}", self.prefix)
422    }
423
424    /// `JA4_o`: hashed like `JA4`, but over the lists in the order the
425    /// client sent them, SNI and ALPN included.
426    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    /// `JA4_ro`: the lists in the order the client sent them, unhashed.
437    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/// Builders for ClientHello bytes, for tests and benchmarks in this and
450/// other crates. Not part of the API.
451#[doc(hidden)]
452pub mod testing {
453    /// An SNI extension payload for `host`.
454    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    /// An ALPN extension payload offering `protocols` in order.
465    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    /// A supported_versions extension payload (client form).
477    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    /// A signature_algorithms extension payload.
486    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    /// A ClientHello message body. `extensions` of `None` leaves the
495    /// extensions block out, as an old client would.
496    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    /// `body` as a handshake message split into records of at most
524    /// `record_size` payload bytes each.
525    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    /// The ClientHello behind the examples of the JA4 specification: the
539    /// cipher, extension and signature algorithm lists of its `JA4_ro`
540    /// example, in that order, with GREASE values mixed in the way
541    /// browsers send them. Its fingerprints are the specification's.
542    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    /// The hash inputs and outputs the specification prints.
595    #[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        // Without signature algorithms the string ends without `_`.
610        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    /// Every form the specification gives for its example ClientHello.
619    #[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    /// A TLS 1.2 client with no extensions at all: the legacy version
636    /// counts, no SNI is `i`, no ALPN is `00`, and the extension hash is
637    /// the empty value.
638    #[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        // supported_versions wins over the legacy version, GREASE aside.
657        assert_eq!(
658            "t13i0101",
659            &prefix(&[(0x002b, supported_versions(&[0x0303, 0x0304, 0xfafa]))])
660                [..8]
661        );
662        // Only GREASE in it: the legacy version stands.
663        assert_eq!(
664            "t10i0101",
665            &prefix(&[(0x002b, supported_versions(&[0x2a2a]))])[..8]
666        );
667        // An unknown version, no ciphers, an empty extensions block.
668        let fp = fingerprint(&client_hello_body(0x9999, &[], Some(&[])));
669        assert_eq!("t00i000000_000000000000_000000000000", fp.ja4());
670        // ALPN: first protocol only, first and last character.
671        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        // Not alphanumeric at either end: the hex form's ends.
677        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        // SNI counts as an extension and turns `i` into `d`.
683        assert_eq!("t10d0101", &prefix(&[(0x0000, sni("a.b"))])[..8]);
684    }
685
686    /// Counts stop at 99, GREASE never counts.
687    #[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    /// The extension hash leaves SNI and ALPN out; the original-order
698    /// hash keeps them.
699    #[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        // An odd-length cipher list.
739        // The cipher list length sits right after the 32-byte session id.
740        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        // Split over many records, and followed by more data.
754        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        // Every prefix is incomplete, never invalid.
762        for len in 0..records.len() {
763            assert_eq!(
764                ClientHelloStatus::Incomplete,
765                read_client_hello(&records[..len]),
766                "{len}"
767            );
768        }
769        // Not TLS, not a ClientHello, too large.
770        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}