Skip to content

Commit 43f2f50

Browse files
committed
crypto/tls: rotate session ticket keys
Automatically rotate session ticket keys for servers that don't already have sessionTicketKeys and that haven't called SetSessionTicketKeys. Now, session ticket keys will be rotated every 24 hours with a lifetime of 7 days. This adds a small performance cost to existing clients that don't provide a session ticket encrypted with a fresh enough session ticket key, which would require a full handshake. Updates #25256 Change-Id: I15b46af7a82aab9a108bceb706bbf66243a1510f Reviewed-on: https://go-review.googlesource.com/c/go/+/230679 Run-TryBot: Katie Hockman <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]>
1 parent f0cea84 commit 43f2f50

File tree

6 files changed

+172
-91
lines changed

6 files changed

+172
-91
lines changed

src/crypto/tls/common.go

+135-68
Original file line numberDiff line numberDiff line change
@@ -501,15 +501,10 @@ type Config struct {
501501
// If GetConfigForClient is nil, the Config passed to Server() will be
502502
// used for all connections.
503503
//
504-
// Uniquely for the fields in the returned Config, session ticket keys
505-
// will be duplicated from the original Config if not set.
506-
// Specifically, if SetSessionTicketKeys was called on the original
507-
// config but not on the returned config then the ticket keys from the
508-
// original config will be copied into the new config before use.
509-
// Otherwise, if SessionTicketKey was set in the original config but
510-
// not in the returned config then it will be copied into the returned
511-
// config before use. If neither of those cases applies then the key
512-
// material from the returned config will be used for session tickets.
504+
// If SessionTicketKey was explicitly set on the returned Config, or if
505+
// SetSessionTicketKeys was called on the returned Config, those keys will
506+
// be used. Otherwise, the original Config keys will be used (and possibly
507+
// rotated if they are automatically managed).
513508
GetConfigForClient func(*ClientHelloInfo) (*Config, error)
514509

515510
// VerifyPeerCertificate, if not nil, is called after normal
@@ -579,10 +574,10 @@ type Config struct {
579574
// See RFC 5077 and the PSK mode of RFC 8446. If zero, it will be filled
580575
// with random data before the first server handshake.
581576
//
582-
// If multiple servers are terminating connections for the same host
583-
// they should all have the same SessionTicketKey. If the
584-
// SessionTicketKey leaks, previously recorded and future TLS
585-
// connections using that key might be compromised.
577+
// Deprecated: if this field is left at zero, session ticket keys will be
578+
// automatically rotated every day and dropped after seven days. For
579+
// customizing the rotation schedule or synchronizing servers that are
580+
// terminating connections for the same host, use SetSessionTicketKeys.
586581
SessionTicketKey [32]byte
587582

588583
// ClientSessionCache is a cache of ClientSessionState entries for TLS
@@ -622,20 +617,32 @@ type Config struct {
622617
// used for debugging.
623618
KeyLogWriter io.Writer
624619

625-
serverInitOnce sync.Once // guards calling (*Config).serverInit
626-
627-
// mutex protects sessionTicketKeys.
620+
// mutex protects sessionTicketKeys and autoSessionTicketKeys.
628621
mutex sync.RWMutex
629-
// sessionTicketKeys contains zero or more ticket keys. If the length
630-
// is zero, SessionTicketsDisabled must be true. The first key is used
631-
// for new tickets and any subsequent keys can be used to decrypt old
632-
// tickets.
622+
// sessionTicketKeys contains zero or more ticket keys. If set, it means the
623+
// the keys were set with SessionTicketKey or SetSessionTicketKeys. The
624+
// first key is used for new tickets and any subsequent keys can be used to
625+
// decrypt old tickets. The slice contents are not protected by the mutex
626+
// and are immutable.
633627
sessionTicketKeys []ticketKey
628+
// autoSessionTicketKeys is like sessionTicketKeys but is owned by the
629+
// auto-rotation logic. See Config.ticketKeys.
630+
autoSessionTicketKeys []ticketKey
634631
}
635632

636-
// ticketKeyNameLen is the number of bytes of identifier that is prepended to
637-
// an encrypted session ticket in order to identify the key used to encrypt it.
638-
const ticketKeyNameLen = 16
633+
const (
634+
// ticketKeyNameLen is the number of bytes of identifier that is prepended to
635+
// an encrypted session ticket in order to identify the key used to encrypt it.
636+
ticketKeyNameLen = 16
637+
638+
// ticketKeyLifetime is how long a ticket key remains valid and can be used to
639+
// resume a client connection.
640+
ticketKeyLifetime = 7 * 24 * time.Hour // 7 days
641+
642+
// ticketKeyRotation is how often the server should rotate the session ticket key
643+
// that is used for new tickets.
644+
ticketKeyRotation = 24 * time.Hour
645+
)
639646

640647
// ticketKey is the internal representation of a session ticket key.
641648
type ticketKey struct {
@@ -644,16 +651,19 @@ type ticketKey struct {
644651
keyName [ticketKeyNameLen]byte
645652
aesKey [16]byte
646653
hmacKey [16]byte
654+
// created is the time at which this ticket key was created. See Config.ticketKeys.
655+
created time.Time
647656
}
648657

649658
// ticketKeyFromBytes converts from the external representation of a session
650659
// ticket key to a ticketKey. Externally, session ticket keys are 32 random
651660
// bytes and this function expands that into sufficient name and key material.
652-
func ticketKeyFromBytes(b [32]byte) (key ticketKey) {
661+
func (c *Config) ticketKeyFromBytes(b [32]byte) (key ticketKey) {
653662
hashed := sha512.Sum512(b[:])
654663
copy(key.keyName[:], hashed[:ticketKeyNameLen])
655664
copy(key.aesKey[:], hashed[ticketKeyNameLen:ticketKeyNameLen+16])
656665
copy(key.hmacKey[:], hashed[ticketKeyNameLen+16:ticketKeyNameLen+32])
666+
key.created = c.time()
657667
return key
658668
}
659669

@@ -664,15 +674,8 @@ const maxSessionTicketLifetime = 7 * 24 * time.Hour
664674
// Clone returns a shallow clone of c. It is safe to clone a Config that is
665675
// being used concurrently by a TLS client or server.
666676
func (c *Config) Clone() *Config {
667-
// Running serverInit ensures that it's safe to read
668-
// SessionTicketsDisabled.
669-
c.serverInitOnce.Do(func() { c.serverInit(nil) })
670-
671-
var sessionTicketKeys []ticketKey
672677
c.mutex.RLock()
673-
sessionTicketKeys = c.sessionTicketKeys
674-
c.mutex.RUnlock()
675-
678+
defer c.mutex.RUnlock()
676679
return &Config{
677680
Rand: c.Rand,
678681
Time: c.Time,
@@ -699,66 +702,130 @@ func (c *Config) Clone() *Config {
699702
DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled,
700703
Renegotiation: c.Renegotiation,
701704
KeyLogWriter: c.KeyLogWriter,
702-
sessionTicketKeys: sessionTicketKeys,
705+
sessionTicketKeys: c.sessionTicketKeys,
706+
autoSessionTicketKeys: c.autoSessionTicketKeys,
703707
}
704708
}
705709

706-
// serverInit is run under c.serverInitOnce to do initialization of c. If c was
707-
// returned by a GetConfigForClient callback then the argument should be the
708-
// Config that was passed to Server, otherwise it should be nil.
709-
func (c *Config) serverInit(originalConfig *Config) {
710-
if c.SessionTicketsDisabled || len(c.ticketKeys()) != 0 {
710+
// deprecatedSessionTicketKey is set as the prefix of SessionTicketKey if it was
711+
// randomized for backwards compatibility but is not in use.
712+
var deprecatedSessionTicketKey = []byte("DEPRECATED")
713+
714+
// initLegacySessionTicketKeyRLocked ensures the legacy SessionTicketKey field is
715+
// randomized if empty, and that sessionTicketKeys is populated from it otherwise.
716+
func (c *Config) initLegacySessionTicketKeyRLocked() {
717+
// Don't write if SessionTicketKey is already defined as our deprecated string,
718+
// or if it is defined by the user but sessionTicketKeys is already set.
719+
if c.SessionTicketKey != [32]byte{} &&
720+
(bytes.HasPrefix(c.SessionTicketKey[:], deprecatedSessionTicketKey) || len(c.sessionTicketKeys) > 0) {
711721
return
712722
}
713723

714-
alreadySet := false
715-
for _, b := range c.SessionTicketKey {
716-
if b != 0 {
717-
alreadySet = true
718-
break
724+
// We need to write some data, so get an exclusive lock and re-check any conditions.
725+
c.mutex.RUnlock()
726+
defer c.mutex.RLock()
727+
c.mutex.Lock()
728+
defer c.mutex.Unlock()
729+
if c.SessionTicketKey == [32]byte{} {
730+
if _, err := io.ReadFull(c.rand(), c.SessionTicketKey[:]); err != nil {
731+
panic(fmt.Sprintf("tls: unable to generate random session ticket key: %v", err))
719732
}
733+
// Write the deprecated prefix at the beginning so we know we created
734+
// it. This key with the DEPRECATED prefix isn't used as an actual
735+
// session ticket key, and is only randomized in case the application
736+
// reuses it for some reason.
737+
copy(c.SessionTicketKey[:], deprecatedSessionTicketKey)
738+
} else if !bytes.HasPrefix(c.SessionTicketKey[:], deprecatedSessionTicketKey) && len(c.sessionTicketKeys) == 0 {
739+
c.sessionTicketKeys = []ticketKey{c.ticketKeyFromBytes(c.SessionTicketKey)}
720740
}
721741

722-
if !alreadySet {
723-
if originalConfig != nil {
724-
copy(c.SessionTicketKey[:], originalConfig.SessionTicketKey[:])
725-
} else if _, err := io.ReadFull(c.rand(), c.SessionTicketKey[:]); err != nil {
726-
c.SessionTicketsDisabled = true
727-
return
742+
}
743+
744+
// ticketKeys returns the ticketKeys for this connection.
745+
// If configForClient has explicitly set keys, those will
746+
// be returned. Otherwise, the keys on c will be used and
747+
// may be rotated if auto-managed.
748+
// During rotation, any expired session ticket keys are deleted from
749+
// c.sessionTicketKeys. If the session ticket key that is currently
750+
// encrypting tickets (ie. the first ticketKey in c.sessionTicketKeys)
751+
// is not fresh, then a new session ticket key will be
752+
// created and prepended to c.sessionTicketKeys.
753+
func (c *Config) ticketKeys(configForClient *Config) []ticketKey {
754+
// If the ConfigForClient callback returned a Config with explicitly set
755+
// keys, use those, otherwise just use the original Config.
756+
if configForClient != nil {
757+
configForClient.mutex.RLock()
758+
if configForClient.SessionTicketsDisabled {
759+
return nil
760+
}
761+
configForClient.initLegacySessionTicketKeyRLocked()
762+
if len(configForClient.sessionTicketKeys) != 0 {
763+
ret := configForClient.sessionTicketKeys
764+
configForClient.mutex.RUnlock()
765+
return ret
728766
}
767+
configForClient.mutex.RUnlock()
729768
}
730769

731-
if originalConfig != nil {
732-
originalConfig.mutex.RLock()
733-
c.sessionTicketKeys = originalConfig.sessionTicketKeys
734-
originalConfig.mutex.RUnlock()
735-
} else {
736-
c.sessionTicketKeys = []ticketKey{ticketKeyFromBytes(c.SessionTicketKey)}
770+
c.mutex.RLock()
771+
defer c.mutex.RUnlock()
772+
if c.SessionTicketsDisabled {
773+
return nil
774+
}
775+
c.initLegacySessionTicketKeyRLocked()
776+
if len(c.sessionTicketKeys) != 0 {
777+
return c.sessionTicketKeys
778+
}
779+
// Fast path for the common case where the key is fresh enough.
780+
if len(c.autoSessionTicketKeys) > 0 && c.time().Sub(c.autoSessionTicketKeys[0].created) < ticketKeyRotation {
781+
return c.autoSessionTicketKeys
737782
}
738-
}
739783

740-
func (c *Config) ticketKeys() []ticketKey {
741-
c.mutex.RLock()
742-
// c.sessionTicketKeys is constant once created. SetSessionTicketKeys
743-
// will only update it by replacing it with a new value.
744-
ret := c.sessionTicketKeys
784+
// autoSessionTicketKeys are managed by auto-rotation.
745785
c.mutex.RUnlock()
746-
return ret
786+
defer c.mutex.RLock()
787+
c.mutex.Lock()
788+
defer c.mutex.Unlock()
789+
// Re-check the condition in case it changed since obtaining the new lock.
790+
if len(c.autoSessionTicketKeys) == 0 || c.time().Sub(c.autoSessionTicketKeys[0].created) >= ticketKeyRotation {
791+
var newKey [32]byte
792+
if _, err := io.ReadFull(c.rand(), newKey[:]); err != nil {
793+
panic(fmt.Sprintf("unable to generate random session ticket key: %v", err))
794+
}
795+
valid := make([]ticketKey, 0, len(c.autoSessionTicketKeys)+1)
796+
valid = append(valid, c.ticketKeyFromBytes(newKey))
797+
for _, k := range c.autoSessionTicketKeys {
798+
// While rotating the current key, also remove any expired ones.
799+
if c.time().Sub(k.created) < ticketKeyLifetime {
800+
valid = append(valid, k)
801+
}
802+
}
803+
c.autoSessionTicketKeys = valid
804+
}
805+
return c.autoSessionTicketKeys
747806
}
748807

749-
// SetSessionTicketKeys updates the session ticket keys for a server. The first
750-
// key will be used when creating new tickets, while all keys can be used for
751-
// decrypting tickets. It is safe to call this function while the server is
752-
// running in order to rotate the session ticket keys. The function will panic
753-
// if keys is empty.
808+
// SetSessionTicketKeys updates the session ticket keys for a server.
809+
//
810+
// The first key will be used when creating new tickets, while all keys can be
811+
// used for decrypting tickets. It is safe to call this function while the
812+
// server is running in order to rotate the session ticket keys. The function
813+
// will panic if keys is empty.
814+
//
815+
// Calling this function will turn off automatic session ticket key rotation.
816+
//
817+
// If multiple servers are terminating connections for the same host they should
818+
// all have the same session ticket keys. If the session ticket keys leaks,
819+
// previously recorded and future TLS connections using those keys might be
820+
// compromised.
754821
func (c *Config) SetSessionTicketKeys(keys [][32]byte) {
755822
if len(keys) == 0 {
756823
panic("tls: keys must have at least one key")
757824
}
758825

759826
newKeys := make([]ticketKey, len(keys))
760827
for i, bytes := range keys {
761-
newKeys[i] = ticketKeyFromBytes(bytes)
828+
newKeys[i] = c.ticketKeyFromBytes(bytes)
762829
}
763830

764831
c.mutex.Lock()

src/crypto/tls/conn.go

+5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ type Conn struct {
6262
// NewSessionTicket messages. nil if config.SessionTicketsDisabled.
6363
resumptionSecret []byte
6464

65+
// ticketKeys is the set of active session ticket keys for this
66+
// connection. The first one is used to encrypt new tickets and
67+
// all are tried to decrypt tickets.
68+
ticketKeys []ticketKey
69+
6570
// clientFinishedIsFirst is true if the client sent the first Finished
6671
// message during the most recent handshake. This is recorded because
6772
// the first transmitted Finished message is the tls-unique

src/crypto/tls/handshake_client_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,21 @@ func testResumption(t *testing.T, version uint16) {
937937
t.Fatal("ticket didn't change after resumption")
938938
}
939939

940+
// An old session ticket can resume, but the server will provide a ticket encrypted with a fresh key.
941+
serverConfig.Time = func() time.Time { return time.Now().Add(24*time.Hour + time.Minute) }
942+
testResumeState("ResumeWithOldTicket", true)
943+
if bytes.Equal(ticket[:ticketKeyNameLen], getTicket()[:ticketKeyNameLen]) {
944+
t.Fatal("old first ticket matches the fresh one")
945+
}
946+
947+
// Now the session tickey key is expired, so a full handshake should occur.
948+
serverConfig.Time = func() time.Time { return time.Now().Add(24*8*time.Hour + time.Minute) }
949+
testResumeState("ResumeWithExpiredTicket", false)
950+
if bytes.Equal(ticket, getTicket()) {
951+
t.Fatal("expired first ticket matches the fresh one")
952+
}
953+
954+
serverConfig.Time = func() time.Time { return time.Now() } // reset the time back
940955
key1 := randomKey()
941956
serverConfig.SetSessionTicketKeys([][32]byte{key1})
942957

src/crypto/tls/handshake_server.go

+6-8
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ type serverHandshakeState struct {
3737

3838
// serverHandshake performs a TLS handshake as a server.
3939
func (c *Conn) serverHandshake() error {
40-
// If this is the first server handshake, we generate a random key to
41-
// encrypt the tickets with.
42-
c.config.serverInitOnce.Do(func() { c.config.serverInit(nil) })
43-
4440
clientHello, err := c.readClientHello()
4541
if err != nil {
4642
return err
@@ -143,16 +139,18 @@ func (c *Conn) readClientHello() (*clientHelloMsg, error) {
143139
return nil, unexpectedMessageError(clientHello, msg)
144140
}
145141

142+
var configForClient *Config
143+
originalConfig := c.config
146144
if c.config.GetConfigForClient != nil {
147145
chi := clientHelloInfo(c, clientHello)
148-
if newConfig, err := c.config.GetConfigForClient(chi); err != nil {
146+
if configForClient, err = c.config.GetConfigForClient(chi); err != nil {
149147
c.sendAlert(alertInternalError)
150148
return nil, err
151-
} else if newConfig != nil {
152-
newConfig.serverInitOnce.Do(func() { newConfig.serverInit(c.config) })
153-
c.config = newConfig
149+
} else if configForClient != nil {
150+
c.config = configForClient
154151
}
155152
}
153+
c.ticketKeys = originalConfig.ticketKeys(configForClient)
156154

157155
clientVersions := clientHello.supportedVersions
158156
if len(clientHello.supportedVersions) == 0 {

src/crypto/tls/handshake_server_test.go

+4-10
Original file line numberDiff line numberDiff line change
@@ -1496,12 +1496,8 @@ var getConfigForClientTests = []struct {
14961496
},
14971497
"",
14981498
func(config *Config) error {
1499-
// The value of SessionTicketKey should have been
1500-
// duplicated into the per-connection Config.
1501-
for i := range config.SessionTicketKey {
1502-
if b := config.SessionTicketKey[i]; b != byte(i) {
1503-
return fmt.Errorf("SessionTicketKey was not duplicated from original Config: byte %d has value %d", i, b)
1504-
}
1499+
if config.SessionTicketKey == [32]byte{} {
1500+
return fmt.Errorf("expected SessionTicketKey to be set")
15051501
}
15061502
return nil
15071503
},
@@ -1522,10 +1518,8 @@ var getConfigForClientTests = []struct {
15221518
},
15231519
"",
15241520
func(config *Config) error {
1525-
// The session ticket keys should have been duplicated
1526-
// into the per-connection Config.
1527-
if l := len(config.sessionTicketKeys); l != 1 {
1528-
return fmt.Errorf("got len(sessionTicketKeys) == %d, wanted 1", l)
1521+
if config.SessionTicketKey == [32]byte{} {
1522+
return fmt.Errorf("expected SessionTicketKey to be set")
15291523
}
15301524
return nil
15311525
},

0 commit comments

Comments
 (0)