]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/tls: improved 0-RTT QUIC API
authorDamien Neil <dneil@google.com>
Sun, 22 Oct 2023 20:31:59 +0000 (16:31 -0400)
committerDamien Neil <dneil@google.com>
Wed, 22 May 2024 17:23:54 +0000 (17:23 +0000)
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>
api/next/63691.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/tls/63691.md [new file with mode: 0644]
src/crypto/tls/handshake_client.go
src/crypto/tls/handshake_client_test.go
src/crypto/tls/handshake_client_tls13.go
src/crypto/tls/handshake_server_tls13.go
src/crypto/tls/quic.go
src/crypto/tls/quic_test.go
src/crypto/tls/ticket.go

diff --git a/api/next/63691.txt b/api/next/63691.txt
new file mode 100644 (file)
index 0000000..ba419e2
--- /dev/null
@@ -0,0 +1,8 @@
+pkg crypto/tls, const QUICResumeSession = 8 #63691
+pkg crypto/tls, const QUICResumeSession QUICEventKind #63691
+pkg crypto/tls, const QUICStoreSession = 9 #63691
+pkg crypto/tls, const QUICStoreSession QUICEventKind #63691
+pkg crypto/tls, method (*QUICConn) StoreSession(*SessionState) error #63691
+pkg crypto/tls, type QUICConfig struct, EnableStoreSessionEvent bool #63691
+pkg crypto/tls, type QUICEvent struct, SessionState *SessionState #63691
+pkg crypto/tls, type QUICSessionTicketOptions struct, Extra [][]uint8 #63691
diff --git a/doc/next/6-stdlib/99-minor/crypto/tls/63691.md b/doc/next/6-stdlib/99-minor/crypto/tls/63691.md
new file mode 100644 (file)
index 0000000..67ed04c
--- /dev/null
@@ -0,0 +1,3 @@
+The [QUICConn] type used by QUIC implementations includes new events
+reporting on the state of session resumption, and provides a way for
+the QUIC layer to add data to session tickets and session cache entries.
index 53d4f9050313f37d74360469882f9f28c0b075d7..1a1738591130e2e587d20ff5ed1e0e1257324f98 100644 (file)
@@ -366,7 +366,7 @@ func (c *Conn) loadSession(hello *clientHelloMsg) (
                        return nil, nil, nil, nil
                }
 
-               hello.sessionTicket = cs.ticket
+               hello.sessionTicket = session.ticket
                return
        }
 
@@ -394,10 +394,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
@@ -410,7 +412,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}
@@ -940,8 +942,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
 }
