PROTOCOLS-FUN . JA4+

JA4+ Fingerprinting

identity from the wireFoxIO JA4+ suite
rev 2026.08
members JA4 · H · S · T · L · X · SSH · q
beats JA3 sort-before-hash
truth coherence > any one hash

JA4+ is a family of fingerprints for how a client (or server) speaks at each layer: TLS, HTTP, TCP, latency, certificate, SSH, QUIC. It replaced JA3 because it sorts lists before hashing - so it survives the per-connection TLS extension shuffling Chrome shipped in 2023 that made JA3 blocklists useless. Universal rules below apply to every member: every hash is sha256[:12] (12 hex chars), GREASE (RFC 8701) is stripped everywhere before counting/hashing, an empty list hashes to 000000000000, counts are 2-digit capped at 99, and lists are sorted only where the sender could reorder them. Hover the JA4 string below.

sourceTLS ClientHellofingerprintsthe client's TLS library/build

The flagship. Format a_b_c = readable prefix, then two hashes. Real Chrome below - hover a segment for how it's derived.

the #1 gotcha - the SNI/ALPN asymmetry. SNI and ALPN are counted in the extension count (chars 6-7) but removed before the extension hash. In FoxIO's tls12.pcap the count is 15 while the hashed list holds 13 - the difference is exactly SNI + ALPN. Get this wrong and every fingerprint is off.
hash c is half-sorted. The extension list is sorted (an evader can reorder extensions), but the signature-algorithm list appended after the last _ is kept in wire order (it's an OS/stack tell). Version comes from the supported_versions extension (highest non-GREASE), not the legacy version field.
JA4SServerHelloJA4Xthe X.509 cert

JA4S <proto><ver><ext#><alpn>_<chosen-cipher>_<exthash> - fingerprints the server's TLS stack + its per-connection choices. Clusters C2 servers sharing a config.

t130200_1301_a56c5b993250
t13 02 00TCP/TLS1.3, 02 ServerHello extensions, 00 ALPN selected
1301the single cipher the server chose (TLS_AES_128_GCM_SHA256)
a56c5b993250sha256[:12] of ServerHello extensions in wire order (NOT sorted) - the server owns its order, so order is signal

JA4X <issuerhash>_<subjecthash>_<exthash> - hashes the certificate's OIDs (field types), not their values, so tooling that randomizes CN/org but not cert shape still collides.

2166164053c1_2166164053c1_30d204a01551
issuer == subject = self-signed, the tell that clusters C2 frameworks (Sliver, Havoc). OIDs are hashed as DER content bytes only (drop tag + length). Instructive: a Go TLS server's JA4S is byte-identical to Sliver C2 - JA4S fingerprints the library, not the app.
sourcethe HTTP request headh1 line-framing + h2 HPACK

Format a_b_c_d. Fingerprints the HTTP client by method, header set + order, cookies, and language.

ge11cn060000_4e59edc1297a_4da5efaf0cbd_000000000000
segvaluederivation
ge 11method + versionfirst 2 chars of method (ge=GET), 11=HTTP/1.1 (20=h2, 30=h3)
c / ncookie · refererflags: Cookie header present? Referer present?
06header countheaders excluding Cookie + Referer, cap 99
0000Accept-Languagefirst 4 alnum of the first Accept-Language (en-US -> enus); 0000 if none
_bheader hashsha256[:12] of header names in WIRE ORDER, minus Cookie/Referer/h2 pseudo-headers
_c _dcookie hashessha256[:12] of sorted cookie names, then sorted name=value pairs; 000000000000 if none
header ORDER feeds hash b. Any framework that parses headers into a map (Go net/http, most stacks) destroys order and computes a wrong JA4H. You must read the raw request head - see the "from the wire" point on the Spoof tab.
JA4Tthe TCP SYNinvisible to a socket — needs eBPF

JA4T <window>_<option-kinds>_<mss>_<wscale> fingerprints the OS TCP stack. The SYN is consumed by the kernel before accept() returns, so a normal server can't see it - eBPF recovers it off the wire.

64240_2-1-3-1-1-4_1460_8
64240TCP window (Windows 64240, Linux/mac 65535)
2-1-3-1-1-4option kinds in WIRE ORDER (2 MSS, 4 SACK-OK, 8 timestamps, 1 NOP, 3 wscale, 0 EOL) - NOT sorted; the order is what separates OS stacks
1460 _ 8MSS value, window-scale shift. The 4th field is wscale, not TTL (common error)

Ground truth: Windows 64240_2-1-3-1-1-4_1460_8 · Ubuntu 65535_2-4-8-1-3_1460_8 · macOS 65535_2-1-3-1-1-8-4-0-0_1460_6. Windows omits timestamps (opt 8); an MSS below 1460 betrays a tunnel/VPN/proxy in front of the host.

JA4L <latency_us>_<ttl> - one-way latency estimate (half the handshake delta, microseconds) + IP TTL / IPv6 hop-limit (64 Linux/mac, 128 Windows, 255 gear, minus hops).

1424_64
JA4L is physics, not a field. You can rewrite the TTL byte, but you can't forge round-trip time without actually being that far away. It's the hard floor of the spoofability gradient. (Endpoint caveat: the server-leg JA4LS is really your own SYN-processing time, not network latency.)
JA4SSHencrypted SSH cadenceJA4 qQUIC Initial

JA4SSH reads an encrypted SSH session's behaviour over a 200-packet window - it fingerprints activity, not just identity. c<len>s<len>_c<pkts>s<pkts>_c<acks>s<acks>.

c76s76_c71s59_c0s70
the ACK counts are the tell. Interactive forward session = client types (c69s0); a reverse shell flips the ACKs to the server (c0s70) because the server is the one typing. Same identity, opposite direction - visible without decrypting a byte of payload.

JA4 q is identical to JA4, with protocol char q, but the ClientHello lives inside the encrypted QUIC Initial.

q13d0310h3_55b375c5d22e_cd85d2d88918
"encrypted" doesn't mean unfingerprintable. QUIC v1 Initials are AEAD-protected, but the keys derive (RFC 9001) from the Destination Connection ID, which travels in the clear. Any observer derives the keys, decrypts the Initial, reassembles the CRYPTO frames, and parses the ClientHello. QUIC's own key schedule hands out the handshake.
the headline changesort before hash

JA3 hashed the cipher + extension lists in wire order. In early 2023 Chrome began randomizing TLS extension order per connection (default ~Chrome 110). With ~15! orderings, every Chrome connection produced a different JA3 - blocklists on the internet's most common client became meaningless, and salesforce/ja3 was archived.

sort-before-hashJA4 sorts the cipher + extension lists, collapsing randomized order back to one stable fingerprint
readable prefixJA4's a segment is greppable before you touch a hash; JA3 was one opaque MD5
reason-preservingseparate cipher + extension hashes, sigalgs kept in order - drift is diagnosable, not just "different MD5"
myth to correct: GREASE is stripped by both JA3 and JA4, so GREASE is not the differentiator - sorting is. The rule throughout JA4+: canonicalize what an evader can cheaply randomize (cipher/extension order), preserve what the stack fixes (sigalg order, TCP option order, ServerHello order).
gradientapp → library → kerneltruthcross-layer coherence

Forging gets harder the lower you go, because control moves from app to library to kernel:

memberforgehow
JA4H (HTTP)trivialclient owns method/headers/order - just write bytes
JA4 (TLS)moderatedrive cipher/ext/curve/sigalg sets via uTLS; easier than JA3 (need the right set, not order)
JA4T (TCP)hardno per-conn socket knob; raw-packet the SYN or rewrite it on egress with eBPF (order included)
JA4L (latency)can'tround-trip physics; TTL you can set, timing you'd have to actually be there
from the wire, not the library. Computing JA4 from a parsed library struct (Go's tls.ClientHelloInfo, a header map) is lossy and wrong: the lib strips GREASE, hides extension order, drops what it doesn't model, and a header map destroys JA4H order. An honest fingerprinter sits at the wire (proxy / sensor / eBPF), then replays the untouched bytes into the stack.
a single hash is not identity - coherence is. JA4 collapses exactly where an adversary borrows a real browser's stack: against browser-driving agents JA4-alone can score worse than raw IP, while the full multi-layer stack reaches ~0.99. The signal is agreement across layers - TLS says Chrome-on-Windows but the SYN says Linux = a tool wearing a mask. Ambiguity is real: Sliver's cipher hash is shared with Evilginx and stock Go, because it fingerprints the Go stack, not Sliver.
consumed byCloudflare, AWS, Fastly, Akamai, Zeek, Suricata...
what it's for
  • Bot / scraper ID - JA4 near-perfectly separates HTTP-library bots; the stack can't lie as cheaply as a UA string.
  • The "User-Agent lies" problem - a request claims Chrome-on-Windows while its JA4 is stock Go or Python; JA4 fingerprints the stack, catching the spoof.
  • C2 / malware clustering - implants share a stack with their C2 (Sliver, Cobalt Strike, IcedID, Qakbot have published JA4/S/X/H values); value is in the combination, not one hash.
  • Reverse-shell detection via JA4SSH ACK-flip; proxy/VPN detection via JA4T sub-1460 MSS + stack-inconsistent SYNs.
ecosystem

Cloudflare JA4 Signals (hourly behavioural aggregates keyed on JA4; WAF field cf.bot_management.ja4) - and Cloudflare states plainly that fingerprints are easily spoofed, treating JA4 as a population join key, not an identity. Also AWS CloudFront/WAF, Fastly, Akamai, F5, HAProxy; analysis in Wireshark, Zeek (official package), Suricata (ja4.hash), Censys. Licensing: JA4 is BSD-3; the rest of JA4+ is FoxIO License 1.1 (research/internal, not for monetization).

a working reference implementation: ja4-loko computes every member above from raw wire data (validated byte-exact vs FoxIO vectors), recovers JA4T/JA4L via an eBPF tc sensor, and can forge JA4/JA4H/JA4T to demonstrate the gradient. See also Attack & Mitigation and eBPF in the Network Stack.