Every TLS handshake carries a fingerprint of the software behind it. Before any application data moves, the client sends its ClientHello — TLS version, cipher list, extensions, curves, and the order they’re in — and that arrangement comes from whatever library built the client, not from the user-agent sitting above it. Hash it and you get a JA3, or the sturdier JA4: a short string that says, in effect, this is the software that opened the connection.
Nearly everyone uses that string to ask one question: does this fingerprint resemble Chrome? Stand the handshake next to a known-good Chrome and flag whatever doesn’t line up.
I wanted a different question — not what does it look like, but how does it behave? Resemblance you can fake in an afternoon; behaviour is much harder, because it isn’t a property of the handshake at all — it’s a property of everything that handshake has ever done. Answering that question is the reason I built tls-reputation.com .
One handshake barely talks
A single ClientHello, on its own, tells you less than you’d hope. The one thing you can honestly read off it in isolation is ALPN — a modern browser offers both h2 and http/1.1, in that order, and a client that offers only http/1.1, or lists them the wrong way round, isn’t the browser it claims to be. Past that, staring at one handshake doesn’t get you far.
h2, http/1.1 row, overwhelmingly a real Chromium engine — and the neutral tail is what's still anonymous.Getting inside the firehose
To ask the behaviour question you need a corpus, and there’s an unusual place to build one — from inside a residential proxy network. When you’re a node in that network you don’t see a single connection, you see the firehose: every JA3/JA4 flowing through, and — the part that matters — the SNI attached to each one, the domain it’s reaching for.
So I put myself inside one. These pools are fed by bandwidth-sharing SDKs — the “get paid to share your connection” kind bundled into free apps — so I wrote my own client for one of them and joined as a swarm of peers, a Sybil fleet, and then did nothing but listen. No real traffic of my own; just a spread of vantage points, logging the handshakes and SNIs that strangers’ sessions dragged past them. That pile of observations is the corpus, and it’s live now — you can poke at it yourself , or start from the graph just below. (What those borrowed sessions are actually doing in there — the brute-forcing, the scraping, the credential runs — is a separate story for another post.)
www.facebook.com is a TLS fingerprint, sized by how much of that domain's traffic it carried. Click one to expand it — and watch a mainstream-browser fingerprint fan out to a hundred unrelated domains while a single-purpose tool stays pinned to a handful. Drag nodes around; reseed from any other domain by changing ?sni=.The tell is promiscuity, not shape
That’s where the tell lives. Not the shape of the fingerprint — its promiscuity. A real browser’s fingerprint touches the handful of domains its human actually visits. Some fingerprints come through, reaching for hundreds of unrelated SNIs in a row: a bank, a sneaker drop, a social API, an airline, another bank. Nobody browses like that. That spread is a tool being pointed at one target after another, and the fingerprint is just the barrel it fires through.
And the handshake usually names the tool anyway. A JA3/JA4 that matches python-requests, or Go’s default HTTP client, or plain curl — especially the exact stack those ship with inside an Alpine container — was never a person, and I didn’t need the User-Agent to know it. The UA is the field an operator dresses up. The TLS stack is the one they forget.
So TLS stops being a per-connection resemblance check and becomes a reputation layer: you don’t judge one handshake, you judge how lit-up it is across everything you’ve watched it do. The credential-stuffing work showed the mirror image of this — an attacker minting 60 different JA3s across 64 connections, working so hard to look varied that the variety itself became the signature. Promiscuous or paranoid, the fingerprint gives the population away.
Naming a million handshakes
The corpus does something else, too. Alongside the promiscuity score it carries a ground-truth naming layer — I ran the real clients, every Python version, Node, Go, Java, Rust, and the browsers, in every base image, and recorded the exact JA4 each one emits, so a fingerprint can be turned back into a name. That lets me ask the pile a blunter question than is this abusive: what is it? About two-thirds of the connections resolve to a client I can name, and one thing dominates by a wide margin — a Chrome/Chromium TLS fingerprint.
It’s not fake Chrome. It’s real Chrome.
The lazy read is “mostly Chrome”; the lazy correction is “mostly fake Chrome — curl-impersonate and the uTLS crowd minting Chrome’s JA4 out of a library.” The corpus says both are wrong. Chrome since 110 shuffles its own ClientHello on every connection — a randomized extension order — so a single Chrome JA4 fans out into a hundred-odd distinct JA3s, a moving target most impersonators never bother to reproduce. Run that test on the Chrome-fingerprinted traffic and better than 99% of it, by volume, permutes correctly. The static impersonators — perfect JA4 but a fixed hello — are a rounding error. (uTLS and the curl_cffi / tls-client crowd built on it can replicate the shuffle too; in this corpus they barely register.) This isn’t something dressed up as Chrome. It’s Chrome.
Which reframes who’s on the other end. The cheap tier is what the stack named a moment ago — requests-in-Alpine, Go’s default client, the tools that never tried. But the bulk of the traffic skips impersonation and runs the genuine engine: headless Chrome under Puppeteer or Playwright, the Chromium antidetect browsers, Electron, Android’s WebView. They share Chrome’s TLS stack down to the byte, so the fingerprint can’t tell them apart from each other — or from a real person’s Chrome — which is precisely why they’re used. (iOS’s WebView speaks Apple’s own TLS, not Chrome’s, so it isn’t in this pile: what you’re looking at is the Chromium lineage, desktop and Android.) And the moment an adversary stops faking a browser and starts driving one, the handshake can’t tell the driver from the driven.
Where the real Chrome actually goes
There’s a satisfying sanity check hiding in the numbers. About 56% of all the connections carried a Chrome/Chromium TLS fingerprint — and Chrome’s real-world market share is roughly that. The browser slice of the corpus mirrors the actual web, which is the kind of thing that makes you trust a dataset.
It also almost fooled me. Because when I pulled the destinations — the SNIs that Chrome traffic was reaching — the market-share resemblance fell apart. 34% was a major short-video platform’s internal API and attestation endpoints; 12% was the anti-bot layer itself — ThreatMetrix, DataDome, PerimeterX, hCaptcha, Cloudflare Turnstile, Queue-it — traffic grinding through defences rather than tripping them; 11% was retail scalping (big-box retailers and collectibles storefronts), 5% ticketing (major upcoming sporting events and the big ticketing platforms). Fonts, analytics and payment SDKs — the subresources a browser fetches when it renders a real page — were about a tenth of it, and Stripe was a rounding error. This wasn’t rendering checkout pages; it was hitting APIs. And the give-away, the domains that exist for exactly one reason: ip-scan.adspower.net, app.octobrowser.net, remote-ip-v6.insomniacbrowser.com, aycd.io, hypersolutions.co — anti-detect browsers and sneaker-bot tooling, phoning home. That is the “real Chromium” from a section ago: it’s Chromium because anti-detect browsers are Chromium forks, which is the whole reason it fingerprints as genuine Chrome and permutes like it. The fingerprint was honest and the market-share math was reassuring; the destinations were scalpers, account farms, and the automation industry that sells the disguise.
The baseline I didn’t build
One honest caveat: a reputation layer is only as good as the baseline you measure against. The proxy firehose shows you what abusive traffic looks like — it doesn’t tell you what a normal human population’s handshakes look like, and without that second pole you’re grading against a curve you drew yourself. The cleanest baseline I could think of was to stand up an open WiFi with a captive portal that, once someone agreed to it, recorded the ClientHello on the way through, and hold that distribution up against the corpus. I never built it, and I’m not going to — the consent-and-privacy friction isn’t worth a tidier histogram. But the gap it would fill is real, and worth naming: reputation without a ground truth is a hunch with a database behind it.
Where TLS runs out
So here’s the shape of it. TLS reputation is genuinely good at two things: naming the software behind a handshake, and catching the lazy tier — the clients that never tried to blend in at all. For those, the handshake is a confession.
But the bulk of the automated traffic isn’t lazy, and it isn’t faking anything. It’s driving a real browser, and a real browser’s handshake is a real browser’s handshake. At that point the TLS layer has told you everything it can: it can say this is Chrome, but it cannot say this is a human’s Chrome. And the one question that actually matters — is there a person here? — is exactly the one the handshake can’t answer.
Which is why you can’t stop at the network. The moment the fingerprint comes back “real Chrome,” the only way forward is to make the client prove it’s more than a fingerprint — run a JS challenge, measure the hardware, watch what it does. TLS narrows the field and names the cheap attackers for free; the JS challenge is where you separate the browser that’s being driven from the human who’s actually there. I use this corpus as one layer inside a larger detection stack.