Skip to content

Test the case where ECH is not configured, which is the one that breaks users - #62

Merged
yschimke merged 2 commits into
mainfrom
claude/okhttp-testbed-issue-5-y78q89
Aug 14, 2026
Merged

Test the case where ECH is not configured, which is the one that breaks users#62
yschimke merged 2 commits into
mainfrom
claude/okhttp-testbed-issue-5-y78q89

Conversation

@yschimke

Copy link
Copy Markdown
Owner

Closes #22 — the last open bullet on it, GREASE. Progresses #5.

A client that supports ECH sends the encrypted_client_hello extension even with no configuration for the name, so that a handshake using ECH and one not using it look alike on the wire. The failure that reaches real users is therefore not ECH breaking: it is a middlebox objecting to the extension and breaking a handshake from a client that was never trying to use ECH at all.

The public half

EchGreaseTest asks that directly, and unglamorously: an ordinary OkHttpClient — no ECH configuration, no DoH, nothing arranged — fetches from each public server that speaks ECH, and each has to serve it. It runs on every platform rather than only where ECH works, which is the point: the JVM cannot do ECH today, and that must not stop it reaching servers that can. No version gate and no Conscrypt, because it asserts nothing needing either.

Each case also requires the server to say ECH was not used:

Server The server's own words
cloudflare-ech.com/cdn-cgi/trace sni=plaintext
tls-ech.dev You are not using ECH
defo.ie/ech-check.php SSL_ECH_STATUS: not attempted

Without that the success would be ambiguous — a run where ECH had quietly started working would pass while measuring something else entirely. The marker is the server's wording rather than a header we impose, so a server that changes it fails loudly here instead of silently asserting nothing.

The local half

test-server's /tls now reports the ClientHello's extension IDs, in order — most of what a JA3 or JA4 fingerprint is computed from. The extensions' contents are parsed away by crypto/tls and stay unreported, so this still answers what OkHttp offered rather than exactly how a CDN fingerprints it; the README says so. GREASE values (RFC 8701) are named as such rather than left as sixteen mystery hex codes, and 0xfe0d is called out on its own as encryptedClientHelloOffered — "offered", because whether it was real or GREASE is deliberately invisible from the server side.

ClientHelloExtensionsTest gates on the fixture's own consistency: the list is recorded, it carries the two extensions no TLS 1.3 handshake can omit (server_name, supported_versions), and the ECH flag agrees with the list it was derived from. What OkHttp itself offered is recorded, not asserted — today's JVM offers no ECH extension at all, and pinning that would turn the feature arriving into a failed test.

Verification

  • EchGreaseTest run live: all three servers pass, on the pinned release and on 5.5.0-SNAPSHOT.
  • go test ./... green. The new Go test covers the extension naming, including the GREASE bit-pattern and a near-miss that must not match it, and asserts Go's own client offers no ECH — so the flag's false means "not offered" rather than "never looked".
  • ClientHelloExtensionsTest needs Docker, which this environment doesn't have, so CI is its first real run. Its parsing was checked against a genuine /tls body from the server running locally: 12 extensions recorded, both required ones present, flag consistent with the list.

Generated by Claude Code

claude added 2 commits August 14, 2026 21:48
…ks users

A client that supports ECH sends the `encrypted_client_hello` extension even
with no configuration for the name, so that a handshake using ECH and one not
using it look alike on the wire. The failure that reaches users is therefore not
ECH breaking: it is a middlebox objecting to the extension and breaking a
handshake from a client that was never trying to use ECH at all.

`EchGreaseTest` asks that directly, and unglamorously: an ordinary
`OkHttpClient` — no ECH configuration, no DoH, nothing arranged — fetches from
each public server that speaks ECH, and each has to serve it. It runs on every
platform rather than only where ECH works, which is the point, since the JVM
cannot do ECH today and that must not stop it reaching servers that can. No
version gate and no Conscrypt: it asserts nothing that needs either.

Each case also requires the server to say ECH was *not* used — Cloudflare's
`sni=plaintext`, DEfO's `SSL_ECH_STATUS: not attempted`, tls-ech.dev's "You are
not using ECH". Without that a success would be ambiguous, and a run where ECH
had quietly started working would pass while testing something else.

The local half is the offer itself. `test-server`'s `/tls` now reports the
ClientHello's extension IDs in order, which is most of what a JA3 or JA4
fingerprint is computed from — the extensions' contents are parsed away by
crypto/tls and stay unreported, so this still answers what OkHttp offered rather
than exactly how a CDN fingerprints it. GREASE values (RFC 8701) are named as
such rather than left as sixteen mystery hex codes, and `0xfe0d` is called out
on its own.

`ClientHelloExtensionsTest` gates on the fixture's own consistency — the list is
recorded, it carries the two extensions no TLS 1.3 handshake can omit, and the
ECH flag agrees with the list it was derived from — and records what OkHttp
offered without asserting it. Today's JVM offers no ECH extension at all, and
pinning that would turn the feature arriving into a failed test.

Verified: `EchGreaseTest` passes live against all three servers on both the
pinned release and the snapshot. The Go tests cover the extension naming and
assert Go's own client does not offer ECH, so the flag's false means "not
offered" rather than "never looked". `ClientHelloExtensionsTest` needs Docker
and runs in CI; its parsing was checked against a real `/tls` body from the
server running locally.

Progresses #5. Closes #22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTGos7kiusobSSixhsD19N
`theOfferCarriesItsExtensionList` asserted `server_name` was offered, and CI
disagreed. It was the assertion that was wrong, not OkHttp: the fixture is
reached as `localhost`, and the JDK omits SNI for a name with no dot in it, so
the extension is genuinely absent. Requiring it asserted a fact about the
container's address rather than about the client — exactly the trap the
ClientHello section of the README warns about.

`supported_versions` stands on its own: TLS 1.3 is negotiated through it rather
than through the record header, and the report says 1.3 was negotiated, so its
absence would mean the record was wrong.

Reproduced without Docker by running test-server natively and driving it with
OkHttp, which is also how the finding was confirmed rather than guessed at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTGos7kiusobSSixhsD19N
@yschimke
yschimke merged commit 4519016 into main Aug 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ECH against public servers, beyond the local fixture

2 participants