Make cache options updateable; new remove methods

These are useful for advanced applications (like Caddy) which would
like to remove certificates from the
cache in a controlled way, and operate the
cache with new settings while running.
This commit is contained in:
Matthew Holt 2023-07-08 09:56:51 -06:00
parent d8b13df4d1
commit 93a28b732a
No known key found for this signature in database
GPG Key ID: 2A349DD577D586A5
9 changed files with 91 additions and 43 deletions

View File

@ -49,6 +49,7 @@ import (
type Cache struct {
// User configuration of the cache
options CacheOptions
optionsMu sync.RWMutex
// The cache is keyed by certificate hash
cache map[string]Certificate
@ -56,7 +57,7 @@ type Cache struct {
// cacheIndex is a map of SAN to cache key (cert hash)
cacheIndex map[string][]string
// Protects the cache and index maps
// Protects the cache and cacheIndex maps
mu sync.RWMutex
// Close this channel to cancel asset maintenance
@ -128,6 +129,12 @@ func NewCache(opts CacheOptions) *Cache {
return c
}
func (certCache *Cache) SetOptions(opts CacheOptions) {
certCache.optionsMu.Lock()
certCache.options = opts
certCache.optionsMu.Unlock()
}
// Stop stops the maintenance goroutine for
// certificates in certCache. It blocks until
// stopping is complete. Once a cache is
@ -226,7 +233,11 @@ func (certCache *Cache) unsyncedCacheCertificate(cert Certificate) {
// if the cache is at capacity, make room for new cert
cacheSize := len(certCache.cache)
if certCache.options.Capacity > 0 && cacheSize >= certCache.options.Capacity {
certCache.optionsMu.RLock()
atCapacity := certCache.options.Capacity > 0 && cacheSize >= certCache.options.Capacity
certCache.optionsMu.RUnlock()
if atCapacity {
// Go maps are "nondeterministic" but not actually random,
// so although we could just chop off the "front" of the
// map with less code, that is a heavily skewed eviction
@ -256,6 +267,7 @@ func (certCache *Cache) unsyncedCacheCertificate(cert Certificate) {
certCache.cacheIndex[name] = append(certCache.cacheIndex[name], cert.hash)
}
certCache.optionsMu.RLock()
certCache.logger.Debug("added certificate to cache",
zap.Strings("subjects", cert.Names),
zap.Time("expiration", expiresAt(cert.Leaf)),
@ -264,6 +276,7 @@ func (certCache *Cache) unsyncedCacheCertificate(cert Certificate) {
zap.String("hash", cert.hash),
zap.Int("cache_size", len(certCache.cache)),
zap.Int("cache_capacity", certCache.options.Capacity))
certCache.optionsMu.RUnlock()
}
// removeCertificate removes cert from the cache.
@ -290,6 +303,7 @@ func (certCache *Cache) removeCertificate(cert Certificate) {
// delete the actual cert from the cache
delete(certCache.cache, cert.hash)
certCache.optionsMu.RLock()
certCache.logger.Debug("removed certificate from cache",
zap.Strings("subjects", cert.Names),
zap.Time("expiration", expiresAt(cert.Leaf)),
@ -298,6 +312,7 @@ func (certCache *Cache) removeCertificate(cert Certificate) {
zap.String("hash", cert.hash),
zap.Int("cache_size", len(certCache.cache)),
zap.Int("cache_capacity", certCache.options.Capacity))
certCache.optionsMu.RUnlock()
}
// replaceCertificate atomically replaces oldCert with newCert in
@ -314,11 +329,13 @@ func (certCache *Cache) replaceCertificate(oldCert, newCert Certificate) {
zap.Time("new_expiration", expiresAt(newCert.Leaf)))
}
func (certCache *Cache) getAllMatchingCerts(name string) []Certificate {
// getAllMatchingCerts returns all certificates with exactly this subject
// (wildcards are NOT expanded).
func (certCache *Cache) getAllMatchingCerts(subject string) []Certificate {
certCache.mu.RLock()
defer certCache.mu.RUnlock()
allCertKeys := certCache.cacheIndex[name]
allCertKeys := certCache.cacheIndex[subject]
certs := make([]Certificate, len(allCertKeys))
for i := range allCertKeys {
@ -339,7 +356,11 @@ func (certCache *Cache) getAllCerts() []Certificate {
}
func (certCache *Cache) getConfig(cert Certificate) (*Config, error) {
cfg, err := certCache.options.GetConfigForCert(cert)
certCache.optionsMu.RLock()
getCert := certCache.options.GetConfigForCert
certCache.optionsMu.RUnlock()
cfg, err := getCert(cert)
if err != nil {
return nil, err
}
@ -373,6 +394,33 @@ func (certCache *Cache) AllMatchingCertificates(name string) []Certificate {
return certs
}
// RemoveManaged removes managed certificates for the given subjects from the cache.
// This effectively stops maintenance of those certificates.
func (certCache *Cache) RemoveManaged(subjects []string) {
deleteQueue := make([]string, 0, len(subjects))
for _, subject := range subjects {
certs := certCache.getAllMatchingCerts(subject) // does NOT expand wildcards; exact matches only
for _, cert := range certs {
if !cert.managed {
continue
}
deleteQueue = append(deleteQueue, cert.hash)
}
}
certCache.Remove(deleteQueue)
}
// Remove removes certificates with the given hashes from the cache.
// This is effectively used to unload manually-loaded certificates.
func (certCache *Cache) Remove(hashes []string) {
certCache.mu.Lock()
for _, h := range hashes {
cert := certCache.cache[h]
certCache.removeCertificate(cert)
}
certCache.mu.Unlock()
}
var (
defaultCache *Cache
defaultCacheMu sync.Mutex

View File

@ -21,6 +21,9 @@ func TestNewCache(t *testing.T) {
c := NewCache(CacheOptions{GetConfigForCert: noop})
defer c.Stop()
c.optionsMu.RLock()
defer c.optionsMu.RUnlock()
if c.options.RenewCheckInterval != DefaultRenewCheckInterval {
t.Errorf("Expected RenewCheckInterval to be set to default value, but it wasn't: %s", c.options.RenewCheckInterval)
}

View File

@ -155,29 +155,32 @@ func (cfg *Config) loadManagedCertificate(ctx context.Context, domain string) (C
// CacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
// and keyFile, which must be in PEM format. It stores the certificate in
// the in-memory cache.
// the in-memory cache and returns the hash, useful for removing from the cache.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedCertificatePEMFile(ctx context.Context, certFile, keyFile string, tags []string) error {
func (cfg *Config) CacheUnmanagedCertificatePEMFile(ctx context.Context, certFile, keyFile string, tags []string) (string, error) {
cert, err := cfg.makeCertificateFromDiskWithOCSP(ctx, cfg.Storage, certFile, keyFile)
if err != nil {
return err
return "", err
}
cert.Tags = tags
cfg.certCache.cacheCertificate(cert)
cfg.emit(ctx, "cached_unmanaged_cert", map[string]any{"sans": cert.Names})
return nil
return cert.hash, nil
}
// CacheUnmanagedTLSCertificate adds tlsCert to the certificate cache.
// CacheUnmanagedTLSCertificate adds tlsCert to the certificate cache
//
// and returns the hash, useful for removing from the cache.
//
// It staples OCSP if possible.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedTLSCertificate(ctx context.Context, tlsCert tls.Certificate, tags []string) error {
func (cfg *Config) CacheUnmanagedTLSCertificate(ctx context.Context, tlsCert tls.Certificate, tags []string) (string, error) {
var cert Certificate
err := fillCertFromLeaf(&cert, tlsCert)
if err != nil {
return err
return "", err
}
err = stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, nil)
if err != nil {
@ -186,22 +189,23 @@ func (cfg *Config) CacheUnmanagedTLSCertificate(ctx context.Context, tlsCert tls
cfg.emit(ctx, "cached_unmanaged_cert", map[string]any{"sans": cert.Names})
cert.Tags = tags
cfg.certCache.cacheCertificate(cert)
return nil
return cert.hash, nil
}
// CacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
// of the certificate and key, then caches it in memory.
// of the certificate and key, then caches it in memory, and returns the hash,
// which is useful for removing from the cache.
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedCertificatePEMBytes(ctx context.Context, certBytes, keyBytes []byte, tags []string) error {
func (cfg *Config) CacheUnmanagedCertificatePEMBytes(ctx context.Context, certBytes, keyBytes []byte, tags []string) (string, error) {
cert, err := cfg.makeCertificateWithOCSP(ctx, certBytes, keyBytes)
if err != nil {
return err
return "", err
}
cert.Tags = tags
cfg.certCache.cacheCertificate(cert)
cfg.emit(ctx, "cached_unmanaged_cert", map[string]any{"sans": cert.Names})
return nil
return cert.hash, nil
}
// makeCertificateFromDiskWithOCSP makes a Certificate by loading the

View File

@ -209,7 +209,10 @@ func New(certCache *Cache, cfg Config) *Config {
if certCache == nil {
panic("a certificate cache is required")
}
if certCache.options.GetConfigForCert == nil {
certCache.optionsMu.RLock()
getConfigForCert := certCache.options.GetConfigForCert
defer certCache.optionsMu.RUnlock()
if getConfigForCert == nil {
panic("cache must have GetConfigForCert set in its options")
}
return newWithCache(certCache, cfg)
@ -450,28 +453,6 @@ func (cfg *Config) manageOne(ctx context.Context, domainName string, async bool)
return renew()
}
// Unmanage causes the certificates for domainNames to stop being managed.
// If there are certificates for the supplied domain names in the cache, they
// are evicted from the cache.
func (cfg *Config) Unmanage(domainNames []string) {
var deleteQueue []Certificate
for _, domainName := range domainNames {
certs := cfg.certCache.AllMatchingCertificates(domainName)
for _, cert := range certs {
if !cert.managed {
continue
}
deleteQueue = append(deleteQueue, cert)
}
}
cfg.certCache.mu.Lock()
for _, cert := range deleteQueue {
cfg.certCache.removeCertificate(cert)
}
cfg.certCache.mu.Unlock()
}
// ObtainCertSync generates a new private key and obtains a certificate for
// name using cfg in the foreground; i.e. interactively and without retries.
// It stows the renewed certificate and its assets in storage if successful.

View File

@ -22,7 +22,6 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
@ -35,6 +34,7 @@ import (
"strings"
"github.com/klauspost/cpuid/v2"
"github.com/zeebo/blake3"
"go.uber.org/zap"
"golang.org/x/net/idna"
)
@ -271,7 +271,7 @@ func (cfg *Config) loadCertResource(ctx context.Context, issuer Issuer, certName
// which is the chain of DER-encoded bytes. It returns the
// hex encoding of the hash.
func hashCertificateChain(certChain [][]byte) string {
h := sha256.New()
h := blake3.New()
for _, certInChain := range certChain {
h.Write(certInChain)
}

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/libdns/libdns v0.2.1
github.com/mholt/acmez v1.2.0
github.com/miekg/dns v1.1.55
github.com/zeebo/blake3 v0.2.3
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.10.0
golang.org/x/net v0.11.0

7
go.sum
View File

@ -1,5 +1,6 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
@ -11,6 +12,12 @@ github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=

View File

@ -341,7 +341,9 @@ func (cfg *Config) getCertDuringHandshake(ctx context.Context, hello *tls.Client
// perfectly full while still being able to load needed certs from storage.
// See https://caddy.community/t/error-tls-alert-internal-error-592-again/13272
// and caddyserver/caddy#4320.
cfg.certCache.optionsMu.RLock()
cacheCapacity := float64(cfg.certCache.options.Capacity)
cfg.certCache.optionsMu.RUnlock()
cacheAlmostFull := cacheCapacity > 0 && float64(cacheSize) >= cacheCapacity*.9
loadDynamically := cfg.OnDemand != nil || cacheAlmostFull

View File

@ -53,8 +53,10 @@ func (certCache *Cache) maintainAssets(panicCount int) {
}
}()
certCache.optionsMu.RLock()
renewalTicker := time.NewTicker(certCache.options.RenewCheckInterval)
ocspTicker := time.NewTicker(certCache.options.OCSPCheckInterval)
certCache.optionsMu.RUnlock()
log.Info("started background certificate maintenance")