crypto/x509: keep smaller root cert representation in memory until needed

(from patchset 1, c12c890c64dd6372b3893af1e6f5ab11802c9e81, of
https://go-review.googlesource.com/c/go/+/230025/1, with merges fixes
due to parent commit's differents from its ps1..ps3)

Instead of parsing the PEM files and then storing the *Certificate
values forever, still parse them to see if they're valid and pick out
some fields, but then only store the decoded pem.Block.Bytes until
that cert is first needed.

Saves about 500K of memory on my (Debian stable) machine after doing a
tls.Dial or calling x509.SystemCertPool.

A more aggressive version of this is still possible: we can not keep
the pem.Block.Bytes in memory either, and re-read them from disk when
necessary. But dealing with files disappearing and even large
multi-cert PEM files changing (with offsets sliding around) made this
conservative version attractive. It doesn't change the
slurp-roots-on-startup semantics. It just does so with less memory
retained.

Change-Id: I3aea333f4749ae3b0026042ec3ff7ac015c72204
reviewable/pr331/r1
Brad Fitzpatrick 5 years ago
parent f5993f2440
commit bfc1261ab6

@ -5,15 +5,26 @@
package x509 package x509
import ( import (
"crypto/sha256"
"encoding/pem" "encoding/pem"
"errors" "errors"
"runtime" "runtime"
"sync"
) )
type sum224 [sha256.Size224]byte
// CertPool is a set of certificates. // CertPool is a set of certificates.
type CertPool struct { type CertPool struct {
bySubjectKeyId map[string][]int // cert.SubjectKeyId => getCert index bySubjectKeyId map[string][]int // cert.SubjectKeyId => getCert index(es)
byName map[string][]int // cert.RawSubject => getCert index byName map[string][]int // cert.RawSubject => getCert index(es)
// haveSum maps from sum224(cert.Raw) to true. It's used only
// for AddCert duplicate detection, to avoid CertPool.contains
// calls in the AddCert path (because the contains method can
// call getCert and otherwise negate savings from lazy getCert
// funcs).
haveSum map[sum224]bool
// getCert contains funcs that return the certificates. // getCert contains funcs that return the certificates.
getCert []func() (*Certificate, error) getCert []func() (*Certificate, error)
@ -28,6 +39,7 @@ func NewCertPool() *CertPool {
return &CertPool{ return &CertPool{
bySubjectKeyId: make(map[string][]int), bySubjectKeyId: make(map[string][]int),
byName: make(map[string][]int), byName: make(map[string][]int),
haveSum: make(map[sum224]bool),
} }
} }
@ -49,6 +61,7 @@ func (s *CertPool) copy() *CertPool {
p := &CertPool{ p := &CertPool{
bySubjectKeyId: make(map[string][]int, len(s.bySubjectKeyId)), bySubjectKeyId: make(map[string][]int, len(s.bySubjectKeyId)),
byName: make(map[string][]int, len(s.byName)), byName: make(map[string][]int, len(s.byName)),
haveSum: make(map[sum224]bool, len(s.haveSum)),
getCert: make([]func() (*Certificate, error), len(s.getCert)), getCert: make([]func() (*Certificate, error), len(s.getCert)),
rawSubjects: make([][]byte, len(s.rawSubjects)), rawSubjects: make([][]byte, len(s.rawSubjects)),
} }
@ -62,6 +75,9 @@ func (s *CertPool) copy() *CertPool {
copy(indexes, v) copy(indexes, v)
p.byName[k] = indexes p.byName[k] = indexes
} }
for k := range s.haveSum {
p.haveSum[k] = true
}
copy(p.getCert, s.getCert) copy(p.getCert, s.getCert)
copy(p.rawSubjects, s.rawSubjects) copy(p.rawSubjects, s.rawSubjects)
return p return p
@ -127,7 +143,7 @@ func (s *CertPool) AddCert(cert *Certificate) {
if cert == nil { if cert == nil {
panic("adding nil Certificate to CertPool") panic("adding nil Certificate to CertPool")
} }
err := s.AddCertFunc(string(cert.RawSubject), string(cert.SubjectKeyId), func() (*Certificate, error) { err := s.AddCertFunc(sha256.Sum224(cert.Raw), string(cert.RawSubject), string(cert.SubjectKeyId), func() (*Certificate, error) {
return cert, nil return cert, nil
}) })
if err != nil { if err != nil {
@ -141,23 +157,16 @@ func (s *CertPool) AddCert(cert *Certificate) {
// The rawSubject is Certificate.RawSubject and must be non-empty. // The rawSubject is Certificate.RawSubject and must be non-empty.
// The subjectKeyID is Certificate.SubjectKeyId and may be empty. // The subjectKeyID is Certificate.SubjectKeyId and may be empty.
// The getCert func may be called 0 or more times. // The getCert func may be called 0 or more times.
func (s *CertPool) AddCertFunc(rawSubject, subjectKeyID string, getCert func() (*Certificate, error)) error { func (s *CertPool) AddCertFunc(rawSum224 sum224, rawSubject, subjectKeyID string, getCert func() (*Certificate, error)) error {
if getCert == nil { if getCert == nil {
panic("getCert can't be nil") panic("getCert can't be nil")
} }
// Check that the certificate isn't being added twice. // Check that the certificate isn't being added twice.
if len(s.byName[rawSubject]) > 0 { if s.haveSum[rawSum224] {
c, err := getCert()
if err != nil {
return err
}
if dup, err := s.contains(c); dup {
return nil return nil
} else if err != nil {
return err
}
} }
s.haveSum[rawSum224] = true
n := len(s.getCert) n := len(s.getCert)
s.getCert = append(s.getCert, getCert) s.getCert = append(s.getCert, getCert)
@ -187,16 +196,26 @@ func (s *CertPool) AppendCertsFromPEM(pemCerts []byte) (ok bool) {
continue continue
} }
cert, err := ParseCertificate(block.Bytes) certBytes := block.Bytes
cert, err := ParseCertificate(certBytes)
if err != nil { if err != nil {
continue continue
} }
var lazyCert struct {
s.AddCert(cert) sync.Once
v *Certificate
}
s.AddCertFunc(sha256.Sum224(cert.Raw), string(cert.RawSubject), string(cert.SubjectKeyId), func() (*Certificate, error) {
lazyCert.Do(func() {
// This can't fail, as the same bytes already parsed above.
lazyCert.v, _ = ParseCertificate(certBytes)
certBytes = nil
})
return lazyCert.v, nil
})
ok = true ok = true
} }
return ok
return
} }
// Subjects returns a list of the DER-encoded subjects of // Subjects returns a list of the DER-encoded subjects of

Loading…
Cancel
Save