Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 158 additions & 2 deletions vlib/net/quic/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1231,8 +1231,164 @@ way a closed TCP port does, so auto-racing every `https://` request
against h3 would regress the common case; real happy-eyeballs-style
fallback is deferred as a separate follow-up feature).

13. Server support — explicitly out of committed scope, but Phases 1-9 are
designed to need no rework for it (`role` field already present).
## Phase 13: Server support — IN PROGRESS (13a started)

Opened following a scoping pass mapping the completed client against what
server support needs (see tracking issue #27675 comment). Phases 1-9's core
QUIC layer is already role-parameterized (`role QuicRole` on `QuicConn`;
`is_locally_initiated`/`initial_*_limit_for_stream` already take a role
explicitly) — none of that foundation needs rework. Same one-sub-phase-per-
stacked-PR convention as Phase 12's 12a-12d.

- [x] **13a** — TLS 1.3 server handshake:
- [x] Message construction, all five pieces (`tls13_server_hello.v`,
`tls13_certificate.v`, `tls13_messages.v`): `build_server_hello`,
`build_encrypted_extensions`, `encode_certificate`,
`encode_certificate_verify` (ECDSA P-256 signing only —
`sig_scheme_ecdsa_secp256r1_sha256`; RSA-PSS signing needs a
`mbedtls_pk_sign_ext` V wrapper that doesn't exist yet, only the
verify side does), `build_finished` (verified against the real RFC
8448 §3 vector, not just round-tripped against this module's own
parser), `build_hello_retry_request`. Every function round-trips
through its ALREADY-EXISTING, independently-written parse
counterpart — a real cross-check, not tautological. Caught one
real bug this way before commit: a server's `key_share` is a bare
`KeyShareEntry` (RFC 8446 §4.2.8), not the client's list-wrapped
shape.
- [x] Server-side state machine (`tls13_server_handshake.v`):
`Tls13ServerHandshake.respond_to_client_hello` — parses+validates a
ClientHello (cipher suite, TLS 1.3, secp256r1 key_share,
ecdsa_secp256r1_sha256 in signature_algorithms, ALPN common
protocol, RFC 9000 §7.3 transport-parameter role restrictions —
added `parse_client_hello`/`decode_alpn_offer`/
`parse_key_share_extension_client`/
`parse_signature_algorithms_extension_client`/
`parse_supported_versions_from_client` to `tls13_client_hello.v`
for this, none of which existed before), does real ECDH against
the client's offered key_share, then builds the ENTIRE response
flight (ServerHello through this server's own Finished) in one
call — RFC 8446 §7.1/Figure 3 lets application traffic secrets
derive right after the server's own Finished, no dependency on the
client's Finished arriving. `process_finished` verifies the
client's Finished and confirms the handshake. Simpler state
machine than the client's (2 states, not 6): a server never waits
on a peer message between ClientHello and its own Finished.
HelloRetryRequest is NOT wired in (group mismatch is a hard
failure) — the SAME deliberate-defer scope choice
`Tls13ClientHandshake.process_server_hello` already made for its
own first-HRR gap; `build_hello_retry_request` exists and is
unit-tested at the message layer but not yet driven by this state
machine. Verified via a REAL client-vs-server integration test
(`tls13_server_handshake_test.v`): fresh ECDHE keys on both
sides (not fixed RFC vectors), the client's own real
`process_server_hello`/`process_encrypted_extensions`/
`verify_finished` all independently accept this server's real
output, and this server's `process_finished` accepts a real client
Finished built the same way. Certificate/CertificateVerify chain
verification is NOT exercised end-to-end (no EC certificate
fixture in this repo — the same documented gap as
`encode_certificate_verify`'s own tests).
- [x] **13b** — Retry + address validation:
- [x] `encode_retry_packet` (`retry.v`) — builds a complete Retry packet,
reusing `compute_retry_integrity_tag` directly (already
side-agnostic). Round-trips through the already-existing,
independently-written client-role `verify_retry_integrity_tag`/
`parse_retry_packet` — the strongest cross-check available: not
just "well-formed," but "the exact code that will receive this in
production accepts it."
- [x] `generate_retry_token`/`validate_retry_token`/
`validate_retry_token_for_attempt` (`retry_token.v`, new file) —
AEAD-sealed (AES-128-GCM), authenticated address-validation tokens
satisfying RFC 9000 §8.1.4's difficult-to-guess and integrity
requirements via the AEAD tag itself. NEW_TOKEN-frame issuance
(§8.1.3, tokens reusable across future connections) is explicitly
out of scope — v1 only issues tokens via Retry. Single-use replay
tracking beyond a short expiry window is deferred to 13d, once a
real listening socket exists to own a consumed-token cache's
lifetime; a short `max_age_ms` window satisfies §8.1.4's "prevented
OR limited" replay requirement in the interim.
- [x] `AntiAmplificationLimiter` (`anti_amplification.v`, new file) —
RFC 9000 §8.1's 3x pre-validation send limit, mirroring
`flow_control.v`'s `FlowControlWindow` shape deliberately. A
standalone, tested accounting primitive — not yet wired into any
connection/datagram-processing loop, since that loop doesn't exist
until 13d.
- **Found and flagged, not fixed here (out of scope for this PR)**: while
picking a CSPRNG for the token nonce, discovered `conn.v`'s `dial()`
uses V's general-purpose `rand` module (wyrand-backed, NOT
cryptographically secure) for `original_dcid`/`scid`/`client_random` —
all three are security-relevant values that should use `crypto.rand`
instead (same API, OS-backed, already used elsewhere in this codebase).
This is a real gap in already-merged code (Phase 9, PR #28129), not
Phase 13 work — flagged as a separate follow-up task, not fixed inline.
- [x] **13c** — Connection ID lifecycle:
- [x] `NewConnectionIdFrame`/`RetireConnectionIdFrame` wire codec
(`frame.v`) — `encode_new_connection_id_frame`/
`parse_new_connection_id_frame` and their RETIRE_CONNECTION_ID
counterparts, types 0x18/0x19, previously falling through
`parse_frame`'s generic "not yet implemented" branch. Enforces the
two frame-local RFC 9000 §19.15 requirements (`retire_prior_to` ≤
`sequence_number`; connection ID length in 1-20 bytes), on both the
encode and decode sides so a caller can't construct a frame this
module's own parser would then reject. Every OTHER §19.15/§19.16
requirement (zero-length-DCID prohibition, duplicate/conflicting
sequence numbers, a RETIRE_CONNECTION_ID referencing the current
packet's own DCID) needs connection state `parse_frame` doesn't
have — deferred to the caller, the same division already
established for `HandshakeDoneFrame`'s role check.
- [x] `generate_stateless_reset_token` (`stateless_reset.v`) — RFC 9000
§10.3.2's recommended construction, `HMAC-SHA-256(static_key,
connection_id)` truncated to 16 bytes: a server-instance-local
secret plus the connection ID deterministically reproduces the
SAME token, so an endpoint that has lost all per-connection state
(the entire premise of a stateless reset) can still recompute it.
Cross-checked against `StatelessResetTracker.is_stateless_reset`
(already-existing, independently-written matching logic) — proving
a token this function generates is actually recognized by the
exact code that would validate it in production.
- **Deliberately still out of scope** (per `stateless_reset.v`'s own
long-standing note, unchanged by 13c): driving an ACTIVE SET of usable
connection IDs — issuing more as the peer retires them,
`active_connection_id_limit` accounting, `CONNECTION_ID_LIMIT_ERROR`
enforcement. That full lifecycle exists to support connection
migration, which PROGRESS.md already lists as a separate, explicitly
deferrable follow-up below — 13c ships the wire codec and the token
primitive it depends on, not the state machine that would consume them.
- [x] **13d-1** — Server-role handshake wiring: `QuicConn` gained real
`.server`-role support (role-aware directional key selection, a
role-branched handshake dispatch, RFC 9001 §4.1.2's role-asymmetric
handshake-confirmation semantics, server-side HANDSHAKE_DONE sending)
and a new `accept()` constructor (`accept.v`) mirroring `dial()`.
Found + fixed two RFC-conformance bugs via adversarial review before
commit: the RFC 9000 §7.2 bootstrap-DCID exception for a server's
first-received ClientHello, and RFC 9001 §4.9.1's send-vs-receive
Initial-key-discard trigger asymmetry between roles (a naive
client-shaped trigger applied to both roles discarded the server's
Initial keys before the client had sent anything back, stalling the
handshake on ordinary first-round-trip packet loss). Also closed a
missing RFC 9000 §14.1 anti-amplification floor check in `accept()`.
`accept()` deliberately does NOT decide Retry-vs-direct-accept policy
(needs cross-connection-attempt state only 13d-2's listener has) and
does not fragment a large certificate chain's Handshake CRYPTO flight
across multiple packets (both documented scope limits, not blockers).
- [ ] **13d-2** — UDP listener + connection demux: one socket routing many
concurrent connections by connection ID (not 4-tuple, since QUIC
supports migration) — no analog in the client's transport today; the
new-connection acceptance path (unrecognized DCID → Retry-or-accept,
wiring 13b's `AntiAmplificationLimiter`/Retry machinery before calling
13d-1's `accept()`).
- [ ] **13e** — `h3_server.v` wiring, mirroring `h2_server.v`'s established
shape (minimal/serial first, concurrency as an explicit follow-up),
plus server certificate/key loading.
- [ ] *(optional, deferrable)* Connection migration (`PATH_CHALLENGE`/
`PATH_RESPONSE`) — both unimplemented today; a minimal v1 server can
ship without full migration support, same as the client structurally
deferred it.

Still out of scope regardless: 0-RTT (Phase 14, separate); server push stays
permanently disabled (RFC 9114 §7.2.7, a Phase 12 decision independent of
server support existing at all).

14. 0-RTT — explicitly out of committed scope.

## Scope decisions in effect (see tracking issue for rationale)
Expand Down
Loading
Loading