Skip to content

Commit e8379ab

Browse files
committed
crypto/x509: add support for CertPool to load certs lazily
This will allow building CertPools that consume less memory. (Most certs are never accessed. Different users/programs access different ones, but not many.) This CL only adds the new internal mechanism (and uses it for the old AddCert) but does not modify any existing root pool behavior. (That is, the default Unix roots are still all slurped into memory as of this CL) Change-Id: Ib3a42e4050627b5e34413c595d8ced839c7bfa14 Reviewed-on: https://go-review.googlesource.com/c/go/+/229917 Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Go Bot <gobot@golang.org> Trust: Brad Fitzpatrick <bradfitz@golang.org> Trust: Roland Shoemaker <roland@golang.org> Reviewed-by: Filippo Valsorda <filippo@golang.org> Reviewed-by: Roland Shoemaker <roland@golang.org>
1 parent 2c80de7 commit e8379ab

File tree

9 files changed

+192
-56
lines changed

9 files changed

+192
-56
lines changed

src/crypto/x509/cert_pool.go

Lines changed: 98 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,87 @@ package x509
66

77
import (
88
"bytes"
9+
"crypto/sha256"
910
"encoding/pem"
1011
"errors"
1112
"runtime"
1213
)
1314

15+
type sum224 [sha256.Size224]byte
16+
1417
// CertPool is a set of certificates.
1518
type CertPool struct {
16-
byName map[string][]int
17-
certs []*Certificate
19+
byName map[string][]int // cert.RawSubject => index into lazyCerts
20+
21+
// lazyCerts contains funcs that return a certificate,
22+
// lazily parsing/decompressing it as needed.
23+
lazyCerts []lazyCert
24+
25+
// haveSum maps from sum224(cert.Raw) to true. It's used only
26+
// for AddCert duplicate detection, to avoid CertPool.contains
27+
// calls in the AddCert path (because the contains method can
28+
// call getCert and otherwise negate savings from lazy getCert
29+
// funcs).
30+
haveSum map[sum224]bool
31+
}
32+
33+
// lazyCert is minimal metadata about a Cert and a func to retrieve it
34+
// in its normal expanded *Certificate form.
35+
type lazyCert struct {
36+
// rawSubject is the Certificate.RawSubject value.
37+
// It's the same as the CertPool.byName key, but in []byte
38+
// form to make CertPool.Subjects (as used by crypto/tls) do
39+
// fewer allocations.
40+
rawSubject []byte
41+
42+
// getCert returns the certificate.
43+
//
44+
// It is not meant to do network operations or anything else
45+
// where a failure is likely; the func is meant to lazily
46+
// parse/decompress data that is already known to be good. The
47+
// error in the signature primarily is meant for use in the
48+
// case where a cert file existed on local disk when the program
49+
// started up is deleted later before it's read.
50+
getCert func() (*Certificate, error)
1851
}
1952

2053
// NewCertPool returns a new, empty CertPool.
2154
func NewCertPool() *CertPool {
2255
return &CertPool{
23-
byName: make(map[string][]int),
56+
byName: make(map[string][]int),
57+
haveSum: make(map[sum224]bool),
2458
}
2559
}
2660

61+
// len returns the number of certs in the set.
62+
// A nil set is a valid empty set.
63+
func (s *CertPool) len() int {
64+
if s == nil {
65+
return 0
66+
}
67+
return len(s.lazyCerts)
68+
}
69+
70+
// cert returns cert index n in s.
71+
func (s *CertPool) cert(n int) (*Certificate, error) {
72+
return s.lazyCerts[n].getCert()
73+
}
74+
2775
func (s *CertPool) copy() *CertPool {
2876
p := &CertPool{
29-
byName: make(map[string][]int, len(s.byName)),
30-
certs: make([]*Certificate, len(s.certs)),
77+
byName: make(map[string][]int, len(s.byName)),
78+
lazyCerts: make([]lazyCert, len(s.lazyCerts)),
79+
haveSum: make(map[sum224]bool, len(s.haveSum)),
3180
}
3281
for k, v := range s.byName {
3382
indexes := make([]int, len(v))
3483
copy(indexes, v)
3584
p.byName[k] = indexes
3685
}
37-
copy(p.certs, s.certs)
86+
for k := range s.haveSum {
87+
p.haveSum[k] = true
88+
}
89+
copy(p.lazyCerts, s.lazyCerts)
3890
return p
3991
}
4092

