mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
46ae4a6652
Renewed certificates would not be reloaded into the cache because their names conflict with names of certificates already in the cache; this was intentional when loading new certs to avoid confusion, but is problematic when renewing, since the old certificate doesn't get evicted from the cache. (Oops.) Here, I remedy this situation by explicitly deleting the old cert from the cache before adding the renewed one back in.
281 lines
8.4 KiB
Go
281 lines
8.4 KiB
Go
package caddytls
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"io/ioutil"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ocsp"
|
|
)
|
|
|
|
// certCache stores certificates in memory,
|
|
// keying certificates by name. Certificates
|
|
// should not overlap in the names they serve,
|
|
// because a name only maps to one certificate.
|
|
var certCache = make(map[string]Certificate)
|
|
var certCacheMu sync.RWMutex
|
|
|
|
// Certificate is a tls.Certificate with associated metadata tacked on.
|
|
// Even if the metadata can be obtained by parsing the certificate,
|
|
// we can be more efficient by extracting the metadata once so it's
|
|
// just there, ready to use.
|
|
type Certificate struct {
|
|
tls.Certificate
|
|
|
|
// Names is the list of names this certificate is written for.
|
|
// The first is the CommonName (if any), the rest are SAN.
|
|
// This should be the exact list of keys by which this cert
|
|
// is accessed in the cache, careful to avoid overlap.
|
|
Names []string
|
|
|
|
// NotAfter is when the certificate expires.
|
|
NotAfter time.Time
|
|
|
|
// OCSP contains the certificate's parsed OCSP response.
|
|
OCSP *ocsp.Response
|
|
|
|
// Config is the configuration with which the certificate was
|
|
// loaded or obtained and with which it should be maintained.
|
|
Config *Config
|
|
}
|
|
|
|
// getCertificate gets a certificate that matches name (a server name)
|
|
// from the in-memory cache. If there is no exact match for name, it
|
|
// will be checked against names of the form '*.example.com' (wildcard
|
|
// certificates) according to RFC 6125. If a match is found, matched will
|
|
// be true. If no matches are found, matched will be false and a default
|
|
// certificate will be returned with defaulted set to true. If no default
|
|
// certificate is set, defaulted will be set to false.
|
|
//
|
|
// The logic in this function is adapted from the Go standard library,
|
|
// which is by the Go Authors.
|
|
//
|
|
// This function is safe for concurrent use.
|
|
func getCertificate(name string) (cert Certificate, matched, defaulted bool) {
|
|
var ok bool
|
|
|
|
// Not going to trim trailing dots here since RFC 3546 says,
|
|
// "The hostname is represented ... without a trailing dot."
|
|
// Just normalize to lowercase.
|
|
name = strings.ToLower(name)
|
|
|
|
certCacheMu.RLock()
|
|
defer certCacheMu.RUnlock()
|
|
|
|
// exact match? great, let's use it
|
|
if cert, ok = certCache[name]; ok {
|
|
matched = true
|
|
return
|
|
}
|
|
|
|
// try replacing labels in the name with wildcards until we get a match
|
|
labels := strings.Split(name, ".")
|
|
for i := range labels {
|
|
labels[i] = "*"
|
|
candidate := strings.Join(labels, ".")
|
|
if cert, ok = certCache[candidate]; ok {
|
|
matched = true
|
|
return
|
|
}
|
|
}
|
|
|
|
// if nothing matches, use the default certificate or bust
|
|
cert, defaulted = certCache[""]
|
|
return
|
|
}
|
|
|
|
// CacheManagedCertificate loads the certificate for domain into the
|
|
// cache, flagging it as Managed and, if onDemand is true, as "OnDemand"
|
|
// (meaning that it was obtained or loaded during a TLS handshake).
|
|
//
|
|
// This method is safe for concurrent use.
|
|
func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error) {
|
|
storage, err := cfg.StorageFor(cfg.CAUrl)
|
|
if err != nil {
|
|
return Certificate{}, err
|
|
}
|
|
siteData, err := storage.LoadSite(domain)
|
|
if err != nil {
|
|
return Certificate{}, err
|
|
}
|
|
cert, err := makeCertificate(siteData.Cert, siteData.Key)
|
|
if err != nil {
|
|
return cert, err
|
|
}
|
|
cert.Config = cfg
|
|
cacheCertificate(cert)
|
|
return cert, nil
|
|
}
|
|
|
|
// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
|
|
// and keyFile, which must be in PEM format. It stores the certificate in
|
|
// memory. The Managed and OnDemand flags of the certificate will be set to
|
|
// false.
|
|
//
|
|
// This function is safe for concurrent use.
|
|
func cacheUnmanagedCertificatePEMFile(certFile, keyFile string) error {
|
|
cert, err := makeCertificateFromDisk(certFile, keyFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cacheCertificate(cert)
|
|
return nil
|
|
}
|
|
|
|
// cacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
|
|
// of the certificate and key, then caches it in memory.
|
|
//
|
|
// This function is safe for concurrent use.
|
|
func cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error {
|
|
cert, err := makeCertificate(certBytes, keyBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cacheCertificate(cert)
|
|
return nil
|
|
}
|
|
|
|
// makeCertificateFromDisk makes a Certificate by loading the
|
|
// certificate and key files. It fills out all the fields in
|
|
// the certificate except for the Managed and OnDemand flags.
|
|
// (It is up to the caller to set those.)
|
|
func makeCertificateFromDisk(certFile, keyFile string) (Certificate, error) {
|
|
certPEMBlock, err := ioutil.ReadFile(certFile)
|
|
if err != nil {
|
|
return Certificate{}, err
|
|
}
|
|
keyPEMBlock, err := ioutil.ReadFile(keyFile)
|
|
if err != nil {
|
|
return Certificate{}, err
|
|
}
|
|
return makeCertificate(certPEMBlock, keyPEMBlock)
|
|
}
|
|
|
|
// makeCertificate turns a certificate PEM bundle and a key PEM block into
|
|
// a Certificate, with OCSP and other relevant metadata tagged with it,
|
|
// except for the OnDemand and Managed flags. It is up to the caller to
|
|
// set those properties.
|
|
func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
|
|
var cert Certificate
|
|
|
|
// Convert to a tls.Certificate
|
|
tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
|
if err != nil {
|
|
return cert, err
|
|
}
|
|
|
|
// Extract relevant metadata and staple OCSP
|
|
err = fillCertFromLeaf(&cert, tlsCert)
|
|
if err != nil {
|
|
return cert, err
|
|
}
|
|
err = stapleOCSP(&cert, certPEMBlock)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Stapling OCSP: %v", err)
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// fillCertFromLeaf populates metadata fields on cert from tlsCert.
|
|
func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
|
|
if len(tlsCert.Certificate) == 0 {
|
|
return errors.New("certificate is empty")
|
|
}
|
|
cert.Certificate = tlsCert
|
|
|
|
// the leaf cert should be the one for the site; it has what we need
|
|
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if leaf.Subject.CommonName != "" {
|
|
cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)}
|
|
}
|
|
for _, name := range leaf.DNSNames {
|
|
if name != leaf.Subject.CommonName {
|
|
cert.Names = append(cert.Names, strings.ToLower(name))
|
|
}
|
|
}
|
|
for _, ip := range leaf.IPAddresses {
|
|
if ipStr := ip.String(); ipStr != leaf.Subject.CommonName {
|
|
cert.Names = append(cert.Names, strings.ToLower(ipStr))
|
|
}
|
|
}
|
|
for _, email := range leaf.EmailAddresses {
|
|
if email != leaf.Subject.CommonName {
|
|
cert.Names = append(cert.Names, strings.ToLower(email))
|
|
}
|
|
}
|
|
if len(cert.Names) == 0 {
|
|
return errors.New("certificate has no names")
|
|
}
|
|
|
|
cert.NotAfter = leaf.NotAfter
|
|
|
|
return nil
|
|
}
|
|
|
|
// cacheCertificate adds cert to the in-memory cache. If the cache is
|
|
// empty, cert will be used as the default certificate. If the cache is
|
|
// full, random entries are deleted until there is room to map all the
|
|
// names on the certificate.
|
|
//
|
|
// This certificate will be keyed to the names in cert.Names. Any names
|
|
// already used as a cache key will NOT be replaced by this cert; in
|
|
// other words, no overlap is allowed, and this certificate will not
|
|
// service those pre-existing names.
|
|
//
|
|
// This function is safe for concurrent use.
|
|
func cacheCertificate(cert Certificate) {
|
|
if cert.Config == nil {
|
|
cert.Config = new(Config)
|
|
}
|
|
certCacheMu.Lock()
|
|
if _, ok := certCache[""]; !ok {
|
|
// use as default - must be *appended* to end of list, or bad things happen!
|
|
cert.Names = append(cert.Names, "")
|
|
}
|
|
for len(certCache)+len(cert.Names) > 10000 {
|
|
// for simplicity, just remove random elements
|
|
for key := range certCache {
|
|
if key == "" { // ... but not the default cert
|
|
continue
|
|
}
|
|
delete(certCache, key)
|
|
break
|
|
}
|
|
}
|
|
for i := 0; i < len(cert.Names); i++ {
|
|
name := cert.Names[i]
|
|
if _, ok := certCache[name]; ok {
|
|
// do not allow certificates to overlap in the names they serve;
|
|
// this ambiguity causes problems because it is confusing while
|
|
// maintaining certificates; see OCSP maintenance code and
|
|
// https://caddy.community/t/random-ocsp-response-errors-for-random-clients/2473?u=matt.
|
|
log.Printf("[NOTICE] There is already a certificate loaded for %s, "+
|
|
"so certificate for %v will not service that name",
|
|
name, cert.Names)
|
|
cert.Names = append(cert.Names[:i], cert.Names[i+1:]...)
|
|
i--
|
|
continue
|
|
}
|
|
certCache[name] = cert
|
|
}
|
|
certCacheMu.Unlock()
|
|
}
|
|
|
|
// uncacheCertificate deletes name's certificate from the
|
|
// cache. If name is not a key in the certificate cache,
|
|
// this function does nothing.
|
|
func uncacheCertificate(name string) {
|
|
certCacheMu.Lock()
|
|
delete(certCache, name)
|
|
certCacheMu.Unlock()
|
|
}
|