HTTP/2 and header fingerprints
Why it matters Even with an identical TLS fingerprint, the very first HTTP/2 messages a browser sends, and the order of its headers, differ between browsers. Detection systems use this to check whether a client really is the browser its user agent claims. You will tell three simulated browsers apart, and catch a script pretending to be one.
Two people can wear the same uniform and still be told apart by how they fill in a form: the order they write things, which boxes they tick first. HTTP/2 openings are a form every browser fills in its own way.
HTTP/2: a newer version of HTTP that browsers use for most HTTPS sites today, alongside the even newer HTTP/3. It sends requests as small binary messages called frames over one connection, and needs ALPN h2 in the handshake.
Frame: one HTTP/2 message. A SETTINGS frame announces limits, a WINDOW_UPDATE frame says how much data the browser is ready to receive, and a HEADERS frame carries a request's headers.
What the opening reveals
- SETTINGS. A list of (id, value) pairs, such as
(1, 32768)for a table size or(4, 4194304)for a window size. Which settings appear, their values and their order are chosen by the browser's code. - WINDOW_UPDATE. One number: how much extra data the browser is willing to receive.
- Priority. How the browser says which requests matter most. Browsers do this differently.
- Pseudo-header order. Every HTTP/2 request starts with four special headers:
:method,:authority(the site),:schemeand:path. The protocol does not fix their order, so each browser picks one. - Header order. The ordinary headers, such as
user-agentandaccept, also come in a browser-specific order.
One line to compare
A common way to write this down is one text line with four parts separated by |:
1:32768;3:250;4:4194304;6:131072|10420225|0|m,s,a,p
settings (id:value;...) window prio pseudo-header initialsThe initials are the first letter after the colon: :method becomes m.
Header order and casing: hiding in plain sight
In HTTP/2, header names must be sent in lower case. In the older HTTP/1.1, the case was up to the sender, so User-Agent versus user-agent could itself tell clients apart. Order works in both. In our world, Brook and Cobalt are built on the same fictional engine: their HTTP/2 settings line is identical, and only their header order differs. So you need both to tell them apart.
User agent claim: the user-agent header is just text the client writes about itself. Any program can copy it. The handshake and HTTP/2 details are harder to fake by accident, so detectors compare them with the claim.
The practice world gives you h2_connection(name, seed) for Brook, Cobalt and Fennel, plus "Toolkit": a fictional script that copies Brook's user agent but opens HTTP/2 its own way. Each connection is a dict with settings, window_update, priority, pseudo_headers, headers (a list of (name, value) pairs) and user_agent. All values are fictional.
To join pairs into text: ";".join("%d:%d" % (k, v) for k, v in pairs). To take the name before the slash in "Brook/31.0": ua.split("/")[0].
In other languages
Building the settings part of the line elsewhere:
// JavaScript
const settings = conn.settings.map(([k, v]) => `${k}:${v}`).join(";");
const pseudo = conn.pseudo_headers.map(h => h[1]).join(",");
const line = [settings, conn.window_update, conn.priority, pseudo].join("|");// Go
parts := []string{}
for _, s := range conn.Settings {
parts = append(parts, fmt.Sprintf("%d:%d", s.ID, s.Value))
}
settings := strings.Join(parts, ";")// C#
var settings = string.Join(";", conn.Settings.Select(s => $"{s.Id}:{s.Value}"));The defence
For ordinary users, the protection is the same as for TLS: use a common, current browser, so your HTTP/2 opening matches millions of others. For site builders: if you use these signals to fight fraud, use them to add friction (such as a second check), never as proof of who a person is, and never to profile people.
Recap
- A browser's HTTP/2 opening (settings, window size, priority, pseudo-header order) is a fingerprint.
- Header order, and in HTTP/1.1 header casing, adds more.
- Two browsers on the same engine can share settings and differ only in header order.
- Detectors compare these signals with the user agent claim to spot clients that are not what they say.
Write six short functions:
h2_fingerprint(conn): the settings asid:valuejoined by;, then|, thewindow_update,|, thepriority,|, and the pseudo-header initials (the character after the colon) joined by,.header_order(conn): the header names, in order, joined by,.full_fingerprint(conn):h2_fingerprint(conn) + "|" + header_order(conn).learn(training): from a list of(name, conn)pairs, return a dict mapping eachfull_fingerprintto its name.identify(conn, known): the name inknownfor this connection's full fingerprint, or"unknown".claim_matches(conn, known):Trueifidentifygives the same name as the user agent claims (the text before/inconn["user_agent"]), otherwiseFalse.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.