Repository navigation
Proposal: new JA4+ method for an "ephemeral-extension-invariant" stable client fingerprint (tentative name JA4E) #303
Description
Context
About six months ago (Jan 2, 2026) ntop published an analysis ("Is JA4 Now Obsolete?", https://www.ntop.org/is-ja4-now-obsolete/) showing that JA4 can produce different fingerprints for the same client across consecutive connections. Their reproduction used Safari as the example: Safari uses the TLS 1.3 pre-shared key extension (RFC 8446) to resume a previous session, and the presence/parameters of that extension (along with related "ephemeral" extensions like session_ticket and padding) change the JA4_b hash even though the underlying client/library is unchanged.
We want to flag that Safari is just the example ntop happened to use — we've independently observed the same instability (fresh handshake vs. resumed handshake producing different JA4 values for the same client) across several different browsers, not just Safari. This isn't a Safari-specific quirk, it's a structural property of how JA4 treats connection-state-dependent extensions.
ntop's own mitigation in nDPI is an opt-in flag (tls,metadata.ja_ignore_ephemeral_tls_extn) that strips a defined set of "ephemeral" extensions (session ticket, pre-shared key, padding) before computing JA4, restoring a stable fingerprint for the same client across reconnects. Their writeup explicitly frames this as a stopgap "until a JA5 standard is published."
Prior art / existing implementation
We ran into this same instability problem and needed a stable JA4, so we already implemented and upstreamed a fix as a PR to the fingerproxy Go library:
Summary of that implementation, for reference:
- Adds an opt-in
StableJA4Fingerprintalongside the existing raw JA4, leaving raw JA4 behavior unchanged. - Excludes three extensions from both the extension count and the extension hash input:
padding(0x0015),session_ticket(0x0023),pre_shared_key(0x0029) — the same set ntop identifies as ephemeral. - Includes test coverage for full vs. resumed/PSK handshakes (Chrome), each individually-ignored extension, and confirmation that non-ephemeral extensions still affect the stable hash as expected.
This isn't meant as "the" reference implementation, just evidence that (a) the problem is reproducible and fixable with a small, well-scoped rule, and (b) at least two independent parties (ntop/nDPI and us) converged on excluding the same three extensions.
Problem
JA4 was designed to be both unique and stable for a given TLS client implementation. The current spec, however, does not define how to treat extensions whose presence/content varies per-connection for reasons unrelated to client identity (session resumption, ticket rotation, padding length). This means:
- Different parties invent their own ad-hoc fix (ntop/nDPI, and separately us against fingerproxy), fragmenting the ecosystem instead of having one canonical answer.
- Anyone relying on JA4 for fingerprint-based grouping/detection (one of JA4's stated use cases) gets false negatives when a stable client appears as multiple distinct fingerprints across resumed sessions.
Proposal
We'd like to propose a new JA4+ method — a connection-stable variant of JA4 that normalizes or excludes a well-defined set of "ephemeral" TLS extensions before hashing, so the same client/library produces the same fingerprint across session-resumption events.
Conceptually this would sit alongside JA4 the same way JA4S sits alongside JA4 today: same family, distinct, explicitly-scoped purpose. We are not proposing to change JA4 itself — backward compatibility and existing detections built on JA4 should be unaffected.
Open questions we'd like FoxIO's input on, since JA4+ naming/spec is maintained by FoxIO:
- Naming: JA4S is already taken (TLS Server fingerprinting), so we can't reuse "stable" => JA4S as we originally considered. We're tentatively suggesting JA4E ("ephemeral-extension-invariant"), but defer to your judgement on the letter/naming convention.
- Scope of "ephemeral": should the initial set match nDPI's and our fingerproxy PR's (session_ticket / RFC 9149, pre_shared_key / RFC 8446, padding / RFC 7685), or should it be extensible/versioned as new ephemeral extensions appear in future TLS revisions?
- Relationship to JA4 vs JA5: should this be a JA4+ sibling method, or fold into whatever JA5 ends up being? Happy to align with whatever direction FoxIO is already planning.
We are raising this as a specification proposal, not a documentation PR, since JA4+ naming and the formal definition of each method are maintained by FoxIO and subject to licensing/trademark considerations (per the JA4+ README licensing section). Once there's agreement on the name and the precise extension list/algorithm, we're happy to help with spec docs and/or a reference implementation.
References
- https://www.ntop.org/is-ja4-now-obsolete/ (ntop, Jan 2026)
- feat(ja4): add stable JA4 fingerprint variant wi1dcard/fingerproxy#42 (our existing Go implementation of a stable JA4 variant)
- RFC 8446 (TLS 1.3, pre-shared key extension)
- RFC 9149 (TLS 1.3 session ticket extension)
- RFC 7685 (TLS padding extension)
Activity
elpy1 commentedon Jul 10, 2026
I implemented essentially this same normalisation for a fingerprint observatory (thumbprint.me) and reached the same conclusion from our live traffic and controlled captures. Everything below is reproducible from the public catalog.
For the internal normalised TLS stack fingerprint, I strip four extensions:
0015padding - RFC 76850023session_ticket - RFC 50770029pre_shared_key - RFC 8446002aearly_data - RFC 8446The first three match nDPI and the fingerproxy PR, and they reproduce the findings: across our controlled matrix, stripping them collapses 51 distinct (cipher-list, extension-list, signature-algorithm-list) keys to 40 - all are browser session captures, with
pre_shared_keythe dominant splitter, followed bysession_ticketandpadding.We also strip
002aearly_data on protocol grounds (no measured data). RFC 8446 4.2.10 states a client may only sendearly_datain a ClientHello together with apre_shared_key, so it should never appear except as part of the same resumption state we already strip via0029. In our own data it changes nothing:002ais present in zero of our captures. But a deployment that enablesearly_datacould result in resumed-0-RTT hellos carrying002athat diverge from resumed-non-0-RTT ones, so it might be worth including it by default.Validated against our controlled capture matrix - 21 distinct client families (browsers, curl/wget and their bindings, Rust hyper/reqwest, Go, Node, Python, PowerShell, URLSession) driven through the capture endpoints across linux/macOS/windows, 218 sessions (as of 2026-07-10), each with the client identity declared before capture. The full catalog is publicly available at
https://thumbprint.me/api/v1/catalog. The rule: after stripping the set, no two clients on different TLS libraries should share a (cipher-list, normalised-extension-list, sigalgs-list) key.Result on the current matrix: zero cross-library collisions. The only clients that share a key are same-library - Chrome/Chromium/Edge, curl and its bindings, hyper/reqwest etc, which seems correct considering JA4 fingerprints the client based on the TLS stack.
Two example before/after pairs from our captures, each a single browser recorded on a cold connection and then on a resumed one:
Chrome 149 over TCP (catalog session 201):
Safari 26.4 over QUIC/HTTP3 (catalog session 200) - the same client ntop used:
In both, the resumed ClientHello differs from the cold one by exactly one added extension -
0029pre_shared_key - which ticks the JA4_a extension count up by one (16 -> 17 and 11 -> 12 above) and changes the JA4_c hash, so the same browser reports two different JA4s. Cipher list and every other extension are identical. Stripping the above set normalises both members of each pair back to a single key. Each linked catalog page shows the captures assocaited with each browser session, with the full rawja4_rbehind each, so the added0029is visible directly.john-althouse commentedon Jul 21, 2026
Keeping these ephemeral extensions as part of the fingerprint was intentional as having a different fingerprint for each TLS library depending on what it's doing is valuable information to have. Yes, this means each TLS library will have multiple (~4-8) fingerprints which can be annoying to maintain manually but as we move to automated ja4db enrichment, is a non-issue.
We could update the standard to include a JA4_e option which ignores these extensions so that a single library produces a single JA4_e fingerprint. However, I believe the best course of action is to simply categorize the fingerprints in your lookup tables. You may begin to notice that certain destination pages always expect a resumed fingerprint and not an initial:
decadence commentedon Jul 28, 2026
How do we access ja4db? I filled request form but got no answer.
lucaderi commentedon Sep 21, 2026
@john-althouse @giuseongit In order to resume this thread I have written this https://www.ntop.org/introducing-ja5-a-stable-collision-resistant-successor-to-ja4/ and I am open to work together if you're interested.
john-althouse commentedon Sep 22, 2026
@lucaderi I've reached out over email. We should absolutely work together.