What JA4 Fingerprints Miss
Introduction
While researching TLS fingerprints, I wanted to test what a matching JA4 value actually tells us. It groups connections by parts of their handshake. But can two connections in that group still behave differently?
I built a small local experiment to check. Two clients offered the same cipher suites in a different order. They produced the same JA4 value. The server still chose a different key exchange for each one.
That difference mattered to a rule I added on the server. One connection met the rule. The other did not. A separate rule based only on their shared JA4 could not tell them apart.
What JA4 keeps
TLS is the protocol behind HTTPS. A client starts a connection by sending a ClientHello. This message lists options it supports, such as TLS versions and cipher suites. The client and server use the handshake to agree on how to protect the connection.
JA4 turns selected parts of that message into a short fingerprint. It includes a TLS version indicator, counts of ciphers and extensions, a small ALPN label, and two hashes. ALPN is the part that helps negotiate an application protocol, such as HTTP/2.
The JA4 specification sorts cipher identifiers before hashing them. It also sorts extension identifiers and includes signature algorithms. This makes the fingerprint less sensitive to some ordering changes.
That is useful when grouping similar traffic. It also means the fingerprint leaves some details out.
The detail that disappears
Think about two lists:
Client A: [option 1, option 2]
Client B: [option 2, option 1]
After sorting:
Client A: [option 1, option 2]
Client B: [option 1, option 2]
The lists contain the same options. They express different preferences. Sorting keeps the options and removes their original order.
That distinction applies to TLS 1.2. Its cipher list is sent in preference order, according to the TLS 1.2 specification. A server can use that order when choosing a cipher suite. The result depends on the server's configuration.
My question was whether this lost detail could change a real connection outcome.
My local test
I used two TLS clients, a proxy that calculated JA4, and a local TLS server. Everything ran on loopback. I pinned the proxy version and used TLS 1.2 for both clients.
The clients offered these two cipher suites:
ECDHE-RSA-AES128-GCM-SHA256AES128-GCM-SHA256
The names are long, but the difference here is small enough to explain. The first uses an ephemeral Diffie-Hellman key exchange. The second uses RSA key exchange. The first supports forward secrecy: stealing the server's long-term private key later should not be enough to decrypt previously recorded sessions.
Both clients supported both suites. Their only configured difference was the order of that list. I set the server to follow the client's cipher preference.
| Client | First choice | Server selected | Local rule |
|---|---|---|---|
| A | ECDHE suite | ECDHE key exchange | Allow |
| B | RSA suite | RSA key exchange | Deny |
The rule was one I wrote for the test. It allowed the selected ECDHE suite and denied the selected RSA suite. It was a simple way to make the connection difference visible.
Both clients received this exact JA4 value:
t12d020800_08dfa304a768_36cef8aed422
I checked the captured ClientHello messages to confirm the reversed cipher order. I also recorded the selected cipher and the server's rule outcome. Each order produced the expected result in two runs.
I then repeated the experiment in a fresh project with a new certificate and an AES-256 cipher pair. The two orders again shared a JA4 value and produced different key exchanges. The shared value for that second pair was different from the one above.
Adding a JA4 block rule
Next, I added the shared fingerprint to the proxy's JA4 blocklist.
Both clients were blocked before they reached the TLS server. That included the client that would have met my server's rule. The proxy made the same decision because the value it checked was identical.
This is a normalization collision. Different inputs become the same representation after information is removed. There was no need to find a SHA-256 collision. In this case, sorting had already erased the difference before hashing.
A rule that only sees that shared value cannot recover the missing preference order.
What this test shows
The test shows a specific limit: one JA4 value can cover connections with different negotiated properties. A rule based on those properties may need information that JA4 does not retain.
The server configuration matters. A server that enforces its own cipher preference could choose the same suite for both clients. This example also uses TLS 1.2. TLS 1.3 removed static RSA key exchange, so this exact pair does not carry over.
I also cannot use a local test to estimate how often this happens in production. The policy was constructed for the experiment. I did not measure real users being blocked or the false-positive rate of a deployed service.
JA4 still gives me a useful way to group connections. This result changes how much I infer from a match.
What I would log
For a similar debugging task, I would keep a few observations together:
- The JA4 value and the tool version that produced it.
- The offered cipher order and the cipher the server selected.
- The negotiated TLS version and application protocol.
- The exact rule that allowed or blocked the connection.
Those fields answer different questions. A fingerprint explains why two connections were grouped. The selected parameters explain what they negotiated. The rule explains why they received a particular outcome.
The part I found most useful was tracing that chain all the way through. I could see the original difference, where it disappeared, and where it still affected the connection. Now, when two JA4 values match, I treat that as a starting point for comparison.