index eb0fe368e0bff4cef9faade3a0c1cf29171edabc..a32b48aa9eb7b2f6fd351299b16312abdd7b6bd7 100644 (file)
@@ -923,7 +923,7 @@ func testResumption(t *testing.T, version uint16) {
        }
 
        getTicket := func() []byte {
-               return clientConfig.ClientSessionCache.(*lruSessionCache).q.Front().Value.(*lruSessionCacheEntry).state.ticket
+               return clientConfig.ClientSessionCache.(*lruSessionCache).q.Front().Value.(*lruSessionCacheEntry).state.session.ticket
        }
        deleteTicket := func() {
                ticketKey := clientConfig.ClientSessionCache.(*lruSessionCache).q.Front().Value.(*lruSessionCacheEntry).sessionKey
@@ -1107,6 +1107,10 @@ func (c *serializingClientCache) Get(sessionKey string) (session *ClientSessionS
 }
 
 func (c *serializingClientCache) Put(sessionKey string, cs *ClientSessionState) {
+       if cs == nil {
+               c.ticket, c.state = nil, nil
+               return
+       }
        ticket, state, err := cs.ResumptionState()
        if err != nil {
                c.t.Error(err)
index 06f3f82742b20bd64b001f0e7f112ae17d67b199..820532b45b26e4ecb7392858467b23901e857619 100644 (file)
@@ -783,8 +783,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)
        }
index 3bc3e91f8767bf35e265d6139f183fa720b05ed4..f24c2671acd4353d2cd5682c23de38677a3f0314 100644 (file)
@@ -377,6 +377,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.
@@ -856,10 +862,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")
@@ -874,6 +880,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)
index 3518169bf729bacce06c1130b79a5181074eddd8..8e722c6a590578881e66d0d906a68c033e59ef7c 100644 (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
@@ -148,13 +181,14 @@ func QUICClient(config *QUICConfig) *QUICConn {
 //
 // The config's MinVersion must be at least TLS 1.3.
 func QUICServer(config *QUICConfig) *QUICConn {
-       return newQUICConn(Server(nil, config.TLSConfig))
+       return newQUICConn(Server(nil, config.TLSConfig), 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{
@@ -190,6 +224,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
@@ -255,6 +294,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.
@@ -272,7 +312,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.
@@ -356,6 +414,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,
index 323906a2f25102e8c7519d87b92c6238d1fb3e16..5a6f66e4deaeb4624a2ca5721ae0c99deb2b06d7 100644 (file)
@@ -5,6 +5,7 @@
 package tls
 
 import (
+       "bytes"
        "context"
        "errors"
        "reflect"
@@ -12,12 +13,15 @@ import (
 )
 
 type testQUICConn struct {
-       t           *testing.T
-       conn        *QUICConn
-       readSecret  map[QUICEncryptionLevel]suiteSecret
-       writeSecret map[QUICEncryptionLevel]suiteSecret
-       gotParams   []byte
-       complete    bool
+       t                 *testing.T
+       conn              *QUICConn
+       readSecret        map[QUICEncryptionLevel]suiteSecret
+       writeSecret       map[QUICEncryptionLevel]suiteSecret
+       ticketOpts        QUICSessionTicketOptions
+       onResumeSession   func(*SessionState)
+       gotParams         []byte
+       earlyDataRejected bool
+       complete          bool
 }
 
 func newTestQUICClient(t *testing.T, config *Config) *testQUICConn {
@@ -48,7 +52,7 @@ type suiteSecret struct {
 }
 
 func (q *testQUICConn) setReadSecret(level QUICEncryptionLevel, suite uint16, secret []byte) {
-       if _, ok := q.writeSecret[level]; !ok {
+       if _, ok := q.writeSecret[level]; !ok && level != QUICEncryptionLevelEarly {
                q.t.Errorf("SetReadSecret for level %v called before SetWriteSecret", level)
        }
        if level == QUICEncryptionLevelApplication && !q.complete {
@@ -61,7 +65,9 @@ func (q *testQUICConn) setReadSecret(level QUICEncryptionLevel, suite uint16, se
                q.readSecret = map[QUICEncryptionLevel]suiteSecret{}
        }
        switch level {
-       case QUICEncryptionLevelHandshake, QUICEncryptionLevelApplication:
+       case QUICEncryptionLevelHandshake,
+               QUICEncryptionLevelEarly,
+               QUICEncryptionLevelApplication:
                q.readSecret[level] = suiteSecret{suite, secret}
        default:
                q.t.Errorf("SetReadSecret for unexpected level %v", level)
@@ -76,7 +82,9 @@ func (q *testQUICConn) setWriteSecret(level QUICEncryptionLevel, suite uint16, s
                q.writeSecret = map[QUICEncryptionLevel]suiteSecret{}
        }
        switch level {
-       case QUICEncryptionLevelHandshake, QUICEncryptionLevelApplication:
+       case QUICEncryptionLevelHandshake,
+               QUICEncryptionLevelEarly,
+               QUICEncryptionLevelApplication:
                q.writeSecret[level] = suiteSecret{suite, secret}
        default:
                q.t.Errorf("SetWriteSecret for unexpected level %v", level)
@@ -128,11 +136,16 @@ func runTestQUICConnection(ctx context.Context, cli, srv *testQUICConn, onEvent
                case QUICHandshakeDone:
                        a.complete = true
                        if a == srv {
-                               opts := QUICSessionTicketOptions{}
-                               if err := srv.conn.SendSessionTicket(opts); err != nil {
+                               if err := srv.conn.SendSessionTicket(srv.ticketOpts); err != nil {
                                        return err
                                }
                        }
+               case QUICResumeSession:
+                       if a.onResumeSession != nil {
+                               a.onResumeSession(e.SessionState)
+                       }
+               case QUICRejectedEarlyData:
+                       a.earlyDataRejected = true
                }
                if e.Kind != QUICNoEvent {
                        idleCount = 0
@@ -487,3 +500,113 @@ func TestQUICCanceledWaitingForTransportParams(t *testing.T) {
                t.Errorf("conn.Close() = %v, want alertCloseNotify", err)
        }
 }
+
+func TestQUICEarlyData(t *testing.T) {
+       clientConfig := testConfig.Clone()
+       clientConfig.MinVersion = VersionTLS13
+       clientConfig.ClientSessionCache = NewLRUClientSessionCache(1)
+       clientConfig.ServerName = "example.go.dev"
+       clientConfig.NextProtos = []string{"h3"}
+
+       serverConfig := testConfig.Clone()
+       serverConfig.MinVersion = VersionTLS13
+       serverConfig.NextProtos = []string{"h3"}
+
+       cli := newTestQUICClient(t, clientConfig)
+       cli.conn.SetTransportParameters(nil)
+       srv := newTestQUICServer(t, serverConfig)
+       srv.conn.SetTransportParameters(nil)
+       srv.ticketOpts.EarlyData = true
+       if err := runTestQUICConnection(context.Background(), cli, srv, nil); err != nil {
+               t.Fatalf("error during first connection handshake: %v", err)
+       }
+       if cli.conn.ConnectionState().DidResume {
+               t.Errorf("first connection unexpectedly used session resumption")
+       }
+
+       cli2 := newTestQUICClient(t, clientConfig)
+       cli2.conn.SetTransportParameters(nil)
+       srv2 := newTestQUICServer(t, serverConfig)
+       srv2.conn.SetTransportParameters(nil)
+       if err := runTestQUICConnection(context.Background(), cli2, srv2, nil); err != nil {
+               t.Fatalf("error during second connection handshake: %v", err)
+       }
+       if !cli2.conn.ConnectionState().DidResume {
+               t.Errorf("second connection did not use session resumption")
+       }
+       cliSecret := cli2.writeSecret[QUICEncryptionLevelEarly]
+       if cliSecret.secret == nil {
+               t.Errorf("client did not receive early data write secret")
+       }
+       srvSecret := srv2.readSecret[QUICEncryptionLevelEarly]
+       if srvSecret.secret == nil {
+               t.Errorf("server did not receive early data read secret")
+       }
+       if cliSecret.suite != srvSecret.suite || !bytes.Equal(cliSecret.secret, srvSecret.secret) {
+               t.Errorf("client early data secret does not match server")
+       }
+}
+
+func TestQUICEarlyDataDeclined(t *testing.T) {
+       t.Run("server", func(t *testing.T) {
+               testQUICEarlyDataDeclined(t, true)
+       })
+       t.Run("client", func(t *testing.T) {
+               testQUICEarlyDataDeclined(t, false)
+       })
+}
+
+func testQUICEarlyDataDeclined(t *testing.T, server bool) {
+       clientConfig := testConfig.Clone()
+       clientConfig.MinVersion = VersionTLS13
+       clientConfig.ClientSessionCache = NewLRUClientSessionCache(1)
+       clientConfig.ServerName = "example.go.dev"
+       clientConfig.NextProtos = []string{"h3"}
+
+       serverConfig := testConfig.Clone()
+       serverConfig.MinVersion = VersionTLS13
+       serverConfig.NextProtos = []string{"h3"}
+
+       cli := newTestQUICClient(t, clientConfig)
+       cli.conn.SetTransportParameters(nil)
+       srv := newTestQUICServer(t, serverConfig)
+       srv.conn.SetTransportParameters(nil)
+       srv.ticketOpts.EarlyData = true
+       if err := runTestQUICConnection(context.Background(), cli, srv, nil); err != nil {
+               t.Fatalf("error during first connection handshake: %v", err)
+       }
+       if cli.conn.ConnectionState().DidResume {
+               t.Errorf("first connection unexpectedly used session resumption")
+       }
+
+       cli2 := newTestQUICClient(t, clientConfig)
+       cli2.conn.SetTransportParameters(nil)
+       srv2 := newTestQUICServer(t, serverConfig)
+       srv2.conn.SetTransportParameters(nil)
+       declineEarlyData := func(state *SessionState) {
+               state.EarlyData = false
+       }
+       if server {
+               srv2.onResumeSession = declineEarlyData
+       } else {
+               cli2.onResumeSession = declineEarlyData
+       }
+       if err := runTestQUICConnection(context.Background(), cli2, srv2, nil); err != nil {
+               t.Fatalf("error during second connection handshake: %v", err)
+       }
+       if !cli2.conn.ConnectionState().DidResume {
+               t.Errorf("second connection did not use session resumption")
+       }
+       _, cliEarlyData := cli2.writeSecret[QUICEncryptionLevelEarly]
+       if server {
+               if !cliEarlyData {
+                       t.Errorf("client did not receive early data write secret")
+               }
+               if !cli2.earlyDataRejected {
+                       t.Errorf("client did not receive QUICEarlyDataRejected")
+               }
+       }
+       if _, srvEarlyData := srv2.readSecret[QUICEncryptionLevelEarly]; srvEarlyData {
+               t.Errorf("server received early data read secret")
+       }
+}
index 04e1dd6685d0a4a951bdeb20b180b9da6f34825d..06aec5aa63f901aa080037ccc9fb043f10c67394 100644 (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
@@ -396,7 +397,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
 }
 
@@ -406,7 +406,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
@@ -415,7 +418,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
 }