Skip to content

Proposal: new JA4+ method for an "ephemeral-extension-invariant" stable client fingerprint (tentative name JA4E) #303

Description

@giuseongit

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:

wi1dcard/fingerproxy#42

Summary of that implementation, for reference:

  • Adds an opt-in StableJA4Fingerprint alongside 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:

  1. 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.
  2. 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?
  3. 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

Activity

  1. elpy1 commented on Jul 10, 2026

    @elpy1
    Contributor

    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:

    • 0015 padding - RFC 7685
    • 0023 session_ticket - RFC 5077
    • 0029 pre_shared_key - RFC 8446
    • 002a early_data - RFC 8446

    The 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_key the dominant splitter, followed by session_ticket and padding.

    We also strip 002a early_data on protocol grounds (no measured data). RFC 8446 4.2.10 states a client may only send early_data in a ClientHello together with a pre_shared_key, so it should never appear except as part of the same resumption state we already strip via 0029. In our own data it changes nothing: 002a is present in zero of our captures. But a deployment that enables early_data could result in resumed-0-RTT hellos carrying 002a that 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):

    cold      t13d1516h2_8daaf6152771_d8a2da3f94cd
    resumed   t13d1517h2_8daaf6152771_b6f405a00624
    

    Safari 26.4 over QUIC/HTTP3 (catalog session 200) - the same client ntop used:

    cold      q13d0311h3_55b375c5d22e_f2a83c8e78ae
    resumed   q13d0312h3_55b375c5d22e_151122171f7d
    

    In both, the resumed ClientHello differs from the cold one by exactly one added extension - 0029 pre_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 raw ja4_r behind each, so the added 0029 is visible directly.

  2. john-althouse commented on Jul 21, 2026

    @john-althouse
    Collaborator

    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:

    chromium 14x initial    t13d1516h2_8daaf6152771_d8a2da3f94cd
    chromium 14x resumed    t13d1517h2_8daaf6152771_b6f405a00624
    
  3. decadence commented on Jul 28, 2026

    @decadence

    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

    How do we access ja4db? I filled request form but got no answer.

  4. lucaderi commented on Sep 21, 2026

    @lucaderi

    @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.

  5. john-althouse commented on Sep 22, 2026

    @john-althouse
    Collaborator

    @lucaderi I've reached out over email. We should absolutely work together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions