The first thing your browser sends when it connects to an HTTPS site is a message called the ClientHello, and it goes out in the clear — before any key is agreed, before a single byte is encrypted. It has to. Encryption is a negotiation, and you can’t negotiate in a language the other side can’t read yet. So the ClientHello is a plaintext list of everything your client is prepared to do: which TLS version it wants, which cipher suites it supports, which extensions it’s using, which elliptic curves it knows. It reads like a menu.
Here’s the part that turns out to matter. Every piece of software builds that menu a little differently. Chrome offers one specific set of ciphers in one specific order, with one specific list of extensions. Firefox offers a different set. curl offers something much shorter. A Python requests script, a Go program, a piece of malware phoning home to its command-and-control server — each has its own idiosyncratic way of saying hello, baked in by whatever TLS library it was built against. And that means the shape of the hello identifies the software, before it has proven who it is, before it has asked for anything, before anything is encrypted.
That’s the whole idea behind TLS fingerprinting, and it’s one of the cleaner examples of a recurring truth in security: you are identifiable not just by what you say, but by how you say it.
JA3: hashing the hello
In 2017, three engineers at Salesforce — John Althouse, Jeff Atkinson, and Josh Atkins — published a way to turn that observation into something you could actually match on. They called it JA3, after their initials.
The method is almost aggressively simple. Take five fields out of the ClientHello — the TLS version, the list of cipher suites, the list of extensions, the elliptic curves, and the elliptic curve point formats. Write each one out as its numeric values, join them with commas, and you get a string like a long comma-separated fingerprint of the client’s capabilities. Then run MD5 over the whole thing. Out comes a 32-character hex hash. That hash is the JA3 fingerprint, and every distinct combination of those fields produces a different one.
What made it powerful was that a fingerprint isn’t tied to an IP address or a cookie or anything the client controls at the application layer. It’s a property of the software — of the TLS library and how it was configured. A botnet operator could rotate through ten thousand IP addresses, but if every node was built from the same tooling, they all whispered the same JA3 hash on the way in. Malware families turned out to have remarkably stable, distinctive fingerprints, because their authors rarely touched the TLS stack. Defenders could take a JA3 from a known-bad sample and hunt for every other connection that matched it, across the whole network, without ever decrypting a thing.
It caught on everywhere — intrusion detection systems, bot-detection services, CDN edge logic. For a few years, JA3 was the quiet workhorse of “is this client who it claims to be.”
The field that killed it
There was a flaw in JA3, and it was hiding in one word: order.
JA3 read the extensions in the exact sequence they appeared in the ClientHello and hashed them in that sequence. Reorder the same extensions and you get a completely different MD5. For years this didn’t matter, because clients sent their extensions in a fixed order — the order was part of what made the fingerprint specific.
Then Google decided that fixed order was a problem. Not a fingerprinting problem — an ossification problem. When millions of clients always send fields in the same order, middleboxes and servers start quietly depending on that order, and the protocol calcifies: you can no longer change the thing without breaking the internet. Google had already fought this battle with GREASE (RFC 8701), where Chrome deliberately jams random reserved values into its ClientHello so that servers are forced to tolerate values they don’t recognize, keeping the ecosystem limber. Randomizing extension order was the same medicine for a different joint.
So in early 2023, shipping in Chromium 110 (and leaking into the wild a little earlier through 108 and 109 builds), Chrome began shuffling the order of its TLS extensions on every single connection. Fastly watched it happen: starting around January 20th, 2023, the fraction of Chrome clients arriving with the old, stable JA3 fell off a cliff.
The math is brutal for JA3. Chrome’s ClientHello carries roughly fifteen extensions, and the number of ways to order fifteen things is fifteen factorial — about 1.3 trillion. For all practical purposes, every connection from a modern Chrome now produces a unique JA3 hash. The very thing that made JA3 specific — its sensitivity to order — is what made it shatter the moment order became random. A fingerprinting scheme that gives every visit a brand-new fingerprint isn’t a fingerprinting scheme anymore.
JA4: sorting before you hash
The fix came from John Althouse again — the same person, now at his own company, FoxIO — who published the JA4+ suite in 2023. JA4 is what you build when you already know how the first version dies.
The central repair is obvious in hindsight: sort the lists before hashing them. JA4 takes the cipher suites and the extensions, puts them in a canonical numeric order, and then hashes. Now it doesn’t matter what order the client sent them in — shuffled or not, the same underlying set collapses to the same fingerprint. Chrome’s randomization, which vaporized JA3, does nothing to JA4. And while it’s at it, JA4 ignores GREASE values wherever they appear, so those random reserved entries stop polluting the result too.
JA4 also stopped being an opaque blob. Instead of one MD5 smear, a JA4 fingerprint has three human-readable parts in an a_b_c shape. Here’s a real one:
t13d1516h2_8daaf6152771_e5627efa2ab1
The first section, t13d1516h2, you can read straight off: t for TLS-over-TCP (it’d be q for QUIC), 13 for TLS 1.3, d because a server name (SNI) is present, 15 cipher suites, 16 extensions, and h2 — the first ALPN value, meaning the client offered HTTP/2. The second section is a truncated SHA-256 of the sorted cipher list; the third, a truncated SHA-256 of the sorted extensions plus signature algorithms. Because the parts are separated, you can hunt on just the readable prefix, or just the cipher hash, instead of needing an exact match on the whole thing. It’s a fingerprint you can actually reason about.
And JA4 is only the TLS piece of a family. The suite extends the same idea to the parts of a connection that also leak identity: JA4S for how the server replies, JA4H for the shape of an HTTP request, JA4X for the fields in an X.509 certificate, JA4L for latency, JA4T for the TCP layer. The insight generalized. Almost every layer of a connection has an involuntary accent.
The recon lesson underneath
Strip away the version numbers and TLS fingerprinting is a statement about how identity actually leaks, and it’s the same statement that Certificate Transparency and passive DNS make from other angles: the most durable intelligence about a system comes from the exhaust it can’t help emitting.
You cannot open a TLS connection without sending a ClientHello, and you cannot send a ClientHello without revealing the particular dialect of your TLS stack. It’s not a header you can strip or a cookie you can clear. It’s structural. The observer doesn’t have to touch you, doesn’t have to decrypt anything, doesn’t have to wait for you to log in — the identifying signal is in the opening syllable, sent involuntarily, in the clear.
Which is exactly why the arms race that followed is so predictable. If you’re on the other side — a scraper, a pen-tester, an automation that needs to look like a browser — you don’t want your Go program’s honest fingerprint announcing “bot” to every CDN. So tools like uTLS and curl-impersonate appeared, letting a client forge Chrome’s exact hello, cipher for cipher, extension for extension. And the defenders’ answer is to look for the inconsistencies: a client claiming Chrome’s JA4 while its HTTP behavior, its TCP timing, or its JA4H tell a different story. You can wear another software’s accent, but keeping every layer’s story straight at once is hard. The fingerprint of a good forgery is that it’s too perfect in one place and wrong in another.
There’s a limit worth stating plainly, because fingerprinting gets oversold: a TLS fingerprint identifies software, not a person. Millions of people run the same Chrome build and share a fingerprint; it is a category, not a name. It says “this is a real Chrome” or “this is Python pretending,” and that’s genuinely useful for sorting humans from automation. It does not, on its own, tell anyone who you are. The privacy question only sharpens when the fingerprint is one column in a wider table — joined to your IP, your timing, your behavior — and the whole is more identifying than the parts.
But the core fact is the one worth carrying around. Long before you send a request, before the padlock closes, before anything is secret, your client has already introduced itself by the specific way it says hello. It always did. JA3 and JA4 are just the tools that learned to listen for the accent.