TLS client fingerprinting has become one of the workhorse techniques in network security, and JA4 (FoxIO) has been the de facto standard for identifying TLS clients from their ClientHello messages. But recent production experience at ntop surfaced two real limitations, and today we’re announcing nDPI TLS Fingerprint (TLSFP in short), an open, patent-free extension of JA4 that addresses both.
Ephemeral Extensions Break Hash Stability
JA4 builds its fingerprint from the full set of TLS extensions a client presents, hashed together with the cipher suite list. The trouble is that not every extension reflects something stable about the client’s TLS stack. Extensions like session_ticket, pre_shared_key, and padding can appear or disappear from one connection to the next for the same client build, simply because of session-resumption state or record-size padding heuristics and not because the client changed.
We first documented this in “Is JA4 Now Obsolete?” blog article, where a single macOS Safari instance produced multiple different JA4 values across consecutive connections to the same site, purely because Safari opportunistically resumes sessions via the TLS 1.3 PSK extension (RFC 8446). This is a real problem for security use cases: an analyst or a detection rule relying on JA4 for allow-listing or threat-hunting sees the same client “change identity” for no meaningful reason, which either causes false negatives or forces defenders to loosen matching rules in ways that also let attackers blend in more easily. As ephemeral TLS extensions become more common (session resumption in particular is increasingly the norm rather than the exception) this instability only gets worse over time. This is exactly the gap the community discussion in the FoxIO JA4 issue tracker later converged on independently.
Blindness to Supported TLS Groups
A separate, subtler issue is that JA4 only ever looks at which extensions are present, never at the values carried inside them. In particular, the supported_groups extension (it lists the elliptic curves and key-exchange groups a client is willing to negotiate) is invisible to JA4’s hash beyond the bare fact that the extension exists. That means two TLS stacks can end up with an identical JA4 fingerprint even though they support completely different curve sets. This is exactly the kind of gap that fingerprint-evasion tooling can exploit, cloning a browser’s published JA4 value while leaving a generic TLS library’s default curve list untouched underneath.
What TLSFP Does
TLSFP keeps JA4’s overall structure and cipher-suite handling intact, and adds two things:
- Ephemeral-extension sanitization. A curated, versioned registry of ephemeral extension types (
padding,session_ticket,pre_shared_key,early_data,cookie,psk_key_exchange_modes) is excluded from both the extension count and the extension hash before fingerprint computation. The result is a fingerprint that stays stable for the same client across resumption-heavy sessions, without discarding any of JA4’s other signal. - A fourth hash field,
TLSFP_d, computed from the sorted, GREASE-filteredsupported_groupsvalues. This gives TLSFP visibility into curve/group selection that JA4 never had, which directly helps reduce false positives and hash collisions between TLS stacks that otherwise look identical to JA4.
The result is a four-field string, TLSFP_a_TLSFP_b_TLSFP_c_TLSFP_d, where TLSFP_b (the cipher-suite hash) is byte-identical to JA4_b for backward compatibility, and the extension and supported-groups handling do the new work.
Example: JA4 vs. TLSFP on the same ClientHello
The spec’s test vectors illustrate the difference concretely. For a client whose ClientHello includes padding and pre_shared_key among 16 total extensions, and that advertises five supported groups:
# JA4 (unsanitized): extension count includes padding + PSK
JA4 = t13d1516h2_acb858a92679_562e8b7393e5
# TLSFP: same client, ephemeral extensions excluded from count and hash,
# plus a new supported-groups hash appended
TLSFP = t13d1514h2_acb858a92679_7dc829385eb9_0a47a2b05960
A few things to note in that output:
TLSFP_b(acb858a92679) is identical in both cipher-suite handling is unchanged.- The extension-count digits in
_adrop from16to14oncepaddingandpre_shared_keyare excluded, andJA5_cchanges accordingly since it hashes the sanitized extension list instead of the raw one. TLSFP_d(0a47a2b05960) is brand new: a truncated SHA-256 of the sorted, GREASE-filteredsupported_groupslist. Whensupported_groupsis absent (legacy RSA-only clients), this field is left empty rather than hashed, so “absent” stays distinguishable from “present but sparse”:
Open and Patent-Free by Design
We’re publishing the TLSFP specification openly, under a permissive license, specifically so it can be adopted widely without licensing friction, the same spirit that made JA4 useful across the ecosystem, but without the ambiguity we’ve seen crop up around JA4+ naming and licensing. The draft specification, including the full byte-level algorithm, ephemeral extension registry, and worked test vectors, is maintained alongside nDPI.
References
- ntop, nDPI TLS Client Fingerprint Format
- ntop, Is JA4 Now Obsolete?
- FoxIO JA4 issue #303, community discussion on the same ephemeral-extension problem
