fec917d27b
Closes the gap that left the runner's `resumption` testcase as the last unsupported standard test. The TLS layer now: - Derives `resumption_master_secret` (RFC 8446 §7.1) right after appending client Finished to the transcript. Cached on the key schedule so it can seed PSK derivations from any subsequent NewSessionTicket the server emits. - Parses NewSessionTicket bodies (RFC 8446 §4.6.1) when they arrive post-handshake at Application level. For each ticket: derive the per-ticket PSK via `HKDF-Expand-Label(resumption_master_secret, "resumption", ticket_nonce, 32)` and surface a self-contained TlsResumptionState (ticket bytes + PSK + cipher suite + age-add + issued-at) through a new TlsSecretsListener.onNewSessionTicket callback. QuicConnection's tlsListener forwards to a public onResumptionTicket lambda the application sets. - On a fresh TlsClient construction with a non-null `resumption` argument: seed the early secret from the cached PSK (`HKDF-Extract(IKM=PSK, salt=0)` — the Quartz Hkdf.extract signature is `(IKM, salt)` despite the misleading first-parameter name; non-PSK deriveEarly passes zeros for both so the order didn't matter and the bug only surfaced now), build the resumption ClientHello with `pre_shared_key` as the LAST extension carrying a single identity (the cached ticket) and a binder over the PartialClientHello, splice the binder bytes into the encoded message after a one-shot SHA-256 hash of bytes 0..len-35. - State machine: when ServerHello carries `pre_shared_key` with the selected_identity we offered (we only ever send identity index 0, any other value is a hard fail), latch `pskAccepted = true`. WAITING_CERTIFICATE_OR_FINISHED then accepts Finished without the Certificate/CertificateVerify pair the full-handshake path requires — the PSK itself transitively authenticates the server via the prior issuing connection. - If we offered PSK but the server didn't pick it (full-handshake fallback), hard-fail. The fallback path needs to clear the PSK-seeded early secret and re-run derivation against zeros, which is real work; the runner's resumption testcase requires server acceptance anyway, so this gate isn't load-bearing for matrix green. Production callers that care about the fallback can wire it later. InteropClient adds a `runResumptionTest` that splits the runner's URL list in half across two sequential connections — first runs a full handshake and captures the NewSessionTicket via onResumptionTicket, second runs the PSK handshake with the cached state. ✓ R against aioquic, picoquic, quic-go. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>