ja4 0.1.0

A fast, pure-rust, no_std implementation of the JA4 TLS client fingerprinting algorithm.
Documentation
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
use ja4::parse_ja4;
use rand::RngExt;

fn generate_packets(count: usize) -> Vec<Vec<u8>> {
    let mut rng = rand::rng();
    let mut packets = Vec::with_capacity(count);

    for _ in 0..count {
        let mut handshake = vec![0x03, 0x03]; // TLS 1.2

        // 32 bytes random
        for _ in 0..32 {
            handshake.push(rng.random::<u8>());
        }

        // Session ID length (0 to 32)
        let sess_len = rng.random_range(0..=32);
        handshake.push(sess_len as u8);
        for _ in 0..sess_len {
            handshake.push(rng.random::<u8>());
        }

        // Ciphers (10 to 40)
        let num_ciphers: u16 = rng.random_range(10..=40);
        handshake.extend_from_slice(&(num_ciphers * 2).to_be_bytes());
        for _ in 0..num_ciphers {
            handshake.extend_from_slice(&rng.random::<u16>().to_be_bytes());
        }

        // Compression methods (1 byte)
        handshake.extend_from_slice(&[0x01, 0x00]);

        // Extensions
        let mut extensions_payload = Vec::new();
        let num_extensions = rng.random_range(5..=19);
        for i in 0..num_extensions {
            let ext_type = match i {
                0 => 0x0000, // SNI
                1 => 0x0010, // ALPN
                2 => 0x000d, // Sig Algs
                3 => 0x002b, // Supported Versions
                _ => rng.random::<u16>(),
            };

            extensions_payload.extend_from_slice(&ext_type.to_be_bytes());

            if ext_type == 0x0000 {
                extensions_payload.extend_from_slice(&[0x00, 0x00]); // SNI len 0
            } else if ext_type == 0x0010 {
                let alpn_len: u16 = rng.random_range(2..=11);
                extensions_payload.extend_from_slice(&(3 + alpn_len).to_be_bytes());
                extensions_payload.extend_from_slice(&(1 + alpn_len).to_be_bytes());
                extensions_payload.push(alpn_len as u8);
                for _ in 0..alpn_len {
                    extensions_payload.push(b'a' + rng.random_range(0..26) as u8);
                }
            } else if ext_type == 0x000d {
                let num_sigs: u16 = rng.random_range(2..=11);
                extensions_payload.extend_from_slice(&(2 + num_sigs * 2).to_be_bytes());
                extensions_payload.extend_from_slice(&(num_sigs * 2).to_be_bytes());
                for _ in 0..num_sigs {
                    extensions_payload.extend_from_slice(&rng.random::<u16>().to_be_bytes());
                }
            } else if ext_type == 0x002b {
                extensions_payload.extend_from_slice(&[0x00, 0x03, 0x02, 0x03, 0x04]);
            } else {
                let ext_len: u16 = rng.random_range(0..=15);
                extensions_payload.extend_from_slice(&ext_len.to_be_bytes());
                for _ in 0..ext_len {
                    extensions_payload.push(rng.random::<u8>());
                }
            }
        }

        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, // ClientHello
            ((handshake.len() >> 16) & 0xFF) as u8,
            ((handshake.len() >> 8) & 0xFF) as u8,
            (handshake.len() & 0xFF) as u8,
        ];
        packet.extend_from_slice(&handshake);
        packets.push(packet);
    }

    packets
}

fn bench_parse_ja4(c: &mut Criterion) {
    const BATCH_SIZE: usize = 1024;
    let quic_packets = generate_packets(BATCH_SIZE);

    let tcp_packets: Vec<Vec<u8>> = quic_packets
        .iter()
        .map(|p| {
            let mut tcp = vec![0x16, 0x03, 0x01];
            tcp.extend_from_slice(&(p.len() as u16).to_be_bytes());
            tcp.extend_from_slice(p);
            tcp
        })
        .collect();

    c.bench_function("parse_ja4_quic_randomized", |b| {
        let mut i = 0;
        b.iter(|| {
            let packet = &quic_packets[i & (BATCH_SIZE - 1)];
            i += 1;
            parse_ja4(black_box(packet), black_box(true))
        })
    });

    c.bench_function("parse_ja4_tcp_randomized", |b| {
        let mut i = 0;
        b.iter(|| {
            let packet = &tcp_packets[i & (BATCH_SIZE - 1)];
            i += 1;
            parse_ja4(black_box(packet), black_box(false))
        })
    });
}

criterion_group!(benches, bench_parse_ja4);
criterion_main!(benches);