Commit bedad34b authored by Matthew Holt's avatar Matthew Holt

Clean up some significant portions of the TLS management code

parent 0e7635c5
...@@ -23,7 +23,7 @@ func activateHTTPS(cctx caddy.Context) error { ...@@ -23,7 +23,7 @@ func activateHTTPS(cctx caddy.Context) error {
// place certificates and keys on disk // place certificates and keys on disk
for _, c := range ctx.siteConfigs { for _, c := range ctx.siteConfigs {
err := c.TLS.ObtainCert(operatorPresent) err := c.TLS.ObtainCert(c.TLS.Hostname, operatorPresent)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -18,11 +18,13 @@ import ( ...@@ -18,11 +18,13 @@ import (
// acmeMu ensures that only one ACME challenge occurs at a time. // acmeMu ensures that only one ACME challenge occurs at a time.
var acmeMu sync.Mutex var acmeMu sync.Mutex
// ACMEClient is an acme.Client with custom state attached. // ACMEClient is a wrapper over acme.Client with
// some custom state attached. It is used to obtain,
// renew, and revoke certificates with ACME.
type ACMEClient struct { type ACMEClient struct {
*acme.Client
AllowPrompts bool AllowPrompts bool
config *Config config *Config
acmeClient *acme.Client
} }
// newACMEClient creates a new ACMEClient given an email and whether // newACMEClient creates a new ACMEClient given an email and whether
...@@ -100,7 +102,11 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) ...@@ -100,7 +102,11 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
} }
} }
c := &ACMEClient{Client: client, AllowPrompts: allowPrompts, config: config} c := &ACMEClient{
AllowPrompts: allowPrompts,
config: config,
acmeClient: client,
}
if config.DNSProvider == "" { if config.DNSProvider == "" {
// Use HTTP and TLS-SNI challenges by default // Use HTTP and TLS-SNI challenges by default
...@@ -116,15 +122,15 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) ...@@ -116,15 +122,15 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
// See if TLS challenge needs to be handled by our own facilities // See if TLS challenge needs to be handled by our own facilities
if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, TLSSNIChallengePort)) { if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, TLSSNIChallengePort)) {
c.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{}) c.acmeClient.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{})
} }
// Always respect user's bind preferences by using config.ListenHost // Always respect user's bind preferences by using config.ListenHost
err := c.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = c.SetTLSAddress(net.JoinHostPort(config.ListenHost, "")) err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, ""))
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -145,23 +151,50 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) ...@@ -145,23 +151,50 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
} }
// Use the DNS challenge exclusively // Use the DNS challenge exclusively
c.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
c.SetChallengeProvider(acme.DNS01, prov) c.acmeClient.SetChallengeProvider(acme.DNS01, prov)
} }
return c, nil return c, nil
} }
// Obtain obtains a single certificate for names. It stores the certificate // Obtain obtains a single certificate for name. It stores the certificate
// on the disk if successful. // on the disk if successful. This function is safe for concurrent use.
func (c *ACMEClient) Obtain(names []string) error { //
// Right now our storage mechanism only supports one name per certificate,
// so this function (along with Renew and Revoke) only accepts one domain
// as input. It can be easily modified to support SAN certificates if our
// storage mechanism is upgraded later.
//
// Callers who have access to a Config value should use the ObtainCert
// method on that instead of this lower-level method.
func (c *ACMEClient) Obtain(name string) error {
// Get access to ACME storage
storage, err := c.config.StorageFor(c.config.CAUrl)
if err != nil {
return err
}
// We must lock the obtain with the storage engine
if lockObtained, err := storage.LockRegister(name); err != nil {
return err
} else if !lockObtained {
log.Printf("[INFO] Certificate for %v is already being obtained elsewhere", name)
return nil
}
defer func() {
if err := storage.UnlockRegister(name); err != nil {
log.Printf("[ERROR] Unable to unlock obtain lock for %v: %v", name, err)
}
}()
Attempts: Attempts:
for attempts := 0; attempts < 2; attempts++ { for attempts := 0; attempts < 2; attempts++ {
namesObtaining.Add(names) namesObtaining.Add([]string{name})
acmeMu.Lock() acmeMu.Lock()
certificate, failures := c.ObtainCertificate(names, true, nil) certificate, failures := c.acmeClient.ObtainCertificate([]string{name}, true, nil)
acmeMu.Unlock() acmeMu.Unlock()
namesObtaining.Remove(names) namesObtaining.Remove([]string{name})
if len(failures) > 0 { if len(failures) > 0 {
// Error - try to fix it or report it to the user and abort // Error - try to fix it or report it to the user and abort
var errMsg string // we'll combine all the failures into a single error message var errMsg string // we'll combine all the failures into a single error message
...@@ -178,7 +211,7 @@ Attempts: ...@@ -178,7 +211,7 @@ Attempts:
promptedForAgreement = true promptedForAgreement = true
} }
if Agreed || !c.AllowPrompts { if Agreed || !c.AllowPrompts {
err := c.AgreeToTOS() err := c.acmeClient.AgreeToTOS()
if err != nil { if err != nil {
return errors.New("error agreeing to updated terms: " + err.Error()) return errors.New("error agreeing to updated terms: " + err.Error())
} }
...@@ -193,13 +226,9 @@ Attempts: ...@@ -193,13 +226,9 @@ Attempts:
} }
// Success - immediately save the certificate resource // Success - immediately save the certificate resource
storage, err := c.config.StorageFor(c.config.CAUrl)
if err != nil {
return err
}
err = saveCertResource(storage, certificate) err = saveCertResource(storage, certificate)
if err != nil { if err != nil {
return fmt.Errorf("error saving assets for %v: %v", names, err) return fmt.Errorf("error saving assets for %v: %v", name, err)
} }
break break
...@@ -208,13 +237,11 @@ Attempts: ...@@ -208,13 +237,11 @@ Attempts:
return nil return nil
} }
// Renew renews the managed certificate for name. Right now our storage // Renew renews the managed certificate for name. This function is
// mechanism only supports one name per certificate, so this function only // safe for concurrent use.
// accepts one domain as input. It can be easily modified to support SAN
// certificates if, one day, they become desperately needed enough that our
// storage mechanism is upgraded to be more complex to support SAN certs.
// //
// Anyway, this function is safe for concurrent use. // Callers who have access to a Config value should use the RenewCert
// method on that instead of this lower-level method.
func (c *ACMEClient) Renew(name string) error { func (c *ACMEClient) Renew(name string) error {
// Get access to ACME storage // Get access to ACME storage
storage, err := c.config.StorageFor(c.config.CAUrl) storage, err := c.config.StorageFor(c.config.CAUrl)
...@@ -251,7 +278,7 @@ func (c *ACMEClient) Renew(name string) error { ...@@ -251,7 +278,7 @@ func (c *ACMEClient) Renew(name string) error {
for attempts := 0; attempts < 2; attempts++ { for attempts := 0; attempts < 2; attempts++ {
namesObtaining.Add([]string{name}) namesObtaining.Add([]string{name})
acmeMu.Lock() acmeMu.Lock()
newCertMeta, err = c.RenewCertificate(certMeta, true) newCertMeta, err = c.acmeClient.RenewCertificate(certMeta, true)
acmeMu.Unlock() acmeMu.Unlock()
namesObtaining.Remove([]string{name}) namesObtaining.Remove([]string{name})
if err == nil { if err == nil {
...@@ -259,10 +286,10 @@ func (c *ACMEClient) Renew(name string) error { ...@@ -259,10 +286,10 @@ func (c *ACMEClient) Renew(name string) error {
break break
} }
// If the legal terms changed and need to be agreed to again, // If the legal terms were updated and need to be
// we can handle that. // agreed to again, we can handle that.
if _, ok := err.(acme.TOSError); ok { if _, ok := err.(acme.TOSError); ok {
err := c.AgreeToTOS() err := c.acmeClient.AgreeToTOS()
if err != nil { if err != nil {
return err return err
} }
...@@ -304,7 +331,7 @@ func (c *ACMEClient) Revoke(name string) error { ...@@ -304,7 +331,7 @@ func (c *ACMEClient) Revoke(name string) error {
return err return err
} }
err = c.Client.RevokeCertificate(siteData.Cert) err = c.acmeClient.RevokeCertificate(siteData.Cert)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -3,13 +3,9 @@ package caddytls ...@@ -3,13 +3,9 @@ package caddytls
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"time"
"log"
"net/url" "net/url"
"strings" "strings"
...@@ -116,24 +112,21 @@ type OnDemandState struct { ...@@ -116,24 +112,21 @@ type OnDemandState struct {
// If it reaches MaxObtain, on-demand issuances must fail. // If it reaches MaxObtain, on-demand issuances must fail.
ObtainedCount int32 ObtainedCount int32
// Based on max_certs in tls config, it specifies the // Set from max_certs in tls config, it specifies the
// maximum number of certificates that can be issued. // maximum number of certificates that can be issued.
MaxObtain int32 MaxObtain int32
} }
// ObtainCert obtains a certificate for c.Hostname, as long as a certificate // ObtainCert obtains a certificate for name using c, as long
// does not already exist in storage on disk. It only obtains and stores // as a certificate does not already exist in storage for that
// certificates (and their keys) to disk, it does not load them into memory. // name. The name must qualify and c must be flagged as Managed.
// If allowPrompts is true, the user may be shown a prompt. If proxyACME is // This function is a no-op if storage already has a certificate
// true, the relevant ACME challenges will be proxied to the alternate port. // for name.
func (c *Config) ObtainCert(allowPrompts bool) error { //
return c.obtainCertName(c.Hostname, allowPrompts) // It only obtains and stores certificates (and their keys),
} // it does not load them into memory. If allowPrompts is true,
// the user may be shown a prompt.
// obtainCertName gets a certificate for name using the ACME config c func (c *Config) ObtainCert(name string, allowPrompts bool) error {
// if c and name both qualify. It places the certificate in storage.
// It is a no-op if the storage already has a certificate for name.
func (c *Config) obtainCertName(name string, allowPrompts bool) error {
if !c.Managed || !HostQualifies(name) { if !c.Managed || !HostQualifies(name) {
return nil return nil
} }
...@@ -142,29 +135,13 @@ func (c *Config) obtainCertName(name string, allowPrompts bool) error { ...@@ -142,29 +135,13 @@ func (c *Config) obtainCertName(name string, allowPrompts bool) error {
if err != nil { if err != nil {
return err return err
} }
siteExists, err := storage.SiteExists(name) siteExists, err := storage.SiteExists(name)
if err != nil { if err != nil {
return err return err
} }
if siteExists { if siteExists {
return nil return nil
} }
// We must lock the obtain with the storage engine
if lockObtained, err := storage.LockRegister(name); err != nil {
return err
} else if !lockObtained {
log.Printf("[INFO] Certificate for %v is already being obtained elsewhere", name)
return nil
}
defer func() {
if err := storage.UnlockRegister(name); err != nil {
log.Printf("[ERROR] Unable to unlock obtain lock for %v: %v", name, err)
}
}()
if c.ACMEEmail == "" { if c.ACMEEmail == "" {
c.ACMEEmail = getEmail(storage, allowPrompts) c.ACMEEmail = getEmail(storage, allowPrompts)
} }
...@@ -173,86 +150,17 @@ func (c *Config) obtainCertName(name string, allowPrompts bool) error { ...@@ -173,86 +150,17 @@ func (c *Config) obtainCertName(name string, allowPrompts bool) error {
if err != nil { if err != nil {
return err return err
} }
return client.Obtain(name)
return client.Obtain([]string{name})
} }
// RenewCert renews the certificate for c.Hostname. If there is already a lock // RenewCert renews the certificate for name using c. It stows the
// on renewal, this will not perform the renewal and no error will occur. // renewed certificate and its assets in storage if successful.
func (c *Config) RenewCert(allowPrompts bool) error { func (c *Config) RenewCert(name string, allowPrompts bool) error {
return c.renewCertName(c.Hostname, allowPrompts)
}
// renewCertName renews the certificate for the given name. If there is already
// a lock on renewal, this will not perform the renewal and no error will
// occur.
func (c *Config) renewCertName(name string, allowPrompts bool) error {
storage, err := c.StorageFor(c.CAUrl)
if err != nil {
return err
}
// We must lock the renewal with the storage engine
if lockObtained, err := storage.LockRegister(name); err != nil {
return err
} else if !lockObtained {
log.Printf("[INFO] Certificate for %v is already being renewed elsewhere", name)
return nil
}
defer func() {
if err := storage.UnlockRegister(name); err != nil {
log.Printf("[ERROR] Unable to unlock renewal lock for %v: %v", name, err)
}
}()
// Prepare for renewal (load PEM cert, key, and meta)
siteData, err := storage.LoadSite(name)
if err != nil {
return err
}
var certMeta acme.CertificateResource
err = json.Unmarshal(siteData.Meta, &certMeta)
certMeta.Certificate = siteData.Cert
certMeta.PrivateKey = siteData.Key
client, err := newACMEClient(c, allowPrompts) client, err := newACMEClient(c, allowPrompts)
if err != nil { if err != nil {
return err return err
} }
return client.Renew(name)
// Perform renewal and retry if necessary, but not too many times.
var newCertMeta acme.CertificateResource
var success bool
for attempts := 0; attempts < 2; attempts++ {
namesObtaining.Add([]string{name})
acmeMu.Lock()
newCertMeta, err = client.RenewCertificate(certMeta, true)
acmeMu.Unlock()
namesObtaining.Remove([]string{name})
if err == nil {
success = true
break
}
// If the legal terms were updated and need to be
// agreed to again, we can handle that.
if _, ok := err.(acme.TOSError); ok {
err := client.AgreeToTOS()
if err != nil {
return err
}
continue
}
// For any other kind of error, wait 10s and try again.
time.Sleep(10 * time.Second)
}
if !success {
return errors.New("too many renewal attempts; last error: " + err.Error())
}
return saveCertResource(storage, newCertMeta)
} }
// StorageFor obtains a TLS Storage instance for the given CA URL which should // StorageFor obtains a TLS Storage instance for the given CA URL which should
......
...@@ -123,7 +123,6 @@ func (s FileStorage) readFile(file string) ([]byte, error) { ...@@ -123,7 +123,6 @@ func (s FileStorage) readFile(file string) ([]byte, error) {
return nil, ErrNotExist(err) return nil, ErrNotExist(err)
} }
return b, err return b, err
} }
// SiteExists implements Storage.SiteExists by checking for the presence of // SiteExists implements Storage.SiteExists by checking for the presence of
......
...@@ -172,22 +172,25 @@ func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certi ...@@ -172,22 +172,25 @@ func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certi
return cg.getCertDuringHandshake(name, true, false) return cg.getCertDuringHandshake(name, true, false)
} }
// looks like it's up to us to do all the work and obtain the cert // looks like it's up to us to do all the work and obtain the cert.
// make a chan others can wait on if needed
wait = make(chan struct{}) wait = make(chan struct{})
obtainCertWaitChans[name] = wait obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
// Unblock waiters and delete waitgroup when we return // do the obtain
defer func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}()
log.Printf("[INFO] Obtaining new certificate for %s", name) log.Printf("[INFO] Obtaining new certificate for %s", name)
err := cfg.ObtainCert(name, false)
// immediately unblock anyone waiting for it; doing this in
// a defer would risk deadlock because of the recursive call
// to getCertDuringHandshake below when we return!
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
if err := cfg.obtainCertName(name, false); err != nil { if err != nil {
// Failed to solve challenge, so don't allow another on-demand // Failed to solve challenge, so don't allow another on-demand
// issue for this name to be attempted for a little while. // issue for this name to be attempted for a little while.
failedIssuanceMu.Lock() failedIssuanceMu.Lock()
...@@ -208,7 +211,7 @@ func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certi ...@@ -208,7 +211,7 @@ func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certi
lastIssueTime = time.Now() lastIssueTime = time.Now()
lastIssueTimeMu.Unlock() lastIssueTimeMu.Unlock()
// The certificate is already on disk; now just start over to load it and serve it // certificate is already on disk; now just start over to load it and serve it
return cg.getCertDuringHandshake(name, true, false) return cg.getCertDuringHandshake(name, true, false)
} }
...@@ -265,17 +268,18 @@ func (cg configGroup) renewDynamicCertificate(name string, cfg *Config) (Certifi ...@@ -265,17 +268,18 @@ func (cg configGroup) renewDynamicCertificate(name string, cfg *Config) (Certifi
obtainCertWaitChans[name] = wait obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
// unblock waiters and delete waitgroup when we return // do the renew
defer func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}()
log.Printf("[INFO] Renewing certificate for %s", name) log.Printf("[INFO] Renewing certificate for %s", name)
err := cfg.RenewCert(name, false)
// immediately unblock anyone waiting for it; doing this in
// a defer would risk deadlock because of the recursive call
// to getCertDuringHandshake below when we return!
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
err := cfg.renewCertName(name, false)
if err != nil { if err != nil {
return Certificate{}, err return Certificate{}, err
} }
......
...@@ -99,12 +99,20 @@ func RenewManagedCertificates(allowPrompts bool) (err error) { ...@@ -99,12 +99,20 @@ func RenewManagedCertificates(allowPrompts bool) (err error) {
continue continue
} }
// This works well because managed certs are only associated with one name per config. // Get the name which we should use to renew this certificate;
// Note, the renewal inside here may not actually occur and no error will be returned // we only support managing certificates with one name per cert,
// due to renewal lock (i.e. because a renewal is already happening). This lack of // so this should be easy. We can't rely on cert.Config.Hostname
// error is by intention to force cache invalidation as though it has renewed. // because it may be a wildcard value from the Caddyfile (e.g.
err := cert.Config.RenewCert(allowPrompts) // *.something.com) which, as of 2016, is not supported by ACME.
var renewName string
for _, name := range cert.Names {
if name != "" {
renewName = name
break
}
}
err := cert.Config.RenewCert(renewName, allowPrompts)
if err != nil { if err != nil {
if allowPrompts && timeLeft < 0 { if allowPrompts && timeLeft < 0 {
// Certificate renewal failed, the operator is present, and the certificate // Certificate renewal failed, the operator is present, and the certificate
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment