]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] crypto/tls: add verifiedChains expiration checking during...
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 22:03:25 +0000 (14:03 -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 #77356
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>
Reviewed-on: https://go-review.googlesource.com/c/go/+/740064
Reviewed-by: Damien Neil <dneil@google.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
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 6fe6f34cd210d8bdffb6b8e32e0c8051a08d4256..7208e69334a5e895e31192b8e1cf83e15db56743 100644 (file)
@@ -1803,3 +1803,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 8d09d186529904ac91b0dfac14904773d925744c..3752df09b6dcbe3dae7a24108e5db839fa45b317 100644 (file)
@@ -429,9 +429,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)
@@ -443,6 +440,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 088c66fadb2a4446453b6b8852966ad3006e47e1..d4d05e5462905e0ff13d4764521a3a45dda2c759 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 a6d64a506a0542f1498764cbf1da1b5962ba6c26..406dab48eefaf9b867847d2975e0a6d0d57ad04e 100644 (file)
@@ -13,6 +13,7 @@ import (
        "crypto/rand"
        "crypto/tls/internal/fips140tls"
        "crypto/x509"
+       "crypto/x509/pkix"
        "encoding/pem"
        "errors"
        "fmt"
@@ -2121,3 +2122,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 71f6c1b5a4c2d986ad12c5a147814c32844ce409..3dba595331d925eda992b673cb0974b8673f0f2e 100644 (file)
@@ -410,7 +410,7 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error {
                        continue
                }
                if sessionHasClientCerts && c.config.ClientAuth >= VerifyClientCertIfGiven &&
-                       len(sessionState.verifiedChains) == 0 {
+                       !anyUnexpiredChain(sessionState.verifiedChains, c.config.time()) {
                        continue
                }