Files
amethyst/quic
Vitor Pamplona a38a56ea78 feat(quic): 0-RTT (early data) — picoquic + quic-go pass
Closes the matrix gap. The TLS layer now drives a full RFC 9001 §4.10
0-RTT path:

- Resumption ClientHello includes the empty `early_data` extension
  when the cached TlsResumptionState carries maxEarlyDataSize > 0
  (parsed from the prior connection's NewSessionTicket early_data
  extension).
- TlsClient.start, post-CH-transcript-snapshot: derive
  client_early_traffic_secret + surface via
  TlsSecretsListener.onEarlyDataKeysReady.
- QuicConnection.zeroRttSendProtection slot installed in the listener
  and cleared in onApplicationKeysReady (RFC 9001 §4.10 forbids 0-RTT
  use after 1-RTT keys are available).
- TlsResumptionState now also carries peerTransportParameters +
  negotiatedAlpn from the issuing connection so a resumed connection
  can pre-load flow-control limits (initial_max_data,
  initial_max_streams_bidi, etc.) BEFORE the new ServerHello arrives.
  Without this, peerMaxStreamsBidi=0 and pre-handshake stream
  creation fails. RFC 9001 §7.4.1 explicitly carves out which
  parameters MUST be remembered for 0-RTT vs which MUST NOT (CIDs,
  ack delay).
- QuicConnectionWriter.buildApplicationPacket: dual 0-RTT / 1-RTT
  path. When 1-RTT keys are absent but 0-RTT keys are present, build
  a long-header type=0x01 ZERO_RTT packet (sharing the Application
  packet number space per RFC 9000 §17.2.3) and skip ACK frames
  (server cannot ACK 0-RTT-level packets). Once 1-RTT installs, the
  writer naturally falls through to short-header.
- InteropClient runResumptionTest gains a `zerortt` flag. When set,
  iter 0 fetches NOTHING (just establishes + waits the existing
  200ms post-handshake window for the NewSessionTicket to arrive +
  closes), and iter 1 opens all URLs as bidi streams + enqueues GETs
  + driver.wakeup BEFORE awaitHandshake so the writer ships them as
  0-RTT packets coalesced with (or right after) the resumed
  ClientHello in the first datagram.

Results:
- ✓ picoquic: 0-RTT 10682 bytes, 1-RTT 238 bytes — within the
  runner's 50% / 5000-byte 1-RTT cap.
- ✓ quic-go: 0-RTT 10693 bytes, 1-RTT 1488 bytes — same.
- ✕ aioquic: server rejects our 0-RTT (only 3 STREAM frames come
  back from 40 GETs sent); no rejection-fallback wired (a real
  implementation would track which app data was sent in 0-RTT and
  replay in 1-RTT after EE comes back without early_data
  acceptance). Out of scope for this pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:03:41 -04:00
..