Skip to content

fix three client-side ECH bugs (grease 0-RTT, HRR/SH, NULL configs)#596

Merged
kazuho merged 13 commits intomasterfrom
topic/fix-grease-ech-0rtt
Apr 20, 2026
Merged

fix three client-side ECH bugs (grease 0-RTT, HRR/SH, NULL configs)#596
kazuho merged 13 commits intomasterfrom
topic/fix-grease-ech-0rtt

Conversation

@kazuho
Copy link
Copy Markdown
Member

@kazuho kazuho commented Apr 17, 2026

Summary

Three client-side ECH bugs, each with a regression test:

  1. GREASE ECH prevented 0-RTT session resumption against non-ECH servers.
  2. Per RFC 9849 §6.1.5, a HelloRetryRequest that confirmed ECH followed by a
    ServerHello that does not confirm must abort with illegal_parameter;
    picotls silently fell back to the outer ClientHello.
  3. handshake_properties.client.ech.configs = {NULL, 0} should disable ECH
    (as picotls.h documents), but the client was still sending GREASE.

Details

  • grease 0-RTT, closes Greasing ECH seems to prevent 0RTT #576: GREASE reused the real-ECH inner/outer
    PSK path, randomizing the outer PSK identity/binder and breaking resumption.
    Refactored to send a normal resumption-capable ClientHello with a dummy but
    syntactically valid ECH extension, matching RFC 9849 §6.2.1.
  • HRR/SH consistency: client_ech_select_hello demoted the post-HRR
    ACCEPTED state back to OFFERED on an SH confirmation mismatch. It now
    aborts with PTLS_ALERT_ILLEGAL_PARAMETER.
  • NULL configs: the grease branch triggered on configs.len == 0
    regardless of .base. Added the missing configs.base != NULL gate so the
    default zero-initialized handshake_properties does not force GREASE.

Plus internal cleanup: the three ECH bit fields collapsed into a single state
enum, test-harness config handling simplified, and binder computation
deduplicated.

Validation

build/default/test-openssl.t — all 17 subtests pass. Each fix has a
dedicated regression test that fails without its corresponding code change.

Closes #576.

kazuho and others added 2 commits April 17, 2026 15:50
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Comment thread t/picotls.c Outdated
Comment thread t/picotls.c Outdated
Comment thread lib/picotls.c
kazuho and others added 5 commits April 17, 2026 18:37
Instead of building a separate grease ECH extension, reuse the real ECH
machinery (inner/outer CH split) then discard the inner state and adopt
the outer. This removes the duplicate `build_grease_ech_extension`
function and ensures the PSK binder is correctly computed over the outer
CH that the server actually sees.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The transcript hash update and PSK binder computation pattern was
duplicated between the inner CH path and the grease ECH path. Extract
it into a shared helper function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Revert block-scoped random_secret back to function scope
- Remove early break in ECH EE handler; use if/else instead so the
  case follows the standard src = end pattern
- Deduplicate client_decode_ech_config_list call between grease and
  real ECH paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reuse test_resumption(0, 0) instead of repeating the three subtest
calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of a global variable to signal grease mode, use a distinct
ciphers pointer that get_test_ech_mode recognizes. This encodes the
mode into ctx->ech.client.ciphers which is already being manipulated
by the test harness.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kazuho kazuho marked this pull request as ready for review April 18, 2026 01:36
Expose the ECH configs iovec as a test-wide global (analogous to ctx /
ctx_peer) and use the library's own convention: base == NULL means no
ECH, len == 0 means grease, len > 0 means real ECH. This eliminates
the enum, sentinel cipher list, and translation function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kazuho kazuho requested a review from huitema April 18, 2026 01:58
@kazuho
Copy link
Copy Markdown
Member Author

kazuho commented Apr 18, 2026

@huitema WDYT?

@huitema
Copy link
Copy Markdown
Collaborator

huitema commented Apr 18, 2026

I need to build picoquic against the PR and redo my ECH tests to validate this. Give me a bit of time, please...

Comment thread lib/picotls.c Outdated
@kazuho kazuho changed the title [codex] fix 0-RTT resumption for GREASE ECH fix 0-RTT resumption for GREASE ECH Apr 19, 2026
Comment thread lib/picotls.c Outdated
@@ -2375,6 +2397,7 @@ static int send_client_hello(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion
}
else if (properties->client.ech.configs.base != NULL) {

This would allow three states: 
- if ech.configs is set with a non zero string, do ECH
- if ech.configs is set explicitly with a zero length string but a valid base, do grease,
- if ech.configs is left non configured, with NULL base, do not grease.

An alternative would be to add a "do grease" lag in the properties structure.

kazuho and others added 3 commits April 20, 2026 07:38
The three bit fields encoded only four reachable states. Replace them
with a single state enum (NONE/OFFERED/ACCEPTED/GREASE); GREASE is
client-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the RFC 9849 §6.1.5 case: client offers ECH, HRR confirms
acceptance, but the ServerHello's ECH confirmation is corrupted. The
client must abort with illegal_parameter rather than fall back to the
outer ClientHello.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per RFC 9849 Section 6.1.5, if HelloRetryRequest confirms ECH
acceptance, the ServerHello MUST also confirm it; otherwise the client
terminates with an illegal_parameter alert. Previously, the client
silently demoted state from ACCEPTED back to OFFERED and fell back to
the outer ClientHello.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kazuho kazuho force-pushed the topic/fix-grease-ech-0rtt branch from 344b589 to 61fd8f1 Compare April 19, 2026 22:47
Copy link
Copy Markdown
Collaborator

@huitema huitema left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure of what is happening. The comment in line 844 of t/picotls.c says that "base == NULL means no ECH, len == 0 means grease, len > 0 means real ECH", but the code in "sendClientHello" does not test for "base == NULL" on line 2403 of picotls.c. Did we miss someting, or am I reading it wrong?

kazuho and others added 2 commits April 20, 2026 08:25
Observes the ClientHello extensions seen by the server. With
handshake_properties passed and client.ech.configs left zero-initialized
({NULL, 0}), the client must not send the ECH extension, matching the
behavior documented in picotls.h.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The header documented that {NULL, 0} disables ECH, but the client-side
implementation took the grease branch whenever configs.len was zero --
including the zero-initialized {NULL, 0} default. That forced grease on
any handshake that supplied handshake_properties for unrelated reasons
(e.g., QUIC additional extensions), even though the context's ECH setup
wasn't meant to apply. Gate the grease branch on configs.base != NULL to
match the documented API, and expand the comment in picotls.h to cover
all three states explicitly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread include/picotls.h
@kazuho kazuho changed the title fix 0-RTT resumption for GREASE ECH fix three client-side ECH bugs (grease 0-RTT, HRR/SH, NULL configs) Apr 19, 2026
Copy link
Copy Markdown
Collaborator

@huitema huitema left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now! Thanks for fixing this bug.

@kazuho kazuho merged commit bfa6787 into master Apr 20, 2026
15 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.

Greasing ECH seems to prevent 0RTT

2 participants