Skip to content

Commit 360600b

Browse files
rolandshoemakergopherbot
authored andcommitted
crypto/tls: replace custom intern cache with weak cache
Uses the new weak package to replace the existing custom intern cache with a map of weak.Pointers instead. This simplifies the cache, and means we don't need to store a slice of handles on the Conn anymore. Change-Id: I5c2bf6ef35fac4255e140e184f4e48574b34174c Reviewed-on: https://go-review.googlesource.com/c/go/+/644176 TryBot-Bypass: Roland Shoemaker <roland@golang.org> Reviewed-by: Michael Knyszek <mknyszek@google.com> Auto-Submit: Roland Shoemaker <roland@golang.org>
1 parent e6dacf9 commit 360600b

File tree

7 files changed

+57
-172
lines changed

7 files changed

+57
-172
lines changed

src/crypto/tls/cache.go

Lines changed: 21 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,88 +8,37 @@ import (
88
"crypto/x509"
99
"runtime"
1010
"sync"
11-
"sync/atomic"
11+
"weak"
1212
)
1313

14-
type cacheEntry struct {
15-
refs atomic.Int64
16-
cert *x509.Certificate
17-
}
18-
19-
// certCache implements an intern table for reference counted x509.Certificates,
20-
// implemented in a similar fashion to BoringSSL's CRYPTO_BUFFER_POOL. This
21-
// allows for a single x509.Certificate to be kept in memory and referenced from
22-
// multiple Conns. Returned references should not be mutated by callers. Certificates
23-
// are still safe to use after they are removed from the cache.
24-
//
25-
// Certificates are returned wrapped in an activeCert struct that should be held by
26-
// the caller. When references to the activeCert are freed, the number of references
27-
// to the certificate in the cache is decremented. Once the number of references
28-
// reaches zero, the entry is evicted from the cache.
29-
//
30-
// The main difference between this implementation and CRYPTO_BUFFER_POOL is that
31-
// CRYPTO_BUFFER_POOL is a more generic structure which supports blobs of data,
32-
// rather than specific structures. Since we only care about x509.Certificates,
33-
// certCache is implemented as a specific cache, rather than a generic one.
34-
//
35-
// See https://boringssl.googlesource.com/boringssl/+/master/include/openssl/pool.h
36-
// and https://boringssl.googlesource.com/boringssl/+/master/crypto/pool/pool.c
37-
// for the BoringSSL reference.
38-
type certCache struct {
39-
sync.Map
40-
}
41-
42-
var globalCertCache = new(certCache)
43-
44-
// activeCert is a handle to a certificate held in the cache. Once there are
45-
// no alive activeCerts for a given certificate, the certificate is removed
46-
// from the cache by a cleanup.
47-
type activeCert struct {
48-
cert *x509.Certificate
49-
}
14+
// weakCertCache provides a cache of *x509.Certificates, allowing multiple
15+
// connections to reuse parsed certificates, instead of re-parsing the
16+
// certificate for every connection, which is an expensive operation.
17+
type weakCertCache struct{ sync.Map }
5018

