Skip to content

v1.9.0 — extensions can be added, and unperformable cipher offers are refused

Choose a tag to compare

@latentharbor latentharbor released this 04 Aug 08:08
· 13 commits to main since this release

Two gaps in the fingerprint story, both of which let a caller ask for one identity and get another
without being told.

extensionOrder can only arrange extensions, never add them

The builder filters to the extensions it actually generated and sorts those. buildClientHello has
always taken extraExtensions, but nothing threaded it from the public config — its one caller was
the HelloRetryRequest cookie. tls.extraExtensions now does.

This matters because profiles.chrome presents a Chromium cipher list, group list and GREASE
placement over an extension set that is curl's. Chromium sends five this package never builds:
signed_certificate_timestamp (18), compress_certificate (27), session_ticket (35),
application_settings (17613) and encrypted_client_hello (65037). A JA3 or JA4 hash sees that.

A test reads that list straight out of the committed Chromium capture and asserts it, so the gap
cannot drift quietly. Supplied extensions are ordered like any other and reproduced on an HRR retry
— a second hello that changed its extension set would be malformed (RFC 8446 §4.1.2) and a signal in
itself.

tls.ciphers was taken verbatim

A list containing a CBC or RSA-key-exchange suite put a number on the wire that a server could
select, after which the AEAD layer had nothing to build: the failure landed mid-handshake rather
than at configuration
. Such a list is now refused up front.

Behaviour change: an explicit list containing TLS_CHACHA20_POLY1305_SHA256 with no injected
implementation is now refused rather than silently filtered. The old behaviour kept the suite off
the wire — right outcome, wrong means: a caller who wrote the three-suite Chromium list got a
two-suite hello and was never told the fingerprint was not the one they asked for. Presenting an
unrequested identity is the worst outcome available to a package like this one.

profiles.chrome is unaffected — it declares requires: ['cipher:chacha20'], so applyProfile
refuses it before connect is reached.

1255 offline tests pass.