diff --git a/src/crypto/tls/common.go b/src/crypto/tls/common.go index 099a11ca63da75..c082ce4156fb58 100644 --- a/src/crypto/tls/common.go +++ b/src/crypto/tls/common.go @@ -978,6 +978,10 @@ func (c *Config) ticketKeyFromBytes(b [32]byte) (key ticketKey) { // ticket, and the lifetime we set for all tickets we send. const maxSessionTicketLifetime = 7 * 24 * time.Hour +// Maximum allowed mismatch between the stated age of a ticket and the server-observed one. +// See https://datatracker.ietf.org/doc/html/rfc8446#section-8.3 +const maxSessionTicketSkewAllowance = 10 * time.Second + // Clone returns a shallow clone of c or nil if c is nil. It is safe to clone a [Config] that is // being used concurrently by a TLS client or server. func (c *Config) Clone() *Config { diff --git a/src/crypto/tls/handshake_messages_test.go b/src/crypto/tls/handshake_messages_test.go index fa81a72b0de018..0d6d2fa594e815 100644 --- a/src/crypto/tls/handshake_messages_test.go +++ b/src/crypto/tls/handshake_messages_test.go @@ -421,9 +421,9 @@ func (*SessionState) Generate(rand *rand.Rand, size int) reflect.Value { s.alpnProtocol = string(randomBytes(rand.Intn(10), rand)) } if isTLS13 { + s.ageAdd = uint32(rand.Int63() & math.MaxUint32) if s.isClient { s.useBy = uint64(rand.Int63()) - s.ageAdd = uint32(rand.Int63() & math.MaxUint32) } } else { s.curveID = CurveID(rand.Intn(30000) + 1) diff --git a/src/crypto/tls/handshake_server_tls13.go b/src/crypto/tls/handshake_server_tls13.go index b066924e291686..e56aceea791a40 100644 --- a/src/crypto/tls/handshake_server_tls13.go +++ b/src/crypto/tls/handshake_server_tls13.go @@ -346,7 +346,8 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error { } createdAt := time.Unix(int64(sessionState.createdAt), 0) - if c.config.time().Sub(createdAt) > maxSessionTicketLifetime { + serverAge := c.config.time().Sub(createdAt) + if serverAge > maxSessionTicketLifetime || serverAge < -maxSessionTicketLifetime { continue } @@ -374,12 +375,6 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error { continue } - if c.quic != nil && c.quic.enableSessionEvents { - if err := c.quicResumeSession(sessionState); err != nil { - return err - } - } - hs.earlySecret = tls13.NewEarlySecret(hs.suite.hash.New, sessionState.secret) binderKey := hs.earlySecret.ResumptionBinderKey() // Clone the transcript in case a HelloRetryRequest was recorded. @@ -400,17 +395,8 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error { return errors.New("tls: invalid PSK binder") } - if c.quic != nil && hs.clientHello.earlyData && i == 0 && - sessionState.EarlyData && sessionState.cipherSuite == hs.suite.id && - sessionState.alpnProtocol == c.clientProtocol { - hs.earlyData = true - - transcript := hs.suite.hash.New() - if err := transcriptMsg(hs.clientHello, transcript); err != nil { - return err - } - earlyTrafficSecret := hs.earlySecret.ClientEarlyTrafficSecret(transcript) - if err := c.quicSetReadSecret(QUICEncryptionLevelEarly, hs.suite.id, earlyTrafficSecret); err != nil { + if c.quic != nil && c.quic.enableSessionEvents { + if err := c.quicResumeSession(sessionState); err != nil { return err } } @@ -424,6 +410,33 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error { hs.hello.selectedIdentityPresent = true hs.hello.selectedIdentity = uint16(i) hs.usingPSK = true + + if !hs.clientHello.earlyData || !sessionState.EarlyData || i != 0 { + return nil + } + + clientAge := time.Duration(identity.obfuscatedTicketAge-sessionState.ageAdd) * time.Millisecond + if ageDiff := serverAge - clientAge; ageDiff > maxSessionTicketSkewAllowance || ageDiff < -maxSessionTicketSkewAllowance { + return nil + } + + if sessionState.cipherSuite != hs.suite.id || sessionState.alpnProtocol != c.clientProtocol { + return nil + } + + hs.earlyData = true + + earlyTranscript := hs.suite.hash.New() + if err := transcriptMsg(hs.clientHello, earlyTranscript); err != nil { + return err + } + + earlyTrafficSecret := hs.earlySecret.ClientEarlyTrafficSecret(earlyTranscript) + if c.quic != nil { + if err := c.quicSetReadSecret(QUICEncryptionLevelEarly, hs.suite.id, earlyTrafficSecret); err != nil { + return err + } + } return nil } @@ -978,6 +991,15 @@ func (c *Conn) sendSessionTicket(earlyData bool, extra [][]byte) error { state.secret = psk state.EarlyData = earlyData state.Extra = extra + + // ticket_age_add is a random 32-bit value. + // See RFC 8446, section 4.6.1 + ageAdd := make([]byte, 4) + if _, err := c.config.rand().Read(ageAdd); err != nil { + return err + } + state.ageAdd = byteorder.LEUint32(ageAdd) + if c.config.WrapSession != nil { var err error m.label, err = c.config.WrapSession(c.connectionStateLocked(), state) @@ -996,15 +1018,7 @@ func (c *Conn) sendSessionTicket(earlyData bool, extra [][]byte) error { } } m.lifetime = uint32(maxSessionTicketLifetime / time.Second) - - // ticket_age_add is a random 32-bit value. See RFC 8446, section 4.6.1 - // The value is not stored anywhere; we never need to check the ticket age - // because 0-RTT is not supported. - ageAdd := make([]byte, 4) - if _, err := c.config.rand().Read(ageAdd); err != nil { - return err - } - m.ageAdd = byteorder.LEUint32(ageAdd) + m.ageAdd = state.ageAdd if earlyData { // RFC 9001, Section 4.6.1 diff --git a/src/crypto/tls/ticket.go b/src/crypto/tls/ticket.go index 11f7efde858539..a2fadbafe72110 100644 --- a/src/crypto/tls/ticket.go +++ b/src/crypto/tls/ticket.go @@ -17,6 +17,11 @@ import ( "golang.org/x/crypto/cryptobyte" ) +// TicketVersion 0x03 are reserved for older TLS version first byte. +// TicketVersion 0xff is reserved as an indication to read one more byte if +// we run out of all non-reserved version numbers. +const currentTicketVersion = 0 + // A SessionState is a resumable session. type SessionState struct { // Encoded as a SessionState (in the language of RFC 8446, Section 3). @@ -30,6 +35,7 @@ type SessionState struct { // opaque Extra<0..2^24-1>; // // struct { + // uint8 ticket_version; // uint16 version; // SessionStateType type; // uint16 cipher_suite; @@ -45,14 +51,16 @@ type SessionState struct { // case 1: opaque alpn<1..2^8-1>; // }; // select (SessionState.version) { - // case VersionTLS10..VersionTLS12: uint16 curve_id; - // case VersionTLS13: select (SessionState.type) { - // case server: Empty; - // case client: struct { - // uint64 use_by; - // uint32 age_add; + // case VersionTLS10..VersionTLS12: + // uint16 curve_id; + // case VersionTLS13: + // uint32 age_add; + // select (SessionState.type) { + // case server: Empty; + // case client: struct { + // uint64 use_by; + // }; // }; - // }; // }; // } SessionState; // @@ -110,6 +118,7 @@ type SessionState struct { // between Go versions. func (s *SessionState) Bytes() ([]byte, error) { var b cryptobyte.Builder + b.AddUint8(currentTicketVersion) b.AddUint16(s.version) if s.isClient { b.AddUint8(2) // client @@ -165,9 +174,9 @@ func (s *SessionState) Bytes() ([]byte, error) { }) } if s.version >= VersionTLS13 { + b.AddUint32(s.ageAdd) if s.isClient { addUint64(&b, s.useBy) - b.AddUint32(s.ageAdd) } } else { b.AddUint16(uint16(s.curveID)) @@ -187,6 +196,12 @@ func certificatesToBytesSlice(certs []*x509.Certificate) [][]byte { func ParseSessionState(data []byte) (*SessionState, error) { ss := &SessionState{} s := cryptobyte.String(data) + + var ticketVersion uint8 + if !s.ReadUint8(&ticketVersion) || ticketVersion != ticketVersion { + return nil, errors.New("tls: invalid session encoding") + } + var typ, extMasterSecret, earlyData uint8 var cert Certificate var extra cryptobyte.String @@ -280,8 +295,11 @@ func ParseSessionState(data []byte) (*SessionState, error) { ss.alpnProtocol = string(alpn) } if ss.version >= VersionTLS13 { + if !s.ReadUint32(&ss.ageAdd) { + return nil, errors.New("tls: invalid session encoding") + } if ss.isClient { - if !s.ReadUint64(&ss.useBy) || !s.ReadUint32(&ss.ageAdd) { + if !s.ReadUint64(&ss.useBy) { return nil, errors.New("tls: invalid session encoding") } }