Resume TLS sessions and drive 0-RTT in the QUIC handshake - #19
Merged
Conversation
Motivation: A QUIC client resumes a prior TLS session to abbreviate the handshake, and with 0-RTT it sends application data in its first flight, before the handshake completes (RFC 9001 § 4.6, RFC 8446 § 2.2). BoringSSL exposes the primitives — SSL_set_session, the new-session callback, SSL_set_early_data_enabled / SSL_set_quic_early_data_context, and SSL_early_data_accepted — but NIOSSLQUICHandshake surfaced none of them, so a QUIC transport built on it could neither capture a session ticket nor resume one. Modifications: The initializer takes a Resumption value (default .none). A client offers a stored session with .resume(session:) for a 1-RTT resumption, or .offerEarlyData(session:) to also send 0-RTT; a server accepts 0-RTT bound to a context with .acceptEarlyData(context:). Because 0-RTT is keyed on the resumption secret, the session rides inside the early-data case, so asking for early data without a session cannot be expressed. A client captures the tickets a server issues after the handshake: the new-session callback serializes each SSL_SESSION, drained via drainNewSessions(). sessionReused and earlyDataAccepted report the outcome, and a server's 0-RTT refusal surfaces as the new State.earlyDataRejected, where advance() resets the TLS state and drives the full handshake. The initializer's parameters are reordered so the three required ones (context, role, localTransportParameters) lead, then the optional refinements. Validation: NIOSSLQUICHandshakeTests cover a resume-and-reuse round trip (a ticket from a first handshake yields sessionReused on a second) and a corrupt-session throw; the existing call sites move to the new parameter order. Result: NIOSSLQUICHandshake can resume a TLS session and drive 0-RTT early data, the TLS half of RFC 9001 § 4.6. Only session resumption is wired into the QUIC transport for now; the 0-RTT paths are in place for the transport to adopt next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR
This was referenced Jul 1, 2026
ydnar
marked this pull request as ready for review
July 1, 2026 23:10
ydnar
pushed a commit
to alta/swift-nio-quic
that referenced
this pull request
Jul 1, 2026
alta/swift-nio-ssl#19 landed the QUIC-TLS resumption API on the fork's main, so Package.swift and Package.resolved move off the quic-session-resumption branch back to main (238005f), the reproducible pin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR
ydnar
added a commit
to alta/swift-nio-quic
that referenced
this pull request
Jul 1, 2026
* Resume a prior TLS session in the QUIC handshake Motivation: A QUIC client reconnecting to a server it has spoken to before can resume the prior TLS session instead of running a full handshake, an abbreviated exchange keyed on a pre-shared secret (RFC 8446 § 2.2, RFC 9001 § 4.6). The server issues session tickets after the handshake; the client keeps them and offers one on a later connection. The swift-nio-ssl fork now surfaces this through NIOSSLQUICHandshake (alta/swift-nio-ssl#19): a Resumption value to offer a session, drainNewSessions() to capture the tickets a server issues, and sessionReused to report whether resumption took. This stack's TLS adapter drove none of it. Modifications: TLSHandshake adopts the fork's resumption API. Its initializer takes a `resumption` value a client uses to offer a stored session; it captures the server's post-handshake tickets (takeNewSessions()); and it reports sessionReused. Adopting the fork also means its reordered initializer (the three required parameters lead) and its new earlyDataRejected state, which the adapter drives on to a full handshake — unreached until this stack offers 0-RTT. Package.swift and Package.resolved pin the fork to its `quic-session-resumption` branch pending #19's merge, after which they revert to `main`. Validation: TLSHandshakeTests.resumesACapturedSession runs a full handshake, captures the server's ticket, offers it on a second handshake over the same contexts, and asserts the second resumed (sessionReused). Result: The QUIC TLS adapter resumes a prior TLS session end to end, proving the fork's resumption API (alta/swift-nio-ssl#19) consumable before it merges. Wiring resumption into the public connection path — a client session store on QUICConfiguration, keyed by server identity like tokenStore — is the follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR * Wire session resumption through the public API and interop Motivation: The TLS adapter can resume a session, but nothing above it drives that: a real QUICConnection ran a full handshake every time, and the runner's resumption case stayed unsupported. Modifications: A client session store, mirroring the NEW_TOKEN tokenStore. QUICClientSessionStore (protocol + the built-in bounded-LRU QUICInMemoryClientSessionStore, single-use popSession per RFC 8446 § 4.2.11) hangs off QUICConfiguration.clientSessionStore, keyed by the server dialed — the SNI hostname or the address. The offer is popped on connect and threaded through makeCore into the handshake at construction, since the session must reach TLS before the ClientHello; the capture side mirrors tokenStore — the server's post-handshake ticket is drained from the core (takeNewSessionToStore) and cached in DatagramHandler.deliver (storeSessionIfReceived). The quic-interop client sets a shared in-memory store for the resumption case and dials a fresh connection per request; the case is added to supportedTestCases and the CI matrix, and the negative 127-guard that asserted it unsupported is removed. Validation: TransportSimulatorTests.sessionResumption runs a first connection to capture a ticket and a second that resumes, asserting the second's handshake was abbreviated (didResumeSession); the resumption interop leg proves it against a real peer. docs/PLAN.md and TRACEABILITY.md record it. Result: Client TLS session resumption works end to end through the public API, opt-in via QUICConfiguration.clientSessionStore, and the resumption interop case is enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR * Wire session resumption through the public API and interop Motivation: The TLS adapter can resume a session, but nothing above it drives that: a real QUICConnection ran a full handshake every time, and the runner's resumption case stayed unsupported. Modifications: A client session store, mirroring the NEW_TOKEN tokenStore. QUICSessionStore (protocol + the built-in bounded-LRU QUICInMemorySessionStore, single-use popSession per RFC 8446 § 4.2.11) hangs off QUICConfiguration.clientSessionStore, keyed by the server dialed — the SNI hostname or the address. The offer is popped on connect and threaded through makeCore into the handshake at construction, since the session must reach TLS before the ClientHello; the capture side mirrors tokenStore — the server's post-handshake ticket is drained from the core (takeNewSessionToStore) and cached in DatagramHandler.deliver (storeSessionIfReceived). The quic-interop client sets a shared in-memory store for the resumption case and dials a fresh connection per request; the case is added to supportedTestCases and the CI matrix, and the negative 127-guard that asserted it unsupported is removed. Validation: TransportSimulatorTests.sessionResumption runs a first connection to capture a ticket and a second that resumes, asserting the second's handshake was abbreviated (didResumeSession); the resumption interop leg proves it against a real peer. docs/PLAN.md and TRACEABILITY.md record it. Result: Client TLS session resumption works end to end through the public API, opt-in via QUICConfiguration.clientSessionStore, and the resumption interop case is enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR * Combine the token and session store keys into QUICServerKey QUICTokenStoreKey and QUICSessionStoreKey were identical — a server identified by its SNI hostname or its socket address. Merge them into one shared QUICServerKey, now the Key typealias of both QUICTokenStore and QUICSessionStore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR * Drop the unused public NIOCore import from the stores Combining the store keys into QUICServerKey moved the only public use of SocketAddress out of QUICTokenStore and QUICSessionStore, so their `public import NIOCore` no longer backs any public declaration — which the package's InternalImportsByDefault feature rejects under warnings-as-errors. NIOCore is unused in both now, so drop the import; QUICServerKey.swift keeps its own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR * Re-pin swift-nio-ssl to the merged fork main alta/swift-nio-ssl#19 landed the QUIC-TLS resumption API on the fork's main, so Package.swift and Package.resolved move off the quic-session-resumption branch back to main (238005f), the reproducible pin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Jul 2, 2026
ydnar
added a commit
that referenced
this pull request
Jul 2, 2026
Motivation: The QUIC-TLS API gained a Resumption enum with 0-RTT early data (.offerEarlyData / .acceptEarlyData) and the earlyDataAccepted accessor in #19, but only 1-RTT session resumption was covered by a test. A client offering early data on a resumed session, and a server accepting it against a matching quic_early_data_context, went unexercised (RFC 9001 section 4.6). Modifications: Add testOfferedEarlyDataIsAccepted: a first handshake whose server enables early data mints a ticket, and a second handshake offers 0-RTT on it (.offerEarlyData) to a server accepting the same context (.acceptEarlyData); both sides then report earlyDataAccepted. A companion earlyDataSession helper mints the early-data-capable ticket. Result: The 0-RTT accept path is covered: earlyDataAccepted is exercised true on both the offering client and the accepting server. Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR Co-authored-by: Claude <noreply@anthropic.com>
ydnar
added a commit
that referenced
this pull request
Jul 17, 2026
Motivation: A QUIC client resumes a prior TLS session to abbreviate the handshake, and with 0-RTT it sends application data in its first flight, before the handshake completes (RFC 9001 § 4.6, RFC 8446 § 2.2). BoringSSL exposes the primitives — SSL_set_session, the new-session callback, SSL_set_early_data_enabled / SSL_set_quic_early_data_context, and SSL_early_data_accepted — but NIOSSLQUICHandshake surfaced none of them, so a QUIC transport built on it could neither capture a session ticket nor resume one. Modifications: The initializer takes a Resumption value (default .none). A client offers a stored session with .resume(session:) for a 1-RTT resumption, or .offerEarlyData(session:) to also send 0-RTT; a server accepts 0-RTT bound to a context with .acceptEarlyData(context:). Because 0-RTT is keyed on the resumption secret, the session rides inside the early-data case, so asking for early data without a session cannot be expressed. A client captures the tickets a server issues after the handshake: the new-session callback serializes each SSL_SESSION, drained via drainNewSessions(). sessionReused and earlyDataAccepted report the outcome, and a server's 0-RTT refusal surfaces as the new State.earlyDataRejected, where advance() resets the TLS state and drives the full handshake. The initializer's parameters are reordered so the three required ones (context, role, localTransportParameters) lead, then the optional refinements. Validation: NIOSSLQUICHandshakeTests cover a resume-and-reuse round trip (a ticket from a first handshake yields sessionReused on a second) and a corrupt-session throw; the existing call sites move to the new parameter order. Result: NIOSSLQUICHandshake can resume a TLS session and drive 0-RTT early data, the TLS half of RFC 9001 § 4.6. Only session resumption is wired into the QUIC transport for now; the 0-RTT paths are in place for the transport to adopt next. Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR Co-authored-by: Claude <noreply@anthropic.com>
ydnar
added a commit
that referenced
this pull request
Jul 17, 2026
Motivation: The QUIC-TLS API gained a Resumption enum with 0-RTT early data (.offerEarlyData / .acceptEarlyData) and the earlyDataAccepted accessor in #19, but only 1-RTT session resumption was covered by a test. A client offering early data on a resumed session, and a server accepting it against a matching quic_early_data_context, went unexercised (RFC 9001 section 4.6). Modifications: Add testOfferedEarlyDataIsAccepted: a first handshake whose server enables early data mints a ticket, and a second handshake offers 0-RTT on it (.offerEarlyData) to a server accepting the same context (.acceptEarlyData); both sides then report earlyDataAccepted. A companion earlyDataSession helper mints the early-data-capable ticket. Result: The 0-RTT accept path is covered: earlyDataAccepted is exercised true on both the offering client and the accepting server. Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR Co-authored-by: Claude <noreply@anthropic.com>
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.
Motivation:
A QUIC client resumes a prior TLS session to abbreviate the handshake, and with 0-RTT it sends application data in its first flight, before the handshake completes (RFC 9001 § 4.6, RFC 8446 § 2.2). BoringSSL exposes the primitives —
SSL_set_session, the new-session callback,SSL_set_early_data_enabled/SSL_set_quic_early_data_context, andSSL_early_data_accepted— butNIOSSLQUICHandshakesurfaced none of them, so a QUIC transport built on it could neither capture a session ticket nor resume one.Modifications:
The initializer takes a
Resumptionvalue (default.none). A client offers a stored session with.resume(session:)for a 1-RTT resumption, or.offerEarlyData(session:)to also send 0-RTT; a server accepts 0-RTT bound to a context with.acceptEarlyData(context:). Because 0-RTT is keyed on the resumption secret, the session rides inside the early-data case, so asking for early data without a session cannot be expressed.A client captures the tickets a server issues after the handshake: the new-session callback serializes each
SSL_SESSION, drained viadrainNewSessions().sessionReusedandearlyDataAcceptedreport the outcome, and a server's 0-RTT refusal surfaces as the newState.earlyDataRejected, whereadvance()resets the TLS state and drives the full handshake.The initializer's parameters are reordered so the three required ones (
context,role,localTransportParameters) lead, then the optional refinements — the surface had grown by appending. All public types stay Swift/NIOSSL only:Resumptioncarries opaque[UInt8], and no BoringSSL type crosses the API.Validation:
NIOSSLQUICHandshakeTestscover a resume-and-reuse round trip (a ticket from a first handshake yieldssessionReusedon a second) and a corrupt-session throw; the existing call sites move to the new parameter order. The whole suite passes, and the warnings-as-errors build and strict format lint are clean.Result:
NIOSSLQUICHandshakecan resume a TLS session and drive 0-RTT early data, the TLS half of RFC 9001 § 4.6. Only session resumption is wired into the QUIC transport for now; the 0-RTT paths (.offerEarlyData/.acceptEarlyData,.earlyDataRejected,earlyDataAccepted) are in place for the transport to adopt next, so the fork is touched once for both.🤖 Generated with Claude Code
Generated by Claude Code