|
|
@ -23,6 +23,7 @@ import ( |
|
|
|
"crypto/x509" |
|
|
|
"crypto/x509" |
|
|
|
"crypto/x509/pkix" |
|
|
|
"crypto/x509/pkix" |
|
|
|
"encoding/asn1" |
|
|
|
"encoding/asn1" |
|
|
|
|
|
|
|
"encoding/json" |
|
|
|
"fmt" |
|
|
|
"fmt" |
|
|
|
weakrand "math/rand" |
|
|
|
weakrand "math/rand" |
|
|
|
"net" |
|
|
|
"net" |
|
|
@ -31,7 +32,9 @@ import ( |
|
|
|
"time" |
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
|
|
"github.com/mholt/acmez" |
|
|
|
"github.com/mholt/acmez" |
|
|
|
|
|
|
|
"github.com/mholt/acmez/acme" |
|
|
|
"go.uber.org/zap" |
|
|
|
"go.uber.org/zap" |
|
|
|
|
|
|
|
"golang.org/x/net/idna" |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
// Config configures a certificate manager instance.
|
|
|
|
// Config configures a certificate manager instance.
|
|
|
@ -54,45 +57,48 @@ type Config struct { |
|
|
|
|
|
|
|
|
|
|
|
// DefaultServerName specifies a server name
|
|
|
|
// DefaultServerName specifies a server name
|
|
|
|
// to use when choosing a certificate if the
|
|
|
|
// to use when choosing a certificate if the
|
|
|
|
// ClientHello's ServerName field is empty
|
|
|
|
// ClientHello's ServerName field is empty.
|
|
|
|
DefaultServerName string |
|
|
|
DefaultServerName string |
|
|
|
|
|
|
|
|
|
|
|
// The state needed to operate on-demand TLS;
|
|
|
|
// The state needed to operate on-demand TLS;
|
|
|
|
// if non-nil, on-demand TLS is enabled and
|
|
|
|
// if non-nil, on-demand TLS is enabled and
|
|
|
|
// certificate operations are deferred to
|
|
|
|
// certificate operations are deferred to
|
|
|
|
// TLS handshakes (or as-needed)
|
|
|
|
// TLS handshakes (or as-needed).
|
|
|
|
// TODO: Can we call this feature "Reactive/Lazy/Passive TLS" instead?
|
|
|
|
// TODO: Can we call this feature "Reactive/Lazy/Passive TLS" instead?
|
|
|
|
OnDemand *OnDemandConfig |
|
|
|
OnDemand *OnDemandConfig |
|
|
|
|
|
|
|
|
|
|
|
// Add the must staple TLS extension to the CSR
|
|
|
|
// Adds the must staple TLS extension to the CSR.
|
|
|
|
MustStaple bool |
|
|
|
MustStaple bool |
|
|
|
|
|
|
|
|
|
|
|
// The type that issues certificates; the
|
|
|
|
// The source for getting new certificates; the
|
|
|
|
// default Issuer is ACMEManager
|
|
|
|
// default Issuer is ACMEManager. If multiple
|
|
|
|
Issuer Issuer |
|
|
|
// issuers are specified, they will be tried in
|
|
|
|
|
|
|
|
// turn until one succeeds.
|
|
|
|
// The type that revokes certificates; must
|
|
|
|
Issuers []Issuer |
|
|
|
// be configured in conjunction with the Issuer
|
|
|
|
|
|
|
|
// field such that both the Issuer and Revoker
|
|
|
|
|
|
|
|
// are related (because issuance information is
|
|
|
|
|
|
|
|
// required for revocation)
|
|
|
|
|
|
|
|
Revoker Revoker |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// The source of new private keys for certificates;
|
|
|
|
// The source of new private keys for certificates;
|
|
|
|
// the default KeySource is StandardKeyGenerator
|
|
|
|
// the default KeySource is StandardKeyGenerator.
|
|
|
|
KeySource KeyGenerator |
|
|
|
KeySource KeyGenerator |
|
|
|
|
|
|
|
|
|
|
|
// CertSelection chooses one of the certificates
|
|
|
|
// CertSelection chooses one of the certificates
|
|
|
|
// with which the ClientHello will be completed;
|
|
|
|
// with which the ClientHello will be completed;
|
|
|
|
// if not set, DefaultCertificateSelector will
|
|
|
|
// if not set, DefaultCertificateSelector will
|
|
|
|
// be used
|
|
|
|
// be used.
|
|
|
|
CertSelection CertificateSelector |
|
|
|
CertSelection CertificateSelector |
|
|
|
|
|
|
|
|
|
|
|
// The storage to access when storing or
|
|
|
|
// OCSP configures how OCSP is handled. By default,
|
|
|
|
// loading TLS assets
|
|
|
|
// OCSP responses are fetched for every certificate
|
|
|
|
|
|
|
|
// with a responder URL, and cached on disk. Changing
|
|
|
|
|
|
|
|
// these defaults is STRONGLY discouraged unless you
|
|
|
|
|
|
|
|
// have a compelling reason to put clients at greater
|
|
|
|
|
|
|
|
// risk and reduce their privacy.
|
|
|
|
|
|
|
|
OCSP OCSPConfig |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// The storage to access when storing or loading
|
|
|
|
|
|
|
|
// TLS assets. Default is the local file system.
|
|
|
|
Storage Storage |
|
|
|
Storage Storage |
|
|
|
|
|
|
|
|
|
|
|
// Set a logger to enable logging
|
|
|
|
// Set a logger to enable logging.
|
|
|
|
Logger *zap.Logger |
|
|
|
Logger *zap.Logger |
|
|
|
|
|
|
|
|
|
|
|
// required pointer to the in-memory cert cache
|
|
|
|
// required pointer to the in-memory cert cache
|
|
|
@ -116,6 +122,9 @@ type Config struct { |
|
|
|
// same, default certificate cache. All configs returned
|
|
|
|
// same, default certificate cache. All configs returned
|
|
|
|
// by NewDefault() are based on the values of the fields of
|
|
|
|
// by NewDefault() are based on the values of the fields of
|
|
|
|
// Default at the time it is called.
|
|
|
|
// Default at the time it is called.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// This is the only way to get a config that uses the
|
|
|
|
|
|
|
|
// default certificate cache.
|
|
|
|
func NewDefault() *Config { |
|
|
|
func NewDefault() *Config { |
|
|
|
defaultCacheMu.Lock() |
|
|
|
defaultCacheMu.Lock() |
|
|
|
if defaultCache == nil { |
|
|
|
if defaultCache == nil { |
|
|
@ -153,7 +162,7 @@ func NewDefault() *Config { |
|
|
|
// the vast majority of cases, there will be only a
|
|
|
|
// the vast majority of cases, there will be only a
|
|
|
|
// single Config, thus the default cache (which always
|
|
|
|
// single Config, thus the default cache (which always
|
|
|
|
// uses the default Config) and default config will
|
|
|
|
// uses the default Config) and default config will
|
|
|
|
// suffice, and you should use New() instead.
|
|
|
|
// suffice, and you should use NewDefault() instead.
|
|
|
|
func New(certCache *Cache, cfg Config) *Config { |
|
|
|
func New(certCache *Cache, cfg Config) *Config { |
|
|
|
if certCache == nil { |
|
|
|
if certCache == nil { |
|
|
|
panic("a certificate cache is required") |
|
|
|
panic("a certificate cache is required") |
|
|
@ -196,23 +205,11 @@ func newWithCache(certCache *Cache, cfg Config) *Config { |
|
|
|
if cfg.Storage == nil { |
|
|
|
if cfg.Storage == nil { |
|
|
|
cfg.Storage = Default.Storage |
|
|
|
cfg.Storage = Default.Storage |
|
|
|
} |
|
|
|
} |
|
|
|
if cfg.Issuer == nil { |
|
|
|
if len(cfg.Issuers) == 0 { |
|
|
|
cfg.Issuer = Default.Issuer |
|
|
|
cfg.Issuers = Default.Issuers |
|
|
|
if cfg.Issuer == nil { |
|
|
|
if len(cfg.Issuers) == 0 { |
|
|
|
// okay really, we need an issuer,
|
|
|
|
// at least one issuer is absolutely required
|
|
|
|
// that's kind of the point; most
|
|
|
|
cfg.Issuers = []Issuer{NewACMEManager(&cfg, DefaultACME)} |
|
|
|
// people would probably want ACME
|
|
|
|
|
|
|
|
cfg.Issuer = NewACMEManager(&cfg, DefaultACME) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// issuer and revoker go together; if user
|
|
|
|
|
|
|
|
// specifies their own issuer, we don't want
|
|
|
|
|
|
|
|
// to override their revoker, hence we only
|
|
|
|
|
|
|
|
// do this if Issuer was also nil
|
|
|
|
|
|
|
|
if cfg.Revoker == nil { |
|
|
|
|
|
|
|
cfg.Revoker = Default.Revoker |
|
|
|
|
|
|
|
if cfg.Revoker == nil { |
|
|
|
|
|
|
|
cfg.Revoker = NewACMEManager(&cfg, DefaultACME) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -223,7 +220,6 @@ func newWithCache(certCache *Cache, cfg Config) *Config { |
|
|
|
cfg.Storage = defaultFileStorage |
|
|
|
cfg.Storage = defaultFileStorage |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ensure the unexported fields are valid
|
|
|
|
|
|
|
|
cfg.certCache = certCache |
|
|
|
cfg.certCache = certCache |
|
|
|
|
|
|
|
|
|
|
|
return &cfg |
|
|
|
return &cfg |
|
|
@ -254,6 +250,29 @@ func (cfg *Config) ManageSync(domainNames []string) error { |
|
|
|
return cfg.manageAll(nil, domainNames, false) |
|
|
|
return cfg.manageAll(nil, domainNames, false) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ClientCredentials returns a list of TLS client certificate chains for the given identifiers.
|
|
|
|
|
|
|
|
// The return value can be used in a tls.Config to enable client authentication using managed certificates.
|
|
|
|
|
|
|
|
// Any certificates that need to be obtained or renewed for these identifiers will be managed accordingly.
|
|
|
|
|
|
|
|
func (cfg *Config) ClientCredentials(ctx context.Context, identifiers []string) ([]tls.Certificate, error) { |
|
|
|
|
|
|
|
err := cfg.manageAll(ctx, identifiers, false) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
var chains []tls.Certificate |
|
|
|
|
|
|
|
for _, id := range identifiers { |
|
|
|
|
|
|
|
certRes, err := cfg.loadCertResourceAnyIssuer(id) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return chains, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
chain, err := tls.X509KeyPair(certRes.CertificatePEM, certRes.PrivateKeyPEM) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return chains, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
chains = append(chains, chain) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return chains, nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ManageAsync is the same as ManageSync, except that ACME
|
|
|
|
// ManageAsync is the same as ManageSync, except that ACME
|
|
|
|
// operations are performed asynchronously (in the background).
|
|
|
|
// operations are performed asynchronously (in the background).
|
|
|
|
// This method returns before certificates are ready. It is
|
|
|
|
// This method returns before certificates are ready. It is
|
|
|
@ -360,6 +379,28 @@ func (cfg *Config) manageOne(ctx context.Context, domainName string, async bool) |
|
|
|
return nil |
|
|
|
return nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ObtainCert obtains a certificate for name using cfg, as long
|
|
|
|
// ObtainCert obtains a certificate for name using cfg, as long
|
|
|
|
// as a certificate does not already exist in storage for that
|
|
|
|
// as a certificate does not already exist in storage for that
|
|
|
|
// name. The name must qualify and cfg must be flagged as Managed.
|
|
|
|
// name. The name must qualify and cfg must be flagged as Managed.
|
|
|
@ -372,27 +413,22 @@ func (cfg *Config) manageOne(ctx context.Context, domainName string, async bool) |
|
|
|
// TODO: consider moving interactive param into the Config struct,
|
|
|
|
// TODO: consider moving interactive param into the Config struct,
|
|
|
|
// and maybe retry settings into the Config struct as well? (same for RenewCert)
|
|
|
|
// and maybe retry settings into the Config struct as well? (same for RenewCert)
|
|
|
|
func (cfg *Config) ObtainCert(ctx context.Context, name string, interactive bool) error { |
|
|
|
func (cfg *Config) ObtainCert(ctx context.Context, name string, interactive bool) error { |
|
|
|
if cfg.storageHasCertResources(name) { |
|
|
|
if len(cfg.Issuers) == 0 { |
|
|
|
return nil |
|
|
|
return fmt.Errorf("no issuers configured; impossible to obtain or check for existing certificate in storage") |
|
|
|
} |
|
|
|
|
|
|
|
issuer, err := cfg.getPrecheckedIssuer(ctx, []string{name}, interactive) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return err |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
if issuer == nil { |
|
|
|
if cfg.storageHasCertResourcesAnyIssuer(name) { |
|
|
|
return nil |
|
|
|
return nil |
|
|
|
} |
|
|
|
} |
|
|
|
return cfg.obtainWithIssuer(ctx, issuer, name, interactive) |
|
|
|
// ensure storage is writeable and readable
|
|
|
|
} |
|
|
|
// TODO: this is not necessary every time; should only perform check once every so often for each storage, which may require some global state...
|
|
|
|
|
|
|
|
err := cfg.checkStorage() |
|
|
|
func loggerNamed(l *zap.Logger, name string) *zap.Logger { |
|
|
|
if err != nil { |
|
|
|
if l == nil { |
|
|
|
return fmt.Errorf("failed storage check: %v - storage is probably misconfigured", err) |
|
|
|
return nil |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
return l.Named(name) |
|
|
|
return cfg.obtainCert(ctx, name, interactive) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (cfg *Config) obtainWithIssuer(ctx context.Context, issuer Issuer, name string, interactive bool) error { |
|
|
|
func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool) error { |
|
|
|
log := loggerNamed(cfg.Logger, "obtain") |
|
|
|
log := loggerNamed(cfg.Logger, "obtain") |
|
|
|
|
|
|
|
|
|
|
|
if log != nil { |
|
|
|
if log != nil { |
|
|
@ -400,10 +436,10 @@ func (cfg *Config) obtainWithIssuer(ctx context.Context, issuer Issuer, name str |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ensure idempotency of the obtain operation for this name
|
|
|
|
// ensure idempotency of the obtain operation for this name
|
|
|
|
lockKey := cfg.lockKey("cert_acme", name) |
|
|
|
lockKey := cfg.lockKey(certIssueLockOp, name) |
|
|
|
err := acquireLock(ctx, cfg.Storage, lockKey) |
|
|
|
err := acquireLock(ctx, cfg.Storage, lockKey) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
return fmt.Errorf("unable to acquire lock '%s': %v", lockKey, err) |
|
|
|
} |
|
|
|
} |
|
|
|
defer func() { |
|
|
|
defer func() { |
|
|
|
if log != nil { |
|
|
|
if log != nil { |
|
|
@ -424,7 +460,7 @@ func (cfg *Config) obtainWithIssuer(ctx context.Context, issuer Issuer, name str |
|
|
|
|
|
|
|
|
|
|
|
f := func(ctx context.Context) error { |
|
|
|
f := func(ctx context.Context) error { |
|
|
|
// check if obtain is still needed -- might have been obtained during lock
|
|
|
|
// check if obtain is still needed -- might have been obtained during lock
|
|
|
|
if cfg.storageHasCertResources(name) { |
|
|
|
if cfg.storageHasCertResourcesAnyIssuer(name) { |
|
|
|
if log != nil { |
|
|
|
if log != nil { |
|
|
|
log.Info("certificate already exists in storage", zap.String("identifier", name)) |
|
|
|
log.Info("certificate already exists in storage", zap.String("identifier", name)) |
|
|
|
} |
|
|
|
} |
|
|
@ -445,8 +481,24 @@ func (cfg *Config) obtainWithIssuer(ctx context.Context, issuer Issuer, name str |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
issuedCert, err := issuer.Issue(ctx, csr) |
|
|
|
// try to obtain from each issuer until we succeed
|
|
|
|
|
|
|
|
var issuedCert *IssuedCertificate |
|
|
|
|
|
|
|
var issuerUsed Issuer |
|
|
|
|
|
|
|
for _, issuer := range cfg.Issuers { |
|
|
|
|
|
|
|
if prechecker, ok := issuer.(PreChecker); ok { |
|
|
|
|
|
|
|
err = prechecker.PreCheck(ctx, []string{name}, interactive) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
issuedCert, err = issuer.Issue(ctx, csr) |
|
|
|
|
|
|
|
if err == nil { |
|
|
|
|
|
|
|
issuerUsed = issuer |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
// TODO: only the error from the last issuer will be returned, oh well?
|
|
|
|
return fmt.Errorf("[%s] Obtain: %w", name, err) |
|
|
|
return fmt.Errorf("[%s] Obtain: %w", name, err) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -457,7 +509,7 @@ func (cfg *Config) obtainWithIssuer(ctx context.Context, issuer Issuer, name str |
|
|
|
PrivateKeyPEM: privKeyPEM, |
|
|
|
PrivateKeyPEM: privKeyPEM, |
|
|
|
IssuerData: issuedCert.Metadata, |
|
|
|
IssuerData: issuedCert.Metadata, |
|
|
|
} |
|
|
|
} |
|
|
|
err = cfg.saveCertResource(certRes) |
|
|
|
err = cfg.saveCertResource(issuerUsed, certRes) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return fmt.Errorf("[%s] Obtain: saving assets: %v", name, err) |
|
|
|
return fmt.Errorf("[%s] Obtain: saving assets: %v", name, err) |
|
|
|
} |
|
|
|
} |
|
|
@ -480,21 +532,32 @@ func (cfg *Config) obtainWithIssuer(ctx context.Context, issuer Issuer, name str |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (cfg *Config) storageHasCertResourcesAnyIssuer(name string) bool { |
|
|
|
|
|
|
|
for _, iss := range cfg.Issuers { |
|
|
|
|
|
|
|
if cfg.storageHasCertResources(iss, name) { |
|
|
|
|
|
|
|
return true |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// RenewCert renews the certificate for name using cfg. It stows the
|
|
|
|
// RenewCert renews the certificate for name using cfg. It stows the
|
|
|
|
// renewed certificate and its assets in storage if successful. It
|
|
|
|
// renewed certificate and its assets in storage if successful. It
|
|
|
|
// DOES NOT update the in-memory cache with the new certificate.
|
|
|
|
// DOES NOT update the in-memory cache with the new certificate.
|
|
|
|
func (cfg *Config) RenewCert(ctx context.Context, name string, interactive bool) error { |
|
|
|
func (cfg *Config) RenewCert(ctx context.Context, name string, interactive bool) error { |
|
|
|
issuer, err := cfg.getPrecheckedIssuer(ctx, []string{name}, interactive) |
|
|
|
if len(cfg.Issuers) == 0 { |
|
|
|
if err != nil { |
|
|
|
return fmt.Errorf("no issuers configured; impossible to renew or check existing certificate in storage") |
|
|
|
return err |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
if issuer == nil { |
|
|
|
// ensure storage is writeable and readable
|
|
|
|
return nil |
|
|
|
// TODO: this is not necessary every time; should only perform check once every so often for each storage, which may require some global state...
|
|
|
|
|
|
|
|
err := cfg.checkStorage() |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("failed storage check: %v - storage is probably misconfigured", err) |
|
|
|
} |
|
|
|
} |
|
|
|
return cfg.renewWithIssuer(ctx, issuer, name, interactive) |
|
|
|
return cfg.renewCert(ctx, name, interactive) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (cfg *Config) renewWithIssuer(ctx context.Context, issuer Issuer, name string, interactive bool) error { |
|
|
|
func (cfg *Config) renewCert(ctx context.Context, name string, interactive bool) error { |
|
|
|
log := loggerNamed(cfg.Logger, "renew") |
|
|
|
log := loggerNamed(cfg.Logger, "renew") |
|
|
|
|
|
|
|
|
|
|
|
if log != nil { |
|
|
|
if log != nil { |
|
|
@ -502,10 +565,10 @@ func (cfg *Config) renewWithIssuer(ctx context.Context, issuer Issuer, name stri |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ensure idempotency of the renew operation for this name
|
|
|
|
// ensure idempotency of the renew operation for this name
|
|
|
|
lockKey := cfg.lockKey("cert_acme", name) |
|
|
|
lockKey := cfg.lockKey(certIssueLockOp, name) |
|
|
|
err := acquireLock(ctx, cfg.Storage, lockKey) |
|
|
|
err := acquireLock(ctx, cfg.Storage, lockKey) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
return fmt.Errorf("unable to acquire lock '%s': %v", lockKey, err) |
|
|
|
} |
|
|
|
} |
|
|
|
defer func() { |
|
|
|
defer func() { |
|
|
|
if log != nil { |
|
|
|
if log != nil { |
|
|
@ -526,7 +589,7 @@ func (cfg *Config) renewWithIssuer(ctx context.Context, issuer Issuer, name stri |
|
|
|
|
|
|
|
|
|
|
|
f := func(ctx context.Context) error { |
|
|
|
f := func(ctx context.Context) error { |
|
|
|
// prepare for renewal (load PEM cert, key, and meta)
|
|
|
|
// prepare for renewal (load PEM cert, key, and meta)
|
|
|
|
certRes, err := cfg.loadCertResource(name) |
|
|
|
certRes, err := cfg.loadCertResourceAnyIssuer(name) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
@ -556,8 +619,24 @@ func (cfg *Config) renewWithIssuer(ctx context.Context, issuer Issuer, name stri |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
issuedCert, err := issuer.Issue(ctx, csr) |
|
|
|
// try to obtain from each issuer until we succeed
|
|
|
|
|
|
|
|
var issuedCert *IssuedCertificate |
|
|
|
|
|
|
|
var issuerUsed Issuer |
|
|
|
|
|
|
|
for _, issuer := range cfg.Issuers { |
|
|
|
|
|
|
|
if prechecker, ok := issuer.(PreChecker); ok { |
|
|
|
|
|
|
|
err = prechecker.PreCheck(ctx, []string{name}, interactive) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
issuedCert, err = issuer.Issue(ctx, csr) |
|
|
|
|
|
|
|
if err == nil { |
|
|
|
|
|
|
|
issuerUsed = issuer |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
// TODO: only the error from the last issuer will be returned, oh well?
|
|
|
|
return fmt.Errorf("[%s] Renew: %w", name, err) |
|
|
|
return fmt.Errorf("[%s] Renew: %w", name, err) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -568,7 +647,7 @@ func (cfg *Config) renewWithIssuer(ctx context.Context, issuer Issuer, name stri |
|
|
|
PrivateKeyPEM: certRes.PrivateKeyPEM, |
|
|
|
PrivateKeyPEM: certRes.PrivateKeyPEM, |
|
|
|
IssuerData: issuedCert.Metadata, |
|
|
|
IssuerData: issuedCert.Metadata, |
|
|
|
} |
|
|
|
} |
|
|
|
err = cfg.saveCertResource(newCertRes) |
|
|
|
err = cfg.saveCertResource(issuerUsed, newCertRes) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return fmt.Errorf("[%s] Renew: saving assets: %v", name, err) |
|
|
|
return fmt.Errorf("[%s] Renew: saving assets: %v", name, err) |
|
|
|
} |
|
|
|
} |
|
|
@ -602,7 +681,12 @@ func (cfg *Config) generateCSR(privateKey crypto.PrivateKey, sans []string) (*x5 |
|
|
|
} else if u, err := url.Parse(name); err == nil && strings.Contains(name, "/") { |
|
|
|
} else if u, err := url.Parse(name); err == nil && strings.Contains(name, "/") { |
|
|
|
csrTemplate.URIs = append(csrTemplate.URIs, u) |
|
|
|
csrTemplate.URIs = append(csrTemplate.URIs, u) |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
csrTemplate.DNSNames = append(csrTemplate.DNSNames, name) |
|
|
|
// convert IDNs to ASCII according to RFC 5280 section 7
|
|
|
|
|
|
|
|
normalizedName, err := idna.ToASCII(name) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, fmt.Errorf("converting identifier '%s' to ASCII: %v", name, err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
csrTemplate.DNSNames = append(csrTemplate.DNSNames, normalizedName) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -619,28 +703,29 @@ func (cfg *Config) generateCSR(privateKey crypto.PrivateKey, sans []string) (*x5 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// RevokeCert revokes the certificate for domain via ACME protocol. It requires
|
|
|
|
// RevokeCert revokes the certificate for domain via ACME protocol. It requires
|
|
|
|
// that cfg.Issuer is properly configured with the same issuer that issued the
|
|
|
|
// that cfg.Issuers is properly configured with the same issuer that issued the
|
|
|
|
// certificate being revoked. See RFC 5280 §5.3.1 for reason codes.
|
|
|
|
// certificate being revoked. See RFC 5280 §5.3.1 for reason codes.
|
|
|
|
func (cfg *Config) RevokeCert(ctx context.Context, domain string, reason int, interactive bool) error { |
|
|
|
func (cfg *Config) RevokeCert(ctx context.Context, domain string, reason int, interactive bool) error { |
|
|
|
rev := cfg.Revoker |
|
|
|
for i, issuer := range cfg.Issuers { |
|
|
|
if rev == nil { |
|
|
|
issuerKey := issuer.IssuerKey() |
|
|
|
rev = Default.Revoker |
|
|
|
|
|
|
|
|
|
|
|
rev, ok := issuer.(Revoker) |
|
|
|
|
|
|
|
if !ok { |
|
|
|
|
|
|
|
return fmt.Errorf("issuer %d (%s) is not a Revoker", i, issuerKey) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
certRes, err := cfg.loadCertResource(domain) |
|
|
|
certRes, err := cfg.loadCertResource(issuer, domain) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
issuerKey := cfg.Issuer.IssuerKey() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !cfg.Storage.Exists(StorageKeys.SitePrivateKey(issuerKey, domain)) { |
|
|
|
if !cfg.Storage.Exists(StorageKeys.SitePrivateKey(issuerKey, domain)) { |
|
|
|
return fmt.Errorf("private key not found for %s", certRes.SANs) |
|
|
|
return fmt.Errorf("private key not found for %s", certRes.SANs) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
err = rev.Revoke(ctx, certRes, reason) |
|
|
|
err = rev.Revoke(ctx, certRes, reason) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
return fmt.Errorf("issuer %d (%s): %v", i, issuerKey, err) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
cfg.emit("cert_revoked", domain) |
|
|
|
cfg.emit("cert_revoked", domain) |
|
|
@ -657,6 +742,7 @@ func (cfg *Config) RevokeCert(ctx context.Context, domain string, reason int, in |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return fmt.Errorf("certificate revoked, but unable to delete certificate metadata: %v", err) |
|
|
|
return fmt.Errorf("certificate revoked, but unable to delete certificate metadata: %v", err) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
return nil |
|
|
|
} |
|
|
|
} |
|
|
@ -692,27 +778,50 @@ func (cfg *Config) TLSConfig() *tls.Config { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// getPrecheckedIssuer returns an Issuer with pre-checks
|
|
|
|
// getChallengeInfo loads the challenge info from either the internal challenge memory
|
|
|
|
// completed, if it is also a PreChecker. It also checks
|
|
|
|
// or the external storage (implying distributed solving). The second return value
|
|
|
|
// that storage is functioning. If a nil Issuer is returned
|
|
|
|
// indicates whether challenge info was loaded from external storage. If true, the
|
|
|
|
// with a nil error, that means to skip this operation
|
|
|
|
// challenge is being solved in a distributed fashion; if false, from internal memory.
|
|
|
|
// (not an error, just a no-op).
|
|
|
|
// If no matching challenge information can be found, an error is returned.
|
|
|
|
func (cfg *Config) getPrecheckedIssuer(ctx context.Context, names []string, interactive bool) (Issuer, error) { |
|
|
|
func (cfg *Config) getChallengeInfo(identifier string) (Challenge, bool, error) { |
|
|
|
// ensure storage is writeable and readable
|
|
|
|
// first, check if our process initiated this challenge; if so, just return it
|
|
|
|
// TODO: this is not necessary every time; should only
|
|
|
|
chalData, ok := GetACMEChallenge(identifier) |
|
|
|
// perform check once every so often for each storage,
|
|
|
|
if ok { |
|
|
|
// which may require some global state...
|
|
|
|
return chalData, false, nil |
|
|
|
err := cfg.checkStorage() |
|
|
|
} |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, fmt.Errorf("failed storage check: %v - storage is probably misconfigured", err) |
|
|
|
// otherwise, perhaps another instance in the cluster initiated it; check
|
|
|
|
|
|
|
|
// the configured storage to retrieve challenge data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var chalInfo acme.Challenge |
|
|
|
|
|
|
|
var chalInfoBytes []byte |
|
|
|
|
|
|
|
var tokenKey string |
|
|
|
|
|
|
|
for _, issuer := range cfg.Issuers { |
|
|
|
|
|
|
|
ds := distributedSolver{ |
|
|
|
|
|
|
|
storage: cfg.Storage, |
|
|
|
|
|
|
|
storageKeyIssuerPrefix: storageKeyACMECAPrefix(issuer.IssuerKey()), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
tokenKey = ds.challengeTokensKey(identifier) |
|
|
|
|
|
|
|
var err error |
|
|
|
|
|
|
|
chalInfoBytes, err = cfg.Storage.Load(tokenKey) |
|
|
|
|
|
|
|
if err == nil { |
|
|
|
|
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
if prechecker, ok := cfg.Issuer.(PreChecker); ok { |
|
|
|
if _, ok := err.(ErrNotExist); ok { |
|
|
|
err := prechecker.PreCheck(ctx, names, interactive) |
|
|
|
continue |
|
|
|
if err != nil { |
|
|
|
} |
|
|
|
return nil, err |
|
|
|
return Challenge{}, false, fmt.Errorf("opening distributed challenge token file %s: %v", tokenKey, err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if len(chalInfoBytes) == 0 { |
|
|
|
|
|
|
|
return Challenge{}, false, fmt.Errorf("no information found to solve challenge for identifier: %s", identifier) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
err := json.Unmarshal(chalInfoBytes, &chalInfo) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return Challenge{}, false, fmt.Errorf("decoding challenge token file %s (corrupted?): %v", tokenKey, err) |
|
|
|
} |
|
|
|
} |
|
|
|
return cfg.Issuer, nil |
|
|
|
|
|
|
|
|
|
|
|
return Challenge{Challenge: chalInfo}, true, nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// checkStorage tests the storage by writing random bytes
|
|
|
|
// checkStorage tests the storage by writing random bytes
|
|
|
@ -758,8 +867,8 @@ func (cfg *Config) checkStorage() error { |
|
|
|
// associated with cfg's certificate cache has all the
|
|
|
|
// associated with cfg's certificate cache has all the
|
|
|
|
// resources related to the certificate for domain: the
|
|
|
|
// resources related to the certificate for domain: the
|
|
|
|
// certificate, the private key, and the metadata.
|
|
|
|
// certificate, the private key, and the metadata.
|
|
|
|
func (cfg *Config) storageHasCertResources(domain string) bool { |
|
|
|
func (cfg *Config) storageHasCertResources(issuer Issuer, domain string) bool { |
|
|
|
issuerKey := cfg.Issuer.IssuerKey() |
|
|
|
issuerKey := issuer.IssuerKey() |
|
|
|
certKey := StorageKeys.SiteCert(issuerKey, domain) |
|
|
|
certKey := StorageKeys.SiteCert(issuerKey, domain) |
|
|
|
keyKey := StorageKeys.SitePrivateKey(issuerKey, domain) |
|
|
|
keyKey := StorageKeys.SitePrivateKey(issuerKey, domain) |
|
|
|
metaKey := StorageKeys.SiteMeta(issuerKey, domain) |
|
|
|
metaKey := StorageKeys.SiteMeta(issuerKey, domain) |
|
|
@ -771,18 +880,19 @@ func (cfg *Config) storageHasCertResources(domain string) bool { |
|
|
|
// lockKey returns a key for a lock that is specific to the operation
|
|
|
|
// lockKey returns a key for a lock that is specific to the operation
|
|
|
|
// named op being performed related to domainName and this config's CA.
|
|
|
|
// named op being performed related to domainName and this config's CA.
|
|
|
|
func (cfg *Config) lockKey(op, domainName string) string { |
|
|
|
func (cfg *Config) lockKey(op, domainName string) string { |
|
|
|
return fmt.Sprintf("%s_%s_%s", op, domainName, cfg.Issuer.IssuerKey()) |
|
|
|
return fmt.Sprintf("%s_%s", op, domainName) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// managedCertNeedsRenewal returns true if certRes is
|
|
|
|
// managedCertNeedsRenewal returns true if certRes is expiring soon or already expired,
|
|
|
|
// expiring soon or already expired, or if the process
|
|
|
|
// or if the process of decoding the cert and checking its expiration returned an error.
|
|
|
|
// of checking the expiration returned an error.
|
|
|
|
|
|
|
|
func (cfg *Config) managedCertNeedsRenewal(certRes CertificateResource) (time.Duration, bool) { |
|
|
|
func (cfg *Config) managedCertNeedsRenewal(certRes CertificateResource) (time.Duration, bool) { |
|
|
|
cert, err := makeCertificate(certRes.CertificatePEM, certRes.PrivateKeyPEM) |
|
|
|
certChain, err := parseCertsFromPEMBundle(certRes.CertificatePEM) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return 0, true |
|
|
|
return 0, true |
|
|
|
} |
|
|
|
} |
|
|
|
return time.Until(cert.Leaf.NotAfter), cert.NeedsRenewal(cfg) |
|
|
|
remaining := time.Until(certChain[0].NotAfter) |
|
|
|
|
|
|
|
needsRenew := currentlyInRenewalWindow(certChain[0].NotBefore, certChain[0].NotAfter, cfg.RenewalWindowRatio) |
|
|
|
|
|
|
|
return remaining, needsRenew |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (cfg *Config) emit(eventName string, data interface{}) { |
|
|
|
func (cfg *Config) emit(eventName string, data interface{}) { |
|
|
@ -792,11 +902,40 @@ func (cfg *Config) emit(eventName string, data interface{}) { |
|
|
|
cfg.OnEvent(eventName, data) |
|
|
|
cfg.OnEvent(eventName, data) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func loggerNamed(l *zap.Logger, name string) *zap.Logger { |
|
|
|
|
|
|
|
if l == nil { |
|
|
|
|
|
|
|
return nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return l.Named(name) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// CertificateSelector is a type which can select a certificate to use given multiple choices.
|
|
|
|
// CertificateSelector is a type which can select a certificate to use given multiple choices.
|
|
|
|
type CertificateSelector interface { |
|
|
|
type CertificateSelector interface { |
|
|
|
SelectCertificate(*tls.ClientHelloInfo, []Certificate) (Certificate, error) |
|
|
|
SelectCertificate(*tls.ClientHelloInfo, []Certificate) (Certificate, error) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// OCSPConfig configures how OCSP is handled.
|
|
|
|
|
|
|
|
type OCSPConfig struct { |
|
|
|
|
|
|
|
// Disable automatic OCSP stapling; strongly
|
|
|
|
|
|
|
|
// discouraged unless you have a good reason.
|
|
|
|
|
|
|
|
// Disabling this puts clients at greater risk
|
|
|
|
|
|
|
|
// and reduces their privacy.
|
|
|
|
|
|
|
|
DisableStapling bool |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// A map of OCSP responder domains to replacement
|
|
|
|
|
|
|
|
// domains for querying OCSP servers. Used for
|
|
|
|
|
|
|
|
// overriding the OCSP responder URL that is
|
|
|
|
|
|
|
|
// embedded in certificates. Mapping to an empty
|
|
|
|
|
|
|
|
// URL will disable OCSP from that responder.
|
|
|
|
|
|
|
|
ResponderOverrides map[string]string |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// certIssueLockOp is the name of the operation used
|
|
|
|
|
|
|
|
// when naming a lock to make it mutually exclusive
|
|
|
|
|
|
|
|
// with other certificate issuance operations for a
|
|
|
|
|
|
|
|
// certain name.
|
|
|
|
|
|
|
|
const certIssueLockOp = "issue_cert" |
|
|
|
|
|
|
|
|
|
|
|
// Constants for PKIX MustStaple extension.
|
|
|
|
// Constants for PKIX MustStaple extension.
|
|
|
|
var ( |
|
|
|
var ( |
|
|
|
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} |
|
|
|
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} |
|
|
|