mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-14 14:49:32 +01:00
d2ea21d0d8
* use certmagic for more extensible/robust ACME cert handling * accept TOS based on config option Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv>
808 lines
25 KiB
Go
Vendored
808 lines
25 KiB
Go
Vendored
// 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 (
|
|
"bytes"
|
|
"context"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"fmt"
|
|
weakrand "math/rand"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mholt/acmez"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Config configures a certificate manager instance.
|
|
// An empty Config is not valid: use New() to obtain
|
|
// a valid Config.
|
|
type Config struct {
|
|
// How much of a certificate's lifetime becomes the
|
|
// renewal window, which is the span of time at the
|
|
// end of the certificate's validity period in which
|
|
// it should be renewed; for most certificates, the
|
|
// global default is good, but for extremely short-
|
|
// lived certs, you may want to raise this to ~0.5.
|
|
RenewalWindowRatio float64
|
|
|
|
// An optional event callback clients can set
|
|
// to subscribe to certain things happening
|
|
// internally by this config; invocations are
|
|
// synchronous, so make them return quickly!
|
|
OnEvent func(event string, data interface{})
|
|
|
|
// DefaultServerName specifies a server name
|
|
// to use when choosing a certificate if the
|
|
// ClientHello's ServerName field is empty
|
|
DefaultServerName string
|
|
|
|
// The state needed to operate on-demand TLS;
|
|
// if non-nil, on-demand TLS is enabled and
|
|
// certificate operations are deferred to
|
|
// TLS handshakes (or as-needed)
|
|
// TODO: Can we call this feature "Reactive/Lazy/Passive TLS" instead?
|
|
OnDemand *OnDemandConfig
|
|
|
|
// Add the must staple TLS extension to the CSR
|
|
MustStaple bool
|
|
|
|
// The type that issues certificates; the
|
|
// default Issuer is ACMEManager
|
|
Issuer Issuer
|
|
|
|
// The type that revokes certificates; must
|
|
// 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 default KeySource is StandardKeyGenerator
|
|
KeySource KeyGenerator
|
|
|
|
// CertSelection chooses one of the certificates
|
|
// with which the ClientHello will be completed;
|
|
// if not set, DefaultCertificateSelector will
|
|
// be used
|
|
CertSelection CertificateSelector
|
|
|
|
// The storage to access when storing or
|
|
// loading TLS assets
|
|
Storage Storage
|
|
|
|
// Set a logger to enable logging
|
|
Logger *zap.Logger
|
|
|
|
// required pointer to the in-memory cert cache
|
|
certCache *Cache
|
|
}
|
|
|
|
// NewDefault makes a valid config based on the package
|
|
// Default config. Most users will call this function
|
|
// instead of New() since most use cases require only a
|
|
// single config for any and all certificates.
|
|
//
|
|
// If your requirements are more advanced (for example,
|
|
// multiple configs depending on the certificate), then use
|
|
// New() instead. (You will need to make your own Cache
|
|
// first.) If you only need a single Config to manage your
|
|
// certs (even if that config changes, as long as it is the
|
|
// only one), customize the Default package variable before
|
|
// calling NewDefault().
|
|
//
|
|
// All calls to NewDefault() will return configs that use the
|
|
// same, default certificate cache. All configs returned
|
|
// by NewDefault() are based on the values of the fields of
|
|
// Default at the time it is called.
|
|
func NewDefault() *Config {
|
|
defaultCacheMu.Lock()
|
|
if defaultCache == nil {
|
|
defaultCache = NewCache(CacheOptions{
|
|
// the cache will likely need to renew certificates,
|
|
// so it will need to know how to do that, which
|
|
// depends on the certificate being managed and which
|
|
// can change during the lifetime of the cache; this
|
|
// callback makes it possible to get the latest and
|
|
// correct config with which to manage the cert,
|
|
// but if the user does not provide one, we can only
|
|
// assume that we are to use the default config
|
|
GetConfigForCert: func(Certificate) (*Config, error) {
|
|
return NewDefault(), nil
|
|
},
|
|
})
|
|
}
|
|
certCache := defaultCache
|
|
defaultCacheMu.Unlock()
|
|
|
|
return newWithCache(certCache, Default)
|
|
}
|
|
|
|
// New makes a new, valid config based on cfg and
|
|
// uses the provided certificate cache. certCache
|
|
// MUST NOT be nil or this function will panic.
|
|
//
|
|
// Use this method when you have an advanced use case
|
|
// that requires a custom certificate cache and config
|
|
// that may differ from the Default. For example, if
|
|
// not all certificates are managed/renewed the same
|
|
// way, you need to make your own Cache value with a
|
|
// GetConfigForCert callback that returns the correct
|
|
// configuration for each certificate. However, for
|
|
// the vast majority of cases, there will be only a
|
|
// single Config, thus the default cache (which always
|
|
// uses the default Config) and default config will
|
|
// suffice, and you should use New() instead.
|
|
func New(certCache *Cache, cfg Config) *Config {
|
|
if certCache == nil {
|
|
panic("a certificate cache is required")
|
|
}
|
|
if certCache.options.GetConfigForCert == nil {
|
|
panic("cache must have GetConfigForCert set in its options")
|
|
}
|
|
return newWithCache(certCache, cfg)
|
|
}
|
|
|
|
// newWithCache ensures that cfg is a valid config by populating
|
|
// zero-value fields from the Default Config. If certCache is
|
|
// nil, this function panics.
|
|
func newWithCache(certCache *Cache, cfg Config) *Config {
|
|
if certCache == nil {
|
|
panic("cannot make a valid config without a pointer to a certificate cache")
|
|
}
|
|
|
|
if cfg.OnDemand == nil {
|
|
cfg.OnDemand = Default.OnDemand
|
|
}
|
|
if cfg.RenewalWindowRatio == 0 {
|
|
cfg.RenewalWindowRatio = Default.RenewalWindowRatio
|
|
}
|
|
if cfg.OnEvent == nil {
|
|
cfg.OnEvent = Default.OnEvent
|
|
}
|
|
if cfg.KeySource == nil {
|
|
cfg.KeySource = Default.KeySource
|
|
}
|
|
if cfg.DefaultServerName == "" {
|
|
cfg.DefaultServerName = Default.DefaultServerName
|
|
}
|
|
if cfg.OnDemand == nil {
|
|
cfg.OnDemand = Default.OnDemand
|
|
}
|
|
if !cfg.MustStaple {
|
|
cfg.MustStaple = Default.MustStaple
|
|
}
|
|
if cfg.Storage == nil {
|
|
cfg.Storage = Default.Storage
|
|
}
|
|
if cfg.Issuer == nil {
|
|
cfg.Issuer = Default.Issuer
|
|
if cfg.Issuer == nil {
|
|
// okay really, we need an issuer,
|
|
// that's kind of the point; most
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// absolutely don't allow a nil storage,
|
|
// because that would make almost anything
|
|
// a config can do pointless
|
|
if cfg.Storage == nil {
|
|
cfg.Storage = defaultFileStorage
|
|
}
|
|
|
|
// ensure the unexported fields are valid
|
|
cfg.certCache = certCache
|
|
|
|
return &cfg
|
|
}
|
|
|
|
// ManageSync causes the certificates for domainNames to be managed
|
|
// according to cfg. If cfg.OnDemand is not nil, then this simply
|
|
// whitelists the domain names and defers the certificate operations
|
|
// to when they are needed. Otherwise, the certificates for each
|
|
// name are loaded from storage or obtained from the CA. If loaded
|
|
// from storage, they are renewed if they are expiring or expired.
|
|
// It then caches the certificate in memory and is prepared to serve
|
|
// them up during TLS handshakes.
|
|
//
|
|
// Note that name whitelisting for on-demand management only takes
|
|
// effect if cfg.OnDemand.DecisionFunc is not set (is nil); it will
|
|
// not overwrite an existing DecisionFunc, nor will it overwrite
|
|
// its decision; i.e. the implicit whitelist is only used if no
|
|
// DecisionFunc is set.
|
|
//
|
|
// This method is synchronous, meaning that certificates for all
|
|
// domainNames must be successfully obtained (or renewed) before
|
|
// it returns. It returns immediately on the first error for any
|
|
// of the given domainNames. This behavior is recommended for
|
|
// interactive use (i.e. when an administrator is present) so
|
|
// that errors can be reported and fixed immediately.
|
|
func (cfg *Config) ManageSync(domainNames []string) error {
|
|
return cfg.manageAll(nil, domainNames, false)
|
|
}
|
|
|
|
// ManageAsync is the same as ManageSync, except that ACME
|
|
// operations are performed asynchronously (in the background).
|
|
// This method returns before certificates are ready. It is
|
|
// crucial that the administrator monitors the logs and is
|
|
// notified of any errors so that corrective action can be
|
|
// taken as soon as possible. Any errors returned from this
|
|
// method occurred before ACME transactions started.
|
|
//
|
|
// As long as logs are monitored, this method is typically
|
|
// recommended for non-interactive environments.
|
|
//
|
|
// If there are failures loading, obtaining, or renewing a
|
|
// certificate, it will be retried with exponential backoff
|
|
// for up to about 30 days, with a maximum interval of about
|
|
// 24 hours. Cancelling ctx will cancel retries and shut down
|
|
// any goroutines spawned by ManageAsync.
|
|
func (cfg *Config) ManageAsync(ctx context.Context, domainNames []string) error {
|
|
return cfg.manageAll(ctx, domainNames, true)
|
|
}
|
|
|
|
func (cfg *Config) manageAll(ctx context.Context, domainNames []string, async bool) error {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
for _, domainName := range domainNames {
|
|
// if on-demand is configured, defer obtain and renew operations
|
|
if cfg.OnDemand != nil {
|
|
if !cfg.OnDemand.whitelistContains(domainName) {
|
|
cfg.OnDemand.hostWhitelist = append(cfg.OnDemand.hostWhitelist, domainName)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// otherwise, begin management immediately
|
|
err := cfg.manageOne(ctx, domainName, async)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cfg *Config) manageOne(ctx context.Context, domainName string, async bool) error {
|
|
// first try loading existing certificate from storage
|
|
cert, err := cfg.CacheManagedCertificate(domainName)
|
|
if err != nil {
|
|
if _, ok := err.(ErrNotExist); !ok {
|
|
return fmt.Errorf("%s: caching certificate: %v", domainName, err)
|
|
}
|
|
// if we don't have one in storage, obtain one
|
|
obtain := func() error {
|
|
err := cfg.ObtainCert(ctx, domainName, !async)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: obtaining certificate: %w", domainName, err)
|
|
}
|
|
cert, err = cfg.CacheManagedCertificate(domainName)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: caching certificate after obtaining it: %v", domainName, err)
|
|
}
|
|
return nil
|
|
}
|
|
if async {
|
|
// Leave the job name empty so as to allow duplicate 'obtain'
|
|
// jobs; this is because Caddy calls ManageAsync() before the
|
|
// previous config is stopped (and before its context is
|
|
// canceled), which means that if an obtain job is still
|
|
// running for the same domain, Submit() would not queue the
|
|
// new one because it is still running, even though it is
|
|
// (probably) about to be canceled (it might not if the new
|
|
// config fails to finish loading, however). In any case, we
|
|
// presume it is safe to enqueue a duplicate obtain job because
|
|
// either the old one (or sometimes the new one) is about to be
|
|
// canceled. This seems like reasonable logic for any consumer
|
|
// of this lib. See https://github.com/caddyserver/caddy/issues/3202
|
|
jm.Submit(cfg.Logger, "", obtain)
|
|
return nil
|
|
}
|
|
return obtain()
|
|
}
|
|
|
|
// for an existing certificate, make sure it is renewed
|
|
renew := func() error {
|
|
err := cfg.RenewCert(ctx, domainName, !async)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: renewing certificate: %w", domainName, err)
|
|
}
|
|
// successful renewal, so update in-memory cache
|
|
err = cfg.reloadManagedCertificate(cert)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: reloading renewed certificate into memory: %v", domainName, err)
|
|
}
|
|
return nil
|
|
}
|
|
if cert.NeedsRenewal(cfg) {
|
|
if async {
|
|
jm.Submit(cfg.Logger, "renew_"+domainName, renew)
|
|
return nil
|
|
}
|
|
return renew()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ObtainCert obtains a certificate for name using cfg, as long
|
|
// as a certificate does not already exist in storage for that
|
|
// name. The name must qualify and cfg must be flagged as Managed.
|
|
// This function is a no-op if storage already has a certificate
|
|
// for name.
|
|
//
|
|
// It only obtains and stores certificates (and their keys),
|
|
// it does not load them into memory. If interactive is true,
|
|
// the user may be shown a prompt.
|
|
// TODO: consider moving interactive param into the Config struct,
|
|
// 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 {
|
|
if cfg.storageHasCertResources(name) {
|
|
return nil
|
|
}
|
|
issuer, err := cfg.getPrecheckedIssuer(ctx, []string{name}, interactive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if issuer == nil {
|
|
return nil
|
|
}
|
|
return cfg.obtainWithIssuer(ctx, issuer, name, interactive)
|
|
}
|
|
|
|
func loggerNamed(l *zap.Logger, name string) *zap.Logger {
|
|
if l == nil {
|
|
return nil
|
|
}
|
|
return l.Named(name)
|
|
}
|
|
|
|
func (cfg *Config) obtainWithIssuer(ctx context.Context, issuer Issuer, name string, interactive bool) error {
|
|
log := loggerNamed(cfg.Logger, "obtain")
|
|
|
|
if log != nil {
|
|
log.Info("acquiring lock", zap.String("identifier", name))
|
|
}
|
|
|
|
// ensure idempotency of the obtain operation for this name
|
|
lockKey := cfg.lockKey("cert_acme", name)
|
|
err := acquireLock(ctx, cfg.Storage, lockKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if log != nil {
|
|
log.Info("releasing lock", zap.String("identifier", name))
|
|
}
|
|
if err := releaseLock(cfg.Storage, lockKey); err != nil {
|
|
if log != nil {
|
|
log.Error("unable to unlock",
|
|
zap.String("identifier", name),
|
|
zap.String("lock_key", lockKey),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
}()
|
|
if log != nil {
|
|
log.Info("lock acquired", zap.String("identifier", name))
|
|
}
|
|
|
|
f := func(ctx context.Context) error {
|
|
// check if obtain is still needed -- might have been obtained during lock
|
|
if cfg.storageHasCertResources(name) {
|
|
if log != nil {
|
|
log.Info("certificate already exists in storage", zap.String("identifier", name))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
privateKey, err := cfg.KeySource.GenerateKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
privKeyPEM, err := encodePrivateKey(privateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
csr, err := cfg.generateCSR(privateKey, []string{name})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
issuedCert, err := issuer.Issue(ctx, csr)
|
|
if err != nil {
|
|
return fmt.Errorf("[%s] Obtain: %w", name, err)
|
|
}
|
|
|
|
// success - immediately save the certificate resource
|
|
certRes := CertificateResource{
|
|
SANs: namesFromCSR(csr),
|
|
CertificatePEM: issuedCert.Certificate,
|
|
PrivateKeyPEM: privKeyPEM,
|
|
IssuerData: issuedCert.Metadata,
|
|
}
|
|
err = cfg.saveCertResource(certRes)
|
|
if err != nil {
|
|
return fmt.Errorf("[%s] Obtain: saving assets: %v", name, err)
|
|
}
|
|
|
|
cfg.emit("cert_obtained", name)
|
|
|
|
if log != nil {
|
|
log.Info("certificate obtained successfully", zap.String("identifier", name))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if interactive {
|
|
err = f(ctx)
|
|
} else {
|
|
err = doWithRetry(ctx, log, f)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// RenewCert renews the certificate for name using cfg. It stows the
|
|
// renewed certificate and its assets in storage if successful. It
|
|
// DOES NOT update the in-memory cache with the new certificate.
|
|
func (cfg *Config) RenewCert(ctx context.Context, name string, interactive bool) error {
|
|
issuer, err := cfg.getPrecheckedIssuer(ctx, []string{name}, interactive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if issuer == nil {
|
|
return nil
|
|
}
|
|
return cfg.renewWithIssuer(ctx, issuer, name, interactive)
|
|
}
|
|
|
|
func (cfg *Config) renewWithIssuer(ctx context.Context, issuer Issuer, name string, interactive bool) error {
|
|
log := loggerNamed(cfg.Logger, "renew")
|
|
|
|
if log != nil {
|
|
log.Info("acquiring lock", zap.String("identifier", name))
|
|
}
|
|
|
|
// ensure idempotency of the renew operation for this name
|
|
lockKey := cfg.lockKey("cert_acme", name)
|
|
err := acquireLock(ctx, cfg.Storage, lockKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if log != nil {
|
|
log.Info("releasing lock", zap.String("identifier", name))
|
|
}
|
|
if err := releaseLock(cfg.Storage, lockKey); err != nil {
|
|
if log != nil {
|
|
log.Error("unable to unlock",
|
|
zap.String("identifier", name),
|
|
zap.String("lock_key", lockKey),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
}()
|
|
if log != nil {
|
|
log.Info("lock acquired", zap.String("identifier", name))
|
|
}
|
|
|
|
f := func(ctx context.Context) error {
|
|
// prepare for renewal (load PEM cert, key, and meta)
|
|
certRes, err := cfg.loadCertResource(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check if renew is still needed - might have been renewed while waiting for lock
|
|
timeLeft, needsRenew := cfg.managedCertNeedsRenewal(certRes)
|
|
if !needsRenew {
|
|
if log != nil {
|
|
log.Info("certificate appears to have been renewed already",
|
|
zap.String("identifier", name),
|
|
zap.Duration("remaining", timeLeft))
|
|
}
|
|
return nil
|
|
}
|
|
if log != nil {
|
|
log.Info("renewing certificate",
|
|
zap.String("identifier", name),
|
|
zap.Duration("remaining", timeLeft))
|
|
}
|
|
|
|
privateKey, err := decodePrivateKey(certRes.PrivateKeyPEM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
csr, err := cfg.generateCSR(privateKey, []string{name})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
issuedCert, err := issuer.Issue(ctx, csr)
|
|
if err != nil {
|
|
return fmt.Errorf("[%s] Renew: %w", name, err)
|
|
}
|
|
|
|
// success - immediately save the renewed certificate resource
|
|
newCertRes := CertificateResource{
|
|
SANs: namesFromCSR(csr),
|
|
CertificatePEM: issuedCert.Certificate,
|
|
PrivateKeyPEM: certRes.PrivateKeyPEM,
|
|
IssuerData: issuedCert.Metadata,
|
|
}
|
|
err = cfg.saveCertResource(newCertRes)
|
|
if err != nil {
|
|
return fmt.Errorf("[%s] Renew: saving assets: %v", name, err)
|
|
}
|
|
|
|
cfg.emit("cert_renewed", name)
|
|
|
|
if log != nil {
|
|
log.Info("certificate renewed successfully", zap.String("identifier", name))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if interactive {
|
|
err = f(ctx)
|
|
} else {
|
|
err = doWithRetry(ctx, log, f)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (cfg *Config) generateCSR(privateKey crypto.PrivateKey, sans []string) (*x509.CertificateRequest, error) {
|
|
csrTemplate := new(x509.CertificateRequest)
|
|
|
|
for _, name := range sans {
|
|
if ip := net.ParseIP(name); ip != nil {
|
|
csrTemplate.IPAddresses = append(csrTemplate.IPAddresses, ip)
|
|
} else if strings.Contains(name, "@") {
|
|
csrTemplate.EmailAddresses = append(csrTemplate.EmailAddresses, name)
|
|
} else if u, err := url.Parse(name); err == nil && strings.Contains(name, "/") {
|
|
csrTemplate.URIs = append(csrTemplate.URIs, u)
|
|
} else {
|
|
csrTemplate.DNSNames = append(csrTemplate.DNSNames, name)
|
|
}
|
|
}
|
|
|
|
if cfg.MustStaple {
|
|
csrTemplate.ExtraExtensions = append(csrTemplate.ExtraExtensions, mustStapleExtension)
|
|
}
|
|
|
|
csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return x509.ParseCertificateRequest(csrDER)
|
|
}
|
|
|
|
// RevokeCert revokes the certificate for domain via ACME protocol. It requires
|
|
// that cfg.Issuer is properly configured with the same issuer that issued the
|
|
// 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 {
|
|
rev := cfg.Revoker
|
|
if rev == nil {
|
|
rev = Default.Revoker
|
|
}
|
|
|
|
certRes, err := cfg.loadCertResource(domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
issuerKey := cfg.Issuer.IssuerKey()
|
|
|
|
if !cfg.Storage.Exists(StorageKeys.SitePrivateKey(issuerKey, domain)) {
|
|
return fmt.Errorf("private key not found for %s", certRes.SANs)
|
|
}
|
|
|
|
err = rev.Revoke(ctx, certRes, reason)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg.emit("cert_revoked", domain)
|
|
|
|
err = cfg.Storage.Delete(StorageKeys.SiteCert(issuerKey, domain))
|
|
if err != nil {
|
|
return fmt.Errorf("certificate revoked, but unable to delete certificate file: %v", err)
|
|
}
|
|
err = cfg.Storage.Delete(StorageKeys.SitePrivateKey(issuerKey, domain))
|
|
if err != nil {
|
|
return fmt.Errorf("certificate revoked, but unable to delete private key: %v", err)
|
|
}
|
|
err = cfg.Storage.Delete(StorageKeys.SiteMeta(issuerKey, domain))
|
|
if err != nil {
|
|
return fmt.Errorf("certificate revoked, but unable to delete certificate metadata: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TLSConfig is an opinionated method that returns a
|
|
// recommended, modern TLS configuration that can be
|
|
// used to configure TLS listeners, which also supports
|
|
// the TLS-ALPN challenge and serves up certificates
|
|
// managed by cfg.
|
|
//
|
|
// Unlike the package TLS() function, this method does
|
|
// not, by itself, enable certificate management for
|
|
// any domain names.
|
|
//
|
|
// Feel free to further customize the returned tls.Config,
|
|
// but do not mess with the GetCertificate or NextProtos
|
|
// fields unless you know what you're doing, as they're
|
|
// necessary to solve the TLS-ALPN challenge.
|
|
func (cfg *Config) TLSConfig() *tls.Config {
|
|
return &tls.Config{
|
|
// these two fields necessary for TLS-ALPN challenge
|
|
GetCertificate: cfg.GetCertificate,
|
|
NextProtos: []string{acmez.ACMETLS1Protocol},
|
|
|
|
// the rest recommended for modern TLS servers
|
|
MinVersion: tls.VersionTLS12,
|
|
CurvePreferences: []tls.CurveID{
|
|
tls.X25519,
|
|
tls.CurveP256,
|
|
},
|
|
CipherSuites: preferredDefaultCipherSuites(),
|
|
PreferServerCipherSuites: true,
|
|
}
|
|
}
|
|
|
|
// getPrecheckedIssuer returns an Issuer with pre-checks
|
|
// completed, if it is also a PreChecker. It also checks
|
|
// that storage is functioning. If a nil Issuer is returned
|
|
// with a nil error, that means to skip this operation
|
|
// (not an error, just a no-op).
|
|
func (cfg *Config) getPrecheckedIssuer(ctx context.Context, names []string, interactive bool) (Issuer, error) {
|
|
// 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()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed storage check: %v - storage is probably misconfigured", err)
|
|
}
|
|
if prechecker, ok := cfg.Issuer.(PreChecker); ok {
|
|
err := prechecker.PreCheck(ctx, names, interactive)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return cfg.Issuer, nil
|
|
}
|
|
|
|
// checkStorage tests the storage by writing random bytes
|
|
// to a random key, and then loading those bytes and
|
|
// comparing the loaded value. If this fails, the provided
|
|
// cfg.Storage mechanism should not be used.
|
|
func (cfg *Config) checkStorage() error {
|
|
key := fmt.Sprintf("rw_test_%d", weakrand.Int())
|
|
contents := make([]byte, 1024*10) // size sufficient for one or two ACME resources
|
|
_, err := weakrand.Read(contents)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = cfg.Storage.Store(key, contents)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
deleteErr := cfg.Storage.Delete(key)
|
|
if deleteErr != nil {
|
|
if cfg.Logger != nil {
|
|
cfg.Logger.Error("deleting test key from storage",
|
|
zap.String("key", key), zap.Error(err))
|
|
}
|
|
}
|
|
// if there was no other error, make sure
|
|
// to return any error returned from Delete
|
|
if err == nil {
|
|
err = deleteErr
|
|
}
|
|
}()
|
|
loaded, err := cfg.Storage.Load(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !bytes.Equal(contents, loaded) {
|
|
return fmt.Errorf("load yielded different value than was stored; expected %d bytes, got %d bytes of differing elements", len(contents), len(loaded))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// storageHasCertResources returns true if the storage
|
|
// associated with cfg's certificate cache has all the
|
|
// resources related to the certificate for domain: the
|
|
// certificate, the private key, and the metadata.
|
|
func (cfg *Config) storageHasCertResources(domain string) bool {
|
|
issuerKey := cfg.Issuer.IssuerKey()
|
|
certKey := StorageKeys.SiteCert(issuerKey, domain)
|
|
keyKey := StorageKeys.SitePrivateKey(issuerKey, domain)
|
|
metaKey := StorageKeys.SiteMeta(issuerKey, domain)
|
|
return cfg.Storage.Exists(certKey) &&
|
|
cfg.Storage.Exists(keyKey) &&
|
|
cfg.Storage.Exists(metaKey)
|
|
}
|
|
|
|
// 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.
|
|
func (cfg *Config) lockKey(op, domainName string) string {
|
|
return fmt.Sprintf("%s_%s_%s", op, domainName, cfg.Issuer.IssuerKey())
|
|
}
|
|
|
|
// managedCertNeedsRenewal returns true if certRes is
|
|
// expiring soon or already expired, or if the process
|
|
// of checking the expiration returned an error.
|
|
func (cfg *Config) managedCertNeedsRenewal(certRes CertificateResource) (time.Duration, bool) {
|
|
cert, err := makeCertificate(certRes.CertificatePEM, certRes.PrivateKeyPEM)
|
|
if err != nil {
|
|
return 0, true
|
|
}
|
|
return time.Until(cert.Leaf.NotAfter), cert.NeedsRenewal(cfg)
|
|
}
|
|
|
|
func (cfg *Config) emit(eventName string, data interface{}) {
|
|
if cfg.OnEvent == nil {
|
|
return
|
|
}
|
|
cfg.OnEvent(eventName, data)
|
|
}
|
|
|
|
// CertificateSelector is a type which can select a certificate to use given multiple choices.
|
|
type CertificateSelector interface {
|
|
SelectCertificate(*tls.ClientHelloInfo, []Certificate) (Certificate, error)
|
|
}
|
|
|
|
// Constants for PKIX MustStaple extension.
|
|
var (
|
|
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
|
ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
|
mustStapleExtension = pkix.Extension{
|
|
Id: tlsFeatureExtensionOID,
|
|
Value: ocspMustStapleFeature,
|
|
}
|
|
)
|