From 529caf01aff2314585688c0f92f009d0ad0914be Mon Sep 17 00:00:00 2001
From: Roland Shoemaker <roland@golang.org>
Date: Mon, 26 Jan 2026 10:55:32 -0800
Subject: [PATCH 1/2] [release-branch.go1.24] crypto/tls: add verifiedChains
 expiration checking during resumption

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 #77355
Updates CVE-2025-68121

CVE: CVE-2025-68121
Upstream-Status: Backport [https://github.com/golang/go/commit/5f07b226f9aa]

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/+/740061
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Auto-Submit: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
(cherry picked from commit 5f07b226f9aa185aca4b88a9ae58456d7800fc06)
Signed-off-by: Deepak Rathore <deeratho@cisco.com>
---
 src/crypto/tls/common.go                 |  13 +++
 src/crypto/tls/handshake_client.go       |  10 +-
 src/crypto/tls/handshake_server.go       |   2 +-
 src/crypto/tls/handshake_server_test.go  | 122 +++++++++++++++++++++++
 src/crypto/tls/handshake_server_tls13.go |   2 +-
 5 files changed, 144 insertions(+), 5 deletions(-)

diff --git a/src/crypto/tls/common.go b/src/crypto/tls/common.go
index 849e8b0a20..738c7e100b 100644
--- a/src/crypto/tls/common.go
+++ b/src/crypto/tls/common.go
@@ -1555,3 +1555,16 @@ func (e *CertificateVerificationError) Error() string {
 func (e *CertificateVerificationError) Unwrap() error {
	return e.Err
 }
+
+// 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
+}
diff --git a/src/crypto/tls/handshake_client.go b/src/crypto/tls/handshake_client.go
index 08a2d47974..c2ff9e1959 100644
--- a/src/crypto/tls/handshake_client.go
+++ b/src/crypto/tls/handshake_client.go
@@ -322,9 +322,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)
@@ -336,6 +333,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
		}
	}
diff --git a/src/crypto/tls/handshake_server.go b/src/crypto/tls/handshake_server.go
index 17b6891783..608b2535f1 100644
--- a/src/crypto/tls/handshake_server.go
+++ b/src/crypto/tls/handshake_server.go
@@ -483,7 +483,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
	}

diff --git a/src/crypto/tls/handshake_server_test.go b/src/crypto/tls/handshake_server_test.go
index 0f10a3e7a6..9eff106ecf 100644
--- a/src/crypto/tls/handshake_server_test.go
+++ b/src/crypto/tls/handshake_server_test.go
@@ -12,6 +12,7 @@ import (
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
+	"crypto/x509/pkix"
	"encoding/pem"
	"errors"
	"fmt"
@@ -2049,3 +2050,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))
+}
diff --git a/src/crypto/tls/handshake_server_tls13.go b/src/crypto/tls/handshake_server_tls13.go
index 5aa69e9640..a48a296721 100644
--- a/src/crypto/tls/handshake_server_tls13.go
+++ b/src/crypto/tls/handshake_server_tls13.go
@@ -346,7 +346,7 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error {
			continue
		}
		if sessionHasClientCerts && c.config.ClientAuth >= VerifyClientCertIfGiven &&
-			len(sessionState.verifiedChains) == 0 {
+			!anyUnexpiredChain(sessionState.verifiedChains, c.config.time()) {
			continue
		}

--
2.35.6
