go icon indicating copy to clipboard operation
go copied to clipboard

crypto/tls: improved 0-RTT QUIC APIs

Open neild opened this issue 2 years ago • 14 comments

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 avatar Oct 23 '23 15:10 neild

\cc @marten-seemann @FiloSottile

neild avatar Oct 23 '23 15:10 neild

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

neild avatar Oct 23 '23 15:10 neild

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

gopherbot avatar Oct 23 '23 15:10 gopherbot

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: https://github.com/golang/go/issues/63722
  2. It needs to set a fake net.Conn on the ClientHelloInfo. See https://github.com/golang/go/issues/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 avatar Oct 23 '23 16:10 marten-seemann

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.

marten-seemann avatar Oct 31 '23 12:10 marten-seemann

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.

neild avatar Oct 31 '23 23:10 neild

Updated the top comment to match the new QUICResumeSession.

rsc avatar Jan 18 '24 21:01 rsc

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 avatar Jan 26 '24 02:01 rsc

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

rsc avatar Jan 31 '24 18:01 rsc

Yes, the top comment is accurate.

neild avatar Jan 31 '24 18:01 neild

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

rsc avatar Feb 08 '24 23:02 rsc

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

rsc avatar Feb 14 '24 23:02 rsc

@neild Is the plan to land this in Go 1.23?

marten-seemann avatar May 16 '24 04:05 marten-seemann

@marten-seemann Yes, assuming nothing goes awry.

neild avatar May 16 '24 14:05 neild

Change https://go.dev/cl/594475 mentions this issue: crypto/tls: apply QUIC session event flag to QUICResumeSession events

gopherbot avatar Jun 24 '24 17:06 gopherbot