1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
|
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"fmt"
weakrand "math/rand/v2"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
// Cache is a structure that stores certificates in memory.
// A Cache indexes certificates by name for quick access
// during TLS handshakes, and avoids duplicating certificates
// in memory. Generally, there should only be one per process.
// However, that is not a strict requirement; but using more
// than one is a code smell, and may indicate an
// over-engineered design.
//
// An empty cache is INVALID and must not be used. Be sure
// to call NewCache to get a valid value.
//
// These should be very long-lived values and must not be
// copied. Before all references leave scope to be garbage
// collected, ensure you call Stop() to stop maintenance on
// the certificates stored in this cache and release locks.
//
// Caches are not usually manipulated directly; create a
// Config value with a pointer to a Cache, and then use
// the Config to interact with the cache. Caches are
// agnostic of any particular storage or ACME config,
// since each certificate may be managed and stored
// differently.
type Cache struct {
// User configuration of the cache
options CacheOptions
optionsMu sync.RWMutex
// The cache is keyed by certificate hash
cache map[string]Certificate
// cacheIndex is a map of SAN to cache key (cert hash)
cacheIndex map[string][]string
// Protects the cache and cacheIndex maps
mu sync.RWMutex
// Close this channel to cancel asset maintenance
stopChan chan struct{}
// Used to signal when stopping is completed
doneChan chan struct{}
logger *zap.Logger
}
// NewCache returns a new, valid Cache for efficiently
// accessing certificates in memory. It also begins a
// maintenance goroutine to tend to the certificates
// in the cache. Call Stop() when you are done with the
// cache so it can clean up locks and stuff.
//
// Most users of this package will not need to call this
// because a default certificate cache is created for you.
// Only advanced use cases require creating a new cache.
//
// This function panics if opts.GetConfigForCert is not
// set. The reason is that a cache absolutely needs to
// be able to get a Config with which to manage TLS
// assets, and it is not safe to assume that the Default
// config is always the correct one, since you have
// created the cache yourself.
//
// See the godoc for Cache to use it properly. When
// no longer needed, caches should be stopped with
// Stop() to clean up resources even if the process
// is being terminated, so that it can clean up
// any locks for other processes to unblock!
func NewCache(opts CacheOptions) *Cache {
c := &Cache{
cache: make(map[string]Certificate),
cacheIndex: make(map[string][]string),
stopChan: make(chan struct{}),
doneChan: make(chan struct{}),
logger: opts.Logger,
}
// absolutely do not allow a nil logger; panics galore
if c.logger == nil {
c.logger = defaultLogger
}
c.SetOptions(opts)
go c.maintainAssets(0)
return c
}
func (certCache *Cache) SetOptions(opts CacheOptions) {
// assume default options if necessary
if opts.OCSPCheckInterval <= 0 {
opts.OCSPCheckInterval = DefaultOCSPCheckInterval
}
if opts.RenewCheckInterval <= 0 {
opts.RenewCheckInterval = DefaultRenewCheckInterval
}
if opts.Capacity < 0 {
opts.Capacity = 0
}
// this must be set, because we cannot not
// safely assume that the Default Config
// is always the correct one to use
if opts.GetConfigForCert == nil {
panic("cache must be initialized with a GetConfigForCert callback")
}
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
// stopped, it cannot be reused.
func (certCache *Cache) Stop() {
close(certCache.stopChan) // signal to stop
<-certCache.doneChan // wait for stop to complete
}
// CacheOptions is used to configure certificate caches.
// Once a cache has been created with certain options,
// those settings cannot be changed.
type CacheOptions struct {
// REQUIRED. A function that returns a configuration
// used for managing a certificate, or for accessing
// that certificate's asset storage (e.g. for
// OCSP staples, etc). The returned Config MUST
// be associated with the same Cache as the caller,
// use New to obtain a valid Config.
//
// The reason this is a callback function, dynamically
// returning a Config (instead of attaching a static
// pointer to a Config on each certificate) is because
// the config for how to manage a domain's certificate
// might change from maintenance to maintenance. The
// cache is so long-lived, we cannot assume that the
// host's situation will always be the same; e.g. the
// certificate might switch DNS providers, so the DNS
// challenge (if used) would need to be adjusted from
// the last time it was run ~8 weeks ago.
GetConfigForCert ConfigGetter
// How often to check certificates for renewal;
// if unset, DefaultOCSPCheckInterval will be used.
OCSPCheckInterval time.Duration
// How often to check certificates for renewal;
// if unset, DefaultRenewCheckInterval will be used.
RenewCheckInterval time.Duration
// Maximum number of certificates to allow in the cache.
// If reached, certificates will be randomly evicted to
// make room for new ones. 0 means unlimited.
Capacity int
// Set a logger to enable logging
Logger *zap.Logger
}
// ConfigGetter is a function that returns a prepared,
// valid config that should be used when managing the
// given certificate or its assets.
type ConfigGetter func(Certificate) (*Config, error)
// cacheCertificate calls unsyncedCacheCertificate with a write lock.
//
// This function is safe for concurrent use.
func (certCache *Cache) cacheCertificate(cert Certificate) {
certCache.mu.Lock()
certCache.unsyncedCacheCertificate(cert)
certCache.mu.Unlock()
}
// unsyncedCacheCertificate adds cert to the in-memory cache unless
// it already exists in the cache (according to cert.Hash). It
// updates the name index.
//
// This function is NOT safe for concurrent use. Callers MUST acquire
// a write lock on certCache.mu first.
func (certCache *Cache) unsyncedCacheCertificate(cert Certificate) {
// if this certificate already exists in the cache, this is basically
// a no-op so we reuse existing cert (prevent duplication), but we do
// modify the cert to add tags it may be missing (see issue #211)
if existingCert, ok := certCache.cache[cert.hash]; ok {
logMsg := "certificate already cached"
if len(cert.Tags) > 0 {
for _, tag := range cert.Tags {
if !existingCert.HasTag(tag) {
existingCert.Tags = append(existingCert.Tags, tag)
}
}
certCache.cache[cert.hash] = existingCert
logMsg += "; appended any missing tags to cert"
}
certCache.logger.Debug(logMsg,
zap.Strings("subjects", cert.Names),
zap.Time("expiration", expiresAt(cert.Leaf)),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
zap.String("hash", cert.hash),
zap.Strings("tags", cert.Tags))
return
}
// if the cache is at capacity, make room for new cert
cacheSize := len(certCache.cache)
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
// strategy; generating random numbers is cheap and
// ensures a much better distribution.
rnd := weakrand.IntN(cacheSize)
i := 0
for _, randomCert := range certCache.cache {
if i >= rnd && randomCert.managed { // don't evict manually-loaded certs
certCache.logger.Debug("cache full; evicting random certificate",
zap.Strings("removing_subjects", randomCert.Names),
zap.String("removing_hash", randomCert.hash),
zap.Strings("inserting_subjects", cert.Names),
zap.String("inserting_hash", cert.hash))
certCache.removeCertificate(randomCert)
break
}
i++
}
}
// store the certificate
certCache.cache[cert.hash] = cert
// update the index so we can access it by name
for _, name := range cert.Names {
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)),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
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.
//
// This function is NOT safe for concurrent use; callers
// MUST first acquire a write lock on certCache.mu.
func (certCache *Cache) removeCertificate(cert Certificate) {
// delete all mentions of this cert from the name index
for _, name := range cert.Names {
keyList := certCache.cacheIndex[name]
for i := 0; i < len(keyList); i++ {
if keyList[i] == cert.hash {
keyList = append(keyList[:i], keyList[i+1:]...)
i--
}
}
if len(keyList) == 0 {
delete(certCache.cacheIndex, name)
} else {
certCache.cacheIndex[name] = keyList
}
}
// 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)),
zap.Bool("managed", cert.managed),
zap.String("issuer_key", cert.issuerKey),
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
// the cache.
//
// This method is safe for concurrent use.
func (certCache *Cache) replaceCertificate(oldCert, newCert Certificate) {
certCache.mu.Lock()
certCache.removeCertificate(oldCert)
certCache.unsyncedCacheCertificate(newCert)
certCache.mu.Unlock()
certCache.logger.Info("replaced certificate in cache",
zap.Strings("subjects", newCert.Names),
zap.Time("new_expiration", expiresAt(newCert.Leaf)))
}
// 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[subject]
certs := make([]Certificate, len(allCertKeys))
for i := range allCertKeys {
certs[i] = certCache.cache[allCertKeys[i]]
}
return certs
}
func (certCache *Cache) getAllCerts() []Certificate {
certCache.mu.RLock()
defer certCache.mu.RUnlock()
certs := make([]Certificate, 0, len(certCache.cache))
for _, cert := range certCache.cache {
certs = append(certs, cert)
}
return certs
}
func (certCache *Cache) getConfig(cert Certificate) (*Config, error) {
certCache.optionsMu.RLock()
getCert := certCache.options.GetConfigForCert
certCache.optionsMu.RUnlock()
cfg, err := getCert(cert)
if err != nil {
return nil, err
}
if cfg == nil {
// this is bad if this happens, probably a programmer error (oops)
return nil, fmt.Errorf("no configuration associated with certificate: %v;", cert.Names)
}
if cfg.certCache == nil {
return nil, fmt.Errorf("config returned for certificate %v has nil cache; expected %p (this one)",
cert.Names, certCache)
}
if cfg.certCache != certCache {
return nil, fmt.Errorf("config returned for certificate %v is not nil and points to different cache; got %p, expected %p (this one)",
cert.Names, cfg.certCache, certCache)
}
return cfg, nil
}
// AllMatchingCertificates returns a list of all certificates that could
// be used to serve the given SNI name, including exact SAN matches and
// wildcard matches.
func (certCache *Cache) AllMatchingCertificates(name string) []Certificate {
// get exact matches first
certs := certCache.getAllMatchingCerts(name)
// then look for wildcard matches by replacing each
// label of the domain name with wildcards
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
certs = append(certs, certCache.getAllMatchingCerts(candidate)...)
}
return certs
}
// SubjectIssuer pairs a subject name with an issuer ID/key.
type SubjectIssuer struct {
Subject, IssuerKey string
}
// RemoveManaged removes managed certificates for the given subjects from the cache.
// This effectively stops maintenance of those certificates. If an IssuerKey is
// specified alongside the subject, only certificates for that subject from the
// specified issuer will be removed.
func (certCache *Cache) RemoveManaged(subjects []SubjectIssuer) {
deleteQueue := make([]string, 0, len(subjects))
for _, subj := range subjects {
certs := certCache.getAllMatchingCerts(subj.Subject) // does NOT expand wildcards; exact matches only
for _, cert := range certs {
if !cert.managed {
continue
}
if subj.IssuerKey == "" || cert.issuerKey == subj.IssuerKey {
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
)
|