]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/tls: add verifiedChains expiration checking during resumption
authorRoland Shoemaker <roland@golang.org>
Mon, 26 Jan 2026 18:55:32 +0000 (10:55 -0800)
committerGopher Robot <gobot@golang.org>
Wed, 28 Jan 2026 16:13:28 +0000 (08:13 -0800)
When resuming a session, check that the verifiedChains contain at least
one chain that is still valid at the time of resumption. If not, trigger
a new handshake.

Updates #77113
Updates #77217
Updates CVE-2025-68121

Change-Id: I14f585c43da17802513cbdd5b10c552d7a38b34e
Reviewed-on: https://go-review.googlesource.com/c/go/+/739321
Reviewed-by: Coia Prant <coiaprant@gmail.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
src/crypto/tls/common.go
src/crypto/tls/handshake_client.go
src/crypto/tls/handshake_server.go
src/crypto/tls/handshake_server_test.go
src/crypto/tls/handshake_server_tls13.go

index 099a11ca63da758274f86e2354a2bd1a7b06fee0..65cff5f5b9a1b69b2a142dde62ce1683ba6c8436 100644 (file)
@@ -1846,3 +1846,16 @@ func fipsAllowChain(chain []*x509.Certificate) bool {
 
        return true
 }
+
+// anyUnexpiredChain reports if at least one of verifiedChains is still
+// unexpired. If verifiedChains is empty, it returns false.
+func anyUnexpiredChain(verifiedChains [][]*x509.Certificate, now time.Time) bool {
+       for _, chain := range verifiedChains {
+               if len(chain) != 0 && !slices.ContainsFunc(chain, func(cert *x509.Certificate) bool {
+                       return now.Before(cert.NotBefore) || now.After(cert.NotAfter) // cert is expired
+               }) {
+                       return true
+               }
+       }
+       return false
+}
index c2b1b7037a46cf87d8c87b822ed5c57eeb1d692a..d1ad9d582bdfb86df3efff52adba7ef7e80744bb 100644 (file)
@@ -397,9 +397,6 @@ func (c *Conn) loadSession(hello *clientHelloMsg) (
                return nil, nil, nil, nil
        }
 
-       // Check that the cached server certificate is not expired, and that it's
-       // valid for the ServerName. This should be ensured by the cache key, but
-       // protect the application from a faulty ClientSessionCache implementation.
        if c.config.time().After(session.peerCertificates[0].NotAfter) {
                // Expired certificate, delete the entry.
                c.config.ClientSessionCache.Put(cacheKey, nil)
@@ -411,6 +408,13 @@ func (c *Conn) loadSession(hello *clientHelloMsg) (
                        return nil, nil, nil, nil
                }
                if err := session.peerCertificates[0].VerifyHostname(c.config.ServerName); err != nil {
+                       // This should be ensured by the cache key, but protect the
+                       // application from a faulty ClientSessionCache implementation.
+                       return nil, nil, nil, nil
+               }
+               if !anyUnexpiredChain(session.verifiedChains, c.config.time()) {
+                       // No valid chains, delete the entry.
+                       c.config.ClientSessionCache.Put(cacheKey, nil)
                        return nil, nil, nil, nil
                }
        }
index efdaeae6f7e39a23ae1f9b1cb6adbd7649d66935..64053e1a9e52bbbc0658df053b7dbf9fe78fffd4 100644 (file)
@@ -524,7 +524,7 @@ func (hs *serverHandshakeState) checkForResumption() error {
                return nil
        }
        if sessionHasClientCerts && c.config.ClientAuth >= VerifyClientCertIfGiven &&
