Commit 9e4a2919 authored by Matt Holt's avatar Matt Holt Committed by GitHub

caddytls: Fix handling of IP-only TLS configs and empty-SNI handshakes (#2452)

* caddytls: Fix empty SNI handling (new -default-sni flag)

vendor: update certmagic, needed to support this

Hopefully fixes #2451, fixes #2438, and fixes #2414

* caddytls: Don't overwrite certmagic Manager (fixes #2407)

Supersedes #2447

* vendor: Update certmagic to fix nil pointer deref and TLS-ALPN cleanup

* Improve -default-sni flag help text
parent fa10b027
...@@ -46,6 +46,7 @@ func init() { ...@@ -46,6 +46,7 @@ func init() {
flag.BoolVar(&certmagic.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement") flag.BoolVar(&certmagic.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&certmagic.CA, "ca", certmagic.CA, "URL to certificate authority's ACME server directory") flag.StringVar(&certmagic.CA, "ca", certmagic.CA, "URL to certificate authority's ACME server directory")
flag.StringVar(&certmagic.DefaultServerName, "default-sni", certmagic.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
flag.BoolVar(&certmagic.DisableHTTPChallenge, "disable-http-challenge", certmagic.DisableHTTPChallenge, "Disable the ACME HTTP challenge") flag.BoolVar(&certmagic.DisableHTTPChallenge, "disable-http-challenge", certmagic.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
flag.BoolVar(&certmagic.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge") flag.BoolVar(&certmagic.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable") flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
...@@ -108,7 +109,7 @@ func Run() { ...@@ -108,7 +109,7 @@ func Run() {
} }
} }
//Load all additional envs as soon as possible // load all additional envs as soon as possible
if err := LoadEnvFromFile(envFile); err != nil { if err := LoadEnvFromFile(envFile); err != nil {
mustLogFatalf("%v", err) mustLogFatalf("%v", err)
} }
......
...@@ -93,7 +93,7 @@ type Config struct { ...@@ -93,7 +93,7 @@ type Config struct {
} }
// NewConfig returns a new Config with a pointer to the instance's // NewConfig returns a new Config with a pointer to the instance's
// certificate cache. You will usually need to set Other fields on // certificate cache. You will usually need to set other fields on
// the returned Config for successful practical use. // the returned Config for successful practical use.
func NewConfig(inst *caddy.Instance) *Config { func NewConfig(inst *caddy.Instance) *Config {
inst.StorageMu.RLock() inst.StorageMu.RLock()
...@@ -257,9 +257,13 @@ func MakeTLSConfig(configs []*Config) (*tls.Config, error) { ...@@ -257,9 +257,13 @@ func MakeTLSConfig(configs []*Config) (*tls.Config, error) {
// configs with the same hostname pattern; should // configs with the same hostname pattern; should
// be OK since we already asserted they are roughly // be OK since we already asserted they are roughly
// the same); during TLS handshakes, configs are // the same); during TLS handshakes, configs are
// loaded based on the hostname pattern, according // loaded based on the hostname pattern according
// to client's SNI // to client's ServerName (SNI) value
configMap[cfg.Hostname] = cfg if cfg.Hostname == "0.0.0.0" || cfg.Hostname == "::" {
configMap[""] = cfg
} else {
configMap[cfg.Hostname] = cfg
}
} }
// Is TLS disabled? By now, we know that all // Is TLS disabled? By now, we know that all
......
...@@ -17,6 +17,8 @@ package caddytls ...@@ -17,6 +17,8 @@ package caddytls
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"log"
"net"
"strings" "strings"
"github.com/mholt/caddy/telemetry" "github.com/mholt/caddy/telemetry"
...@@ -38,14 +40,31 @@ type configGroup map[string]*Config ...@@ -38,14 +40,31 @@ type configGroup map[string]*Config
// This function follows nearly the same logic to lookup // This function follows nearly the same logic to lookup
// a hostname as the getCertificate function uses. // a hostname as the getCertificate function uses.
func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config { func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config {
name := certmagic.CertNameFromClientHello(hello) name := certmagic.NormalizedName(hello.ServerName)
if name == "" {
name = certmagic.NormalizedName(certmagic.DefaultServerName)
}
// if SNI is empty, prefer matching IP address (it is
// more specific than a "catch-all" configuration)
if name == "" && hello.Conn != nil {
addr := hello.Conn.LocalAddr().String()
ip, _, err := net.SplitHostPort(addr)
if err == nil {
addr = ip
}
if config, ok := cg[addr]; ok {
return config
}
}
// exact match? great, let's use it // otherwise, try an exact match
if config, ok := cg[name]; ok { if config, ok := cg[name]; ok {
return config return config
} }
// try replacing labels in the name with wildcards until we get a match // then try replacing labels in the name with
// wildcards until we get a match
labels := strings.Split(name, ".") labels := strings.Split(name, ".")
for i := range labels { for i := range labels {
labels[i] = "*" labels[i] = "*"
...@@ -55,14 +74,25 @@ func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config { ...@@ -55,14 +74,25 @@ func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config {
} }
} }
// try a config that serves all names (the above // try a config that matches all names - this
// loop doesn't try empty string; for hosts defined // is needed to match configs defined without
// with only a port, for instance, like ":443") - // a specific host, like ":443", when SNI is
// also known as the default config // a non-empty value
if config, ok := cg[""]; ok { if config, ok := cg[""]; ok {
return config return config
} }
// failover with a random config: this is necessary
// because we might be needing to solve a TLS-ALPN
// ACME challenge for a name that we don't have a
// TLS configuration for; any config will do for
// this purpose
for _, config := range cg {
return config
}
log.Printf("[ERROR] No TLS configuration available for ClientHello with ServerName: %s", hello.ServerName)
return nil return nil
} }
......
...@@ -54,18 +54,6 @@ func setupTLS(c *caddy.Controller) error { ...@@ -54,18 +54,6 @@ func setupTLS(c *caddy.Controller) error {
config.Enabled = true config.Enabled = true
// a single certificate cache is used by the whole caddy.Instance; get a pointer to it
certCache, ok := c.Get(CertCacheInstStorageKey).(*certmagic.Cache)
if !ok || certCache == nil {
certCache = certmagic.NewCache(certmagic.DefaultStorage)
c.OnShutdown(func() error {
certCache.Stop()
return nil
})
c.Set(CertCacheInstStorageKey, certCache)
}
config.Manager = certmagic.NewWithCache(certCache, certmagic.Config{})
// we use certmagic events to collect metrics for telemetry // we use certmagic events to collect metrics for telemetry
config.Manager.OnEvent = func(event string, data interface{}) { config.Manager.OnEvent = func(event string, data interface{}) {
switch event { switch event {
......
...@@ -47,11 +47,18 @@ func TestMain(m *testing.M) { ...@@ -47,11 +47,18 @@ func TestMain(m *testing.M) {
} }
func TestSetupParseBasic(t *testing.T) { func TestSetupParseBasic(t *testing.T) {
cfg := &Config{Manager: &certmagic.Config{}} tmpdir, err := ioutil.TempDir("", "caddytls_setup_test_")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
certCache := certmagic.NewCache(&certmagic.FileStorage{Path: tmpdir})
cfg := &Config{Manager: certmagic.NewWithCache(certCache, certmagic.Config{})}
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
c := caddy.NewTestController("", `tls `+certFile+` `+keyFile+``) c := caddy.NewTestController("", `tls `+certFile+` `+keyFile+``)
err := setupTLS(c) err = setupTLS(c)
if err != nil { if err != nil {
t.Errorf("Expected no errors, got: %v", err) t.Errorf("Expected no errors, got: %v", err)
} }
...@@ -126,11 +133,18 @@ func TestSetupParseWithOptionalParams(t *testing.T) { ...@@ -126,11 +133,18 @@ func TestSetupParseWithOptionalParams(t *testing.T) {
alpn http/1.1 alpn http/1.1
}` }`
cfg := &Config{Manager: &certmagic.Config{}} tmpdir, err := ioutil.TempDir("", "caddytls_setup_test_")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
certCache := certmagic.NewCache(&certmagic.FileStorage{Path: tmpdir})
cfg := &Config{Manager: certmagic.NewWithCache(certCache, certmagic.Config{})}
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
c := caddy.NewTestController("", params) c := caddy.NewTestController("", params)
err := setupTLS(c) err = setupTLS(c)
if err != nil { if err != nil {
t.Errorf("Expected no errors, got: %v", err) t.Errorf("Expected no errors, got: %v", err)
} }
......
...@@ -315,7 +315,7 @@ func (cfg *Config) cacheCertificate(cert Certificate) Certificate { ...@@ -315,7 +315,7 @@ func (cfg *Config) cacheCertificate(cert Certificate) Certificate {
// (yes, if certs overlap in the names they serve, one will // (yes, if certs overlap in the names they serve, one will
// overwrite another here, but that's just how it goes) // overwrite another here, but that's just how it goes)
for _, name := range cert.Names { for _, name := range cert.Names {
cfg.certificates[name] = cert.Hash cfg.certificates[NormalizedName(name)] = cert.Hash
} }
// store the certificate // store the certificate
......
...@@ -484,6 +484,12 @@ var ( ...@@ -484,6 +484,12 @@ var (
// the risk of rate limiting. // the risk of rate limiting.
CertObtainTimeout time.Duration CertObtainTimeout time.Duration
// Set the default server name for clients
// not indicating a server name using SNI.
// In most cases this will be the primary
// domain that is being served.
DefaultServerName string
// The state needed to operate on-demand TLS // The state needed to operate on-demand TLS
OnDemand *OnDemandConfig OnDemand *OnDemandConfig
......
...@@ -215,7 +215,7 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) { ...@@ -215,7 +215,7 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) {
// lockKey returns a key for a lock that is specific to the operation // 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. // named op being performed related to domainName and this config's CA.
func (cfg *Config) lockKey(op, domainName string) string { func (cfg *Config) lockKey(op, domainName string) string {
return fmt.Sprintf("%s:%s:%s", op, domainName, cfg.CA) return fmt.Sprintf("%s_%s_%s", op, domainName, cfg.CA)
} }
// Obtain obtains a single certificate for name. It stores the certificate // Obtain obtains a single certificate for name. It stores the certificate
......
...@@ -101,6 +101,11 @@ type Config struct { ...@@ -101,6 +101,11 @@ type Config struct {
// the risk of rate limiting. // the risk of rate limiting.
CertObtainTimeout time.Duration 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 // The state needed to operate on-demand TLS
OnDemand *OnDemandConfig OnDemand *OnDemandConfig
...@@ -207,6 +212,9 @@ func NewWithCache(certCache *Cache, cfg Config) *Config { ...@@ -207,6 +212,9 @@ func NewWithCache(certCache *Cache, cfg Config) *Config {
if cfg.CertObtainTimeout == 0 { if cfg.CertObtainTimeout == 0 {
cfg.CertObtainTimeout = CertObtainTimeout cfg.CertObtainTimeout = CertObtainTimeout
} }
if cfg.DefaultServerName == "" {
cfg.DefaultServerName = DefaultServerName
}
if cfg.OnDemand == nil { if cfg.OnDemand == nil {
cfg.OnDemand = OnDemand cfg.OnDemand = OnDemand
} }
......
...@@ -67,13 +67,8 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif ...@@ -67,13 +67,8 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif
} }
} }
wrapped := wrappedClientHelloInfo{
ClientHelloInfo: clientHello,
serverNameOrIP: CertNameFromClientHello(clientHello),
}
// get the certificate and serve it up // get the certificate and serve it up
cert, err := cfg.getCertDuringHandshake(wrapped, true, true) cert, err := cfg.getCertDuringHandshake(clientHello, true, true)
if err == nil && cfg.OnEvent != nil { if err == nil && cfg.OnEvent != nil {
cfg.OnEvent("tls_handshake_completed", clientHello) cfg.OnEvent("tls_handshake_completed", clientHello)
} }
...@@ -96,57 +91,82 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif ...@@ -96,57 +91,82 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif
// which is by the Go Authors. // which is by the Go Authors.
// //
// This function is safe for concurrent use. // This function is safe for concurrent use.
func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defaulted bool) { func (cfg *Config) getCertificate(hello *tls.ClientHelloInfo) (cert Certificate, matched, defaulted bool) {
var certKey string name := NormalizedName(hello.ServerName)
var ok bool
cfg.certCache.mu.RLock() cfg.certCache.mu.RLock()
defer cfg.certCache.mu.RUnlock() defer cfg.certCache.mu.RUnlock()
// exact match? great, let's use it var certKey string
if certKey, ok = cfg.certificates[name]; ok { var ok bool
cert = cfg.certCache.cache[certKey]
matched = true
return
}
// try replacing labels in the name with wildcards until we get a match if name == "" {
labels := strings.Split(name, ".") // if SNI is empty, prefer matching IP address
for i := range labels { if hello.Conn != nil {
labels[i] = "*" addr := hello.Conn.LocalAddr().String()
candidate := strings.Join(labels, ".") ip, _, err := net.SplitHostPort(addr)
if certKey, ok = cfg.certificates[candidate]; ok { if err == nil {
addr = ip
}
if certKey, ok = cfg.certificates[addr]; ok {
cert = cfg.certCache.cache[certKey]
matched = true
return
}
}
// fall back to a "default" certificate, if specified
if cfg.DefaultServerName != "" {
normDefault := NormalizedName(cfg.DefaultServerName)
if certKey, ok := cfg.certificates[normDefault]; ok {
cert = cfg.certCache.cache[certKey]
defaulted = true
return
}
}
} else {
// if SNI is specified, try an exact match first
if certKey, ok = cfg.certificates[name]; ok {
cert = cfg.certCache.cache[certKey] cert = cfg.certCache.cache[certKey]
matched = true matched = true
return return
} }
}
// check the certCache directly to see if the SNI name is // try replacing labels in the name with
// already the key of the certificate it wants; this implies // wildcards until we get a match
// that the SNI can contain the hash of a specific cert labels := strings.Split(name, ".")
// (chain) it wants and we will still be able to serve it up for i := range labels {
// (this behavior, by the way, could be controversial as to labels[i] = "*"
// whether it complies with RFC 6066 about SNI, but I think candidate := strings.Join(labels, ".")
// it does, soooo...) if certKey, ok = cfg.certificates[candidate]; ok {
// (this is how we solved the former ACME TLS-SNI challenge) cert = cfg.certCache.cache[certKey]
if directCert, ok := cfg.certCache.cache[name]; ok { matched = true
cert = directCert return
matched = true }
return }
}
// if nothing matches, use a "default" certificate (See issues // check the certCache directly to see if the SNI name is
// mholt/caddy#2035 and mholt/caddy#1303; any change to this // already the key of the certificate it wants; this implies
// behavior must account for hosts defined like ":443" or // that the SNI can contain the hash of a specific cert
// "0.0.0.0:443" where the hostname is empty or a catch-all // (chain) it wants and we will still be able to serve it up
// IP or something.) // (this behavior, by the way, could be controversial as to
if certKey, ok := cfg.certificates[""]; ok { // whether it complies with RFC 6066 about SNI, but I think
cert = cfg.certCache.cache[certKey] // it does, soooo...)
defaulted = true // (this is how we solved the former ACME TLS-SNI challenge)
return if directCert, ok := cfg.certCache.cache[name]; ok {
cert = directCert
matched = true
return
}
} }
// otherwise, we're bingo on ammo; see issues
// mholt/caddy#2035 and mholt/caddy#1303 (any
// change to certificate matching behavior must
// account for hosts defined where the hostname
// is empty or a catch-all, like ":443" or
// "0.0.0.0:443")
return return
} }
...@@ -161,9 +181,11 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau ...@@ -161,9 +181,11 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau
// certificate is available. // certificate is available.
// //
// This function is safe for concurrent use. // This function is safe for concurrent use.
func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) { func (cfg *Config) getCertDuringHandshake(hello *tls.ClientHelloInfo, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
name := NormalizedName(hello.ServerName)
// First check our in-memory cache to see if we've already loaded it // First check our in-memory cache to see if we've already loaded it
cert, matched, defaulted := cfg.getCertificate(hello.serverNameOrIP) cert, matched, defaulted := cfg.getCertificate(hello)
if matched { if matched {
return cert, nil return cert, nil
} }
...@@ -172,11 +194,11 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe ...@@ -172,11 +194,11 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
// obtain a needed certificate // obtain a needed certificate
if cfg.OnDemand != nil && loadIfNecessary { if cfg.OnDemand != nil && loadIfNecessary {
// Then check to see if we have one on disk // Then check to see if we have one on disk
loadedCert, err := cfg.CacheManagedCertificate(hello.serverNameOrIP) loadedCert, err := cfg.CacheManagedCertificate(name)
if err == nil { if err == nil {
loadedCert, err = cfg.handshakeMaintenance(hello, loadedCert) loadedCert, err = cfg.handshakeMaintenance(hello, loadedCert)
if err != nil { if err != nil {
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", hello.serverNameOrIP, err) log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
} }
return loadedCert, nil return loadedCert, nil
} }
...@@ -184,14 +206,14 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe ...@@ -184,14 +206,14 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
// By this point, we need to ask the CA for a certificate // By this point, we need to ask the CA for a certificate
// Make sure the certificate should be obtained based on config // Make sure the certificate should be obtained based on config
err := cfg.checkIfCertShouldBeObtained(hello.serverNameOrIP) err := cfg.checkIfCertShouldBeObtained(name)
if err != nil { if err != nil {
return Certificate{}, err return Certificate{}, err
} }
// Name has to qualify for a certificate // Name has to qualify for a certificate
if !HostQualifies(hello.serverNameOrIP) { if !HostQualifies(name) {
return cert, fmt.Errorf("hostname '%s' does not qualify for certificate", hello.serverNameOrIP) return cert, fmt.Errorf("hostname '%s' does not qualify for certificate", name)
} }
// Obtain certificate from the CA // Obtain certificate from the CA
...@@ -204,7 +226,7 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe ...@@ -204,7 +226,7 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
return cert, nil return cert, nil
} }
return Certificate{}, fmt.Errorf("no certificate available for %s", hello.serverNameOrIP) return Certificate{}, fmt.Errorf("no certificate available for '%s'", name)
} }
// checkIfCertShouldBeObtained checks to see if an on-demand tls certificate // checkIfCertShouldBeObtained checks to see if an on-demand tls certificate
...@@ -222,10 +244,12 @@ func (cfg *Config) checkIfCertShouldBeObtained(name string) error { ...@@ -222,10 +244,12 @@ func (cfg *Config) checkIfCertShouldBeObtained(name string) error {
// hello, it will wait and use what the other goroutine obtained. // hello, it will wait and use what the other goroutine obtained.
// //
// This function is safe for use by multiple concurrent goroutines. // This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Certificate, error) { func (cfg *Config) obtainOnDemandCertificate(hello *tls.ClientHelloInfo) (Certificate, error) {
name := NormalizedName(hello.ServerName)
// We must protect this process from happening concurrently, so synchronize. // We must protect this process from happening concurrently, so synchronize.
obtainCertWaitChansMu.Lock() obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[hello.serverNameOrIP] wait, ok := obtainCertWaitChans[name]
if ok { if ok {
// lucky us -- another goroutine is already obtaining the certificate. // lucky us -- another goroutine is already obtaining the certificate.
// wait for it to finish obtaining the cert and then we'll use it. // wait for it to finish obtaining the cert and then we'll use it.
...@@ -237,32 +261,32 @@ func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Cert ...@@ -237,32 +261,32 @@ func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Cert
// 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 // make a chan others can wait on if needed
wait = make(chan struct{}) wait = make(chan struct{})
obtainCertWaitChans[hello.serverNameOrIP] = wait obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
// obtain the certificate // obtain the certificate
log.Printf("[INFO] Obtaining new certificate for %s", hello.serverNameOrIP) log.Printf("[INFO] Obtaining new certificate for %s", name)
err := cfg.ObtainCert(hello.serverNameOrIP, false) err := cfg.ObtainCert(name, false)
// immediately unblock anyone waiting for it; doing this in // immediately unblock anyone waiting for it; doing this in
// a defer would risk deadlock because of the recursive call // a defer would risk deadlock because of the recursive call
// to getCertDuringHandshake below when we return! // to getCertDuringHandshake below when we return!
obtainCertWaitChansMu.Lock() obtainCertWaitChansMu.Lock()
close(wait) close(wait)
delete(obtainCertWaitChans, hello.serverNameOrIP) delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
if 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()
failedIssuance[hello.serverNameOrIP] = time.Now() failedIssuance[name] = time.Now()
go func(name string) { go func(name string) {
time.Sleep(5 * time.Minute) time.Sleep(5 * time.Minute)
failedIssuanceMu.Lock() failedIssuanceMu.Lock()
delete(failedIssuance, name) delete(failedIssuance, name)
failedIssuanceMu.Unlock() failedIssuanceMu.Unlock()
}(hello.serverNameOrIP) }(name)
failedIssuanceMu.Unlock() failedIssuanceMu.Unlock()
return Certificate{}, err return Certificate{}, err
} }
...@@ -280,7 +304,7 @@ func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Cert ...@@ -280,7 +304,7 @@ func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Cert
// handshakeMaintenance performs a check on cert for expiration and OCSP validity. // handshakeMaintenance performs a check on cert for expiration and OCSP validity.
// //
// This function is safe for use by multiple concurrent goroutines. // This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certificate) (Certificate, error) { func (cfg *Config) handshakeMaintenance(hello *tls.ClientHelloInfo, cert Certificate) (Certificate, error) {
// Check cert expiration // Check cert expiration
timeLeft := cert.NotAfter.Sub(time.Now().UTC()) timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < cfg.RenewDurationBefore { if timeLeft < cfg.RenewDurationBefore {
...@@ -296,7 +320,7 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi ...@@ -296,7 +320,7 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi
if err != nil { if err != nil {
// An error with OCSP stapling is not the end of the world, and in fact, is // An error with OCSP stapling is not the end of the world, and in fact, is
// quite common considering not all certs have issuer URLs that support it. // quite common considering not all certs have issuer URLs that support it.
log.Printf("[ERROR] Getting OCSP for %s: %v", hello.serverNameOrIP, err) log.Printf("[ERROR] Getting OCSP for %s: %v", hello.ServerName, err)
} }
cfg.certCache.mu.Lock() cfg.certCache.mu.Lock()
cfg.certCache.cache[cert.Hash] = cert cfg.certCache.cache[cert.Hash] = cert
...@@ -313,10 +337,11 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi ...@@ -313,10 +337,11 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi
// ClientHello. // ClientHello.
// //
// This function is safe for use by multiple concurrent goroutines. // This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, currentCert Certificate) (Certificate, error) { func (cfg *Config) renewDynamicCertificate(hello *tls.ClientHelloInfo, currentCert Certificate) (Certificate, error) {
name := NormalizedName(hello.ServerName)
obtainCertWaitChansMu.Lock() obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[hello.serverNameOrIP] wait, ok := obtainCertWaitChans[name]
if ok { if ok {
// lucky us -- another goroutine is already renewing the certificate. // lucky us -- another goroutine is already renewing the certificate.
// wait for it to finish, then we'll use the new one. // wait for it to finish, then we'll use the new one.
...@@ -327,24 +352,24 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current ...@@ -327,24 +352,24 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current
// looks like it's up to us to do all the work and renew the cert // looks like it's up to us to do all the work and renew the cert
wait = make(chan struct{}) wait = make(chan struct{})
obtainCertWaitChans[hello.serverNameOrIP] = wait obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
// renew and reload the certificate // renew and reload the certificate
log.Printf("[INFO] Renewing certificate for %s", hello.serverNameOrIP) log.Printf("[INFO] Renewing certificate for %s", name)
err := cfg.RenewCert(hello.serverNameOrIP, false) err := cfg.RenewCert(name, false)
if err == nil { if err == nil {
// even though the recursive nature of the dynamic cert loading // even though the recursive nature of the dynamic cert loading
// would just call this function anyway, we do it here to // would just call this function anyway, we do it here to
// make the replacement as atomic as possible. // make the replacement as atomic as possible.
newCert, err := currentCert.configs[0].CacheManagedCertificate(hello.serverNameOrIP) newCert, err := currentCert.configs[0].CacheManagedCertificate(name)
if err != nil { if err != nil {
log.Printf("[ERROR] loading renewed certificate for %s: %v", hello.serverNameOrIP, err) log.Printf("[ERROR] loading renewed certificate for %s: %v", name, err)
} else { } else {
// replace the old certificate with the new one // replace the old certificate with the new one
err = cfg.certCache.replaceCertificate(currentCert, newCert) err = cfg.certCache.replaceCertificate(currentCert, newCert)
if err != nil { if err != nil {
log.Printf("[ERROR] Replacing certificate for %s: %v", hello.serverNameOrIP, err) log.Printf("[ERROR] Replacing certificate for %s: %v", name, err)
} }
} }
} }
...@@ -354,7 +379,7 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current ...@@ -354,7 +379,7 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current
// to getCertDuringHandshake below when we return! // to getCertDuringHandshake below when we return!
obtainCertWaitChansMu.Lock() obtainCertWaitChansMu.Lock()
close(wait) close(wait)
delete(obtainCertWaitChans, hello.serverNameOrIP) delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
if err != nil { if err != nil {
...@@ -370,7 +395,7 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current ...@@ -370,7 +395,7 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current
// is present, it makes a certificate to solve this challenge and returns it. // is present, it makes a certificate to solve this challenge and returns it.
// A boolean true is returned if a valid certificate is returned. // A boolean true is returned if a valid certificate is returned.
func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInfo) (Certificate, bool, error) { func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInfo) (Certificate, bool, error) {
tokenKey := distributedSolver{}.challengeTokensKey(clientHello.ServerName) tokenKey := distributedSolver{config: cfg}.challengeTokensKey(clientHello.ServerName)
chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey) chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey)
if err != nil { if err != nil {
if _, ok := err.(ErrNotExist); ok { if _, ok := err.(ErrNotExist); ok {
...@@ -396,36 +421,10 @@ func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInf ...@@ -396,36 +421,10 @@ func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInf
return Certificate{Certificate: *cert}, true, nil return Certificate{Certificate: *cert}, true, nil
} }
// CertNameFromClientHello returns a normalized name for which to // NormalizedName returns a cleaned form of serverName that is
// look up a certificate given this ClientHelloInfo. If the client // used for consistency when referring to a SNI value.
// did not send a ServerName value, the connection's local IP is func NormalizedName(serverName string) string {
// assumed. return strings.ToLower(strings.TrimSpace(serverName))
func CertNameFromClientHello(hello *tls.ClientHelloInfo) string {
// Not going to trim trailing dots here since RFC 3546 says,
// "The hostname is represented ... without a trailing dot."
// Just normalize to lowercase and remove any leading or
// trailing whitespace n case the hello was sloppily made
name := strings.ToLower(strings.TrimSpace(hello.ServerName))
// if SNI is not set, assume IP of listener
if name == "" && hello.Conn != nil {
addr := hello.Conn.LocalAddr().String()
ip, _, err := net.SplitHostPort(addr)
if err == nil {
name = ip
}
}
return name
}
// wrappedClientHelloInfo is a type that allows us to
// attach a name with which to look for a certificate
// to a given ClientHelloInfo, since not all clients
// use SNI and some self-signed certificates use IP.
type wrappedClientHelloInfo struct {
*tls.ClientHelloInfo
serverNameOrIP string
} }
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname. // obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
......
...@@ -35,6 +35,8 @@ func (certCache *Cache) maintainAssets() { ...@@ -35,6 +35,8 @@ func (certCache *Cache) maintainAssets() {
renewalTicker := time.NewTicker(certCache.RenewInterval) renewalTicker := time.NewTicker(certCache.RenewInterval)
ocspTicker := time.NewTicker(certCache.OCSPInterval) ocspTicker := time.NewTicker(certCache.OCSPInterval)
log.Printf("[INFO][%s] Started certificate maintenance routine", certCache.storage)
for { for {
select { select {
case <-renewalTicker.C: case <-renewalTicker.C:
......
...@@ -50,7 +50,7 @@ func (s tlsALPNSolver) Present(domain, token, keyAuth string) error { ...@@ -50,7 +50,7 @@ func (s tlsALPNSolver) Present(domain, token, keyAuth string) error {
// CleanUp removes the challenge certificate from the cache. // CleanUp removes the challenge certificate from the cache.
func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error { func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error {
s.certCache.mu.Lock() s.certCache.mu.Lock()
delete(s.certCache.cache, domain) delete(s.certCache.cache, tlsALPNCertKeyName(domain))
s.certCache.mu.Unlock() s.certCache.mu.Unlock()
return nil return nil
} }
......
...@@ -138,7 +138,7 @@ ...@@ -138,7 +138,7 @@
"importpath": "github.com/mholt/certmagic", "importpath": "github.com/mholt/certmagic",
"repository": "https://github.com/mholt/certmagic", "repository": "https://github.com/mholt/certmagic",
"vcs": "git", "vcs": "git",
"revision": "c1d472b46046ee329c099086d689ada0c44d56b0", "revision": "a7f18a937c080b88693cd4e14d48e42cc067b268",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
......
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