// 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 // 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 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(StorageKeys.SiteCert(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 }