Test the case where ECH is not configured, which is the one that breaks users - #62
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #22 — the last open bullet on it, GREASE. Progresses #5.
A client that supports ECH sends the
encrypted_client_helloextension 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
EchGreaseTestasks that directly, and unglamorously: an ordinaryOkHttpClient— 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:
cloudflare-ech.com/cdn-cgi/tracesni=plaintexttls-ech.devYou are not using ECHdefo.ie/ech-check.phpSSL_ECH_STATUS: not attemptedWithout 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/tlsnow 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 bycrypto/tlsand 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, and0xfe0dis called out on its own asencryptedClientHelloOffered— "offered", because whether it was real or GREASE is deliberately invisible from the server side.ClientHelloExtensionsTestgates 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
EchGreaseTestrun live: all three servers pass, on the pinned release and on5.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'sfalsemeans "not offered" rather than "never looked".ClientHelloExtensionsTestneeds Docker, which this environment doesn't have, so CI is its first real run. Its parsing was checked against a genuine/tlsbody from the server running locally: 12 extensions recorded, both required ones present, flag consistent with the list.Generated by Claude Code