From e4207b343a53be24b72e8dc8d80080106463c915 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Wed, 10 Jun 2026 08:01:24 -0400 Subject: [PATCH] feat(ja3-ja4-tls-fingerprinting): JA3/JA4 TLS fingerprinting tool in Rust (M0-M5) Passive TLS client and server fingerprinting from packet captures. Two-crate workspace: tlsfp-core (parsing and fingerprint logic, no I/O, unsafe forbidden) and tlsfp (clap CLI). Edition 2024, MSRV 1.85. Core (M0-M4): a hand-rolled bounds-checked TLS parser for ClientHello, ServerHello, and the TLS 1.2 Certificate chain, plus the full fingerprint family computed byte-exact against published vectors: JA3/JA3S (MD5), JA4/JA4_r, JA4S, JA4H, JA4X, and JA4T. GREASE is stripped, wire order and unknown cipher and extension values are preserved. Pipeline (M5): a pcap and pcapng file reader behind a PacketSource trait, etherparse L2-L4 decode (Ethernet, raw IP, BSD loopback, Linux SLL and SLL2, VLAN and QinQ), and a hand-rolled per-flow TCP reassembler with wrapping serial arithmetic, out-of-order parking, first-write-wins overlap resolution, and bounded memory. The tlsfp pcap subcommand fingerprints every handshake in a capture, as readable lines or NDJSON. Verified three ways: 94 tests including unit KATs, nine real-pcap full-pipeline KATs against the FoxIO sample corpus, and property tests that reassemble arbitrarily segmented and reordered streams. Builds clean on stable and MSRV 1.85; clippy -D warnings and rustfmt clean. Test captures under testdata/pcap are vendored unmodified from the FoxIO ja4 repository with per-file SHA-256 provenance. --- .../.github/workflows/ci.yml | 46 + .../ja3-ja4-tls-fingerprinting/.gitignore | 9 + .../ja3-ja4-tls-fingerprinting/Cargo.lock | 1985 +++++++++++++++++ .../ja3-ja4-tls-fingerprinting/Cargo.toml | 66 + .../ja3-ja4-tls-fingerprinting/LICENSE | 21 + .../ja3-ja4-tls-fingerprinting/NOTICE.md | 61 + .../crates/tlsfp-core/Cargo.toml | 34 + .../crates/tlsfp-core/src/der.rs | 110 + .../crates/tlsfp-core/src/error.rs | 57 + .../crates/tlsfp-core/src/fingerprint.rs | 100 + .../crates/tlsfp-core/src/grease.rs | 52 + .../crates/tlsfp-core/src/hash.rs | 26 + .../crates/tlsfp-core/src/ja3.rs | 125 ++ .../crates/tlsfp-core/src/ja4.rs | 263 +++ .../crates/tlsfp-core/src/ja4h.rs | 195 ++ .../crates/tlsfp-core/src/ja4t.rs | 81 + .../crates/tlsfp-core/src/ja4x.rs | 175 ++ .../crates/tlsfp-core/src/lib.rs | 37 + .../crates/tlsfp-core/src/parse/cert.rs | 70 + .../crates/tlsfp-core/src/parse/hello.rs | 246 ++ .../crates/tlsfp-core/src/parse/mod.rs | 23 + .../crates/tlsfp-core/src/parse/reader.rs | 173 ++ .../crates/tlsfp-core/src/parse/record.rs | 179 ++ .../crates/tlsfp-core/src/pipeline/decode.rs | 347 +++ .../crates/tlsfp-core/src/pipeline/event.rs | 135 ++ .../crates/tlsfp-core/src/pipeline/flow.rs | 430 ++++ .../crates/tlsfp-core/src/pipeline/mod.rs | 433 ++++ .../crates/tlsfp-core/src/pipeline/source.rs | 362 +++ .../crates/tlsfp-core/src/pipeline/tls.rs | 401 ++++ .../crates/tlsfp-core/src/registry.rs | 91 + .../crates/tlsfp-core/tests/common/mod.rs | 161 ++ .../crates/tlsfp-core/tests/ja3.rs | 55 + .../crates/tlsfp-core/tests/ja4.rs | 88 + .../crates/tlsfp-core/tests/ja4_suite.rs | 64 + .../crates/tlsfp-core/tests/parse.rs | 105 + .../crates/tlsfp-core/tests/pipeline.rs | 196 ++ .../crates/tlsfp-core/tests/reassembly.rs | 114 + .../crates/tlsfp/Cargo.toml | 38 + .../crates/tlsfp/src/cli.rs | 126 ++ .../crates/tlsfp/src/main.rs | 15 + .../ja3-ja4-tls-fingerprinting/deny.toml | 30 + .../ja3-ja4-tls-fingerprinting/rustfmt.toml | 4 + .../testdata/pcap/CVE-2018-6794.pcap | Bin 0 -> 7413 bytes .../testdata/pcap/PROVENANCE.md | 49 + .../testdata/pcap/browsers-x509.pcapng | Bin 0 -> 62320 bytes ...chrome-cloudflare-quic-with-secrets.pcapng | Bin 0 -> 44236 bytes .../testdata/pcap/gre-erspan-vxlan.pcap | Bin 0 -> 1271 bytes .../testdata/pcap/http1-with-cookies.pcapng | Bin 0 -> 2900 bytes .../pcap/quic-with-several-tls-frames.pcapng | Bin 0 -> 1780 bytes .../testdata/pcap/tls-alpn-h2.pcap | Bin 0 -> 3827 bytes .../testdata/pcap/tls-handshake.pcapng | Bin 0 -> 198844 bytes .../testdata/pcap/tls-non-ascii-alpn.pcapng | Bin 0 -> 2560 bytes .../testdata/pcap/tls12.pcap | Bin 0 -> 611 bytes 53 files changed, 7378 insertions(+) create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.github/workflows/ci.yml create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.gitignore create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.lock create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.toml create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/LICENSE create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/NOTICE.md create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/Cargo.toml create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/error.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/fingerprint.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/grease.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/hash.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja3.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4h.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/lib.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/cert.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/hello.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/mod.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/reader.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/record.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/event.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/flow.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/source.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/tls.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/registry.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/common/mod.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja3.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4_suite.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/parse.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/pipeline.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/reassembly.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/Cargo.toml create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/cli.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/main.rs create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/deny.toml create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/rustfmt.toml create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/CVE-2018-6794.pcap create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/PROVENANCE.md create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/browsers-x509.pcapng create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/chrome-cloudflare-quic-with-secrets.pcapng create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/gre-erspan-vxlan.pcap create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/http1-with-cookies.pcapng create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/quic-with-several-tls-frames.pcapng create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-alpn-h2.pcap create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-handshake.pcapng create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-non-ascii-alpn.pcapng create mode 100644 PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls12.pcap diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.github/workflows/ci.yml b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.github/workflows/ci.yml new file mode 100644 index 00000000..9c7ef790 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +# ©AngelaMos | 2026 +# ci.yml + +name: ci + +on: + push: + paths: + - "PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/**" + pull_request: + paths: + - "PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/**" + +defaults: + run: + working-directory: PROJECTS/intermediate/ja3-ja4-tls-fingerprinting + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable, "1.85"] + steps: + - uses: actions/checkout@v4 + - name: Install libpcap + run: sudo apt-get update && sudo apt-get install -y libpcap-dev + - name: Install Rust ${{ matrix.rust }} + run: rustup toolchain install ${{ matrix.rust }} --component clippy rustfmt --profile minimal + - name: Build + run: cargo +${{ matrix.rust }} build --workspace --all-targets + - name: Test + run: cargo +${{ matrix.rust }} test --workspace + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install libpcap + run: sudo apt-get update && sudo apt-get install -y libpcap-dev + - name: Install Rust + run: rustup toolchain install stable --component clippy rustfmt --profile minimal + - name: Format + run: cargo fmt --all --check + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.gitignore b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.gitignore new file mode 100644 index 00000000..50b47180 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/.gitignore @@ -0,0 +1,9 @@ +# ©AngelaMos | 2026 +# .gitignore + +docs/ +target/ +*.db +*.sqlite +fuzz/corpus/ +fuzz/artifacts/ diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.lock b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.lock new file mode 100644 index 00000000..1759a05a --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.lock @@ -0,0 +1,1985 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "circular" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "etherparse" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a48e7bdc36cdf86876a6890c0840957029374ea07f6697c96fa15898730375" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "pcap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2eecc2ddc671ec563b5b39f846556aade68a65d1afb14d8fe6b30b0457d75" +dependencies = [ + "bitflags 1.3.2", + "errno 0.2.8", + "libc", + "libloading", + "pkg-config", + "regex", + "windows-sys 0.36.1", +] + +[[package]] +name = "pcap-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8d57cc6bdf76d7abd6d3cc1113278047dab29c2ff6d97190e8d1c29d4efdac" +dependencies = [ + "circular", + "nom", + "rusticata-macros", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.0", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno 0.3.14", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno 0.3.14", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tlsfp" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "clap", + "etherparse", + "flume", + "pcap", + "pcap-parser", + "rusqlite", + "serde", + "serde_json", + "tlsfp-core", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tlsfp-core" +version = "0.1.0" +dependencies = [ + "aes", + "aes-gcm", + "ctr", + "etherparse", + "hex", + "hkdf", + "md-5", + "pcap-parser", + "proptest", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.toml b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.toml new file mode 100644 index 00000000..5cf2cdc8 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/Cargo.toml @@ -0,0 +1,66 @@ +# ©AngelaMos | 2026 +# Cargo.toml + +[workspace] +resolver = "3" +members = ["crates/tlsfp-core", "crates/tlsfp"] + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +license = "MIT" +authors = ["Carter Perez"] +repository = "https://github.com/CarterPerez-dev/Cybersecurity-Projects" + +[workspace.dependencies] +tlsfp-core = { path = "crates/tlsfp-core" } + +winnow = "0.7" +md-5 = "0.10" +sha2 = "0.10" +hex = "0.4" +smallvec = "1" +thiserror = "2" +anyhow = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +etherparse = "0.18" +pcap-parser = "0.16" +pcap = "2" + +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal"] } +flume = "0.11" + +rusqlite = { version = "0.32", features = ["bundled"] } + +axum = "0.8" +tower-http = { version = "0.6", features = ["fs", "trace", "cors"] } +tower = "0.5" + +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +clap = { version = "4", features = ["derive"] } + +hkdf = "0.12" +aes = "0.8" +aes-gcm = "0.10" +ctr = "0.9" + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" +doc_markdown = "allow" + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/LICENSE b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/LICENSE new file mode 100644 index 00000000..a615e738 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Carter Perez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/NOTICE.md b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/NOTICE.md new file mode 100644 index 00000000..8947fe21 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/NOTICE.md @@ -0,0 +1,61 @@ +# Third-party fingerprint algorithm licensing + +This project implements several published TLS fingerprinting algorithms. They do +not all carry the same license, and the split matters. This notice records what +applies to what. + +## Our own code + +Everything under `crates/` is original work licensed under the MIT License (see +`LICENSE`). No source code from any reference implementation was copied. The +algorithms were implemented from their published specifications. + +## JA3 and JA3S + +JA3 and JA3S were created by John Althouse, Jeff Atkinson, and Josh Atkins at +Salesforce and released under the BSD 3-Clause license at +`https://github.com/salesforce/ja3`. The algorithm is free to implement and use. +That repository was archived in May 2025 and is no longer maintained. + +## JA4 (TLS client fingerprint) + +JA4, the TLS client fingerprint, is licensed by FoxIO under the BSD 3-Clause +license, separately from the rest of the JA4+ suite. FoxIO has stated it holds +no patents on JA4 TLS client fingerprinting. The canonical license text is at +`https://github.com/FoxIO-LLC/ja4/blob/main/LICENSE-JA4`. JA4 may be implemented +and used without restriction. + +## JA4S, JA4H, JA4X, JA4T (the rest of the JA4+ suite) + +The remaining JA4+ fingerprints implemented here are licensed under the FoxIO +License 1.1, and the methods are patent pending. The canonical license text is +at `https://github.com/FoxIO-LLC/ja4/blob/main/LICENSE`. + +The FoxIO License 1.1 grants the right to use and modify these methods for non +commercial purposes, which it defines to include personal use, academic research +and development, and internal evaluation. It excludes any use for which a fee is +charged and excludes providing the methods as a hosted or managed service to +others. + +This project is a free, open source, educational tool. It is not sold, not +monetized, and not offered as a service. It therefore falls within the non +commercial grant of the FoxIO License 1.1. Anyone who forks this project and +intends to monetize it, or to offer it as a hosted service, must obtain an OEM +license from FoxIO for the JA4+ methods first. + +This is the same boundary that led the Suricata project to ship only JA4 and not +the rest of the JA4+ suite: their GPL licensing is incompatible with the FoxIO +License 1.1. This project is MIT licensed and non commercial, so both the GPL +incompatibility and the monetization restriction are avoided. + +## Seeded fingerprint data + +The intelligence database is seeded from public sources with their own terms: + +- abuse.ch SSLBL JA3 feed is released under CC0 and a snapshot may be + redistributed. Its known limitation, that the fingerprints have not been + tested against benign traffic and may produce false positives, is surfaced in + the tool output. +- The salesforce/ja3 application lists are MIT licensed. +- The ja4db.com database has no stated redistribution license, so it is fetched + at install time rather than bundled, and entries are validated on import. diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/Cargo.toml b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/Cargo.toml new file mode 100644 index 00000000..63237bba --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/Cargo.toml @@ -0,0 +1,34 @@ +# ©AngelaMos | 2026 +# Cargo.toml + +[package] +name = "tlsfp-core" +description = "TLS handshake parsing and JA3/JA4 fingerprint computation" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +md-5.workspace = true +sha2.workspace = true +hex.workspace = true +smallvec.workspace = true +thiserror.workspace = true +serde.workspace = true +etherparse.workspace = true +pcap-parser.workspace = true +hkdf.workspace = true +aes.workspace = true +aes-gcm.workspace = true +ctr.workspace = true + +[dev-dependencies] +hex.workspace = true +proptest = "1" +serde_json.workspace = true diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs new file mode 100644 index 00000000..7041b881 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs @@ -0,0 +1,110 @@ +// ©AngelaMos | 2026 +// der.rs + +use crate::error::{ParseError, Result}; + +/// DER tag bytes that the certificate walk needs to recognize. +pub mod tag { + pub const OBJECT_IDENTIFIER: u8 = 0x06; + pub const SEQUENCE: u8 = 0x30; + pub const SET: u8 = 0x31; + pub const CONTEXT_3: u8 = 0xa3; +} + +/// A minimal reader for the subset of DER that X.509 certificates use. +/// +/// It decodes one tag length value triple at a time and hands back the content +/// as a borrowed slice. It deliberately does not understand the meaning of any +/// structure; the JA4X walk drives it, deciding which fields to descend into and +/// which to skip. Keeping the reader this small keeps it auditable, which +/// matters because certificate parsers are a classic source of memory safety +/// bugs and this one has no `unsafe` and cannot read out of bounds. +pub struct Der<'a> { + buf: &'a [u8], + pos: usize, +} + +impl<'a> Der<'a> { + #[must_use] + pub const fn new(buf: &'a [u8]) -> Self { + Self { buf, pos: 0 } + } + + #[must_use] + pub const fn is_empty(&self) -> bool { + self.pos >= self.buf.len() + } + + fn byte(&mut self) -> Result { + if self.pos >= self.buf.len() { + return Err(ParseError::Truncated { needed: 1, have: 0 }); + } + let b = self.buf[self.pos]; + self.pos += 1; + Ok(b) + } + + /// Reads one tag length value triple and returns the tag and the content + /// slice, advancing past the whole triple. + pub fn read_tlv(&mut self) -> Result<(u8, &'a [u8])> { + let tag = self.byte()?; + let len = self.read_length()?; + if self.buf.len() - self.pos < len { + return Err(ParseError::LengthOverrun { + field: "der", + declared: len, + available: self.buf.len() - self.pos, + }); + } + let content = &self.buf[self.pos..self.pos + len]; + self.pos += len; + Ok((tag, content)) + } + + fn read_length(&mut self) -> Result { + let first = self.byte()?; + if first & 0x80 == 0 { + return Ok(first as usize); + } + let count = (first & 0x7f) as usize; + if count == 0 || count > core::mem::size_of::() { + return Err(ParseError::Misaligned(count)); + } + let mut len = 0usize; + for _ in 0..count { + len = (len << 8) | self.byte()? as usize; + } + Ok(len) + } +} + +#[cfg(test)] +mod tests { + use super::{Der, tag}; + + #[test] + fn reads_short_form_sequence() { + let data = [0x30, 0x03, 0x06, 0x01, 0x55]; + let mut der = Der::new(&data); + let (t, content) = der.read_tlv().unwrap(); + assert_eq!(t, tag::SEQUENCE); + assert_eq!(content, &[0x06, 0x01, 0x55]); + assert!(der.is_empty()); + } + + #[test] + fn reads_long_form_length() { + let mut data = vec![0x04, 0x82, 0x01, 0x00]; + data.extend(std::iter::repeat_n(0xaa, 256)); + let mut der = Der::new(&data); + let (_t, content) = der.read_tlv().unwrap(); + assert_eq!(content.len(), 256); + } + + #[test] + fn rejects_length_overrun() { + let data = [0x06, 0x05, 0x55, 0x04]; + let mut der = Der::new(&data); + assert!(der.read_tlv().is_err()); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/error.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/error.rs new file mode 100644 index 00000000..df68f4fd --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/error.rs @@ -0,0 +1,57 @@ +// ©AngelaMos | 2026 +// error.rs + +use thiserror::Error; + +/// Errors produced while parsing TLS records, handshake messages, or QUIC +/// initial packets. +/// +/// Every variant is stack only. No variant carries a heap allocation, so the +/// malformed packet path never touches the allocator. This matters because a +/// fingerprinting engine spends most of its time rejecting traffic that is not +/// a clean handshake, and allocating on each rejection adds jitter at line rate. +#[derive(Debug, Error, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ParseError { + #[error("buffer too short: needed {needed} bytes, had {have}")] + Truncated { needed: usize, have: usize }, + + #[error("not a TLS handshake record (content type {0:#04x})")] + NotHandshake(u8), + + #[error("unexpected handshake message type {0:#04x}")] + UnexpectedHandshake(u8), + + #[error("length field {field} declares {declared} bytes but {available} remain")] + LengthOverrun { + field: &'static str, + declared: usize, + available: usize, + }, + + #[error("vector length {0} is not a whole number of elements")] + Misaligned(usize), + + #[error("trailing {0} bytes after a complete message")] + Trailing(usize), + + #[error("handshake message spans more bytes than the reassembly cap allows")] + OversizedHandshake, + + #[error("malformed extension {ext_type:#06x}")] + BadExtension { ext_type: u16 }, + + #[error("not a QUIC long header initial packet")] + NotQuicInitial, + + #[error("unsupported QUIC version {0:#010x}")] + UnsupportedQuicVersion(u32), + + #[error("QUIC header protection or AEAD removal failed")] + QuicCryptoFailure, + + #[error("no CRYPTO frame carrying a ClientHello was present")] + NoCryptoClientHello, +} + +pub type Result = core::result::Result; diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/fingerprint.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/fingerprint.rs new file mode 100644 index 00000000..9ea4bbfe --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/fingerprint.rs @@ -0,0 +1,100 @@ +// ©AngelaMos | 2026 +// fingerprint.rs + +use core::fmt; + +use serde::Serialize; + +/// A JA3 or JA3S fingerprint: the MD5 digest of the pre hash string. +/// +/// JA3 is carried as the raw sixteen byte digest rather than a hex string so +/// that equality, hashing, and database keys operate on the compact binary form +/// and the hex rendering happens only at display boundaries. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize)] +#[serde(into = "String")] +pub struct Ja3([u8; 16]); + +impl Ja3 { + #[must_use] + pub const fn from_digest(digest: [u8; 16]) -> Self { + Self(digest) + } + + #[must_use] + pub const fn bytes(&self) -> &[u8; 16] { + &self.0 + } +} + +impl fmt::Display for Ja3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.0 { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +impl fmt::Debug for Ja3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ja3({self})") + } +} + +impl From for String { + fn from(value: Ja3) -> Self { + value.to_string() + } +} + +/// A fingerprint from the JA4 family, carried in both its hashed canonical form +/// and its raw pre hash form. +/// +/// The raw form is the unhashed list of cipher and extension values. It is kept +/// alongside the hash because it is the form an analyst reads when explaining +/// why two clients differ, and because clustering on the raw lists is more +/// informative than clustering on opaque digests. +#[derive(Clone, PartialEq, Eq, Hash, Serialize)] +pub struct Ja4Family { + pub hash: String, + pub raw: String, +} + +impl Ja4Family { + #[must_use] + pub fn new(hash: String, raw: String) -> Self { + Self { hash, raw } + } +} + +impl fmt::Display for Ja4Family { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.hash) + } +} + +impl fmt::Debug for Ja4Family { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ja4Family(hash={}, raw={})", self.hash, self.raw) + } +} + +#[cfg(test)] +mod tests { + use super::{Ja3, Ja4Family}; + + #[test] + fn ja3_display_is_lowercase_hex() { + let fp = Ja3::from_digest([ + 0xad, 0xa7, 0x02, 0x06, 0xe4, 0x06, 0x42, 0xa3, 0xe4, 0x46, 0x1f, 0x35, 0x50, 0x32, + 0x41, 0xd5, + ]); + assert_eq!(fp.to_string(), "ada70206e40642a3e4461f35503241d5"); + } + + #[test] + fn ja4_family_displays_hash() { + let fp = Ja4Family::new("t13d1516h2_8daaf6152771_e5627efa2ab1".into(), "raw".into()); + assert_eq!(fp.to_string(), "t13d1516h2_8daaf6152771_e5627efa2ab1"); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/grease.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/grease.rs new file mode 100644 index 00000000..8d582220 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/grease.rs @@ -0,0 +1,52 @@ +// ©AngelaMos | 2026 +// grease.rs + +/// The sixteen GREASE values reserved by RFC 8701. +/// +/// GREASE (Generate Random Extensions And Sustain Extensibility) values are +/// inserted by clients into cipher lists, extension lists, supported groups, +/// supported versions, and signature algorithms. They are deliberate noise +/// whose only purpose is to keep middleboxes tolerant of unknown values. Both +/// JA3 and JA4 strip them before hashing so that the same client produces a +/// stable fingerprint regardless of which GREASE values it happened to pick. +/// +/// Every value has the form `0xZaZa` where the two bytes are equal and the low +/// nibble of each is `a`. +pub const GREASE_VALUES: [u16; 16] = [ + 0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, + 0xcaca, 0xdada, 0xeaea, 0xfafa, +]; + +/// Returns true when `value` is one of the sixteen RFC 8701 GREASE values. +/// +/// The check exploits the structural regularity of the GREASE set rather than +/// scanning the table: both bytes must be equal and the low nibble of each must +/// be `0xa`. This is a single pair of comparisons rather than a sixteen way +/// branch, which keeps it cheap on the per packet path. +#[inline] +#[must_use] +pub const fn is_grease(value: u16) -> bool { + let high = (value >> 8) as u8; + let low = (value & 0x00ff) as u8; + high == low && (low & 0x0f) == 0x0a +} + +#[cfg(test)] +mod tests { + use super::{GREASE_VALUES, is_grease}; + + #[test] + fn table_matches_structural_check() { + for v in 0..=u16::MAX { + let in_table = GREASE_VALUES.contains(&v); + assert_eq!(in_table, is_grease(v), "mismatch for {v:#06x}"); + } + } + + #[test] + fn known_non_grease_values() { + for v in [0x0000_u16, 0x0010, 0x1301, 0x00ff, 0x5600, 0xc02f] { + assert!(!is_grease(v), "{v:#06x} should not be grease"); + } + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/hash.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/hash.rs new file mode 100644 index 00000000..849b87ae --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/hash.rs @@ -0,0 +1,26 @@ +// ©AngelaMos | 2026 +// hash.rs + +use sha2::{Digest, Sha256}; + +/// Returns the first twelve hex characters of the SHA256 digest of `bytes`. +/// +/// The whole JA4 family truncates SHA256 to twelve hex characters, which is six +/// bytes of digest. Twelve characters is enough to make accidental collisions +/// vanishingly unlikely across the fingerprint space while keeping the +/// fingerprint short enough to read and to paste into a search box. +#[must_use] +pub fn sha256_hex12(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + hex::encode(&digest[..6]) +} + +#[cfg(test)] +mod tests { + use super::sha256_hex12; + + #[test] + fn known_digest_prefix() { + assert_eq!(sha256_hex12(b""), "e3b0c44298fc"); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja3.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja3.rs new file mode 100644 index 00000000..0f530075 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja3.rs @@ -0,0 +1,125 @@ +// ©AngelaMos | 2026 +// ja3.rs + +use std::fmt::Write as _; + +use md5::{Digest, Md5}; +use smallvec::SmallVec; + +use crate::fingerprint::Ja3; +use crate::grease::is_grease; +use crate::parse::{ClientHello, ServerHello}; + +/// Computes the JA3 string and digest for a ClientHello. +/// +/// JA3 concatenates five fields, in order: the legacy version, the cipher +/// suites, the extension types, the supported groups, and the elliptic curve +/// point formats. Within a field the values are decimal and joined with +/// hyphens; the fields themselves are joined with commas. GREASE values are +/// removed from every list field so that the deliberate noise a client inserts +/// does not change its fingerprint. The MD5 of that string is the JA3 hash. +/// +/// JA3 is kept here despite being effectively dead for modern browser traffic, +/// because malware fingerprints in public feeds are still expressed as JA3 and +/// because watching JA3 collapse next to JA4 is the clearest way to understand +/// why JA4 exists. +#[must_use] +pub fn ja3_string(ch: &ClientHello) -> String { + let ext_types: SmallVec<[u16; 16]> = ch.extensions.iter().map(|e| e.ext_type).collect(); + let groups = ch.supported_groups(); + let formats = ch.ec_point_formats(); + + let mut s = String::new(); + let _ = write!(s, "{}", ch.legacy_version); + s.push(','); + append_u16_hyphenated(&mut s, &ch.cipher_suites); + s.push(','); + append_u16_hyphenated(&mut s, &ext_types); + s.push(','); + append_u16_hyphenated(&mut s, &groups); + s.push(','); + append_u8_hyphenated(&mut s, &formats); + s +} + +/// Computes the JA3 digest for a ClientHello. +#[must_use] +pub fn ja3(ch: &ClientHello) -> Ja3 { + digest(&ja3_string(ch)) +} + +/// Computes the JA3S string for a ServerHello. +/// +/// JA3S mirrors JA3 on the server side with three fields: the version, the +/// single chosen cipher suite, and the extension types. A server and the exact +/// ClientHello it answered together identify a deployment more tightly than +/// either side alone. +#[must_use] +pub fn ja3s_string(sh: &ServerHello) -> String { + let ext_types: SmallVec<[u16; 16]> = sh.extensions.iter().map(|e| e.ext_type).collect(); + let mut s = String::new(); + let _ = write!(s, "{},{},", sh.legacy_version, sh.cipher_suite); + append_u16_hyphenated(&mut s, &ext_types); + s +} + +/// Computes the JA3S digest for a ServerHello. +#[must_use] +pub fn ja3s(sh: &ServerHello) -> Ja3 { + digest(&ja3s_string(sh)) +} + +fn digest(pre_hash: &str) -> Ja3 { + let out = Md5::digest(pre_hash.as_bytes()); + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&out); + Ja3::from_digest(bytes) +} + +fn append_u16_hyphenated(out: &mut String, values: &[u16]) { + let mut first = true; + for &v in values { + if is_grease(v) { + continue; + } + if !first { + out.push('-'); + } + first = false; + let _ = write!(out, "{v}"); + } +} + +fn append_u8_hyphenated(out: &mut String, values: &[u8]) { + let mut first = true; + for &v in values { + if !first { + out.push('-'); + } + first = false; + let _ = write!(out, "{v}"); + } +} + +#[cfg(test)] +mod tests { + use super::digest; + + #[test] + fn salesforce_client_vector_one() { + let pre = "769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0"; + assert_eq!(digest(pre).to_string(), "ada70206e40642a3e4461f35503241d5"); + } + + #[test] + fn salesforce_client_vector_two_empty_fields() { + let pre = "769,4-5-10-9-100-98-3-6-19-18-99,,,"; + assert_eq!(digest(pre).to_string(), "de350869b8c85de67a350c8d186f11e6"); + } + + #[test] + fn server_vector_round_trips_through_md5() { + let pre = "769,47,65281-0-11-35-5-16"; + assert_eq!(digest(pre).to_string(), "836ce314215654b5b1f85f97c73e506f"); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4.rs new file mode 100644 index 00000000..f4629b4d --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4.rs @@ -0,0 +1,263 @@ +// ©AngelaMos | 2026 +// ja4.rs + +use std::fmt::Write as _; + +use smallvec::SmallVec; + +use crate::fingerprint::Ja4Family; +use crate::grease::is_grease; +use crate::hash::sha256_hex12; +use crate::parse::{ClientHello, ServerHello}; +use crate::registry::{extension, ja4_version_code}; + +/// The transport that carried the handshake. +/// +/// JA4 records the transport in its first character because the same TLS stack +/// produces a recognizably different ClientHello over QUIC than over TCP, and an +/// analyst wants to see that difference at a glance rather than infer it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Transport { + Tcp, + Quic, + Dtls, +} + +impl Transport { + const fn marker(self) -> char { + match self { + Transport::Tcp => 't', + Transport::Quic => 'q', + Transport::Dtls => 'd', + } + } +} + +const EMPTY_HASH: &str = "000000000000"; + +/// Computes the JA4 fingerprint of a ClientHello in both hashed and raw forms. +/// +/// The fingerprint has three underscore separated sections. The first is ten +/// human readable characters: transport, version, whether SNI was present, +/// cipher count, extension count, and two characters derived from the first +/// ALPN value. The second is a truncated SHA256 of the cipher list sorted by +/// value. The third is a truncated SHA256 of the extension list sorted by value, +/// with SNI and ALPN removed and the signature algorithms appended in their +/// original order. +/// +/// Sorting the cipher and extension lists before hashing is the whole reason +/// JA4 survived what killed JA3. When Chrome began shuffling its extension order +/// on every connection, JA3, which hashes extensions in wire order, produced a +/// fresh hash for every Chrome request. JA4 sorts first, so order shuffling +/// leaves the fingerprint unchanged. +#[must_use] +pub fn ja4(ch: &ClientHello, transport: Transport) -> Ja4Family { + let prefix = ja4_prefix(ch, transport); + + let (cipher_csv, _) = sorted_hex_csv(&ch.cipher_suites, &[]); + let cipher_hash = truncated_sha256(&cipher_csv); + + let ext_raw = ja4_extension_raw(ch); + let ext_hash = truncated_sha256(&ext_raw); + + let hash = format!("{prefix}_{cipher_hash}_{ext_hash}"); + let raw = format!("{prefix}_{cipher_csv}_{ext_raw}"); + Ja4Family::new(hash, raw) +} + +/// Computes the JA4S fingerprint of a ServerHello. +/// +/// JA4S mirrors JA4 on the server side, with three differences that follow from +/// what a server controls. The server picks exactly one cipher suite, so the +/// cipher section is that single value in hex rather than a hash of a list. The +/// extensions are hashed in the order the server sent them, not sorted, because +/// a server does not shuffle its own extensions. And there is no SNI field, +/// because the server is not the party naming a host. +#[must_use] +pub fn ja4s(sh: &ServerHello, transport: Transport) -> Ja4Family { + let version = ja4_version_code(sh.selected_version()); + let alpn = alpn_chars(sh.alpn_protocol()); + let ext_count = sh.extensions.len().min(99); + + let mut prefix = String::with_capacity(7); + prefix.push(transport.marker()); + prefix.push_str(version); + let _ = write!(prefix, "{ext_count:02}"); + prefix.push_str(&alpn); + + let cipher_hex = format!("{:04x}", sh.cipher_suite); + + let ext_csv = wire_order_hex_csv( + &sh.extensions + .iter() + .map(|e| e.ext_type) + .collect::>(), + ); + let ext_hash = truncated_sha256(&ext_csv); + + let hash = format!("{prefix}_{cipher_hex}_{ext_hash}"); + let raw = format!("{prefix}_{cipher_hex}_{ext_csv}"); + Ja4Family::new(hash, raw) +} + +fn wire_order_hex_csv(values: &[u16]) -> String { + let hexed: SmallVec<[String; 16]> = values.iter().map(|v| format!("{v:04x}")).collect(); + hexed.join(",") +} + +fn ja4_prefix(ch: &ClientHello, transport: Transport) -> String { + let version = select_version(ch); + let sni = if ch.has_extension(extension::SERVER_NAME) { + 'd' + } else { + 'i' + }; + let cipher_count = capped_count(ch.cipher_suites.iter().copied()); + let ext_count = capped_count(ch.extensions.iter().map(|e| e.ext_type)); + let alpn = ja4_alpn(ch); + + let mut prefix = String::with_capacity(10); + prefix.push(transport.marker()); + prefix.push_str(ja4_version_code(version)); + prefix.push(sni); + let _ = write!(prefix, "{cipher_count:02}{ext_count:02}"); + prefix.push_str(&alpn); + prefix +} + +/// Selects the JA4 version word: the highest non GREASE value from the supported +/// versions extension when present, otherwise the legacy record version. +fn select_version(ch: &ClientHello) -> u16 { + ch.supported_versions() + .iter() + .copied() + .filter(|v| !is_grease(*v)) + .max() + .unwrap_or(ch.legacy_version) +} + +fn capped_count(values: impl Iterator) -> usize { + values.filter(|v| !is_grease(*v)).count().min(99) +} + +/// Derives the two ALPN characters. +/// +/// The implementation follows the published JA4 specification rather than the +/// FoxIO Python reference. The two diverge for ALPN values whose first or last +/// byte is not an ASCII alphanumeric: the specification says to print the first +/// and last characters of the hex encoding of the value, while the Python +/// reference emits a fixed fallback. The specification is the more informative +/// and more portable choice, so it is what this code does. +fn ja4_alpn(ch: &ClientHello) -> String { + alpn_chars(ch.alpn_protocols().first().copied()) +} + +fn alpn_chars(first: Option<&[u8]>) -> String { + let Some(first) = first else { + return "00".to_string(); + }; + if first.is_empty() { + return "00".to_string(); + } + + let first_byte = first[0]; + let last_byte = first[first.len() - 1]; + + if is_ascii_alphanumeric(first_byte) && is_ascii_alphanumeric(last_byte) { + let mut out = String::with_capacity(2); + out.push(first_byte as char); + out.push(last_byte as char); + out + } else { + let encoded = hex::encode(first); + let mut chars = encoded.chars(); + let first_char = chars.next().unwrap_or('0'); + let last_char = encoded.chars().last().unwrap_or('0'); + let mut out = String::with_capacity(2); + out.push(first_char); + out.push(last_char); + out + } +} + +const fn is_ascii_alphanumeric(byte: u8) -> bool { + byte.is_ascii_digit() || byte.is_ascii_uppercase() || byte.is_ascii_lowercase() +} + +/// Builds the raw, pre hash extension string for section three. +/// +/// The extension types are sorted by hex value after removing GREASE, SNI, and +/// ALPN. If a signature algorithms extension is present, its values are appended +/// after an underscore, in their original order. If it is absent, the string +/// ends without a trailing underscore, which is the behavior the specification +/// requires and which changes the resulting hash. +fn ja4_extension_raw(ch: &ClientHello) -> String { + let excluded = [extension::SERVER_NAME, extension::ALPN]; + let ext_types: SmallVec<[u16; 16]> = ch.extensions.iter().map(|e| e.ext_type).collect(); + let (ext_csv, _) = sorted_hex_csv(&ext_types, &excluded); + + let sig_algs = ch.signature_algorithms(); + if ch.has_extension(extension::SIGNATURE_ALGORITHMS) { + let sig_csv = unsorted_hex_csv(&sig_algs); + format!("{ext_csv}_{sig_csv}") + } else { + ext_csv + } +} + +fn sorted_hex_csv(values: &[u16], excluded: &[u16]) -> (String, usize) { + let mut hexed: SmallVec<[String; 32]> = values + .iter() + .copied() + .filter(|v| !is_grease(*v) && !excluded.contains(v)) + .map(|v| format!("{v:04x}")) + .collect(); + hexed.sort_unstable(); + (hexed.join(","), hexed.len()) +} + +fn unsorted_hex_csv(values: &[u16]) -> String { + let hexed: SmallVec<[String; 16]> = values + .iter() + .copied() + .filter(|v| !is_grease(*v)) + .map(|v| format!("{v:04x}")) + .collect(); + hexed.join(",") +} + +fn truncated_sha256(input: &str) -> String { + if input.is_empty() { + return EMPTY_HASH.to_string(); + } + sha256_hex12(input.as_bytes()) +} + +#[cfg(test)] +mod tests { + use super::{EMPTY_HASH, truncated_sha256}; + + #[test] + fn empty_input_is_the_zero_hash() { + assert_eq!(truncated_sha256(""), EMPTY_HASH); + } + + #[test] + fn truncation_is_twelve_hex_chars() { + let h = truncated_sha256("1301,1302,1303"); + assert_eq!(h.len(), 12); + assert!(h.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn foxio_cipher_section_vector() { + let ciphers = "002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9"; + assert_eq!(truncated_sha256(ciphers), "8daaf6152771"); + } + + #[test] + fn foxio_extension_section_vector() { + let exts = "0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601"; + assert_eq!(truncated_sha256(exts), "e5627efa2ab1"); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4h.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4h.rs new file mode 100644 index 00000000..be52079b --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4h.rs @@ -0,0 +1,195 @@ +// ©AngelaMos | 2026 +// ja4h.rs + +use crate::fingerprint::Ja4Family; +use crate::hash::sha256_hex12; + +/// A parsed HTTP request, holding what JA4H reads. +/// +/// JA4H fingerprints an HTTP client from one request: its method, version, +/// whether it carries cookies and a referer, the names of its other headers in +/// the order they were sent, its accept language, and its cookie names and +/// values. A request that omits an accept language and sends no cookies is far +/// more likely to be a script than a person, and JA4H makes that visible in the +/// first few characters. +/// +/// This applies to cleartext HTTP only. Over HTTPS the request is encrypted and +/// invisible to a passive observer, and HTTP/2 carries its headers in HPACK, +/// which a passive tool cannot decode without following the whole connection. +#[derive(Debug, Clone)] +pub struct HttpRequest { + pub method: String, + pub version: String, + pub headers: Vec<(String, String)>, +} + +/// Parses an HTTP/1.x request from the start of a reassembled byte stream. +/// +/// Returns `None` when the bytes are not a well formed request line followed by +/// headers. Header values keep their original bytes; header names keep their +/// original case because JA4H hashes them as sent. +#[must_use] +pub fn parse_http_request(bytes: &[u8]) -> Option { + let text = std::str::from_utf8(bytes).ok()?; + let mut lines = text.split("\r\n"); + + let request_line = lines.next()?; + let mut parts = request_line.split(' '); + let method = parts.next()?.to_string(); + let _target = parts.next()?; + let http_token = parts.next()?; + if !http_token.starts_with("HTTP/") { + return None; + } + let version = http_token.trim_start_matches("HTTP/").replace('.', ""); + + let mut headers = Vec::new(); + for line in lines { + if line.is_empty() { + break; + } + let (name, value) = line.split_once(':')?; + headers.push((name.to_string(), value.trim_start().to_string())); + } + + Some(HttpRequest { + method, + version, + headers, + }) +} + +/// Computes the JA4H fingerprint for a parsed HTTP request. +#[must_use] +pub fn ja4h(req: &HttpRequest) -> Ja4Family { + let method = method_code(&req.method); + let version = version_code(&req.version); + let cookie_flag = if has_header(req, "cookie") { 'c' } else { 'n' }; + let referer_flag = if has_named_header(req, "referer") { + 'r' + } else { + 'n' + }; + + let counted: Vec<&str> = req + .headers + .iter() + .map(|(name, _)| name.as_str()) + .filter(|name| is_counted_header(name)) + .collect(); + let header_len = counted.len().min(99); + let lang = accept_language(req); + + let prefix = format!("{method}{version}{cookie_flag}{referer_flag}{header_len:02}{lang}"); + let header_hash = sha12(&counted.join(",")); + + let cookies = cookie_pairs(req); + let (cookie_hash, value_hash, raw_cookie_tail) = if let Some(mut pairs) = cookies { + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + let names: Vec<&str> = pairs.iter().map(|p| p.0.as_str()).collect(); + let entries: Vec<&str> = pairs.iter().map(|p| p.1.as_str()).collect(); + let tail = format!("{}_{}", names.join(","), entries.join(",")); + (sha12(&names.join(",")), sha12(&entries.join(",")), tail) + } else { + (ZERO_HASH.to_string(), ZERO_HASH.to_string(), String::new()) + }; + + let hash = format!("{prefix}_{header_hash}_{cookie_hash}_{value_hash}"); + let raw = format!("{prefix}_{}_{raw_cookie_tail}", counted.join(",")); + Ja4Family::new(hash, raw) +} + +const ZERO_HASH: &str = "000000000000"; + +fn method_code(method: &str) -> String { + method.to_lowercase().chars().take(2).collect() +} + +fn version_code(version: &str) -> String { + match version { + "2" | "20" => "20".to_string(), + "3" | "30" => "30".to_string(), + "10" => "10".to_string(), + _ => "11".to_string(), + } +} + +fn has_header(req: &HttpRequest, prefix_lower: &str) -> bool { + req.headers + .iter() + .any(|(name, _)| name.to_lowercase().starts_with(prefix_lower)) +} + +fn has_named_header(req: &HttpRequest, name_lower: &str) -> bool { + req.headers + .iter() + .any(|(name, _)| name.to_lowercase() == name_lower) +} + +fn is_counted_header(name: &str) -> bool { + let lower = name.to_lowercase(); + !name.starts_with(':') && !lower.starts_with("cookie") && lower != "referer" && !name.is_empty() +} + +fn accept_language(req: &HttpRequest) -> String { + let Some((_, value)) = req + .headers + .iter() + .find(|(name, _)| name.to_lowercase() == "accept-language") + else { + return "0000".to_string(); + }; + let normalized = value.replace('-', "").replace(';', ",").to_lowercase(); + let first = normalized.split(',').next().unwrap_or(""); + let mut code: String = first.chars().take(4).collect(); + while code.len() < 4 { + code.push('0'); + } + code +} + +fn cookie_pairs(req: &HttpRequest) -> Option> { + let (_, value) = req + .headers + .iter() + .find(|(name, _)| name.to_lowercase() == "cookie")?; + let pairs = value + .split(';') + .map(str::trim) + .filter(|p| !p.is_empty()) + .map(|p| { + let name = p.split('=').next().unwrap_or(p).trim().to_string(); + (name, p.to_string()) + }) + .collect(); + Some(pairs) +} + +fn sha12(joined: &str) -> String { + sha256_hex12(joined.as_bytes()) +} + +#[cfg(test)] +mod tests { + use super::{parse_http_request, version_code}; + + #[test] + fn parses_request_line_and_headers() { + let raw = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"; + let req = parse_http_request(raw).unwrap(); + assert_eq!(req.method, "GET"); + assert_eq!(req.version, "11"); + assert_eq!(req.headers.len(), 2); + assert_eq!( + req.headers[0], + ("Host".to_string(), "example.com".to_string()) + ); + } + + #[test] + fn version_codes() { + assert_eq!(version_code("11"), "11"); + assert_eq!(version_code("10"), "10"); + assert_eq!(version_code("2"), "20"); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs new file mode 100644 index 00000000..7cdc7609 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs @@ -0,0 +1,81 @@ +// ©AngelaMos | 2026 +// ja4t.rs + +use std::fmt::Write as _; + +use smallvec::SmallVec; + +/// The TCP layer inputs to a JA4T fingerprint, read from a SYN or SYN ACK. +/// +/// JA4T fingerprints the TCP stack rather than the TLS stack. It is computed +/// from fields that an operating system sets the same way on every connection +/// but that differ between operating systems: the advertised window size, the +/// kinds and order of TCP options, the maximum segment size, and the window +/// scale factor. Pairing it with JA4 exposes a class of evasion that TLS only +/// fingerprinting misses, where a tool wears a browser's TLS clothing while its +/// host operating system speaks with a different TCP accent. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TcpFingerprintInput { + pub window_size: u16, + pub option_kinds: SmallVec<[u8; 8]>, + pub mss: u16, + pub window_scale: u8, +} + +/// Computes the JA4T or JA4TS string. +/// +/// The format is the window size, then the option kinds joined with hyphens, +/// then the maximum segment size, then the window scale, with the four parts +/// separated by underscores. A missing MSS or window scale option is reported as +/// zero. The same function serves both the client SYN, which yields JA4T, and +/// the server SYN ACK, which yields JA4TS. +#[must_use] +pub fn ja4t(input: &TcpFingerprintInput) -> String { + let mut options = String::new(); + let mut first = true; + for kind in &input.option_kinds { + if !first { + options.push('-'); + } + first = false; + let _ = write!(options, "{kind}"); + } + + format!( + "{}_{}_{}_{}", + input.window_size, options, input.mss, input.window_scale + ) +} + +#[cfg(test)] +mod tests { + use super::{TcpFingerprintInput, ja4t}; + use smallvec::SmallVec; + + fn input(window: u16, kinds: &[u8], mss: u16, scale: u8) -> TcpFingerprintInput { + TcpFingerprintInput { + window_size: window, + option_kinds: SmallVec::from_slice(kinds), + mss, + window_scale: scale, + } + } + + #[test] + fn foxio_linux_default_vector() { + let i = input(64240, &[2, 1, 3, 1, 1, 4], 1460, 8); + assert_eq!(ja4t(&i), "64240_2-1-3-1-1-4_1460_8"); + } + + #[test] + fn foxio_windowed_vector() { + let i = input(65535, &[2, 1, 3, 1, 1, 8, 4, 0, 0], 1346, 6); + assert_eq!(ja4t(&i), "65535_2-1-3-1-1-8-4-0-0_1346_6"); + } + + #[test] + fn missing_mss_and_scale_report_zero() { + let i = input(8192, &[2, 4], 0, 0); + assert_eq!(ja4t(&i), "8192_2-4_0_0"); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs new file mode 100644 index 00000000..df2af350 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs @@ -0,0 +1,175 @@ +// ©AngelaMos | 2026 +// ja4x.rs + +use smallvec::SmallVec; + +use crate::der::{Der, tag}; +use crate::error::{ParseError, Result}; +use crate::hash::sha256_hex12; + +/// Computes the JA4X fingerprint of one DER encoded X.509 certificate. +/// +/// JA4X does not fingerprint the contents of a certificate. It fingerprints how +/// the certificate was built: which object identifiers appear in the issuer +/// name, which appear in the subject name, and which appear among the +/// extensions, each in the order they were written. Two certificates minted by +/// the same software with the same template share a JA4X even when every name +/// and serial differs, which is what makes it useful for clustering certificates +/// from one toolchain or one malware family. +/// +/// Passively this only works on TLS 1.2 and earlier, where the certificate +/// travels in the clear. TLS 1.3 encrypts the certificate message, so a passive +/// observer never sees it. +pub fn ja4x(cert_der: &[u8]) -> Result { + let (issuer_oids, subject_oids, ext_oids) = certificate_oids(cert_der)?; + + let issuer_hash = sha256_hex12(issuer_oids.join(",").as_bytes()); + let subject_hash = sha256_hex12(subject_oids.join(",").as_bytes()); + let ext_hash = sha256_hex12(ext_oids.join(",").as_bytes()); + + Ok(format!("{issuer_hash}_{subject_hash}_{ext_hash}")) +} + +type OidList = SmallVec<[String; 8]>; + +fn certificate_oids(cert_der: &[u8]) -> Result<(OidList, OidList, OidList)> { + let mut outer = Der::new(cert_der); + let (_, certificate) = outer.read_tlv()?; + let mut cert = Der::new(certificate); + let (_, tbs) = cert.read_tlv()?; + + let mut fields: SmallVec<[(u8, &[u8]); 10]> = SmallVec::new(); + let mut walker = Der::new(tbs); + while !walker.is_empty() { + fields.push(walker.read_tlv()?); + } + + let mut idx = 0; + if fields.first().is_some_and(|(t, _)| *t == 0xa0) { + idx += 1; + } + idx += 2; + let issuer = field_content(&fields, idx)?; + idx += 1; + idx += 1; + let subject = field_content(&fields, idx)?; + idx += 1; + idx += 1; + + let extensions = fields[idx..] + .iter() + .find(|(t, _)| *t == tag::CONTEXT_3) + .map(|(_, c)| *c); + + let issuer_oids = name_oids(issuer)?; + let subject_oids = name_oids(subject)?; + let ext_oids = match extensions { + Some(content) => extension_oids(content)?, + None => OidList::new(), + }; + + Ok((issuer_oids, subject_oids, ext_oids)) +} + +fn field_content<'a>(fields: &[(u8, &'a [u8])], idx: usize) -> Result<&'a [u8]> { + fields + .get(idx) + .map(|(_, c)| *c) + .ok_or(ParseError::Truncated { needed: 1, have: 0 }) +} + +/// Collects the attribute type object identifiers from a Name, in order. +/// +/// A Name is a sequence of relative distinguished names, each a set of attribute +/// type and value pairs. The fingerprint reads only the attribute type, the +/// leading object identifier of each pair, and renders it as the hex of its DER +/// content bytes, which is exactly the form JA4X hashes. +fn name_oids(name: &[u8]) -> Result { + let mut oids = OidList::new(); + let mut rdns = Der::new(name); + while !rdns.is_empty() { + let (_, rdn) = rdns.read_tlv()?; + let mut set = Der::new(rdn); + while !set.is_empty() { + let (_, atv) = set.read_tlv()?; + let mut pair = Der::new(atv); + let (oid_tag, oid) = pair.read_tlv()?; + if oid_tag == tag::OBJECT_IDENTIFIER { + oids.push(hex::encode(oid)); + } + } + } + Ok(oids) +} + +/// Collects the extension object identifiers, in order. +fn extension_oids(context: &[u8]) -> Result { + let mut oids = OidList::new(); + let mut wrapper = Der::new(context); + let (_, sequence) = wrapper.read_tlv()?; + let mut exts = Der::new(sequence); + while !exts.is_empty() { + let (_, ext) = exts.read_tlv()?; + let mut fields = Der::new(ext); + let (oid_tag, oid) = fields.read_tlv()?; + if oid_tag == tag::OBJECT_IDENTIFIER { + oids.push(hex::encode(oid)); + } + } + Ok(oids) +} + +#[cfg(test)] +mod tests { + use super::{extension_oids, name_oids}; + + fn oid(content: &[u8]) -> Vec { + let mut v = vec![0x06, u8::try_from(content.len()).unwrap()]; + v.extend_from_slice(content); + v + } + + fn tlv(tag: u8, content: &[u8]) -> Vec { + let mut v = vec![tag, u8::try_from(content.len()).unwrap()]; + v.extend_from_slice(content); + v + } + + #[test] + fn extracts_issuer_oids_in_order() { + let mut name = Vec::new(); + for content in [ + &[0x55u8, 0x04, 0x06][..], + &[0x55, 0x04, 0x0a], + &[0x55, 0x04, 0x0b], + &[0x55, 0x04, 0x03], + ] { + let atv = tlv(0x30, &{ + let mut a = oid(content); + a.extend(tlv(0x13, b"x")); + a + }); + name.extend(tlv(0x31, &atv)); + } + + let oids = name_oids(&name).unwrap(); + assert_eq!(oids.as_slice(), &["550406", "55040a", "55040b", "550403"]); + } + + #[test] + fn extracts_extension_oids() { + let mut sequence = Vec::new(); + for content in [&[0x55u8, 0x1d, 0x23][..], &[0x55, 0x1d, 0x0e]] { + let ext = tlv(0x30, &{ + let mut e = oid(content); + e.extend(tlv(0x04, b"v")); + e + }); + sequence.extend(ext); + } + let context = tlv(0x30, &sequence); + + let oids = extension_oids(&context).unwrap(); + assert_eq!(oids.as_slice(), &["551d23", "551d0e"]); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/lib.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/lib.rs new file mode 100644 index 00000000..50fb0aed --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/lib.rs @@ -0,0 +1,37 @@ +// ©AngelaMos | 2026 +// lib.rs + +//! TLS handshake parsing and JA3/JA4 family fingerprint computation. +//! +//! This crate is the engine. It parses TLS records and handshake messages, the +//! TLS carried inside QUIC initial packets, and computes the JA3, JA3S, JA4, +//! JA4S, JA4H, JA4X, and JA4T fingerprints. It depends on nothing that touches +//! a network interface, a database, or an async runtime, so it can be embedded, +//! fuzzed, and unit tested in isolation. + +pub mod der; +pub mod error; +pub mod fingerprint; +pub mod grease; +pub mod hash; +pub mod ja3; +pub mod ja4; +pub mod ja4h; +pub mod ja4t; +pub mod ja4x; +pub mod parse; +pub mod pipeline; +pub mod registry; + +pub use error::{ParseError, Result}; +pub use fingerprint::{Ja3, Ja4Family}; +pub use grease::{GREASE_VALUES, is_grease}; +pub use ja3::{ja3, ja3_string, ja3s, ja3s_string}; +pub use ja4::{Transport, ja4, ja4s}; +pub use ja4h::{HttpRequest, ja4h, parse_http_request}; +pub use ja4t::{TcpFingerprintInput, ja4t}; +pub use ja4x::ja4x; +pub use parse::{ClientHello, Extension, ServerHello}; +pub use pipeline::event::{FingerprintEvent, StreamEvent}; +pub use pipeline::source::{PacketSource, PcapFileSource, RawFrame, SourceError}; +pub use pipeline::{Counters, Pipeline, PipelineConfig}; diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/cert.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/cert.rs new file mode 100644 index 00000000..5545777c --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/cert.rs @@ -0,0 +1,70 @@ +// ©AngelaMos | 2026 +// cert.rs + +use smallvec::SmallVec; + +use crate::error::Result; +use crate::parse::reader::Reader; + +/// The DER certificates carried by one TLS Certificate handshake message. +pub type CertificateList<'pkt> = SmallVec<[&'pkt [u8]; 4]>; + +/// Extracts the DER encoded certificates from a Certificate message body. +/// +/// The body is a three byte total length followed by entries that are each a +/// three byte length and the raw DER bytes. This is the TLS 1.2 framing; it is +/// the only framing a passive observer ever parses, because TLS 1.3 moved the +/// Certificate message inside the encrypted part of the handshake. The +/// certificates come back in wire order, leaf first, which is the order JA4X +/// reports them in. +pub fn certificate_der_list(body: &[u8]) -> Result> { + let mut r = Reader::new(body); + let mut list = r.sub_u24_vec()?; + let mut certs = CertificateList::new(); + while !list.is_empty() { + certs.push(list.take_u24_vec()?); + } + Ok(certs) +} + +#[cfg(test)] +mod tests { + use super::certificate_der_list; + use crate::error::ParseError; + + fn message(certs: &[&[u8]]) -> Vec { + let total: usize = certs.iter().map(|c| c.len() + 3).sum(); + let mut v = Vec::new(); + v.extend_from_slice(&u32::try_from(total).unwrap().to_be_bytes()[1..]); + for cert in certs { + v.extend_from_slice(&u32::try_from(cert.len()).unwrap().to_be_bytes()[1..]); + v.extend_from_slice(cert); + } + v + } + + #[test] + fn splits_a_two_certificate_chain() { + let body = message(&[&[0x30, 0x01, 0xaa], &[0x30, 0x02, 0xbb, 0xcc]]); + let certs = certificate_der_list(&body).unwrap(); + assert_eq!(certs.len(), 2); + assert_eq!(certs[0], &[0x30, 0x01, 0xaa]); + assert_eq!(certs[1], &[0x30, 0x02, 0xbb, 0xcc]); + } + + #[test] + fn truncated_entry_is_an_error_not_a_panic() { + let mut body = message(&[&[0x30, 0x01, 0xaa]]); + body.truncate(body.len() - 1); + assert!(matches!( + certificate_der_list(&body), + Err(ParseError::Truncated { .. }) + )); + } + + #[test] + fn empty_chain_is_empty() { + let certs = certificate_der_list(&[0, 0, 0]).unwrap(); + assert!(certs.is_empty()); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/hello.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/hello.rs new file mode 100644 index 00000000..39b32d7a --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/hello.rs @@ -0,0 +1,246 @@ +// ©AngelaMos | 2026 +// hello.rs + +use smallvec::SmallVec; + +use crate::error::Result; +use crate::parse::reader::Reader; +use crate::registry::extension; + +/// A single TLS extension as it appeared on the wire. +/// +/// The extension body is borrowed from the packet, not copied. Specific +/// extensions are decoded on demand through the accessor methods on +/// [`ClientHello`] so that the common parse path does no work for extensions a +/// given fingerprint does not read. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Extension<'pkt> { + pub ext_type: u16, + pub data: &'pkt [u8], +} + +/// A parsed ClientHello, holding exactly what the fingerprint algorithms read. +/// +/// Cipher suites and extensions are stored in wire order. Order is preserved +/// because JA3 hashes extensions in their original order, and because the +/// difference between wire order and sorted order is itself a signal: a client +/// that permutes its extension order on every connection is doing something a +/// fixed order client is not. +#[derive(Debug, Clone)] +pub struct ClientHello<'pkt> { + pub legacy_version: u16, + pub cipher_suites: SmallVec<[u16; 32]>, + pub extensions: SmallVec<[Extension<'pkt>; 16]>, + pub is_sslv2: bool, +} + +/// A parsed ServerHello. +/// +/// The server selects exactly one cipher suite, so `cipher_suite` is a single +/// value rather than a list. Extensions are again kept in wire order. +#[derive(Debug, Clone)] +pub struct ServerHello<'pkt> { + pub legacy_version: u16, + pub cipher_suite: u16, + pub extensions: SmallVec<[Extension<'pkt>; 16]>, +} + +fn parse_extensions<'pkt>(r: &mut Reader<'pkt>) -> Result; 16]>> { + let mut exts = SmallVec::new(); + if r.is_empty() { + return Ok(exts); + } + let mut block = r.sub_u16_vec()?; + while !block.is_empty() { + let ext_type = block.u16()?; + let data = block.take_u16_vec()?; + exts.push(Extension { ext_type, data }); + } + Ok(exts) +} + +/// Parses the body of a ClientHello handshake message. +/// +/// The input is the message body, meaning the four byte handshake header has +/// already been stripped. The session id, which fingerprinting ignores, is +/// skipped, as are the compression methods; getting those two skips right is a +/// classic source of bugs because a parser that forgets them reads the wrong +/// bytes as cipher suites. +pub fn parse_client_hello(body: &[u8]) -> Result> { + let mut r = Reader::new(body); + let legacy_version = r.u16()?; + let _random = r.take(32)?; + let _session_id = r.take_u8_vec()?; + + let mut cipher_reader = r.sub_u16_vec()?; + let mut cipher_suites = SmallVec::new(); + while !cipher_reader.is_empty() { + cipher_suites.push(cipher_reader.u16()?); + } + + let _compression = r.take_u8_vec()?; + let extensions = parse_extensions(&mut r)?; + + Ok(ClientHello { + legacy_version, + cipher_suites, + extensions, + is_sslv2: false, + }) +} + +/// Parses the body of a ServerHello handshake message. +pub fn parse_server_hello(body: &[u8]) -> Result> { + let mut r = Reader::new(body); + let legacy_version = r.u16()?; + let _random = r.take(32)?; + let _session_id = r.take_u8_vec()?; + let cipher_suite = r.u16()?; + let _compression = r.u8()?; + let extensions = parse_extensions(&mut r)?; + + Ok(ServerHello { + legacy_version, + cipher_suite, + extensions, + }) +} + +impl<'pkt> ServerHello<'pkt> { + fn extension(&self, ext_type: u16) -> Option<&Extension<'pkt>> { + self.extensions.iter().find(|e| e.ext_type == ext_type) + } + + /// Returns the version the server selected. + /// + /// In TLS 1.3 the negotiated version lives in the supported versions + /// extension as a single value rather than in the legacy version word, which + /// the server pins to TLS 1.2 for compatibility. JA4S reads the real version + /// from the extension when it is present. + #[must_use] + pub fn selected_version(&self) -> u16 { + self.extension(extension::SUPPORTED_VERSIONS) + .and_then(|ext| { + let mut r = Reader::new(ext.data); + r.u16().ok() + }) + .unwrap_or(self.legacy_version) + } + + /// Returns the ALPN protocol the server chose, if any. + #[must_use] + pub fn alpn_protocol(&self) -> Option<&'pkt [u8]> { + let ext = self.extension(extension::ALPN)?; + let mut r = Reader::new(ext.data); + let mut list = r.sub_u16_vec().ok()?; + list.take_u8_vec().ok().filter(|p| !p.is_empty()) + } +} + +impl<'pkt> ClientHello<'pkt> { + fn extension(&self, ext_type: u16) -> Option<&Extension<'pkt>> { + self.extensions.iter().find(|e| e.ext_type == ext_type) + } + + #[must_use] + pub fn has_extension(&self, ext_type: u16) -> bool { + self.extension(ext_type).is_some() + } + + /// Returns the server name from the SNI extension, if a host name entry is + /// present. + #[must_use] + pub fn server_name(&self) -> Option<&'pkt str> { + let ext = self.extension(extension::SERVER_NAME)?; + let mut list = Reader::new(ext.data).sub_u16_vec().ok()?; + while !list.is_empty() { + let name_type = list.u8().ok()?; + let name = list.take_u16_vec().ok()?; + if name_type == 0 { + return core::str::from_utf8(name).ok(); + } + } + None + } + + /// Returns the supported groups, the field JA3 calls elliptic curves. + #[must_use] + pub fn supported_groups(&self) -> SmallVec<[u16; 16]> { + self.u16_list(extension::SUPPORTED_GROUPS) + } + + /// Returns the elliptic curve point formats. + #[must_use] + pub fn ec_point_formats(&self) -> SmallVec<[u8; 4]> { + let mut out = SmallVec::new(); + let Some(ext) = self.extension(extension::EC_POINT_FORMATS) else { + return out; + }; + let mut r = Reader::new(ext.data); + let Ok(list) = r.take_u8_vec() else { + return out; + }; + out.extend_from_slice(list); + out + } + + /// Returns the protocol versions the client offers in the supported versions + /// extension. JA4 selects its version field from the highest non GREASE + /// value here when the extension is present. + #[must_use] + pub fn supported_versions(&self) -> SmallVec<[u16; 8]> { + let mut out = SmallVec::new(); + let Some(ext) = self.extension(extension::SUPPORTED_VERSIONS) else { + return out; + }; + let mut r = Reader::new(ext.data); + let Ok(list) = r.take_u8_vec() else { + return out; + }; + let mut inner = Reader::new(list); + while let Ok(v) = inner.u16() { + out.push(v); + } + out + } + + /// Returns the signature algorithms in their original order. JA4 appends + /// these, unsorted, to the extension hash input. + #[must_use] + pub fn signature_algorithms(&self) -> SmallVec<[u16; 16]> { + self.u16_list(extension::SIGNATURE_ALGORITHMS) + } + + /// Returns the ALPN protocol identifiers in order. JA4 uses the first one. + #[must_use] + pub fn alpn_protocols(&self) -> SmallVec<[&'pkt [u8]; 4]> { + let mut out = SmallVec::new(); + let Some(ext) = self.extension(extension::ALPN) else { + return out; + }; + let mut r = Reader::new(ext.data); + let Ok(mut list) = r.sub_u16_vec() else { + return out; + }; + while let Ok(proto) = list.take_u8_vec() { + out.push(proto); + } + out + } + + fn u16_list(&self, ext_type: u16) -> SmallVec<[u16; 16]> { + let mut out = SmallVec::new(); + let Some(ext) = self.extension(ext_type) else { + return out; + }; + let mut r = Reader::new(ext.data); + let Ok(list) = r.take_u16_vec() else { + return out; + }; + let mut inner = Reader::new(list); + while let Ok(v) = inner.u16() { + out.push(v); + } + out + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/mod.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/mod.rs new file mode 100644 index 00000000..cd44327d --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/mod.rs @@ -0,0 +1,23 @@ +// ©AngelaMos | 2026 +// mod.rs + +//! Hand written, bounds checked TLS parsing. +//! +//! The parser is deliberately not built on a parser combinator framework. It is +//! the security critical core of the tool, and the byte by byte reader here maps +//! directly onto the wire format described in the TLS RFCs, which makes it easy +//! to audit against the specification. Reassembly of fragmented handshakes is +//! handled before parsing, so every parse function sees a complete message and +//! never has to model partial input. + +pub mod cert; +pub mod hello; +pub mod reader; +pub mod record; + +pub use cert::{CertificateList, certificate_der_list}; +pub use hello::{ClientHello, Extension, ServerHello, parse_client_hello, parse_server_hello}; +pub use reader::Reader; +pub use record::{ + first_handshake_message, handshake_bytes, is_sslv2_client_hello, parse_sslv2_client_hello, +}; diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/reader.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/reader.rs new file mode 100644 index 00000000..b4a8ccfc --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/reader.rs @@ -0,0 +1,173 @@ +// ©AngelaMos | 2026 +// reader.rs + +use crate::error::{ParseError, Result}; + +/// A forward only cursor over a borrowed byte slice with bounds checked reads. +/// +/// Every read advances the cursor and returns an error rather than panicking +/// when the buffer is too short. This is the foundation the whole parser stands +/// on: because the cursor can never read past the end of the slice, the parser +/// has no `unsafe`, cannot index out of bounds, and treats a truncated or +/// hostile packet as an ordinary error instead of a crash. The slices it hands +/// back borrow from the original packet buffer, so parsing copies nothing. +pub struct Reader<'pkt> { + buf: &'pkt [u8], + pos: usize, +} + +impl<'pkt> Reader<'pkt> { + #[must_use] + pub const fn new(buf: &'pkt [u8]) -> Self { + Self { buf, pos: 0 } + } + + #[must_use] + pub const fn remaining(&self) -> usize { + self.buf.len() - self.pos + } + + #[must_use] + pub const fn is_empty(&self) -> bool { + self.remaining() == 0 + } + + fn need(&self, n: usize) -> Result<()> { + if self.remaining() < n { + return Err(ParseError::Truncated { + needed: n, + have: self.remaining(), + }); + } + Ok(()) + } + + pub fn u8(&mut self) -> Result { + self.need(1)?; + let v = self.buf[self.pos]; + self.pos += 1; + Ok(v) + } + + pub fn u16(&mut self) -> Result { + self.need(2)?; + let v = u16::from_be_bytes([self.buf[self.pos], self.buf[self.pos + 1]]); + self.pos += 2; + Ok(v) + } + + /// Reads a 24 bit big endian length, the width TLS uses for handshake + /// message bodies and certificate entries. + pub fn u24(&mut self) -> Result { + self.need(3)?; + let v = u32::from_be_bytes([ + 0, + self.buf[self.pos], + self.buf[self.pos + 1], + self.buf[self.pos + 2], + ]); + self.pos += 3; + Ok(v) + } + + pub fn u32(&mut self) -> Result { + self.need(4)?; + let v = u32::from_be_bytes([ + self.buf[self.pos], + self.buf[self.pos + 1], + self.buf[self.pos + 2], + self.buf[self.pos + 3], + ]); + self.pos += 4; + Ok(v) + } + + /// Borrows the next `n` bytes and advances past them. + pub fn take(&mut self, n: usize) -> Result<&'pkt [u8]> { + self.need(n)?; + let slice = &self.buf[self.pos..self.pos + n]; + self.pos += n; + Ok(slice) + } + + /// Reads a one byte length prefix, then borrows that many bytes. + pub fn take_u8_vec(&mut self) -> Result<&'pkt [u8]> { + let len = self.u8()? as usize; + self.take(len) + } + + /// Reads a two byte length prefix, then borrows that many bytes. + pub fn take_u16_vec(&mut self) -> Result<&'pkt [u8]> { + let len = self.u16()? as usize; + self.take(len) + } + + /// Reads a three byte length prefix, then borrows that many bytes. + pub fn take_u24_vec(&mut self) -> Result<&'pkt [u8]> { + let len = self.u24()? as usize; + self.take(len) + } + + /// Returns a sub reader over a two byte length prefixed region. + /// + /// This is the workhorse for nested vectors such as the extensions block, + /// where an outer length governs a region that itself contains a sequence of + /// length prefixed elements. + pub fn sub_u16_vec(&mut self) -> Result> { + let len = self.u16()? as usize; + let slice = self.take(len)?; + Ok(Reader::new(slice)) + } + + /// Returns a sub reader over a three byte length prefixed region, the width + /// the Certificate message uses for its certificate list. + pub fn sub_u24_vec(&mut self) -> Result> { + let len = self.u24()? as usize; + let slice = self.take(len)?; + Ok(Reader::new(slice)) + } +} + +#[cfg(test)] +mod tests { + use super::Reader; + use crate::error::ParseError; + + #[test] + fn reads_widths_in_order() { + let data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]; + let mut r = Reader::new(&data); + assert_eq!(r.u8().unwrap(), 0x01); + assert_eq!(r.u16().unwrap(), 0x0203); + assert_eq!(r.u24().unwrap(), 0x0004_0506); + assert!(r.is_empty()); + } + + #[test] + fn short_read_is_an_error_not_a_panic() { + let data = [0x01]; + let mut r = Reader::new(&data); + assert_eq!( + r.u16().unwrap_err(), + ParseError::Truncated { needed: 2, have: 1 } + ); + } + + #[test] + fn length_prefixed_take_respects_bounds() { + let data = [0x03, 0xaa, 0xbb, 0xcc, 0xff]; + let mut r = Reader::new(&data); + assert_eq!(r.take_u8_vec().unwrap(), &[0xaa, 0xbb, 0xcc]); + assert_eq!(r.u8().unwrap(), 0xff); + } + + #[test] + fn sub_vector_isolates_a_region() { + let data = [0x00, 0x02, 0x11, 0x22, 0x33]; + let mut r = Reader::new(&data); + let mut sub = r.sub_u16_vec().unwrap(); + assert_eq!(sub.remaining(), 2); + assert_eq!(sub.u16().unwrap(), 0x1122); + assert_eq!(r.u8().unwrap(), 0x33); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/record.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/record.rs new file mode 100644 index 00000000..6cf0656b --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/parse/record.rs @@ -0,0 +1,179 @@ +// ©AngelaMos | 2026 +// record.rs + +use std::borrow::Cow; + +use smallvec::SmallVec; + +use crate::error::{ParseError, Result}; +use crate::parse::hello::ClientHello; +use crate::parse::reader::Reader; +use crate::registry::content_type; + +/// Reassembles the cleartext handshake flight from a TLS record stream. +/// +/// A handshake message can be split across several TLS records, and several +/// short messages can share one record. This walks the record framing and +/// concatenates the payloads of the handshake records so the caller sees one +/// contiguous handshake byte stream. The common case is a single record holding +/// a single ClientHello, and that case borrows the original bytes with no copy. +/// Only genuinely fragmented flights allocate. +/// +/// Records carrying anything other than handshake data are ignored. In TLS 1.3 +/// the later handshake messages travel inside records typed as application data +/// and are encrypted, so they never reach this function, which is correct: the +/// only handshake bytes we can read in the clear are the first flight. +pub fn handshake_bytes(stream: &[u8]) -> Result> { + let mut segments: SmallVec<[&[u8]; 4]> = SmallVec::new(); + let mut r = Reader::new(stream); + + while r.remaining() >= 5 { + let ctype = r.u8()?; + let _version = r.u16()?; + let payload = r.take_u16_vec()?; + if ctype == content_type::HANDSHAKE { + segments.push(payload); + } else if !segments.is_empty() { + break; + } + } + + match segments.as_slice() { + [] => Err(ParseError::Truncated { + needed: 5, + have: stream.len(), + }), + [only] => Ok(Cow::Borrowed(*only)), + many => { + let mut joined = Vec::with_capacity(many.iter().map(|s| s.len()).sum()); + for seg in many { + joined.extend_from_slice(seg); + } + Ok(Cow::Owned(joined)) + } + } +} + +/// Returns the body of the first handshake message of the requested type. +/// +/// The handshake header is a one byte type and a three byte length. This walks +/// the messages in the reassembled flight and returns the body slice of the +/// first one whose type matches, so the caller never has to reason about the +/// header widths. +pub fn first_handshake_message(handshake: &[u8], want_type: u8) -> Result<&[u8]> { + let mut r = Reader::new(handshake); + while r.remaining() >= 4 { + let msg_type = r.u8()?; + let len = r.u24()? as usize; + let body = r.take(len)?; + if msg_type == want_type { + return Ok(body); + } + } + Err(ParseError::UnexpectedHandshake(want_type)) +} + +/// Returns true when the stream begins with an SSLv2 style ClientHello. +/// +/// SSLv2 framing sets the high bit of the first length byte and places the +/// message type in the first body byte. Type 1 is CLIENT-HELLO. Some old +/// malware opens with this backward compatible hello even when it intends to +/// negotiate TLS, so detecting it keeps the TLS parser from misreading the +/// SSLv2 header as a TLS record. +#[must_use] +pub fn is_sslv2_client_hello(stream: &[u8]) -> bool { + stream.len() >= 3 && (stream[0] & 0x80) != 0 && stream[2] == 1 +} + +/// Parses an SSLv2 style ClientHello into the common ClientHello shape. +/// +/// SSLv2 carries no extensions, supported groups, or point formats, so those +/// stay empty, which matches the community consensus for fingerprinting an +/// SSLv2 hello. Cipher specs are three bytes each. Specs that begin with a zero +/// byte are SSLv3 and TLS cipher suites carried in the backward compatible +/// hello, and those are the values a fingerprint cares about, so they are +/// extracted as their two byte suite numbers. True SSLv2 only specs are +/// counted but cannot be expressed as two byte suites and are skipped. +pub fn parse_sslv2_client_hello(stream: &[u8]) -> Result> { + let mut r = Reader::new(stream); + let len_hi = r.u8()? & 0x7f; + let len_lo = r.u8()?; + let _record_len = (u16::from(len_hi) << 8) | u16::from(len_lo); + + let msg_type = r.u8()?; + if msg_type != 1 { + return Err(ParseError::UnexpectedHandshake(msg_type)); + } + + let legacy_version = r.u16()?; + let cipher_spec_len = r.u16()? as usize; + let session_id_len = r.u16()? as usize; + let challenge_len = r.u16()? as usize; + + let cipher_specs = r.take(cipher_spec_len)?; + let _session_id = r.take(session_id_len)?; + let _challenge = r.take(challenge_len)?; + + let mut cipher_suites = SmallVec::new(); + let mut specs = Reader::new(cipher_specs); + while specs.remaining() >= 3 { + let kind = specs.u8()?; + let suite = specs.u16()?; + if kind == 0 { + cipher_suites.push(suite); + } + } + + Ok(ClientHello { + legacy_version, + cipher_suites, + extensions: SmallVec::new(), + is_sslv2: true, + }) +} + +#[cfg(test)] +mod tests { + use super::{first_handshake_message, handshake_bytes, is_sslv2_client_hello}; + use crate::registry::handshake_type; + + fn record(ctype: u8, payload: &[u8]) -> Vec { + let mut v = vec![ctype, 0x03, 0x03]; + let len = u16::try_from(payload.len()).unwrap(); + v.extend_from_slice(&len.to_be_bytes()); + v.extend_from_slice(payload); + v + } + + #[test] + fn single_record_borrows() { + let stream = record(22, &[0x01, 0x00, 0x00, 0x00]); + let hs = handshake_bytes(&stream).unwrap(); + assert!(matches!(hs, std::borrow::Cow::Borrowed(_))); + assert_eq!(hs.as_ref(), &[0x01, 0x00, 0x00, 0x00]); + } + + #[test] + fn fragmented_records_join() { + let mut stream = record(22, &[0x01, 0x00, 0x00, 0x06, 0xaa, 0xbb]); + stream.extend(record(22, &[0xcc, 0xdd, 0xee, 0xff])); + let hs = handshake_bytes(&stream).unwrap(); + assert!(matches!(hs, std::borrow::Cow::Owned(_))); + assert_eq!(hs.as_ref().len(), 10); + } + + #[test] + fn finds_the_requested_message() { + let hs = [ + 0x02, 0x00, 0x00, 0x01, 0x99, 0x01, 0x00, 0x00, 0x02, 0xaa, 0xbb, + ]; + let body = first_handshake_message(&hs, handshake_type::CLIENT_HELLO).unwrap(); + assert_eq!(body, &[0xaa, 0xbb]); + } + + #[test] + fn sslv2_detection() { + assert!(is_sslv2_client_hello(&[0x80, 0x2e, 0x01, 0x00, 0x02])); + assert!(!is_sslv2_client_hello(&[0x16, 0x03, 0x01, 0x00])); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs new file mode 100644 index 00000000..6db1b4bd --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs @@ -0,0 +1,347 @@ +// ©AngelaMos | 2026 +// decode.rs + +use std::net::{IpAddr, SocketAddr}; + +use etherparse::{EtherType, NetSlice, SlicedPacket, TransportSlice}; +use smallvec::SmallVec; + +use crate::ja4t::TcpFingerprintInput; + +/// LINKTYPE registry numbers this decoder understands. +/// +/// The values come from the tcpdump link layer header type registry. They are +/// redeclared here as plain constants because pcap file readers and live +/// captures both report them as bare integers, and the decoder is the single +/// place that interprets them. +pub mod link_type { + pub const NULL: i32 = 0; + pub const ETHERNET: i32 = 1; + pub const RAW: i32 = 101; + pub const LOOP: i32 = 108; + pub const LINUX_SLL: i32 = 113; + pub const IPV4: i32 = 228; + pub const IPV6: i32 = 229; + pub const LINUX_SLL2: i32 = 276; +} + +/// The BSD null and loopback link headers are four bytes of address family. +const NULL_HEADER_LEN: usize = 4; + +/// The Linux cooked capture v2 header is twenty bytes with the protocol in the +/// first two. +const SLL2_HEADER_LEN: usize = 20; + +/// The TCP kind numbers the JA4T walk extracts values from. +const TCP_OPT_END: u8 = 0; +const TCP_OPT_NOP: u8 = 1; +const TCP_OPT_MSS: u8 = 2; +const TCP_OPT_WSCALE: u8 = 3; +const TCP_OPT_MSS_LEN: u8 = 4; +const TCP_OPT_WSCALE_LEN: u8 = 3; + +/// The TCP flag bits, exactly as byte thirteen of the header carries them. +/// +/// Keeping the flags as the wire bitfield instead of a fistful of bools means +/// the struct mirrors the protocol and reads the byte the packet already +/// holds, rather than rebuilding it from a handful of accessor calls. Adding +/// a flag later is then a constant, not a field. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TcpFlags(u8); + +impl TcpFlags { + pub const FIN: u8 = 0x01; + pub const SYN: u8 = 0x02; + pub const RST: u8 = 0x04; + pub const ACK: u8 = 0x10; + + /// The offset of the flags byte within a TCP header. + const FLAGS_BYTE: usize = 13; + + #[must_use] + pub const fn new(bits: u8) -> Self { + Self(bits) + } + + /// Reads the flags byte from a TCP header slice. + /// + /// A missing byte cannot happen for a slice the decoder hands in, since + /// the transport layer is only present when a full header parsed, but the + /// bounds checked read keeps this honest under direct unit testing. + #[must_use] + fn from_header(header: &[u8]) -> Self { + Self(header.get(Self::FLAGS_BYTE).copied().unwrap_or(0)) + } + + #[must_use] + pub const fn syn(self) -> bool { + self.0 & Self::SYN != 0 + } + + #[must_use] + pub const fn ack(self) -> bool { + self.0 & Self::ACK != 0 + } + + #[must_use] + pub const fn fin(self) -> bool { + self.0 & Self::FIN != 0 + } + + #[must_use] + pub const fn rst(self) -> bool { + self.0 & Self::RST != 0 + } +} + +/// The TCP level facts about one decoded segment. +#[derive(Debug, Clone, Copy)] +pub struct TcpMeta { + pub seq: u32, + pub flags: TcpFlags, + pub window_size: u16, +} + +/// One TCP segment decoded out of a captured frame. +/// +/// Addresses are directional: `src` sent this segment to `dst`. The JA4T +/// input is walked eagerly, but only for SYN packets, because those are the +/// only packets whose options JA4T reads and the walk needs the option bytes +/// that do not outlive the decode. +#[derive(Debug)] +pub struct DecodedSegment<'pkt> { + pub src: SocketAddr, + pub dst: SocketAddr, + pub tcp: TcpMeta, + pub syn_fingerprint: Option, + pub payload: &'pkt [u8], +} + +/// Why a frame produced no segment. The distinction only feeds counters, but +/// the counters are how an operator learns what a capture was made of. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Skip { + UnsupportedLinkType, + NotIp, + NotTcp, + Malformed, +} + +/// Decodes a captured frame down to its TCP segment, if it has one. +/// +/// VLAN tags, including stacked QinQ, are stepped over by etherparse. Frames +/// the decoder does not understand are skipped with a reason rather than +/// failing the capture: a fingerprinting pipeline must shrug off the GRE +/// tunnel, the ARP chatter, and the malformed frame that share every real +/// network with the TLS it cares about. +pub fn decode_frame(link: i32, data: &[u8]) -> Result, Skip> { + let sliced = match link { + link_type::ETHERNET => SlicedPacket::from_ethernet(data), + link_type::LINUX_SLL => SlicedPacket::from_linux_sll(data), + link_type::RAW | link_type::IPV4 | link_type::IPV6 => SlicedPacket::from_ip(data), + link_type::NULL | link_type::LOOP => { + let Some(inner) = data.get(NULL_HEADER_LEN..) else { + return Err(Skip::Malformed); + }; + SlicedPacket::from_ip(inner) + } + link_type::LINUX_SLL2 => { + let Some(proto) = data.first_chunk::<2>() else { + return Err(Skip::Malformed); + }; + let Some(inner) = data.get(SLL2_HEADER_LEN..) else { + return Err(Skip::Malformed); + }; + SlicedPacket::from_ether_type(EtherType(u16::from_be_bytes(*proto)), inner) + } + _ => return Err(Skip::UnsupportedLinkType), + }; + let sliced = sliced.map_err(|_| Skip::Malformed)?; + + let (src_ip, dst_ip): (IpAddr, IpAddr) = match &sliced.net { + Some(NetSlice::Ipv4(v4)) => ( + IpAddr::V4(v4.header().source_addr()), + IpAddr::V4(v4.header().destination_addr()), + ), + Some(NetSlice::Ipv6(v6)) => ( + IpAddr::V6(v6.header().source_addr()), + IpAddr::V6(v6.header().destination_addr()), + ), + Some(NetSlice::Arp(_)) | None => return Err(Skip::NotIp), + }; + + let Some(TransportSlice::Tcp(tcp)) = &sliced.transport else { + return Err(Skip::NotTcp); + }; + + let flags = TcpFlags::from_header(tcp.slice()); + let syn_fingerprint = flags + .syn() + .then(|| tcp_fingerprint_input(tcp.window_size(), tcp.options())); + + Ok(DecodedSegment { + src: SocketAddr::new(src_ip, tcp.source_port()), + dst: SocketAddr::new(dst_ip, tcp.destination_port()), + tcp: TcpMeta { + seq: tcp.sequence_number(), + flags, + window_size: tcp.window_size(), + }, + syn_fingerprint, + payload: tcp.payload(), + }) +} + +/// Walks raw TCP options into the JA4T input. +/// +/// JA4T records every option kind in order, including each NOP and each +/// trailing end of list byte, because the padding pattern is part of how an +/// operating system's stack writes a SYN. The walk is deliberately tolerant: +/// a truncated or nonsense length byte ends the walk after recording the kind +/// it was found on, so a hostile SYN cannot push the parser out of bounds. +pub fn tcp_fingerprint_input(window_size: u16, options: &[u8]) -> TcpFingerprintInput { + let mut kinds: SmallVec<[u8; 8]> = SmallVec::new(); + let mut mss = 0u16; + let mut window_scale = 0u8; + + let mut i = 0; + while i < options.len() { + let kind = options[i]; + kinds.push(kind); + if kind == TCP_OPT_END || kind == TCP_OPT_NOP { + i += 1; + continue; + } + let Some(&len) = options.get(i + 1) else { + break; + }; + if len < 2 { + break; + } + let Some(body) = options.get(i + 2..i + usize::from(len)) else { + break; + }; + if kind == TCP_OPT_MSS && len == TCP_OPT_MSS_LEN { + if let Some(value) = body.first_chunk::<2>() { + mss = u16::from_be_bytes(*value); + } + } + if kind == TCP_OPT_WSCALE && len == TCP_OPT_WSCALE_LEN { + if let Some(&value) = body.first() { + window_scale = value; + } + } + i += usize::from(len); + } + + TcpFingerprintInput { + window_size, + option_kinds: kinds, + mss, + window_scale, + } +} + +#[cfg(test)] +mod tests { + use super::{Skip, TcpFlags, decode_frame, link_type, tcp_fingerprint_input}; + use crate::ja4t::ja4t; + use etherparse::PacketBuilder; + + fn tcp_frame(payload: &[u8]) -> Vec { + let builder = PacketBuilder::ethernet2([1; 6], [2; 6]) + .ipv4([10, 0, 0, 1], [10, 0, 0, 2], 64) + .tcp(40000, 443, 1000, 64240); + let mut out = Vec::with_capacity(builder.size(payload.len())); + builder.write(&mut out, payload).unwrap(); + out + } + + #[test] + fn flags_byte_decodes_to_the_right_bits() { + let syn = TcpFlags::from_header(&[0u8; 14]); + assert!(!syn.syn()); + + let mut header = [0u8; 20]; + header[13] = TcpFlags::SYN | TcpFlags::ACK; + let flags = TcpFlags::from_header(&header); + assert!(flags.syn() && flags.ack()); + assert!(!flags.fin() && !flags.rst()); + + assert!(!TcpFlags::from_header(&[]).syn()); + } + + #[test] + fn decodes_an_ethernet_tcp_frame() { + let frame = tcp_frame(b"hello"); + let seg = decode_frame(link_type::ETHERNET, &frame).unwrap(); + assert_eq!(seg.src.to_string(), "10.0.0.1:40000"); + assert_eq!(seg.dst.to_string(), "10.0.0.2:443"); + assert_eq!(seg.tcp.seq, 1000); + assert_eq!(seg.payload, b"hello"); + } + + #[test] + fn decodes_a_vlan_tagged_frame() { + let builder = PacketBuilder::ethernet2([1; 6], [2; 6]) + .single_vlan(etherparse::VlanId::try_new(7).unwrap()) + .ipv4([10, 0, 0, 1], [10, 0, 0, 2], 64) + .tcp(40000, 443, 1, 64240); + let mut frame = Vec::with_capacity(builder.size(0)); + builder.write(&mut frame, &[]).unwrap(); + + let seg = decode_frame(link_type::ETHERNET, &frame).unwrap(); + assert_eq!(seg.dst.port(), 443); + } + + #[test] + fn non_tcp_and_garbage_are_skips_not_panics() { + let builder = PacketBuilder::ethernet2([1; 6], [2; 6]) + .ipv4([10, 0, 0, 1], [10, 0, 0, 2], 64) + .udp(5000, 53); + let mut udp = Vec::with_capacity(builder.size(4)); + builder.write(&mut udp, &[0xde, 0xad, 0xbe, 0xef]).unwrap(); + + assert!(matches!( + decode_frame(link_type::ETHERNET, &udp), + Err(Skip::NotTcp) + )); + assert!(matches!( + decode_frame(link_type::ETHERNET, &[0x01, 0x02]), + Err(Skip::Malformed) + )); + assert!(matches!( + decode_frame(147, &udp), + Err(Skip::UnsupportedLinkType) + )); + } + + #[test] + fn ja4t_walk_reproduces_the_linux_default_vector() { + let options = [ + 0x02, 0x04, 0x05, 0xb4, 0x01, 0x03, 0x03, 0x08, 0x01, 0x01, 0x04, 0x02, + ]; + let input = tcp_fingerprint_input(64240, &options); + assert_eq!(ja4t(&input), "64240_2-1-3-1-1-4_1460_8"); + } + + #[test] + fn ja4t_walk_counts_trailing_end_of_list_padding() { + let options = [ + 0x02, 0x04, 0x05, 0x42, 0x01, 0x03, 0x03, 0x06, 0x01, 0x01, 0x08, 0x0a, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, + ]; + let input = tcp_fingerprint_input(65535, &options); + assert_eq!(ja4t(&input), "65535_2-1-3-1-1-8-4-0-0_1346_6"); + } + + #[test] + fn ja4t_walk_survives_truncated_options() { + let input = tcp_fingerprint_input(1024, &[0x02, 0x04, 0x05]); + assert_eq!(input.option_kinds.as_slice(), &[0x02]); + assert_eq!(input.mss, 0); + + let zero_len = tcp_fingerprint_input(1024, &[0x05, 0x00, 0x02]); + assert_eq!(zero_len.option_kinds.as_slice(), &[0x05]); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/event.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/event.rs new file mode 100644 index 00000000..70d2c9fe --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/event.rs @@ -0,0 +1,135 @@ +// ©AngelaMos | 2026 +// event.rs + +use std::fmt; +use std::net::SocketAddr; + +use serde::Serialize; + +use crate::fingerprint::{Ja3, Ja4Family}; + +/// A fingerprint produced by one direction of one flow, without addressing. +/// +/// The protocol layer emits these; the pipeline wraps them with the flow's +/// addresses and timestamp to make a [`FingerprintEvent`]. Keeping the two +/// layers apart means the protocol extractor can be tested with bare byte +/// streams, no packets required. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum StreamEvent { + ClientHello { + ja3: Ja3, + ja3_raw: String, + ja4: Ja4Family, + sni: Option, + alpn: Option, + }, + ServerHello { + ja3s: Ja3, + ja3s_raw: String, + ja4s: Ja4Family, + }, + Certificate { + ja4x: String, + }, + HttpRequest { + ja4h: Ja4Family, + method: String, + host: Option, + }, + TcpSyn { + ja4t: String, + }, + TcpSynAck { + ja4ts: String, + }, +} + +/// One fingerprint observation, addressed and timestamped. +/// +/// `src` is always the party that sent the fingerprinted bytes: the client +/// for a ClientHello or SYN, the server for a ServerHello or certificate. +#[derive(Debug, Clone, Serialize)] +pub struct FingerprintEvent { + pub ts_nanos: u64, + pub src: SocketAddr, + pub dst: SocketAddr, + #[serde(flatten)] + pub event: StreamEvent, +} + +impl fmt::Display for FingerprintEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let secs = self.ts_nanos / 1_000_000_000; + let millis = self.ts_nanos % 1_000_000_000 / 1_000_000; + write!(f, "{secs}.{millis:03} {} -> {} ", self.src, self.dst)?; + match &self.event { + StreamEvent::ClientHello { + ja3, + ja4, + sni, + alpn, + .. + } => { + write!(f, "client_hello ja4={} ja3={ja3}", ja4.hash)?; + if let Some(sni) = sni { + write!(f, " sni={sni}")?; + } + if let Some(alpn) = alpn { + write!(f, " alpn={alpn}")?; + } + Ok(()) + } + StreamEvent::ServerHello { ja3s, ja4s, .. } => { + write!(f, "server_hello ja4s={} ja3s={ja3s}", ja4s.hash) + } + StreamEvent::Certificate { ja4x } => write!(f, "certificate ja4x={ja4x}"), + StreamEvent::HttpRequest { ja4h, method, host } => { + write!(f, "http_request ja4h={} method={method}", ja4h.hash)?; + if let Some(host) = host { + write!(f, " host={host}")?; + } + Ok(()) + } + StreamEvent::TcpSyn { ja4t } => write!(f, "tcp_syn ja4t={ja4t}"), + StreamEvent::TcpSynAck { ja4ts } => write!(f, "tcp_syn_ack ja4ts={ja4ts}"), + } + } +} + +#[cfg(test)] +mod tests { + use super::{FingerprintEvent, StreamEvent}; + + #[test] + fn json_shape_is_tagged_and_flat() { + let event = FingerprintEvent { + ts_nanos: 1_500_000_000, + src: "10.0.0.1:40000".parse().unwrap(), + dst: "10.0.0.2:443".parse().unwrap(), + event: StreamEvent::Certificate { + ja4x: "7d5dbb3783b4_ba7ce0880c07_7bf9a7bf7029".into(), + }, + }; + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["kind"], "certificate"); + assert_eq!(json["ja4x"], "7d5dbb3783b4_ba7ce0880c07_7bf9a7bf7029"); + assert_eq!(json["src"], "10.0.0.1:40000"); + } + + #[test] + fn display_is_one_greppable_line() { + let event = FingerprintEvent { + ts_nanos: 1_234_000_000, + src: "10.0.0.1:40000".parse().unwrap(), + dst: "10.0.0.2:443".parse().unwrap(), + event: StreamEvent::TcpSyn { + ja4t: "64240_2-1-3-1-1-4_1460_8".into(), + }, + }; + assert_eq!( + event.to_string(), + "1.234 10.0.0.1:40000 -> 10.0.0.2:443 tcp_syn ja4t=64240_2-1-3-1-1-4_1460_8" + ); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/flow.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/flow.rs new file mode 100644 index 00000000..a2e28ae7 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/flow.rs @@ -0,0 +1,430 @@ +// ©AngelaMos | 2026 +// flow.rs + +use std::collections::BTreeMap; +use std::net::SocketAddr; + +/// A bidirectional flow identity. +/// +/// The two endpoints are stored in sorted order so that a packet and its reply +/// hash to the same key. Which endpoint is the client is a separate question, +/// answered by who sent the SYN or, failing that, who spoke a ClientHello, and +/// it is deliberately not baked into the key: captures routinely start in the +/// middle of connections, and a key that guessed wrong would split one +/// conversation into two. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FlowKey { + pub lo: SocketAddr, + pub hi: SocketAddr, +} + +/// Which endpoint of a [`FlowKey`] sent a given segment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + FromLo, + FromHi, +} + +impl Direction { + #[must_use] + pub const fn index(self) -> usize { + match self { + Direction::FromLo => 0, + Direction::FromHi => 1, + } + } + + /// The source and destination addresses of traffic flowing this way. + #[must_use] + pub const fn addresses(self, key: &FlowKey) -> (SocketAddr, SocketAddr) { + match self { + Direction::FromLo => (key.lo, key.hi), + Direction::FromHi => (key.hi, key.lo), + } + } +} + +impl FlowKey { + /// Normalizes a directional (source, destination) pair into a key plus the + /// direction the packet travelled. + #[must_use] + pub fn from_pair(src: SocketAddr, dst: SocketAddr) -> (Self, Direction) { + if src <= dst { + (Self { lo: src, hi: dst }, Direction::FromLo) + } else { + (Self { lo: dst, hi: src }, Direction::FromHi) + } + } +} + +/// The midpoint of the sequence space. In TCP serial arithmetic an offset at +/// or beyond this point is read as the segment sitting behind the anchor, not +/// absurdly far ahead of it. +const HALF_SERIAL_SPACE: u32 = 0x8000_0000; + +/// Resource limits for one reassembled direction of one flow. +#[derive(Debug, Clone, Copy)] +pub struct ReassemblyLimits { + /// Most contiguous bytes kept. Everything a passive fingerprinter reads + /// sits in the first kilobytes of a stream, so this is a cap on patience, + /// not on correctness. + pub max_assembled_bytes: usize, + /// Most bytes parked in the out of order buffer. + pub max_pending_bytes: usize, + /// Most segments parked in the out of order buffer. + pub max_pending_segments: usize, +} + +/// What [`StreamReassembler::push`] did with a segment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PushOutcome { + /// The contiguous stream grew; the protocol layer should look again. + Grew, + /// Nothing new: a duplicate, pure overlap, or empty segment. + Unchanged, + /// The segment was parked out of order for later. + Parked, + /// The segment fell outside the window this reassembler is willing to + /// track, or a buffer limit was hit, and it was dropped. + Dropped, +} + +/// Reassembles one direction of a TCP stream into contiguous bytes. +/// +/// This is the piece most toy fingerprinting tools skip, and skipping it is +/// why they miss handshakes: a ClientHello, and even more so a certificate +/// chain, regularly spans several segments, and those segments arrive +/// reordered on any path with packet loss. The reassembler anchors at the +/// sequence number the SYN names or, on a flow whose start the capture +/// missed, at the first segment it sees. Everything else is a relative +/// offset from that anchor in wrapping serial arithmetic: in order segments +/// append to one contiguous buffer, out of order segments park in a map +/// keyed by offset until the gap before them fills. +/// +/// Data from before the anchor on a SYN-less flow is gone; a streaming +/// engine cannot retroactively prepend, and accepting that loss explicitly +/// is what Suricata does for midstream pickup too. A segment that straddles +/// the anchor is trimmed to its useful part rather than discarded. +/// +/// Overlaps resolve first write wins: bytes already accepted are never +/// rewritten by a later segment. A passive observer cannot know which copy +/// the receiver kept, and the CVE-2018-6794 capture in the test corpus exists +/// precisely because inconsistent overlap handling let attackers show an IDS +/// a different stream than the one the victim read. First write wins is one +/// deterministic, documented answer. +#[derive(Debug)] +pub struct StreamReassembler { + limits: ReassemblyLimits, + anchor: Option, + assembled: Vec, + pending: BTreeMap>, + pending_bytes: usize, + released: bool, + capped: bool, +} + +impl StreamReassembler { + #[must_use] + pub fn new(limits: ReassemblyLimits) -> Self { + Self { + limits, + anchor: None, + assembled: Vec::new(), + pending: BTreeMap::new(), + pending_bytes: 0, + released: false, + capped: false, + } + } + + /// The contiguous bytes assembled so far, from the anchor onward. + #[must_use] + pub fn data(&self) -> &[u8] { + &self.assembled + } + + /// Pins the stream start, used when a SYN reveals the true initial + /// sequence number before any data arrives. Later anchors are ignored. + pub fn anchor(&mut self, seq: u32) { + if self.anchor.is_none() { + self.anchor = Some(seq); + } + } + + /// Drops every buffer and refuses all future data. + /// + /// Called once the protocol layer has what it needs, or knows it never + /// will. This is what keeps memory flat when a capture contains long + /// lived flows: the flow entry stays, the payload buffers do not. + pub fn release(&mut self) { + self.assembled = Vec::new(); + self.pending = BTreeMap::new(); + self.pending_bytes = 0; + self.released = true; + } + + #[must_use] + pub fn released(&self) -> bool { + self.released + } + + /// True when the assembled cap was hit and the tail of the stream is gone. + #[must_use] + pub fn capped(&self) -> bool { + self.capped + } + + /// Offers one segment to the stream. + pub fn push(&mut self, seq: u32, payload: &[u8]) -> PushOutcome { + if self.released || payload.is_empty() { + return PushOutcome::Unchanged; + } + if self.capped { + return PushOutcome::Dropped; + } + let anchor = *self.anchor.get_or_insert(seq); + + let offset = seq.wrapping_sub(anchor); + if offset >= HALF_SERIAL_SPACE { + let stale = offset.wrapping_neg() as usize; + if stale >= payload.len() { + return PushOutcome::Unchanged; + } + return self.push(anchor, &payload[stale..]); + } + let window_end = self + .limits + .max_assembled_bytes + .saturating_add(self.limits.max_pending_bytes); + if offset as usize > window_end { + return PushOutcome::Dropped; + } + + let assembled_len = self.assembled.len(); + if (offset as usize) < assembled_len { + let overlap = assembled_len - offset as usize; + if overlap >= payload.len() { + return PushOutcome::Unchanged; + } + return self.append_in_order(&payload[overlap..]); + } + if offset as usize == assembled_len { + return self.append_in_order(payload); + } + + if self.pending.len() >= self.limits.max_pending_segments + || self.pending_bytes.saturating_add(payload.len()) > self.limits.max_pending_bytes + { + return PushOutcome::Dropped; + } + match self.pending.entry(offset) { + std::collections::btree_map::Entry::Occupied(existing) => { + if existing.get().len() >= payload.len() { + return PushOutcome::Unchanged; + } + self.pending_bytes += payload.len() - existing.get().len(); + *existing.into_mut() = payload.to_vec(); + } + std::collections::btree_map::Entry::Vacant(slot) => { + self.pending_bytes += payload.len(); + slot.insert(payload.to_vec()); + } + } + PushOutcome::Parked + } + + fn append_in_order(&mut self, payload: &[u8]) -> PushOutcome { + let room = self + .limits + .max_assembled_bytes + .saturating_sub(self.assembled.len()); + if room == 0 { + self.mark_capped(); + return PushOutcome::Dropped; + } + let take = payload.len().min(room); + self.assembled.extend_from_slice(&payload[..take]); + if take < payload.len() { + self.mark_capped(); + } else { + self.drain_pending(); + } + PushOutcome::Grew + } + + /// Splices parked segments onto the contiguous buffer while they touch it. + fn drain_pending(&mut self) { + while let Some(entry) = self.pending.first_entry() { + let offset = *entry.key() as usize; + if offset > self.assembled.len() { + break; + } + let segment = entry.remove(); + self.pending_bytes -= segment.len(); + let overlap = self.assembled.len() - offset; + if overlap >= segment.len() { + continue; + } + let room = self + .limits + .max_assembled_bytes + .saturating_sub(self.assembled.len()); + let take = (segment.len() - overlap).min(room); + self.assembled + .extend_from_slice(&segment[overlap..overlap + take]); + if take < segment.len() - overlap { + self.mark_capped(); + return; + } + } + } + + /// Once the assembled cap is hit nothing later can ever become contiguous, + /// so the parked segments are garbage. Drop them and refuse new data, but + /// keep the assembled prefix: it is still a valid stream head and whatever + /// the protocol layer already read from it stands. + fn mark_capped(&mut self) { + self.capped = true; + self.pending = BTreeMap::new(); + self.pending_bytes = 0; + } +} + +#[cfg(test)] +mod tests { + use super::{FlowKey, PushOutcome, ReassemblyLimits, StreamReassembler}; + + fn limits() -> ReassemblyLimits { + ReassemblyLimits { + max_assembled_bytes: 64, + max_pending_bytes: 64, + max_pending_segments: 4, + } + } + + fn reasm() -> StreamReassembler { + StreamReassembler::new(limits()) + } + + #[test] + fn both_directions_share_one_key() { + let a: std::net::SocketAddr = "10.0.0.1:40000".parse().unwrap(); + let b: std::net::SocketAddr = "10.0.0.2:443".parse().unwrap(); + let (forward, fwd_dir) = FlowKey::from_pair(a, b); + let (reverse, rev_dir) = FlowKey::from_pair(b, a); + assert_eq!(forward, reverse); + assert_ne!(fwd_dir, rev_dir); + assert_eq!(fwd_dir.addresses(&forward), (a, b)); + assert_eq!(rev_dir.addresses(&reverse), (b, a)); + } + + #[test] + fn in_order_segments_concatenate() { + let mut r = reasm(); + assert_eq!(r.push(100, b"hell"), PushOutcome::Grew); + assert_eq!(r.push(104, b"o"), PushOutcome::Grew); + assert_eq!(r.data(), b"hello"); + } + + #[test] + fn out_of_order_segments_wait_for_the_gap() { + let mut r = reasm(); + assert_eq!(r.push(100, b"hell"), PushOutcome::Grew); + assert_eq!(r.push(107, b"orld"), PushOutcome::Parked); + assert_eq!(r.data(), b"hell"); + assert_eq!(r.push(104, b"o w"), PushOutcome::Grew); + assert_eq!(r.data(), b"hello world"); + } + + #[test] + fn anchor_from_syn_orders_data_arriving_backwards() { + let mut r = reasm(); + r.anchor(1000); + assert_eq!(r.push(1004, b"data"), PushOutcome::Parked); + assert_eq!(r.push(1000, b"more"), PushOutcome::Grew); + assert_eq!(r.data(), b"moredata"); + } + + #[test] + fn retransmissions_change_nothing() { + let mut r = reasm(); + r.push(100, b"abcdef"); + assert_eq!(r.push(100, b"abcdef"), PushOutcome::Unchanged); + assert_eq!(r.push(102, b"cd"), PushOutcome::Unchanged); + assert_eq!(r.data(), b"abcdef"); + } + + #[test] + fn overlapping_segment_keeps_the_first_write() { + let mut r = reasm(); + r.push(100, b"abcdef"); + assert_eq!(r.push(103, b"XXXghi"), PushOutcome::Grew); + assert_eq!(r.data(), b"abcdefghi"); + } + + #[test] + fn parked_overlap_keeps_the_first_write_too() { + let mut r = reasm(); + r.anchor(100); + assert_eq!(r.push(104, b"efgh"), PushOutcome::Parked); + assert_eq!(r.push(100, b"abcdEFG"), PushOutcome::Grew); + assert_eq!(r.data(), b"abcdEFGh"); + } + + #[test] + fn sequence_numbers_wrap_around_zero() { + let mut r = reasm(); + let anchor = u32::MAX - 1; + assert_eq!(r.push(anchor, b"ab"), PushOutcome::Grew); + assert_eq!(r.push(0, b"cd"), PushOutcome::Grew); + assert_eq!(r.push(2, b"ef"), PushOutcome::Grew); + assert_eq!(r.data(), b"abcdef"); + } + + #[test] + fn stale_pre_anchor_data_is_ignored_and_far_future_dropped() { + let mut r = reasm(); + r.push(1000, b"ab"); + assert_eq!(r.push(990, b"old"), PushOutcome::Unchanged); + assert_eq!(r.push(100_000, b"far"), PushOutcome::Dropped); + assert_eq!(r.data(), b"ab"); + } + + #[test] + fn segment_straddling_the_anchor_is_trimmed_not_lost() { + let mut r = reasm(); + r.anchor(1000); + assert_eq!(r.push(996, b"oldNEW"), PushOutcome::Grew); + assert_eq!(r.data(), b"EW"); + } + + #[test] + fn assembled_cap_truncates_but_keeps_the_prefix() { + let mut r = reasm(); + let big = vec![0x41u8; 100]; + assert_eq!(r.push(0, &big), PushOutcome::Grew); + assert_eq!(r.data().len(), limits().max_assembled_bytes); + assert_eq!(r.push(100, b"more"), PushOutcome::Dropped); + } + + #[test] + fn pending_limits_drop_excess_segments() { + let mut r = reasm(); + r.anchor(0); + assert_eq!(r.push(10, b"a"), PushOutcome::Parked); + assert_eq!(r.push(20, b"b"), PushOutcome::Parked); + assert_eq!(r.push(30, b"c"), PushOutcome::Parked); + assert_eq!(r.push(40, b"d"), PushOutcome::Parked); + assert_eq!(r.push(50, b"e"), PushOutcome::Dropped); + } + + #[test] + fn release_drops_buffers_and_refuses_data() { + let mut r = reasm(); + r.push(0, b"abc"); + r.release(); + assert!(r.released()); + assert_eq!(r.data(), b""); + assert_eq!(r.push(3, b"def"), PushOutcome::Unchanged); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs new file mode 100644 index 00000000..fc98eba6 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs @@ -0,0 +1,433 @@ +// ©AngelaMos | 2026 +// mod.rs + +//! The passive fingerprinting pipeline: frames in, fingerprint events out. +//! +//! The stages are deliberately separable. A [`PacketSource`] yields raw link +//! layer frames from a capture file or, later, a live interface. The decoder +//! strips the frame down to a TCP segment. The flow table reassembles each +//! direction of each conversation into a contiguous byte stream, surviving +//! reordering, retransmission, and overlap. The protocol layer watches each +//! stream until it recognizes a TLS flight or an HTTP request head and emits +//! fingerprints. Nothing in here touches a network interface, so the whole +//! pipeline runs byte exact in tests against vendored captures. + +pub mod decode; +pub mod event; +pub mod flow; +pub mod source; +pub mod tls; + +use std::collections::HashMap; + +use serde::Serialize; + +use crate::ja4t::ja4t; +use crate::pipeline::decode::{Skip, decode_frame}; +use crate::pipeline::event::{FingerprintEvent, StreamEvent}; +use crate::pipeline::flow::{FlowKey, PushOutcome, ReassemblyLimits, StreamReassembler}; +use crate::pipeline::source::{PacketSource, RawFrame, SourceError}; +use crate::pipeline::tls::StreamProtocol; + +/// Tuning knobs for the pipeline. +/// +/// The defaults are sized for handshake harvesting: generous enough that a +/// fat certificate chain or a multi segment ClientHello always fits, small +/// enough that an adversarial capture cannot turn the flow table into a +/// memory bomb. +#[derive(Debug, Clone, Copy)] +pub struct PipelineConfig { + /// Flows tracked at once before the table sheds its oldest entries. + pub max_flows: usize, + /// A flow untouched for this long is eligible for eviction. + pub idle_timeout_nanos: u64, + /// Contiguous bytes kept per direction. + pub max_assembled_bytes: usize, + /// Out of order bytes parked per direction. + pub max_pending_bytes: usize, + /// Out of order segments parked per direction. + pub max_pending_segments: usize, +} + +impl PipelineConfig { + pub const DEFAULT_MAX_FLOWS: usize = 65_536; + pub const DEFAULT_IDLE_TIMEOUT_NANOS: u64 = 60 * 1_000_000_000; + pub const DEFAULT_MAX_ASSEMBLED_BYTES: usize = 256 * 1024; + pub const DEFAULT_MAX_PENDING_BYTES: usize = 256 * 1024; + pub const DEFAULT_MAX_PENDING_SEGMENTS: usize = 128; + + fn limits(&self) -> ReassemblyLimits { + ReassemblyLimits { + max_assembled_bytes: self.max_assembled_bytes, + max_pending_bytes: self.max_pending_bytes, + max_pending_segments: self.max_pending_segments, + } + } +} + +impl Default for PipelineConfig { + fn default() -> Self { + Self { + max_flows: Self::DEFAULT_MAX_FLOWS, + idle_timeout_nanos: Self::DEFAULT_IDLE_TIMEOUT_NANOS, + max_assembled_bytes: Self::DEFAULT_MAX_ASSEMBLED_BYTES, + max_pending_bytes: Self::DEFAULT_MAX_PENDING_BYTES, + max_pending_segments: Self::DEFAULT_MAX_PENDING_SEGMENTS, + } + } +} + +/// What the pipeline saw, for the operator and for the miss rate honesty +/// check: a fingerprinting tool that cannot say what it failed to read is a +/// tool whose silence gets mistaken for absence. +#[derive(Debug, Default, Clone, Copy, Serialize)] +pub struct Counters { + pub frames: u64, + pub bytes: u64, + pub tcp_segments: u64, + pub skipped_unsupported_link_type: u64, + pub skipped_not_ip: u64, + pub skipped_not_tcp: u64, + pub skipped_malformed: u64, + pub flows_created: u64, + pub flows_evicted_idle: u64, + pub flows_evicted_pressure: u64, + pub segments_dropped: u64, + pub events: u64, + pub streams_capped: u64, + pub unfinished_tls_streams: u64, +} + +/// One direction of one tracked flow. +struct StreamHalf { + reassembler: StreamReassembler, + protocol: StreamProtocol, + syn_fingerprint_emitted: bool, +} + +impl StreamHalf { + fn new(limits: ReassemblyLimits) -> Self { + Self { + reassembler: StreamReassembler::new(limits), + protocol: StreamProtocol::Undecided, + syn_fingerprint_emitted: false, + } + } +} + +struct FlowState { + halves: [StreamHalf; 2], + last_seen_nanos: u64, +} + +impl FlowState { + fn new(limits: ReassemblyLimits) -> Self { + Self { + halves: [StreamHalf::new(limits), StreamHalf::new(limits)], + last_seen_nanos: 0, + } + } + + fn finished(&self) -> bool { + self.halves.iter().all(|h| h.protocol.finished()) + } +} + +/// The passive fingerprinting engine. +/// +/// Feed it frames, take events out through the sink closure. The pipeline is +/// synchronous and single threaded by design: one pipeline owns its flow +/// table outright, and running one per worker beats sharing a locked table +/// between workers. +pub struct Pipeline { + config: PipelineConfig, + flows: HashMap, + counters: Counters, +} + +impl Pipeline { + #[must_use] + pub fn new(config: PipelineConfig) -> Self { + Self { + config, + flows: HashMap::new(), + counters: Counters::default(), + } + } + + #[must_use] + pub fn counters(&self) -> &Counters { + &self.counters + } + + /// Drains a source through the pipeline, sending every event to `sink`. + pub fn run( + &mut self, + source: &mut S, + mut sink: impl FnMut(FingerprintEvent), + ) -> Result<(), SourceError> { + while let Some(frame) = source.next_frame()? { + self.feed(&frame, &mut sink); + } + self.finish(); + Ok(()) + } + + /// Processes one captured frame. + pub fn feed(&mut self, frame: &RawFrame<'_>, sink: &mut impl FnMut(FingerprintEvent)) { + self.counters.frames += 1; + self.counters.bytes += frame.data.len() as u64; + + let segment = match decode_frame(frame.link_type, frame.data) { + Ok(segment) => segment, + Err(skip) => { + match skip { + Skip::UnsupportedLinkType => { + self.counters.skipped_unsupported_link_type += 1; + } + Skip::NotIp => self.counters.skipped_not_ip += 1, + Skip::NotTcp => self.counters.skipped_not_tcp += 1, + Skip::Malformed => self.counters.skipped_malformed += 1, + } + return; + } + }; + self.counters.tcp_segments += 1; + + let (key, direction) = FlowKey::from_pair(segment.src, segment.dst); + if !self.flows.contains_key(&key) { + if self.flows.len() >= self.config.max_flows { + self.evict(frame.ts_nanos); + } + self.flows.insert(key, FlowState::new(self.config.limits())); + self.counters.flows_created += 1; + } + let Some(flow) = self.flows.get_mut(&key) else { + return; + }; + flow.last_seen_nanos = flow.last_seen_nanos.max(frame.ts_nanos); + + let (src, dst) = direction.addresses(&key); + let half = &mut flow.halves[direction.index()]; + + if !half.syn_fingerprint_emitted { + if let Some(input) = &segment.syn_fingerprint { + half.syn_fingerprint_emitted = true; + let fingerprint = ja4t(input); + let event = if segment.tcp.flags.ack() { + StreamEvent::TcpSynAck { ja4ts: fingerprint } + } else { + StreamEvent::TcpSyn { ja4t: fingerprint } + }; + self.counters.events += 1; + sink(FingerprintEvent { + ts_nanos: frame.ts_nanos, + src, + dst, + event, + }); + } + } + + if segment.tcp.flags.syn() { + half.reassembler.anchor(segment.tcp.seq.wrapping_add(1)); + } + + let payload_seq = if segment.tcp.flags.syn() { + segment.tcp.seq.wrapping_add(1) + } else { + segment.tcp.seq + }; + let outcome = half.reassembler.push(payload_seq, segment.payload); + if outcome == PushOutcome::Dropped { + self.counters.segments_dropped += 1; + } + + if outcome == PushOutcome::Grew { + let mut emitted = 0u64; + tls::advance(&mut half.protocol, half.reassembler.data(), &mut |event| { + emitted += 1; + sink(FingerprintEvent { + ts_nanos: frame.ts_nanos, + src, + dst, + event, + }); + }); + self.counters.events += emitted; + if half.protocol.finished() && !half.reassembler.released() { + half.reassembler.release(); + } + } + } + + /// Settles the books at end of capture. + /// + /// Streams that were recognized as TLS but never produced a complete + /// handshake message are counted: each one is a handshake the capture + /// clipped, which is exactly the number an operator needs before trusting + /// an absence of fingerprints. + pub fn finish(&mut self) { + for flow in self.flows.values() { + for half in &flow.halves { + if half.protocol.unfinished_tls() { + self.counters.unfinished_tls_streams += 1; + } + if half.reassembler.capped() { + self.counters.streams_capped += 1; + } + } + } + self.flows.clear(); + } + + /// Sheds flows when the table is full: everything idle past the timeout + /// or fully harvested goes, and if nothing qualifies, the single stalest + /// flow goes, so the table never refuses a brand new conversation in + /// favor of a dead one. + fn evict(&mut self, now_nanos: u64) { + let timeout = self.config.idle_timeout_nanos; + let idle: Vec = self + .flows + .iter() + .filter(|(_, flow)| { + now_nanos.saturating_sub(flow.last_seen_nanos) > timeout || flow.finished() + }) + .map(|(key, _)| *key) + .collect(); + + if idle.is_empty() { + let stalest = self + .flows + .iter() + .min_by_key(|(_, flow)| flow.last_seen_nanos) + .map(|(key, _)| *key); + if let Some(key) = stalest { + self.drop_flow(key); + self.counters.flows_evicted_pressure += 1; + } + return; + } + + for key in idle { + self.drop_flow(key); + self.counters.flows_evicted_idle += 1; + } + } + + fn drop_flow(&mut self, key: FlowKey) { + if let Some(flow) = self.flows.remove(&key) { + for half in &flow.halves { + if half.protocol.unfinished_tls() { + self.counters.unfinished_tls_streams += 1; + } + if half.reassembler.capped() { + self.counters.streams_capped += 1; + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{Pipeline, PipelineConfig}; + use crate::pipeline::event::FingerprintEvent; + use crate::pipeline::source::RawFrame; + use etherparse::PacketBuilder; + + fn tcp_frame(src: ([u8; 4], u16), dst: ([u8; 4], u16), seq: u32, payload: &[u8]) -> Vec { + let builder = PacketBuilder::ethernet2([1; 6], [2; 6]) + .ipv4(src.0, dst.0, 64) + .tcp(src.1, dst.1, seq, 64240); + let mut out = Vec::with_capacity(builder.size(payload.len())); + builder.write(&mut out, payload).unwrap(); + out + } + + fn feed_all(pipeline: &mut Pipeline, frames: &[Vec]) -> Vec { + let mut events = Vec::new(); + for (i, data) in frames.iter().enumerate() { + let frame = RawFrame { + ts_nanos: u64::try_from(i).unwrap() * 1_000_000, + link_type: 1, + data, + }; + pipeline.feed(&frame, &mut |e| events.push(e)); + } + events + } + + #[test] + fn http_request_split_across_segments_fingerprints_once() { + let client = ([10, 0, 0, 1], 40000); + let server = ([10, 0, 0, 2], 80); + let request = b"GET / HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"; + let (a, b) = request.split_at(20); + + let frames = vec![ + tcp_frame(client, server, 1000, a), + tcp_frame(client, server, 1000 + u32::try_from(a.len()).unwrap(), b), + ]; + + let mut pipeline = Pipeline::new(PipelineConfig::default()); + let events = feed_all(&mut pipeline, &frames); + assert_eq!(events.len(), 1); + assert_eq!(events[0].src.to_string(), "10.0.0.1:40000"); + assert_eq!(pipeline.counters().tcp_segments, 2); + } + + #[test] + fn out_of_order_delivery_after_a_syn_still_fingerprints() { + let client = ([10, 0, 0, 1], 40001); + let server = ([10, 0, 0, 2], 80); + let request = b"GET / HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"; + let (a, b) = request.split_at(20); + + let syn = { + let builder = PacketBuilder::ethernet2([1; 6], [2; 6]) + .ipv4(client.0, server.0, 64) + .tcp(client.1, server.1, 999, 64240) + .syn(); + let mut out = Vec::with_capacity(builder.size(0)); + builder.write(&mut out, &[]).unwrap(); + out + }; + + let frames = vec![ + syn, + tcp_frame(client, server, 1000 + u32::try_from(a.len()).unwrap(), b), + tcp_frame(client, server, 1000, a), + ]; + + let mut pipeline = Pipeline::new(PipelineConfig::default()); + let events = feed_all(&mut pipeline, &frames); + assert_eq!(events.len(), 2); + assert!(events[0].to_string().contains("tcp_syn ja4t=")); + assert!(events[1].to_string().contains("http_request")); + } + + #[test] + fn pressure_eviction_keeps_the_table_bounded() { + let config = PipelineConfig { + max_flows: 4, + ..PipelineConfig::default() + }; + let mut pipeline = Pipeline::new(config); + + let mut frames = Vec::new(); + for i in 0..8u16 { + let port = 40000 + i; + frames.push(tcp_frame( + ([10, 0, 0, 1], port), + ([10, 0, 0, 2], 80), + 1, + b"x", + )); + } + feed_all(&mut pipeline, &frames); + + assert_eq!(pipeline.counters().flows_created, 8); + assert!(pipeline.counters().flows_evicted_pressure >= 4); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/source.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/source.rs new file mode 100644 index 00000000..8a05413f --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/source.rs @@ -0,0 +1,362 @@ +// ©AngelaMos | 2026 +// source.rs + +use std::fs::File; +use std::io::Read; +use std::path::Path; + +use pcap_parser::traits::PcapReaderIterator; +use pcap_parser::{Block, PcapBlockOwned, PcapError, PcapHeader, create_reader}; +use thiserror::Error; + +/// How many bytes of buffer the file reader starts with. +/// +/// A single capture block must fit in the buffer. Offload features such as TSO +/// can put frames far larger than an MTU into a capture, so the buffer starts +/// generous and can still grow up to [`MAX_BUFFER_CAPACITY`] if a bigger block +/// appears. +const INITIAL_BUFFER_CAPACITY: usize = 1024 * 1024; + +/// The ceiling for buffer growth. A block larger than this is treated as a +/// malformed capture rather than a reason to exhaust memory. +const MAX_BUFFER_CAPACITY: usize = 64 * 1024 * 1024; + +/// Timestamp units per second when a capture does not say otherwise. +/// +/// Both the legacy pcap format and the pcapng default are microsecond +/// resolution. +const DEFAULT_UNITS_PER_SECOND: u64 = 1_000_000; + +const NANOS_PER_SECOND: u64 = 1_000_000_000; +const NANOS_PER_MICRO: u64 = 1_000; + +/// Errors produced while reading frames from a capture source. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum SourceError { + #[error("failed to read capture: {0}")] + Io(#[from] std::io::Error), + + #[error("not a pcap or pcapng capture")] + NotACapture, + + #[error("capture block exceeds the {MAX_BUFFER_CAPACITY} byte buffer ceiling")] + BlockTooLarge, + + #[error("malformed capture: {0}")] + Malformed(String), +} + +/// One link layer frame as captured, with the metadata needed to decode it. +#[derive(Debug, Clone, Copy)] +pub struct RawFrame<'src> { + /// Capture timestamp in nanoseconds since the epoch. Zero when the capture + /// format carries no timestamp for this frame. + pub ts_nanos: u64, + /// The link layer type, using the tcpdump LINKTYPE registry numbers. + pub link_type: i32, + pub data: &'src [u8], +} + +/// A source of captured frames. +/// +/// The trait is a lending iterator: each frame borrows from the source and is +/// only valid until the next call. That shape fits both file readers, which +/// hand out windows into an internal buffer, and live captures, which hand out +/// the kernel's buffer. A consumer that needs to keep a frame longer copies +/// it, and that decision stays visible at the call site. +pub trait PacketSource { + /// Returns the next frame, or `None` when the source is exhausted. + fn next_frame(&mut self) -> Result>, SourceError>; +} + +/// Per interface metadata from a pcapng interface description block. +#[derive(Debug, Clone, Copy)] +struct InterfaceInfo { + link_type: i32, + units_per_second: u64, + ts_offset_seconds: i64, +} + +/// Everything in the source except the parser, split out so the borrow of the +/// parser's buffer held by a block and the mutable borrow needed to stage a +/// frame land on different fields. +#[derive(Default)] +struct SourceState { + interfaces: Vec, + legacy: Option, + legacy_nanos: bool, + frame: Vec, + frame_ts_nanos: u64, + frame_link_type: i32, +} + +impl SourceState { + /// Copies a frame out of the parser's buffer so the borrow on the parser + /// can end before the block is consumed. + fn stage(&mut self, ts_nanos: u64, link_type: i32, data: &[u8]) { + self.frame.clear(); + self.frame.extend_from_slice(data); + self.frame_ts_nanos = ts_nanos; + self.frame_link_type = link_type; + } + + fn handle_legacy_header(&mut self, header: &PcapHeader) { + self.legacy = Some(InterfaceInfo { + link_type: header.network.0, + units_per_second: DEFAULT_UNITS_PER_SECOND, + ts_offset_seconds: 0, + }); + self.legacy_nanos = header.is_nanosecond_precision(); + } + + /// Stages a packet block. Returns false for metadata blocks. + fn handle_block(&mut self, block: &PcapBlockOwned<'_>) -> bool { + match block { + PcapBlockOwned::LegacyHeader(header) => { + self.handle_legacy_header(header); + false + } + PcapBlockOwned::Legacy(frame) => { + let Some(meta) = self.legacy else { + return false; + }; + let fraction = if self.legacy_nanos { + u64::from(frame.ts_usec) + } else { + u64::from(frame.ts_usec) * NANOS_PER_MICRO + }; + let ts = u64::from(frame.ts_sec) + .saturating_mul(NANOS_PER_SECOND) + .saturating_add(fraction); + let len = frame.data.len().min(frame.caplen as usize); + self.stage(ts, meta.link_type, &frame.data[..len]); + true + } + PcapBlockOwned::NG(Block::SectionHeader(_)) => { + self.interfaces.clear(); + false + } + PcapBlockOwned::NG(Block::InterfaceDescription(idb)) => { + self.interfaces.push(InterfaceInfo { + link_type: idb.linktype.0, + units_per_second: idb.ts_resolution().unwrap_or(DEFAULT_UNITS_PER_SECOND), + ts_offset_seconds: idb.ts_offset(), + }); + false + } + PcapBlockOwned::NG(Block::EnhancedPacket(epb)) => { + let Some(meta) = self.interfaces.get(epb.if_id as usize).copied() else { + return false; + }; + let units = (u64::from(epb.ts_high) << 32) | u64::from(epb.ts_low); + let ts = scale_to_nanos(units, meta.units_per_second, meta.ts_offset_seconds); + let len = epb.data.len().min(epb.caplen as usize); + self.stage(ts, meta.link_type, &epb.data[..len]); + true + } + PcapBlockOwned::NG(Block::SimplePacket(spb)) => { + let Some(meta) = self.interfaces.first().copied() else { + return false; + }; + let len = spb.data.len().min(spb.origlen as usize); + self.stage(0, meta.link_type, &spb.data[..len]); + true + } + PcapBlockOwned::NG(_) => false, + } + } +} + +/// Reads frames from a pcap or pcapng file. +/// +/// The two formats are probed automatically. pcapng is handled with its full +/// generality: every interface carries its own link type and timestamp +/// resolution, multiple sections reset the interface list, and metadata blocks +/// such as name resolution and decryption secrets are skipped rather than +/// treated as packets. A truncated final packet, the signature of a capture +/// that was stopped rather than closed, ends iteration cleanly and is reported +/// through [`PcapFileSource::truncated`] instead of failing the whole file. +pub struct PcapFileSource { + reader: Box, + state: SourceState, + buffer_capacity: usize, + truncated: bool, + finished: bool, +} + +impl PcapFileSource { + /// Opens a capture file from a path. + pub fn open(path: impl AsRef) -> Result { + Self::from_reader(File::open(path)?) + } + + /// Builds a source from any byte reader holding pcap or pcapng data. + pub fn from_reader(reader: impl Read + 'static) -> Result { + let reader = create_reader(INITIAL_BUFFER_CAPACITY, reader).map_err(|e| match e { + PcapError::HeaderNotRecognized | PcapError::Eof => SourceError::NotACapture, + PcapError::ReadError => SourceError::Io(std::io::Error::other("read failed")), + other => SourceError::Malformed(other.to_string()), + })?; + Ok(Self { + reader, + state: SourceState::default(), + buffer_capacity: INITIAL_BUFFER_CAPACITY, + truncated: false, + finished: false, + }) + } + + /// Returns true when the file ended in the middle of a block. + pub fn truncated(&self) -> bool { + self.truncated + } +} + +impl PacketSource for PcapFileSource { + fn next_frame(&mut self) -> Result>, SourceError> { + if self.finished { + return Ok(None); + } + loop { + let staged = match self.reader.next() { + Ok((offset, block)) => { + let staged = self.state.handle_block(&block); + self.reader.consume(offset); + staged + } + Err(PcapError::Eof) => { + self.finished = true; + return Ok(None); + } + Err(PcapError::UnexpectedEof) => { + self.finished = true; + self.truncated = true; + return Ok(None); + } + Err(PcapError::Incomplete(_)) => { + self.reader + .refill() + .map_err(|e| SourceError::Malformed(e.to_string()))?; + continue; + } + Err(PcapError::BufferTooSmall) => { + let grown = self.buffer_capacity.saturating_mul(2); + if grown > MAX_BUFFER_CAPACITY || !self.reader.grow(grown) { + self.finished = true; + return Err(SourceError::BlockTooLarge); + } + self.buffer_capacity = grown; + continue; + } + Err(PcapError::ReadError) => { + self.finished = true; + return Err(SourceError::Io(std::io::Error::other("read failed"))); + } + Err( + e @ (PcapError::HeaderNotRecognized + | PcapError::NomError(..) + | PcapError::OwnedNomError(..)), + ) => { + self.finished = true; + return Err(SourceError::Malformed(e.to_string())); + } + }; + if staged { + return Ok(Some(RawFrame { + ts_nanos: self.state.frame_ts_nanos, + link_type: self.state.frame_link_type, + data: &self.state.frame, + })); + } + } + } +} + +/// Converts a timestamp in interface units to nanoseconds since the epoch. +/// +/// The arithmetic runs in 128 bits so the conversion stays exact for every +/// resolution pcapng can express, including nanosecond counts that already +/// fill most of a u64. +fn scale_to_nanos(units: u64, units_per_second: u64, offset_seconds: i64) -> u64 { + if units_per_second == 0 { + return 0; + } + let nanos = u128::from(units) * u128::from(NANOS_PER_SECOND) / u128::from(units_per_second); + let offset = i128::from(offset_seconds) * i128::from(NANOS_PER_SECOND); + u64::try_from( + i128::try_from(nanos) + .unwrap_or(i128::MAX) + .saturating_add(offset), + ) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::{PacketSource, PcapFileSource, SourceError, scale_to_nanos}; + + fn legacy_pcap(frames: &[&[u8]]) -> Vec { + let mut v = Vec::new(); + v.extend_from_slice(&0xa1b2_c3d4_u32.to_le_bytes()); + v.extend_from_slice(&2u16.to_le_bytes()); + v.extend_from_slice(&4u16.to_le_bytes()); + v.extend_from_slice(&0i32.to_le_bytes()); + v.extend_from_slice(&0u32.to_le_bytes()); + v.extend_from_slice(&65535u32.to_le_bytes()); + v.extend_from_slice(&1u32.to_le_bytes()); + for (i, frame) in frames.iter().enumerate() { + let len = u32::try_from(frame.len()).unwrap(); + v.extend_from_slice(&u32::try_from(i + 1).unwrap().to_le_bytes()); + v.extend_from_slice(&500_000u32.to_le_bytes()); + v.extend_from_slice(&len.to_le_bytes()); + v.extend_from_slice(&len.to_le_bytes()); + v.extend_from_slice(frame); + } + v + } + + #[test] + fn reads_legacy_frames_with_timestamps() { + let data = legacy_pcap(&[&[0xaa; 14], &[0xbb; 20]]); + let mut source = PcapFileSource::from_reader(std::io::Cursor::new(data)).unwrap(); + + let one = source.next_frame().unwrap().unwrap(); + assert_eq!(one.link_type, 1); + assert_eq!(one.ts_nanos, 1_500_000_000); + assert_eq!(one.data.len(), 14); + + let two = source.next_frame().unwrap().unwrap(); + assert_eq!(two.data, &[0xbb; 20]); + + assert!(source.next_frame().unwrap().is_none()); + assert!(!source.truncated()); + } + + #[test] + fn truncated_final_frame_ends_cleanly() { + let mut data = legacy_pcap(&[&[0xaa; 14], &[0xbb; 20]]); + data.truncate(data.len() - 5); + let mut source = PcapFileSource::from_reader(std::io::Cursor::new(data)).unwrap(); + + assert!(source.next_frame().unwrap().is_some()); + assert!(source.next_frame().unwrap().is_none()); + assert!(source.truncated()); + assert!(source.next_frame().unwrap().is_none()); + } + + #[test] + fn garbage_is_not_a_capture() { + let err = PcapFileSource::from_reader(std::io::Cursor::new(vec![0x55; 64])) + .err() + .unwrap(); + assert!(matches!(err, SourceError::NotACapture)); + } + + #[test] + fn timestamp_scaling_is_exact_for_common_resolutions() { + assert_eq!(scale_to_nanos(1_500_000, 1_000_000, 0), 1_500_000_000); + assert_eq!(scale_to_nanos(7, 1_000_000_000, 0), 7); + assert_eq!(scale_to_nanos(1, 1, 1), 2_000_000_000); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/tls.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/tls.rs new file mode 100644 index 00000000..9f242408 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/tls.rs @@ -0,0 +1,401 @@ +// ©AngelaMos | 2026 +// tls.rs + +use std::borrow::Cow; + +use crate::ja3::{ja3, ja3_string, ja3s, ja3s_string}; +use crate::ja4::{Transport, ja4, ja4s}; +use crate::ja4h::{ja4h, parse_http_request}; +use crate::ja4x::ja4x; +use crate::parse::reader::Reader; +use crate::parse::{ + certificate_der_list, is_sslv2_client_hello, parse_client_hello, parse_server_hello, + parse_sslv2_client_hello, +}; +use crate::pipeline::event::StreamEvent; +use crate::registry::{content_type, handshake_type, version}; + +/// How many bytes the sniffer needs before it gives up on classifying a +/// stream. Every protocol this pipeline recognizes shows its hand within the +/// first few bytes; eight covers the longest HTTP method prefix. +const SNIFF_DECISION_LEN: usize = 8; + +/// The HTTP methods the sniffer accepts as the start of a cleartext request. +const HTTP_METHOD_PREFIXES: [&[u8]; 9] = [ + b"GET ", + b"POST ", + b"PUT ", + b"HEAD ", + b"DELETE ", + b"OPTIONS ", + b"PATCH ", + b"TRACE ", + b"CONNECT ", +]; + +/// A stream whose HTTP request head has not finished inside this many bytes +/// is not worth waiting on. +const HTTP_HEAD_CAP: usize = 8 * 1024; + +const HTTP_HEAD_TERMINATOR: &[u8] = b"\r\n\r\n"; + +/// A TLS record payload cannot exceed 2^14 plus expansion; RFC 8446 allows +/// 255 bytes of expansion on top of the 16384 byte plaintext limit. A length +/// beyond that means the stream is not actually TLS record framing. +const MAX_TLS_RECORD_LEN: usize = 16384 + 255; + +/// What one direction of a flow is, as far as the protocol layer can tell. +#[derive(Debug)] +pub enum StreamProtocol { + /// Not enough bytes yet to say. + Undecided, + /// TLS record framing; the cleartext first flight is being extracted. + Tls(TlsFlight), + /// A cleartext HTTP/1.x request head is being accumulated. + Http, + /// Recognized and fully harvested; the stream needs no more buffering. + Done, + /// Unrecognized or unparseable; the stream is ignored. + Ignored, +} + +impl StreamProtocol { + /// True when this direction will never produce another event, which is + /// the signal to drop its reassembly buffers. + #[must_use] + pub fn finished(&self) -> bool { + matches!(self, StreamProtocol::Done | StreamProtocol::Ignored) + } + + /// True when the stream was recognized as TLS but the capture ended + /// before a complete hello could be read. Feeds the miss rate counter + /// that tells an operator their capture is clipping handshakes. + #[must_use] + pub fn unfinished_tls(&self) -> bool { + match self { + StreamProtocol::Tls(flight) => !flight.saw_any_message, + _ => false, + } + } +} + +/// Incremental extraction state for one direction's cleartext TLS flight. +/// +/// Only what must survive between walks lives here. A ClientHello ends its +/// direction immediately, so it needs no flag; a ServerHello does not, since +/// a TLS 1.2 Certificate may still be in flight behind it, so the emission +/// guard for it persists. +#[derive(Debug, Default)] +pub struct TlsFlight { + emitted_server_hello: bool, + saw_any_message: bool, +} + +/// Drives protocol detection and extraction over one direction of a stream. +/// +/// `stream` is always the full contiguous bytes from the start of the +/// direction; the extractor re-walks them on each call. That sounds wasteful +/// and is not: the walk is linear over at most the reassembly cap, the +/// interesting messages sit in the first packets, and re-walking from the +/// start is what makes a message that arrives split across three segments +/// parse correctly with no incremental parser state to get wrong. +/// +/// Returns events through `sink` and updates `proto` in place. +pub fn advance(proto: &mut StreamProtocol, stream: &[u8], sink: &mut impl FnMut(StreamEvent)) { + if matches!(proto, StreamProtocol::Undecided) { + *proto = sniff(stream); + } + match proto { + StreamProtocol::Undecided | StreamProtocol::Done | StreamProtocol::Ignored => {} + StreamProtocol::Tls(_) => advance_tls(proto, stream, sink), + StreamProtocol::Http => advance_http(proto, stream, sink), + } +} + +/// Classifies the first bytes of a stream. +fn sniff(stream: &[u8]) -> StreamProtocol { + if stream.len() >= 3 { + if stream[0] == content_type::HANDSHAKE && stream[1] == 0x03 && stream[2] <= 0x04 { + return StreamProtocol::Tls(TlsFlight::default()); + } + if is_sslv2_client_hello(stream) { + return StreamProtocol::Tls(TlsFlight::default()); + } + } + if stream.len() >= SNIFF_DECISION_LEN { + if HTTP_METHOD_PREFIXES.iter().any(|m| stream.starts_with(m)) { + return StreamProtocol::Http; + } + return StreamProtocol::Ignored; + } + StreamProtocol::Undecided +} + +fn advance_http(proto: &mut StreamProtocol, stream: &[u8], sink: &mut impl FnMut(StreamEvent)) { + let head_end = stream + .windows(HTTP_HEAD_TERMINATOR.len()) + .position(|w| w == HTTP_HEAD_TERMINATOR); + let Some(head_end) = head_end else { + if stream.len() > HTTP_HEAD_CAP { + *proto = StreamProtocol::Ignored; + } + return; + }; + + let head = &stream[..head_end + HTTP_HEAD_TERMINATOR.len()]; + if let Some(request) = parse_http_request(head) { + let host = request + .headers + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case("host")) + .map(|(_, value)| value.clone()); + sink(StreamEvent::HttpRequest { + ja4h: ja4h(&request), + method: request.method.clone(), + host, + }); + *proto = StreamProtocol::Done; + } else { + *proto = StreamProtocol::Ignored; + } +} + +fn advance_tls(proto: &mut StreamProtocol, stream: &[u8], sink: &mut impl FnMut(StreamEvent)) { + if is_sslv2_client_hello(stream) { + advance_sslv2(proto, stream, sink); + return; + } + + let Some(flight_bytes) = collect_flight(stream) else { + *proto = StreamProtocol::Ignored; + return; + }; + + let StreamProtocol::Tls(flight) = proto else { + return; + }; + let mut done = walk_messages(flight, flight_bytes.handshake.as_ref(), sink); + if flight_bytes.flight_closed && !done { + done = true; + } + if done { + *proto = StreamProtocol::Done; + } +} + +fn advance_sslv2(proto: &mut StreamProtocol, stream: &[u8], sink: &mut impl FnMut(StreamEvent)) { + match parse_sslv2_client_hello(stream) { + Ok(hello) => { + sink(StreamEvent::ClientHello { + ja3: ja3(&hello), + ja3_raw: ja3_string(&hello), + ja4: ja4(&hello, Transport::Tcp), + sni: None, + alpn: None, + }); + *proto = StreamProtocol::Done; + } + Err(crate::error::ParseError::Truncated { .. }) => {} + Err(_) => *proto = StreamProtocol::Ignored, + } +} + +struct FlightBytes<'stream> { + handshake: Cow<'stream, [u8]>, + /// True when a non handshake record followed the handshake records, which + /// in cleartext TLS means the readable part of the flight is over. + flight_closed: bool, +} + +/// Collects the payloads of the leading complete handshake records. +/// +/// Unlike the strict reassembled flight walker in the parse module, this +/// tolerates a trailing partial record, because the stream is still growing. +/// Returns `None` when the bytes stop looking like TLS record framing at all. +fn collect_flight(stream: &[u8]) -> Option> { + let mut segments: Vec<&[u8]> = Vec::new(); + let mut r = Reader::new(stream); + let mut flight_closed = false; + + while r.remaining() >= 5 { + let Ok(ctype) = r.u8() else { break }; + let Ok(record_version) = r.u16() else { break }; + if !matches!( + ctype, + content_type::CHANGE_CIPHER_SPEC + | content_type::ALERT + | content_type::HANDSHAKE + | content_type::APPLICATION_DATA + ) { + return None; + } + if (record_version & 0xff00) != 0x0300 || (record_version & 0x00ff) > 0x04 { + return None; + } + let Ok(declared) = r.u16() else { break }; + if declared as usize > MAX_TLS_RECORD_LEN { + return None; + } + let Ok(payload) = r.take(declared as usize) else { + break; + }; + if ctype == content_type::HANDSHAKE { + segments.push(payload); + } else if !segments.is_empty() { + flight_closed = true; + break; + } + } + + let handshake = match segments.as_slice() { + [] => Cow::Borrowed(&[][..]), + [only] => Cow::Borrowed(*only), + many => { + let mut joined = Vec::with_capacity(many.iter().map(|s| s.len()).sum()); + for seg in many { + joined.extend_from_slice(seg); + } + Cow::Owned(joined) + } + }; + Some(FlightBytes { + handshake, + flight_closed, + }) +} + +/// Walks the complete handshake messages in the flight, emitting fingerprints +/// for the ones that carry them. Returns true when this direction has yielded +/// everything it ever will. +fn walk_messages( + flight: &mut TlsFlight, + handshake: &[u8], + sink: &mut impl FnMut(StreamEvent), +) -> bool { + let mut r = Reader::new(handshake); + while r.remaining() >= 4 { + let Ok(msg_type) = r.u8() else { break }; + let Ok(len) = r.u24() else { break }; + let Ok(body) = r.take(len as usize) else { + break; + }; + flight.saw_any_message = true; + + match msg_type { + handshake_type::CLIENT_HELLO => { + if let Ok(hello) = parse_client_hello(body) { + sink(StreamEvent::ClientHello { + ja3: ja3(&hello), + ja3_raw: ja3_string(&hello), + ja4: ja4(&hello, Transport::Tcp), + sni: hello.server_name().map(str::to_owned), + alpn: hello + .alpn_protocols() + .first() + .map(|p| String::from_utf8_lossy(p).into_owned()), + }); + } + return true; + } + handshake_type::SERVER_HELLO => { + if !flight.emitted_server_hello { + if let Ok(hello) = parse_server_hello(body) { + flight.emitted_server_hello = true; + let negotiated_tls13 = hello.selected_version() == version::TLS_1_3; + sink(StreamEvent::ServerHello { + ja3s: ja3s(&hello), + ja3s_raw: ja3s_string(&hello), + ja4s: ja4s(&hello, Transport::Tcp), + }); + if negotiated_tls13 { + return true; + } + } + } + } + handshake_type::CERTIFICATE => { + if let Ok(certs) = certificate_der_list(body) { + for cert in certs { + if let Ok(fingerprint) = ja4x(cert) { + sink(StreamEvent::Certificate { ja4x: fingerprint }); + } + } + } + return true; + } + _ => {} + } + } + false +} + +#[cfg(test)] +mod tests { + use super::{StreamProtocol, advance, sniff}; + use crate::pipeline::event::StreamEvent; + + fn record(payload: &[u8]) -> Vec { + let mut v = vec![0x16, 0x03, 0x01]; + v.extend_from_slice(&u16::try_from(payload.len()).unwrap().to_be_bytes()); + v.extend_from_slice(payload); + v + } + + #[test] + fn sniffs_tls_http_and_garbage() { + assert!(matches!( + sniff(&[0x16, 0x03, 0x01, 0x00, 0x05]), + StreamProtocol::Tls(_) + )); + assert!(matches!(sniff(b"GET / HTTP/1.1\r\n"), StreamProtocol::Http)); + assert!(matches!( + sniff(b"SSH-2.0-OpenSSH_9.7"), + StreamProtocol::Ignored + )); + assert!(matches!(sniff(b"GE"), StreamProtocol::Undecided)); + } + + #[test] + fn http_request_yields_ja4h_once_head_completes() { + let mut proto = StreamProtocol::Undecided; + let mut events = Vec::new(); + + let partial = b"GET / HTTP/1.1\r\nHost: example.com\r\n"; + advance(&mut proto, partial, &mut |e| events.push(e)); + assert!(events.is_empty()); + assert!(matches!(proto, StreamProtocol::Http)); + + let full = b"GET / HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"; + advance(&mut proto, full, &mut |e| events.push(e)); + assert_eq!(events.len(), 1); + assert!(matches!( + &events[0], + StreamEvent::HttpRequest { method, host: Some(h), .. } + if method == "GET" && h == "example.com" + )); + assert!(proto.finished()); + } + + #[test] + fn partial_tls_record_waits_for_more_bytes() { + let mut proto = StreamProtocol::Undecided; + let mut events = Vec::new(); + + let full = record(&[0x01, 0x00, 0x00, 0x02, 0xaa, 0xbb]); + advance(&mut proto, &full[..7], &mut |e| events.push(e)); + assert!(events.is_empty()); + assert!(matches!(proto, StreamProtocol::Tls(_))); + assert!(!proto.finished()); + } + + #[test] + fn nonsense_record_length_poisons_the_stream() { + let mut proto = StreamProtocol::Undecided; + let mut events = Vec::new(); + + let stream = [0x16, 0x03, 0x01, 0xff, 0xff, 0x00, 0x00, 0x00]; + advance(&mut proto, &stream, &mut |e| events.push(e)); + assert!(matches!(proto, StreamProtocol::Ignored)); + assert!(events.is_empty()); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/registry.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/registry.rs new file mode 100644 index 00000000..bd6af809 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/registry.rs @@ -0,0 +1,91 @@ +// ©AngelaMos | 2026 +// registry.rs + +//! Named constants for the slices of the TLS and DTLS registries that the +//! fingerprint algorithms reference by number. +//! +//! Wire values are kept as raw `u16` everywhere in this crate rather than being +//! decoded into a closed enum. A fingerprinting engine must preserve cipher and +//! extension values it has never seen, because a future RFC value or a vendor +//! specific extension is exactly the kind of detail that makes one client +//! distinguishable from another. Decoding into a closed enum would collapse all +//! unknown values into a single bucket and corrupt the fingerprint. + +/// TLS and DTLS record content types (the first byte of a record). +pub mod content_type { + pub const CHANGE_CIPHER_SPEC: u8 = 20; + pub const ALERT: u8 = 21; + pub const HANDSHAKE: u8 = 22; + pub const APPLICATION_DATA: u8 = 23; +} + +/// Handshake message types (the first byte of a handshake message body). +pub mod handshake_type { + pub const CLIENT_HELLO: u8 = 1; + pub const SERVER_HELLO: u8 = 2; + pub const CERTIFICATE: u8 = 11; +} + +/// Extension type numbers that the fingerprint algorithms treat specially. +pub mod extension { + pub const SERVER_NAME: u16 = 0x0000; + pub const SUPPORTED_GROUPS: u16 = 0x000a; + pub const EC_POINT_FORMATS: u16 = 0x000b; + pub const SIGNATURE_ALGORITHMS: u16 = 0x000d; + pub const ALPN: u16 = 0x0010; + pub const SUPPORTED_VERSIONS: u16 = 0x002b; +} + +/// Legacy and negotiated protocol version words. +pub mod version { + pub const SSL_2_0: u16 = 0x0002; + pub const SSL_3_0: u16 = 0x0300; + pub const TLS_1_0: u16 = 0x0301; + pub const TLS_1_1: u16 = 0x0302; + pub const TLS_1_2: u16 = 0x0303; + pub const TLS_1_3: u16 = 0x0304; + pub const DTLS_1_0: u16 = 0xfeff; + pub const DTLS_1_2: u16 = 0xfefd; + pub const DTLS_1_3: u16 = 0xfefc; +} + +/// Maps a protocol version word to the two character JA4 version code. +/// +/// The mapping is taken verbatim from the FoxIO JA4 specification. Unknown +/// words collapse to `00`, which is the specified fallback. DTLS words are +/// included even though the Python reference omits them, because the published +/// specification lists them and the Wireshark and Rust references honor them. +#[must_use] +pub fn ja4_version_code(word: u16) -> &'static str { + match word { + version::TLS_1_3 => "13", + version::TLS_1_2 => "12", + version::TLS_1_1 => "11", + version::TLS_1_0 => "10", + version::SSL_3_0 => "s3", + version::SSL_2_0 => "s2", + version::DTLS_1_0 => "d1", + version::DTLS_1_2 => "d2", + version::DTLS_1_3 => "d3", + _ => "00", + } +} + +#[cfg(test)] +mod tests { + use super::ja4_version_code; + use super::version; + + #[test] + fn known_versions_map() { + assert_eq!(ja4_version_code(version::TLS_1_3), "13"); + assert_eq!(ja4_version_code(version::TLS_1_2), "12"); + assert_eq!(ja4_version_code(version::SSL_3_0), "s3"); + assert_eq!(ja4_version_code(version::DTLS_1_2), "d2"); + } + + #[test] + fn unknown_version_falls_back() { + assert_eq!(ja4_version_code(0x7f1d), "00"); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/common/mod.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/common/mod.rs new file mode 100644 index 00000000..3bdf1939 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/common/mod.rs @@ -0,0 +1,161 @@ +// ©AngelaMos | 2026 +// mod.rs + +//! Builders that assemble TLS handshake bytes for tests. +//! +//! Hand assembling wire bytes keeps the fingerprint tests honest. Rather than +//! trusting a higher level library to produce a ClientHello, the tests state the +//! exact cipher list, extension list, and extension bodies, then assert that the +//! parser and the fingerprint algorithms read them back the way the +//! specifications require. +//! +//! Each integration test binary links this module independently, so a builder +//! method that one binary does not call reads as dead code there even though +//! another binary uses it. The allow keeps that shared infrastructure honest +//! without scattering per method annotations. +#![allow(dead_code)] + +/// Assembles a ClientHello handshake message body. +#[derive(Default)] +pub struct ClientHelloBuilder { + legacy_version: u16, + cipher_suites: Vec, + extensions: Vec<(u16, Vec)>, +} + +impl ClientHelloBuilder { + pub fn new() -> Self { + Self { + legacy_version: 0x0303, + ..Self::default() + } + } + + pub fn legacy_version(mut self, v: u16) -> Self { + self.legacy_version = v; + self + } + + pub fn ciphers(mut self, suites: &[u16]) -> Self { + self.cipher_suites = suites.to_vec(); + self + } + + pub fn extension(mut self, ext_type: u16, data: Vec) -> Self { + self.extensions.push((ext_type, data)); + self + } + + /// Adds a server name indication extension for the given host. + pub fn sni(self, host: &str) -> Self { + let host = host.as_bytes(); + let mut entry = vec![0u8]; + push_u16_vec(&mut entry, host); + let mut data = Vec::new(); + push_u16_vec(&mut data, &entry); + self.extension(0x0000, data) + } + + /// Adds a supported groups extension. + pub fn supported_groups(self, groups: &[u16]) -> Self { + let mut list = Vec::new(); + for g in groups { + list.extend_from_slice(&g.to_be_bytes()); + } + let mut data = Vec::new(); + push_u16_vec(&mut data, &list); + self.extension(0x000a, data) + } + + /// Adds an elliptic curve point formats extension. + pub fn ec_point_formats(self, formats: &[u8]) -> Self { + let mut data = Vec::new(); + push_u8_vec(&mut data, formats); + self.extension(0x000b, data) + } + + /// Adds a signature algorithms extension. + pub fn signature_algorithms(self, algs: &[u16]) -> Self { + let mut list = Vec::new(); + for a in algs { + list.extend_from_slice(&a.to_be_bytes()); + } + let mut data = Vec::new(); + push_u16_vec(&mut data, &list); + self.extension(0x000d, data) + } + + /// Adds an ALPN extension advertising the given protocols. + pub fn alpn(self, protos: &[&[u8]]) -> Self { + let mut list = Vec::new(); + for p in protos { + push_u8_vec(&mut list, p); + } + let mut data = Vec::new(); + push_u16_vec(&mut data, &list); + self.extension(0x0010, data) + } + + /// Adds a supported versions extension. + pub fn supported_versions(self, versions: &[u16]) -> Self { + let mut list = Vec::new(); + for v in versions { + list.extend_from_slice(&v.to_be_bytes()); + } + let mut data = Vec::new(); + push_u8_vec(&mut data, &list); + self.extension(0x002b, data) + } + + /// Returns the handshake message body, the input to `parse_client_hello`. + pub fn build_body(&self) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&self.legacy_version.to_be_bytes()); + body.extend_from_slice(&[0u8; 32]); + body.push(0); + + let mut ciphers = Vec::new(); + for c in &self.cipher_suites { + ciphers.extend_from_slice(&c.to_be_bytes()); + } + push_u16_vec(&mut body, &ciphers); + + push_u8_vec(&mut body, &[0]); + + let mut exts = Vec::new(); + for (ext_type, data) in &self.extensions { + exts.extend_from_slice(&ext_type.to_be_bytes()); + push_u16_vec(&mut exts, data); + } + push_u16_vec(&mut body, &exts); + body + } + + /// Wraps the handshake body in a handshake header and a TLS record so the + /// result is a complete record stream. + pub fn build_record(&self) -> Vec { + let body = self.build_body(); + let mut msg = vec![1u8]; + push_u24_vec(&mut msg, &body); + + let mut record = vec![22u8, 0x03, 0x01]; + push_u16_vec(&mut record, &msg); + record + } +} + +fn push_u8_vec(out: &mut Vec, data: &[u8]) { + out.push(u8::try_from(data.len()).unwrap()); + out.extend_from_slice(data); +} + +fn push_u16_vec(out: &mut Vec, data: &[u8]) { + out.extend_from_slice(&u16::try_from(data.len()).unwrap().to_be_bytes()); + out.extend_from_slice(data); +} + +fn push_u24_vec(out: &mut Vec, data: &[u8]) { + let len = u32::try_from(data.len()).unwrap(); + out.extend_from_slice(&len.to_be_bytes()[1..]); + out.extend_from_slice(data); +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja3.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja3.rs new file mode 100644 index 00000000..2e734a78 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja3.rs @@ -0,0 +1,55 @@ +// ©AngelaMos | 2026 +// ja3.rs + +mod common; + +use common::ClientHelloBuilder; +use tlsfp_core::ja3::{ja3, ja3_string}; +use tlsfp_core::parse::parse_client_hello; + +/// Rebuilds the exact ClientHello that produces the first published Salesforce +/// JA3 vector, then checks both the pre hash string and the digest end to end. +/// The SNI body content is arbitrary because JA3 records only that extension +/// type zero was present, not its value. +#[test] +fn end_to_end_matches_salesforce_vector_one() { + let body = ClientHelloBuilder::new() + .legacy_version(0x0301) + .ciphers(&[47, 53, 5, 10, 49161, 49162, 49171, 49172, 50, 56, 19, 4]) + .extension(0x0000, vec![0x00, 0x00]) + .supported_groups(&[23, 24, 25]) + .ec_point_formats(&[0]) + .build_body(); + + let ch = parse_client_hello(&body).unwrap(); + assert_eq!( + ja3_string(&ch), + "769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0" + ); + assert_eq!(ja3(&ch).to_string(), "ada70206e40642a3e4461f35503241d5"); +} + +/// A ClientHello with GREASE in ciphers, extensions, and supported groups must +/// produce the same JA3 as the same hello without GREASE, because GREASE is +/// stripped from every list field before hashing. +#[test] +fn grease_does_not_change_the_fingerprint() { + let clean = ClientHelloBuilder::new() + .legacy_version(0x0303) + .ciphers(&[0x1301, 0x1302]) + .supported_groups(&[0x001d, 0x0017]) + .ec_point_formats(&[0]) + .build_body(); + + let greasy = ClientHelloBuilder::new() + .legacy_version(0x0303) + .ciphers(&[0x0a0a, 0x1301, 0x1302]) + .extension(0x1a1a, vec![]) + .supported_groups(&[0x2a2a, 0x001d, 0x0017]) + .ec_point_formats(&[0]) + .build_body(); + + let clean = parse_client_hello(&clean).unwrap(); + let greasy = parse_client_hello(&greasy).unwrap(); + assert_eq!(ja3(&clean), ja3(&greasy)); +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4.rs new file mode 100644 index 00000000..d1520269 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4.rs @@ -0,0 +1,88 @@ +// ©AngelaMos | 2026 +// ja4.rs + +mod common; + +use common::ClientHelloBuilder; +use tlsfp_core::ja4::{Transport, ja4}; +use tlsfp_core::parse::parse_client_hello; + +const FOXIO_HASH: &str = "t13d1516h2_8daaf6152771_e5627efa2ab1"; +const FOXIO_RAW: &str = "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"; + +const CIPHERS_ORIGINAL_ORDER: [u16; 15] = [ + 0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030, 0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, + 0x009d, 0x002f, 0x0035, +]; + +const OPAQUE_EXTENSIONS: [u16; 10] = [ + 0x0005, 0x0012, 0x0015, 0x0017, 0x001b, 0x0023, 0x002d, 0x0033, 0x4469, 0xff01, +]; + +const SIG_ALGS: [u16; 8] = [ + 0x0403, 0x0804, 0x0401, 0x0503, 0x0805, 0x0501, 0x0806, 0x0601, +]; + +/// Assembles the handshake body for the canonical FoxIO example. +/// +/// The cipher suites are supplied out of order with an injected GREASE value, to +/// prove that sorting and GREASE removal happen before hashing. When `reversed` +/// is set the opaque extensions are added in reverse, which must not change the +/// fingerprint because JA4 sorts the extension list. +fn foxio_body(reversed: bool) -> Vec { + let mut ciphers = vec![0x0a0a]; + ciphers.extend_from_slice(&CIPHERS_ORIGINAL_ORDER); + + let mut builder = ClientHelloBuilder::new() + .ciphers(&ciphers) + .sni("example.com") + .supported_groups(&[0x001d, 0x0017]) + .ec_point_formats(&[0x00]) + .signature_algorithms(&SIG_ALGS) + .alpn(&[b"h2"]) + .supported_versions(&[0x2a2a, 0x0304, 0x0303]); + + if reversed { + for ext in OPAQUE_EXTENSIONS.iter().rev() { + builder = builder.extension(*ext, vec![]); + } + } else { + for ext in OPAQUE_EXTENSIONS { + builder = builder.extension(ext, vec![]); + } + } + + builder.build_body() +} + +#[test] +fn reproduces_foxio_canonical_example() { + let body = foxio_body(false); + let ch = parse_client_hello(&body).unwrap(); + let fp = ja4(&ch, Transport::Tcp); + assert_eq!(fp.raw, FOXIO_RAW); + assert_eq!(fp.hash, FOXIO_HASH); +} + +#[test] +fn extension_order_does_not_change_the_hash() { + let normal = foxio_body(false); + let reversed = foxio_body(true); + let normal = parse_client_hello(&normal).unwrap(); + let reversed = parse_client_hello(&reversed).unwrap(); + assert_eq!( + ja4(&normal, Transport::Tcp).hash, + ja4(&reversed, Transport::Tcp).hash + ); +} + +#[test] +fn transport_marker_switches_first_character() { + let body = foxio_body(false); + let ch = parse_client_hello(&body).unwrap(); + let tcp = ja4(&ch, Transport::Tcp); + let quic = ja4(&ch, Transport::Quic); + assert!(tcp.hash.starts_with('t')); + assert!(quic.hash.starts_with('q')); + assert_eq!(&tcp.hash[1..], &quic.hash[1..]); +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4_suite.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4_suite.rs new file mode 100644 index 00000000..be34d979 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4_suite.rs @@ -0,0 +1,64 @@ +// ©AngelaMos | 2026 +// ja4_suite.rs + +use tlsfp_core::ja4::{Transport, ja4s}; +use tlsfp_core::ja4h::{ja4h, parse_http_request}; +use tlsfp_core::parse::parse_server_hello; + +/// Builds the ServerHello behind the FoxIO JA4S example: TLS 1.3 selected via the +/// supported versions extension, the cipher 0x1301, and two extensions in the +/// order key share then supported versions. +fn foxio_server_hello() -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&0x0303u16.to_be_bytes()); + body.extend_from_slice(&[0u8; 32]); + body.push(0); + body.extend_from_slice(&0x1301u16.to_be_bytes()); + body.push(0); + + let mut exts = Vec::new(); + exts.extend_from_slice(&0x0033u16.to_be_bytes()); + exts.extend_from_slice(&0u16.to_be_bytes()); + exts.extend_from_slice(&0x002bu16.to_be_bytes()); + exts.extend_from_slice(&2u16.to_be_bytes()); + exts.extend_from_slice(&0x0304u16.to_be_bytes()); + + body.extend_from_slice(&u16::try_from(exts.len()).unwrap().to_be_bytes()); + body.extend_from_slice(&exts); + body +} + +#[test] +fn ja4s_reproduces_foxio_example() { + let body = foxio_server_hello(); + let sh = parse_server_hello(&body).unwrap(); + let fp = ja4s(&sh, Transport::Tcp); + assert_eq!(fp.hash, "t130200_1301_234ea6891581"); + assert_eq!(fp.raw, "t130200_1301_0033,002b"); +} + +/// The published JA4H example for a request with four uppercase headers, no +/// cookies, no referer, and no accept language. The header hash is the SHA256 of +/// the comma joined header names in wire order. +#[test] +fn ja4h_reproduces_published_example() { + let raw = b"GET * HTTP/1.1\r\nHOST: a\r\nMAN: b\r\nMX: c\r\nST: d\r\n\r\n"; + let req = parse_http_request(raw).unwrap(); + let fp = ja4h(&req); + assert_eq!( + fp.hash, + "ge11nn040000_a3c882e23515_000000000000_000000000000" + ); +} + +/// Cookies and a referer must flip their flags and stop being counted as plain +/// headers, while the accept language must populate the language characters. +#[test] +fn ja4h_flags_cookies_referer_and_language() { + let raw = b"GET / HTTP/1.1\r\nHost: x\r\nAccept-Language: en-US,en;q=0.9\r\nReferer: http://x\r\nCookie: a=1; b=2\r\n\r\n"; + let req = parse_http_request(raw).unwrap(); + let fp = ja4h(&req); + let prefix = fp.hash.split('_').next().unwrap(); + assert_eq!(prefix, "ge11cr02enus"); + assert!(!fp.hash.contains("_000000000000_000000000000")); +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/parse.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/parse.rs new file mode 100644 index 00000000..5d1dcf3c --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/parse.rs @@ -0,0 +1,105 @@ +// ©AngelaMos | 2026 +// parse.rs + +mod common; + +use common::ClientHelloBuilder; +use proptest::prelude::*; +use tlsfp_core::parse::{ + first_handshake_message, handshake_bytes, parse_client_hello, parse_server_hello, +}; +use tlsfp_core::registry::handshake_type; + +fn sample() -> ClientHelloBuilder { + ClientHelloBuilder::new() + .ciphers(&[0x0a0a, 0x1301, 0x1302, 0x1303, 0xc02b]) + .sni("example.com") + .supported_groups(&[0x1a1a, 0x001d, 0x0017]) + .ec_point_formats(&[0x00]) + .signature_algorithms(&[0x0403, 0x0804, 0x0401]) + .alpn(&[b"h2", b"http/1.1"]) + .supported_versions(&[0x2a2a, 0x0304, 0x0303]) +} + +#[test] +fn parses_every_field_a_fingerprint_reads() { + let body = sample().build_body(); + let ch = parse_client_hello(&body).unwrap(); + + assert_eq!(ch.legacy_version, 0x0303); + assert_eq!( + ch.cipher_suites.as_slice(), + &[0x0a0a, 0x1301, 0x1302, 0x1303, 0xc02b] + ); + assert_eq!(ch.extensions.len(), 6); + assert_eq!(ch.server_name(), Some("example.com")); + assert_eq!(ch.supported_groups().as_slice(), &[0x1a1a, 0x001d, 0x0017]); + assert_eq!(ch.ec_point_formats().as_slice(), &[0x00]); + assert_eq!( + ch.signature_algorithms().as_slice(), + &[0x0403, 0x0804, 0x0401] + ); + assert_eq!(ch.alpn_protocols().first().copied(), Some(b"h2".as_slice())); + assert_eq!( + ch.supported_versions().as_slice(), + &[0x2a2a, 0x0304, 0x0303] + ); + assert!(!ch.is_sslv2); +} + +#[test] +fn reads_client_hello_through_the_record_layer() { + let stream = sample().build_record(); + let hs = handshake_bytes(&stream).unwrap(); + let body = first_handshake_message(&hs, handshake_type::CLIENT_HELLO).unwrap(); + let ch = parse_client_hello(body).unwrap(); + assert_eq!(ch.cipher_suites.len(), 5); +} + +#[test] +fn server_hello_parses_single_cipher() { + let mut body = Vec::new(); + body.extend_from_slice(&0x0303u16.to_be_bytes()); + body.extend_from_slice(&[0u8; 32]); + body.push(0); + body.extend_from_slice(&0x1301u16.to_be_bytes()); + body.push(0); + body.extend_from_slice(&0u16.to_be_bytes()); + + let sh = parse_server_hello(&body).unwrap(); + assert_eq!(sh.cipher_suite, 0x1301); + assert_eq!(sh.legacy_version, 0x0303); +} + +#[test] +fn client_hello_without_extensions_parses() { + let body = ClientHelloBuilder::new() + .ciphers(&[0x002f, 0x0035]) + .build_body(); + let body = &body[..body.len() - 2]; + let ch = parse_client_hello(body).unwrap(); + assert_eq!(ch.cipher_suites.as_slice(), &[0x002f, 0x0035]); + assert!(ch.extensions.is_empty()); +} + +proptest! { + #[test] + fn parser_never_panics_on_arbitrary_bytes(bytes in proptest::collection::vec(any::(), 0..2048)) { + let _ = parse_client_hello(&bytes); + let _ = parse_server_hello(&bytes); + if let Ok(hs) = handshake_bytes(&bytes) { + let _ = first_handshake_message(&hs, handshake_type::CLIENT_HELLO); + } + } + + #[test] + fn well_formed_prefix_with_random_extension_tail_is_stable( + tail in proptest::collection::vec(any::(), 0..256) + ) { + let mut ch = ClientHelloBuilder::new().ciphers(&[0x1301, 0x1302]); + ch = ch.extension(0xabcd, tail); + let body = ch.build_body(); + let parsed = parse_client_hello(&body).unwrap(); + prop_assert_eq!(parsed.cipher_suites.as_slice(), &[0x1301, 0x1302]); + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/pipeline.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/pipeline.rs new file mode 100644 index 00000000..3c670e94 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/pipeline.rs @@ -0,0 +1,196 @@ +// ©AngelaMos | 2026 +// pipeline.rs + +//! Full pipeline known answer tests over the vendored FoxIO captures. +//! +//! These read a real capture file off disk, run it through the entire stack +//! the binary uses, and assert the fingerprints against the values FoxIO +//! published for the same files. A unit test proves an algorithm in isolation; +//! these prove that the file reader, the link layer decoder, the TCP +//! reassembler, the protocol sniffer, and the algorithms together still land +//! on the published answer. Every hash asserted here was independently +//! recomputed from its raw pre hash string with a shell tool before being +//! pinned, so a regression in any stage shows up as a mismatch rather than a +//! silently wrong but self consistent number. + +use std::path::PathBuf; + +use tlsfp_core::pipeline::event::StreamEvent; +use tlsfp_core::{FingerprintEvent, PcapFileSource, Pipeline, PipelineConfig}; + +fn pcap_path(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../testdata/pcap") + .join(name) +} + +/// Runs one capture all the way through the pipeline and returns its events. +fn fingerprint(name: &str) -> Vec { + let mut source = + PcapFileSource::open(pcap_path(name)).unwrap_or_else(|e| panic!("opening {name}: {e}")); + let mut pipeline = Pipeline::new(PipelineConfig::default()); + let mut events = Vec::new(); + pipeline + .run(&mut source, |event| events.push(event)) + .unwrap_or_else(|e| panic!("running {name}: {e}")); + events +} + +fn client_hellos(events: &[FingerprintEvent]) -> Vec<(&str, &str)> { + events + .iter() + .filter_map(|e| match &e.event { + StreamEvent::ClientHello { ja4, .. } => Some((ja4.hash.as_str(), ja4.raw.as_str())), + _ => None, + }) + .collect() +} + +fn ja4s_hashes(events: &[FingerprintEvent]) -> Vec<(&str, &str)> { + events + .iter() + .filter_map(|e| match &e.event { + StreamEvent::ServerHello { ja4s, .. } => Some((ja4s.hash.as_str(), ja4s.raw.as_str())), + _ => None, + }) + .collect() +} + +fn ja4x_hashes(events: &[FingerprintEvent]) -> Vec<&str> { + events + .iter() + .filter_map(|e| match &e.event { + StreamEvent::Certificate { ja4x } => Some(ja4x.as_str()), + _ => None, + }) + .collect() +} + +fn ja4h_hashes(events: &[FingerprintEvent]) -> Vec<&str> { + events + .iter() + .filter_map(|e| match &e.event { + StreamEvent::HttpRequest { ja4h, .. } => Some(ja4h.hash.as_str()), + _ => None, + }) + .collect() +} + +#[test] +fn tls_alpn_h2_reproduces_published_ja4_and_ja4x() { + let events = fingerprint("tls-alpn-h2.pcap"); + + let hellos = client_hellos(&events); + assert_eq!(hellos.len(), 1); + assert_eq!(hellos[0].0, "t12d4605h2_85626a9a5f7f_aaf95bb78ec9"); + + let certs = ja4x_hashes(&events); + assert_eq!( + certs.first().copied(), + Some("7d5dbb3783b4_ba7ce0880c07_7bf9a7bf7029") + ); +} + +#[test] +fn chrome_cloudflare_tcp_handshake_reproduces_published_ja4() { + let events = fingerprint("chrome-cloudflare-quic-with-secrets.pcapng"); + + let hellos = client_hellos(&events); + assert!( + hellos + .iter() + .any(|(hash, _)| *hash == "t13d1516h2_8daaf6152771_e5627efa2ab1"), + "expected the published chrome JA4, saw {hellos:?}" + ); + + let servers = ja4s_hashes(&events); + assert!( + servers + .iter() + .any(|(hash, _)| *hash == "t130200_1301_234ea6891581"), + "expected the published cloudflare JA4S, saw {servers:?}" + ); +} + +#[test] +fn tls_handshake_reproduces_published_ja4s_raw_and_hash() { + let events = fingerprint("tls-handshake.pcapng"); + + // The capture holds many flows to many Google and Cloudflare endpoints, so + // several distinct JA4S values appear. The published vector is the one for + // the first flow, and it must carry both its published hash and raw form. + let servers = ja4s_hashes(&events); + assert!( + servers + .iter() + .any(|(hash, raw)| *hash == "t130200_1301_234ea6891581" + && *raw == "t130200_1301_0033,002b"), + "expected the published JA4S with its raw form, saw {servers:?}" + ); + + let hellos = client_hellos(&events); + assert!(hellos.len() >= 5); + assert!( + hellos + .iter() + .any(|(hash, _)| *hash == "t13d1516h2_8daaf6152771_e5627efa2ab1") + ); +} + +#[test] +fn http1_with_cookies_reproduces_pinned_ja4h() { + let events = fingerprint("http1-with-cookies.pcapng"); + let hashes = ja4h_hashes(&events); + assert_eq!( + hashes, + vec!["ge11cr04da00_8ddaef5d77af_280f366eaa04_c2fb0fe53442"] + ); +} + +#[test] +fn tls12_capture_recomputes_to_pinned_ja4() { + let events = fingerprint("tls12.pcap"); + let hellos = client_hellos(&events); + assert_eq!(hellos.len(), 1); + assert_eq!(hellos[0].0, "t13d1715h2_5b57614c22b0_3d5424432f57"); +} + +#[test] +fn non_ascii_alpn_follows_the_spec_hex_rule() { + let events = fingerprint("tls-non-ascii-alpn.pcapng"); + let hellos = client_hellos(&events); + assert_eq!(hellos.len(), 1); + let (hash, _) = hellos[0]; + let alpn_chars = &hash[8..10]; + assert_eq!( + alpn_chars, "bd", + "spec rule prints first and last hex chars of the raw ALPN value" + ); +} + +#[test] +fn browsers_x509_extracts_every_certificate_in_chain_order() { + let events = fingerprint("browsers-x509.pcapng"); + let certs = ja4x_hashes(&events); + assert!( + certs.len() >= 7, + "expected several certificates, saw {}", + certs.len() + ); + assert!(certs.iter().all(|c| c.matches('_').count() == 2)); +} + +#[test] +fn tunneled_capture_yields_nothing_without_crashing() { + let events = fingerprint("gre-erspan-vxlan.pcap"); + assert!(events.is_empty()); +} + +#[test] +fn evasion_capture_is_processed_without_panicking() { + let events = fingerprint("CVE-2018-6794.pcap"); + assert!( + ja4h_hashes(&events).len() >= 2, + "the reassembler should still recover the HTTP requests" + ); +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/reassembly.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/reassembly.rs new file mode 100644 index 00000000..31a08ac4 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/reassembly.rs @@ -0,0 +1,114 @@ +// ©AngelaMos | 2026 +// reassembly.rs + +//! Property tests for the TCP stream reassembler. +//! +//! The reassembler is the component an adversary reaches first: it consumes +//! attacker controlled sequence numbers and segment boundaries on every +//! connection. Two properties pin it down. The first is correctness under +//! reordering: any stream cut into any segments and delivered in any order +//! must reassemble back to the original bytes, because that is the whole job. +//! The second is the absence of panics under fully arbitrary input, because a +//! passive sensor that can be crashed by a crafted segment is a denial of +//! service waiting to happen. + +use proptest::prelude::*; +use tlsfp_core::pipeline::flow::{PushOutcome, ReassemblyLimits, StreamReassembler}; + +fn generous_limits() -> ReassemblyLimits { + ReassemblyLimits { + max_assembled_bytes: 1 << 20, + max_pending_bytes: 1 << 20, + max_pending_segments: 4096, + } +} + +/// A stream plus its segment boundaries and a delivery permutation. +fn stream_and_delivery() -> impl Strategy, Vec<(usize, usize)>, Vec)> { + prop::collection::vec(any::(), 1..2048).prop_flat_map(|data| { + let len = data.len(); + let cuts = prop::collection::vec(0..len, 0..16); + (Just(data), cuts).prop_flat_map(|(data, mut cuts)| { + cuts.sort_unstable(); + cuts.dedup(); + let boundaries = boundaries_from_cuts(&cuts, data.len()); + let permutation = Just((0..boundaries.len()).collect::>()).prop_shuffle(); + (Just(data), Just(boundaries), permutation) + }) + }) +} + +fn boundaries_from_cuts(cuts: &[usize], len: usize) -> Vec<(usize, usize)> { + let mut points = vec![0]; + points.extend(cuts.iter().copied().filter(|&c| c > 0 && c < len)); + points.push(len); + points.dedup(); + points.windows(2).map(|w| (w[0], w[1])).collect() +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(512))] + + /// Any segmentation delivered in any order reassembles to the original. + #[test] + fn arbitrary_segmentation_and_reordering_reconstructs_the_stream( + (data, boundaries, permutation) in stream_and_delivery(), + ) { + let base_seq = 0x1234_5678u32; + let mut reasm = StreamReassembler::new(generous_limits()); + reasm.anchor(base_seq); + + for &segment_index in &permutation { + let (start, end) = boundaries[segment_index]; + let seq = base_seq.wrapping_add(u32::try_from(start).unwrap()); + reasm.push(seq, &data[start..end]); + } + + prop_assert_eq!(reasm.data(), data.as_slice()); + } + + /// Duplicates layered on top of a complete stream change nothing. + #[test] + fn duplicate_delivery_is_idempotent( + (data, boundaries, permutation) in stream_and_delivery(), + ) { + let base_seq = 42u32; + let mut reasm = StreamReassembler::new(generous_limits()); + reasm.anchor(base_seq); + + for round in 0..2 { + for &segment_index in &permutation { + let (start, end) = boundaries[segment_index]; + let seq = base_seq.wrapping_add(u32::try_from(start).unwrap()); + let outcome = reasm.push(seq, &data[start..end]); + if round == 1 { + prop_assert!(matches!( + outcome, + PushOutcome::Unchanged | PushOutcome::Grew + )); + } + } + } + prop_assert_eq!(reasm.data(), data.as_slice()); + } + + /// Fully arbitrary segments never panic and never exceed the cap. + #[test] + fn arbitrary_input_never_panics_or_overruns( + segments in prop::collection::vec( + (any::(), prop::collection::vec(any::(), 0..64)), + 0..256, + ), + ) { + let limits = ReassemblyLimits { + max_assembled_bytes: 512, + max_pending_bytes: 512, + max_pending_segments: 32, + }; + let mut reasm = StreamReassembler::new(limits); + for (seq, payload) in &segments { + reasm.push(*seq, payload); + prop_assert!(reasm.data().len() <= limits.max_assembled_bytes); + } + } +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/Cargo.toml b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/Cargo.toml new file mode 100644 index 00000000..80fbf70b --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/Cargo.toml @@ -0,0 +1,38 @@ +# ©AngelaMos | 2026 +# Cargo.toml + +[package] +name = "tlsfp" +description = "JA3/JA4 TLS fingerprinting tool with live capture, intel matching, and anomaly detection" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +default-run = "tlsfp" + +[[bin]] +name = "tlsfp" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +tlsfp-core.workspace = true +anyhow.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +pcap-parser.workspace = true +pcap.workspace = true +tokio.workspace = true +flume.workspace = true +rusqlite.workspace = true +axum.workspace = true +tower-http.workspace = true +tower.workspace = true +serde.workspace = true +serde_json.workspace = true +clap.workspace = true +etherparse.workspace = true diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/cli.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/cli.rs new file mode 100644 index 00000000..47b061ba --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/cli.rs @@ -0,0 +1,126 @@ +// ©AngelaMos | 2026 +// cli.rs + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use tracing_subscriber::EnvFilter; + +use tlsfp_core::{PcapFileSource, Pipeline, PipelineConfig}; + +/// JA3/JA4 TLS fingerprinting tool. +/// +/// Fingerprints TLS clients and servers from live capture or packet captures, +/// matches them against a local intelligence database, and flags anomalies such +/// as a fingerprint that disagrees with its own User-Agent. +#[derive(Debug, Parser)] +#[command(name = "tlsfp", version, about, long_about = None)] +pub struct Cli { + /// Increase log verbosity (repeat for more detail). + #[arg(short, long, global = true, action = clap::ArgAction::Count)] + pub verbose: u8, + + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Fingerprint every TLS and QUIC handshake in a packet capture file. + Pcap { + /// Path to a pcap or pcapng file. + path: std::path::PathBuf, + + /// Emit one JSON object per event instead of readable lines. + #[arg(long)] + json: bool, + }, + + /// Capture live from a network interface and fingerprint in real time. + Live { + /// Interface name, for example eth0. + interface: String, + }, + + /// Serve the web dashboard and HTTP API. + Serve { + /// Address to bind, for example 127.0.0.1:8080. + #[arg(default_value = "127.0.0.1:8080")] + bind: String, + }, +} + +impl Cli { + pub fn init_tracing(&self) { + let default = match self.verbose { + 0 => "tlsfp=info", + 1 => "tlsfp=debug", + _ => "tlsfp=trace", + }; + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default)); + tracing_subscriber::fmt().with_env_filter(filter).init(); + } + + pub fn run(self) -> Result<()> { + match self.command { + Command::Pcap { path, json } => run_pcap(&path, json), + Command::Live { interface } => { + anyhow::bail!("live capture on {interface} is not wired up yet") + } + Command::Serve { bind } => { + anyhow::bail!("dashboard on {bind} is not wired up yet") + } + } + } +} + +/// Fingerprints a capture file and prints one event per line on stdout. +/// +/// The summary goes to the log rather than stdout so that piping the output +/// into a tool sees only events, while a human still learns how much of the +/// capture was readable and whether the file was cut short mid packet. +fn run_pcap(path: &std::path::Path, json: bool) -> Result<()> { + let mut source = PcapFileSource::open(path) + .with_context(|| format!("cannot open capture {}", path.display()))?; + let mut pipeline = Pipeline::new(PipelineConfig::default()); + + let stdout = std::io::stdout().lock(); + let mut out = std::io::BufWriter::new(stdout); + let mut write_failure = None; + pipeline.run(&mut source, |event| { + use std::io::Write as _; + let result = if json { + serde_json::to_writer(&mut out, &event) + .map_err(anyhow::Error::from) + .and_then(|()| writeln!(out).map_err(anyhow::Error::from)) + } else { + writeln!(out, "{event}").map_err(anyhow::Error::from) + }; + if write_failure.is_none() { + if let Err(error) = result { + write_failure = Some(error); + } + } + })?; + if let Some(error) = write_failure { + return Err(error.context("writing events to stdout")); + } + { + use std::io::Write as _; + out.flush().context("flushing events to stdout")?; + } + + let counters = pipeline.counters(); + tracing::info!( + frames = counters.frames, + tcp_segments = counters.tcp_segments, + events = counters.events, + flows = counters.flows_created, + unfinished_tls_streams = counters.unfinished_tls_streams, + segments_dropped = counters.segments_dropped, + "capture processed" + ); + if source.truncated() { + tracing::warn!("capture file ended mid packet; the tail was not read"); + } + Ok(()) +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/main.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/main.rs new file mode 100644 index 00000000..ce70db7b --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/src/main.rs @@ -0,0 +1,15 @@ +// ©AngelaMos | 2026 +// main.rs + +mod cli; + +use anyhow::Result; +use clap::Parser; + +use crate::cli::Cli; + +fn main() -> Result<()> { + let cli = Cli::parse(); + cli.init_tracing(); + cli.run() +} diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/deny.toml b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/deny.toml new file mode 100644 index 00000000..2c46c991 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/deny.toml @@ -0,0 +1,30 @@ +# ©AngelaMos | 2026 +# deny.toml + +[graph] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true + +[advisories] +version = 2 +yanked = "deny" + +[licenses] +version = 2 +confidence-threshold = 0.93 +allow = [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "Unicode-3.0", + "Unicode-DFS-2016", + "MPL-2.0", + "CDLA-Permissive-2.0", +] + +[bans] +multiple-versions = "warn" +wildcards = "deny" diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/rustfmt.toml b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/rustfmt.toml new file mode 100644 index 00000000..c59829cf --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/rustfmt.toml @@ -0,0 +1,4 @@ +# ©AngelaMos | 2026 +# rustfmt.toml + +edition = "2024" diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/CVE-2018-6794.pcap b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/CVE-2018-6794.pcap new file mode 100644 index 0000000000000000000000000000000000000000..3ce5450f8608272e451cc98f1331b6f7dce94b40 GIT binary patch literal 7413 zcmeI1eM}o=9LJv)SfJ9y8IAalJd18C(B8Ea3KVo87EoS>yp0&+Jjz3Rp zxM(u|7-mkGItLq@Q?|vI$zpWboLS=35jKtSB`zlZVIq>4EHN=z@b^5|-k{J+Ok9Md zmptU@wfDK_dA`5jeSgpK&87Dzj3^o1K9Z3EK1^D7J^kFrCiEEeX+!B*1Gf&K`j!jG zn2J0I3 zyvo}CeGJRbSQ#ZFeZ2@(H8s`q4$gsd?RLDQ+H5KhN(zw_Tho9{+*l#~3NHr)QM&z2 z4Uv3`-wjuoO=i6LxXVVho>MdBwQjm6wIfWH~@~lD2A5 z;q#pZoYRFZ)m2TkHC7xDI|;5N-p*haF87CmU4(ZzIXmafbrx|Bdm(NVI)soIxeV?v z`$9rHu{|M$iMJ<2Yz?HlhlG_dOvG#|^LmL4BV)Szm9BtQfLn`Rflh?)r)P5dW6yR4 zin|}Nb46CMOYjlCkF?2=t{_W3Yc`+FzAud8Z63)RY!@Y;8~X-D*^1jqM?g?uan#$} zY6QvG11L8R_1GGkt)ZUC4TxlGZd74bBC$>Z4MG|u!mMtPjIkhjX1Tnr53(THv?zCk z5IPE!>HF0Hi4!E_EJ!|EPH9m??Ewgqp+u0Z*&w0v{^&~6dj=e0@?K2HyAGcL$3Wge zDNWCj$8lq6m{#DCV?1r>#+`JRT!w( zsaxONP1*FItU6%Dck2j?j&3PSVJvX#(o=WHt)oMW@78gq`2`yqxb@9#>Lk@^cJEHp zbL%&l2(2+9T!&@5LfzU?u2TLeO4rNOH(92h*C=Y?m3R=Z2e+PrT#z3&ptDej(YB%( zJ0Toq@_whut7fViBv6c53L`$-A@qvgpv1wSbzYZ6E3egSDD`ai(psv4bdV4Uxv^hS zWH-;RR)+FgN%fzXTvWs#wixs>{VkU1&C5kxXkeKRB^Q4QAxhbGGDR;Gj<8Vpb~$BU z4FxEBSOcN3@QLm|d4h#Pr6%J3%yIX?;m4_MQ0KMPE_YrU7F)m4+b6fqQ|G-a z6^K!{+@Jm1fhU3Lq0altj;JM3);3_pcV3lc`XGi?%~;^PKkvCi&Kn(CeCLfb%_D4R z;Jl-MFP&zc{yuq)uf(Mpuf;#W)eQG}L`E{dSJ|1QHcuk`$eKaTX(aym(_kYgE z6A^Eb@9`U{dO3QW z$vdmbn|RRDw~K432A&2H&urFxVfYRc@jXq%M2@bX96b&4PW__0IeC}KdqtBsk)vxZ zN6&(Yy*avXq^7BeWmTGpEJs^2*}n$vMTp!$Xa*=Vh7`Ts?kPrz-QuX#HSBh0fkY`= ujk=W6j8dVEMcpV>uo@i*SBF(f#;uKdV>!bp9*x4na-os!yn({o5&8$Qll370 literal 0 HcmV?d00001 diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/PROVENANCE.md b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/PROVENANCE.md new file mode 100644 index 00000000..f0ecffe0 --- /dev/null +++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/PROVENANCE.md @@ -0,0 +1,49 @@ + + + +# Test capture provenance + +Every capture file in this directory is vendored, bit for bit, from the +FoxIO JA4 reference repository. None of them were created or modified here, +and the SHA-256 digests below let anyone verify that claim offline. + +## Source + +- Repository: +- Commit: `4ab8e3f18f4b27e0b896c4ec2c251e20506eb87e` (fetched 2026-06-10) +- Path within the repository: `pcap/` +- Fetch URL pattern: `https://raw.githubusercontent.com/FoxIO-LLC/ja4//pcap/` + +## Licensing + +The FoxIO repository carries two licenses at the commit above: `LICENSE-JA4` +(BSD 3-Clause, covering the JA4 TLS client fingerprint) and `LICENSE` +(FoxIO License 1.1, covering the JA4+ suite). These capture files are the +repository's own test fixtures and are redistributed here unmodified, with +attribution, solely as test inputs for this non-commercial educational +project. See `NOTICE.md` at the project root for how the licensing split +applies to this project as a whole. + +## Files + +| File | SHA-256 | Why it is here | +| --- | --- | --- | +| `tls-handshake.pcapng` | `5a0c9f3d0f437e16fc68c3ce0d87998edf4f229b335c391361ed301bd22a513e` | TLS 1.3 handshake; anchors the published JA4S vector `t130200_1301_234ea6891581` | +| `tls-alpn-h2.pcap` | `8c00fd3e6c370b39dac61ad3a15c693088f74f3dbc836ee4e8f57105b1e84a91` | TLS 1.2 with ALPN h2; anchors the published JA4 vector `t12d4605h2_85626a9a5f7f_aaf95bb78ec9` and the JA4X DigiCert chain vectors | +| `tls12.pcap` | `d8c9ae8781c9bbba3a1bf5a95d7a6f309a3edd14c64a7c8adbc673d337fd5af4` | Minimal TLS 1.2 ClientHello | +| `tls-non-ascii-alpn.pcapng` | `cf1dd939619b8d65904dfd23b4f21c3255b6f513dc2e12117d5de2633d063f71` | ALPN value with non-ASCII bytes; exercises the spec-vs-reference divergence (FoxIO issue 178) | +| `chrome-cloudflare-quic-with-secrets.pcapng` | `b7c9de1238aef44d53dbe1add125a7b9e344e9063b98850c78dec23632b83942` | Chrome to Cloudflare; TCP stream anchors the published JA4 vector `t13d1516h2_8daaf6152771_e5627efa2ab1`, QUIC stream reserved for the QUIC milestone | +| `browsers-x509.pcapng` | `e05937fe5f3659f1b94b46305e419b878ba309ebcba8308750acaac704112906` | Browser certificate chains for JA4X over real captures | +| `http1-with-cookies.pcapng` | `7083ca41bcd09b21cb92e7f2d5bd09d73f25703be8555bb82642c2495eb15ef9` | Cleartext HTTP/1.1 request with cookies for JA4H over a reassembled stream | +| `gre-erspan-vxlan.pcap` | `5bbdb30a0707e21070ece3cd26068f72c34a0750d97c1e7720166cb4d0baf6d6` | Tunneled traffic; proves the decoder skips what it does not understand instead of crashing | +| `CVE-2018-6794.pcap` | `d1aa18b493bc68bf7cb367ce5fc1ee493262b47baff886bbe42533e7495d8b1d` | TCP stream evasion capture; torture input for the reassembler | +| `quic-with-several-tls-frames.pcapng` | `8d2c8a6787b942091aa63e33bde9f28c214b306dc024a66c552077ad30640e71` | QUIC initial with CRYPTO frames split across packets, reserved for the QUIC milestone | + +## Verifying + +```sh +cd testdata/pcap && sha256sum *.pcap *.pcapng +``` + +Compare the output against the table above. Any mismatch means a file no +longer matches what FoxIO published at the pinned commit. diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/browsers-x509.pcapng b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/browsers-x509.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..357b734cc5bd13d52889aac1e4b15f73a46ffc96 GIT binary patch literal 62320 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

