dispatch/vendor/github.com/mholt/certmagic/config.go
2019-06-09 02:01:48 +02:00

470 lines
14 KiB
Go

// 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 (
"crypto/tls"
"fmt"
"sync"
"time"
"github.com/go-acme/lego/certcrypto"
"github.com/go-acme/lego/certificate"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/challenge/tlsalpn01"
"github.com/go-acme/lego/lego"
)
// Config configures a certificate manager instance.
// An empty Config is not valid: use New() to obtain
// a valid Config.
type Config struct {
// The endpoint of the directory for the ACME
// CA we are to use
CA string
// The email address to use when creating or
// selecting an existing ACME server account
Email string
// Set to true if agreed to the CA's
// subscriber agreement
Agreed bool
// Disable all HTTP challenges
DisableHTTPChallenge bool
// Disable all TLS-ALPN challenges
DisableTLSALPNChallenge bool
// How long before expiration to renew certificates
RenewDurationBefore time.Duration
// How long before expiration to require a renewed
// certificate when in interactive mode, like when
// the program is first starting up (see
// mholt/caddy#1680). A wider window between
// RenewDurationBefore and this value will suppress
// errors under duress (bad) but hopefully this duration
// will give it enough time for the blockage to be
// relieved.
RenewDurationBeforeAtStartup time.Duration
// 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{})
// The host (ONLY the host, not port) to listen
// on if necessary to start a listener to solve
// an ACME challenge
ListenHost string
// The alternate port to use for the ACME HTTP
// challenge; if non-empty, this port will be
// used instead of HTTPChallengePort to spin up
// a listener for the HTTP challenge
AltHTTPPort int
// The alternate port to use for the ACME
// TLS-ALPN challenge; the system must forward
// TLSALPNChallengePort to this port for
// challenge to succeed
AltTLSALPNPort int
// The DNS provider to use when solving the
// ACME DNS challenge
DNSProvider challenge.Provider
// The type of key to use when generating
// certificates
KeyType certcrypto.KeyType
// The maximum amount of time to allow for
// obtaining a certificate. If empty, the
// default from the underlying lego lib is
// used. If set, it must not be too low so
// as to cancel orders too early, running
// the risk of rate limiting.
CertObtainTimeout time.Duration
// 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
OnDemand *OnDemandConfig
// Add the must staple TLS extension to the
// CSR generated by lego/acme
MustStaple bool
// The storage to access when storing or
// loading TLS assets
Storage Storage
// NewManager returns a new Manager. If nil,
// an ACME client will be created and used.
NewManager func(interactive bool) (Manager, error)
// Pointer to the in-memory certificate cache
certCache *Cache
// Map of client config key to ACME clients
// so they can be reused
// TODO: It might be better if these were globally cached, rather than per-config, which are ephemeral... but maybe evict them after a certain time, like 1 day or something
acmeClients map[string]*lego.Client
acmeClientsMu *sync.Mutex
}
// 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 Default, 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")
}
// fill in default values
if cfg.CA == "" {
cfg.CA = Default.CA
}
if cfg.Email == "" {
cfg.Email = Default.Email
}
if cfg.OnDemand == nil {
cfg.OnDemand = Default.OnDemand
}
if !cfg.Agreed {
cfg.Agreed = Default.Agreed
}
if !cfg.DisableHTTPChallenge {
cfg.DisableHTTPChallenge = Default.DisableHTTPChallenge
}
if !cfg.DisableTLSALPNChallenge {
cfg.DisableTLSALPNChallenge = Default.DisableTLSALPNChallenge
}
if cfg.RenewDurationBefore == 0 {
cfg.RenewDurationBefore = Default.RenewDurationBefore
}
if cfg.RenewDurationBeforeAtStartup == 0 {
cfg.RenewDurationBeforeAtStartup = Default.RenewDurationBeforeAtStartup
}
if cfg.OnEvent == nil {
cfg.OnEvent = Default.OnEvent
}
if cfg.ListenHost == "" {
cfg.ListenHost = Default.ListenHost
}
if cfg.AltHTTPPort == 0 {
cfg.AltHTTPPort = Default.AltHTTPPort
}
if cfg.AltTLSALPNPort == 0 {
cfg.AltTLSALPNPort = Default.AltTLSALPNPort
}
if cfg.DNSProvider == nil {
cfg.DNSProvider = Default.DNSProvider
}
if cfg.KeyType == "" {
cfg.KeyType = Default.KeyType
}
if cfg.CertObtainTimeout == 0 {
cfg.CertObtainTimeout = Default.CertObtainTimeout
}
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.NewManager == nil {
cfg.NewManager = Default.NewManager
}
// 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
cfg.acmeClients = make(map[string]*lego.Client)
cfg.acmeClientsMu = new(sync.Mutex)
return &cfg
}
// Manage causes the certificates for domainNames to be managed
// according to cfg. If cfg is enabled for OnDemand, then this
// simply whitelists the domain names. Otherwise, the certificate(s)
// for each name are loaded from storage or obtained from the CA;
// and if loaded from storage, renewed if they are expiring or
// expired. It then caches the certificate in memory and is
// prepared to serve them up during TLS handshakes.
func (cfg *Config) Manage(domainNames []string) error {
for _, domainName := range domainNames {
if !HostQualifies(domainName) {
return fmt.Errorf("name does not qualify for automatic certificate management: %s", domainName)
}
// if on-demand is configured, simply whitelist this name
if cfg.OnDemand != nil {
if !cfg.OnDemand.whitelistContains(domainName) {
cfg.OnDemand.HostWhitelist = append(cfg.OnDemand.HostWhitelist, domainName)
}
continue
}
// try loading an existing certificate; if it doesn't
// exist yet, obtain one and try loading it again
cert, err := cfg.CacheManagedCertificate(domainName)
if err != nil {
if _, ok := err.(ErrNotExist); ok {
// if it doesn't exist, get it, then try loading it again
err := cfg.ObtainCert(domainName, false)
if err != nil {
return fmt.Errorf("%s: obtaining certificate: %v", domainName, err)
}
cert, err = cfg.CacheManagedCertificate(domainName)
if err != nil {
return fmt.Errorf("%s: caching certificate after obtaining it: %v", domainName, err)
}
continue
}
return fmt.Errorf("%s: caching certificate: %v", domainName, err)
}
// for existing certificates, make sure it is renewed
if cert.NeedsRenewal(cfg) {
err := cfg.RenewCert(domainName, false)
if err != nil {
return fmt.Errorf("%s: renewing certificate: %v", domainName, err)
}
}
}
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.
func (cfg *Config) ObtainCert(name string, interactive bool) error {
if cfg.storageHasCertResources(name) {
return nil
}
skip, err := cfg.preObtainOrRenewChecks(name, interactive)
if err != nil {
return err
}
if skip {
return nil
}
manager, err := cfg.newManager(interactive)
if err != nil {
return err
}
return manager.Obtain(name)
}
// RenewCert renews the certificate for name using cfg. It stows the
// renewed certificate and its assets in storage if successful.
func (cfg *Config) RenewCert(name string, interactive bool) error {
skip, err := cfg.preObtainOrRenewChecks(name, interactive)
if err != nil {
return err
}
if skip {
return nil
}
manager, err := cfg.newManager(interactive)
if err != nil {
return err
}
return manager.Renew(name)
}
// RevokeCert revokes the certificate for domain via ACME protocol.
func (cfg *Config) RevokeCert(domain string, interactive bool) error {
manager, err := cfg.newManager(interactive)
if err != nil {
return err
}
return manager.Revoke(domain)
}
// 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{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol},
// the rest recommended for modern TLS servers
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
CipherSuites: preferredDefaultCipherSuites(),
PreferServerCipherSuites: true,
}
}
// preObtainOrRenewChecks perform a few simple checks before
// obtaining or renewing a certificate with ACME, and returns
// whether this name should be skipped (like if it's not
// managed TLS) as well as any error. It ensures that the
// config is Managed, that the name qualifies for a certificate,
// and that an email address is available.
func (cfg *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool, error) {
if !HostQualifies(name) {
return true, nil
}
err := cfg.getEmail(allowPrompts)
if err != nil {
return false, err
}
return false, 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 {
certKey := StorageKeys.SiteCert(cfg.CA, domain)
keyKey := StorageKeys.SitePrivateKey(cfg.CA, domain)
metaKey := StorageKeys.SiteMeta(cfg.CA, domain)
return cfg.Storage.Exists(certKey) &&
cfg.Storage.Exists(keyKey) &&
cfg.Storage.Exists(metaKey)
}
// 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 certificate.Resource) bool {
cert, err := makeCertificate(certRes.Certificate, certRes.PrivateKey)
if err != nil {
return true
}
return cert.NeedsRenewal(cfg)
}
// Manager is a type that can manage a certificate.
// They are usually very short-lived.
type Manager interface {
Obtain(name string) error
Renew(name string) error
Revoke(name string) error
}