2018-12-10 22:00:34 -05:00
|
|
|
// 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/xenolf/lego/certcrypto"
|
|
|
|
"github.com/xenolf/lego/challenge"
|
|
|
|
"github.com/xenolf/lego/challenge/tlsalpn01"
|
|
|
|
"github.com/xenolf/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 state needed to operate on-demand TLS
|
|
|
|
OnDemand *OnDemandConfig
|
|
|
|
|
|
|
|
// Add the must staple TLS extension to the
|
|
|
|
// CSR generated by lego/acme
|
|
|
|
MustStaple bool
|
|
|
|
|
|
|
|
// Map of hostname to certificate hash; used
|
|
|
|
// to complete handshakes and serve the right
|
|
|
|
// certificate given SNI
|
|
|
|
certificates map[string]string
|
|
|
|
|
|
|
|
// Pointer to the certificate store to use
|
|
|
|
certCache *Cache
|
|
|
|
|
|
|
|
// Map of client config key to ACME clients
|
|
|
|
// so they can be reused
|
|
|
|
acmeClients map[string]*lego.Client
|
|
|
|
acmeClientsMu *sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDefault returns a new, valid, default config.
|
|
|
|
//
|
|
|
|
// Calling this function signifies your acceptance to
|
|
|
|
// the CA's Subscriber Agreement and/or Terms of Service.
|
|
|
|
func NewDefault() *Config {
|
|
|
|
return New(Config{Agreed: true})
|
|
|
|
}
|
|
|
|
|
|
|
|
// New makes a valid config based on cfg and uses
|
|
|
|
// a default certificate cache. All calls to
|
|
|
|
// New() will use the same certificate cache.
|
|
|
|
func New(cfg Config) *Config {
|
|
|
|
return NewWithCache(defaultCache, cfg)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewWithCache makes a valid new config based on cfg
|
|
|
|
// and uses the provided certificate cache.
|
|
|
|
func NewWithCache(certCache *Cache, cfg Config) *Config {
|
|
|
|
// avoid nil pointers with sensible defaults
|
|
|
|
if certCache == nil {
|
|
|
|
certCache = defaultCache
|
|
|
|
}
|
|
|
|
if certCache.storage == nil {
|
|
|
|
certCache.storage = DefaultStorage
|
|
|
|
}
|
|
|
|
|
|
|
|
// fill in default values
|
|
|
|
if cfg.CA == "" {
|
|
|
|
cfg.CA = CA
|
|
|
|
}
|
|
|
|
if cfg.Email == "" {
|
|
|
|
cfg.Email = Email
|
|
|
|
}
|
|
|
|
if cfg.OnDemand == nil {
|
|
|
|
cfg.OnDemand = OnDemand
|
|
|
|
}
|
|
|
|
if !cfg.Agreed {
|
|
|
|
cfg.Agreed = Agreed
|
|
|
|
}
|
|
|
|
if !cfg.DisableHTTPChallenge {
|
|
|
|
cfg.DisableHTTPChallenge = DisableHTTPChallenge
|
|
|
|
}
|
|
|
|
if !cfg.DisableTLSALPNChallenge {
|
|
|
|
cfg.DisableTLSALPNChallenge = DisableTLSALPNChallenge
|
|
|
|
}
|
|
|
|
if cfg.RenewDurationBefore == 0 {
|
|
|
|
cfg.RenewDurationBefore = RenewDurationBefore
|
|
|
|
}
|
|
|
|
if cfg.RenewDurationBeforeAtStartup == 0 {
|
|
|
|
cfg.RenewDurationBeforeAtStartup = RenewDurationBeforeAtStartup
|
|
|
|
}
|
|
|
|
if cfg.OnEvent == nil {
|
|
|
|
cfg.OnEvent = OnEvent
|
|
|
|
}
|
|
|
|
if cfg.ListenHost == "" {
|
|
|
|
cfg.ListenHost = ListenHost
|
|
|
|
}
|
|
|
|
if cfg.AltHTTPPort == 0 {
|
|
|
|
cfg.AltHTTPPort = AltHTTPPort
|
|
|
|
}
|
|
|
|
if cfg.AltTLSALPNPort == 0 {
|
|
|
|
cfg.AltTLSALPNPort = AltTLSALPNPort
|
|
|
|
}
|
|
|
|
if cfg.DNSProvider == nil {
|
|
|
|
cfg.DNSProvider = DNSProvider
|
|
|
|
}
|
|
|
|
if cfg.KeyType == "" {
|
|
|
|
cfg.KeyType = KeyType
|
|
|
|
}
|
|
|
|
if cfg.OnDemand == nil {
|
|
|
|
cfg.OnDemand = OnDemand
|
|
|
|
}
|
|
|
|
if !cfg.MustStaple {
|
|
|
|
cfg.MustStaple = MustStaple
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure the unexported fields are valid
|
|
|
|
cfg.certificates = make(map[string]string)
|
|
|
|
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
|
2018-12-11 14:13:48 -05:00
|
|
|
// 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.
|
2018-12-10 22:00:34 -05:00
|
|
|
func (cfg *Config) Manage(domainNames []string) error {
|
|
|
|
for _, domainName := range domainNames {
|
|
|
|
// 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() {
|
|
|
|
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 {
|
|
|
|
skip, err := cfg.preObtainOrRenewChecks(name, interactive)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if skip {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// we expect this to be a new site; if the
|
|
|
|
// cert already exists, then no-op
|
|
|
|
if cfg.certCache.storage.Exists(prefixSiteCert(cfg.CA, name)) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
client, err := cfg.newACMEClient(interactive)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return client.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
|
|
|
|
}
|
|
|
|
client, err := cfg.newACMEClient(interactive)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return client.Renew(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RevokeCert revokes the certificate for domain via ACME protocol.
|
|
|
|
func (cfg *Config) RevokeCert(domain string, interactive bool) error {
|
|
|
|
client, err := cfg.newACMEClient(interactive)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return client.Revoke(domain)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TLSConfig returns a TLS configuration that
|
|
|
|
// can be used to configure TLS listeners. It
|
|
|
|
// supports the TLS-ALPN challenge and serves
|
|
|
|
// up certificates managed by cfg.
|
|
|
|
func (cfg *Config) TLSConfig() *tls.Config {
|
|
|
|
return &tls.Config{
|
|
|
|
GetCertificate: cfg.GetCertificate,
|
|
|
|
NextProtos: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// RenewAllCerts triggers a renewal check of all
|
|
|
|
// certificates in the cache. It only renews
|
|
|
|
// certificates if they need to be renewed.
|
|
|
|
// func (cfg *Config) RenewAllCerts(interactive bool) error {
|
|
|
|
// return cfg.certCache.RenewManagedCertificates(interactive)
|
|
|
|
// }
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.Email == "" {
|
|
|
|
var err error
|
|
|
|
cfg.Email, err = cfg.getEmail(allowPrompts)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false, nil
|
|
|
|
}
|