-               len(sessionState.verifiedChains) == 0 {
+               !anyUnexpiredChain(sessionState.verifiedChains, c.config.time()) {
                return nil
        }
 
index 7e35c252593658a3cb626daecb12bccea0189107..8325c9fac31012de89e217a8d97de1fd8e4b3282 100644 (file)
@@ -13,6 +13,7 @@ import (
        "crypto/rand"
        "crypto/tls/internal/fips140tls"
        "crypto/x509"
+       "crypto/x509/pkix"
        "encoding/pem"
        "errors"
        "fmt"
@@ -2153,3 +2154,124 @@ func TestHandshakeContextHierarchy(t *testing.T) {
                t.Errorf("Unexpected client error: %v", err)
        }
 }
+
+func TestHandshakeChainExpiryResumption(t *testing.T) {
+       t.Run("TLS1.2", func(t *testing.T) {
+               testHandshakeChainExpiryResumption(t, VersionTLS12)
+       })
+       t.Run("TLS1.3", func(t *testing.T) {
+               testHandshakeChainExpiryResumption(t, VersionTLS13)
+       })
+}
+
+func testHandshakeChainExpiryResumption(t *testing.T, version uint16) {
+       now := time.Now()
+
+       createChain := func(leafNotAfter, rootNotAfter time.Time) (leafDER, expiredLeafDER []byte, root *x509.Certificate) {
+               tmpl := &x509.Certificate{
+                       Subject:               pkix.Name{CommonName: "root"},
+                       NotBefore:             rootNotAfter.Add(-time.Hour * 24),
+                       NotAfter:              rootNotAfter,
+                       IsCA:                  true,
+                       BasicConstraintsValid: true,
+               }
+               rootDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &testECDSAPrivateKey.PublicKey, testECDSAPrivateKey)
+               if err != nil {
+                       t.Fatalf("CreateCertificate: %v", err)
+               }
+               root, err = x509.ParseCertificate(rootDER)
+               if err != nil {
+                       t.Fatalf("ParseCertificate: %v", err)
+               }
+
+               tmpl = &x509.Certificate{
+                       Subject:   pkix.Name{},
+                       DNSNames:  []string{"expired-resume.example.com"},
+                       NotBefore: leafNotAfter.Add(-time.Hour * 24),
+                       NotAfter:  leafNotAfter,
+                       KeyUsage:  x509.KeyUsageDigitalSignature,
+               }
+               leafCertDER, err := x509.CreateCertificate(rand.Reader, tmpl, root, &testECDSAPrivateKey.PublicKey, testECDSAPrivateKey)
+               if err != nil {
+                       t.Fatalf("CreateCertificate: %v", err)
+               }
+               tmpl.NotBefore, tmpl.NotAfter = leafNotAfter.Add(-time.Hour*24*365), leafNotAfter.Add(-time.Hour*24*364)
+               expiredLeafDERCertDER, err := x509.CreateCertificate(rand.Reader, tmpl, root, &testECDSAPrivateKey.PublicKey, testECDSAPrivateKey)
+               if err != nil {
+                       t.Fatalf("CreateCertificate: %v", err)
+               }
+
+               return leafCertDER, expiredLeafDERCertDER, root
+       }
+       testExpiration := func(name string, leafNotAfter, rootNotAfter time.Time) {
+               t.Run(name, func(t *testing.T) {
+                       initialLeafDER, expiredLeafDER, initialRoot := createChain(leafNotAfter, rootNotAfter)
+
+                       serverConfig := testConfig.Clone()
+                       serverConfig.MaxVersion = version
+                       serverConfig.Certificates = []Certificate{{
+                               Certificate: [][]byte{initialLeafDER, expiredLeafDER},
+                               PrivateKey:  testECDSAPrivateKey,
+                       }}
+                       serverConfig.ClientCAs = x509.NewCertPool()
+                       serverConfig.ClientCAs.AddCert(initialRoot)
+                       serverConfig.ClientAuth = RequireAndVerifyClientCert
+                       serverConfig.Time = func() time.Time {
+                               return now
+                       }
+                       serverConfig.InsecureSkipVerify = false
+                       serverConfig.ServerName = "expired-resume.example.com"
+
+                       clientConfig := testConfig.Clone()
+                       clientConfig.MaxVersion = version
+                       clientConfig.Certificates = []Certificate{{
+                               Certificate: [][]byte{initialLeafDER, expiredLeafDER},
+                               PrivateKey:  testECDSAPrivateKey,
+                       }}
+                       clientConfig.RootCAs = x509.NewCertPool()
+                       clientConfig.RootCAs.AddCert(initialRoot)
+                       clientConfig.ServerName = "expired-resume.example.com"
+                       clientConfig.ClientSessionCache = NewLRUClientSessionCache(32)
+                       clientConfig.InsecureSkipVerify = false
+                       clientConfig.ServerName = "expired-resume.example.com"
+                       clientConfig.Time = func() time.Time {
+                               return now
+                       }
+
+                       testResume := func(t *testing.T, sc, cc *Config, expectResume bool) {
+                               t.Helper()
+                               ss, cs, err := testHandshake(t, cc, sc)
+                               if err != nil {
+                                       t.Fatalf("handshake: %v", err)
+                               }
+                               if cs.DidResume != expectResume {
+                                       t.Fatalf("DidResume = %v; want %v", cs.DidResume, expectResume)
+                               }
+                               if ss.DidResume != expectResume {
+                                       t.Fatalf("DidResume = %v; want %v", cs.DidResume, expectResume)
+                               }
+                       }
+
+                       testResume(t, serverConfig, clientConfig, false)
+                       testResume(t, serverConfig, clientConfig, true)
+
+                       expiredNow := time.Unix(0, min(leafNotAfter.UnixNano(), rootNotAfter.UnixNano())).Add(time.Minute)
+
+                       freshLeafDER, expiredLeafDER, freshRoot := createChain(expiredNow.Add(time.Hour), expiredNow.Add(time.Hour))
+                       clientConfig.Certificates = []Certificate{{
+                               Certificate: [][]byte{freshLeafDER, expiredLeafDER},
+                               PrivateKey:  testECDSAPrivateKey,
+                       }}
+                       serverConfig.Time = func() time.Time {
+                               return expiredNow
+                       }
+                       serverConfig.ClientCAs = x509.NewCertPool()
+                       serverConfig.ClientCAs.AddCert(freshRoot)
+
+                       testResume(t, serverConfig, clientConfig, false)
+               })
+       }
+
+       testExpiration("LeafExpiresBeforeRoot", now.Add(2*time.Hour), now.Add(3*time.Hour))
+       testExpiration("LeafExpiresAfterRoot", now.Add(2*time.Hour), now.Add(time.Hour))
+}
index b066924e29168648dabc53382d0febaf3b44de07..11dbaa9f0a55f6f2f6e8825eb883f573a93b52e3 100644 (file)
@@ -370,7 +370,7 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error {
                        continue
                }
                if sessionHasClientCerts && c.config.ClientAuth >= VerifyClientCertIfGiven &&
-                       len(sessionState.verifiedChains) == 0 {
+                       !anyUnexpiredChain(sessionState.verifiedChains, c.config.time()) {
                        continue
                }