aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland Shoemaker <roland@golang.org>2026-01-26 10:55:32 -0800
committerGopher Robot <gobot@golang.org>2026-01-28 08:13:28 -0800
commit133b339ca546937919ee3a8027f15470ebeb88b9 (patch)
treec49d90807ce8bac22aecf5bd8c7764dcadc5b68c
parent4f9c3439a37314e63bdae7dad7abfded1647bed2 (diff)
downloadgo-133b339ca546937919ee3a8027f15470ebeb88b9.tar.xz
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 #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>
-rw-r--r--src/crypto/tls/common.go13
-rw-r--r--src/crypto/tls/handshake_client.go10
-rw-r--r--src/crypto/tls/handshake_server.go2
-rw-r--r--src/crypto/tls/handshake_server_test.go122
-rw-r--r--src/crypto/tls/handshake_server_tls13.go2
5 files changed, 144 insertions, 5 deletions
diff --git a/src/crypto/tls/common.go b/src/crypto/tls/common.go
index 099a11ca63..65cff5f5b9 100644
--- a/src/crypto/tls/common.go
+++ b/src/crypto/tls/common.go
@@ -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
+}
diff --git a/src/crypto/tls/handshake_client.go b/src/crypto/tls/handshake_client.go
index c2b1b7037a..d1ad9d582b 100644
--- a/src/crypto/tls/handshake_client.go
+++ b/src/crypto/tls/handshake_client.go
@@ -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
}
}
diff --git a/src/crypto/tls/handshake_server.go b/src/crypto/tls/handshake_server.go
index efdaeae6f7..64053e1a9e 100644
--- a/src/crypto/tls/handshake_server.go
+++ b/src/crypto/tls/handshake_server.go
@@ -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
}
diff --git a/src/crypto/tls/handshake_server_test.go b/src/crypto/tls/handshake_server_test.go
index 7e35c25259..8325c9fac3 100644
--- a/src/crypto/tls/handshake_server_test.go
+++ b/src/crypto/tls/handshake_server_test.go
@@ -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))
+}
diff --git a/src/crypto/tls/handshake_server_tls13.go b/src/crypto/tls/handshake_server_tls13.go
index b066924e29..11dbaa9f0a 100644
--- a/src/crypto/tls/handshake_server_tls13.go
+++ b/src/crypto/tls/handshake_server_tls13.go
@@ -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
}