Skip to content

crypto/tls: advanced sessions APIs #60105

Open
@FiloSottile

Description

@FiloSottile

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.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Accepted

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions