ja4plus 1.0

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.

status: approved approved 2026-08-06 branch model: dev-and-live repo: Crank-Git/ja4plus 10 epics

How this page is maintained

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.

Executive summary

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.

The four findings that shaped this plan

  1. JA4SSH emits at the wrong interval. FoxIO states (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.
  2. The conformance gate does not exist. 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.
  3. Nothing bounds memory and nothing states a thread-safety contract. There is no lock in the package. Every state table grows without limit unless the caller knows to call cleanup_connection, and ja4plus live never calls it. base.py:38 stores the packet object itself on every fingerprint.
  4. Two published statements are false. The README claims the project implements all JA4+ methods; FoxIO documents twelve and this project implements eleven. 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.

Goals

  1. Every method matches the FoxIO reference on all 37 FoxIO vectors.
  2. The conformance suite runs on every pull request and blocks a merge on failure.
  3. The library holds bounded memory under a continuous packet stream.
  4. The library states one thread-safety contract, and tests prove that contract.
  5. No packet input crashes the library, including truncated and corrupt packets.
  6. Test coverage is 90 percent or higher, measured by line.
  7. The public interface carries type annotations, and mypy --strict passes.
  8. Version 1.0.0 is on PyPI, with documentation published on GitHub Pages.
  9. This project and the Go port emit the same fingerprint for every vector.

Non-goals

Users

PersonaWhat they need
Capture analystFingerprints from a saved capture, in a format another tool can read.
Monitor operatorA long-running process that reads a live interface and never exhausts memory.
Library integratorA typed, documented interface with a stated concurrency contract.
MaintainerEvidence that a change did not break conformance.

Architecture

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.

ComponentChoiceWhy it beat the alternative
LanguagePython 3.10+3.9 reached end of life in October 2025. 3.10 is the oldest line still receiving security fixes.
Packet parsingscapy>=2.4.0Reads every capture format needed, with no C toolchain. dpkt is faster but parses fewer link types.
Certificatescryptography>=42Maintained, and already a dependency for JA4X.
Lint and formatruffOne tool replaces flake8, isort and black. Sub-second on this codebase.
Type checkmypy --strictSupports the py.typed marker the typed interface needs. pyright needs Node.js in CI.
Docsmkdocs-materialThe existing docs are Markdown. Sphinx would need a rewrite into reStructuredText.
Docs hostGitHub PagesAlready on GitHub. Read the Docs adds an account and a webhook.
Package hostPyPI, trusted publishingAlready 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.

Parity with ja4plus-go

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.

  1. FoxIO decides behavior. Where FoxIO specifies the output, the vectors decide, and this rule outranks the port.
  2. The port decides interface. Where FoxIO specifies nothing — a field name, a subcommand, a default — the port has shipped a choice and this project adopts it rather than inventing a second one.
  3. The gate is the shared vector set. Both repositories read the same 37 vectors at the same pinned commit. No test here builds, runs or imports the port; that would couple two repositories moving at different speeds and fail for reasons unrelated to the change under test.

This spec plans changes to this repository only. It records what the port should change and files nothing there.

ItemHere todayPort todayAction
Result typedict, 8 keysFingerprintResult, 9 fieldsAdopt the port's field set, including Timestamp. E4
Results per packetOne stringA sliceReturn a list. E4
Parse failuresSwallowed at DEBUGReturnedExpose them. E4
Remote lookup--lookup is opt-in, but the client then calls ja4db.com with no second opt-inThe remote fallback is separately opt-inSeparate local from remote. E7
Shard keytcp:ip:port->ip:portIdenticalAlready at parity. Add a test.
db update / db infoBoth existBoth existAlready at parity. Add a test.
JA4SSH windowmin(packet_count, 10)min(packetCount, 10)Both wrong. FoxIO says 200. Fix here. E1
Parity test6 tests asserting names existReplace 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).

Cross-cutting concerns

ConcernRule
Thread safetyLocked by default. Processor(thread_safe=False) opts out for a caller who shards.
Bounded memoryEvery 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 ruleA 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 handlingA parser that cannot read a packet returns nothing; it does not raise. A caller error raises ValueError.
ValidationEvery packet is hostile input. No parser trusts a length field it read from the packet.
Security postureNo network request unless the caller opts in. A fingerprint sent to a third party discloses observed traffic.
PerformanceMeasured and reported per pull request. No target. A change that halves throughput needs a stated reason.

Epics

  1. FoundationBuild the gates before any behavior changes, so every later epic has something that can fail. Vendor the 37 vectors, run conformance in CI, add ruff/mypy/coverage, raise the Python floor to 3.10, create dev.
  2. Spec conformanceMake all 37 vectors pass. Fix the JA4SSH 200-packet window, expose JA4_o, use FoxIO's propagation-factor table, replace the surface-area parity tests.
  3. Correctness auditFix the 14 registered defects the vectors cannot see: TCP sequence wraparound, quadratic reassembly, repeated JA4L client emission, duplicate cookie names, retained packet objects. Add the malformed-input suite.
  4. Concurrency and resource safetyState the contract and prove it. Per-fingerprinter locks, bounded state tables with age and count limits, eviction on packet arrival, processor statistics.
  5. Typed public interfaceFrozen FingerprintResult matching the port's fields, mypy --strict, py.typed in the wheel, remove the collector.py module deprecated since 0.3.0.
  6. Structured outputSeparate address and port fields, a versioned JSON Lines and CSV schema, a fixed CSV header, results on stdout and diagnostics on stderr.
  7. Live captureMake the monitor a supported mode: ja4plus watch with connection eviction, signal handling, a clean flush on exit, and statistics.
  8. Database enrichmentNo network request without opt-in. Bulk lookup, a bounded cache, a recorded source per result.
  9. DocumentationCorrect the method-coverage claim, state the concurrency contract, run every code sample in CI, publish the mkdocs site.
  10. ReleaseVerify the built wheel in a clean environment before publishing. Ship 1.0.0.

Milestones

MilestoneEpicsWhat shippable means
M1 — Gates0The repository can prove a regression. Nothing user-facing changed.
M2 — Correct1, 2Every fingerprint matches the reference. This alone justifies a 0.7.0 release if 1.0.0 slips.
M3 — Safe3A monitor runs for a day without memory growth.
M4 — Stable4, 5, 6, 7The interface is typed and will not change again before 2.0.0.
M5 — Released8, 9Version 1.0.0 on PyPI with published documentation.

Feature sets

00 — Foundation

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.

01 — Spec conformance

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.

02 — Correctness audit

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.

03 — Concurrency and resource safety

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.

04 — Typed public interface

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.

05 — Structured output

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.

06 — Live capture

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.

07 — Database enrichment

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.

08 — Documentation

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.

09 — Release

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.

Mockups

01 — Command-line output formats

Open in a new tab ↗

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.

Assumptions

  1. You accept a break in the Processor.process_packet return type at 1.0.0.
  2. FoxIO License 1.1 permits redistribution of the vectors with attribution. pyproject.toml already declares LicenseRef-FoxIO-1.1.
  3. The files under 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.
  4. Development Status moves to 5 - Production/Stable at 1.0.0.
  5. Default limits are 10,000 entries and 300 seconds, tuned in Epic 3 against a measurement.
  6. The remote lookup becomes opt-in, which changes behavior for anyone relying on it today.
  7. The port stays at its 2026-05-09 state while this work proceeds. This spec reads it and never writes to it.
  8. The port eventually adopts the 200-packet JA4SSH window. Until it does, the two disagree on that one method.

Open questions

Decided at approval

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.

Open, but not blocking

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.