crypto/tls: improved 0-RTT QUIC APIs
crypto/tls: improved 0-RTT QUIC APIs
This is a proposal to add additional support for 0-RTT (early data) sessions to crypto/tls. This adds additional features on top of #60107 to improve the interactions between the QUIC and TLS layers when resuming sessions with 0-RTT.
Background
QUIC connections negotiate a set of shared state, such as limits on the number of streams each peer may create and the amount of data which may be sent on each stream. When resuming a session with 0-RTT, both the QUIC client and server remember these limits. The client must abide by the remembered limits when sending 0-RTT data, and the server may reject 0-RTT if it is no longer willing to abide by the limits from the previous session.
Application protocols running on QUIC may have similar requirements. For example, HTTP/3 clients may remember stored settings such as QPACK limits.
This means that:
- Servers add additional data to early data session tickets provided to clients.
- Servers recover this data from the ticket when resuming a session.
- Servers can reject early data based on information in the session ticket.
- Clients add additional data to stored session tickets.
- Clients recover this data when attempting to resume a session.
It is mostly possible to do this using existing crypto/tls APIs, but with some limitations which I'll discuss below. The following proposal avoids those limitations and provides an approach which is more consistent with the existing QUIC support APIs.
Proposal
type QUICConfig struct { // existing fields unchanged
// EnableStoreSessionEvent may be set to true to enable the
// [QUICStoreSession] event for client connections.
// When this event is enabled, the application is responsible
// for storing sessions in the client session cache by calling
// [QUICConn.StoreSession].
EnableStoreSessionEvent bool
}
const (
QUICNoEvent QUICEventKind = iota // unchanged
// QUICResumeSession indicates that a client is attempting to resume a previous session.
// QUICEvent.SessionState is set.
//
// For client connections, this event occurs when the session ticket is selected.
// For server connections, this event occurs when receiving the client's session ticket.
//
// The application may set [QUICEvent.SessionState.EarlyData] to false before the
// next call to [QUICConn.NextEvent] to decline 0-RTT even if the session supports it.
QUICResumeSession
// QUICStoreSession indicates that the server has provided state permitting
// the client to resume the session.
// QUICEvent.SessionState is set.
// The application should use QUICConn.Store session to store the SessionState.
// The application may modify the SessionState before storing it.
// This event only occurs on client connections.
QUICStoreSession
)
type QUICEvent struct { // existing fields unchanged
// Set for QUICAttemptEarlyData, QUICResumeSession, and QUICStoreSession.
SessionState *SessionState
}
type QUICSessionTicketOptions struct { // existing fields unchanged
// Extra contains additional data to store in the session ticket.
// See the documentation for [SessionState.Extra].
Extra [][]byte
}
// RejectEarlyData rejects a client's attempt to resume a prior session.
// It must be called after NextEvent returns a QUICAttemptEarlyData event,
// and before the next call to NextEvent.
func (q *QUICConn) RejectEarlyData() error {}
// StoreSession stores a session previously received in a QUICStoreSession event
// in the ClientSessionCache.
// The application may process additional events or modify the SessionState
// before storing the session.
func (q *QUICConn) StoreSession(ss *SessionState) error {}
To summarize these changes:
- New events report on the state of session resumption.
- The QUIC layer may add data to session tickets and session cache entries.
- The QUIC layer may reject early data.
This permits a QUIC implementation to fully manage 0-RTT through the QUICConn event stream.
Motivation
The current crypto/tls API does permit a QUIC implementation to support 0-RTT. However, there are limitations to the current approach.
A server can use the Config.WrapSession and Config.UnwrapSession hooks to add and remove additional data from session tickets. A client can use a Config.ClientSessionCache wrapper to do the same for stored sessions.
In both cases, the QUIC layer needs to take steps to integrate with WrapSession, UnwrapSession, and ClientSessionCache values provided by the user. Users may use these values for offline storage of sessions, or other purposes.
Practically, this means that a QUIC implementation which accepts a tls.Config from the user will need to clone the config and wrap existing WrapSession/UnwrapSession/ClientSessionCache values. The implementation will need to clone the config for, at a minimum, each server with a unique set of transport parameters and for each client connection.
Cloned tls.Configs do not share session ticket keys. Cloning a server Config using auto-rotation will cause that server to not share keys or a rotation schedule with the original Config. For Configs using manual key rotation, calls to Config.SetSessionTicketKeys on the parent Config will not propagate to the cloned Config. There are some ways around this problem, but they aren't obvious and will have an impact on the QUIC API.
Cloning the client Config is less troublesome; I believe all relevant client state can be cloned safely.
Less importantly, but still relevant, the current API is cumbersome, difficult to use, and inconsistent. The QUIC API added in #44886 is based on an event loop and synchronous operations. Using WrapSession/UnwrapSession/ClientSessionCache for ticket management is callback-oriented and asynchronous. This adds a fair amount of mandatory complexity to the QUIC implementation.
The proposal here avoids these issues by permitting the QUIC layer to pass through a tls.Config from the application unchanged. All necessary interactions between the QUIC and TLS layers are managed through the tls.QUICConn.
\cc @marten-seemann @FiloSottile
https://go.dev/cl/536935 contains a draft implementation of this proposal.
Change https://go.dev/cl/536935 mentions this issue: crypto/tls: draft API for QUIC 0-RTT
I'll take a closer look at the API later, but I'd like to point out that cloning the Config is necessary for (at least) three more reasons. If we don't want QUIC stacks to clone Configs (which would be great!), we'll also need to resolve these:
- The current API requires the
MinVersionto be set to TLS 1.3. It's possible that the user'sConfighas a lower value, especially if it's used in a setup that also runs TLS/TCP using the sameConfig. We can get rid of this requirement by automatically setting the minimum version to TLS 1.3 when QUIC is in use: https://github.com/golang/go/issues/63722 - It needs to set a fake
net.Connon theClientHelloInfo. See https://github.com/golang/go/issues/61639 for more details, and the ugly workaround currently implemented in quic-go. - To set the
ServerNamewhen dialing, if none is specified by the user. Implementation in quic-go, and the equivalent in crypto/tls.
Here are a few things I noticed:
- After receiving the
QUICAttemptEarlyDataevent, how does a client disable 0-RTT? While the session ticket (received on a previous connection) might allow for 0-RTT, there are a number of reasons why a client might wish to disable 0-RTT. For example, it might now be dialing usingDialand notDialEarly, or the set of extensions enabled on the new connection might be incompatible with those negotiated on the original connection. Something likeQUICConn.RejectEarlyDatais needed for the client side as well. - It seems like the server won't be able to access data stored in a non-0-RTT session ticket. This is unfortunate, since there's a bunch of transport state (e.g. the latest RTT measurement) that can be stored in the session ticket, no matter if 0-RTT is used or not. We could make the
QUICAttemptEarlyDatamore general, such that it's used for both kinds of session tickets.
Does the client need to disable 0-RTT at the TLS layer? It can just choose not to send any 0-RTT packets.
That said, I think we can adjust the proposal to both simplify it and address both your points.
We change the QUICAttemptEarlyData event to:
// QUICResumeSession indicates that a client is attempting to resume a previous session.
// QUICEvent.SessionState is set.
//
// For client connections, this event occurs when the session ticket is selected.
// For server connections, this event occurs when receiving the client's session ticket.
//
// The application may set [QUICEvent.SessionState.EarlyData] to false before the
// next call to [QUICConn.NextEvent] to decline 0-RTT even if the session supports it.
QUICResumeSession
QUICResumeSession events occur for all resumed sessions, not just ones with early data. (Since QUICConn session tickets are always explicitly sent with QUICConn.SendSessionTicket, including ones without early data enabled, I think it does make sense to provide all resumption tickets here.)
We drop the QUICConn.RejectEarlyData method (less API surface, yay), and let the user modify the SessionState directly. This matches WrapSession/UnwrapSession, which also allow the user to disable early data by modifying the SessionState.
Updated https://go.dev/cl/536935 with these changes.
Updated the top comment to match the new QUICResumeSession.
This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group
@neild is the top comment still an accurate definition of the proposal?
Yes, the top comment is accurate.
Based on the discussion above, this proposal seems like a likely accept. — rsc for the proposal review group
Proposal is in top comment: https://github.com/golang/go/issues/63691#issue-1957426600
No change in consensus, so accepted. 🎉 This issue now tracks the work of implementing the proposal. — rsc for the proposal review group
Proposal is in top comment: https://github.com/golang/go/issues/63691#issue-1957426600
@neild Is the plan to land this in Go 1.23?
@marten-seemann Yes, assuming nothing goes awry.
Change https://go.dev/cl/594475 mentions this issue: crypto/tls: apply QUIC session event flag to QUICResumeSession events