__}y`m 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

->Ra*<% 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 ac%i2Eo8Qy=H9`8T9@>FeRQcz6<^KS(<9y`+ literal 0 HcmV?d00001 diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/chrome-cloudflare-quic-with-secrets.pcapng b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/chrome-cloudflare-quic-with-secrets.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..4968365ab494e658fd42e212debaadb8ec1bf11c GIT binary patch literal 44236 zcmdSBbzGEN*FQXTNq0$uNDVo3BOwS<(lAMPcStFSfJ&!;bR$Sgw@SB23eqVO!aLme z?fD(gInVp}ypO*>-u?M3=DKDsW`FnEYpw6vi@m|c$Hxr<000`2Tr?=i*Q@_D6aW`M z2kc<&X6*xqF+#i06htDIs0t6ui z7b&+e0f4(Y>bhjeR|R!drF**ODsuM}byVc;Dw*qQ%iX!7s$j08q@b;&%Lo>QfI))% z!u)VCF&IA>2;v7L1mHjrTm%Fag$Rlw_=UvygN&EjILb!;C7!-*eB8CtW6BGo4fKX)bf-odLejzwggpr>J zhzi1CFkzs8pr|17Igk*L9|Q)9K%o#I?tgUnv*yf!|9jmDgM|b|MWA3oA*e6{E-C~B zLPWt(Q7{4q77!K_;|B{uMIaDiIGA5d5R3qVU}AzWA*9wt1q7f#kPzgD?*10!Vleh$sRi2t|O96Ak4T76pSvp&+E*#E^;;fx$o!uqXn^FCYdI6oVlU$N_q9y@V`dJ4-(;r!iC`=C|C#~Dl7yRMcOz}Km-C55`ch^>g0#RL`9*({2&1s9BBwb z$S1EuBortNgCPUr-#p=ejR+wi1OXyv3l2rVK_DPVP*m(XJc|hmz{P~YFhQi{A?;WM zCJYe;A}tsq1Oy|e89C=bL4FuAeB)m8!!3e>e8}2H9A*F#vZvpRz-c*Vh6-q>v>q`K@iUOPc6D~?)mXe3v^?e2E)m1b%0Dy{)k%fYWhKYiLjT<>BfqVx5 zKu5j)c|E2d?Hb25p6LI)ag9f0|0%l}^8Q->jpW~`ka)0SA0ug0^soK@6(36DC)`$h zZWt1ieHO_@t5o8@@MxiYb>Eixo+23T#;Dxk=NJq54vIe zW!=~#@xX(Ku!*7>kqZ2`PDHP<{EXqc?`sT^|I*aIF@O)AvKu4sujTI;P}%;67^tv` zV%UDhK!JvWikx4R3pBKX23PW%$-`OB<-Ay*{h~zoRmrB@{q^_ur;LmQT~6c}TVr0| z_WE9YI%><+9hp6Xs@6A}hIRRbFF5v+E(=uv24HMxAVncXB}HrHYUOL?ZUqh&4i>bM zwvqwT0O~e$FoYf01Uti7#Jwnn3yQY_W%GvWctR5{4(To^Z-WqNj-%>Uo|QZC7h$tI6;-uZo!AJOwO_0 zd&nzMxt|EYgY4}(L&U-q7e_-!Lj%wP&`<%Wiq^=!(2#vUeti8+C_5B8BhDf-X8(2MbJ)n5Cfo?|R~ z3iAG1{*D0y_CLqqQ1Sh*v$|OO7?6(Ni9j^&d1~a# z>wcv2C3h%J`>07JS5dvoCt+JB^7Je%HF{2`JMBa(iprOQS@+f0u1X)12M}ciZslN2 z*)=5Kj8Vz+{dOyP^88woE>&ToX>x&lPAPu%mG7?2AK+GJcC!fp|b$=F4JfJk?H>emT(t2WfiDcelQA(N?E0c--B9Lg!dogrn=5xzI!J65k z=;Z*a>8BgK0+w}Qeze)7uN>V2rrx^6YuQBx6^?F_dNyYtlN{`|w#uVgauGb~3MY7& z7kUe}Jy_`sfDf}ijbVy55F+b)A!Wxh2s_sZgvWeTE5aO{o@uvGT6UMF*Q}_CYOX{e z2;phlS*u}wC*NdhtZVx?b|0mELwO*OVzz&l+3eKb!Ejr4?%~|Pm$&XcZ@tSFgz!O(u>1Av)DNYRkD%Y;@I)tP^;ORx%8)7Z=MO^B_%@yJ3I8#N?dmJARtWd%14@^ z7|If#F52px%i?E^{Nq_L7B$$Q%&?lZK;d@&?9RzE&x=CaR#@QS6UO1}(v^99(<8Y1*_%1$ub0)l3Td+mhIiCvWd+Yq z4R20ukIg*ma_gPDgN7>Box%9Mg4-L1XSj$O;}P{{-t%Yn6Ipg4{Xh(-qN*hh9d5a~ z=7NYYNCQ^;B!8c?98QAw>+Jb%$jND-8Nnq~UFMtB7q8r+i5$myE2r87dq7~O&fvUC92J4 zo#7oLiLj?oZb(*tc%wr?wk7Ax5iS{Cq^hQ{XH!jCq%l#LyLGHJ*3WVLI)49^OTv+H z{CDJ4UM{Wn{Dnb8Sm%Grs|=8Qa`rQZA9+nI}_|1i|hHy(5CCSfmobQ_M>bSPj}bwmxaBF3BTM;+tmemEU+i_e`FIeDlW#W z<10Eu1nY`n-VDuCGS=$PkdnSfmpDEBi4b}@_$8|pEAQ3f(y*^uFQIAGe7x@Ykjrp; zeW1&9L|VYfHy$|M=K?xnnF=)3Au1FbN<;KX8*xTvuCjXOtn)bI5pMFJNQ{bhyZFgH zU93PSNa;wQFE48|+%-S)l!sF|4H7 zn_A-eWxU-<*`Md7GVGYv0X9=F#f@16jxDxoUR`R9S}XgO_d3} zBl0NriQLzQ_h|LZoi{cvsav1D*d8*&rtTZBwoYMQ#q_BCUcyoOk@e+D zw0^-+?xZ19XnQrtH&h+>`cSVdebRh0 zU?}&|vd=k`SQcJjdU|Ke(x_sCKZ!?RS(}L_#BO#(v!kKT=otX(`XI{Z9S_S zN%0bK07lgjX8jO(0~v)zwci*%x3kw&Bjd9hE0Qf0dSWsLA#ca>(~E9dZe)z@tG19s z)G}~CDnGQBcXYyJWJxiY3y}|bN12`kILb8xom#zwok@BJ`V0`+KRK7JTJ=&oL}3*C ziw`3G?5}a)yYDjndh*L}Vq~{wV3%^~*!lpIfSu3bWibW0@`-K-b~Yw#^js04WD@eI+*JF_Lh-H6+yuOxy7(>V}kD=<1F@*d)24qSK zFeRev7rb?)S_1{G2Wli*OkXW0k!2~1+&+|3>G6)i*L#ii@%YFAMTOOzBY-b)lIadZ z4#wTQ7>xVLfh1Ewda%U1?>3e<2u~Z^b#4;QXN;Jo#c`LuJS v^-G#^ZMg@Y}aEn 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

    _-|Lv$E6-MwHSsXA*`B~p%LbPM#D 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 literal 0 HcmV?d00001 diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/quic-with-several-tls-frames.pcapng b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/quic-with-several-tls-frames.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..97e8925448c378ea01ee4b9580c176d812041c32 GIT binary patch literal 1780 zcmb7Fe>~IqAOF~fp>iw{;Zt9(4YQfqnylE5zJwthb`n0D48x6WM55d&(sf1XNPS!B z$d3$3NL_xFAKiA`H|}(t^ZRh(BC^}6KknaqJ|6GK>-Bm(Ua#lt`TFPi*50~Ry9)pS zPbX_*HDy$M1Pq`8E}jhVNb(78EJy(zC=U2Q(2j(q#|gNgw;&GUCM3iOz<&ZQ<|Rgg zzP?PNEditCf&-QSiHHs7C4vFGSWcWc0kp$|Xo8(90b>J(3wbdd(2jy95|zCOfB-Or z{CFrV9z+N51l)wkFhMj1B--L_?LoBYeZ=Ci#~}v?LIjxzaUg=dYKamm{VMHwAESy5 zrsNA|a78?b8_e=PbS%Yz=7Oh@Nn|X;o?(Y2(kT=y&4tdyk{s*_Gy;Af`41DVa5tyj%XoEz0)yT2ym3>Z%F=be7eDGbOESTa>lxonQvGnYLND zZ$3w`kN^`vZ2Ckx1L=AI&~``d&V}_)I78uT4-qpF72hkT)sSy7p*SazQzrm0UU+#V zlh(E+=$1#~Z0LLzG#>#+mi{!8Y1P7Lt*Yzn2pXBI=&45Dc&W+~Zf6ZhYZ_dpD*KVK3;WOKRM5p8_3lpwsZ-SxtG6U^CHXHyUGOkw!^@Q; zJ>(M^1+&6GcW2dVLU8(k(bJpHEV>8Wb2KxP?!Bo>6|)fT{im<3Ewt0o{pL92v^4tZ zMV*%Qw44^yl`!X?GoOJF^q5smt5ONBj`awP(g^Oxsa%H?>2<-9O5p?__oB z8w9Dr6W2?dh!EbT%wi)p0tKTf^v@h*b>$L0-c>ZVxtueO1g?kt?#3Q5P+A z?DOU=cRB<%cm|5p$K`VJjqLrlSveMKY($wxip4Ws&2oQEX4j{0;_6)ob<}d&_nL(- z{wmen*nrEtxW(2b7F)EBAv2m|&WW-yGWQD_`$|*DrS(N;@XsgjW9YOWtOgU|jaKzudzSnnRdkQ?kD5bxykT z)unH<_y(xMb-gi@c%R1OH2aCg!Km9-X%A2F`zRZ@>!+UYC!M!ih$YyE{qdX5@0}Gs zT0#YhY(#uT9x8#?7zzN|Uc(Uv=ZJj(!u63D(t=d-^5{}VQ5Vx108 zQd&?1P9j$!cpRJ`ua=6+Q?(ZyR_n@JMd- zfl1gIyVcTN85s#%l5jpP)sSNw@>XWRP=Qf(W!Qn_Kbl`xWO#ZDFKgIMN_6!aDqioO7~Z>Zpy`41{)&;@6eCp->;9XvSXAX> znO0`QzWu123wjcNL&-ni{4DB+9W&imjRIo^9@I4E*VegQ*JX)H$djFT@kM5;8 z@0QqBwH~c>3)1z`3|ZCB^+#{)_W!0dWw_ELy(5Ct0+wd=}pIpWGKiCg0f_ zn$g*t;6gg+Feohk{Fe6pn&tJMAE%qO7G;k5g$$*Brt(OYO~$B z=yAf>M6MtKj2Fa-cpNSV3{M6*A3h2gfPCM3cL%fv|Cp=h5XxMO@qtTSng0{29e)E( C7UFFH literal 0 HcmV?d00001 diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-alpn-h2.pcap b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-alpn-h2.pcap new file mode 100644 index 0000000000000000000000000000000000000000..74bf35acb57a00145bb2b0363acd7ad94019b202 GIT binary patch literal 3827 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 hKTUEFeP&b{J@UOfmYZU2n#7N=gL?n~ literal 0 HcmV?d00001 diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-handshake.pcapng b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-handshake.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..8822715dd2dd01af2de60396ccbe95fdee97f9d6 GIT binary patch literal 198844 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 z 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�LrBl|Bu)$KAC)#;C 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 literal 0 HcmV?d00001 diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-non-ascii-alpn.pcapng b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/testdata/pcap/tls-non-ascii-alpn.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..26ce655d701a0623a3f25620d9d4e4778ae3b89d GIT binary patch literal 2560 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