From 46ce3515649cc39900d4f6cc9d7d39dd3199082b Mon Sep 17 00:00:00 2001 From: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:36:58 -0400 Subject: [PATCH] crypto/tls: improved 0-RTT QUIC API Add synchronous management of stored sessions to QUICConn. This adds QUICStoreSession and QUICResumeSession events, permitting a QUIC implementation to handle session resumption as part of its regular event loop processing. Fixes #63691 Change-Id: I9fe16207cc1986eac084869675bc36e227cbf3f0 Reviewed-on: https://go-review.googlesource.com/c/go/+/536935 LUCI-TryBot-Result: Go LUCI Reviewed-by: Marten Seemann Reviewed-by: Roland Shoemaker --- handshake_client.go | 13 +++--- handshake_client_tls13.go | 8 +++- handshake_server_tls13.go | 11 ++++- quic.go | 91 ++++++++++++++++++++++++++++++++++++--- ticket.go | 10 +++-- 5 files changed, 115 insertions(+), 18 deletions(-) diff --git a/handshake_client.go b/handshake_client.go index 333c884..dd562a2 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -367,7 +367,7 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( return nil, nil, nil, nil } - hello.sessionTicket = cs.ticket + hello.sessionTicket = session.ticket return } @@ -395,10 +395,12 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( return nil, nil, nil, nil } - if c.quic != nil && session.EarlyData { + if c.quic != nil { + c.quicResumeSession(session) + // For 0-RTT, the cipher suite has to match exactly, and we need to be // offering the same ALPN. - if mutualCipherSuiteTLS13(hello.cipherSuites, session.cipherSuite) != nil { + if session.EarlyData && mutualCipherSuiteTLS13(hello.cipherSuites, session.cipherSuite) != nil { for _, alpn := range hello.alpnProtocols { if alpn == session.alpnProtocol { hello.earlyData = true @@ -411,7 +413,7 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( // Set the pre_shared_key extension. See RFC 8446, Section 4.2.11.1. ticketAge := c.config.time().Sub(time.Unix(int64(session.createdAt), 0)) identity := pskIdentity{ - label: cs.ticket, + label: session.ticket, obfuscatedTicketAge: uint32(ticketAge/time.Millisecond) + session.ageAdd, } hello.pskIdentities = []pskIdentity{identity} @@ -936,8 +938,9 @@ func (hs *clientHandshakeState) saveSessionTicket() error { session := c.sessionState() session.secret = hs.masterSecret + session.ticket = hs.ticket - cs := &ClientSessionState{ticket: hs.ticket, session: session} + cs := &ClientSessionState{session: session} c.config.ClientSessionCache.Put(cacheKey, cs) return nil } diff --git a/handshake_client_tls13.go b/handshake_client_tls13.go index 3e69d59..a361019 100644 --- a/handshake_client_tls13.go +++ b/handshake_client_tls13.go @@ -784,8 +784,12 @@ func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error { session.useBy = uint64(c.config.time().Add(lifetime).Unix()) session.ageAdd = msg.ageAdd session.EarlyData = c.quic != nil && msg.maxEarlyData == 0xffffffff // RFC 9001, Section 4.6.1 - cs := &ClientSessionState{ticket: msg.label, session: session} - + session.ticket = msg.label + if c.quic != nil && c.quic.enableStoreSessionEvent { + c.quicStoreSession(session) + return nil + } + cs := &ClientSessionState{session: session} if cacheKey := c.clientSessionCacheKey(); cacheKey != "" { c.config.ClientSessionCache.Put(cacheKey, cs) } diff --git a/handshake_server_tls13.go b/handshake_server_tls13.go index 1488af9..90c55c1 100644 --- a/handshake_server_tls13.go +++ b/handshake_server_tls13.go @@ -430,6 +430,12 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error { continue } + if c.quic != nil { + if err := c.quicResumeSession(sessionState); err != nil { + return err + } + } + hs.earlySecret = hs.suite.extract(sessionState.secret, nil) binderKey := hs.suite.deriveSecret(hs.earlySecret, resumptionBinderLabel, nil) // Clone the transcript in case a HelloRetryRequest was recorded. @@ -909,10 +915,10 @@ func (hs *serverHandshakeStateTLS13) sendSessionTickets() error { if !hs.shouldSendSessionTickets() { return nil } - return c.sendSessionTicket(false) + return c.sendSessionTicket(false, nil) } -func (c *Conn) sendSessionTicket(earlyData bool) error { +func (c *Conn) sendSessionTicket(earlyData bool, extra [][]byte) error { suite := cipherSuiteTLS13ByID(c.cipherSuite) if suite == nil { return errors.New("tls: internal error: unknown cipher suite") @@ -927,6 +933,7 @@ func (c *Conn) sendSessionTicket(earlyData bool) error { state := c.sessionState() state.secret = psk state.EarlyData = earlyData + state.Extra = extra if c.config.WrapSession != nil { var err error m.label, err = c.config.WrapSession(c.connectionStateLocked(), state) diff --git a/quic.go b/quic.go index ed046bc..05859af 100644 --- a/quic.go +++ b/quic.go @@ -49,6 +49,13 @@ type QUICConn struct { // A QUICConfig configures a [QUICConn]. type QUICConfig struct { TLSConfig *Config + + // EnableStoreSessionEvent may be set to true to enable the + // [QUICStoreSession] event for client connections. + // When this event is enabled, sessions are not automatically + // stored in the client session cache. + // The application should use [QUICConn.StoreSession] to store sessions. + EnableStoreSessionEvent bool } // A QUICEventKind is a type of operation on a QUIC connection. @@ -87,10 +94,29 @@ const ( // QUICRejectedEarlyData indicates that the server rejected 0-RTT data even // if we offered it. It's returned before QUICEncryptionLevelApplication // keys are returned. + // This event only occurs on client connections. QUICRejectedEarlyData // QUICHandshakeDone indicates that the TLS handshake has completed. QUICHandshakeDone + + // 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 ) // A QUICEvent is an event occurring on a QUIC connection. @@ -109,6 +135,9 @@ type QUICEvent struct { // Set for QUICSetReadSecret and QUICSetWriteSecret. Suite uint16 + + // Set for QUICResumeSession and QUICStoreSession. + SessionState *SessionState } type quicState struct { @@ -127,12 +156,16 @@ type quicState struct { cancelc <-chan struct{} // handshake has been canceled cancel context.CancelFunc + waitingForDrain bool + // readbuf is shared between HandleData and the handshake goroutine. // HandshakeCryptoData passes ownership to the handshake goroutine by // reading from signalc, and reclaims ownership by reading from blockedc. readbuf []byte transportParams []byte // to send to the peer + + enableStoreSessionEvent bool } // QUICClient returns a new TLS client side connection using QUICTransport as the @@ -140,7 +173,7 @@ type quicState struct { // // The config's MinVersion must be at least TLS 1.3. func QUICClient(config *QUICConfig) *QUICConn { - return newQUICConn(Client(nil, config.TLSConfig)) + return newQUICConn(Client(nil, config.TLSConfig), config) } // QUICServer returns a new TLS server side connection using QUICTransport as the @@ -149,13 +182,14 @@ func QUICClient(config *QUICConfig) *QUICConn { // The config's MinVersion must be at least TLS 1.3. func QUICServer(config *QUICConfig) *QUICConn { c, _ := Server(context.Background(), nil, config.TLSConfig) - return newQUICConn(c) + return newQUICConn(c, config) } -func newQUICConn(conn *Conn) *QUICConn { +func newQUICConn(conn *Conn, config *QUICConfig) *QUICConn { conn.quic = &quicState{ - signalc: make(chan struct{}), - blockedc: make(chan struct{}), + signalc: make(chan struct{}), + blockedc: make(chan struct{}), + enableStoreSessionEvent: config.EnableStoreSessionEvent, } conn.quic.events = conn.quic.eventArr[:0] return &QUICConn{ @@ -191,6 +225,11 @@ func (q *QUICConn) NextEvent() QUICEvent { // to catch callers erroniously retaining it. qs.events[last].Data[0] = 0 } + if qs.nextEvent >= len(qs.events) && qs.waitingForDrain { + qs.waitingForDrain = false + <-qs.signalc + <-qs.blockedc + } if qs.nextEvent >= len(qs.events) { qs.events = qs.events[:0] qs.nextEvent = 0 @@ -256,6 +295,7 @@ func (q *QUICConn) HandleData(level QUICEncryptionLevel, data []byte) error { type QUICSessionTicketOptions struct { // EarlyData specifies whether the ticket may be used for 0-RTT. EarlyData bool + Extra [][]byte } // SendSessionTicket sends a session ticket to the client. @@ -273,7 +313,25 @@ func (q *QUICConn) SendSessionTicket(opts QUICSessionTicketOptions) error { return quicError(errors.New("tls: SendSessionTicket called multiple times")) } q.sessionTicketSent = true - return quicError(c.sendSessionTicket(opts.EarlyData)) + return quicError(c.sendSessionTicket(opts.EarlyData, opts.Extra)) +} + +// 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(session *SessionState) error { + c := q.conn + if !c.isClient { + return quicError(errors.New("tls: StoreSessionTicket called on the server")) + } + cacheKey := c.clientSessionCacheKey() + if cacheKey == "" { + return nil + } + cs := &ClientSessionState{session: session} + c.config.ClientSessionCache.Put(cacheKey, cs) + return nil } // ConnectionState returns basic TLS details about the connection. @@ -357,6 +415,27 @@ func (c *Conn) quicWriteCryptoData(level QUICEncryptionLevel, data []byte) { last.Data = append(last.Data, data...) } +func (c *Conn) quicResumeSession(session *SessionState) error { + c.quic.events = append(c.quic.events, QUICEvent{ + Kind: QUICResumeSession, + SessionState: session, + }) + c.quic.waitingForDrain = true + for c.quic.waitingForDrain { + if err := c.quicWaitForSignal(); err != nil { + return err + } + } + return nil +} + +func (c *Conn) quicStoreSession(session *SessionState) { + c.quic.events = append(c.quic.events, QUICEvent{ + Kind: QUICStoreSession, + SessionState: session, + }) +} + func (c *Conn) quicSetTransportParameters(params []byte) { c.quic.events = append(c.quic.events, QUICEvent{ Kind: QUICTransportParameters, diff --git a/ticket.go b/ticket.go index 2732e6d..37ac35b 100644 --- a/ticket.go +++ b/ticket.go @@ -96,6 +96,7 @@ type SessionState struct { // Client-side TLS 1.3-only fields. useBy uint64 // seconds since UNIX epoch ageAdd uint32 + ticket []byte } // Bytes encodes the session, including any private fields, so that it can be @@ -395,7 +396,6 @@ func (c *Config) decryptTicket(encrypted []byte, ticketKeys []ticketKey) []byte // ClientSessionState contains the state needed by a client to // resume a previous TLS session. type ClientSessionState struct { - ticket []byte session *SessionState } @@ -405,7 +405,10 @@ type ClientSessionState struct { // It can be called by [ClientSessionCache.Put] to serialize (with // [SessionState.Bytes]) and store the session. func (cs *ClientSessionState) ResumptionState() (ticket []byte, state *SessionState, err error) { - return cs.ticket, cs.session, nil + if cs == nil || cs.session == nil { + return nil, nil, nil + } + return cs.session.ticket, cs.session, nil } // NewResumptionState returns a state value that can be returned by @@ -414,7 +417,8 @@ func (cs *ClientSessionState) ResumptionState() (ticket []byte, state *SessionSt // state needs to be returned by [ParseSessionState], and the ticket and session // state must have been returned by [ClientSessionState.ResumptionState]. func NewResumptionState(ticket []byte, state *SessionState) (*ClientSessionState, error) { + state.ticket = ticket return &ClientSessionState{ - ticket: ticket, session: state, + session: state, }, nil }