diff --git a/cache.go b/cache.go index a28021c..152fe18 100644 --- a/cache.go +++ b/cache.go @@ -48,7 +48,8 @@ import ( // differently. type Cache struct { // User configuration of the cache - options CacheOptions + 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 diff --git a/cache_test.go b/cache_test.go index 7d95ba6..87d2e49 100644 --- a/cache_test.go +++ b/cache_test.go @@ -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) } diff --git a/certificates.go b/certificates.go index 9e98340..0719d04 100644 --- a/certificates.go +++ b/certificates.go @@ -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 diff --git a/config.go b/config.go index 40e6c6b..22a2272 100644 --- a/config.go +++ b/config.go @@ -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. diff --git a/crypto.go b/crypto.go index 7a11964..5855ad7 100644 --- a/crypto.go +++ b/crypto.go @@ -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) } diff --git a/go.mod b/go.mod index 4747343..6600f29 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8f2371d..d54c776 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handshake.go b/handshake.go index 68165aa..a3d714e 100644 --- a/handshake.go +++ b/handshake.go @@ -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 diff --git a/maintain.go b/maintain.go index eb340f1..475bb6d 100644 --- a/maintain.go +++ b/maintain.go @@ -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")