mTLS and client certificates: the strongest fingerprint of all
Most of the signals an anti-bot system collects are circumstantial. The cipher list in your ClientHello suggests a Chrome build. The ordering of your HTTP/2 SETTINGS frame suggests a particular networking stack. Your mouse movements suggest a human, or a script pretending to be one. Every one of those is a correlation, a guess about the kind of client that showed up, and a determined operator can forge most of them given enough effort. Then there is the one signal that does not work like that at all. If a server demands a client certificate and you cannot produce a valid signature with the matching private key, the conversation ends before the first byte of application data. No scoring, no probability, no second chance.
That is mutual TLS, and it is the closest thing the web has to a binary identity check at the transport layer. The server proves who it is, the way it always has. The client also proves who it is, by holding a key nobody else holds. This post walks through how that second proof is constructed on the wire, why TLS 1.3 changed where it lives in the handshake, where mTLS actually gets deployed in 2026, and why a client certificate is a categorically different kind of fingerprint from anything in the JA3-to-JA4 family.
The road map: first the handshake itself, message by message, with the exact field names and the exact bytes that get signed. Then the contrast between TLS 1.2 and 1.3, because the privacy properties moved. Then where this lives in production, from zero-trust service meshes to mobile API shields. Then the fingerprinting angle, both the certificate-as-identity argument and the certificate-as-fingerprint reality through JA4X. The through-line is that possession of a private key is not a probabilistic signal, and that single fact reorganizes everything downstream of it.
The CertificateRequest, Certificate, CertificateVerify exchange
A plain TLS handshake is one-sided about identity. The client sends a ClientHello, the server answers with its own certificate chain and a signature proving it owns the matching key, and that is the end of the authentication question. The client checked the server. The server has no idea who the client is beyond an IP address. mTLS closes that gap by adding a symmetric proof: the server asks for a certificate, the client supplies one, and the client signs the handshake to prove the certificate is really its own.
In TLS 1.3, defined in RFC 8446, the request is a discrete handshake message. After the server has sent its ServerHello and the encrypted extensions, it can send a CertificateRequest. The struct is small:
struct { opaque certificate_request_context<0..255>; Extension extensions<2..2^16-1>;} CertificateRequest;The certificate_request_context is an opaque blob. During the main handshake it is empty. It exists to disambiguate post-handshake requests, which we will come back to. The interesting content lives in extensions. Two of them carry the policy. The signature_algorithms extension tells the client which signature schemes the server will accept on the client’s eventual proof, so a client offering only an Ed25519 key learns immediately whether that key is usable. The certificate_authorities extension, when present, lists the distinguished names of CAs the server trusts, which lets a client holding several certificates pick the one chained to an acceptable root. A third extension, oid_filters, lets the server constrain acceptable certificates by the values of specific certificate extensions. The server is, in effect, publishing its acceptance policy before the client commits to anything.
When the client sees a CertificateRequest, it responds with two messages of its own. The first is a Certificate message carrying its certificate chain, structurally identical to the one the server sends. Per RFC 8446, the client omits this message entirely if no CertificateRequest arrived, and if it has no suitable certificate it sends the Certificate message with an empty chain rather than skipping it, so the server can tell the difference between a client that declined and a client that never saw the request. Sending an empty chain lets the server apply its own policy, which may be to fail the handshake or to fall back to some other authentication.
The second message is where the actual proof lives. CertificateVerify carries a digital signature, and that signature is the whole point of the exercise. Possession of a certificate proves nothing; certificates are public. What matters is that the client can sign with the private key that pairs to the public key inside the certificate it just sent. The signature in CertificateVerify covers a hash of the entire handshake transcript up to that point, which means it is bound to this specific connection and cannot be replayed against another.
What exactly gets signed
The construction of the signed content is worth getting exactly right, because it is the part people most often hand-wave. RFC 8446 specifies that the input to the signature is not the raw transcript hash on its own. It is a longer buffer assembled in a fixed order. First come 64 bytes of 0x20, the ASCII space character, repeated. Then a context string. Then a single 0x00 separator byte. Then the transcript hash itself.
For a client’s signature the context string is the exact literal TLS 1.3, client CertificateVerify. The server uses a parallel string with server in place of client. That difference is deliberate. The two context strings guarantee a signature produced for server authentication can never be mistaken for one produced for client authentication, even if an attacker could somehow induce identical transcripts. The 64 spaces of padding are there to defeat a class of cross-protocol attacks where a signature blob might collide with content signed by an older protocol or a different application that uses the same key.
Because the transcript hash includes the server’s random value and the key shares exchanged at the start, the same private key signing the same context string produces a different signature on every connection. There is no static token to capture and replay. An adversary who records a full mTLS handshake off the wire gets a signature that is worthless against any other session, against a different server, even against a reconnection to the same server, because the transcript will differ. The proof is fresh per handshake by construction. That property is what makes the private key, rather than the certificate, the thing that matters. The certificate is a public claim; the signature is the evidence; and the evidence cannot be stockpiled.
After CertificateVerify, the client sends its Finished message, the server validates the chain and the signature against its trust policy, and if everything checks out, application data flows. If the signature does not verify, or the chain does not build to a trusted root, or the certificate is expired or revoked or fails an oid_filters constraint, the server sends an alert and tears the connection down. There is no application-layer error page, no soft block, no challenge. The failure happens inside TLS, below HTTP, which is exactly why mTLS is so unforgiving and so useful.
Why TLS 1.3 moved the proof inside the encryption
In TLS 1.2 the certificate exchange happened in the clear. The server’s certificate, the CertificateRequest, and the client’s certificate all traveled as plaintext handshake records, visible to anyone on the path. A passive observer watching a TLS 1.2 mTLS handshake could read the client’s entire certificate, including its subject, its issuer, its serial number, and whatever identifying information the certificate carried. For a client certificate that names a specific user or device, that is a privacy leak built into the protocol.
TLS 1.3 changed the order of operations. The handshake establishes encryption keys earlier, immediately after the ServerHello, and everything from EncryptedExtensions onward is encrypted. The CertificateRequest, the server’s own Certificate, and crucially the client’s Certificate and CertificateVerify all travel inside that encrypted envelope. A passive observer on a TLS 1.3 connection sees the ClientHello and ServerHello in the clear and then a wall of ciphertext. The client’s identity is no longer on display.
This has a direct consequence for fingerprinting that is easy to miss. In TLS 1.2 the client certificate was a network-visible artifact; you could fingerprint it passively from a packet capture. In TLS 1.3 you cannot, unless you are the server terminating the connection. The certificate became an endpoint signal rather than a wire signal. That shift matters for anyone building detection on top of TLS, and it lines up with the broader move in TLS 1.3 toward hiding handshake metadata, the same impulse that produced Encrypted Client Hello. If you want the gritty detail on where the line between visible and hidden now falls, the TLS 1.3 handshake walk-through and the ECH explainer cover the adjacent ground.
The privacy gain is not free. Encrypting the client’s certificate means a middlebox that used to inspect mTLS handshakes passively can no longer do so, which is part of why some enterprise security appliances were slow to support TLS 1.3 and why a few deployments still pin to 1.2 specifically to keep the certificate exchange visible to their own monitoring. That is a deliberate trade against the protocol’s intent. The newer the stack, the harder it is to see a client’s identity from the network, and the more that identity becomes something only the terminating server holds. For a detection engineer this is the central fact: the strongest identity signal on the connection is also the one you cannot collect unless you are the endpoint being authenticated.
There is one more piece of the 1.3 design worth naming. Post-handshake authentication. A server that does not want to demand a client certificate up front, perhaps because only a few routes require it, can ask for one later. The client signals willingness during the initial handshake by sending the post_handshake_auth extension in its ClientHello. After application data is already flowing, the server can send a fresh CertificateRequest, this time with a non-empty certificate_request_context that ties the request to a specific challenge. The client answers with Certificate, CertificateVerify, and Finished, and the connection continues authenticated. This is how an API can let you browse public endpoints and then require a certificate the moment you touch a privileged one, all on the same connection.
Where mTLS actually lives
The textbook example of mTLS is two banks exchanging payment messages, and that example is real, but it undersells how broadly the mechanism has spread. The biggest deployment of client certificates today is not between companies. It is inside them, between services that never touch the public internet.
The shift came from zero-trust architecture. Google’s BeyondCorp model retired the idea that being inside the corporate network meant you were trusted, and the same logic moved down into the data center. If you cannot trust the network, then every service-to-service call has to authenticate on its own, and mTLS is the natural tool because it authenticates both ends and encrypts the channel in one step. A request from the billing service to the accounts service presents a client certificate; the accounts service verifies it and learns precisely which workload is calling, not just which IP connected.
Making that work at scale required solving an identity problem. A certificate has to name something, and in a fleet of thousands of ephemeral containers, naming them by hostname falls apart immediately. SPIFFE, the Secure Production Identity Framework for Everyone, answers this by giving each workload a URI-shaped identity, a SPIFFE ID like spiffe://cluster.local/ns/billing/sa/default, and encoding it in the certificate. The SPIFFE Verifiable Identity Document, or SVID, in its X.509 form is an ordinary X.509 certificate with that SPIFFE ID placed in the URI Subject Alternative Name field. The spec is strict about what a leaf SVID may contain. It MUST set the basicConstraints cA field to false, MUST set digitalSignature in key usage, and MUST NOT set keyCertSign or cRLSign, so a workload certificate can never be used to mint other certificates. The validation rules are equally strict on the receiving end, which is what lets a verifier trust an SVID without a human in the loop.
In a service mesh, all of this happens without the application knowing. Istio is the common example. Istiod runs an internal certificate authority, and each workload’s sidecar agent generates a private key, sends a certificate signing request to Istiod with its credentials, and receives back a short-lived X.509-SVID. The Envoy proxy beside the workload fetches that certificate over the secret discovery service API and uses it for every outbound and inbound connection. When the billing pod calls the accounts pod, the two Envoy proxies run a full mTLS handshake between themselves, each presenting its SPIFFE certificate, and the application code on either side sees a plain local connection. Under a STRICT PeerAuthentication policy the receiving Envoy refuses any connection that does not present a valid mesh-signed certificate carrying a valid SPIFFE identity. The certificates rotate on the order of hours, which limits the blast radius if a key leaks and means revocation is largely handled by expiry rather than by revocation lists.
That last point is worth sitting with, because revocation is the soft underbelly of every long-lived PKI. The classic way to undo a certificate before it expires is a certificate revocation list or an OCSP query, and both have well-known weaknesses around freshness, availability, and the privacy of the check itself. A mesh sidesteps the whole problem by making certificates so short-lived that revocation reduces to not reissuing one. If a workload is compromised and you stop the agent from renewing its SVID, the credential is dead within the rotation window with no list to distribute and no responder to query. On the public-internet side, where client certificates can live for a year or more, revocation matters again, and the same machinery that frustrates server-certificate revocation applies to client certificates too. The mechanics of that are their own topic, covered in the OCSP and stapling and Web PKI trust store write-ups, but the short version is that short-lived beats revocable nearly every time, and the mesh world figured that out first.
The public-internet face of mTLS is narrower but growing. API providers use it to lock down high-value endpoints. Cloudflare’s API Shield is a representative example: a hostname can be configured to require a client certificate, and once that rule is deployed, any request without a valid certificate is blocked at the edge before it reaches the origin. By default the certificates are issued by a Cloudflare-managed CA, and Enterprise customers can upload their own CAs, up to five at the account level, to validate against their own issuance. The mobile world uses the same primitive in a different shape. An app ships with a client certificate, or fetches one after the user logs in, and presents it on every API call, which means a stolen API endpoint URL is useless without the certificate and its key. This often sits alongside certificate pinning, where the app also hardcodes which server certificate or public key it will accept, so the trust check runs in both directions. The two are frequently confused. Pinning constrains which server the client will trust; mTLS proves to the server which client is calling. A hardened mobile API often does both.
The strongest fingerprint, and the one nobody can spoof
Now the claim in the title. Why call a client certificate the strongest fingerprint of all, when the rest of this blog is full of fingerprints that work without anyone’s cooperation?
Because every other fingerprint is an inference and this one is a proof. A JA3 or JA4 hash describes the shape of your ClientHello, and from that shape a detector infers what kind of client you probably are. The whole JA3-to-JA4 lineage is built on the premise that different stacks produce distinguishable handshakes, and it works well, but it is a statistical signal. Two clients with the same configuration share a fingerprint. A client that copies another’s ClientHello byte for byte, which is exactly what tools in the uTLS family do, inherits its fingerprint and disappears into the crowd. The signal is forgeable because it is just a description of bytes you control.
A client certificate is not a description of bytes. It is a cryptographic assertion that you hold a specific private key, validated against a transcript that changes every connection. You cannot copy someone else’s certificate and pass, because passing requires signing the live handshake with the matching private key, and you do not have it. You cannot replay a captured handshake, because the transcript hash inside the signature is unique to the original session. You cannot brute-force it in any practical sense, because that is the same as breaking the underlying signature scheme. The only way to present a valid client certificate is to actually have been issued one and to actually hold the key. That is identity, not similarity, and it sits at a different point on the spectrum from everything the behavioral and network fingerprinting world produces. If you want the contrast with signals that are pure inference, the work on behavioral biometrics cold-start and session-replay telemetry shows how much machinery goes into guessing what a certificate would simply settle.
This is also why mTLS is the cleanest answer to the bot problem in the contexts where it can be deployed. An anti-bot vendor scoring a request is trying to estimate a probability that the request is automated, and it does so from dozens of weak signals because no single strong one is available on the open web. Inside a service mesh that whole apparatus is unnecessary, because the calling workload either presents a mesh-signed certificate or it does not, and the answer is yes or no. The reason CAPTCHAs and behavioral scoring exist at all is that the public web cannot require client certificates from everyone; the moment you can, the probabilistic machinery falls away. The cryptographic agent-identity drafts now moving through the IETF, including Cloudflare’s Web Bot Auth work which signs requests with an Ed25519 key and publishes the public key at a well-known JWKS endpoint, are an attempt to bring that same yes-or-no property to bot traffic on the public internet without the full weight of PKI-issued client certificates.
There is a fingerprinting angle in the other direction too, and it is worth being precise about it. Certificates are themselves fingerprintable, even when you are not the one being authenticated by them. JA4X, part of the FoxIO JA4+ suite, fingerprints how a certificate was generated rather than what it says. It hashes the structure of the certificate, the OIDs that appear in the issuer and subject relative distinguished names and the OIDs of the X.509 extensions, in the order a particular library emits them, not the actual names or values. Two certificates issued to completely different identities by the same tooling produce the same JA4X, because the tooling lays out the fields the same way. That is useful for clustering infrastructure, for spotting that a batch of certificates all came out of the same generator, and for catching malware command-and-control that reuses a certificate-minting routine. JA4X fingerprints the factory, not the product. It says nothing about whether the holder of a certificate can prove possession of the key, which is the property that actually gates access. The certificate’s structure is a passive fingerprint; the CertificateVerify signature is an active proof; they answer different questions.
*Everything above the line is a detector estimating a probability. The bottom row is a binary cryptographic check. That gap is the whole argument.*When the strongest fingerprint fails
A proof this strong invites a particular kind of mistake: assuming the cryptography means the system around it is sound. It does not. The proof is only as good as the plumbing that handles it, and the plumbing has failed in instructive ways.
The clearest recent example is a session-resumption bug Cloudflare disclosed and fixed in January 2025, tracked as CVE-2025-23419. TLS lets a client resume a previous session with a ticket, skipping the full handshake to save a round trip. The bug was that BoringSSL stored the client certificate from the original full handshake in the cached session, and on resumption it did not revalidate that certificate. An attacker could complete a legitimate mTLS handshake against one Cloudflare zone, capture the session ticket, then present that ticket while connecting to a different zone by swapping the SNI and Host header. The second zone would resume the session and accept the connection as authenticated, even though the certificate was never valid for it. The fix Cloudflare shipped, within roughly 32 hours of disclosure, was to disable TLS session resumption entirely for customers with mTLS enabled. The certificate had done its job perfectly; the resumption cache had quietly let a valid proof from one context be reused in another. That is the recurring failure mode of strong authentication. The signature was never broken. The state management around it was.
So the honest version of the title is this. A client certificate, properly validated, with the signature checked against the live transcript on every connection and no resumption shortcut bypassing that check, is the strongest fingerprint on the web, because it is the one signal that is a proof rather than a guess. It is also a signal that only exists where one party can mandate it, which is why the open web still runs on inference and probably always will. The interesting frontier in 2026 is not making client certificates stronger; they are already as strong as the underlying signature scheme. It is dragging that yes-or-no property out of the controlled environments where mTLS thrives and into the messy public web, where Web Bot Auth and the agent-identity drafts are trying to give a crawler the same kind of unforgeable name that a workload in a service mesh has had for years. Whether the open web accepts a model where unidentified clients are second-class is the question those drafts are really asking, and the cryptography is the easy part of the answer.
Sources & further reading
- IETF (2018), RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 — the normative source for CertificateRequest (4.3.2), Certificate (4.4.2), CertificateVerify (4.4.3) with the context strings, and post-handshake auth (4.6.2).
- Cloudflare (2018), A Detailed Look at RFC 8446 (a.k.a. TLS 1.3) — engineering walk-through of what TLS 1.3 changed, including encrypting the certificate exchange.
- Cloudflare (2025), Resolving a Mutual TLS session resumption vulnerability — disclosure of CVE-2025-23419, the BoringSSL resumption flaw, and the 32-hour fix.
- SPIFFE (2024), The X.509-SVID Specification — the MUST/MUST NOT rules for SPIFFE IDs in the URI SAN, basicConstraints, and key usage.
- SPIFFE (2024), SPIFFE ID Standard — the trust-domain and URI format that names workloads.
- Istio (2026), Security concepts — how Istiod issues SPIFFE certificates, the SDS flow to Envoy, and the mTLS handshake between sidecars.
- Istio (2026), PeerAuthentication reference — PERMISSIVE vs STRICT mTLS modes and what STRICT actually requires.
- Cloudflare (2026), Mutual TLS — API Shield — edge enforcement of client certificates and the bring-your-own-CA model.
- FoxIO / John Althouse (2023), JA4+ Network Fingerprinting — the JA4+ suite, including JA4X, which fingerprints how a certificate was generated rather than its values.
- OWASP (2024), Certificate and Public Key Pinning — the pinning control and how it differs from mutual authentication.
- Cloudflare (2025), Forget IPs: using cryptography to verify bot and agent traffic — Web Bot Auth, Ed25519-signed HTTP requests, and the well-known JWKS directory.
- Teleport (2025), What is Mutual TLS (mTLS)? — a clear overview of the mutual-auth handshake and common deployment patterns.
Further reading
How Cloudflare uses TLS and HTTP/2 fingerprints in bot scoring
A reference on Cloudflare's network-layer fingerprinting: how JA3, JA4, and the HTTP/2 frame profile are computed at the edge, what cf.bot_management exposes, and how those signals feed the 1-99 bot score.
·23 min readTLS fingerprinting: from ClientHello bytes to JA4
What a ClientHello actually contains, why JA3 worked for six years and then stopped, and what JA4 fixes, with a Python reference you can run against your own packet captures.
·15 min readThe TLS ClientHello, field by field: a fingerprinting reference
A field-by-field dissection of the TLS ClientHello, tracing exactly which bytes JA3 and JA4 read: version, cipher suites, compression, extensions, supported_groups, signature_algorithms, supported_versions, key_share, and ALPN.
·19 min read