Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
326 lines
10 KiB
326 lines
10 KiB
// 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" // seeded elsewhere |
|
"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 |
|
|
|
// 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 index 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 { |
|
// 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") |
|
} |
|
|
|
c := &Cache{ |
|
options: opts, |
|
cache: make(map[string]Certificate), |
|
cacheIndex: make(map[string][]string), |
|
stopChan: make(chan struct{}), |
|
doneChan: make(chan struct{}), |
|
logger: opts.Logger, |
|
} |
|
|
|
go c.maintainAssets(0) |
|
|
|
return c |
|
} |
|
|
|
// 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. |
|
// |
|
// 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) { |
|
// no-op if this certificate already exists in the cache |
|
if _, ok := certCache.cache[cert.hash]; ok { |
|
return |
|
} |
|
|
|
// if the cache is at capacity, make room for new cert |
|
cacheSize := len(certCache.cache) |
|
if certCache.options.Capacity > 0 && cacheSize >= certCache.options.Capacity { |
|
// 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 { |
|
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) |
|
} |
|
} |
|
|
|
// 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, cacheKey := range keyList { |
|
if cacheKey == cert.hash { |
|
keyList = append(keyList[:i], keyList[i+1:]...) |
|
} |
|
} |
|
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) |
|
} |
|
|
|
// 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() |
|
if certCache.logger != nil { |
|
certCache.logger.Info("replaced certificate in cache", |
|
zap.Strings("identifiers", newCert.Names), |
|
zap.Time("new_expiration", newCert.Leaf.NotAfter)) |
|
} |
|
} |
|
|
|
func (certCache *Cache) getAllMatchingCerts(name string) []Certificate { |
|
certCache.mu.RLock() |
|
defer certCache.mu.RUnlock() |
|
|
|
allCertKeys := certCache.cacheIndex[name] |
|
|
|
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) { |
|
cfg, err := certCache.options.GetConfigForCert(cert) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if cfg.certCache != nil && 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 |
|
} |
|
|
|
var ( |
|
defaultCache *Cache |
|
defaultCacheMu sync.Mutex |
|
)
|
|
|