Cipher suite ordering as a fingerprint: why order is identity
Two clients can offer the exact same fifteen cipher suites and still be told apart at a glance. The set is identical. What differs is the sequence: which suite the client lists first, which it buries near the end, where the GREASE placeholder lands. That sequence is not decoration. It is a compiled-in artifact of how a specific TLS stack was built, and it is stable enough that a server can read it once, on the first packet, before a single byte of application data is exchanged, and decide with high confidence what is on the other end of the socket.
The interesting part is what happens when someone notices this and tries to hide. Reorder the list to look like Chrome and you have to reproduce Chrome’s order exactly, byte for byte, including the parts that make no cryptographic sense. Reorder it to something novel and you have invented a sequence no real browser ships, which is a fingerprint of its own. The cipher list punishes both leaving it alone and touching it. This post is about why.
What follows: where the ordered list lives in the handshake and what the RFC says it means; why the order is fixed per build rather than per connection; how JA3 turned that order into a hash and how JA4 deliberately threw the order away; what GREASE does to the list; and why the act of reordering is itself observable. The through-line is that order carries identity even when the underlying set does not.
Where the list lives, and what “order” officially means
Every TLS connection opens with a ClientHello. Inside it, near the front, is a variable-length vector the spec calls cipher_suites: a list of two-byte identifiers, each naming a combination the client is willing to use. In TLS 1.2 a suite bundles key exchange, authentication, bulk cipher, and MAC into one code point. In TLS 1.3 the suite was narrowed to just the record-protection algorithm plus a hash for HKDF, with key exchange and authentication moved out into their own negotiation. Either way, the wire format is the same shape: a length prefix, then a packed sequence of uint16 values.
The order is not arbitrary, and the RFC is explicit about what it conveys. RFC 8446, the TLS 1.3 standard, defines cipher_suites as the symmetric options supported by the client “in descending order of client preference.” First in the list means most preferred. The client is telling the server: if you support this one, use it; if not, fall down my list until you find something we share. This is a real protocol semantic, not a fingerprinting accident. The order exists so that two parties can agree on the best mutually-supported option in a single round trip.
What the RFC does not do is force the server to honor that preference. The server, per the same document, “independently selects a cipher suite” from what the client offered. A server may follow the client’s ranking, or it may impose its own, picking its top choice from the intersection regardless of where the client ranked it. So the client’s order is advisory in effect but mandatory in expression: the client must commit to some order, and that committed order travels on the wire whether or not anyone downstream respects it. That commitment is the fingerprint.
*The cipher_suites vector: a length prefix and a packed sequence of two-byte identifiers, listed in the client's stated order of preference. The server may ignore the ranking, but the client still had to commit to one.*For a deeper walk through the rest of the structure around this vector, the ClientHello field reference covers the neighbouring fields. Here the focus stays on this one list.
Why the order is fixed per build
A fingerprint is only useful if it is stable. If Chrome shuffled its cipher list on every connection, there would be nothing to recognize. It does not. The order is decided when the binary is compiled and stays put for the life of that build.
The reason is structural. Chrome’s TLS lives in BoringSSL, Google’s fork of OpenSSL, and the cipher preference list there is a static array baked into the handshake code. There is a community project, chromium-cipher-suite-customizer, that exists precisely because changing this order is not a runtime setting. To reorder Chrome’s TLS 1.3 cipher preferences you edit third_party/boringssl/src/ssl/handshake_client.cc, swap in a variant source file, and recompile the browser. That is the whole mechanism. The cipher order is not read from a config file or negotiated at startup; it is source code, and changing it means producing a different binary.
This is why a given Chrome build emits the same cipher sequence on connection one and connection ten thousand. The list is a property of the build, not the session. And because every user running Chrome 133 on a given platform got the same binary from the same compile, they all emit the same sequence. The cipher order is shared across millions of installs and constant across the lifetime of a release. That combination, identical within a population and stable over time, is exactly what a fingerprint needs.
Firefox behaves the same way for the same reason: NSS carries its own static preference list, ordered differently from BoringSSL’s, and that difference is itself diagnostic. You can frequently tell Blink from Gecko by cipher order alone before looking at anything else. Different engines prioritize ciphers differently because different teams made different judgement calls about which trade-offs matter, and those calls froze into ordered arrays. The order is a kind of accent. It is not chosen to identify the speaker, but it does.
There is one important caveat that separates ciphers from their neighbours in the handshake. Chrome randomizes the order of TLS extensions on every connection, a deliberate anti-ossification change. It does not randomize the cipher list. Per Fastly’s analysis, the permutation that began rolling out on 20 January 2023 “permutes the set of TLS extensions sent in the ClientHello,” and nothing in that work touched cipher ordering. So while extension order went from stable to random in early 2023, cipher order stayed stable. The two fields are governed by different code paths and different intentions, and that distinction matters enormously for what follows. The story of the extension change is told in full in the post on extension randomization; the short version is that it broke one half of JA3 and left the cipher half intact.
How JA3 turned order into a hash
The first widely-adopted way of capturing all this was JA3, open-sourced in 2017 by John Althouse, Jeff Atkinson, and Josh Atkins at Salesforce. JA3 reads five fields out of the ClientHello and joins them into one string: TLS version, then the cipher list, then the extension list, then the supported elliptic curves, then the EC point formats. Within each field the values are joined with hyphens; the fields themselves are joined with commas. The whole string is then hashed with MD5 to give a 32-character fingerprint.
The detail that matters for this post is how JA3 treats the cipher list. It takes the ciphers in the order they appear on the wire. No sorting, no normalization. A raw JA3 string looks like this, with the cipher block sitting second:
769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0Those numbers between the first and second comma are the cipher suites, in decimal, in exactly the sequence the client sent them. Reorder them and you get a different string, and therefore a different MD5. That sensitivity to order was the point. Two clients offering the same set in different sequences were, to JA3, different clients, because in practice they were: a Python requests build and a Chrome build that happened to share a cipher set would still rank them differently, and JA3 caught that.
JA3 does make one exception to taking the list verbatim. It strips GREASE values before hashing. We will get to why in a moment, but the design note in the original tool is worth stating plainly: JA3 “ignores these values completely to ensure that programs utilizing GREASE can still be identified with a single JA3 hash.” Without that filtering, the random GREASE element would scramble the hash on every connection and JA3 would identify nothing. So even the first ordered fingerprint had to carve out one position in the list as noise to be discarded.
*JA3 hashes the cipher list in wire order, so any reordering changes the fingerprint. JA4 sorts the list first, so the hash captures the set but not the sequence. The example values are from the JA4 specification.*How JA4 deliberately threw the order away
By early 2023 the ordered approach had a problem, though not on the cipher field. When Chrome started permuting its extension order on every connection, the extension portion of every JA3 hash became random. Same browser, same build, new JA3 on every page load. JA3 had treated extension order as signal, and Chrome had just turned that signal into noise on purpose.
The answer, released by John Althouse on 26 September 2023 under FoxIO, was JA4+, a suite of fingerprints with JA4 covering the TLS ClientHello. JA4’s central design move is the one that looks backwards at first: it sorts. Where JA3 preserved order, JA4 destroys it before hashing. The JA4 specification describes the cipher component as a truncated hash of “the 4 character hex values of the ciphers, lower case, comma delimited,” with the list “sorted in hex order” and GREASE ignored, “yet still including other non-cipher values such as SCSV (0x00FF, 0x5600) and Experimental/Reserved values (0xFE00-0xFEFF).”
So the worked example from the spec turns this wire order:
1301,1302,1303,c02b,c02f,c02c,c030,cca9,cca8,c013,c014,009c,009d,002f,0035into this sorted list before hashing:
002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9The sorted list is SHA256-hashed and truncated to twelve characters, giving the cipher component 8daaf6152771 in the canonical example fingerprint t13d1516h2_8daaf6152771_e5627efa2ab1. Sorting makes the cipher hash immune to reordering. Chrome can permute extensions all it likes, and an evader can shuffle the cipher list all they like, and the JA4 cipher hash does not move, because the input is sorted to a canonical form first.
This looks like surrender. Order carried information, and JA4 throws it out. But the trade is deliberate and the full JA4 string recovers some of what sorting drops. The leading section, JA4_a, is human-readable rather than hashed, and it encodes the cipher count directly as a two-digit number, alongside the protocol, TLS version, SNI presence, extension count, and ALPN characters. In t13d1516, the 15 is fifteen cipher suites and the 16 is sixteen extensions. So JA4 records how many ciphers there are in the clear, and hashes which ciphers they are in canonical order, while explicitly declining to record the sequence. The count survives; the order does not. The fuller breakdown of every JA4 component, including JA4S, JA4H, and the rest of the family, is in the JA4+ deep dive.
The reason this is the right call is the subject of the next two sections. Order had become a liability for the defender precisely because it had become a battleground.
GREASE: the slot in the list that is supposed to be random
Before the evasion story, one more piece of the list needs explaining, because it is the one element that is random by design. GREASE, standardized in RFC 8701, stands for Generate Random Extensions And Sustain Extensibility. Its purpose has nothing to do with fingerprinting and everything to do with keeping the protocol flexible. Implementations that reject unfamiliar values cause the protocol to ossify, and as the RFC puts it, future peers can find “that the metaphorical joint in the protocol has rusted shut and the new values cannot be deployed without interoperability failures.” GREASE prevents that by having clients advertise reserved nonsense values that servers must learn to ignore, so that real new values can be deployed later.
In the cipher list specifically, RFC 8701 says “a client MAY select one or more GREASE cipher suite values and advertise them in the cipher_suites field.” The reserved values are a sparse, regular set: {0x0a,0x0a}, {0x1a,0x1a}, {0x2a,0x2a}, and so on through {0xfa,0xfa}, sixteen values where both bytes are equal and the low nibble is a. Chrome inserts one of these at the front of its cipher list, chosen at random per connection. Firefox does not GREASE its cipher list the way Chrome does, which is one more cross-engine tell.
For fingerprinting, the GREASE slot is both a problem and a gift. It is a problem because it is the one position in an otherwise static list that genuinely changes between connections, so any fingerprint that does not account for it will see a moving target. That is why both JA3 and JA4 strip GREASE before hashing. It is a gift because the presence of a GREASE cipher, and its conventional position at the head of the list, is itself characteristic of a real Chrome-family stack. A client that GREASEs correctly is demonstrating a property that naive clients lack. The fuller treatment of how GREASE breaks naive fingerprinting, across the whole ClientHello rather than just the cipher list, is in the GREASE post. The point to carry forward is narrow: exactly one slot in the cipher list is meant to vary, the fingerprinters know it, and they handle it. Everything else in the list is expected to be still.
*The GREASE slot at the head is the single element that legitimately varies between connections. Fingerprinters discard it; everything after it is a stable signature of the build.*Why reordering to evade is itself a tell
Now the core of it. Suppose you are running an HTTP client that is not a browser, and a server is reading your cipher order and flagging you because that order does not match any real browser. The obvious move is to change your order to match Chrome’s. This is the entire premise of tools like uTLS and curl-impersonate: present a cipher list, and a wider ClientHello, that is byte-for-byte what a target browser emits. The mechanics of the Go-side approach are covered in the uTLS post. The question here is narrower and more interesting: what does the act of reordering cost you?
It costs you in three ways, and they compound.
First, exactness. To pass as Chrome 133 you must reproduce Chrome 133’s cipher order precisely, including the suites that are present for compatibility and ranked in positions that have no clean cryptographic logic. Curl-impersonate’s Chrome target, for instance, pins a specific fifteen-entry cipher string starting TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256 and continuing through the ECDHE suites in a fixed sequence down to the bare AES128-SHA, AES256-SHA at the tail. Get one position wrong and you have produced a cipher order that no shipping browser emits, which is more suspicious than an honest non-browser order, not less. The fingerprint surface is unforgiving because the target is a single exact sequence, and “close to Chrome” is a region of space that Chrome itself never occupies.
Second, consistency with the rest of the handshake. The cipher order does not travel alone. It rides alongside the extension set, the supported groups, the signature algorithms, the ALPN list, and the GREASE behaviour, and a real Chrome build emits all of these from the same BoringSSL compile. If you borrow Chrome’s cipher order but your TLS library produces NSS-style supported groups or an OpenSSL-style record layer underneath, you have a chimera: a cipher list that says Chrome and a substrate that says something else. JA4 captures the cipher hash, the extension hash, and the count fields in one string precisely so that the components can be cross-checked against each other. A Chrome cipher hash paired with a non-Chrome extension hash is a contradiction a single JA4 string can express. The second-order tells that give away exactly this kind of mismatch are catalogued in the curl-impersonate and uTLS detection post.
Third, and this is the structural trap, the move toward sorted fingerprints means reordering buys you less every year. JA4 sorts the cipher list before hashing. So if a server is fingerprinting with JA4, changing your cipher order does not change your cipher hash at all. You can shuffle to your heart’s content and the canonical-form hash is identical, which means the order you so carefully tuned is invisible to the part of JA4 that cares about which ciphers you offer. What reordering can still do under JA4 is move you between count buckets if you add or drop suites, or create an inconsistency with the unsorted fields elsewhere in the handshake. But the pure act of permuting, the thing JA3 was sensitive to, JA4 simply does not see. The evasion technique and the field it targeted are drifting past each other.
Put those together and the cipher list becomes a field that punishes every choice. Leave your order as your library ships it and you announce your library. Copy a browser’s order imperfectly and you announce a near-miss that no browser produces. Copy it perfectly but fail to match the surrounding handshake and you announce a chimera. And against a JA4-based detector, the reordering you did to escape JA3 has no effect on the cipher hash anyway, while any structural inconsistency it introduced remains visible. Order is identity coming and going.
What the order actually buys a defender in 2026
It is worth being precise about how much weight the cipher order carries today, because it has shifted. In the JA3 era, roughly 2017 through 2022, cipher order was load-bearing. It was one of the few fields that reliably separated clients, and because nothing in the ClientHello was randomized yet, the whole JA3 hash was stable and the cipher block contributed real discriminating power. A bot that did not bother to mimic browser cipher order was trivially caught.
From early 2023 the picture changed in two directions at once. Chrome’s extension permutation broke the extension half of JA3, which pushed defenders toward JA4 and its sorted, randomization-immune hashes. And the maturing of uTLS and curl-impersonate meant that sophisticated bots now reproduce browser cipher order correctly, so order alone no longer separates the careful impersonator from the real browser. The cipher list went from a primary discriminator to a consistency check. Its job now is less “which client is this” and more “do all the fields of this handshake agree with each other, and do they agree with the network and HTTP/2 layers above.” Cloudflare’s JA4-based signals, and the way vendors like DataDome fold TLS fingerprints into a broader first-request verdict, treat the cipher fingerprint as one input among many rather than a standalone verdict; see how Cloudflare uses TLS and HTTP/2 fingerprints and DataDome’s first-request signals.
So the honest summary is that the cipher list is no longer the place a modern detector catches a competent adversary. A competent adversary matches it. What the field still does, reliably, is catch the incompetent ones, and provide a cross-check that makes a competent adversary’s other mistakes legible. The order is a tripwire for the careless and a corroborating witness against the careful.
The deeper point survives all of this version churn. The cipher list demonstrates that a set and a sequence are different quantities of information, and that protocols leak the second one for free. The bytes had to be sent in some order, the order had to come from somewhere, and where it came from is a compiled artifact of a specific build with a specific history. JA3 read that order directly. JA4 read its absence, sorting it away precisely because it had become contested ground. Either way the field testifies. A list of fifteen ciphers is fifteen choices about what to support and one choice about how to rank them, and that last choice, made once at compile time and then repeated on every connection, is the one that says who you are.
Sources & further reading
- Rescorla, E. / IETF (2018), RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 — defines cipher_suites as listed in descending order of client preference and the server’s independent selection.
- Benjamin, D. / IETF (2020), RFC 8701: Applying GREASE to TLS Extensibility — specifies the reserved GREASE cipher values and the anti-ossification rationale for advertising them.
- Althouse, J., Atkinson, J., Atkins, J. / Salesforce (2017), TLS Fingerprinting with JA3 and JA3S — the original JA3 method, its field-order formula, and its handling of cipher order and GREASE.
- Althouse, J. / FoxIO (2023), JA4+ Network Fingerprinting — the JA4 suite announcement, dated 26 September 2023.
- FoxIO-LLC (2023–2026), JA4 technical details — the exact cipher-sorting, GREASE-ignoring, SHA256-truncated-to-12 computation and the JA4_a count fields.
- Fastly (2023), A first look at Chrome’s TLS ClientHello permutation in the wild — documents the 20 January 2023 extension-order randomization that broke JA3, and that it touched extension order rather than cipher order.
- nime-sha256 (2024), chromium-cipher-suite-customizer — shows the cipher preference order living in BoringSSL’s handshake_client.cc and requiring a recompile to change.
- lwthiker (2022), Impersonating Chrome, too — curl-impersonate’s exact Chrome cipher string and its reliance on BoringSSL for GREASE and the cipher order.
- Cloudflare (2024), Advancing Threat Intelligence: JA4 fingerprints and inter-request signals — how JA4 fingerprints are used as one signal among many at the edge.
- Stamus Networks (2023), JA3 fingerprints fade as browsers embrace TLS extension randomization — context on why ordered fingerprints lost discriminating power after 2023.
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