JA3

JA3 is the original TLS client fingerprint, published by Salesforce in 2017 and named for the three engineers who wrote it. It is an MD5 over five fields of the ClientHello, taken in the order the client sent them. It is still everywhere, and it no longer identifies browsers.

How it is built

Five fields, joined with commas. Within a field, values are joined with hyphens. Everything is decimal:

771,4865-4866-4867-49195-49199,0-23-65281-10-11-35-16-5-13,29-23-24,0

771                          TLS version
4865-4866-4867-49195-49199   cipher suites, in the order offered
0-23-65281-10-11-35-16-5-13  extensions, in the order sent
29-23-24                     elliptic curves
0                            point formats

That string is hashed with MD5, and the 32-character result is the JA3. Both forms are worth having: /api/ja3/full returns the string, /api/ja3 the hash. When two clients differ, only the string tells you where.

FieldSourceNotes
771TLS versionThe legacy client_version from the ClientHello. TLS 1.3 clients still write 771 (0x0303) here for compatibility, so this field rarely says anything useful any more
4865-4866-…Cipher suitesIn the order offered, as decimals, GREASE removed
0-23-65281-…ExtensionsIn the order sent, as decimals, GREASE removed. This is the field that broke
29-23-24Elliptic curvesThe supported_groups extension, in order
0Point formatsec_point_formats. Almost universally just 0

GREASE

Clients deliberately advertise values that mean nothing — reserved codepoints such as 0x0a0a and 0x1a1a, defined by RFC 8701. Their purpose is to catch middleboxes that choke on anything they do not recognise, and they are inserted at random positions. Every fingerprint scheme strips them before hashing; a tool that does not will produce a different answer on every connection and be wrong about why.

Why it broke

In 2023 Chrome began permuting the order of its TLS extensions on every connection. This was intentional: the ordering had become a de facto interface, middleboxes and servers had started depending on it, and randomising it keeps that dependency from ossifying.

JA3 hashes the extension field in the order sent. Permute the order and the MD5 changes completely. One Chrome install now produces a different JA3 on every connection, so a JA3 blocklist entry for Chrome matches nothing, and a JA3 allowlist entry expires the moment it is written.

JA4 exists mainly to fix this. It sorts the cipher and extension lists before hashing, so a permutation cancels out.

Where it still works

Non-browser clients. Almost nothing outside the major browsers randomises. Python over OpenSSL, Go’s crypto/tls, Java, curl, and most malware families emit a stable extension order, which means a stable JA3. For identifying scripted traffic it still works exactly as it did in 2017.

Existing intelligence. A decade of threat feeds, IDS rules and incident reports are indexed by JA3 hash. That corpus does not disappear because browsers moved on, and matching against it needs a JA3.

Collisions cut both ways. JA3 was always coarse: any two applications sharing a TLS library and its defaults share a hash. That is a weakness when you want precision and a feature when you want to catch a family of tools at once.

JA3S

The server-side counterpart, hashing the ServerHello: version, the single chosen cipher, and the server’s extensions. Paired with a client JA3 it describes a conversation rather than a party, which is how it gets used to identify command-and-control channels. Not implemented here — this service is the server, so its own JA3S would be a constant.

See your own

curl -s https://kittens.sh/api/ja3/full returns the unhashed string for whatever client you run it from, or the inspector shows every field it was assembled from, in wire order.