summaryrefslogtreecommitdiffstats
path: root/src/crypto/tls/ticket.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/crypto/tls/ticket.go')
-rw-r--r--src/crypto/tls/ticket.go421
1 files changed, 421 insertions, 0 deletions
diff --git a/src/crypto/tls/ticket.go b/src/crypto/tls/ticket.go
new file mode 100644
index 0000000..b71e3af
--- /dev/null
+++ b/src/crypto/tls/ticket.go
@@ -0,0 +1,421 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tls
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/subtle"
+ "crypto/x509"
+ "errors"
+ "io"
+
+ "golang.org/x/crypto/cryptobyte"
+)
+
+// A SessionState is a resumable session.
+type SessionState struct {
+ // Encoded as a SessionState (in the language of RFC 8446, Section 3).
+ //
+ // enum { server(1), client(2) } SessionStateType;
+ //
+ // opaque Certificate<1..2^24-1>;
+ //
+ // Certificate CertificateChain<0..2^24-1>;
+ //
+ // opaque Extra<0..2^24-1>;
+ //
+ // struct {
+ // uint16 version;
+ // SessionStateType type;
+ // uint16 cipher_suite;
+ // uint64 created_at;
+ // opaque secret<1..2^8-1>;
+ // Extra extra<0..2^24-1>;
+ // uint8 ext_master_secret = { 0, 1 };
+ // uint8 early_data = { 0, 1 };
+ // CertificateEntry certificate_list<0..2^24-1>;
+ // CertificateChain verified_chains<0..2^24-1>; /* excluding leaf */
+ // select (SessionState.early_data) {
+ // case 0: Empty;
+ // case 1: opaque alpn<1..2^8-1>;
+ // };
+ // select (SessionState.type) {
+ // case server: Empty;
+ // case client: struct {
+ // select (SessionState.version) {
+ // case VersionTLS10..VersionTLS12: Empty;
+ // case VersionTLS13: struct {
+ // uint64 use_by;
+ // uint32 age_add;
+ // };
+ // };
+ // };
+ // };
+ // } SessionState;
+ //
+
+ // Extra is ignored by crypto/tls, but is encoded by [SessionState.Bytes]
+ // and parsed by [ParseSessionState].
+ //
+ // This allows [Config.UnwrapSession]/[Config.WrapSession] and
+ // [ClientSessionCache] implementations to store and retrieve additional
+ // data alongside this session.
+ //
+ // To allow different layers in a protocol stack to share this field,
+ // applications must only append to it, not replace it, and must use entries
+ // that can be recognized even if out of order (for example, by starting
+ // with an id and version prefix).
+ Extra [][]byte
+
+ // EarlyData indicates whether the ticket can be used for 0-RTT in a QUIC
+ // connection. The application may set this to false if it is true to
+ // decline to offer 0-RTT even if supported.
+ EarlyData bool
+
+ version uint16
+ isClient bool
+ cipherSuite uint16
+ // createdAt is the generation time of the secret on the sever (which for
+ // TLS 1.0–1.2 might be earlier than the current session) and the time at
+ // which the ticket was received on the client.
+ createdAt uint64 // seconds since UNIX epoch
+ secret []byte // master secret for TLS 1.2, or the PSK for TLS 1.3
+ extMasterSecret bool
+ peerCertificates []*x509.Certificate
+ activeCertHandles []*activeCert
+ ocspResponse []byte
+ scts [][]byte
+ verifiedChains [][]*x509.Certificate
+ alpnProtocol string // only set if EarlyData is true
+
+ // Client-side TLS 1.3-only fields.
+ useBy uint64 // seconds since UNIX epoch
+ ageAdd uint32
+}
+
+// Bytes encodes the session, including any private fields, so that it can be
+// parsed by [ParseSessionState]. The encoding contains secret values critical
+// to the security of future and possibly past sessions.
+//
+// The specific encoding should be considered opaque and may change incompatibly
+// between Go versions.
+func (s *SessionState) Bytes() ([]byte, error) {
+ var b cryptobyte.Builder
+ b.AddUint16(s.version)
+ if s.isClient {
+ b.AddUint8(2) // client
+ } else {
+ b.AddUint8(1) // server
+ }
+ b.AddUint16(s.cipherSuite)
+ addUint64(&b, s.createdAt)
+ b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+ b.AddBytes(s.secret)
+ })
+ b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
+ for _, extra := range s.Extra {
+ b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
+ b.AddBytes(extra)
+ })
+ }
+ })
+ if s.extMasterSecret {
+ b.AddUint8(1)
+ } else {
+ b.AddUint8(0)
+ }
+ if s.EarlyData {
+ b.AddUint8(1)
+ } else {
+ b.AddUint8(0)
+ }
+ marshalCertificate(&b, Certificate{
+ Certificate: certificatesToBytesSlice(s.peerCertificates),
+ OCSPStaple: s.ocspResponse,
+ SignedCertificateTimestamps: s.scts,
+ })
+ b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
+ for _, chain := range s.verifiedChains {
+ b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
+ // We elide the first certificate because it's always the leaf.
+ if len(chain) == 0 {
+ b.SetError(errors.New("tls: internal error: empty verified chain"))
+ return
+ }
+ for _, cert := range chain[1:] {
+ b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
+ b.AddBytes(cert.Raw)
+ })
+ }
+ })
+ }
+ })
+ if s.EarlyData {
+ b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+ b.AddBytes([]byte(s.alpnProtocol))
+ })
+ }
+ if s.isClient {
+ if s.version >= VersionTLS13 {
+ addUint64(&b, s.useBy)
+ b.AddUint32(s.ageAdd)
+ }
+ }
+ return b.Bytes()
+}
+
+func certificatesToBytesSlice(certs []*x509.Certificate) [][]byte {
+ s := make([][]byte, 0, len(certs))
+ for _, c := range certs {
+ s = append(s, c.Raw)
+ }
+ return s
+}
+
+// ParseSessionState parses a [SessionState] encoded by [SessionState.Bytes].
+func ParseSessionState(data []byte) (*SessionState, error) {
+ ss := &SessionState{}
+ s := cryptobyte.String(data)
+ var typ, extMasterSecret, earlyData uint8
+ var cert Certificate
+ var extra cryptobyte.String
+ if !s.ReadUint16(&ss.version) ||
+ !s.ReadUint8(&typ) ||
+ (typ != 1 && typ != 2) ||
+ !s.ReadUint16(&ss.cipherSuite) ||
+ !readUint64(&s, &ss.createdAt) ||
+ !readUint8LengthPrefixed(&s, &ss.secret) ||
+ !s.ReadUint24LengthPrefixed(&extra) ||
+ !s.ReadUint8(&extMasterSecret) ||
+ !s.ReadUint8(&earlyData) ||
+ len(ss.secret) == 0 ||
+ !unmarshalCertificate(&s, &cert) {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ for !extra.Empty() {
+ var e []byte
+ if !readUint24LengthPrefixed(&extra, &e) {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ ss.Extra = append(ss.Extra, e)
+ }
+ switch extMasterSecret {
+ case 0:
+ ss.extMasterSecret = false
+ case 1:
+ ss.extMasterSecret = true
+ default:
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ switch earlyData {
+ case 0:
+ ss.EarlyData = false
+ case 1:
+ ss.EarlyData = true
+ default:
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ for _, cert := range cert.Certificate {
+ c, err := globalCertCache.newCert(cert)
+ if err != nil {
+ return nil, err
+ }
+ ss.activeCertHandles = append(ss.activeCertHandles, c)
+ ss.peerCertificates = append(ss.peerCertificates, c.cert)
+ }
+ ss.ocspResponse = cert.OCSPStaple
+ ss.scts = cert.SignedCertificateTimestamps
+ var chainList cryptobyte.String
+ if !s.ReadUint24LengthPrefixed(&chainList) {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ for !chainList.Empty() {
+ var certList cryptobyte.String
+ if !chainList.ReadUint24LengthPrefixed(&certList) {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ var chain []*x509.Certificate
+ if len(ss.peerCertificates) == 0 {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ chain = append(chain, ss.peerCertificates[0])
+ for !certList.Empty() {
+ var cert []byte
+ if !readUint24LengthPrefixed(&certList, &cert) {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ c, err := globalCertCache.newCert(cert)
+ if err != nil {
+ return nil, err
+ }
+ ss.activeCertHandles = append(ss.activeCertHandles, c)
+ chain = append(chain, c.cert)
+ }
+ ss.verifiedChains = append(ss.verifiedChains, chain)
+ }
+ if ss.EarlyData {
+ var alpn []byte
+ if !readUint8LengthPrefixed(&s, &alpn) {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ ss.alpnProtocol = string(alpn)
+ }
+ if isClient := typ == 2; !isClient {
+ if !s.Empty() {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ return ss, nil
+ }
+ ss.isClient = true
+ if len(ss.peerCertificates) == 0 {
+ return nil, errors.New("tls: no server certificates in client session")
+ }
+ if ss.version < VersionTLS13 {
+ if !s.Empty() {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ return ss, nil
+ }
+ if !s.ReadUint64(&ss.useBy) || !s.ReadUint32(&ss.ageAdd) || !s.Empty() {
+ return nil, errors.New("tls: invalid session encoding")
+ }
+ return ss, nil
+}
+
+// sessionState returns a partially filled-out [SessionState] with information
+// from the current connection.
+func (c *Conn) sessionState() (*SessionState, error) {
+ return &SessionState{
+ version: c.vers,
+ cipherSuite: c.cipherSuite,
+ createdAt: uint64(c.config.time().Unix()),
+ alpnProtocol: c.clientProtocol,
+ peerCertificates: c.peerCertificates,
+ activeCertHandles: c.activeCertHandles,
+ ocspResponse: c.ocspResponse,
+ scts: c.scts,
+ isClient: c.isClient,
+ extMasterSecret: c.extMasterSecret,
+ verifiedChains: c.verifiedChains,
+ }, nil
+}
+
+// EncryptTicket encrypts a ticket with the [Config]'s configured (or default)
+// session ticket keys. It can be used as a [Config.WrapSession] implementation.
+func (c *Config) EncryptTicket(cs ConnectionState, ss *SessionState) ([]byte, error) {
+ ticketKeys := c.ticketKeys(nil)
+ stateBytes, err := ss.Bytes()
+ if err != nil {
+ return nil, err
+ }
+ return c.encryptTicket(stateBytes, ticketKeys)
+}
+
+func (c *Config) encryptTicket(state []byte, ticketKeys []ticketKey) ([]byte, error) {
+ if len(ticketKeys) == 0 {
+ return nil, errors.New("tls: internal error: session ticket keys unavailable")
+ }
+
+ encrypted := make([]byte, aes.BlockSize+len(state)+sha256.Size)
+ iv := encrypted[:aes.BlockSize]
+ ciphertext := encrypted[aes.BlockSize : len(encrypted)-sha256.Size]
+ authenticated := encrypted[:len(encrypted)-sha256.Size]
+ macBytes := encrypted[len(encrypted)-sha256.Size:]
+
+ if _, err := io.ReadFull(c.rand(), iv); err != nil {
+ return nil, err
+ }
+ key := ticketKeys[0]
+ block, err := aes.NewCipher(key.aesKey[:])
+ if err != nil {
+ return nil, errors.New("tls: failed to create cipher while encrypting ticket: " + err.Error())
+ }
+ cipher.NewCTR(block, iv).XORKeyStream(ciphertext, state)
+
+ mac := hmac.New(sha256.New, key.hmacKey[:])
+ mac.Write(authenticated)
+ mac.Sum(macBytes[:0])
+
+ return encrypted, nil
+}
+
+// DecryptTicket decrypts a ticket encrypted by [Config.EncryptTicket]. It can
+// be used as a [Config.UnwrapSession] implementation.
+//
+// If the ticket can't be decrypted or parsed, DecryptTicket returns (nil, nil).
+func (c *Config) DecryptTicket(identity []byte, cs ConnectionState) (*SessionState, error) {
+ ticketKeys := c.ticketKeys(nil)
+ stateBytes := c.decryptTicket(identity, ticketKeys)
+ if stateBytes == nil {
+ return nil, nil
+ }
+ s, err := ParseSessionState(stateBytes)
+ if err != nil {
+ return nil, nil // drop unparsable tickets on the floor
+ }
+ return s, nil
+}
+
+func (c *Config) decryptTicket(encrypted []byte, ticketKeys []ticketKey) []byte {
+ if len(encrypted) < aes.BlockSize+sha256.Size {
+ return nil
+ }
+
+ iv := encrypted[:aes.BlockSize]
+ ciphertext := encrypted[aes.BlockSize : len(encrypted)-sha256.Size]
+ authenticated := encrypted[:len(encrypted)-sha256.Size]
+ macBytes := encrypted[len(encrypted)-sha256.Size:]
+
+ for _, key := range ticketKeys {
+ mac := hmac.New(sha256.New, key.hmacKey[:])
+ mac.Write(authenticated)
+ expected := mac.Sum(nil)
+
+ if subtle.ConstantTimeCompare(macBytes, expected) != 1 {
+ continue
+ }
+
+ block, err := aes.NewCipher(key.aesKey[:])
+ if err != nil {
+ return nil
+ }
+ plaintext := make([]byte, len(ciphertext))
+ cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext)
+
+ return plaintext
+ }
+
+ return nil
+}
+
+// ClientSessionState contains the state needed by a client to
+// resume a previous TLS session.
+type ClientSessionState struct {
+ ticket []byte
+ session *SessionState
+}
+
+// ResumptionState returns the session ticket sent by the server (also known as
+// the session's identity) and the state necessary to resume this session.
+//
+// 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
+}
+
+// NewResumptionState returns a state value that can be returned by
+// [ClientSessionCache.Get] to resume a previous session.
+//
+// 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) {
+ return &ClientSessionState{
+ ticket: ticket, session: state,
+ }, nil
+}