Repository: go
Issue: crypto/tls: advanced sessions APIs
Labels: Proposal,Proposal-Accepted,Proposal-Crypto

Currently, we support TLS 1.2 and TLS 1.3 session resumption with very little flexibility: applications can only store sessions in memory on the client side, and can only provide session ticket encryption keys on the server side.

This has a few limitations: we don't support external PSKs, we don't support session IDs on the server side, we don't support storing sessions offline on the client side. Moreover, complex applications like a QUIC stack need extra control over session storage to implement features like 0-RTT.

- #25351
- #25228
- #19199
- #6379
- #46718
- #57753

We propose an advanced sessions API that addresses all of the above.

```
// A SessionState is a resumable session.
type SessionState struct {
    // Secret is the secret necessary for resumption.
    Secret []byte

    // Version is the TLS version with which this session can be used.
    Version uint16

    // CipherSuite needs to be compatible with the cipher suite used
    // to resume this session, meaning its hash has to match.
    CipherSuite uint16

    // Extra is ignored by crypto/tls, but is encoded by [SessionState.Bytes]
    // and parsed by [ParseSessionState].
    //
    // This allows [Config.UnwrapSession]/[Config.WrapSession] and
    // [ClientSessionCache] wrappers to store and retrieve additional data.
    Extra map[string][]byte

    // Unexported fields: certificates, timestamps, and other crypto/tls private
    // information that is not necessary for external PSKs.
}

// Bytes encodes the session, including any private fields, so that it can be
// parsed by [ParseSessionState]. The encoding contains secret values.
//
// The specific encoding should be considered opaque and may change incompatibly
// between Go versions.
func (*SessionState) Bytes() ([]byte, error)

// ParseSessionState parses a [SessionState] encoded by [SessionState.Bytes].
func ParseSessionState([]byte) (*SessionState, error)

type Config struct {
    // UnwrapSession is called on the server to turn a ticket/identity into a
    // usable session. If used with [Config.WrapSession], it involves decrypting
    // the ticket or using it as a handle to recover a serialized
    // [SessionState], and calling [ParseSessionState]. Alternatively, cs can
    // represent an external PSK.
    //
    // TODO: how does the application specify that the PSK must be used, and
    // what do we do if it's incompatible?
    UnwrapSession func(identity []byte, cs *ConnectionState) (*SessionState, error)

    // WrapSession is called on the server to produce a session ticket. It
    // usually involves calling SessionState.Bytes and encrypting its return
    // value, or storing it and returning a handle for it. Alternatively, the
    // application may choose to store additional data in [SessionState.Extra]
    // and call [Config.WrapSession] to use the default encryption method.
    //
    // Warning: this is a dangerous API. The application is in charge of
    // encrypting tickets and rotating keys, and failing to do so correctly can
    // compromise current, previous, and future connections depending on the
    // protocol version.
    WrapSession func(*ConnectionState, *SessionState) ([]byte, error)

    ClientSessionCache ClientSessionCache // unchanged
}

// WrapSession encodes and encrypts a session ticket with the default or
// configured session ticket encryption key.
func (*Config) func WrapSession(*ConnectionState, *SessionState) ([]byte, error)

// UnwrapSession decrypts a ticket encrypted by [Config.WrapSession].
func (*Config) func UnwrapSession(idenity []byte, cs *ConnectionState) (*SessionState, error)

type ClientSessionCache interface { // unchanged
    // TODO: should the sessionKey include the ALPN? What about the suite?
    Get(sessionKey string) (session *ClientSessionState, ok bool)
    Put(sessionKey string, cs *ClientSessionState)
}

type ClientSessionState struct { // unchanged
    // No unexported fields.
}

// ResumptionSession returns the session ticket sent by the server and the state
// necessary to resume this session. It does not return any sessions added with
// AddSession. It's meant to be called by [ClientSessionCache.Put] to serialize
// (with [SessionState.Bytes]) and store the session.
func (*ClientSessionState) ResumptionSession() (identity []byte, state *SessionState, err error)

// AddSession adds a resumable session. This can be a session previously
// retrieved via [ClientSessionState.ResumptionSession], or an external PSK.
//
// TODO: again, how does the application express a requirement for the PSK to be
// used, for example because it provides authentication?
func (*ClientSessionState) AddSession(identity []byte, state *SessionState) error
```

Although we don't implement the spec itself, this API enables an implementation of [RFC 9258 PSK importers](https://www.rfc-editor.org/rfc/rfc9258.html) either externally or in the future in the standard library. On the client side `ClientSessionState.AddSession` is called for each ipskx (one for each KDF supported by the supported cipher suites), and on the server side the identity is parsed in `Config.UnwrapSession` and the correct ipskx is derived and returned. We'll need to add a `Imported` bool to `SessionState` to request the use of the `imp binder` derivation path. (Should we add that already even if we don't add RFC 9258 helpers yet? Should we add RFC 9258 helpers?)



This was designed with @marten-seemann, although note that I changed `ClientSessionState`: instead of exposing a single session as fields, I added methods so that multiple PSKs can be provisioned for use with different hashes.

/cc @golang/security @golang/proposal-review @davidben (if he has opinions)