Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto/tls: improved 0-RTT QUIC APIs #63691

Open
neild opened this issue Oct 23, 2023 · 12 comments
Open

crypto/tls: improved 0-RTT QUIC APIs #63691

neild opened this issue Oct 23, 2023 · 12 comments

Comments

@neild
Copy link
Contributor

neild commented Oct 23, 2023

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.

@neild neild added the Proposal label Oct 23, 2023
@gopherbot gopherbot added this to the Proposal milestone Oct 23, 2023
@neild
Copy link
Contributor Author

neild commented Oct 23, 2023

\cc @marten-seemann @FiloSottile

@neild
Copy link
Contributor Author

neild commented Oct 23, 2023

https://go.dev/cl/536935 contains a draft implementation of this proposal.

@gopherbot
Copy link

Change https://go.dev/cl/536935 mentions this issue: crypto/tls: draft API for QUIC 0-RTT

@marten-seemann
Copy link
Contributor

marten-seemann commented Oct 23, 2023

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:

  1. The current API requires the MinVersion to be set to TLS 1.3. It's possible that the user's Config has a lower value, especially if it's used in a setup that also runs TLS/TCP using the same Config. We can get rid of this requirement by automatically setting the minimum version to TLS 1.3 when QUIC is in use: crypto/tls: don't require Config to set MinVersion = TLS13 when using QUIC #63722
  2. It needs to set a fake net.Conn on the ClientHelloInfo. See crypto/tls: ClientHelloInfo.Conn field is nil (or return value of RemoteAddr()) #61639 for more details, and the ugly workaround currently implemented in quic-go.
  3. To set the ServerName when dialing, if none is specified by the user. Implementation in quic-go, and the equivalent in crypto/tls.

@marten-seemann
Copy link
Contributor

Here are a few things I noticed:

  1. After receiving the QUICAttemptEarlyData event, 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 using Dial and not DialEarly, or the set of extensions enabled on the new connection might be incompatible with those negotiated on the original connection. Something like QUICConn.RejectEarlyData is needed for the client side as well.
  2. 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 QUICAttemptEarlyData more general, such that it's used for both kinds of session tickets.

@neild
Copy link
Contributor Author

neild commented Oct 31, 2023

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.

@rsc
Copy link
Contributor

rsc commented Jan 18, 2024

Updated the top comment to match the new QUICResumeSession.

@rsc
Copy link
Contributor

rsc commented Jan 26, 2024

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

@rsc
Copy link
Contributor

rsc commented Jan 31, 2024

@neild is the top comment still an accurate definition of the proposal?

@neild
Copy link
Contributor Author

neild commented Jan 31, 2024

Yes, the top comment is accurate.

@rsc
Copy link
Contributor

rsc commented Feb 8, 2024

Based on the discussion above, this proposal seems like a likely accept.
— rsc for the proposal review group

Proposal is in top comment: #63691 (comment)

@rsc
Copy link
Contributor

rsc commented Feb 14, 2024

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: #63691 (comment)

@rsc rsc changed the title proposal: crypto/tls: improved 0-RTT QUIC APIs crypto/tls: improved 0-RTT QUIC APIs Feb 14, 2024
@rsc rsc modified the milestones: Proposal, Backlog Feb 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Accepted
Development

No branches or pull requests

4 participants