diff options
Diffstat (limited to 'src/crypto/tls/ticket.go')
-rw-r--r-- | src/crypto/tls/ticket.go | 421 |
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 +} |