0
0
mirror of https://github.com/XTLS/REALITY.git synced 2025-08-22 14:38:35 +00:00

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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Marten Seemann <martenseemann@gmail.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
This commit is contained in:
yuhan6665 2024-08-18 22:36:58 -04:00
parent 45c15646d3
commit 46ce351564
5 changed files with 115 additions and 18 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

87
quic.go
View File

@ -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{}),
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,

View File

@ -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
}