51-
// active increments the number of references to the entry, wraps the
52-
// certificate in the entry in an activeCert, and sets the cleanup.
53-
//
54-
// Note that there is a race between active and the cleanup set on the
55-
// returned activeCert, triggered if active is called after the ref count is
56-
// decremented such that refs may be > 0 when evict is called. We consider this
57-
// safe, since the caller holding an activeCert for an entry that is no longer
58-
// in the cache is fine, with the only side effect being the memory overhead of
59-
// there being more than one distinct reference to a certificate alive at once.
60-
func (cc *certCache) active(e *cacheEntry) *activeCert {
61-
e.refs.Add(1)
62-
a := &activeCert{e.cert}
63-
runtime.AddCleanup(a, func(ce *cacheEntry) {
64-
if ce.refs.Add(-1) == 0 {
65-
cc.evict(ce)
19+
func (wcc *weakCertCache) newCert(der []byte) (*x509.Certificate, error) {
20+
if entry, ok := wcc.Load(string(der)); ok {
21+
if v := entry.(weak.Pointer[x509.Certificate]).Value(); v != nil {
22+
return v, nil
6623
}
67-
}, e)
68-
return a
69-
}
70-
71-
// evict removes a cacheEntry from the cache.
72-
func (cc *certCache) evict(e *cacheEntry) {
73-
cc.Delete(string(e.cert.Raw))
74-
}
75-
76-
// newCert returns a x509.Certificate parsed from der. If there is already a copy
77-
// of the certificate in the cache, a reference to the existing certificate will
78-
// be returned. Otherwise, a fresh certificate will be added to the cache, and
79-
// the reference returned. The returned reference should not be mutated.
80-
func (cc *certCache) newCert(der []byte) (*activeCert, error) {
81-
if entry, ok := cc.Load(string(der)); ok {
82-
return cc.active(entry.(*cacheEntry)), nil
8324
}
8425

8526
cert, err := x509.ParseCertificate(der)
8627
if err != nil {
8728
return nil, err
8829
}
8930

90-
entry := &cacheEntry{cert: cert}
91-
if entry, loaded := cc.LoadOrStore(string(der), entry); loaded {
92-
return cc.active(entry.(*cacheEntry)), nil
31+
wp := weak.Make(cert)
32+
if entry, loaded := wcc.LoadOrStore(string(der), wp); !loaded {
33+
runtime.AddCleanup(cert, func(_ any) { wcc.CompareAndDelete(string(der), entry) }, any(string(der)))
34+
} else if v := entry.(weak.Pointer[x509.Certificate]).Value(); v != nil {
35+
return v, nil
36+
} else {
37+
if wcc.CompareAndSwap(string(der), entry, wp) {
38+
runtime.AddCleanup(cert, func(_ any) { wcc.CompareAndDelete(string(der), wp) }, any(string(der)))
39+
}
9340
}
94-
return cc.active(entry), nil
41+
return cert, nil
9542
}
43+
44+
var globalCertCache = new(weakCertCache)

src/crypto/tls/cache_test.go

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,48 @@
1-
// Copyright 2022 The Go Authors. All rights reserved.
1+
// Copyright 2025 The Go Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
4-
54
package tls
65

76
import (
87
"encoding/pem"
9-
"fmt"
108
"runtime"
119
"testing"
1210
"time"
1311
)
1412

15-
func TestCertCache(t *testing.T) {
16-
cc := certCache{}
13+
func TestWeakCertCache(t *testing.T) {
14+
wcc := weakCertCache{}
1715
p, _ := pem.Decode([]byte(rsaCertPEM))
1816
if p == nil {
1917
t.Fatal("Failed to decode certificate")
2018
}
2119

22-
certA, err := cc.newCert(p.Bytes)
20+
certA, err := wcc.newCert(p.Bytes)
2321
if err != nil {
2422
t.Fatalf("newCert failed: %s", err)
2523
}
26-
certB, err := cc.newCert(p.Bytes)
24+
certB, err := wcc.newCert(p.Bytes)
2725
if err != nil {
2826
t.Fatalf("newCert failed: %s", err)
2927
}
30-
if certA.cert != certB.cert {
28+
if certA != certB {
3129
t.Fatal("newCert returned a unique reference for a duplicate certificate")
3230
}
3331

34-
if entry, ok := cc.Load(string(p.Bytes)); !ok {
32+
if _, ok := wcc.Load(string(p.Bytes)); !ok {
3533
t.Fatal("cache does not contain expected entry")
36-
} else {
37-
if refs := entry.(*cacheEntry).refs.Load(); refs != 2 {
38-
t.Fatalf("unexpected number of references: got %d, want 2", refs)
39-
}
4034
}
4135

42-
timeoutRefCheck := func(t *testing.T, key string, count int64) {
36+
timeoutRefCheck := func(t *testing.T, key string, present bool) {
4337
t.Helper()
4438
timeout := time.After(4 * time.Second)
4539
for {
4640
select {
4741
case <-timeout:
4842
t.Fatal("timed out waiting for expected ref count")
4943
default:
50-
e, ok := cc.Load(key)
51-
if !ok && count != 0 {
52-
t.Fatal("cache does not contain expected key")
53-
} else if count == 0 && !ok {
54-
return
55-
}
56-
57-
if e.(*cacheEntry).refs.Load() == count {
44+
_, ok := wcc.Load(key)
45+
if ok == present {
5846
return
5947
}
6048
}
@@ -77,7 +65,7 @@ func TestCertCache(t *testing.T) {
7765
certA = nil
7866
runtime.GC()
7967

80-
timeoutRefCheck(t, string(p.Bytes), 1)
68+
timeoutRefCheck(t, string(p.Bytes), true)
8169

8270
// Keep certB alive until at least now, so that we can
8371
// purposefully nil it and force the finalizer to be
@@ -86,41 +74,5 @@ func TestCertCache(t *testing.T) {
8674
certB = nil
8775
runtime.GC()
8876

89-
timeoutRefCheck(t, string(p.Bytes), 0)
90-
}
91-
92-
func BenchmarkCertCache(b *testing.B) {
93-
p, _ := pem.Decode([]byte(rsaCertPEM))
94-
if p == nil {
95-
b.Fatal("Failed to decode certificate")
96-
}
97-
98-
cc := certCache{}
99-
b.ReportAllocs()
100-
b.ResetTimer()
101-
// We expect that calling newCert additional times after
102-
// the initial call should not cause additional allocations.
103-
for extra := 0; extra < 4; extra++ {
104-
b.Run(fmt.Sprint(extra), func(b *testing.B) {
105-
actives := make([]*activeCert, extra+1)
106-
b.ResetTimer()
107-
for i := 0; i < b.N; i++ {
108-
var err error
109-
actives[0], err = cc.newCert(p.Bytes)
110-
if err != nil {
111-
b.Fatal(err)
112-
}
113-
for j := 0; j < extra; j++ {
114-
actives[j+1], err = cc.newCert(p.Bytes)
115-
if err != nil {
116-
b.Fatal(err)
117-
}
118-
}
119-
for j := 0; j < extra+1; j++ {
120-
actives[j] = nil
121-
}
122-
runtime.GC()
123-
}
124-
})
125-
}
77+
timeoutRefCheck(t, string(p.Bytes), false)
12678
}

src/crypto/tls/conn.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ type Conn struct {
5454
ocspResponse []byte // stapled OCSP response
5555
scts [][]byte // signed certificate timestamps from server
5656
peerCertificates []*x509.Certificate
57-
// activeCertHandles contains the cache handles to certificates in
58-
// peerCertificates that are used to track active references.
59-
activeCertHandles []*activeCert
6057
// verifiedChains contains the certificate chains that we built, as
6158
// opposed to the ones presented by the server.
6259
verifiedChains [][]*x509.Certificate

src/crypto/tls/handshake_client.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,6 @@ func (hs *clientHandshakeState) processServerHello() (bool, error) {
956956
hs.masterSecret = hs.session.secret
957957
c.extMasterSecret = hs.session.extMasterSecret
958958
c.peerCertificates = hs.session.peerCertificates
959-
c.activeCertHandles = hs.c.activeCertHandles
960959
c.verifiedChains = hs.session.verifiedChains
961960
c.ocspResponse = hs.session.ocspResponse
962961
// Let the ServerHello SCTs override the session SCTs from the original
@@ -1107,23 +1106,21 @@ func checkKeySize(n int) (max int, ok bool) {
11071106
// verifyServerCertificate parses and verifies the provided chain, setting
11081107
// c.verifiedChains and c.peerCertificates or sending the appropriate alert.
11091108
func (c *Conn) verifyServerCertificate(certificates [][]byte) error {
1110-
activeHandles := make([]*activeCert, len(certificates))
11111109
certs := make([]*x509.Certificate, len(certificates))
11121110
for i, asn1Data := range certificates {
11131111
cert, err := globalCertCache.newCert(asn1Data)
11141112
if err != nil {
11151113
c.sendAlert(alertDecodeError)
11161114
return errors.New("tls: failed to parse certificate from server: " + err.Error())
11171115
}
1118-
if cert.cert.PublicKeyAlgorithm == x509.RSA {
1119-
n := cert.cert.PublicKey.(*rsa.PublicKey).N.BitLen()
1116+
if cert.PublicKeyAlgorithm == x509.RSA {
1117+
n := cert.PublicKey.(*rsa.PublicKey).N.BitLen()
11201118
if max, ok := checkKeySize(n); !ok {
11211119
c.sendAlert(alertBadCertificate)
11221120
return fmt.Errorf("tls: server sent certificate containing RSA key larger than %d bits", max)
11231121
}
11241122
}
1125-
activeHandles[i] = cert
1126-
certs[i] = cert.cert
1123+
certs[i] = cert
11271124
}
11281125

11291126
echRejected := c.config.EncryptedClientHelloConfigList != nil && !c.echAccepted
@@ -1188,7 +1185,6 @@ func (c *Conn) verifyServerCertificate(certificates [][]byte) error {
11881185
return fmt.Errorf("tls: server's certificate contains an unsupported type of public key: %T", certs[0].PublicKey)
11891186
}
11901187

1191-
c.activeCertHandles = activeHandles
11921188
c.peerCertificates = certs
11931189

11941190
if c.config.VerifyPeerCertificate != nil && !echRejected {

src/crypto/tls/handshake_client_tls13.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,6 @@ func (hs *clientHandshakeStateTLS13) processServerHello() error {
466466
hs.usingPSK = true
467467
c.didResume = true
468468
c.peerCertificates = hs.session.peerCertificates
469-
c.activeCertHandles = hs.session.activeCertHandles
470469
c.verifiedChains = hs.session.verifiedChains
471470
c.ocspResponse = hs.session.ocspResponse
472471
c.scts = hs.session.scts

src/crypto/tls/handshake_messages_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,6 @@ func TestMarshalUnmarshal(t *testing.T) {
7272
break
7373
}
7474

75-
if m, ok := m.(*SessionState); ok {
76-
m.activeCertHandles = nil
77-
}
78-
7975
if ch, ok := m.(*clientHelloMsg); ok {
8076
// extensions is special cased, as it is only populated by the
8177
// server-side of a handshake and is not expected to roundtrip

src/crypto/tls/ticket.go

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,14 @@ type SessionState struct {
8484
// createdAt is the generation time of the secret on the sever (which for
8585
// TLS 1.0–1.2 might be earlier than the current session) and the time at
8686
// which the ticket was received on the client.
87-
createdAt uint64 // seconds since UNIX epoch
88-
secret []byte // master secret for TLS 1.2, or the PSK for TLS 1.3
89-
extMasterSecret bool
90-
peerCertificates []*x509.Certificate
91-
activeCertHandles []*activeCert
92-
ocspResponse []byte
93-
scts [][]byte
94-
verifiedChains [][]*x509.Certificate
95-
alpnProtocol string // only set if EarlyData is true
87+
createdAt uint64 // seconds since UNIX epoch
88+
secret []byte // master secret for TLS 1.2, or the PSK for TLS 1.3
89+
extMasterSecret bool
90+
peerCertificates []*x509.Certificate
91+
ocspResponse []byte
92+
scts [][]byte
93+
verifiedChains [][]*x509.Certificate
94+
alpnProtocol string // only set if EarlyData is true
9695

9796
// Client-side TLS 1.3-only fields.
9897
useBy uint64 // seconds since UNIX epoch
@@ -239,8 +238,7 @@ func ParseSessionState(data []byte) (*SessionState, error) {
239238
if err != nil {
240239
return nil, err
241240
}
242-
ss.activeCertHandles = append(ss.activeCertHandles, c)
243-
ss.peerCertificates = append(ss.peerCertificates, c.cert)
241+
ss.peerCertificates = append(ss.peerCertificates, c)
244242
}
245243
if ss.isClient && len(ss.peerCertificates) == 0 {
246244
return nil, errors.New("tls: no server certificates in client session")
@@ -270,8 +268,7 @@ func ParseSessionState(data []byte) (*SessionState, error) {
270268
if err != nil {
271269
return nil, err
272270
}
273-
ss.activeCertHandles = append(ss.activeCertHandles, c)
274-
chain = append(chain, c.cert)
271+
chain = append(chain, c)
275272
}
276273
ss.verifiedChains = append(ss.verifiedChains, chain)
277274
}
@@ -300,18 +297,17 @@ func ParseSessionState(data []byte) (*SessionState, error) {
300297
// from the current connection.
301298
func (c *Conn) sessionState() *SessionState {
302299
return &SessionState{
303-
version: c.vers,
304-
cipherSuite: c.cipherSuite,
305-
createdAt: uint64(c.config.time().Unix()),
306-
alpnProtocol: c.clientProtocol,
307-
peerCertificates: c.peerCertificates,
308-
activeCertHandles: c.activeCertHandles,
309-
ocspResponse: c.ocspResponse,
310-
scts: c.scts,
311-
isClient: c.isClient,
312-
extMasterSecret: c.extMasterSecret,
313-
verifiedChains: c.verifiedChains,
314-
curveID: c.curveID,
300+
version: c.vers,
301+
cipherSuite: c.cipherSuite,
302+
createdAt: uint64(c.config.time().Unix()),
303+
alpnProtocol: c.clientProtocol,
304+
peerCertificates: c.peerCertificates,
305+
ocspResponse: c.ocspResponse,
306+
scts: c.scts,
307+
isClient: c.isClient,
308+
extMasterSecret: c.extMasterSecret,
309+
verifiedChains: c.verifiedChains,
310+
curveID: c.curveID,
315311
}
316312
}
317313

0 commit comments

Comments
 (0)