i count the tiles every morning. today there's one more. there's always one more.
|
|
@ -221,10 +221,7 @@ prod-restart:
|
|||
docker compose --env-file .env.prod -f compose.yml -f cloudflared.compose.yml logs -f
|
||||
|
||||
[group('tunnel')]
|
||||
prod-redeploy:
|
||||
docker compose --env-file .env.prod -f compose.yml -f cloudflared.compose.yml down
|
||||
docker compose --env-file .env.prod -f compose.yml -f cloudflared.compose.yml up -d --build
|
||||
docker compose --env-file .env.prod -f compose.yml -f cloudflared.compose.yml logs -f
|
||||
redeploy: (tunnel-down "--remove-orphans") (tunnel-start "--remove-orphans")
|
||||
|
||||
[group('tunnel')]
|
||||
prod-logs *SERVICE:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const PAGE_COPY = {
|
|||
'Field-deployable trip-wires for the quiet hours. Each specimen waits in place, reports when touched, and leaves a forensic note behind.',
|
||||
STRIP_FIELD_STATION: 'FIELD STATION',
|
||||
STRIP_ISSUE: 'ISSUE',
|
||||
STRIP_VOLUME: 'VOL.001',
|
||||
STRIP_VOLUME: 'SPEC.003',
|
||||
STRIP_FOLIO: 'FOLIO',
|
||||
SECTION_01_INDEX: '01 / SPECIES',
|
||||
SECTION_01_TITLE: 'SELECT SPECIES',
|
||||
|
|
|
|||
|
|
@ -153,6 +153,9 @@ ps:
|
|||
# Docker Compose (Production + Cloudflare Tunnel)
|
||||
# =============================================================================
|
||||
|
||||
[group('tunnel')]
|
||||
redeploy: (tunnel-down "--remove-orphans") build (tunnel-start "--remove-orphans")
|
||||
|
||||
[group('tunnel')]
|
||||
tunnel-up *ARGS:
|
||||
docker compose --env-file .env -f compose.yml -f cloudflared.compose.yml up {{ARGS}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
# ©AngelaMos | 2026
|
||||
# .npmrc
|
||||
strict-dep-builds=false
|
||||
auto-install-peers=true
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -120,7 +120,7 @@ export function Component(): React.ReactElement {
|
|||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.state}>
|
||||
<span className={styles.stateLabel}>ANALYZING SPECIMEN\u2026</span>
|
||||
<span className={styles.stateLabel}>ANALYZING SUBJECT\u2026</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ export function Component(): React.ReactElement {
|
|||
return (
|
||||
<div className={styles.state}>
|
||||
<span className={styles.stateCode}>404</span>
|
||||
<span className={styles.stateLabel}>SPECIMEN NOT FOUND</span>
|
||||
<span className={styles.stateLabel}>SUBJECT NOT FOUND</span>
|
||||
<Link to={ROUTES.HOME} className={styles.stateBack}>
|
||||
NEW ANALYSIS
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Binary upload landing page with drag-and-drop file
|
||||
// intake and analysis pipeline visualization
|
||||
//
|
||||
// Renders the Axumortem specimen intake interface: an
|
||||
// Renders the Axumortem subject intake interface: an
|
||||
// animated SVG grain background, hex offset margin
|
||||
// decoration (16 addresses), corner brackets, meta
|
||||
// strip header, and format support badges (ELF/PE/
|
||||
|
|
@ -143,7 +143,7 @@ export function Component(): React.ReactElement {
|
|||
<div className={styles.cornerBR} aria-hidden="true" />
|
||||
|
||||
<header className={styles.metaStrip}>
|
||||
<span>AXM-001</span>
|
||||
<span>SPECIMEN-005</span>
|
||||
<span className={styles.metaCenter}>STATIC ANALYSIS SUITE</span>
|
||||
<span>v0.1.1</span>
|
||||
</header>
|
||||
|
|
@ -164,7 +164,7 @@ export function Component(): React.ReactElement {
|
|||
|
||||
<section className={styles.intake}>
|
||||
<div className={styles.intakeHeader}>
|
||||
<span className={styles.intakeLabel}>SPECIMEN INTAKE</span>
|
||||
<span className={styles.intakeLabel}>SUBJECT INTAKE</span>
|
||||
{file && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -211,7 +211,7 @@ export function Component(): React.ReactElement {
|
|||
className={styles.dropPrompt}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<span className={styles.dropStatus}>AWAITING SPECIMEN</span>
|
||||
<span className={styles.dropStatus}>AWAITING SUBJECT</span>
|
||||
<span className={styles.dropInstruction}>
|
||||
DRAG + DROP BINARY / CLICK TO BROWSE
|
||||
</span>
|
||||
|
|
@ -226,7 +226,7 @@ export function Component(): React.ReactElement {
|
|||
onClick={handleSubmit}
|
||||
disabled={upload.isPending}
|
||||
>
|
||||
{upload.isPending ? 'ANALYZING\u2026' : 'SUBMIT SPECIMEN'}
|
||||
{upload.isPending ? 'ANALYZING\u2026' : 'SUBMIT SUBJECT'}
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
|
|
@ -250,7 +250,7 @@ export function Component(): React.ReactElement {
|
|||
<footer className={styles.footer}>
|
||||
<span>© ANGELAMOS 2026</span>
|
||||
<span className={styles.footerDesignation}>
|
||||
SYS AXM-BDE {'//'} UNIT-001
|
||||
SYS AXM-BDE {'//'} SPECIMEN-005
|
||||
</span>
|
||||
<span>AXUMORTEM</span>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@
|
|||
# ============================================================================
|
||||
FROM node:22-slim AS builder
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
RUN corepack enable && corepack prepare pnpm@10.29.1 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml* ./
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml* frontend/.npmrc* ./
|
||||
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ ps:
|
|||
# Docker Compose (Production + Cloudflare Tunnel)
|
||||
# =============================================================================
|
||||
|
||||
[group('tunnel')]
|
||||
redeploy: (tunnel-down "--remove-orphans") build (tunnel-start "--remove-orphans")
|
||||
|
||||
[group('tunnel')]
|
||||
tunnel-up *ARGS:
|
||||
docker compose --env-file .env -f compose.yml -f cloudflared.compose.yml up {{ARGS}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- README.md -->
|
||||
|
||||
```json
|
||||
████████╗██╗ ███████╗███████╗██████╗
|
||||
╚══██╔══╝██║ ██╔════╝██╔════╝██╔══██╗
|
||||
██║ ██║ ███████╗█████╗ ██████╔╝
|
||||
██║ ██║ ╚════██║██╔══╝ ██╔═══╝
|
||||
██║ ███████╗███████║██║ ██║
|
||||
╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝
|
||||
```
|
||||
|
||||
[](https://github.com/CarterPerez-dev/Cybersecurity-Projects/tree/main/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting)
|
||||
[](https://www.rust-lang.org)
|
||||
[](https://github.com/FoxIO-LLC/ja4)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
|
||||
---
|
||||
|
||||
[](https://mkultraalumni.com)
|
||||
|
||||
> A passive TLS fingerprinting sensor in Rust. Point it at a capture file or a live interface and it computes the JA3, JA4, JA4S, JA4H, JA4X, and JA4T fingerprints of every handshake, matches them against a local intelligence database, and flags the things a fingerprint alone cannot hide: a TLS stack that disagrees with its own User-Agent, a brand-new fingerprint, a client that rotates its identity to evade a blocklist. It reads TCP and the TLS hidden inside QUIC initial packets, never sends a byte, and carries half a million fingerprints a second.
|
||||
|
||||
## Why fingerprint TLS
|
||||
|
||||
When a client opens a TLS connection, the very first message it sends, the ClientHello, is a detailed self-description: which TLS versions it supports, which cipher suites in which order, which extensions, which elliptic curves. A browser, a Go program, a Python script, and a piece of malware each assemble that message differently, because each is built on a different TLS library configured a different way. The ClientHello travels in the clear, before any encryption is negotiated, so a passive observer who never decrypts anything can still read it.
|
||||
|
||||
A fingerprint is a hash of those choices. The same software produces the same fingerprint on every connection, so a fingerprint that is on a blocklist today catches the same malware family tomorrow even if its IP, its domain, and its certificate all changed. That is the idea behind JA3, published by Salesforce in 2017, and JA4, its 2023 successor from FoxIO that fixed the one weakness that eventually killed JA3 for browser traffic: when Chrome started shuffling its extension order on every connection, JA3, which hashes extensions in wire order, produced a fresh hash every time. JA4 sorts first, so the shuffle changes nothing.
|
||||
|
||||
This project builds the whole sensor around that idea, in a language where a parser bug is a memory-safety bug. The fingerprinting core forbids `unsafe`, the capture path is bounded so an adversarial packet cannot exhaust memory, and every fingerprint is checked byte for byte against the reference implementations.
|
||||
|
||||
## What Works Today
|
||||
|
||||
This is not a stub. The tool fingerprints real captures, decrypts real QUIC, matches against real public threat feeds, and raises real alerts, and every capability below is exercised by a known-answer test against a published vector, an integration test against a vendored capture, and a run of the actual `tlsfp` binary.
|
||||
|
||||
**Fingerprints**
|
||||
- **JA3 / JA3S** (MD5 of the ClientHello / ServerHello field list), kept because public malware feeds still speak JA3 and because watching it collapse next to JA4 is the clearest way to see why JA4 exists
|
||||
- **JA4 / JA4S** (the FoxIO TLS client and server fingerprint, sorted cipher and extension lists), the headline fingerprint, stable under extension shuffling
|
||||
- **JA4H** the HTTP client fingerprint, from a cleartext request's method, version, header order, cookies, and accept-language
|
||||
- **JA4X** the X.509 fingerprint, from the issuer, subject, and extension object identifiers of a certificate, which clusters certificates minted by one toolchain
|
||||
- **JA4T / JA4TS** the TCP-stack fingerprint, from the SYN's window size, options, MSS, and window scale, which catches a tool wearing a browser's TLS clothing while its OS speaks with a different TCP accent
|
||||
- GREASE values stripped from every list, so the deliberate noise a modern client inserts never changes its fingerprint
|
||||
|
||||
**Capture**
|
||||
- Reads `pcap` and `pcapng` files, and captures live from an interface through `libpcap` with the raw-socket capabilities dropped to exactly the two the kernel needs
|
||||
- A reassembly layer rebuilds each direction of each TCP conversation, surviving reordering, retransmission, and overlap, so a ClientHello split across segments still fingerprints
|
||||
- Bounded by construction: a flow cap, an idle timeout, and per-stream byte ceilings keep an adversarial capture from turning the flow table into a memory bomb
|
||||
|
||||
**QUIC**
|
||||
- Decrypts QUIC Initial packets to read the ClientHello inside, deriving the client initial keys from the packet's own Destination Connection ID per RFC 9001 (QUIC v1) and RFC 9369 (QUIC v2), with no server-side secret
|
||||
- Reassembles CRYPTO frames across packets, so a QUIC ClientHello spread over several initials still yields a `q`-transport JA4
|
||||
|
||||
**Intelligence**
|
||||
- A bundled SQLite database seeded from three vendored feeds with no network call: abuse.ch SSLBL, the Salesforce `osx-nix` JA3 list, and a small curated C2 set (**271 fingerprints**)
|
||||
- An optional install-time pull of ja4db.com, validated record by record on the way in
|
||||
- Exact lookups plus JA4 fuzzy matching on the capability-and-cipher prefix, scored into a verdict with a threat score and a confidence
|
||||
|
||||
**Detection**
|
||||
- Six rules that run as a capture streams: `known_bad` (a feed hit), `ua_mismatch` (the headline: a JA4 that disagrees with its own User-Agent), `os_mismatch` (a JA4T that disagrees with the OS the User-Agent claims), `first_seen`, `fp_rotation`, and `monoculture`
|
||||
- A forensic `--report` mode that reads a whole capture and prints one ranked summary, folding in intelligence and detection automatically whenever the database is present
|
||||
- A web dashboard ([live demo](https://mkultraalumni.com)) that streams events and alerts over Server-Sent Events, fed by a replayed capture, a live interface, or an external sensor tailing the same database
|
||||
|
||||
See [`learn/CONFORMANCE.md`](learn/CONFORMANCE.md) for the exact published vector each fingerprint is pinned to, and every deliberate scope boundary.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
curl -fsSL https://angelamos.com/tlsfp/install.sh | bash
|
||||
```
|
||||
|
||||
The installer builds the release binary, puts `tlsfp` on your PATH, and seeds the intelligence database. Pass `--live` to also grant the raw-socket capabilities that live capture needs. Then point it at a capture:
|
||||
|
||||
```bash
|
||||
# Fingerprint every handshake in a capture, one line each
|
||||
tlsfp pcap traffic.pcapng
|
||||
|
||||
# Seed the threat feeds, then match and run the detection rules
|
||||
tlsfp intel seed
|
||||
tlsfp pcap traffic.pcapng --report
|
||||
|
||||
# Watch an interface in real time, matching and detecting as it goes
|
||||
sudo setcap cap_net_raw,cap_net_admin=eip "$(command -v tlsfp)"
|
||||
tlsfp live eth0 --intel --detect
|
||||
```
|
||||
|
||||
A single fingerprint line looks like this, a Chrome handshake to a Google host:
|
||||
|
||||
```
|
||||
1675707151.805 192.168.1.168:50112 -> 142.251.16.94:443 client_hello ja4=t13d1516h2_8daaf6152771_e5627efa2ab1 ja3=1c258ebef8eee2dfa3df6d8d07285af9 sni=clientservices.googleapis.com alpn=h2
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> This project uses [`just`](https://github.com/casey/just) as a command runner. Type `just` to see every recipe. `just bench` runs the throughput benchmarks; `just dev-up` brings up the dockerized dashboard with hot reload.
|
||||
>
|
||||
> Install: `curl -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin`
|
||||
|
||||
## Learn
|
||||
|
||||
This project ships a full teaching track. Read it in order, or jump to what you need.
|
||||
|
||||
| Doc | What it covers |
|
||||
|-----|----------------|
|
||||
| [`learn/00-OVERVIEW.md`](learn/00-OVERVIEW.md) | What TLS fingerprinting is, why it works, and a 10-minute tour |
|
||||
| [`learn/01-CONCEPTS.md`](learn/01-CONCEPTS.md) | The ClientHello, JA3 vs JA4, GREASE, evasion, QUIC, passive capture, grounded in real intrusions |
|
||||
| [`learn/02-ARCHITECTURE.md`](learn/02-ARCHITECTURE.md) | The three-crate split, the capture pipeline, the intelligence store, the threat model |
|
||||
| [`learn/03-IMPLEMENTATION.md`](learn/03-IMPLEMENTATION.md) | A code walkthrough from a raw frame to a scored alert, and the reassembly and bounding patterns |
|
||||
| [`learn/ALGORITHMS.md`](learn/ALGORITHMS.md) | How each fingerprint is computed byte by byte, and how a QUIC initial is decrypted |
|
||||
| [`learn/CONFORMANCE.md`](learn/CONFORMANCE.md) | The published vector each fingerprint is pinned to, and every deliberate scope boundary |
|
||||
| [`learn/04-CHALLENGES.md`](learn/04-CHALLENGES.md) | Extension ideas from beginner to expert |
|
||||
|
||||
## Architecture
|
||||
|
||||
Three crates, in a strict dependency line. The engine knows nothing about databases or networks; the intelligence store knows nothing about capture; the binary wires them together.
|
||||
|
||||
```
|
||||
pcap / pcapng file live interface (libpcap) QUIC initial
|
||||
│ │ │
|
||||
└─────────────┬───────────┴──────────────────────────┘
|
||||
│ raw link-layer frames
|
||||
▼
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ tlsfp-core the engine, no I/O, forbids unsafe │
|
||||
│ decode → flow reassembly → TLS/HTTP/QUIC → hash │
|
||||
│ ja3 · ja4 · ja4h · ja4x · ja4t · parse · quic │
|
||||
└───────────────────────┬────────────────────────────┘
|
||||
│ FingerprintEvent
|
||||
┌───────────────────────┴────────────────────────────┐
|
||||
│ tlsfp-intel the judgement, a bundled SQLite DB │
|
||||
│ match (exact + JA4 fuzzy) → score → detection rules │
|
||||
│ matcher · seed · import · detect · signal · schema │
|
||||
└───────────────────────┬────────────────────────────┘
|
||||
│ MatchReport + Alert
|
||||
┌───────────────────────┴────────────────────────────┐
|
||||
│ tlsfp the binary: CLI + web dashboard │
|
||||
│ pcap · live · serve (axum + SSE) · intel · report │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Design decisions:** the engine forbids `unsafe` outright, so a malformed packet can never be more than a parse error. The store is deliberately synchronous, because a lookup is one indexed query and a capture is a plain loop; the async runtime lives only in the web server, where concurrent readers actually need it. JA3 uses MD5 because that is what the original definition and every public JA3 feed use, and reproducing those feed hashes is the whole point of keeping it. The QUIC decryption uses no server secret because the client initial keys are derived from a Connection ID that travels in the clear.
|
||||
|
||||
## Build and Test
|
||||
|
||||
```bash
|
||||
cargo build --release # the shipped binary → target/release/tlsfp
|
||||
cargo test --workspace # 204 unit + integration tests, 1 ignored
|
||||
cargo bench -p tlsfp-core # criterion throughput benchmarks
|
||||
just clippy # clippy::pedantic, warnings as errors
|
||||
just fmt-check # rustfmt
|
||||
```
|
||||
|
||||
Every fingerprint is pinned to a published vector. The JA3 tests reproduce the original Salesforce blog vectors through MD5; the JA4 tests reproduce the FoxIO cipher, extension, and TCP section vectors; the QUIC tests derive the client initial keys and match RFC 9001 Appendix A (v1) and RFC 9369 Appendix A (v2) byte for byte. The reassembly tests rebuild a ClientHello from out-of-order and overlapping segments. The JA4X parser has a property-test fuzz harness because it walks attacker-controlled certificate DER.
|
||||
|
||||
The benchmarks replay vendored captures frame by frame through the whole pipeline. On a modern laptop the pipeline sustains roughly **380,000 to 500,000 fingerprints per second**, comfortably past the project target of 10,000.
|
||||
|
||||
## Run in Docker
|
||||
|
||||
No Rust toolchain on the host? The dashboard runs entirely in containers.
|
||||
|
||||
```bash
|
||||
just up # production stack: built dashboard + backend
|
||||
just dev-up # development stack: vite hot reload
|
||||
```
|
||||
|
||||
The production image is a multi-stage build that compiles the release binary in a Rust builder and ships only the binary plus the built dashboard assets behind nginx. The development stack bind-mounts the frontend and runs `pnpm install` on startup, so an added package is always present after a restart.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ja3-ja4-tls-fingerprinting/
|
||||
├── Cargo.toml # the 3-crate virtual workspace
|
||||
├── crates/
|
||||
│ ├── tlsfp-core/ # the engine: no I/O, forbids unsafe
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── parse/ # TLS record, ClientHello, ServerHello, certificate readers
|
||||
│ │ │ ├── pipeline/ # decode → flow reassembly → TLS/HTTP → event
|
||||
│ │ │ ├── ja3.rs # JA3 / JA3S (the dead-but-still-fed MD5 fingerprint)
|
||||
│ │ │ ├── ja4.rs # JA4 / JA4S (the headline sorted fingerprint)
|
||||
│ │ │ ├── ja4h.rs # JA4H (the HTTP request fingerprint)
|
||||
│ │ │ ├── ja4x.rs # JA4X (the X.509 certificate fingerprint)
|
||||
│ │ │ ├── ja4t.rs # JA4T / JA4TS (the TCP-stack fingerprint)
|
||||
│ │ │ ├── quic.rs # QUIC initial decryption (RFC 9001 + RFC 9369)
|
||||
│ │ │ ├── grease.rs # the GREASE value table and the strip
|
||||
│ │ │ ├── der.rs # the minimal DER reader JA4X needs
|
||||
│ │ │ └── registry.rs # version codes and extension constants
|
||||
│ │ ├── benches/fingerprint.rs# criterion throughput benchmarks
|
||||
│ │ └── tests/ # KAT + integration: ja3, ja4, ja4x, parse, reassembly
|
||||
│ ├── tlsfp-intel/ # the judgement: a bundled SQLite store
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── schema.rs # the migrations
|
||||
│ │ │ ├── seed.rs # the three vendored feeds, compiled in
|
||||
│ │ │ ├── import.rs # the validated ja4db.com importer
|
||||
│ │ │ ├── matcher.rs # exact + JA4 fuzzy lookup, scored into a verdict
|
||||
│ │ │ ├── detect.rs # the six detection rules
|
||||
│ │ │ ├── signal.rs # the User-Agent / OS heuristics the rules read
|
||||
│ │ │ └── model.rs # FpKind, Category, Verdict, the report types
|
||||
│ │ └── seeds/ # the vendored CSV feeds
|
||||
│ └── tlsfp/ # the binary
|
||||
│ └── src/
|
||||
│ ├── cli.rs # the clap command tree
|
||||
│ ├── live.rs # the libpcap capture thread and the async bridge
|
||||
│ ├── report.rs # the forensic --report builder
|
||||
│ └── serve.rs # the axum dashboard + SSE stream
|
||||
├── frontend/ # the anti-design dashboard (Vite + React 19)
|
||||
├── testdata/pcap/ # vendored captures, the integration fixtures
|
||||
├── install.sh # the one-shot curl-able installer
|
||||
└── justfile # every recipe
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[AGPL 3.0](LICENSE). The vendored threat feeds under `crates/tlsfp-intel/seeds/` keep their original licenses, recorded per feed in [`NOTICE.md`](NOTICE.md) and in the `intel_source` table.
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
bottom: 0;
|
||||
width: 78%;
|
||||
height: 60%;
|
||||
background: url('/assets/objects/snake.jpg') right bottom / contain no-repeat;
|
||||
background: url('/assets/objects/xray-lily.png') right bottom / contain no-repeat;
|
||||
opacity: 0.13;
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
|
|
|
|||
|
|
@ -14,11 +14,7 @@ export function Component(): React.ReactElement {
|
|||
return (
|
||||
<div className={styles.page}>
|
||||
<section className={styles.head}>
|
||||
<img
|
||||
src="/assets/objects/xray-lily.webp"
|
||||
alt=""
|
||||
className={styles.lily}
|
||||
/>
|
||||
<img src="/assets/objects/xray-lily.png" alt="" className={styles.lily} />
|
||||
<div className={styles.headText}>
|
||||
<span className={styles.over}>threat intelligence / catalogue</span>
|
||||
<h1 className={styles.title}>THE CORPUS</h1>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function Component(): React.ReactElement {
|
|||
return (
|
||||
<div className={styles.page}>
|
||||
<img
|
||||
src="/assets/objects/xray-butterfly.jpg"
|
||||
src="/assets/objects/xray-butterfly.png"
|
||||
alt=""
|
||||
className={styles.specimen}
|
||||
/>
|
||||
|
|
@ -38,9 +38,6 @@ export function Component(): React.ReactElement {
|
|||
<main className={styles.center}>
|
||||
<span className={styles.over}>sleep is counter surveillance</span>
|
||||
<h1 className={styles.title}>TLSFP</h1>
|
||||
<span className={styles.under}>
|
||||
the fingerprint of every client that speaks tls
|
||||
</span>
|
||||
<p className={styles.thesis}>
|
||||
An x-ray of the handshake. Every client carries a fingerprint it cannot
|
||||
hide, and the instant a user-agent claims to be a browser while its
|
||||
|
|
|
|||
|
|
@ -30,9 +30,18 @@
|
|||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
||||
@include breakpoint-down('lg') {
|
||||
opacity: 0.6;
|
||||
right: -16%;
|
||||
}
|
||||
|
||||
@include breakpoint-down('md') {
|
||||
opacity: 0.4;
|
||||
right: -20%;
|
||||
opacity: 0.3;
|
||||
right: -24%;
|
||||
}
|
||||
|
||||
@include breakpoint-down('sm') {
|
||||
opacity: 0.16;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +102,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
max-width: 60ch;
|
||||
max-width: 80ch;
|
||||
padding-block: $space-8;
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +113,7 @@
|
|||
text-transform: uppercase;
|
||||
color: $fire;
|
||||
text-shadow: 0 0 10px rgb(255, 77, 18, 28%);
|
||||
margin-bottom: $space-5;
|
||||
margin-bottom: $space-6;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -113,26 +122,15 @@
|
|||
line-height: 0.82;
|
||||
letter-spacing: 0.01em;
|
||||
color: $chalk;
|
||||
margin: 0 0 $space-5;
|
||||
margin: 0 0 $space-7;
|
||||
text-shadow: 0 0 48px rgb(47, 123, 255, 16%);
|
||||
}
|
||||
|
||||
// The mono tag reads as a tight stamp under the masthead, so it is kept short
|
||||
// and never allowed to run the full measure of the prose below it.
|
||||
.under {
|
||||
max-width: 34ch;
|
||||
font-family: $font-mono;
|
||||
font-size: $font-size-xs;
|
||||
line-height: 1.55;
|
||||
letter-spacing: 0.12em;
|
||||
color: $ash;
|
||||
}
|
||||
|
||||
// The thesis runs wider and thinner than the tag, a long low band of prose
|
||||
// The thesis runs wider and thinner than the title, a long low band of prose
|
||||
// reaching toward the specimen rather than a narrow stacked column.
|
||||
.thesis {
|
||||
max-width: 54ch;
|
||||
margin-top: $space-8;
|
||||
max-width: clamp(54ch, 30vw + 17ch, 76ch);
|
||||
margin-top: 0;
|
||||
font-size: $font-size-base;
|
||||
line-height: 1.6;
|
||||
color: $paper;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function Component(): React.ReactElement {
|
|||
<div className={styles.page}>
|
||||
<section className={styles.masthead}>
|
||||
<img
|
||||
src="/assets/objects/xray-butterfly.jpg"
|
||||
src="/assets/objects/xray-butterfly.png"
|
||||
alt=""
|
||||
className={styles.butterfly}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- 00-OVERVIEW.md -->
|
||||
|
||||
# JA3/JA4 TLS Fingerprinting: Overview
|
||||
|
||||
## What This Is
|
||||
|
||||
A passive TLS fingerprinting sensor written in Rust. You give it a packet capture or a live network interface, and it computes the JA3, JA4, JA4S, JA4H, JA4X, and JA4T fingerprints of every handshake it sees, looks each one up in a local threat-intelligence database, and raises alerts when something looks wrong. It never sends a packet, never decrypts application data, and never needs a private key. Everything it knows, it learns by watching the parts of a connection that are visible before encryption begins.
|
||||
|
||||
The point of the project is to understand, by building it, how network defenders identify *what software* is talking on their network when they cannot see *what it is saying*. You get a working sensor you can run against real captures, a threat database seeded from real public feeds, and a codebase small enough to read in a weekend.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Encryption hid the contents of network traffic, and the industry largely treats that as solved. What encryption did not hide is the *negotiation* of encryption. Before a TLS client and server agree on a key, they exchange a handshake in plaintext, and that handshake is shaped by the client's software in a way the client cannot easily disguise. A defender who cannot read the traffic can still recognize the talker.
|
||||
|
||||
This matters because attackers reuse tooling. A command-and-control framework, a credential stealer, a botnet implant: each is built on a specific TLS library, configured a specific way, and that configuration produces the same fingerprint on every victim. Block the fingerprint and you block the family, regardless of which IP or domain it hides behind this week.
|
||||
|
||||
The cost of *not* having this visibility is not hypothetical.
|
||||
|
||||
- **SolarWinds / SUNBURST, 2020.** A nation-state actor distributed a backdoor through a trusted software update, and the implant beaconed out over HTTPS that looked, at the network layer, like ordinary traffic. Defenders who only watched IPs and domains had nothing; the malicious infrastructure was new and clean. The traffic that *carried* the beacon, though, had a consistent TLS shape. Fingerprinting is one of the few passive signals that survives an attacker who controls their own certificates and rotates their own infrastructure.
|
||||
- **Cobalt Strike, everywhere.** The single most common post-exploitation framework in real intrusions ships with default TLS profiles whose JA3 hashes are public knowledge. Its default fingerprint, `72a589da586844d7f0818ce684948eea`, is so well known that mature teams alert on it directly. The same hash also appears for some Emotet samples, because they share an underlying TLS library, which is itself a useful lesson: a fingerprint identifies a *toolchain*, not a *threat actor*.
|
||||
- **The browser-impersonation arms race.** Tools like `curl-impersonate` and `utls` exist specifically to make a script's ClientHello identical to a real Chrome's, so that a fingerprint-based filter waves it through. This is why a single fingerprint is never enough, and why this tool computes several and cross-checks them against each other and against the User-Agent.
|
||||
|
||||
**Real world scenarios where this applies:**
|
||||
- **Network security monitoring.** A sensor on a span port or a tap fingerprints every outbound TLS connection and flags the ones that match malware feeds or disagree with their own User-Agent.
|
||||
- **Bot and fraud detection.** A web service fingerprints incoming TLS to tell a real browser from an automated client claiming to be one, without a CAPTCHA.
|
||||
- **Threat hunting and forensics.** An analyst feeds a captured `.pcap` from an incident through the tool and gets a ranked inventory of every client that spoke on the wire, with verdicts.
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
This project teaches how passive identification on an encrypted network actually works. By building it yourself, you will understand:
|
||||
|
||||
**Security concepts:**
|
||||
- **What a TLS handshake reveals.** Why the ClientHello is a fingerprint at all: the cipher suites, extensions, and curves a client offers are a stable signature of the software that built it.
|
||||
- **Why JA4 replaced JA3.** How a single design choice, sorting the lists before hashing, made JA4 survive the extension-order shuffling that made JA3 useless for modern browsers.
|
||||
- **GREASE and deliberate noise.** Why clients inject random values into their own handshakes (RFC 8701), and why a fingerprint must strip them or it changes on every connection.
|
||||
- **Evasion and cross-checking.** How a tool impersonates a browser's TLS, and how pairing the TLS fingerprint with the TCP-stack fingerprint and the User-Agent exposes the lie.
|
||||
- **Passive QUIC.** Why a QUIC initial packet, which is "encrypted," can still be read by anyone, because its key is derived from a value sent in the clear.
|
||||
|
||||
**Technical skills:**
|
||||
- **Parsing adversarial binary input safely.** Walking TLS records and X.509 DER in a language that forbids `unsafe`, so a malformed length is a typed error and never a buffer overrun.
|
||||
- **TCP stream reassembly.** Rebuilding a byte stream from out-of-order, retransmitted, and overlapping segments, the way every real capture arrives.
|
||||
- **Bounding untrusted work.** Putting hard caps on the flow table and per-stream buffers so an attacker cannot make the sensor exhaust memory.
|
||||
- **Designing a workspace.** Splitting an engine that forbids I/O from a store that owns a database from a binary that wires them together, so the engine can be fuzzed and benchmarked in isolation.
|
||||
|
||||
**Tools and techniques:**
|
||||
- **`libpcap`** for live capture, and the Linux capability model (`cap_net_raw`) that lets an unprivileged binary open a raw socket.
|
||||
- **`tshark` / Wireshark** as an oracle: Wireshark computes JA3 and JA4 too, so you can confirm this tool agrees with it on a real capture.
|
||||
- **SQLite** as an embedded, zero-configuration intelligence store, with WAL mode so a dashboard can read while a sensor writes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You do not need prior fingerprinting experience. You do need some comfort with the following.
|
||||
|
||||
**Required knowledge:**
|
||||
- **The TLS handshake at a high level.** That a client sends a ClientHello, a server replies with a ServerHello and a certificate, and a key is agreed. You do not need the cryptography; you need to know what messages exist and which travel in the clear.
|
||||
- **TCP/IP basics.** What a segment is, what a SYN is, why packets arrive out of order. The reassembly layer is the heart of the capture path.
|
||||
- **Basic Rust or a willingness to read it.** The code uses enums, pattern matching, iterators, and `Result`. If you know Go, C++, or Python with type hints, you can follow it.
|
||||
|
||||
**Tools you'll need:**
|
||||
- **A Rust toolchain**, edition 2024 (rustc 1.85 or newer). The `install.sh` script checks for it.
|
||||
- **`libpcap` development headers** for live capture (`libpcap-dev` on Debian or Ubuntu). File-only use does not need them.
|
||||
- **A capture to feed it.** The repository vendors several under `testdata/pcap/`, or capture your own with `tcpdump -w out.pcap`.
|
||||
|
||||
**Helpful but not required:**
|
||||
- **Wireshark**, to compare fingerprints against a second implementation.
|
||||
- A reading of the [FoxIO JA4+ specification](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md) and the [original Salesforce JA3 post](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/). You can also read [`01-CONCEPTS.md`](./01-CONCEPTS.md) and pick up the specs when something is unclear.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd PROJECTS/intermediate/ja3-ja4-tls-fingerprinting
|
||||
|
||||
# Build the binary and seed the threat feeds
|
||||
cargo build --release
|
||||
./target/release/tlsfp intel seed
|
||||
|
||||
# Fingerprint a vendored capture, one line per handshake
|
||||
./target/release/tlsfp pcap testdata/pcap/tls-handshake.pcapng
|
||||
|
||||
# Read the whole capture and print one ranked forensic summary
|
||||
./target/release/tlsfp pcap testdata/pcap/tls-handshake.pcapng --report
|
||||
```
|
||||
|
||||
Expected output: the plain `pcap` command prints one line per handshake, each with a JA4 and a JA3 and, for a ClientHello, the SNI and ALPN. You will see both `t`-transport fingerprints (TLS over TCP) and `q`-transport fingerprints (TLS inside QUIC) in the same capture, because the vendored file contains both. The `--report` command instead prints a single summary: the busiest endpoints, the most common fingerprints, any intelligence hits, and any alerts.
|
||||
|
||||
To watch a live interface, grant the binary the two capabilities it needs and point it at an interface:
|
||||
|
||||
```bash
|
||||
sudo setcap cap_net_raw,cap_net_admin=eip "$(command -v ./target/release/tlsfp)"
|
||||
./target/release/tlsfp live eth0 --intel --detect
|
||||
```
|
||||
|
||||
Stop it with ctrl-c: the first one drains the capture and prints trustworthy final counters, a second exits immediately.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ja3-ja4-tls-fingerprinting/
|
||||
├── crates/
|
||||
│ ├── tlsfp-core/ # the engine: parses TLS, reassembles TCP, computes the fingerprints
|
||||
│ ├── tlsfp-intel/ # the store: a bundled SQLite database, matching, and the detection rules
|
||||
│ └── tlsfp/ # the binary: the CLI and the web dashboard
|
||||
├── testdata/pcap/ # vendored captures used as integration fixtures
|
||||
├── frontend/ # the dashboard (Vite + React 19)
|
||||
└── install.sh # the one-shot installer
|
||||
```
|
||||
|
||||
The single most important file to understand first is `crates/tlsfp-core/src/ja4.rs`. Everything in the engine exists to feed it a parsed ClientHello; everything in the rest of the tool exists to act on what it returns.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Understand the ideas.** Read [01-CONCEPTS.md](./01-CONCEPTS.md) for the ClientHello, JA3 versus JA4, GREASE, evasion, and passive QUIC, each grounded in a real intrusion.
|
||||
2. **See the design.** Read [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) for the three-crate split, the capture pipeline, the intelligence store, and the threat model.
|
||||
3. **Walk the code.** Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) to trace a raw frame all the way to a scored alert.
|
||||
4. **Learn the algorithms.** Read [ALGORITHMS.md](./ALGORITHMS.md) for how each fingerprint is built byte by byte and how a QUIC initial is decrypted.
|
||||
5. **Check the contract.** Read [CONFORMANCE.md](./CONFORMANCE.md) for the published vector each fingerprint is pinned to and every deliberate scope boundary.
|
||||
6. **Extend it.** Read [04-CHALLENGES.md](./04-CHALLENGES.md) for projects from "add a fingerprint" to "build a clustering model".
|
||||
|
||||
## Common Issues
|
||||
|
||||
**`tlsfp live` fails with a permission error**
|
||||
```
|
||||
Error: opening interface eth0: you don't have permission to capture
|
||||
```
|
||||
Solution: live capture opens a raw socket, which an unprivileged user cannot do. Grant the binary the capabilities once with `sudo setcap cap_net_raw,cap_net_admin=eip "$(command -v tlsfp)"`. File capabilities live on the binary, so repeat the grant after every rebuild.
|
||||
|
||||
**`intel lookup` says no database exists**
|
||||
```
|
||||
no intelligence database at ...; run 'tlsfp intel seed' first
|
||||
```
|
||||
Solution: the lookup, stats, and alerts commands read an existing database but never create one. Run `tlsfp intel seed` to build it from the three bundled feeds, which needs no network.
|
||||
|
||||
**A capture shows fewer handshakes than I expected**
|
||||
Solution: a handshake split across many out-of-order segments, or one whose ClientHello arrives after the per-stream byte cap, may not fingerprint. Run with `-v` to see the counters, including `tls_miss_rate` and `segments_dropped`. On a live interface, kernel drops under load do the same; the tool warns when the kernel reports any.
|
||||
|
||||
**The fingerprint disagrees with Wireshark**
|
||||
Solution: check that you are comparing the same handshake and the same fingerprint version. JA4 has a raw form and a hashed form; this tool prints the hashed form by default. The raw form is available in the JSON output (`--json`) for exactly this kind of debugging.
|
||||
|
||||
## Related Projects
|
||||
|
||||
If you found this interesting, look at:
|
||||
- **hsm-emulator**: an advanced project that builds the *other* side of TLS, the key custody an HSM provides, with the same discipline of pinning every output to a published vector.
|
||||
- **bug-bounty-platform**: a full application that handles the kind of traffic this sensor would watch, and shows where a fingerprinting signal would feed into a larger detection pipeline.
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- 01-CONCEPTS.md -->
|
||||
|
||||
# JA3/JA4 TLS Fingerprinting: Concepts
|
||||
|
||||
This document explains the ideas the tool is built on, each grounded in something that actually happened. Read it before the code. Once these click, the implementation is just careful bookkeeping.
|
||||
|
||||
## 1. The ClientHello is a confession
|
||||
|
||||
A TLS connection opens with the client sending a `ClientHello`, the first real message on the wire. It travels in plaintext, because the two sides have not yet agreed on a key to encrypt with. Inside it, the client lists exactly how it is configured:
|
||||
|
||||
- the **TLS versions** it supports,
|
||||
- the **cipher suites** it offers, in a specific order of preference,
|
||||
- a list of **extensions** (SNI, ALPN, supported groups, signature algorithms, key share, and many more),
|
||||
- the **elliptic curves** ("supported groups") it can do,
|
||||
- the **EC point formats** it understands.
|
||||
|
||||
None of this is secret. All of it is chosen by the *software*, not the user. Chrome's TLS stack offers a different set, in a different order, than Firefox's, than Go's `crypto/tls`, than Python's `ssl`, than the bespoke library inside a piece of malware. Two installations of the same Chrome version produce nearly identical ClientHellos; Chrome and a Python script produce obviously different ones.
|
||||
|
||||
```
|
||||
ClientHello
|
||||
├── legacy_version: 0x0303 (TLS 1.2, frozen here even for TLS 1.3)
|
||||
├── random: 32 bytes (ignored, it changes every time)
|
||||
├── cipher_suites: [0x1301, 0x1302, 0x1303, 0xc02b, ...]
|
||||
├── compression_methods: [0x00]
|
||||
└── extensions:
|
||||
├── 0x0000 server_name "example.com"
|
||||
├── 0x000a supported_groups [0x001d, 0x0017, ...]
|
||||
├── 0x000d signature_algorithms [0x0403, 0x0804, ...]
|
||||
├── 0x0010 alpn ["h2", "http/1.1"]
|
||||
├── 0x002b supported_versions [0x0304, 0x0303]
|
||||
└── ...
|
||||
```
|
||||
|
||||
A **fingerprint** turns that structure into a short, stable string. The insight, due to Salesforce in 2017, is that the *shape* of the ClientHello identifies the software even when every other identifier is forged. An attacker can change their IP, their domain, and their certificate for free. Changing their TLS stack means rebuilding their tooling.
|
||||
|
||||
## 2. JA3: the original, and why it died
|
||||
|
||||
JA3 (named for its three authors, John Althouse, Jeff Atkinson, Josh Atkins) builds the fingerprint the obvious way. It takes five fields in order, version, ciphers, extensions, supported groups, point formats, writes each as decimal numbers, joins them, and takes the MD5:
|
||||
|
||||
```
|
||||
769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0
|
||||
│
|
||||
MD5
|
||||
▼
|
||||
ada70206e40642a3e4461f35503241d5
|
||||
```
|
||||
|
||||
For years this worked beautifully. Cobalt Strike's default profile, Trickbot, Emotet, each had a known JA3, and feeds like abuse.ch SSLBL published thousands of malware JA3 hashes that defenders could block directly.
|
||||
|
||||
Then it broke, for a specific and instructive reason. **JA3 hashes the extensions in the exact order the client sent them.** In 2021, Chrome (followed by others) started doing something the TLS spec explicitly permits: shuffling the order of its extensions on every single connection, as a deliberate anti-ossification measure. The cipher list stayed the same, the extensions present stayed the same, but their *order* changed every time. Since JA3 hashes order, every Chrome connection now produced a different JA3 hash. Overnight, JA3 became useless for fingerprinting browsers. A signature that changes every time identifies nothing.
|
||||
|
||||
JA3 is not dead for *malware*, though, which is why this tool still computes it. Malware tooling rarely shuffles extensions, public feeds are still expressed in JA3, and watching JA3 fragment next to a stable JA4 on real browser traffic is the single clearest demonstration of why the successor exists.
|
||||
|
||||
> The Emotet and Cobalt Strike collision is worth sitting with. Both have appeared under the JA3 hash `72a589da586844d7f0818ce684948eea`, not because they are the same malware, but because they were built on the same TLS library configured the same way. A fingerprint identifies a *toolchain*. Treating a fingerprint match as proof of a specific actor is a mistake; treating it as "this came from a known-suspicious toolchain" is correct.
|
||||
|
||||
## 3. JA4: sort first, and survive
|
||||
|
||||
JA4, published by FoxIO in 2023, fixes the order problem with one change: **it sorts the cipher and extension lists before hashing them.** If the set of extensions a client offers is stable but their order is random, then sorting throws the randomness away and keeps the signal. Chrome's shuffled extensions sort back to the same list every time, so JA4 is stable again.
|
||||
|
||||
JA4 also changed the format to be *partly human-readable*, which JA3's opaque MD5 was not. A JA4 fingerprint has three underscore-separated parts:
|
||||
|
||||
```
|
||||
t13d1516h2_8daaf6152771_e5627efa2ab1
|
||||
│ │ │
|
||||
│ │ └── truncated SHA-256 of the sorted extensions (+ sig algs)
|
||||
│ └── truncated SHA-256 of the sorted cipher suites
|
||||
└── 10 readable characters:
|
||||
t transport: t=TCP, q=QUIC, d=DTLS
|
||||
13 TLS version: 1.3
|
||||
d SNI present (d=domain, i=no SNI)
|
||||
15 cipher count (15)
|
||||
16 extension count (16)
|
||||
h2 first ALPN value (h2 = HTTP/2)
|
||||
```
|
||||
|
||||
You can read the prefix at a glance: "TLS 1.3 over TCP, with SNI, 15 ciphers, 16 extensions, speaking HTTP/2." Two clients with different prefixes are visibly different software before you even compare the hashes. The hashes then distinguish clients that share a prefix.
|
||||
|
||||
The two design choices, sorting and a readable prefix, are the entire reason JA4 succeeded where JA3 failed. [ALGORITHMS.md](./ALGORITHMS.md) walks the exact byte construction.
|
||||
|
||||
## 4. GREASE: clients lie on purpose
|
||||
|
||||
If you dump a real Chrome ClientHello, you will see cipher suites and extensions with values like `0x0a0a`, `0x1a1a`, `0x2a2a`. These are not real. They are **GREASE** (Generate Random Extensions And Sustain Extensibility, RFC 8701), random reserved values that a client deliberately injects to keep the ecosystem honest, so that middleboxes and servers cannot start assuming a fixed set of values and break future clients that add new ones.
|
||||
|
||||
GREASE is poison for fingerprinting if you ignore it. The whole point of GREASE is that the values are *random per connection*, so any fingerprint that includes them changes every time, exactly the JA3 order problem in a different costume. Both JA3 and JA4 therefore **strip every GREASE value** from every list before hashing. The set of GREASE values is fixed and known (the `0x?a?a` pattern, sixteen of them), so stripping is a simple filter.
|
||||
|
||||
```
|
||||
offered: [0x1a1a, 0x1301, 0x1302, 0x1303, 0xc02b] <- 0x1a1a is GREASE
|
||||
hashed: [0x1301, 0x1302, 0x1303, 0xc02b] <- stripped before hashing
|
||||
```
|
||||
|
||||
This tool keeps the GREASE table in one place (`grease.rs`) and applies the same strip to every fingerprint, because a fingerprint that forgets to strip GREASE is not wrong occasionally, it is wrong on every modern client.
|
||||
|
||||
## 5. JA4+ is a family, and one fingerprint is never enough
|
||||
|
||||
JA4 is the TLS-client fingerprint, but the same idea applies to other layers, and the family (collectively "JA4+") cross-checks each other. This tool computes all of them:
|
||||
|
||||
| Fingerprint | What it identifies | Read from |
|
||||
|-------------|-------------------|-----------|
|
||||
| **JA4** | the TLS client software | the ClientHello |
|
||||
| **JA4S** | the TLS server software | the ServerHello |
|
||||
| **JA4H** | the HTTP client software | a cleartext HTTP request |
|
||||
| **JA4X** | the certificate-issuing toolchain | an X.509 certificate |
|
||||
| **JA4T** | the client's TCP/IP stack (the OS) | the SYN packet |
|
||||
|
||||
The reason to compute several is **evasion**. A growing class of tools (`curl-impersonate`, `utls`, and others) exists precisely to forge a browser's ClientHello. They make a script's JA4 identical to a real Chrome's, so a JA4-only filter waves them through. But forging one layer does not forge the others. A Python script running on Linux, impersonating Chrome's TLS, still emits a *Linux* TCP SYN, so its JA4T betrays it. It still sends HTTP headers in a script's order, so its JA4H betrays it. It still claims `User-Agent: ...Chrome...`, which now *disagrees* with its TCP stack.
|
||||
|
||||
That disagreement is the single most valuable signal this tool produces, and it is the headline detection rule. [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) describes how the rules combine these, and [ALGORITHMS.md](./ALGORITHMS.md) gives each construction.
|
||||
|
||||
> This is the lesson of the browser-impersonation arms race. As long as defenders fingerprinted only TLS, attackers only had to forge TLS. The defense is not a better single fingerprint; it is *correlation across layers the attacker controls independently*. An attacker who forges all of them perfectly has, in effect, rebuilt a real browser, which is expensive and rare.
|
||||
|
||||
## 6. Passive QUIC: "encrypted" is not "secret"
|
||||
|
||||
QUIC, the transport under HTTP/3, carries its own TLS handshake inside its first packets, and those packets are encrypted. It would seem a passive observer is locked out. It is not, and the reason is a beautiful subtlety.
|
||||
|
||||
QUIC's *Initial* packets are encrypted, but with a key that is **derived from the connection's Destination Connection ID, which is sent in the clear in the packet header.** The encryption exists to defeat dumb middleboxes that would otherwise mangle the packets, not to keep the handshake secret. Anyone who reads the packet can derive the same key, decrypt the Initial, and read the ClientHello inside, exactly as if it were TCP.
|
||||
|
||||
```
|
||||
QUIC Initial packet
|
||||
├── header (cleartext): ... Destination Connection ID = D ...
|
||||
└── payload (encrypted under a key derived from D)
|
||||
│
|
||||
HKDF(D) ──────────┘ anyone can compute this
|
||||
▼
|
||||
decrypted CRYPTO frames
|
||||
▼
|
||||
TLS ClientHello -> q-transport JA4
|
||||
```
|
||||
|
||||
This tool derives the client initial keys from each packet's own Connection ID, following RFC 9001 for QUIC v1 and RFC 9369 for QUIC v2, with no server-side secret and no direction detection. It reassembles the CRYPTO frames (a QUIC ClientHello can span several Initial packets) and feeds the result to the same JA4 code the TCP path uses. The fingerprint comes out with a `q` transport marker. [ALGORITHMS.md](./ALGORITHMS.md) gives the full key schedule.
|
||||
|
||||
## 7. Passive capture and the reassembly problem
|
||||
|
||||
Everything above assumes you can see a clean ClientHello. In a real capture you cannot. TCP delivers a *stream* of bytes chopped into segments that arrive out of order, get retransmitted, and sometimes overlap. A ClientHello large enough to span two segments will not parse if you look at either segment alone.
|
||||
|
||||
So before any fingerprinting, the tool must do what TCP itself does at the endpoint: **reassemble each direction of each conversation into a contiguous byte stream.** It tracks every flow (a four-tuple of source and destination address and port), buffers out-of-order segments until the gap before them fills, and resolves overlaps. Only once a contiguous run of bytes contains a complete TLS record does the parser run.
|
||||
|
||||
This is also where the **security of the sensor itself** lives. A passive tool processes attacker-controlled bytes by definition: anyone on the monitored network can send it whatever they like. So the reassembly layer is bounded in every dimension, a maximum number of tracked flows, an idle timeout that evicts stale ones, a ceiling on buffered out-of-order bytes per direction. Without those bounds, an attacker could open millions of flows, or send segments with a permanent gap that never fills, and watch the sensor's memory climb until the host dies. The bounds turn an unbounded adversarial input into a fixed, survivable cost. [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) shows the exact caps.
|
||||
|
||||
## 8. Memory safety is a security property here
|
||||
|
||||
This tool is written in Rust, and the fingerprinting engine forbids `unsafe` outright. That is not a stylistic preference; it is a direct response to what this code does.
|
||||
|
||||
Every parser in `tlsfp-core` reads **attacker-controlled binary input**: a TLS record from the wire, an X.509 certificate from a hostile server, a QUIC packet from anywhere. This is the exact category of code that produces the worst vulnerabilities in the C world. **Heartbleed (CVE-2014-0160)** was a TLS-parsing bug, a length field trusted without bounds-checking, that let an attacker read server memory. A fingerprinting sensor is *nothing but* TLS and certificate parsing, so in C it would be a Heartbleed factory.
|
||||
|
||||
In Rust, a length field that claims more bytes than exist produces a typed `ParseError`, not an out-of-bounds read. The JA4X certificate parser, which walks the most attacker-friendly format of all (X.509 DER), has a property-test fuzz harness that throws random and truncated certificates at it specifically to prove it returns errors instead of panicking. Safety is the feature that lets a passive sensor sit on a hostile network without itself becoming the vulnerability. [CONFORMANCE.md](./CONFORMANCE.md) records exactly what the parsers accept and reject.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [FoxIO JA4+ technical specifications](https://github.com/FoxIO-LLC/ja4/tree/main/technical_details): the authoritative JA4, JA4S, JA4H, JA4X, JA4T definitions.
|
||||
- [Salesforce, "TLS Fingerprinting with JA3 and JA3S" (2017)](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/): the original.
|
||||
- [RFC 8701](https://www.rfc-editor.org/rfc/rfc8701.html): GREASE.
|
||||
- [RFC 9001](https://www.rfc-editor.org/rfc/rfc9001.html) and [RFC 9369](https://www.rfc-editor.org/rfc/rfc9369.html): QUIC's use of TLS, v1 and v2.
|
||||
- [abuse.ch SSLBL](https://sslbl.abuse.ch/): one of the public feeds this tool seeds from.
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- 02-ARCHITECTURE.md -->
|
||||
|
||||
# JA3/JA4 TLS Fingerprinting: Architecture
|
||||
|
||||
This document describes how the tool is put together: the three crates and why they are separate, the capture pipeline a frame flows through, the intelligence store that judges a fingerprint, and the threat model that shaped every bound. Read [01-CONCEPTS.md](./01-CONCEPTS.md) first; this assumes you know what the fingerprints are.
|
||||
|
||||
## The three-crate split
|
||||
|
||||
The project is one Cargo workspace with three crates in a strict dependency line. Nothing points backwards.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ tlsfp-core the engine │
|
||||
│ parses TLS, reassembles TCP, decrypts QUIC, hashes. │
|
||||
│ Depends on NOTHING that touches a network, a database, │
|
||||
│ or an async runtime. Forbids `unsafe`. Fuzzable. │
|
||||
└───────────────────────────┬──────────────────────────────┘
|
||||
│ tlsfp-core::FingerprintEvent
|
||||
┌───────────────────────────┴──────────────────────────────┐
|
||||
│ tlsfp-intel the judgement │
|
||||
│ owns a bundled SQLite database. Matches a fingerprint │
|
||||
│ to a verdict, runs the detection rules, records alerts. │
|
||||
│ Synchronous on purpose. Depends only on tlsfp-core. │
|
||||
└───────────────────────────┬──────────────────────────────┘
|
||||
│ MatchReport + Alert
|
||||
┌───────────────────────────┴──────────────────────────────┐
|
||||
│ tlsfp the binary │
|
||||
│ the clap CLI and the axum web dashboard. Wires a packet │
|
||||
│ source to the engine to the store to a writer. │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The reason for the split is testability and blast radius. `tlsfp-core` has no I/O, so the entire fingerprinting engine runs byte-exact in unit tests against vendored captures, with no network, no clock, and no database to mock. It can be fuzzed in isolation, which matters because it parses hostile input. `tlsfp-intel` adds exactly one concern, persistence and judgement, and depends only on the engine's output type. The binary is the only crate that knows about interfaces, runtimes, and the terminal. A bug in the dashboard cannot reach the parser; a parser change cannot break the database schema.
|
||||
|
||||
## The capture pipeline
|
||||
|
||||
Inside `tlsfp-core`, a raw frame becomes a fingerprint by flowing through a fixed sequence of stages. Each stage is a separate module so it can be understood and tested alone.
|
||||
|
||||
```
|
||||
PacketSource decode flow reassembly protocol fingerprint
|
||||
──────────── ────── ─────────────── ──────── ───────────
|
||||
pcap file ┌─> strip Ethernet ┌─> StreamReassembler ┌─> watch for a ┌─> ja3() ja4()
|
||||
pcapng file ────┤ strip IP │ per (src,dst, │ TLS record or │ ja4h() ja4x()
|
||||
live (libpcap) │ keep TCP segment │ sport,dport): │ an HTTP head │ ja4t()
|
||||
QUIC initial └─> or UDP datagram │ order, dedup, │ in the └─> StreamEvent
|
||||
│ resolve overlap, │ contiguous + addrs + ts
|
||||
decode.rs │ bound buffers │ bytes = FingerprintEvent
|
||||
│ │
|
||||
source.rs └── flow.rs └── tls.rs event.rs
|
||||
```
|
||||
|
||||
**`source.rs`** abstracts where frames come from. `PcapFileSource` reads a file; the binary's `LiveSource` reads an interface. Both yield a `RawFrame` (a timestamp, a link type, and a byte slice). The engine does not know or care which it got.
|
||||
|
||||
**`decode.rs`** strips the link and network layers off a frame and yields either a TCP `DecodedSegment` or a UDP `DecodedDatagram`, or a `Skip` reason if the frame is not interesting. This is also where the TCP SYN options are walked for JA4T, before reassembly, because JA4T reads the SYN itself, not the stream it opens.
|
||||
|
||||
**`flow.rs`** is the reassembly engine and the heart of the capture path. It keeps a table of flows keyed by the four-tuple. For each direction of each flow it maintains a contiguous byte buffer and a set of parked out-of-order segments. When a segment fills the gap before a parked run, the run is merged in. Overlaps are resolved. This is the stage that makes a ClientHello split across three reordered segments parse correctly, and it is the stage most carefully bounded against abuse (see the threat model below).
|
||||
|
||||
**`tls.rs`** watches each reassembled stream for something to fingerprint: a complete TLS handshake flight (ClientHello, ServerHello, Certificate) or the head of a cleartext HTTP request. When it recognizes one, it parses it (`parse/`) and computes the fingerprint.
|
||||
|
||||
**`event.rs`** defines the output. A `StreamEvent` is the fingerprint plus its kind:
|
||||
|
||||
```rust
|
||||
pub enum StreamEvent {
|
||||
ClientHello { ja3, ja4, sni, alpn, user_agent, ... },
|
||||
ServerHello { ja3s, ja4s, ... },
|
||||
Certificate { ja4x },
|
||||
HttpRequest { ja4h, method, user_agent, ... },
|
||||
TcpSyn { ja4t },
|
||||
TcpSynAck { ja4ts },
|
||||
}
|
||||
```
|
||||
|
||||
Wrapped with the source and destination addresses and a timestamp, it becomes a `FingerprintEvent`, the single type that crosses the boundary out of the engine. Everything downstream consumes `FingerprintEvent` and nothing else.
|
||||
|
||||
The QUIC path (`quic.rs`) is a parallel entry into the same fingerprint code. A UDP datagram on the QUIC path is parsed as a QUIC Initial, its keys are derived from its Connection ID, its payload is decrypted, its CRYPTO frames are reassembled into a ClientHello, and that ClientHello goes through the same `ja4()` as a TCP one, emerging with a `q` transport marker.
|
||||
|
||||
## The intelligence store
|
||||
|
||||
`tlsfp-intel` turns a `FingerprintEvent` into a judgement. It owns an embedded SQLite database, which is the right choice here: zero configuration, a single file, transactional, and able to support a dashboard reading while a sensor writes (it opens in WAL mode with a busy timeout for exactly that). The store is **synchronous**, deliberately. A lookup is one indexed query and a capture is a plain loop; wrapping that in an async runtime would add complexity and buy nothing. The async runtime lives only in the web server, where concurrent readers genuinely exist.
|
||||
|
||||
```
|
||||
FingerprintEvent
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────┐
|
||||
│ matcher.rs exact lookup by (kind, value) │
|
||||
│ + JA4 fuzzy match on the │ intel_fingerprint
|
||||
│ (prefix, cipher-hash) columns │ intel_source
|
||||
│ -> MatchReport (verdict, score) │
|
||||
└───────────────────┬───────────────────────────┘
|
||||
│
|
||||
┌───────────────────┴───────────────────────────┐
|
||||
│ detect.rs records the observation, then │ observation
|
||||
│ runs six rules over a time │ alert
|
||||
│ window, raising Alerts │
|
||||
│ signal.rs the User-Agent / OS heuristics │
|
||||
│ the correlation rules read │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Schema (`schema.rs`).** Versioned migrations build five tables: `intel_source` (one row per feed, with its license), `intel_fingerprint` (the fingerprints, with `part_a`/`part_b` columns precomputed for JA4 fuzzy matching), `observation` (every fingerprint the sensor has seen, with its IP and time), and `alert` (every detection that fired). The observation and alert tables are what make the correlation rules possible: they give the sensor a memory.
|
||||
|
||||
**Seeding (`seed.rs`).** Three feeds are compiled into the binary as CSV and loaded with no network call: abuse.ch SSLBL (97 malware JA3s, CC0), the Salesforce `osx-nix` JA3 list (157 benign client JA3s, BSD-3), and a small curated C2 set (17 fingerprints). 271 in total. An optional `intel import` pulls ja4db.com and validates each record on the way in.
|
||||
|
||||
**Matching (`matcher.rs`).** An exact match is a single indexed lookup. A JA4 *fuzzy* match handles the reality that a client's full JA4 may not be in the feed but its capability-and-cipher prefix is: the value is split into its prefix and cipher-hash parts, and a hit on those alone is reported at a lower confidence. The hits are scored into a `MatchReport` with a threat score and a confidence, the two numbers the rest of the tool acts on.
|
||||
|
||||
**Detection (`detect.rs` + `signal.rs`).** For each event, the engine first records an observation, then runs six rules, all inside one transaction so the observation and any alerts it raises commit together:
|
||||
|
||||
| Rule | Fires when | Severity |
|
||||
|------|-----------|----------|
|
||||
| `known_bad` | the fingerprint matches a malicious feed entry | high / critical |
|
||||
| `ua_mismatch` | a JA4 disagrees with the User-Agent seen from the same IP (**the headline**) | high |
|
||||
| `os_mismatch` | a JA4T's OS disagrees with the OS the User-Agent claims | medium |
|
||||
| `first_seen` | this fingerprint has never been observed before | info |
|
||||
| `fp_rotation` | one IP has shown an unusual number of distinct fingerprints in the window | medium |
|
||||
| `monoculture` | one fingerprint has appeared from an unusual number of IPs in the window | low |
|
||||
|
||||
The correlation rules (`ua_mismatch`, `os_mismatch`) are the ones that catch evasion, and they are why the store keeps a memory: to know that the IP now sending a forged-Chrome JA4 also sent, ten seconds ago, an HTTP request whose User-Agent it can compare against. `signal.rs` holds the heuristics they read, for example that a Windows TCP stack and a Linux one differ in their SYN options, so a JA4T can be checked against a `User-Agent` that claims Windows.
|
||||
|
||||
## The binary: CLI and dashboard
|
||||
|
||||
`tlsfp` is the only crate that touches the outside world.
|
||||
|
||||
- **`cli.rs`** is the clap command tree: `pcap`, `live`, `serve`, and the `intel` subcommands. It owns the streaming output, the JSON serialization, and the wiring of a source to the engine to the store.
|
||||
- **`live.rs`** runs libpcap on a dedicated OS thread and bridges it to an async consumer, because libpcap's blocking read cannot be safely driven from inside the async runtime. It also drops privileges to the two capabilities capture needs.
|
||||
- **`report.rs`** is the forensic `--report` builder. Instead of streaming one line per event, it accumulates an in-process picture of every endpoint, fingerprint, and miss, then prints one ranked summary, folding in intelligence and detection automatically whenever a database is present.
|
||||
- **`serve.rs`** is the axum dashboard. It serves the built frontend assets and streams events and alerts to the browser over Server-Sent Events. The live feed has three sources: a replayed capture file (paced, optionally looping), a live interface, or, by default, a tail of the database so a separate `tlsfp live --detect` sensor surfaces in the browser.
|
||||
|
||||
```
|
||||
tlsfp live --detect ──writes──> intel.db <──tails── tlsfp serve ──SSE──> browser
|
||||
(WAL mode lets the reader and writer share the file)
|
||||
```
|
||||
|
||||
This is the deployment shape the WAL mode and busy timeout exist for: a headless sensor writing alerts into the database on one process, and a dashboard reading them out on another, against the same file, without either blocking the other.
|
||||
|
||||
## The threat model
|
||||
|
||||
A passive sensor processes input chosen entirely by an adversary. Anyone on the monitored network can send it any bytes. The architecture treats every input as hostile and bounds every cost.
|
||||
|
||||
**What the tool defends against:**
|
||||
|
||||
- **Malformed packets.** Every parser in the engine returns a typed error on bad input and never reads out of bounds, because the engine forbids `unsafe`. A truncated length, an extension that claims more bytes than the record holds, a certificate that ends mid-field: all are errors, not crashes. The JA4X parser, which walks the most hostile format (X.509 DER), is fuzzed with a property test to prove it.
|
||||
- **Memory exhaustion via the flow table.** An attacker who opens millions of flows, or sends segments with a gap that never fills, would grow an unbounded reassembler without limit. The flow table has a hard cap on tracked flows, an idle timeout that evicts the stale, and per-direction ceilings on both contiguous and parked-out-of-order bytes. The cost of an adversarial capture is fixed and survivable.
|
||||
- **A QUIC decryption that releases unverified plaintext.** The QUIC path verifies the AEAD tag before trusting the decrypted CRYPTO frames, so a forged Initial fails the tag and is dropped rather than feeding garbage to the parser.
|
||||
|
||||
**What the tool explicitly does not do:**
|
||||
|
||||
- **It does not decrypt application data.** It reads only what is plaintext on the wire: the handshake, the cleartext HTTP head, the QUIC Initial. It never has a private key and never sees TLS-1.3 certificates or HTTP/2 headers, which are encrypted.
|
||||
- **It does not attribute.** A fingerprint identifies a toolchain, not an actor. A `known_bad` hit means "this came from a known-suspicious toolchain," not "this is attacker X." The Emotet / Cobalt Strike JA3 collision is the standing reminder.
|
||||
- **It does not actively probe.** It sends nothing. A target cannot detect the sensor by watching for scans, because there are none.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) traces one frame all the way through this architecture, from `decode.rs` to a row in the `alert` table.
|
||||
- [ALGORITHMS.md](./ALGORITHMS.md) opens up the fingerprint stage: the exact byte construction of each hash and the QUIC key schedule.
|
||||
- [CONFORMANCE.md](./CONFORMANCE.md) records the published vector each stage is pinned to and every deliberate boundary.
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- 03-IMPLEMENTATION.md -->
|
||||
|
||||
# JA3/JA4 TLS Fingerprinting: Implementation
|
||||
|
||||
This document walks the code. It follows one packet from the moment it is read off disk to the moment a row lands in the `alert` table, then pulls out the three patterns that make the engine safe on a hostile network. It names files and functions, never line numbers, so it stays correct as the code moves. Read [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) first for the map.
|
||||
|
||||
## A frame's journey, end to end
|
||||
|
||||
Run `tlsfp pcap traffic.pcapng --detect`. Here is what happens to one frame carrying a ClientHello.
|
||||
|
||||
### 1. The source yields a frame
|
||||
|
||||
`PcapFileSource::open` (in `pipeline/source.rs`) opens the capture and `next_frame` hands back a `RawFrame`: a timestamp in nanoseconds, the link type, and a borrowed byte slice. The slice borrows the source's own read buffer, so no copy happens here. The binary's `Pipeline::run` loop pulls frames and feeds each to `Pipeline::feed`.
|
||||
|
||||
### 2. Decode strips the lower layers
|
||||
|
||||
`decode_frame` (in `pipeline/decode.rs`) uses `etherparse` to peel off the Ethernet and IP headers. It returns a `Decoded` value: a `DecodedSegment` for TCP (with the four-tuple, the sequence number, the flags, and the payload), a `DecodedDatagram` for UDP, or a `Skip` if the frame is not something we fingerprint. If the segment is a bare SYN, this is also where the TCP options are walked for JA4T, because JA4T reads the SYN itself, not the stream.
|
||||
|
||||
### 3. The flow table reassembles the stream
|
||||
|
||||
The segment's four-tuple becomes a `FlowKey`, and `FlowKey::from_pair` also returns a `Direction` so the two halves of the conversation stay separate. The pipeline looks up (or creates) the `StreamReassembler` for that direction and calls `push(seq, payload)`.
|
||||
|
||||
`StreamReassembler::push` (in `pipeline/flow.rs`) is the careful part. It places the payload at its sequence offset, merges it with the contiguous run if it fills the gap, parks it if it arrives early, and resolves any overlap with bytes already held. It returns a `PushOutcome` telling the caller whether new contiguous bytes became available. The contiguous bytes are reachable through `data()`. This is what makes a ClientHello split across reordered segments parse: the parser only ever sees the clean, in-order stream.
|
||||
|
||||
### 4. The protocol layer recognizes a handshake
|
||||
|
||||
With fresh contiguous bytes available, `StreamProtocol` (in `pipeline/tls.rs`) inspects the head of the stream. It reads the TLS record header, and when a complete handshake message is present it dispatches: a ClientHello to `parse_client_hello`, a ServerHello to `parse_server_hello`, a Certificate to the certificate reader. For a cleartext stream that looks like HTTP instead, it parses the request head for JA4H.
|
||||
|
||||
### 5. The parser reads the ClientHello without copying
|
||||
|
||||
`parse_client_hello` (in `parse/hello.rs`) walks the message with a `Reader` (in `parse/reader.rs`), a cursor that does bounds-checked reads over the borrowed bytes. Every field, the legacy version, the cipher suites, the extensions, is returned as a slice or a small vector that borrows the original buffer (`ClientHello<'pkt>` carries the lifetime). A length that overruns the buffer returns `Err(ParseError)`; the parser never reads past the end. Convenience accessors like `server_name`, `alpn_protocol`, and `supported_groups` decode individual extensions on demand.
|
||||
|
||||
### 6. The fingerprint is computed
|
||||
|
||||
The parsed `ClientHello` goes to `ja3` (in `ja3.rs`) and `ja4` (in `ja4.rs`). Each strips GREASE, assembles its string, and hashes it, JA3 with MD5, JA4 with truncated SHA-256. [ALGORITHMS.md](./ALGORITHMS.md) gives the exact construction. The results, plus the SNI, ALPN, and any User-Agent, become a `StreamEvent::ClientHello`, which `event.rs` wraps with the addresses and timestamp into a `FingerprintEvent`. That is the value that leaves the engine.
|
||||
|
||||
### 7. The store judges and detects
|
||||
|
||||
Back in the binary, the `FingerprintEvent` goes to `tlsfp-intel`. `IntelStore::match_event` runs the matcher (`matcher.rs`) over every fingerprint the event carries and returns a `MatchReport` for each that hit. `IntelStore::detect` opens a transaction and calls `detect::run` (in `detect.rs`), which:
|
||||
|
||||
1. records the event as a row in `observation`,
|
||||
2. runs the six rules, correlating the new fingerprint against what this IP and this fingerprint have done inside the time window,
|
||||
3. persists any `Alert` it raises into the `alert` table,
|
||||
4. commits, so the observation and its alerts land atomically.
|
||||
|
||||
### 8. The writer prints
|
||||
|
||||
The binary serializes the event and any reports and alerts, as a readable line or as JSON, and writes it to stdout. For `--report` it instead feeds the event to the `ReportBuilder`, which accumulates and prints one summary at the end. The frame is done.
|
||||
|
||||
```
|
||||
RawFrame ─decode─> DecodedSegment ─push─> contiguous bytes ─parse─> ClientHello
|
||||
│ │
|
||||
│ ja3()/ja4()
|
||||
│ ▼
|
||||
└──────────────────────────────────────────────────────> FingerprintEvent
|
||||
│
|
||||
match_event + detect (one txn) │
|
||||
▼
|
||||
MatchReport + Alert ─> stdout
|
||||
```
|
||||
|
||||
## Pattern one: zero-copy, bounds-checked parsing
|
||||
|
||||
The engine parses hostile binary input, so the parsing strategy is the security strategy. Two choices carry the weight.
|
||||
|
||||
**Everything borrows.** `parse_client_hello` returns a `ClientHello<'pkt>` that holds slices into the original packet buffer. No field is copied out during parsing. This keeps the hot path allocation-free (the benchmarks in `benches/fingerprint.rs` show why that matters) and means a ClientHello with a hundred extensions costs no more memory than the packet it came in.
|
||||
|
||||
**Every read is bounds-checked, and overruns are errors, not panics.** The `Reader` is the only thing that advances through the bytes, and each of its reads checks the remaining length first. A truncated length field, an extension that claims more bytes than the record holds, a certificate that ends mid-OID: every one returns `Err(ParseError)`. The `parse/` tests feed exactly these malformed inputs and assert errors. Because the whole crate sets `unsafe_code = "forbid"` in `Cargo.toml`, there is no escape hatch by which a parser bug could become an out-of-bounds read. This is the structural answer to the Heartbleed class of bug discussed in [01-CONCEPTS.md](./01-CONCEPTS.md): in C the bug is a memory disclosure, here it is a `Result::Err`.
|
||||
|
||||
## Pattern two: bounded reassembly
|
||||
|
||||
`StreamReassembler` is where an adversary's input could grow without limit, so every dimension is capped by `ReassemblyLimits`:
|
||||
|
||||
- a ceiling on **contiguous bytes** kept per direction, so a single huge stream cannot grow forever,
|
||||
- a ceiling on **parked out-of-order bytes**, so an attacker who sends segment 2 but never segment 1 cannot make the reassembler hold the gap open indefinitely,
|
||||
- a cap on **parked segments**, so many tiny out-of-order segments cannot blow up the bookkeeping.
|
||||
|
||||
When a stream hits a cap it is marked `capped()` and stops accepting more, rather than growing. At the pipeline level a flow cap and an idle timeout bound the *number* of flows, evicting the oldest and stalest when the table is full. The counters (`Counters`, reported with `-v`) expose `segments_dropped` and `unfinished_tls_streams` so an operator can see when bounds bit. The design principle: an adversarial capture must cost a *fixed, known* amount of memory, never an unbounded one. [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) frames this as the threat model; this is where it is enforced.
|
||||
|
||||
## Pattern three: passive QUIC decryption
|
||||
|
||||
`quic.rs` is the most cryptographically involved part of the engine, and the comments in it are worth reading in full. The flow:
|
||||
|
||||
1. **Locate the Initial.** `InitialPacket::parse` finds an Initial packet inside a UDP datagram and reads the cleartext header fields: the version, the Destination Connection ID, the token. A connection ID longer than twenty bytes marks the packet as not the version we handle, and is rejected rather than misparsed.
|
||||
2. **Derive the keys.** `InitialPacket::client_keys` calls `InitialKeys::client(dcid, version)`. The derivation is straight from RFC 9001 (v1) and RFC 9369 (v2): `HKDF-Extract` the connection ID under the version's `INITIAL_SALT`, `HKDF-Expand-Label` to the `"client in"` secret, then expand that to the AEAD key, the IV, and the header-protection key. The salt is the only thing that differs between v1 and v2, and it is exactly what makes keys derived under the wrong version fail the tag.
|
||||
3. **Open the packet.** `InitialPacket::open` removes header protection, then runs the AEAD open. The tag check here is load-bearing: a passive observer cannot tell a client Initial from a server one by any cleartext field, so the fact that *only a packet the client actually protected under these keys will verify* is what identifies the direction. A forged or server packet fails the tag and is dropped, never released to the parser.
|
||||
4. **Reassemble the CRYPTO frames.** `walk_crypto_frames` iterates the decrypted frames, and a `CryptoAssembler` stitches them, by offset, into the TLS ClientHello (a QUIC ClientHello can span several Initial packets). `client_hello()` yields the assembled bytes.
|
||||
5. **Fingerprint.** The assembled ClientHello goes through the same `ja4()` as the TCP path, with `Transport::Quic`, producing a `q`-prefixed fingerprint.
|
||||
|
||||
The key-derivation code is pinned to the RFC test vectors: `client_initial_keys_match_rfc9001_appendix_a1` and `client_initial_keys_match_rfc9369_appendix_a` derive the keys and assert them byte-for-byte against the published Appendix A values. [CONFORMANCE.md](./CONFORMANCE.md) records the boundaries of the QUIC support.
|
||||
|
||||
## How to read the codebase yourself
|
||||
|
||||
A productive order, given the journey above:
|
||||
|
||||
1. `parse/reader.rs`, then `parse/hello.rs`. The `Reader` is the whole safety story in one small file; `hello.rs` is the most important parse.
|
||||
2. `ja4.rs`, then `ja3.rs`. The headline fingerprint, then the legacy one it replaced. Read them side by side to see the sort.
|
||||
3. `pipeline/flow.rs`. The reassembler. The bounds are the interesting part.
|
||||
4. `pipeline/mod.rs`. The loop that ties the stages together, where `PipelineConfig` sets the bounds.
|
||||
5. `quic.rs`. Save it for last; it is self-contained and the comments carry it.
|
||||
|
||||
Then cross to `tlsfp-intel`: `matcher.rs` for scoring, `detect.rs` for the rules. Finally `tlsfp/src/cli.rs` to see it all wired.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [ALGORITHMS.md](./ALGORITHMS.md) opens up step 6 (and the QUIC key schedule of pattern three) in byte-level detail.
|
||||
- [CONFORMANCE.md](./CONFORMANCE.md) records what every stage accepts, rejects, and is pinned to.
|
||||
- [04-CHALLENGES.md](./04-CHALLENGES.md) suggests changes that will force you to understand these patterns by modifying them.
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- 04-CHALLENGES.md -->
|
||||
|
||||
# JA3/JA4 TLS Fingerprinting: Challenges
|
||||
|
||||
The fastest way to understand this codebase is to change it. These challenges are ordered roughly by difficulty, each names the files it touches, and each ends with how you would prove it works the way the rest of the project proves things: a known-answer test, an integration capture, or a real run of the binary. Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) and [ALGORITHMS.md](./ALGORITHMS.md) first.
|
||||
|
||||
## Beginner
|
||||
|
||||
These build familiarity with the pipeline without touching the cryptography.
|
||||
|
||||
### 1. Add a fingerprint kind to `intel lookup`
|
||||
|
||||
The CLI's `intel lookup` accepts a fixed set of fingerprint kinds. Trace `FpKind::from_token` in `model.rs` and the error message in `cli.rs`, and make sure every kind the engine emits is accepted (and that the help text lists them).
|
||||
|
||||
*Prove it:* run `tlsfp intel lookup ja4t 64240_2-1-3-1-1-4_1460_8` and get a verdict instead of an "unknown kind" error.
|
||||
|
||||
### 2. A new readable output column
|
||||
|
||||
The streaming output prints the JA4 and JA3 per line. Add the SNI's registered domain (the eTLD+1) as a column, or a one-character flag when the fingerprint had an intelligence hit. The formatting lives in `write_event` in `cli.rs`.
|
||||
|
||||
*Prove it:* run `tlsfp pcap testdata/pcap/tls-handshake.pcapng` and read the new column.
|
||||
|
||||
### 3. Seed a fourth feed
|
||||
|
||||
`seed.rs` compiles three CSV feeds into the binary. Add a fourth (find a public JA4 or JA3 feed, record its license in `NOTICE.md` and the `intel_source` table). Follow the existing `load_*` functions and the `NewFingerprint` shape.
|
||||
|
||||
*Prove it:* `tlsfp intel seed` then `tlsfp intel stats` shows the new feed and its row count.
|
||||
|
||||
## Intermediate
|
||||
|
||||
These require understanding a parser or the detection engine.
|
||||
|
||||
### 4. A new detection rule
|
||||
|
||||
`detect.rs` runs six rules over a time window, reading the `observation` table for memory. Add a seventh. A good candidate: **`port_anomaly`**, a TLS handshake whose JA4 says HTTP/2 (`h2` ALPN) arriving on a port that is not 443 or 8443, which is a common C2 trait. The rule reads the event's destination port and the ALPN already in the prefix; it needs no new table.
|
||||
|
||||
*Prove it:* add a unit test in `tests/detect.rs` that feeds a crafted `FingerprintEvent` and asserts the alert, the way the existing rule tests do.
|
||||
|
||||
### 5. Decode DTLS handshakes
|
||||
|
||||
The JA4 code already has a `d` transport marker and a DTLS version word, but the pipeline never feeds it a DTLS handshake. DTLS rides UDP and frames its handshake differently from TLS (it adds message sequence numbers and fragment offsets). Wire a DTLS path in `pipeline/decode.rs` and a DTLS record reader alongside the TLS one in `parse/record.rs`, then route to the existing `ja4` with `Transport::Dtls`.
|
||||
|
||||
*Prove it:* capture a WebRTC or OpenVPN-DTLS handshake, add it to `testdata/pcap/`, and add an integration test asserting a `d`-transport JA4.
|
||||
|
||||
### 6. Tune and measure the bounds
|
||||
|
||||
The reassembler's `ReassemblyLimits` and the pipeline's `PipelineConfig` set the memory ceilings. Build a deliberately adversarial capture (thousands of flows, segments with permanent gaps) with a small script, and watch the counters (`-v`) show `segments_dropped` and flow eviction. Then find the smallest bounds that still fingerprint the vendored captures cleanly.
|
||||
|
||||
*Prove it:* a `cargo bench` run before and after, plus the counters from `-v` on your adversarial capture, showing bounded memory.
|
||||
|
||||
## Advanced
|
||||
|
||||
These reach into the cryptography or the architecture.
|
||||
|
||||
### 7. Pin a full QUIC v2 protected-packet vector
|
||||
|
||||
[CONFORMANCE.md](./CONFORMANCE.md) notes that the v2 *key schedule* is pinned to RFC 9369 Appendix A but the full v2 *protected packet* is not, because the only transcription available was byte-corrupted. Obtain the v2 protected packet bytes from a byte-exact source (a raw capture or a trusted hex dump, not a copy-paste through a summarizer), add it to the `quic.rs` tests, and assert the decrypted ClientHello.
|
||||
|
||||
*Prove it:* a new KAT in `quic.rs` that decrypts the v2 packet and matches the expected ClientHello, alongside the existing v1 full-packet test.
|
||||
|
||||
### 8. Active fingerprint scanning (a separate tool)
|
||||
|
||||
This sensor is strictly passive. Build a *separate* binary (a new crate, so the passive guarantee in `tlsfp-core` is never violated) that actively connects to a host, completes a handshake, and fingerprints the *server's* JA4S, the way `tlsd`-style scanners do. This is a different threat model (you are now sending packets and are detectable), so it must be a clearly separate tool.
|
||||
|
||||
*Prove it:* fingerprint a known server (for example a Cloudflare host) and confirm its JA4S is stable across runs.
|
||||
|
||||
### 9. A clustering view of the catalogue
|
||||
|
||||
The dashboard shows individual fingerprints. Add a view that **clusters** them: group JA4s by shared prefix (same TLS version, cipher count, extension count, ALPN), so an analyst sees "these forty fingerprints are all variations of one TLS stack." The data is already in the `part_a` column the JA4 fuzzy matcher uses.
|
||||
|
||||
*Prove it:* a dashboard view, plus a `matcher.rs` query that returns prefix groups with their member counts.
|
||||
|
||||
## Expert
|
||||
|
||||
These are research-grade and open-ended.
|
||||
|
||||
### 10. Detect impersonation by cross-layer disagreement, statistically
|
||||
|
||||
The `ua_mismatch` and `os_mismatch` rules are heuristic and binary. Replace them with a model: for each `(JA4, JA4T, User-Agent)` triple seen on the network, learn the *normal* joint distribution, then score new triples by how surprising they are. A real Chrome on Windows is common; a forged-Chrome JA4 with a Linux JA4T is rare and should score high. This is the principled version of the evasion detection in [01-CONCEPTS.md](./01-CONCEPTS.md).
|
||||
|
||||
*Prove it:* feed a capture containing `curl-impersonate` traffic (generate it yourself) and show the model flags it while leaving real browsers alone.
|
||||
|
||||
### 11. Survive an adversary who fingerprints your sensor
|
||||
|
||||
A sophisticated attacker on the monitored network may try to *detect* or *evade* the sensor by crafting traffic that exploits the bounds: flows tuned to ride just under the eviction timeout, ClientHellos fragmented to defeat reassembly, GREASE in unexpected positions. Threat-model the sensor itself, build the adversarial captures, and harden the pipeline against each. This is the passive-sensor analogue of evading an IDS.
|
||||
|
||||
*Prove it:* a documented set of adversarial captures in `testdata/`, each with a test asserting the sensor either fingerprints correctly or fails safe (bounded, no crash, no missed handshake that fit within the bounds).
|
||||
|
||||
### 12. Encrypted Client Hello (ECH)
|
||||
|
||||
The whole project rests on the ClientHello being plaintext. **ECH** (Encrypted Client Hello) is the IETF effort to change that, encrypting the sensitive parts of the ClientHello under a key published in DNS. Study the ECH draft, determine exactly what a passive observer can and cannot still see (the outer ClientHello is still visible, and it is itself fingerprintable), and write up how JA4 degrades under ECH adoption and what signal survives.
|
||||
|
||||
*Prove it:* a written analysis grounded in the ECH specification, plus a capture of an ECH handshake showing what the tool can still fingerprint from the outer hello.
|
||||
|
||||
## A note on contributing back
|
||||
|
||||
If you build something here that is genuinely useful, the JA4+ ecosystem is young and the public feeds are thin. A well-licensed feed, a clustering view, or a rigorous ECH analysis would be welcome upstream at [FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4) and in the broader community. The point of the project is to learn how this works; the bonus is that the field still has open problems an afternoon of work can move.
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- ALGORITHMS.md -->
|
||||
|
||||
# JA3/JA4 TLS Fingerprinting: Algorithms
|
||||
|
||||
This is the byte-level reference. For each fingerprint it gives the exact construction, a worked example pinned to a published vector, and the rules that bite at the edges. Every example here is also a test in the codebase, so if you doubt a step you can run it. Read [01-CONCEPTS.md](./01-CONCEPTS.md) for *why* these are shaped the way they are; this is the *how*.
|
||||
|
||||
A note on truncated hashes: JA4 and its relatives take a SHA-256 and keep only the **first 12 hex characters** (the first 6 bytes). JA3 takes a full MD5 (32 hex characters). Where this document writes "truncated SHA-256," it means those first 12 hex characters.
|
||||
|
||||
## JA3 and JA3S
|
||||
|
||||
JA3 concatenates five decimal fields from the ClientHello, joins values inside a field with `-` and the fields with `,`, and takes the MD5.
|
||||
|
||||
```
|
||||
field 1: legacy_version (one number)
|
||||
field 2: cipher_suites (GREASE stripped, wire order, joined with -)
|
||||
field 3: extension_types (GREASE stripped, wire order, joined with -)
|
||||
field 4: supported_groups (GREASE stripped, joined with -)
|
||||
field 5: ec_point_formats (joined with -)
|
||||
|
||||
joined with commas, then MD5
|
||||
```
|
||||
|
||||
**Worked example** (the original Salesforce vector, `salesforce_client_vector_one` in `ja3.rs`):
|
||||
|
||||
```
|
||||
769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0
|
||||
│ MD5
|
||||
▼
|
||||
ada70206e40642a3e4461f35503241d5
|
||||
```
|
||||
|
||||
The leading `769` is the `legacy_version` field in decimal: `769` is `0x0301`, TLS 1.0, which is what this old vector carried. A modern TLS 1.2 or 1.3 client puts `771` (`0x0303`) here, because TLS 1.3 freezes the legacy field at 1.2 and signals the real version in an extension. Whatever the client wrote, it is rendered as a plain decimal number. The list fields are kept in **wire order**, which is exactly why extension-order shuffling broke JA3.
|
||||
|
||||
**JA3S** is the server's answer: three fields from the ServerHello (version, the single selected cipher, the extensions), same comma join, same MD5. A server picks one cipher, so field two is one number, not a list.
|
||||
|
||||
The whole reason this tool still computes JA3 is that the public feeds (`abuse.ch SSLBL` and the curated C2 set) are expressed in JA3, and the detection engine looks the JA3 up alongside the JA4 so a feed hit is never missed.
|
||||
|
||||
## JA4 and JA4S
|
||||
|
||||
JA4 has three underscore-separated sections: a 10-character readable prefix `_` the cipher hash `_` the extension hash. The construction is in `ja4()` and `ja4_prefix()` in `ja4.rs`.
|
||||
|
||||
### The prefix (10 characters)
|
||||
|
||||
```
|
||||
t 13 d 15 16 h2
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ └── first ALPN value: first + last char (or "00")
|
||||
│ │ │ │ └──────── extension count, two digits, capped at 99
|
||||
│ │ │ └────────────── cipher count, two digits, capped at 99
|
||||
│ │ └─────────────────── SNI: 'd' if a server_name extension is present, else 'i'
|
||||
│ └───────────────────────── TLS version word: 13, 12, 11, 10, or d1/d2/d3 for DTLS
|
||||
└────────────────────────────── transport: t=TCP, q=QUIC, d=DTLS
|
||||
```
|
||||
|
||||
Two subtleties the code handles. The **version** is the highest non-GREASE value in the `supported_versions` extension, not the `legacy_version` field, because a TLS 1.3 client freezes the legacy field at 1.2 (`select_version` in `ja4.rs`). The **ALPN characters** are the first and last byte of the first ALPN protocol if both are ASCII alphanumeric (so `h2` stays `h2`, `http/1.1` becomes `h1`); if not, the code falls back to the first and last hex nibble; an absent ALPN is `00` (`alpn_chars` in `ja4.rs`).
|
||||
|
||||
### The cipher hash
|
||||
|
||||
Take the cipher suites, strip GREASE, format each as four lowercase hex digits, **sort the strings**, join with `,`, and take the truncated SHA-256.
|
||||
|
||||
**Worked example** (`foxio_cipher_section_vector` in `ja4.rs`):
|
||||
|
||||
```
|
||||
sorted cipher CSV:
|
||||
002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9
|
||||
│ SHA-256, first 12 hex
|
||||
▼
|
||||
8daaf6152771
|
||||
```
|
||||
|
||||
The **sort** is the entire JA4 innovation. The client may send these in any order, and a modern client shuffles them, but sorting throws the order away and keeps the set. That is why this hash is stable where JA3's field-three was not.
|
||||
|
||||
### The extension hash
|
||||
|
||||
Take the extension types, **remove SNI (`0x0000`) and ALPN (`0x0010`)** because those are already represented in the prefix, strip GREASE, format as four hex digits, sort, join with `,`. Then, if a `signature_algorithms` extension is present, append `_` and the signature algorithms **in their original order** (not sorted, because their order is itself meaningful and stable). Take the truncated SHA-256 of the whole thing (`ja4_extension_raw` in `ja4.rs`).
|
||||
|
||||
**Worked example** (`foxio_extension_section_vector` in `ja4.rs`):
|
||||
|
||||
```
|
||||
sorted exts (SNI + ALPN removed) sig algs (original order)
|
||||
0005,000a,000b,000d,0012,0015,0017,001b, _ 0403,0804,0401,0503,
|
||||
0023,002b,002d,0033,4469,ff01 0805,0501,0806,0601
|
||||
│ SHA-256, first 12 hex
|
||||
▼
|
||||
e5627efa2ab1
|
||||
```
|
||||
|
||||
### Putting it together
|
||||
|
||||
```
|
||||
t13d1516h2 _ 8daaf6152771 _ e5627efa2ab1
|
||||
```
|
||||
|
||||
That is a real Chrome JA4. JA4 also has a **raw** form, where the cipher and extension sections are the CSV strings themselves instead of their hashes, for debugging; the tool emits it under `--json`.
|
||||
|
||||
**JA4S** mirrors this for the server: the server picks one cipher (so the cipher section is that single value in hex, not a hash of a list) and its extensions are hashed in **wire order, not sorted**, because a server does not shuffle its own extensions (`ja4s` in `ja4.rs`).
|
||||
|
||||
## JA4H: the HTTP client
|
||||
|
||||
JA4H fingerprints one cleartext HTTP request (`ja4h.rs`). It reads the method, the HTTP version, whether the request carries cookies and a referer, the count and order of the other header names, the accept-language, and the cookie names and values. The signal it captures: a request that omits an accept-language and sends no cookies is far more likely to be a script than a person, and the prefix makes that visible in the first few characters.
|
||||
|
||||
This works on **cleartext HTTP only**. Over HTTPS the request is encrypted; over HTTP/2 the headers are HPACK-compressed and unreadable to a passive observer. So JA4H fires on plain `http://` traffic, which on a modern network is the automated and legacy clients, exactly the interesting population.
|
||||
|
||||
## JA4X: the certificate toolchain
|
||||
|
||||
JA4X fingerprints an X.509 certificate by *how it was built*, not what it says (`ja4x.rs`). It extracts three lists of object identifiers (OIDs): those in the issuer name, those in the subject name, and those among the extensions, each in the order they were written into the DER. Each list is joined with `,` and hashed (truncated SHA-256), giving three sections:
|
||||
|
||||
```
|
||||
issuer_oids_hash _ subject_oids_hash _ extension_oids_hash
|
||||
```
|
||||
|
||||
Two certificates minted by the same software with the same template share a JA4X even when every name, serial, and key differs, which clusters certificates from one malware family or one CA toolchain. Passively this only works on **TLS 1.2 and earlier**, where the Certificate message travels in the clear; TLS 1.3 encrypts it. The OID extraction walks attacker-controlled DER through the minimal reader in `der.rs`, which is why `tests/ja4x.rs` includes a property-test fuzz harness: a crafted certificate must produce an error or a fingerprint, never a panic.
|
||||
|
||||
## JA4T: the TCP stack
|
||||
|
||||
JA4T fingerprints the TCP/IP stack (the operating system) from a single SYN (`ja4t.rs`). The format is four parts joined with `_`:
|
||||
|
||||
```
|
||||
window_size _ option_kinds(joined with -) _ MSS _ window_scale
|
||||
```
|
||||
|
||||
The option kinds are the TCP option *kind numbers* in the order they appear in the SYN; a missing MSS or window scale option is reported as `0`.
|
||||
|
||||
**Worked examples** (`foxio_windows_default_vector` and `foxio_windowed_vector` in `ja4t.rs`):
|
||||
|
||||
```
|
||||
Windows default: 64240_2-1-3-1-1-4_1460_8
|
||||
Windowed: 65535_2-1-3-1-1-8-4-0-0_1346_6
|
||||
```
|
||||
|
||||
JA4T is the layer an evasion tool forgets. A script impersonating Chrome's TLS still rides its host OS's TCP stack, so its JA4T says "Linux" while its forged JA4 and its `User-Agent` say "Windows Chrome." That three-way disagreement is what the `os_mismatch` and `ua_mismatch` detection rules turn into an alert.
|
||||
|
||||
## GREASE stripping
|
||||
|
||||
Every list-based fingerprint above strips GREASE first (`grease.rs`). The GREASE values are the sixteen reserved code points of the form `0x?a?a` (`0x0a0a`, `0x1a1a`, ... `0xfafa`), defined by RFC 8701. A client injects them at random to keep the ecosystem extensible, so including them would make the fingerprint change on every connection. `is_grease` is a single check against that pattern, applied in every CSV builder before sorting or hashing.
|
||||
|
||||
## QUIC Initial key derivation
|
||||
|
||||
To read the ClientHello inside a QUIC Initial packet, the tool derives the packet's protection keys from its Destination Connection ID, which is in the clear (`quic.rs`, `InitialKeys::client`). The schedule is HKDF (RFC 5869) with the labels and salt from RFC 9001 (v1) and RFC 9369 (v2):
|
||||
|
||||
```
|
||||
initial_secret = HKDF-Extract(salt = INITIAL_SALT[version], ikm = DCID)
|
||||
client_secret = HKDF-Expand-Label(initial_secret, "client in", 32)
|
||||
key = HKDF-Expand-Label(client_secret, <version key label>, 16)
|
||||
iv = HKDF-Expand-Label(client_secret, <version iv label>, 12)
|
||||
hp = HKDF-Expand-Label(client_secret, <version hp label>, 16)
|
||||
```
|
||||
|
||||
The **only** thing that differs between v1 and v2 is the salt (and the expand labels), and that is deliberate: keys derived under the wrong version's salt fail the AEAD tag, which is how the tool tells a v1 packet from a v2 one without trusting a version field it has not yet authenticated. With the keys in hand, `InitialPacket::open` removes header protection and runs the AEAD open. The tag check is what identifies the *client* direction: a passive observer cannot distinguish a client Initial from a server one by any cleartext field, but only a packet the client actually protected under these keys verifies, so a successful open *is* the proof it was a client Initial.
|
||||
|
||||
The derivation is pinned byte-for-byte to the RFCs: `client_initial_keys_match_rfc9001_appendix_a1` and `client_initial_keys_match_rfc9369_appendix_a` in `quic.rs` derive the keys from the appendix's connection ID and assert the appendix's key, IV, and header-protection values.
|
||||
|
||||
## Hash choices, summarized
|
||||
|
||||
| Fingerprint | Hash | Why |
|
||||
|-------------|------|-----|
|
||||
| JA3 / JA3S | MD5, full | matches the original definition and every public JA3 feed |
|
||||
| JA4 / JA4S | SHA-256, first 12 hex | the FoxIO definition; collision-resistant, shorter to read |
|
||||
| JA4H / JA4X / JA4T | SHA-256, first 12 hex (sections) | same family, same truncation |
|
||||
|
||||
MD5 for JA3 is not a security choice and not a weakness here: a fingerprint is an identifier, not a authenticator, and reproducing the feeds' exact hashes is the requirement. JA4 moved to SHA-256 because it was a clean break and there was no installed base to stay compatible with.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [CONFORMANCE.md](./CONFORMANCE.md) lists every published vector each of these is pinned to, and the exact scope each fingerprint does and does not cover.
|
||||
- [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) shows where in the pipeline these functions are called.
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<!-- ©AngelaMos | 2026 -->
|
||||
<!-- CONFORMANCE.md -->
|
||||
|
||||
# JA3/JA4 TLS Fingerprinting: Conformance
|
||||
|
||||
A fingerprint is only useful if it agrees with everyone else's. A JA4 this tool computes must equal the JA4 FoxIO's reference implementation computes for the same handshake, or a hash on a shared feed means nothing. This document states exactly what each fingerprint conforms to, the published vector it is pinned to in the test suite, and every place the tool deliberately narrows its scope. Where it says "pinned," there is a known-answer test that fails if the output drifts.
|
||||
|
||||
## Reference specifications
|
||||
|
||||
| Fingerprint | Authority | Document |
|
||||
|-------------|-----------|----------|
|
||||
| JA3 / JA3S | Salesforce (2017) | the original [TLS Fingerprinting with JA3 and JA3S](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/) |
|
||||
| JA4 / JA4S | FoxIO (2023) | [JA4](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md), [JA4S](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4S.md) |
|
||||
| JA4H | FoxIO | [JA4H](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4H.md) |
|
||||
| JA4X | FoxIO | [JA4X](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4X.md) |
|
||||
| JA4T / JA4TS | FoxIO | [JA4T](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4T.md) |
|
||||
| GREASE handling | IETF | [RFC 8701](https://www.rfc-editor.org/rfc/rfc8701.html) |
|
||||
| QUIC v1 keys | IETF | [RFC 9001](https://www.rfc-editor.org/rfc/rfc9001.html) Section 5.2, Appendix A |
|
||||
| QUIC v2 keys | IETF | [RFC 9369](https://www.rfc-editor.org/rfc/rfc9369.html) Section 3.3.1, Appendix A |
|
||||
|
||||
The FoxIO JA4+ specifications carry their own license (FoxIO License 1.1), which governs the *specification*, not this independent implementation of it. This tool ships under AGPL 3.0.
|
||||
|
||||
## Per-fingerprint conformance
|
||||
|
||||
### JA3 / JA3S
|
||||
|
||||
- **Conforms to** the original Salesforce field order and the MD5 of the comma-joined decimal fields.
|
||||
- **Pinned to** the two client vectors from the Salesforce post and a server vector, in `ja3.rs` (`salesforce_client_vector_one` produces `ada70206e40642a3e4461f35503241d5`; `salesforce_client_vector_two_empty_fields` covers the empty-field case; `server_vector_round_trips_through_md5` covers JA3S).
|
||||
- **Scope.** Computed from the ClientHello and ServerHello. GREASE is stripped per RFC 8701. JA3 is retained despite being unstable on modern browsers because the public malware feeds are expressed in it; see [01-CONCEPTS.md](./01-CONCEPTS.md).
|
||||
|
||||
### JA4 / JA4S
|
||||
|
||||
- **Conforms to** the FoxIO JA4 construction: a 10-character readable prefix, a truncated-SHA-256 of the sorted cipher list, and a truncated-SHA-256 of the sorted extension list with SNI and ALPN removed and the signature algorithms appended in original order.
|
||||
- **Pinned to** the FoxIO section vectors in `ja4.rs`: the cipher section `8daaf6152771` and the extension section `e5627efa2ab1`, which together with the prefix form the canonical `t13d1516h2_8daaf6152771_e5627efa2ab1`. An all-empty ClientHello produces the zero hash `000000000000` (`empty_input_is_the_zero_hash`), and the truncation is asserted to be exactly 12 hex characters (`truncation_is_twelve_hex_chars`).
|
||||
- **Scope.** Both the hashed and the raw form are produced; the CLI prints the hashed form and exposes the raw form under `--json`. The version word is taken from `supported_versions`, not the frozen legacy field. JA4S hashes server extensions in wire order, not sorted.
|
||||
|
||||
### JA4H
|
||||
|
||||
- **Conforms to** the FoxIO JA4H construction over a single cleartext HTTP request.
|
||||
- **Pinned to** the request-line and header parse in `ja4h.rs` (`parses_request_line_and_headers`, `version_codes`).
|
||||
- **Scope.** Cleartext HTTP/1.x only. HTTPS requests are encrypted and HTTP/2 headers are HPACK-compressed, so neither is fingerprinted passively. This is a property of passive observation, not a limitation of the implementation.
|
||||
|
||||
### JA4X
|
||||
|
||||
- **Conforms to** the FoxIO JA4X construction: three truncated-SHA-256 hashes over the issuer OIDs, the subject OIDs, and the extension OIDs, each in DER order.
|
||||
- **Pinned to** the OID-extraction tests in `ja4x.rs` (`extracts_issuer_oids_in_order`, `extracts_extension_oids`) and hardened by the property-test fuzz harness in `tests/ja4x.rs`.
|
||||
- **Scope.** Passive JA4X works on **TLS 1.2 and earlier**, where the Certificate message is in the clear; TLS 1.3 encrypts it. The DER reader (`der.rs`) is bounded: a certificate that ends mid-field returns an error or an empty section, never a panic (`certificate_ending_after_subject_is_handled_without_panic`). An out-of-bounds slice on a crafted certificate is a security bug, not a cosmetic one, which is why this path is fuzzed.
|
||||
|
||||
### JA4T / JA4TS
|
||||
|
||||
- **Conforms to** the FoxIO JA4T format: window size, option kinds, MSS, and window scale, joined with underscores.
|
||||
- **Pinned to** the FoxIO vectors in `ja4t.rs` (`foxio_windows_default_vector` produces `64240_2-1-3-1-1-4_1460_8`; `foxio_windowed_vector`; `missing_mss_and_scale_report_zero`) and to the decoder that walks SYN options in `pipeline/decode.rs` (`ja4t_walk_reproduces_the_windows_default_vector`, `ja4t_walk_counts_trailing_end_of_list_padding`, `ja4t_walk_survives_truncated_options`).
|
||||
- **Scope.** Read from a SYN (JA4T) or SYN-ACK (JA4TS). A missing MSS or window scale option is reported as `0`, per the spec. The option walker tolerates truncated and padded option lists.
|
||||
|
||||
## QUIC conformance
|
||||
|
||||
- **Conforms to** RFC 9001 (QUIC v1) and RFC 9369 (QUIC v2) for client Initial key derivation and packet protection removal.
|
||||
- **Pinned to** the RFC Appendix A key vectors in `quic.rs`: `client_initial_keys_match_rfc9001_appendix_a1` and `client_initial_keys_match_rfc9369_appendix_a` derive the key, IV, and header-protection key from the appendix connection ID and assert them byte-for-byte. The integration capture `quic-with-several-tls-frames.pcapng` yields the stable `q13d0310h3_55b375c5d22e_cd85d2d88918`.
|
||||
- **Scope and a deliberate boundary.** The tool decrypts **client Initial** packets only, deriving keys from each packet's own Destination Connection ID with no server secret and no direction detection. It does not track connection migration, does not decrypt 0-RTT or Handshake packets, and does not reassemble across a connection ID change. A version field other than v1 or v2 increments `quic_version_unsupported` and is skipped rather than guessed. The full v2 *protected-packet* decrypt is not pinned to a published full-packet vector (the only available transcription was byte-corrupted in transit); the v2 **key schedule** is pinned to RFC 9369 Appendix A, and the decrypt path is the same AEAD the v1 full-packet test exercises.
|
||||
|
||||
## Parsing and robustness boundaries
|
||||
|
||||
The engine forbids `unsafe`, so the following are guarantees, not aspirations:
|
||||
|
||||
- **A malformed length is an error, never an out-of-bounds read.** Every parser reads through the bounds-checked `Reader`; a field that claims more bytes than remain returns `ParseError`. The `parse` and `reassembly` test suites feed truncated and overlapping input and assert clean errors.
|
||||
- **An adversarial capture costs bounded memory.** The flow table caps the number of tracked flows, evicts on an idle timeout, and ceilings both contiguous and parked-out-of-order bytes per direction. A stream that hits a cap is marked `capped` and stops growing. The counters expose `segments_dropped` and `unfinished_tls_streams`.
|
||||
- **A QUIC packet that fails its AEAD tag is dropped.** Unverified plaintext is never released to the TLS parser.
|
||||
|
||||
## Deliberate non-goals
|
||||
|
||||
These are choices, recorded so they are not mistaken for gaps:
|
||||
|
||||
- **No application-data decryption.** The tool reads only what is plaintext on the wire. It has no private key and never sees TLS 1.3 certificates or HTTP/2 headers.
|
||||
- **No active probing.** The tool sends nothing. It cannot be detected by watching for scans.
|
||||
- **No attribution.** A fingerprint identifies a toolchain, not an actor. The shared `72a589da586844d7f0818ce684948eea` JA3 across some Emotet and Cobalt Strike samples is the standing example: same TLS library, different malware. A `known_bad` verdict means "known-suspicious toolchain," nothing more.
|
||||
- **No DTLS fingerprinting yet.** The transport marker `d` exists in the JA4 code, but DTLS handshake capture is not wired into the pipeline. This is the most natural extension; see [04-CHALLENGES.md](./04-CHALLENGES.md).
|
||||
|
||||
## How to re-verify
|
||||
|
||||
```bash
|
||||
cargo test --workspace # every pinned vector above runs here
|
||||
cargo test -p tlsfp-core quic # just the QUIC key-schedule vectors
|
||||
```
|
||||
|
||||
Against a second implementation, capture a handshake and compare:
|
||||
|
||||
```bash
|
||||
tlsfp pcap capture.pcapng --json | jq '.ja4.hash, .ja4.raw'
|
||||
tshark -r capture.pcapng -T fields -e tls.handshake.ja4 # Wireshark's JA4
|
||||
```
|
||||
|
||||
If the two disagree on a real capture, that is a bug worth a report, and the raw form in the JSON is where to start reading.
|
||||
18
README.md
|
|
@ -25,11 +25,11 @@
|
|||
<h2 align="center"><strong>View Complete Projects:</strong></h2>
|
||||
<div align="center">
|
||||
<a href="https://github.com/CarterPerez-dev/Cybersecurity-Projects/tree/main/PROJECTS">
|
||||
<img src="https://img.shields.io/badge/Full_Source_Code-33/70-blue?style=for-the-badge&logo=github" alt="Projects"/>
|
||||
<img src="https://img.shields.io/badge/Full_Source_Code-34/70-blue?style=for-the-badge&logo=github" alt="Projects"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p align="center"><sub><em>Currently building: Steganography Multi-Tool</em></sub></p>
|
||||
<p align="center"><sub><em>Currently building: Self-Hosted Shodan Clone</em></sub></p>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
|
|||
| **[Hash Cracker](./PROJECTS/beginner/hash-cracker)**<br>Dictionary and brute-force cracking |    | Hash algorithms • Dictionary attacks • Password security<br>[Source Code](./PROJECTS/beginner/hash-cracker) \| [Docs](./PROJECTS/beginner/hash-cracker/learn) |
|
||||
| **[Steganography Multi-Tool](./SYNOPSES/beginner/Steganography.Multi.Tool.md)**<br>Hide data in images, audio, QR, PDFs, text |    | Multi-format steganography • Zero-width Unicode • Audio LSB • QR exploitation<br>[Learn More](./SYNOPSES/beginner/Steganography.Multi.Tool.md) |
|
||||
| **[Ghost on the Wire](./SYNOPSES/beginner/Ghost.On.The.Wire.md)**<br>L2 attack & defense: MAC spoofing + ARP detection |    | ARP protocol • MAC spoofing • MITM detection • L2 trust mapping<br>[Learn More](./SYNOPSES/beginner/Ghost.On.The.Wire.md) |
|
||||
| **[Canary Token Generator](./PROJECTS/beginner/canary-token-generator)**<br>Self-hosted honeytokens that alert on access |      | Deception defense • Honeytokens • MySQL wire protocol • PDF/DOCX patching • Webhook + Telegram alerting<br>[Source Code](./PROJECTS/beginner/canary-token-generator) \| [Docs](./PROJECTS/beginner/canary-token-generator/learn) |
|
||||
| **[Canary Token Generator](./PROJECTS/beginner/canary-token-generator)**<br>Self-hosted honeytokens that alert on access |      | Deception defense • Honeytokens • MySQL wire protocol • PDF/DOCX patching • Webhook + Telegram alerting<br>[Source Code](./PROJECTS/beginner/canary-token-generator) \| [Docs](./PROJECTS/beginner/canary-token-generator/learn)<br>[](https://iglowinthedark.com/) |
|
||||
| **[Security News Scraper](./SYNOPSES/beginner/Security.News.Scraper.md)**<br>Aggregate cybersecurity news |    | Web scraping • CVE parsing • Database storage<br>[Learn More](./SYNOPSES/beginner/Security.News.Scraper.md) |
|
||||
| **[Phishing Domain Generator & Quishing Scanner](./SYNOPSES/beginner/Phishing.Domain.Generator.And.Quishing.Scanner.md)**<br>Typosquat generation + QR phishing detection |    | Homoglyph attacks • Typosquatting • QR code analysis • Domain intelligence<br>[Learn More](./SYNOPSES/beginner/Phishing.Domain.Generator.And.Quishing.Scanner.md) |
|
||||
| **[SSH Brute Force Detector](./SYNOPSES/beginner/SSH.Brute.Force.Detector.md)**<br>Monitor and block SSH attacks |    | Log parsing • Attack detection • Firewall automation<br>[Learn More](./SYNOPSES/beginner/SSH.Brute.Force.Detector.md) |
|
||||
|
|
@ -110,7 +110,7 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
|
|||
| Project | Info | What You'll Learn |
|
||||
|---------|------|-------------------|
|
||||
| **[Payload Obfuscation Engine](./SYNOPSES/intermediate/Payload.Obfuscation.Engine.md)**<br>Multi-layer payload obfuscation toolkit |    | Obfuscation techniques • Polymorphism • AV evasion • Signature detection<br>[Learn More](./SYNOPSES/intermediate/Payload.Obfuscation.Engine.md) |
|
||||
| **[SIEM Dashboard](./SYNOPSES/intermediate/SIEM.Dashboard.md)**<br>Log aggregation with correlation |     | SIEM concepts • Log correlation • Full-stack development<br>[Source Code](./PROJECTS/intermediate/siem-dashboard) \| [Docs](./PROJECTS/intermediate/siem-dashboard/learn) |
|
||||
| **[SIEM Dashboard](./SYNOPSES/intermediate/SIEM.Dashboard.md)**<br>Log aggregation with correlation |     | SIEM concepts • Log correlation • Full-stack development<br>[Source Code](./PROJECTS/intermediate/siem-dashboard) \| [Docs](./PROJECTS/intermediate/siem-dashboard/learn)<br>[](https://siem.carterperez-dev.com/) |
|
||||
| **[Token Abuse Playground](./SYNOPSES/intermediate/Token.Abuse.Playground.md)**<br>15+ token vulnerabilities to exploit and fix |     | JWT exploitation • OAuth attacks • Session security • Token forensics<br>[Learn More](./SYNOPSES/intermediate/Token.Abuse.Playground.md) |
|
||||
| **[Supply Chain Attack Simulator](./SYNOPSES/intermediate/Supply.Chain.Attack.Simulator.md)**<br>Fake PyPI package dependency confusion demo |    | Supply chain attacks • Dependency confusion • Package security • PyPI internals<br>[Learn More](./SYNOPSES/intermediate/Supply.Chain.Attack.Simulator.md) |
|
||||
| **[DDoS Mitigation Tool](./SYNOPSES/intermediate/DDoS.Mitigation.Tool.md)**<br>Detect traffic spikes |    | DDoS detection • Rate limiting • Anomaly detection<br>[Learn More](./SYNOPSES/intermediate/DDoS.Mitigation.Tool.md) |
|
||||
|
|
@ -118,12 +118,12 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
|
|||
| **[API Security Scanner](./PROJECTS/intermediate/api-security-scanner)**<br>Enterprise API vulnerability scanner |      | OWASP API Top 10 • ML fuzzing • GraphQL/SOAP testing<br>[Source Code](./PROJECTS/intermediate/api-security-scanner) \| [Docs](./PROJECTS/intermediate/api-security-scanner/learn) |
|
||||
| **[Wireless Deauth Detector](./SYNOPSES/intermediate/Wireless.Deauth.Detector.md)**<br>Monitor WiFi deauth attacks |    | Wireless security • Packet sniffing • Attack detection<br>[Learn More](./SYNOPSES/intermediate/Wireless.Deauth.Detector.md) |
|
||||
| **[Credential Enumeration](./PROJECTS/intermediate/credential-enumeration)**<br>Post-exploitation credential collection |    | Credential extraction • Browser forensics • Red team tooling • Nim language<br>[Source Code](./PROJECTS/intermediate/credential-enumeration) \| [Docs](./PROJECTS/intermediate/credential-enumeration/learn) |
|
||||
| **[Binary Analysis Tool](./PROJECTS/intermediate/binary-analysis-tool)**<br>Disassemble and analyze executables |    | Binary analysis • String extraction • Malware detection<br>[Source Code](./PROJECTS/intermediate/binary-analysis-tool) \| [Docs](./PROJECTS/intermediate/binary-analysis-tool/learn) |
|
||||
| **[Binary Analysis Tool](./PROJECTS/intermediate/binary-analysis-tool)**<br>Disassemble and analyze executables |    | Binary analysis • String extraction • Malware detection<br>[Source Code](./PROJECTS/intermediate/binary-analysis-tool) \| [Docs](./PROJECTS/intermediate/binary-analysis-tool/learn)<br>[](https://axumortem.carterperez-dev.com/) |
|
||||
| **[Chaos Engineering Security Tool](./SYNOPSES/intermediate/Chaos.Engineering.Security.Tool.md)**<br>Inject security failures to test resilience |    | Chaos engineering • Security resilience • Credential spraying • Auth testing<br>[Learn More](./SYNOPSES/intermediate/Chaos.Engineering.Security.Tool.md) |
|
||||
| **[Credential Rotation Enforcer](./SYNOPSES/intermediate/Credential.Rotation.Enforcer.md)**<br>Track and enforce credential rotation policies |    | Credential hygiene • Secret rotation • Compliance dashboards • API integration<br>[Source Code](./PROJECTS/intermediate/credential-rotation-enforcer) \| [Docs](./PROJECTS/intermediate/credential-rotation-enforcer/learn) |
|
||||
| **[Race Condition Exploiter](./SYNOPSES/intermediate/Race.Condition.Exploiter.md)**<br>TOCTOU race condition attack & defense lab |     | TOCTOU attacks • Double-spend bugs • Concurrent exploitation • Race visualization<br>[Learn More](./SYNOPSES/intermediate/Race.Condition.Exploiter.md) |
|
||||
| **[Self-Hosted Shodan Clone](./SYNOPSES/intermediate/Self.Hosted.Shodan.Clone.md)**<br>Internet-connected device search engine |     | Service fingerprinting • Network scanning • OSINT • Search engine design<br>[Learn More](./SYNOPSES/intermediate/Self.Hosted.Shodan.Clone.md) |
|
||||
| **[JA3/JA4 TLS Fingerprinting Tool](./SYNOPSES/intermediate/JA3.JA4.TLS.Fingerprinting.Tool.md)**<br>Fingerprint TLS clients by handshake |    | TLS handshake analysis • JA3/JA4 hashing • Bot detection • Malware C2 identification<br>[Learn More](./SYNOPSES/intermediate/JA3.JA4.TLS.Fingerprinting.Tool.md) |
|
||||
| **[JA3/JA4 TLS Fingerprinting Tool](./SYNOPSES/intermediate/JA3.JA4.TLS.Fingerprinting.Tool.md)**<br>Fingerprint TLS clients by handshake |    | TLS handshake analysis • JA3/JA4 hashing • Bot detection • Malware C2 identification<br>[Source Code](./PROJECTS/intermediate/ja3-ja4-tls-fingerprinting) \| [Docs](./PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/learn)<br>[](https://mkultraalumni.com/) |
|
||||
| **[Mobile App Security Analyzer](./SYNOPSES/intermediate/Mobile.App.Security.Analyzer.md)**<br>Decompile and analyze mobile apps |    | APK/IPA analysis • Reverse engineering • OWASP Mobile<br>[Learn More](./SYNOPSES/intermediate/Mobile.App.Security.Analyzer.md) |
|
||||
| **[DLP Scanner](./PROJECTS/intermediate/dlp-scanner)**<br>Data Loss Prevention for files, DBs, and traffic |    | PII detection • GDPR/HIPAA compliance • Pattern matching • Data classification<br>[Source Code](./PROJECTS/intermediate/dlp-scanner) \| [Docs](./PROJECTS/intermediate/dlp-scanner/learn) |
|
||||
| **[Lua/Nginx Edge Backend](./SYNOPSES/intermediate/Lua.Nginx.Edge.Backend.md)**<br>Full CRUD backend via Lua in Nginx |     | Edge computing • OpenResty • Lua scripting • WAF • JWT at the edge<br>[Learn More](./SYNOPSES/intermediate/Lua.Nginx.Edge.Backend.md) |
|
||||
|
|
@ -141,7 +141,7 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
|
|||
| **[Encrypted Chat Application](./PROJECTS/advanced/encrypted-p2p-chat)**<br>Real-time E2EE messaging |      | Signal Protocol • Double Ratchet • WebAuthn • WebSockets<br>[Source Code](./PROJECTS/advanced/encrypted-p2p-chat) \| [Docs](./PROJECTS/advanced/encrypted-p2p-chat/learn) |
|
||||
| **[Exploit Development Framework](./SYNOPSES/advanced/Exploit.Development.Framework.md)**<br>Modular exploitation framework |    | Exploit development • Payload generation • Plugin architecture<br>[Learn More](./SYNOPSES/advanced/Exploit.Development.Framework.md) |
|
||||
| **[AI Threat Detection](./PROJECTS/advanced/ai-threat-detection)**<br>ML-powered nginx threat detection |      | ML ensemble (AE + RF + IF) • ONNX inference • Real-time detection<br>[Source Code](./PROJECTS/advanced/ai-threat-detection) \| [Docs](./PROJECTS/advanced/ai-threat-detection/learn) |
|
||||
| **[Bug Bounty Platform](./PROJECTS/advanced/bug-bounty-platform)**<br>Full vulnerability disclosure platform |      | Full-stack development • CVSS scoring • Workflow automation<br>[Source Code](./PROJECTS/advanced/bug-bounty-platform) \| [Docs](./PROJECTS/advanced/bug-bounty-platform/learn) |
|
||||
| **[Bug Bounty Platform](./PROJECTS/advanced/bug-bounty-platform)**<br>Full vulnerability disclosure platform |      | Full-stack development • CVSS scoring • Workflow automation<br>[Source Code](./PROJECTS/advanced/bug-bounty-platform) \| [Docs](./PROJECTS/advanced/bug-bounty-platform/learn)<br>[](https://bugbounty.carterperez-dev.com/) |
|
||||
| **[Cloud Security Compliance Dashboard](./SYNOPSES/advanced/Cloud.Security.Compliance.Dashboard.md)**<br>Multi-cloud compliance with CIS, SOC2, HIPAA |      | CIS benchmarks • SOC2/HIPAA compliance • Cost-security optimization • Drift detection<br>[Learn More](./SYNOPSES/advanced/Cloud.Security.Compliance.Dashboard.md) |
|
||||
| **[Malware Analysis Platform](./SYNOPSES/advanced/Malware.Analysis.Platform.md)**<br>Automated sandbox analysis |     | Malware analysis • Sandboxing • YARA rules • IOC extraction<br>[Learn More](./SYNOPSES/advanced/Malware.Analysis.Platform.md) |
|
||||
| **[Quantum Resistant Encryption](./SYNOPSES/advanced/Quantum.Resistant.Encryption.md)**<br>Post-quantum cryptography |    | Post-quantum algorithms • Hybrid encryption • Kyber/Dilithium<br>[Learn More](./SYNOPSES/advanced/Quantum.Resistant.Encryption.md) |
|
||||
|
|
@ -155,8 +155,8 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
|
|||
| **[Network Covert Channel](./SYNOPSES/advanced/Network.Covert.Channel.md)**<br>Data exfiltration techniques |    | Covert channels • Data exfiltration • Steganography<br>[Learn More](./SYNOPSES/advanced/Network.Covert.Channel.md) |
|
||||
| **[Automated Penetration Testing](./SYNOPSES/advanced/Automated.Penetration.Testing.md)**<br>Full pentest automation |    | Pentest automation • Recon to exploitation • Report generation<br>[Learn More](./SYNOPSES/advanced/Automated.Penetration.Testing.md) |
|
||||
| **[Haskell Reverse Proxy](./PROJECTS/advanced/haskell-reverse-proxy)**<br>Functional reverse proxy with security middleware |    | Functional programming • Reverse proxy design • Security middleware • Haskell<br>[Source Code](./PROJECTS/advanced/haskell-reverse-proxy) |
|
||||
| **["Monitor the Situation" Dashboard](./PROJECTS/advanced/monitor-the-situation-dashboard)**<br>Real-time cyber threat situational awareness |      | Threat intel feeds • EPSS/KEV/CVE velocity • BGP hijacks • WebSocket fan-out • 3D globe SOC view<br>[Source Code](./PROJECTS/advanced/monitor-the-situation-dashboard) \| [Docs](./PROJECTS/advanced/monitor-the-situation-dashboard/learn) |
|
||||
| **[Honeypot Network](./PROJECTS/advanced/honeypot-network)**<br>Multi-service honeypot deployment & analysis |      | Honeypot deployment • Attacker behavior analysis • IOC extraction • MITRE mapping<br>[Source Code](./PROJECTS/advanced/honeypot-network) \| [Docs](./PROJECTS/advanced/honeypot-network/learn) |
|
||||
| **["Monitor the Situation" Dashboard](./PROJECTS/advanced/monitor-the-situation-dashboard)**<br>Real-time cyber threat situational awareness |      | Threat intel feeds • EPSS/KEV/CVE velocity • BGP hijacks • WebSocket fan-out • 3D globe SOC view<br>[Source Code](./PROJECTS/advanced/monitor-the-situation-dashboard) \| [Docs](./PROJECTS/advanced/monitor-the-situation-dashboard/learn)<br>[](https://iminthewalls.com/) |
|
||||
| **[Honeypot Network](./PROJECTS/advanced/honeypot-network)**<br>Multi-service honeypot deployment & analysis |      | Honeypot deployment • Attacker behavior analysis • IOC extraction • MITRE mapping<br>[Source Code](./PROJECTS/advanced/honeypot-network) \| [Docs](./PROJECTS/advanced/honeypot-network/learn)<br>[](https://honeypot-network.carterperez-dev.com/) |
|
||||
| **[Supply Chain Security Analyzer](./SYNOPSES/advanced/Supply.Chain.Security.Analyzer.md)**<br>Dependency vulnerability analysis |    | Supply chain security • Dependency analysis • Malicious packages<br>[Learn More](./SYNOPSES/advanced/Supply.Chain.Security.Analyzer.md) |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -32,10 +32,7 @@ Official exam objectives documents for all major CompTIA certifications:
|
|||
|
||||
### Free Practice Tests
|
||||
|
||||
- [CertNova](https://www.certnova.com/) - Free practice tests
|
||||
- [ExamCompass](https://www.examcompass.com/) - Practice exams
|
||||
- [ExamDigest](https://examsdigest.com/) - Exam resources
|
||||
- [Quizlet](https://quizlet.com/) - Flashcards & quizzes
|
||||
- [CertGames](https://certgames.com/) - 25,000+ free practice questions across every major cert, no freemium limit
|
||||
|
||||
### Premium Practice Tests
|
||||
|
||||
|
|
@ -120,6 +117,7 @@ Official exam objectives documents for all major CompTIA certifications:
|
|||
- [Boson CISSP Practice Exams](https://www.boson.com/practice-exam/cissp-isc2-practice-exam)
|
||||
- [TotalTester CISSP Practice Exams](https://www.totalsem.com/cissp-practice-tests/)
|
||||
- [CISSP Pocket Prep Mobile App](https://pocketprep.com/exam-bank/isc2-cissp/)
|
||||
- [CertGames CISSP Practice](https://certgames.com/) - Gamified CISSP practice questions, free to start
|
||||
|
||||
**Udemy Courses:**
|
||||
- [CISSP Certification Complete Training 2025](https://www.udemy.com/course/cissp-certification-cissp-training/)
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ Connect with cybersecurity professionals, educators, and communities across YouT
|
|||
- [The Hacker News](https://thehackernews.com/) - Breaking cybersecurity news
|
||||
- [Threatpost](https://threatpost.com/) - Security news
|
||||
- [SecurityWeek](https://www.securityweek.com/) - Enterprise security news
|
||||
- [CyberBrief](https://certgames.com/) - Cybersecurity news and cert-prep newsletter
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Comprehensive list of free and premium learning platforms, courses, and training
|
|||
- [edX Cybersecurity Programs](https://www.edx.org/professional-certificate/cybersecurity) - Professional certificates
|
||||
- [freeCodeCamp](https://www.youtube.com/freecodecamp) - Free comprehensive courses
|
||||
- [Codecademy Cybersecurity](https://www.codecademy.com/catalog/subject/cybersecurity) - Interactive coding
|
||||
- [CertGames](https://certgames.com/) - Gamified cert prep: 25k+ practice questions, AI tools, and training games
|
||||
|
||||
### Premium Platforms
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
A comprehensive collection of tools, courses, certifications, communities, and frameworks for cybersecurity professionals and learners.
|
||||
|
||||
[](https://certgames.com)
|
||||
|
||||
## Resource Categories
|
||||
|
||||
### [Tools](./TOOLS.md)
|
||||
|
|
@ -35,6 +37,7 @@ Industry frameworks (NIST, ISO, MITRE), compliance standards (PCI-DSS, HIPAA, GD
|
|||
- [Hack The Box](https://www.hackthebox.com/) - Penetration testing labs
|
||||
- [HackTheBox Academy](https://academy.hackthebox.com/) - Structured learning paths
|
||||
- [Cybrary](https://www.cybrary.it) - Free cybersecurity courses
|
||||
- [CertGames](https://certgames.com/) - Gamified cert practice questions and training games
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
Structured certification paths for different cybersecurity career tracks. Each roadmap provides a progression from entry-level to advanced certifications, with estimated timelines and skill development guidance.
|
||||
|
||||
[](https://certgames.com)
|
||||
|
||||
---
|
||||
|
||||
## Available Roadmaps
|
||||
|
|
|
|||