Make the JA4+ fingerprints provably correct, make the library safe to run for a week, then publish version 1.0.0 with documentation that is true.
A person maintains this page by hand, and no command generates it.
docs/specs/spec.md is the specification. This page is a summary of it, and
the page lags it. Two commits wrote this page, and the later one is f6d808b
of #175. The page holds no Changelog section, and docs/specs/spec.md
records 68 Changelog rounds before #211.
Read docs/specs/spec.md wherever the two disagree. #211
records this note and corrects the two statements that contradicted the measurement.
Whether this page survives is a question for the user.
#477 corrects the vector-fallback rule of the Open, but not blocking
card. That card gave precedence to the expected-output files and named no
reader, and this page states the corrected rule 118 lines above it.
tests/test_documentation_fallback_rule.py now holds every statement of the
rule against a shape, so a rewording that drops the reader fails a case.
#211 left a third statement, and #449 corrects it. The image count of
the Open, but not blocking card read seven, and this page states eleven
120 lines above it. tests/test_documentation_image_count.py now reads a
count out of the inventory rather than one forbidden phrase. A rewording of the same
wrong number therefore fails a case.
ja4plus is a Python library and command-line program that reads network
packets and produces JA4+ fingerprints. Version 0.6.0 is on PyPI. The codebase is
5,413 lines with 7,382 lines of tests, and 582 of its 584 tests pass today.
The problem is not that the project is unfinished. The problem is that its central claim — that a fingerprint matches the FoxIO reference — is not checked anywhere that can fail. The conformance suite exists, reads vectors that are downloaded on demand and excluded from version control, and never runs in continuous integration. Reading the code against the published FoxIO material found one method that is knowingly wrong, and a set of defects that only appear on traffic the vectors do not cover.
(runs every 200 SSH packets by default). ja4ssh.py:170 uses
min(self.packet_count, 10), above a comment reading
"For testing purposes". FoxIO's expected output for ssh.pcapng
is one fingerprint, c36s36_c76s124_c0s0 — the two counts sum to 200.
The Go port carries the same defect, which is why a parity test never caught it.test.yml neither
downloads the vectors nor selects the spec_validation marker, so those
12 tests skip in every run. The downloader fetches 6 of the 37 vectors FoxIO
publishes, and the 31 it skips include every QUIC, DHCP, X.509, IPv6, tunnelled and
malformed case.cleanup_connection, and
ja4plus live never calls it. base.py:38 stores the packet
object itself on every fingerprint.JA4LFingerprinter writes both JA4L and JA4LS, so ten
fingerprinter classes carry eleven methods. #387 corrected the count of ten that this
finding first stated. No code sample in the README, in docs/, or in
examples/ is executed by anything.The plan sequences the work so each epic can fail loudly. Epic 0 builds the gates. Epic 1 makes the vectors pass. Epic 2 fixes what the vectors cannot see. Epic 3 bounds memory and states the concurrency contract. Epics 4 to 7 settle the interface. Epics 8 and 9 make the documentation true and publish version 1.0.0.
mypy --strict passes.| Persona | What they need |
|---|---|
| Capture analyst | Fingerprints from a saved capture, in a format another tool can read. |
| Monitor operator | A long-running process that reads a live interface and never exhausts memory. |
| Library integrator | A typed, documented interface with a stated concurrency contract. |
| Maintainer | Evidence that a change did not break conformance. |
A pure Python package with two runtime dependencies. A packet enters
Processor.process_packet. The processor calls ten fingerprinters in a fixed
order. Each reads the packet, updates its state table, and emits a fingerprint or nothing.
The processor returns the collected results.
| Component | Choice | Why it beat the alternative |
|---|---|---|
| Language | Python 3.10+ | 3.9 reached end of life in October 2025. 3.10 is the oldest line still receiving security fixes. |
| Packet parsing | scapy>=2.4.0 | Reads every capture format needed, with no C toolchain. dpkt is faster but parses fewer link types. |
| Certificates | cryptography>=42 | Maintained, and already a dependency for JA4X. |
| Lint and format | ruff | One tool replaces flake8, isort and black. Sub-second on this codebase. |
| Type check | mypy --strict | Supports the py.typed marker the typed interface needs. pyright needs Node.js in CI. |
| Docs | mkdocs-material | The existing docs are Markdown. Sphinx would need a rewrite into reStructuredText. |
| Docs host | GitHub Pages | Already on GitHub. Read the Docs adds an account and a webhook. |
| Package host | PyPI, trusted publishing | Already configured. No token stored. |
Verified against https://github.com/FoxIO-LLC/ja4 (retrieved 2026-08-06, upstream default branch at a commit dated 2026-07-21). Upstream PRs #297 and #299 since May 2026 change the Wireshark plugin and the README, not the fingerprint semantics — so the May 2026 baseline this project targets is still current.
A Go port exists at Crank-Git/ja4plus-go, worked in a separate session.
processor.py:3 already states that this project mirrors it. Three rules
govern the relationship.
This spec plans changes to this repository only. It records what the port should change and files nothing there.
| Item | Here today | Port today | Action |
|---|---|---|---|
| Result type | dict, 8 keys | FingerprintResult, 9 fields | Adopt the port's field set, including Timestamp. E4 |
| Results per packet | One string | A slice | Return a list. E4 |
| Parse failures | Swallowed at DEBUG | Returned | Expose them. E4 |
| Remote lookup | --lookup is opt-in, but the client then calls ja4db.com with no second opt-in | The remote fallback is separately opt-in | Separate local from remote. E7 |
| Shard key | tcp:ip:port->ip:port | Identical | Already at parity. Add a test. |
db update / db info | Both exist | Both exist | Already at parity. Add a test. |
| JA4SSH window | min(packet_count, 10) | min(packetCount, 10) | Both wrong. FoxIO says 200. Fix here. E1 |
| Parity test | 6 tests asserting names exist | — | Replace with the vector suite. E1 |
Verified against https://github.com/Crank-Git/ja4plus-go
(types.go, processor.go, lookup.go,
ja4ssh.go, retrieved 2026-08-06, master at 2026-05-09).
| Concern | Rule |
|---|---|
| Thread safety | Locked by default. Processor(thread_safe=False) opts out for a caller who shards. |
| Bounded memory | Every state table has a maximum entry count and a maximum age. No packet object is retained after process_packet returns. |
| The boundary of the state rule | A state table survives across packets. A structure that one packet or one request builds and releases is not a state table, and it holds neither bound. The cookie list of one HTTP request is such a structure, and #175 records the ruling. The boundary removes no bound from a state table. The six unbounded state tables that #179 records keep their bound. |
| Error handling | A parser that cannot read a packet returns nothing; it does not raise. A caller error raises ValueError. |
| Validation | Every packet is hostile input. No parser trusts a length field it read from the packet. |
| Security posture | No network request unless the caller opts in. A fingerprint sent to a third party discloses observed traffic. |
| Performance | Measured and reported per pull request. No target. A change that halves throughput needs a stated reason. |
dev.JA4_o, use FoxIO's propagation-factor table, replace the surface-area
parity tests.FingerprintResult matching the port's fields,
mypy --strict, py.typed in the wheel, remove the
collector.py module deprecated since 0.3.0.ja4plus watch with
connection eviction, signal handling, a clean flush on exit, and statistics.| Milestone | Epics | What shippable means |
|---|---|---|
| M1 — Gates | 0 | The repository can prove a regression. Nothing user-facing changed. |
| M2 — Correct | 1, 2 | Every fingerprint matches the reference. This alone justifies a 0.7.0 release if 1.0.0 slips. |
| M3 — Safe | 3 | A monitor runs for a day without memory growth. |
| M4 — Stable | 4, 5, 6, 7 | The interface is typed and will not change again before 2.0.0. |
| M5 — Released | 8, 9 | Version 1.0.0 on PyPI with published documentation. |
17 requirements. Vendor the vectors with a NOTICE recording the upstream
commit and FoxIO License 1.1 attribution; un-ignore them; run conformance, ruff, ruff
format, mypy and coverage on every pull request; raise the floor to Python 3.10; pin every
action to a commit; add Dependabot; fix the two tests that shell out to a bare
python instead of sys.executable.
Key criteria: the conformance suite passes with no network access, and fails when it collects zero vectors — a silent skip is indistinguishable from a pass, which is how this gap survived.
12 requirements. All 37 vectors pass. JA4SSH emits every 200 SSH packets. JA4 and JA4S
expose JA4_o. calculate_distance reads the propagation factor
from FoxIO's hop-count table instead of hardcoding 1.6.
Key criteria: ssh.pcapng produces exactly one JA4SSH
fingerprint equal to c36s36_c76s124_c0s0; tls12.pcap produces
JA4_o = t13d1715h2_5b234860e130_014157ec0da2.
Eleven of the twelve FoxIO methods carry no complete text specification, and
JA4 is the one method that holds one. Where a reader reads the image and finds it
ambiguous, the expected-output files decide and the reading goes in
docs/implementation_notes.md. An image nobody read reaches no fallback.
docs/specs/foxio/README.md holds the inventory.
13 requirements over a 14-row audit register, each row citing a file and line. The
register covers: 32-bit sequence wraparound in tcp_stream.py:53; the
quadratic duplicate scan at :37; unbounded per-stream segment storage at
:41; JA4L re-emitting a client fingerprint on every later ACK
(ja4l.py:223); JA4H building its two cookie hashes from different cookie sets
(ja4h.py:163); the dead identical branch at ja4.py:268; packet
retention at base.py:38.
Key criteria: 10,000 segments on one stream complete in under a second;
100 post-handshake ACKs produce exactly one JA4L-C; every FoxIO malformed and
crash capture, and a truncated copy of every capture, raise nothing.
15 requirements. One RLock per fingerprinter rather than one global lock,
so ten threads can work on different methods at once. Eviction runs on packet arrival
using the packet timestamp, not the wall clock — a capture replaying an hour in
ten seconds would otherwise evict state it still needs. The library starts no thread.
Key criteria: eight threads on one processor for 60 seconds produce the
same fingerprint set as one thread; a test that replaces the lock with an object which
fails when acquired proves thread_safe=False acquires nothing.
13 requirements. A frozen FingerprintResult whose field names are the
snake-case form of the port's struct — including type rather than
method, because the port calls it Type. Item access survives one
major version behind a DeprecationWarning.
Key criteria: mypy --strict clean;
py.typed present in the wheel; import ja4plus.collector fails.
13 requirements. Addresses and ports as separate fields instead of one packed
source string; a schema_version on every output line; a CSV header
that is identical whatever flags were passed. The table format explicitly carries no
stability promise; JSON Lines and CSV do.
Key criteria: the CSV header is byte-identical with and without
--lookup; piping into head -1 produces no traceback.
14 requirements. ja4plus watch owns a connection table and calls
cleanup_connection on eviction — today's ja4plus live never does.
The privilege check attempts a capture rather than reading os.geteuid, because
Linux can grant CAP_NET_RAW without granting user zero.
Key criteria: 100,000 packets across 50,000 connections hold the tracked
count at or below the limit; SIGTERM exits zero with a complete output file.
15 requirements. --lookup becomes local-only; --lookup-remote
opts into ja4db.com and prints a one-time notice naming the service.
db update writes to the platform cache directory, never inside the installed
package.
Key criteria: a test that fails on any outbound socket passes while the default client looks up 1,000 fingerprints.
15 requirements. The README states which of FoxIO's twelve methods are implemented and
which are not, states the concurrency contract and the memory defaults, and every code
sample in the README, docs/ and examples/ runs in CI.
mkdocs build --strict fails on a broken link.
14 requirements. The version lives in one place. The publish workflow installs the built wheel into a clean environment, runs the CLI and the conformance suite against it, and publishes only if both pass — a partial publish to PyPI cannot be undone.
Referenced by features/05-structured-output.md. Mockups are
guidance, not a contract: they convey intent and roughly what a user sees. An implementer
may diverge where the requirements and acceptance criteria are still met. The parts that
are a contract — the CSV column order and the JSON field set — live in the feature
file. If the frame below is blank, use the link above; a local file is an opaque origin and
may not render in an iframe.
Processor.process_packet return type at 1.0.0.pyproject.toml already declares LicenseRef-FoxIO-1.1.python/test/testdata/ are the reference — not those under
wireshark/test/testdata/, where tls12.pcap.json is an empty
array while the Python one holds four fingerprints.Development Status moves to 5 - Production/Stable at 1.0.0.JA4TScan is declined. It sends crafted packets rather than reading
traffic, so it needs raw-socket transmission, a target-host argument, and privileges the
read path never needs. A program that sends packets to a host reaches a network the
operator did not capture, and that capability is larger than fingerprint production.
Epic 8 corrects the README's coverage claim instead. Round 3 recorded this as a
permanent non-goal on 2026-08-06. The user restated it on 2026-08-08 as a
decline that is reversible, and Non-goals in
docs/specs/spec.md holds the later form. #197 holds the reading.
dev becomes the GitHub default branch. A pull request
then opens against dev with no manual step. master stays the
release branch and is protected. Epic 0 makes both changes.
docs/specs/foxio/README.md holds the inventory. Where a reader reads the
image and finds it ambiguous, the expected-output files decide and the reading goes in
docs/implementation_notes.md. An image nobody read reaches no
fallback.ja4db.com API publishes no versioned document that
I could find. The client treats any unexpected response shape as a miss.Spec package: docs/specs/spec.md · 10 feature files under
features/ · 1 mockup under mockups/. Regenerated 2026-08-06.
JA4+ is a set of standards created by FoxIO. This project is an independent implementation of the published specification.