Chrome's TLS extension permutation means the browser sends the extensions in its ClientHello in a fresh random order on every connection. The extensions themselves do not change, nor do their contents; only the sequence does. So two handshakes from one Chrome, a second apart, look different when you diff them line by line and identical when you compare the extensions as a set.
We met it the plain way, on the first run of our own handshake probe, when two captures that should have matched did not.
What Chrome's TLS extension permutation moves, and what it leaves
The ClientHello carries several lists. Only one of them is shuffled.
| Part of the ClientHello | Shuffled per connection? |
|---|---|
| Extension order | Yes |
| Which extensions are present | No |
| Cipher suite order | No |
| Supported groups, signature algorithms, versions | No, the order is a preference |
ALPN (h2 before http/1.1) | No |
| GREASE values | Drawn at random each time |
pre_shared_key | Always last when present |
The last row comes from the protocol. RFC 8446, the TLS 1.3 specification, says extensions may appear in any order except pre_shared_key, which must be the last extension in the ClientHello. That one sentence is what makes the shuffle legal, and also what limits it. BoringSSL, the TLS library Chrome is built on, has a switch for exactly this, and Chrome turns it on.
GREASE is the other moving part, and it moves differently. Chrome fills a few slots in several lists with reserved values such as 0x0a0a, 0x2a2a or 0xdada, picked at random per connection. A probe that prints GREASE as raw hex will show a "difference" in a list nobody shuffled.
The fingerprint is changed inside the engine, not painted on topIn Mosaic: Its own Chromium. Tesseract Chrome: values changed in C++, not JavaScript. Free holds 3 profiles. Pro is $49 a month for any number.
Mosaic is a desktop app
Email yourself the link and open it on your computer.
How it works: Its own Chromium
Why Chrome shuffles on purpose
The reason is ossification. When one browser with a large share of traffic always sends one exact order, servers, load balancers and middleboxes start to rely on it, sometimes by accident and sometimes by fingerprinting Chrome and assuming what it will do next. Then the order can no longer change without breaking somebody, and a protocol that says "any order" in its specification has a single order in practice.
Shuffling makes the specification's promise true on the wire. Equipment that cannot cope with a new order breaks the day it meets Chrome, not years later when Chrome needs to add something. GREASE does the same job for unknown values: a server that chokes on a code point it has never seen chokes on GREASE first.
What one handshake can no longer tell you
For anyone reading handshakes, the order of extensions stopped being a trait of the client. JA3, which hashes the extensions in the order they arrive, changes on almost every Chrome connection; JA4 sorts them before hashing and holds still. Why Chrome's JA3 changes works through both with our measured values.
The more interesting consequence runs the other way. A Chrome ClientHello carries well over a dozen extensions besides GREASE. Not all of them move freely, so take a deliberately low figure: if only twelve can move, there are 479,001,600 possible orders. A real Chrome repeating its exact order on two connections is roughly a one-in-half-a-billion event.
So the order is still a signal, just not inside one handshake. A client that copies Chrome's extension list exactly and sends it in the same sequence every time has made a perfect single ClientHello and an impossible pair of them. Tools that imitate Chrome's handshake have had to add the shuffle for this reason, and a copy made before the shuffle existed stands out to anyone who compares two connections.
The two differences we found that were not there
Our engine is Chromium built from stock source, so its handshake should be Chrome's. We checked with a raw TCP listener that reads the first TLS record each browser sends and hangs up, which took about a minute to write and needed no rebuild (the listener, and how to parse the record). We pointed Tesseract Chrome and branded Google Chrome at it on the same Mac.
The first run printed two differing fields. The extension list differed, and that was the permutation. The first signature algorithm differed too, 0xdada against 0x2a2a, and that was GREASE, missed only because that one line printed raw hex while the extension list labelled GREASE. Label GREASE everywhere, compare the extensions as a sorted set and the other lists in order, and every field matched: version, cipher suites, extension set and count, supported groups, supported versions, signature algorithms and ALPN. The field-by-field method is in comparing two TLS handshakes.
We kept the shuffle as it is. It comes from the Chromium code we build, and a profile that sent a fixed order, or any order Chrome would not send, would be the only browser with that habit. That is also why our engine has no TLS fingerprint setting.