@@ -64,7 +116,7 @@ func SystemCertPool() (*CertPool, error) {
64116

65117
// findPotentialParents returns the indexes of certificates in s which might
66118
// have signed cert.
67-
func (s *CertPool) findPotentialParents(cert *Certificate) []int {
119+
func (s *CertPool) findPotentialParents(cert *Certificate) []*Certificate {
68120
if s == nil {
69121
return nil
70122
}
@@ -75,41 +127,46 @@ func (s *CertPool) findPotentialParents(cert *Certificate) []int {
75127
// AKID and SKID match
76128
// AKID present, SKID missing / AKID missing, SKID present
77129
// AKID and SKID don't match
78-
var matchingKeyID, oneKeyID, mismatchKeyID []int
130+
var matchingKeyID, oneKeyID, mismatchKeyID []*Certificate
79131
for _, c := range s.byName[string(cert.RawIssuer)] {
80-
candidate := s.certs[c]
132+
candidate, err := s.cert(c)
133+
if err != nil {
134+
continue
135+
}
81136
kidMatch := bytes.Equal(candidate.SubjectKeyId, cert.AuthorityKeyId)
82137
switch {
83138
case kidMatch:
84-
matchingKeyID = append(matchingKeyID, c)
139+
matchingKeyID = append(matchingKeyID, candidate)
85140
case (len(candidate.SubjectKeyId) == 0 && len(cert.AuthorityKeyId) > 0) ||
86141
(len(candidate.SubjectKeyId) > 0 && len(cert.AuthorityKeyId) == 0):
87-
oneKeyID = append(oneKeyID, c)
142+
oneKeyID = append(oneKeyID, candidate)
88143
default:
89-
mismatchKeyID = append(mismatchKeyID, c)
144+
mismatchKeyID = append(mismatchKeyID, candidate)
90145
}
91146
}
92147

93148
found := len(matchingKeyID) + len(oneKeyID) + len(mismatchKeyID)
94149
if found == 0 {
95150
return nil
96151
}
97-
candidates := make([]int, 0, found)
152+
candidates := make([]*Certificate, 0, found)
98153
candidates = append(candidates, matchingKeyID...)
99154
candidates = append(candidates, oneKeyID...)
100155
candidates = append(candidates, mismatchKeyID...)
101-
102156
return candidates
103157
}
104158

105159
func (s *CertPool) contains(cert *Certificate) bool {
106160
if s == nil {
107161
return false
108162
}
109-
110163
candidates := s.byName[string(cert.RawSubject)]
111-
for _, c := range candidates {
112-
if s.certs[c].Equal(cert) {
164+
for _, i := range candidates {
165+
c, err := s.cert(i)
166+
if err != nil {
167+
return false
168+
}
169+
if c.Equal(cert) {
113170
return true
114171
}
115172
}
@@ -122,17 +179,32 @@ func (s *CertPool) AddCert(cert *Certificate) {
122179
if cert == nil {
123180
panic("adding nil Certificate to CertPool")
124181
}
182+
s.addCertFunc(sha256.Sum224(cert.Raw), string(cert.RawSubject), func() (*Certificate, error) {
183+
return cert, nil
184+
})
185+
}
186+
187+
// addCertFunc adds metadata about a certificate to a pool, along with
188+
// a func to fetch that certificate later when needed.
189+
//
190+
// The rawSubject is Certificate.RawSubject and must be non-empty.
191+
// The getCert func may be called 0 or more times.
192+
func (s *CertPool) addCertFunc(rawSum224 sum224, rawSubject string, getCert func() (*Certificate, error)) {
193+
if getCert == nil {
194+
panic("getCert can't be nil")
195+
}
125196

126197
// Check that the certificate isn't being added twice.
127-
if s.contains(cert) {
198+
if s.haveSum[rawSum224] {
128199
return
129200
}
130201

131-
n := len(s.certs)
132-
s.certs = append(s.certs, cert)
133-
134-
name := string(cert.RawSubject)
135-
s.byName[name] = append(s.byName[name], n)
202+
s.haveSum[rawSum224] = true
203+
s.lazyCerts = append(s.lazyCerts, lazyCert{
204+
rawSubject: []byte(rawSubject),
205+
getCert: getCert,
206+
})
207+
s.byName[rawSubject] = append(s.byName[rawSubject], len(s.lazyCerts)-1)
136208
}
137209

138210
// AppendCertsFromPEM attempts to parse a series of PEM encoded certificates.
@@ -167,9 +239,9 @@ func (s *CertPool) AppendCertsFromPEM(pemCerts []byte) (ok bool) {
167239
// Subjects returns a list of the DER-encoded subjects of
168240
// all of the certificates in the pool.
169241
func (s *CertPool) Subjects() [][]byte {
170-
res := make([][]byte, len(s.certs))
171-
for i, c := range s.certs {
172-
res[i] = c.RawSubject
242+
res := make([][]byte, s.len())
243+
for i, lc := range s.lazyCerts {
244+
res[i] = lc.rawSubject
173245
}
174246
return res
175247
}

src/crypto/x509/name_constraints_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1941,7 +1941,7 @@ func TestConstraintCases(t *testing.T) {
19411941
// Skip tests with CommonName set because OpenSSL will try to match it
19421942
// against name constraints, while we ignore it when it's not hostname-looking.
19431943
if !test.noOpenSSL && testNameConstraintsAgainstOpenSSL && test.leaf.cn == "" {
1944-
output, err := testChainAgainstOpenSSL(leafCert, intermediatePool, rootPool)
1944+
output, err := testChainAgainstOpenSSL(t, leafCert, intermediatePool, rootPool)
19451945
if err == nil && len(test.expectedError) > 0 {
19461946
t.Errorf("#%d: unexpectedly succeeded against OpenSSL", i)
19471947
if debugOpenSSLFailure {
@@ -1993,7 +1993,7 @@ func TestConstraintCases(t *testing.T) {
19931993
pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
19941994
return buf.String()
19951995
}
1996-
t.Errorf("#%d: root:\n%s", i, certAsPEM(rootPool.certs[0]))
1996+
t.Errorf("#%d: root:\n%s", i, certAsPEM(rootPool.mustCert(t, 0)))
19971997
t.Errorf("#%d: leaf:\n%s", i, certAsPEM(leafCert))
19981998
}
19991999

@@ -2019,19 +2019,19 @@ func writePEMsToTempFile(certs []*Certificate) *os.File {
20192019
return file
20202020
}
20212021

2022-
func testChainAgainstOpenSSL(leaf *Certificate, intermediates, roots *CertPool) (string, error) {
2022+
func testChainAgainstOpenSSL(t *testing.T, leaf *Certificate, intermediates, roots *CertPool) (string, error) {
20232023
args := []string{"verify", "-no_check_time"}
20242024

2025-
rootsFile := writePEMsToTempFile(roots.certs)
2025+
rootsFile := writePEMsToTempFile(allCerts(t, roots))
20262026
if debugOpenSSLFailure {
20272027
println("roots file:", rootsFile.Name())
20282028
} else {
20292029
defer os.Remove(rootsFile.Name())
20302030
}
20312031
args = append(args, "-CAfile", rootsFile.Name())
20322032

2033-
if len(intermediates.certs) > 0 {
2034-
intermediatesFile := writePEMsToTempFile(intermediates.certs)
2033+
if intermediates.len() > 0 {
2034+
intermediatesFile := writePEMsToTempFile(allCerts(t, intermediates))
20352035
if debugOpenSSLFailure {
20362036
println("intermediates file:", intermediatesFile.Name())
20372037
} else {

src/crypto/x509/root_cgo_darwin.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,11 @@ func _loadSystemRootsWithCgo() (*CertPool, error) {
313313
untrustedRoots.AppendCertsFromPEM(buf)
314314

315315
trustedRoots := NewCertPool()
316-
for _, c := range roots.certs {
316+
for _, lc := range roots.lazyCerts {
317+
c, err := lc.getCert()
318+
if err != nil {
319+
return nil, err
320+
}
317321
if !untrustedRoots.contains(c) {
318322
trustedRoots.AddCert(c)
319323
}

src/crypto/x509/root_darwin_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestSystemRoots(t *testing.T) {
2424

2525
// There are 174 system roots on Catalina, and 163 on iOS right now, require
2626
// at least 100 to make sure this is not completely broken.
27-
if want, have := 100, len(sysRoots.certs); have < want {
27+
if want, have := 100, sysRoots.len(); have < want {
2828
t.Errorf("want at least %d system roots, have %d", want, have)
2929
}
3030

@@ -43,11 +43,14 @@ func TestSystemRoots(t *testing.T) {
4343
t.Logf("loadSystemRootsWithCgo: %v", cgoSysRootsDuration)
4444

4545
// Check that the two cert pools are the same.
46-
sysPool := make(map[string]*Certificate, len(sysRoots.certs))
47-
for _, c := range sysRoots.certs {
46+
sysPool := make(map[string]*Certificate, sysRoots.len())
47+
for i := 0; i < sysRoots.len(); i++ {
48+
c := sysRoots.mustCert(t, i)
4849
sysPool[string(c.Raw)] = c
4950
}
50-
for _, c := range cgoRoots.certs {
51+
for i := 0; i < cgoRoots.len(); i++ {
52+
c := cgoRoots.mustCert(t, i)
53+
5154
if _, ok := sysPool[string(c.Raw)]; ok {
5255
delete(sysPool, string(c.Raw))
5356
} else {

src/crypto/x509/root_unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func loadSystemRoots() (*CertPool, error) {
7575
}
7676
}
7777

78-
if len(roots.certs) > 0 || firstErr == nil {
78+
if roots.len() > 0 || firstErr == nil {
7979
return roots, nil
8080
}
8181

src/crypto/x509/root_unix_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,15 @@ func TestEnvVars(t *testing.T) {
113113

114114
// Verify that the returned certs match, otherwise report where the mismatch is.
115115
for i, cn := range tc.cns {
116-
if i >= len(r.certs) {
116+
if i >= r.len() {
117117
t.Errorf("missing cert %v @ %v", cn, i)
118-
} else if r.certs[i].Subject.CommonName != cn {
119-
fmt.Printf("%#v\n", r.certs[0].Subject)
120-
t.Errorf("unexpected cert common name %q, want %q", r.certs[i].Subject.CommonName, cn)
118+
} else if r.mustCert(t, i).Subject.CommonName != cn {
119+
fmt.Printf("%#v\n", r.mustCert(t, 0).Subject)
120+
t.Errorf("unexpected cert common name %q, want %q", r.mustCert(t, i).Subject.CommonName, cn)
121121
}
122122
}
123-
if len(r.certs) > len(tc.cns) {
124-
t.Errorf("got %v certs, which is more than %v wanted", len(r.certs), len(tc.cns))
123+
if r.len() > len(tc.cns) {
124+
t.Errorf("got %v certs, which is more than %v wanted", r.len(), len(tc.cns))
125125
}
126126
})
127127
}
@@ -197,7 +197,8 @@ func TestLoadSystemCertsLoadColonSeparatedDirs(t *testing.T) {
197197
strCertPool := func(p *CertPool) string {
198198
return string(bytes.Join(p.Subjects(), []byte("\n")))
199199
}
200-
if !reflect.DeepEqual(gotPool, wantPool) {
200+
201+
if !certPoolEqual(gotPool, wantPool) {
201202
g, w := strCertPool(gotPool), strCertPool(wantPool)
202203
t.Fatalf("Mismatched certPools\nGot:\n%s\n\nWant:\n%s", g, w)
203204
}

src/crypto/x509/root_windows.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ func createStoreContext(leaf *Certificate, opts *VerifyOptions) (*syscall.CertCo
3838
}
3939

4040
if opts.Intermediates != nil {
41-
for _, intermediate := range opts.Intermediates.certs {
41+
for i := 0; i < opts.Intermediates.len(); i++ {
42+
intermediate, err := opts.Intermediates.cert(i)
43+
if err != nil {
44+
return nil, err
45+
}
4246
ctx, err := syscall.CertCreateCertificateContext(syscall.X509_ASN_ENCODING|syscall.PKCS_7_ASN_ENCODING, &intermediate.Raw[0], uint32(len(intermediate.Raw)))
4347
if err != nil {
4448
return nil, err

src/crypto/x509/verify.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -761,11 +761,13 @@ func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err e
761761
if len(c.Raw) == 0 {
762762
return nil, errNotParsed
763763
}
764-
if opts.Intermediates != nil {
765-
for _, intermediate := range opts.Intermediates.certs {
766-
if len(intermediate.Raw) == 0 {
767-
return nil, errNotParsed
768-
}
764+
for i := 0; i < opts.Intermediates.len(); i++ {
765+
c, err := opts.Intermediates.cert(i)
766+
if err != nil {
767+
return nil, fmt.Errorf("crypto/x509: error fetching intermediate: %w", err)
768+
}
769+
if len(c.Raw) == 0 {
770+
return nil, errNotParsed
769771
}
770772
}
771773

@@ -891,11 +893,11 @@ func (c *Certificate) buildChains(cache map[*Certificate][][]*Certificate, curre
891893
}
892894
}
893895

894-
for _, rootNum := range opts.Roots.findPotentialParents(c) {
895-
considerCandidate(rootCertificate, opts.Roots.certs[rootNum])
896+
for _, root := range opts.Roots.findPotentialParents(c) {
897+
considerCandidate(rootCertificate, root)
896898
}
897-
for _, intermediateNum := range opts.Intermediates.findPotentialParents(c) {
898-
considerCandidate(intermediateCertificate, opts.Intermediates.certs[intermediateNum])
899+
for _, intermediate := range opts.Intermediates.findPotentialParents(c) {
900+
considerCandidate(intermediateCertificate, intermediate)
899901
}
900902

901903
if len(chains) > 0 {

0 commit comments

Comments
 (0)