diff --git a/caddy/build.go b/caddy/build.go index 850d607bd..8277858de 100644 --- a/caddy/build.go +++ b/caddy/build.go @@ -42,11 +42,13 @@ import ( ) var goos, goarch, goarm string +var race bool func init() { flag.StringVar(&goos, "goos", "", "GOOS for which to build") flag.StringVar(&goarch, "goarch", "", "GOARCH for which to build") flag.StringVar(&goarm, "goarm", "", "GOARM for which to build") + flag.BoolVar(&race, "race", false, "Enable race detector") } func main() { @@ -67,6 +69,9 @@ func main() { args := []string{"build", "-ldflags", ldflags} args = append(args, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath)) args = append(args, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath)) + if race { + args = append(args, "-race") + } cmd := exec.Command("go", args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout @@ -77,6 +82,9 @@ func main() { "GOARCH=" + goarch, "GOARM=" + goarm, } { + if race && env == "CGO_ENABLED=0" { + continue + } cmd.Env = append(cmd.Env, env) } diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index a7e3e3149..106b7cd9c 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -33,7 +33,7 @@ import ( "github.com/mholt/caddy" "github.com/mholt/caddy/caddytls" "github.com/mholt/caddy/telemetry" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" "gopkg.in/natefinch/lumberjack.v2" _ "github.com/mholt/caddy/caddyhttp" // plug in the HTTP server type @@ -47,7 +47,7 @@ func init() { flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement") flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-v02.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory") flag.BoolVar(&caddytls.DisableHTTPChallenge, "disable-http-challenge", caddytls.DisableHTTPChallenge, "Disable the ACME HTTP challenge") - flag.BoolVar(&caddytls.DisableTLSSNIChallenge, "disable-tls-sni-challenge", caddytls.DisableTLSSNIChallenge, "Disable the ACME TLS-SNI challenge") + flag.BoolVar(&caddytls.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", caddytls.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge") flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable") flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")") flag.StringVar(&cpu, "cpu", "100%", "CPU cap") diff --git a/caddyhttp/httpserver/https.go b/caddyhttp/httpserver/https.go index a037a86d0..86c15547a 100644 --- a/caddyhttp/httpserver/https.go +++ b/caddyhttp/httpserver/https.go @@ -207,7 +207,7 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig { Addr: Address{Original: addr, Host: host, Port: port}, ListenHost: cfg.ListenHost, middleware: []Middleware{redirMiddleware}, - TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort, AltTLSSNIPort: cfg.TLS.AltTLSSNIPort}, + TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort, AltTLSALPNPort: cfg.TLS.AltTLSALPNPort}, Timeouts: cfg.Timeouts, } } diff --git a/caddyhttp/httpserver/mitm.go b/caddyhttp/httpserver/mitm.go index d2faf5f3f..6736610e9 100644 --- a/caddyhttp/httpserver/mitm.go +++ b/caddyhttp/httpserver/mitm.go @@ -74,6 +74,7 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values) r.Header.Get("X-FCCKV2") != "" || // Fortinet info.advertisesHeartbeatSupport() { // no major browsers have ever implemented Heartbeat + // TODO: Move the heartbeat check into each "looksLike" function... checked = true mitm = true } else if strings.Contains(ua, "Edge") || strings.Contains(ua, "MSIE") || diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 4abcbbb6d..32af0f722 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -169,12 +169,12 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd // If default HTTP or HTTPS ports have been customized, // make sure the ACME challenge ports match - var altHTTPPort, altTLSSNIPort string + var altHTTPPort, altTLSALPNPort string if HTTPPort != DefaultHTTPPort { altHTTPPort = HTTPPort } if HTTPSPort != DefaultHTTPSPort { - altTLSSNIPort = HTTPSPort + altTLSALPNPort = HTTPSPort } // Make our caddytls.Config, which has a pointer to the @@ -183,7 +183,7 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd caddytlsConfig := caddytls.NewConfig(h.instance) caddytlsConfig.Hostname = addr.Host caddytlsConfig.AltHTTPPort = altHTTPPort - caddytlsConfig.AltTLSSNIPort = altTLSSNIPort + caddytlsConfig.AltTLSALPNPort = altTLSALPNPort // Save the config to our master list, and key it for lookups cfg := &SiteConfig{ diff --git a/caddytls/client.go b/caddytls/client.go index 2b27f515d..3c971e48a 100644 --- a/caddytls/client.go +++ b/caddytls/client.go @@ -27,7 +27,7 @@ import ( "github.com/mholt/caddy" "github.com/mholt/caddy/telemetry" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // acmeMu ensures that only one ACME challenge occurs at a time. @@ -121,68 +121,69 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) } if config.DNSProvider == "" { - // Use HTTP and TLS-SNI challenges by default + // Use HTTP and TLS-ALPN challenges by default - // See if HTTP challenge needs to be proxied + // figure out which ports we'll be serving the challenges on useHTTPPort := HTTPChallengePort + useTLSALPNPort := TLSALPNChallengePort if config.AltHTTPPort != "" { useHTTPPort = config.AltHTTPPort } + if config.AltTLSALPNPort != "" { + useTLSALPNPort = config.AltTLSALPNPort + } if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) { useHTTPPort = DefaultHTTPAlternatePort } - // TODO: tls-sni challenge was removed in January 2018, but a variant of it might return - // See which port TLS-SNI challenges will be accomplished on - // useTLSSNIPort := TLSSNIChallengePort - // if config.AltTLSSNIPort != "" { - // useTLSSNIPort = config.AltTLSSNIPort - // } - // err := c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort)) - // if err != nil { - // return nil, err - // } - - // if using file storage, we can distribute the HTTP challenge across - // all instances sharing the acme folder; either way, we must still set - // the address for the default HTTP provider server - var useDistributedHTTPSolver bool + // if using file storage, we can distribute the HTTP or TLS-ALPN challenge + // across all instances sharing the acme folder; either way, we must still + // set the address for the default provider server + var useDistributedSolver bool if storage, err := c.config.StorageFor(c.config.CAUrl); err == nil { if _, ok := storage.(*FileStorage); ok { - useDistributedHTTPSolver = true + useDistributedSolver = true } } - if useDistributedHTTPSolver { - c.acmeClient.SetChallengeProvider(acme.HTTP01, distributedHTTPSolver{ - // being careful to respect user's listener bind preferences - httpProviderServer: acme.NewHTTPProviderServer(config.ListenHost, useHTTPPort), + if useDistributedSolver { + // ... being careful to respect user's listener bind preferences + c.acmeClient.SetChallengeProvider(acme.HTTP01, distributedSolver{ + providerServer: acme.NewHTTPProviderServer(config.ListenHost, useHTTPPort), + }) + c.acmeClient.SetChallengeProvider(acme.TLSALPN01, distributedSolver{ + providerServer: acme.NewTLSALPNProviderServer(config.ListenHost, useTLSALPNPort), }) } else { // Always respect user's bind preferences by using config.ListenHost. - // NOTE(Sep'16): At time of writing, SetHTTPAddress() and SetTLSAddress() - // must be called before SetChallengeProvider() (see above), since they reset - // the challenge provider back to the default one! (still true in March 2018) + // NOTE(Nov'18): At time of writing, SetHTTPAddress() and SetTLSAddress() + // reset the challenge provider back to the default one, overriding + // anything set by SetChalllengeProvider(). Calling them mutually + // excuslively is safe, as is calling Set*Address() before SetChallengeProvider(). err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) if err != nil { return nil, err } + err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, useTLSALPNPort)) + if err != nil { + return nil, err + } } - // TODO: tls-sni challenge was removed in January 2018, but a variant of it might return - // See if TLS challenge needs to be handled by our own facilities - // if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort)) { - // c.acmeClient.SetChallengeProvider(acme.TLSSNI01, tlsSNISolver{certCache: config.certCache}) - // } + // if this server is already listening on the TLS-ALPN port we're supposed to use, + // then wire up this config's ACME client to use our own facilities for solving + // the challenge: our own certificate cache, since we already have a listener + if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useTLSALPNPort)) { + c.acmeClient.SetChallengeProvider(acme.TLSALPN01, tlsALPNSolver{certCache: config.certCache}) + } // Disable any challenges that should not be used var disabledChallenges []acme.Challenge if DisableHTTPChallenge { disabledChallenges = append(disabledChallenges, acme.HTTP01) } - // TODO: tls-sni challenge was removed in January 2018, but a variant of it might return - // if DisableTLSSNIChallenge { - // disabledChallenges = append(disabledChallenges, acme.TLSSNI01) - // } + if DisableTLSALPNChallenge { + disabledChallenges = append(disabledChallenges, acme.TLSALPN01) + } if len(disabledChallenges) > 0 { c.acmeClient.ExcludeChallenges(disabledChallenges) } @@ -203,9 +204,7 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) } // Use the DNS challenge exclusively - // TODO: tls-sni challenge was removed in January 2018, but a variant of it might return - // c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) - c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01}) + c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSALPN01}) c.acmeClient.SetChallengeProvider(acme.DNS01, prov) } @@ -312,7 +311,7 @@ func (c *ACMEClient) Renew(name string) error { certMeta.PrivateKey = siteData.Key // Perform renewal and retry if necessary, but not too many times. - var newCertMeta acme.CertificateResource + var newCertMeta *acme.CertificateResource var success bool for attempts := 0; attempts < 2; attempts++ { namesObtaining.Add([]string{name}) @@ -321,10 +320,8 @@ func (c *ACMEClient) Renew(name string) error { acmeMu.Unlock() namesObtaining.Remove([]string{name}) if err == nil { - // double-check that we actually got a certificate; check a couple fields - // TODO: This is a temporary workaround for what I think is a bug in the acmev2 package (March 2018) - // but it might not hurt to keep this extra check in place - if newCertMeta.Domain == "" || newCertMeta.Certificate == nil { + // double-check that we actually got a certificate; check a couple fields, just in case + if newCertMeta == nil || newCertMeta.Domain == "" || newCertMeta.Certificate == nil { err = errors.New("returned certificate was empty; probably an unchecked error renewing it") } else { success = true diff --git a/caddytls/config.go b/caddytls/config.go index bc1060f6e..e093d3f0e 100644 --- a/caddytls/config.go +++ b/caddytls/config.go @@ -26,7 +26,7 @@ import ( "github.com/klauspost/cpuid" "github.com/mholt/caddy" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // Config describes how TLS should be configured and used. @@ -102,10 +102,10 @@ type Config struct { AltHTTPPort string // The alternate port (ONLY port, not host) - // to use for the ACME TLS-SNI challenge. - // The system must forward TLSSNIChallengePort + // to use for the ACME TLS-ALPN challenge; + // the system must forward TLSALPNChallengePort // to this port for challenge to succeed - AltTLSSNIPort string + AltTLSALPNPort string // The string identifier of the DNS provider // to use when solving the ACME DNS challenge @@ -343,6 +343,18 @@ func (c *Config) buildStandardTLSConfig() error { } } + // ensure ALPN includes the ACME TLS-ALPN protocol + var alpnFound bool + for _, a := range c.ALPN { + if a == acme.ACMETLS1Protocol { + alpnFound = true + break + } + } + if !alpnFound { + c.ALPN = append(c.ALPN, acme.ACMETLS1Protocol) + } + config.MinVersion = c.ProtocolMinVersion config.MaxVersion = c.ProtocolMaxVersion config.ClientAuth = c.ClientAuth @@ -695,13 +707,13 @@ var defaultCurves = []tls.CurveID{ } const ( - // HTTPChallengePort is the officially designated port for + // HTTPChallengePort is the officially-designated port for // the HTTP challenge according to the ACME spec. HTTPChallengePort = "80" - // TLSSNIChallengePort is the officially designated port for - // the TLS-SNI challenge according to the ACME spec. - TLSSNIChallengePort = "443" + // TLSALPNChallengePort is the officially-designated port for + // the TLS-ALPN challenge according to the ACME spec. + TLSALPNChallengePort = "443" // DefaultHTTPAlternatePort is the port on which the ACME // client will open a listener and solve the HTTP challenge. diff --git a/caddytls/crypto.go b/caddytls/crypto.go index c6d7719f6..3f24b78a3 100644 --- a/caddytls/crypto.go +++ b/caddytls/crypto.go @@ -42,7 +42,7 @@ import ( "golang.org/x/crypto/ocsp" "github.com/mholt/caddy" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // loadPrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes. diff --git a/caddytls/filestorage.go b/caddytls/filestorage.go index a43c62b89..4d5aebdde 100644 --- a/caddytls/filestorage.go +++ b/caddytls/filestorage.go @@ -281,11 +281,13 @@ func (s *FileStorage) MostRecentUserEmail() string { func fileSafe(str string) string { str = strings.ToLower(str) str = strings.TrimSpace(str) - repl := strings.NewReplacer("..", "", + repl := strings.NewReplacer( + "..", "", "/", "", "\\", "", // TODO: Consider also replacing "@" with "_at_" (but migrate existing accounts...) "+", "_plus_", + "*", "wildcard_", "%", "", "$", "", "`", "", @@ -297,8 +299,7 @@ func fileSafe(str string) string { "#", "", "&", "", "|", "", - "\"", "", - "'", "", - "*", "wildcard_") + `"`, "", + "'", "") return repl.Replace(str) } diff --git a/caddytls/handshake.go b/caddytls/handshake.go index a7f61a096..86bf656df 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -16,17 +16,20 @@ package caddytls import ( "crypto/tls" + "encoding/json" "errors" "fmt" "log" "net/http" "net/url" + "os" "strings" "sync" "sync/atomic" "time" "github.com/mholt/caddy/telemetry" + "github.com/xenolf/lego/acme" ) // configGroup is a type that keys configs by their hostname @@ -111,6 +114,32 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif go telemetry.SetNested("tls_client_hello", info.Key(), info) } + // special case: serve up the certificate for a TLS-ALPN ACME challenge + // (https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05) + for _, proto := range clientHello.SupportedProtos { + if proto == acme.ACMETLS1Protocol { + cfg.certCache.RLock() + challengeCert, ok := cfg.certCache.cache[tlsALPNCertKeyName(clientHello.ServerName)] + cfg.certCache.RUnlock() + if !ok { + // see if this challenge was started in a cluster; try distributed challenge solver + // (note that the tls.Config's ALPN settings must include the ACME TLS-ALPN challenge + // protocol string, otherwise a valid certificate will not solve the challenge; we + // should already have taken care of that when we made the tls.Config) + challengeCert, ok, err := cfg.tryDistributedChallengeSolver(clientHello) + if err != nil { + log.Printf("[ERROR][%s] TLS-ALPN: %v", clientHello.ServerName, err) + } + if ok { + return &challengeCert.Certificate, nil + } + + return nil, fmt.Errorf("no certificate to complete TLS-ALPN challenge for SNI name: %s", clientHello.ServerName) + } + return &challengeCert.Certificate, nil + } + } + // get the certificate and serve it up cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) if err == nil { @@ -166,17 +195,12 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau } // check the certCache directly to see if the SNI name is - // already the key of the certificate it wants! this is vital - // for supporting the TLS-SNI challenge, since the tlsSNISolver - // just puts the temporary certificate in the instance cache, - // with no regard for configs; this also means that the SNI - // can contain the hash of a specific cert (chain) it wants - // and we will still be able to serve it up + // already the key of the certificate it wants; this implies + // that the SNI can contain the hash of a specific cert + // (chain) it wants and we will still be able to serveit up // (this behavior, by the way, could be controversial as to // whether it complies with RFC 6066 about SNI, but I think - // it does soooo...) - // NOTE/TODO: TLS-SNI challenge is changing, as of Jan. 2018 - // but what will be different, if it ever returns, is unclear + // it does, soooo...) if directCert, ok := cfg.certCache.cache[name]; ok { cert = directCert matched = true @@ -477,6 +501,39 @@ func (cfg *Config) renewDynamicCertificate(name string, currentCert Certificate) return cfg.getCertDuringHandshake(name, true, false) } +// tryDistributedChallengeSolver is to be called when the clientHello pertains to +// a TLS-ALPN challenge and a certificate is required to solve it. This method +// checks the distributed store of challenge info files and, if a matching ServerName +// is present, it makes a certificate to solve this challenge and returns it. +// A boolean true is returned if a valid certificate is returned. +func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInfo) (Certificate, bool, error) { + filePath := distributedSolver{}.challengeTokensPath(clientHello.ServerName) + f, err := os.Open(filePath) + if err != nil { + if os.IsNotExist(err) { + return Certificate{}, false, nil + } + return Certificate{}, false, fmt.Errorf("opening distributed challenge token file %s: %v", filePath, err) + } + defer f.Close() + + var chalInfo challengeInfo + err = json.NewDecoder(f).Decode(&chalInfo) + if err != nil { + return Certificate{}, false, fmt.Errorf("decoding challenge token file %s (corrupted?): %v", filePath, err) + } + + cert, err := acme.TLSALPNChallengeCert(chalInfo.Domain, chalInfo.KeyAuth) + if err != nil { + return Certificate{}, false, fmt.Errorf("making TLS-ALPN challenge certificate: %v", err) + } + if cert == nil { + return Certificate{}, false, fmt.Errorf("got nil TLS-ALPN challenge certificate but no error") + } + + return Certificate{Certificate: *cert}, true, nil +} + // ClientHelloInfo is our own version of the standard lib's // tls.ClientHelloInfo. As of May 2018, any fields populated // by the Go standard library are not guaranteed to have their diff --git a/caddytls/httphandler.go b/caddytls/httphandler.go index 75ec2cc3c..fc63f3f1d 100644 --- a/caddytls/httphandler.go +++ b/caddytls/httphandler.go @@ -25,7 +25,7 @@ import ( "os" "strings" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) const challengeBasePath = "/.well-known/acme-challenge" @@ -87,7 +87,7 @@ func HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, listenHost str // storage, and attempts to complete the challenge for it. It // returns true if the challenge was handled; false otherwise. func tryDistributedChallengeSolver(w http.ResponseWriter, r *http.Request) bool { - filePath := distributedHTTPSolver{}.challengeTokensPath(r.Host) + filePath := distributedSolver{}.challengeTokensPath(r.Host) f, err := os.Open(filePath) if err != nil { if !os.IsNotExist(err) { @@ -112,7 +112,7 @@ func tryDistributedChallengeSolver(w http.ResponseWriter, r *http.Request) bool w.Header().Add("Content-Type", "text/plain") w.Write([]byte(chalInfo.KeyAuth)) r.Close = true - log.Printf("[INFO][%s] Served key authentication", chalInfo.Domain) + log.Printf("[INFO][%s] Served key authentication (distributed)", chalInfo.Domain) return true } diff --git a/caddytls/maintain.go b/caddytls/maintain.go index b24b62125..c3f21f921 100644 --- a/caddytls/maintain.go +++ b/caddytls/maintain.go @@ -146,7 +146,7 @@ func RenewManagedCertificates(allowPrompts bool) (err error) { // happen to run their maintenance checks at approximately the same times; // both might start renewal at about the same time and do two renewals and one // will overwrite the other. Hence TLS storage plugins. This is sort of a TODO. - // NOTE 2: It is super-important to note that the TLS-SNI challenge requires + // NOTE 2: It is super-important to note that the TLS-ALPN challenge requires // a write lock on the cache in order to complete its challenge, so it is extra // vital that this renew operation does not happen inside our read lock! renewQueue = append(renewQueue, cert) diff --git a/caddytls/setup_test.go b/caddytls/setup_test.go index c961939e0..a47d71720 100644 --- a/caddytls/setup_test.go +++ b/caddytls/setup_test.go @@ -22,7 +22,7 @@ import ( "testing" "github.com/mholt/caddy" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) func TestMain(m *testing.M) { diff --git a/caddytls/tls.go b/caddytls/tls.go index 206908892..36554c401 100644 --- a/caddytls/tls.go +++ b/caddytls/tls.go @@ -39,7 +39,7 @@ import ( "strings" "github.com/mholt/caddy" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // HostQualifies returns true if the hostname alone @@ -72,7 +72,7 @@ func HostQualifies(hostname string) bool { // saveCertResource saves the certificate resource to disk. This // includes the certificate file itself, the private key, and the // metadata file. -func saveCertResource(storage Storage, cert acme.CertificateResource) error { +func saveCertResource(storage Storage, cert *acme.CertificateResource) error { // Save cert, private key, and metadata siteData := &SiteData{ Cert: cert.Certificate, @@ -97,55 +97,63 @@ func Revoke(host string) error { return client.Revoke(host) } -// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return -// // tlsSNISolver is a type that can solve TLS-SNI challenges using -// // an existing listener and our custom, in-memory certificate cache. -// type tlsSNISolver struct { -// certCache *certificateCache -// } +// tlsALPNSolver is a type that can solve TLS-ALPN challenges using +// an existing listener and our custom, in-memory certificate cache. +type tlsALPNSolver struct { + certCache *certificateCache +} -// // Present adds the challenge certificate to the cache. -// func (s tlsSNISolver) Present(domain, token, keyAuth string) error { -// cert, acmeDomain, err := acme.TLSSNI01ChallengeCert(keyAuth) -// if err != nil { -// return err -// } -// certHash := hashCertificateChain(cert.Certificate) -// s.certCache.Lock() -// s.certCache.cache[acmeDomain] = Certificate{ -// Certificate: cert, -// Names: []string{acmeDomain}, -// Hash: certHash, // perhaps not necesssary -// } -// s.certCache.Unlock() -// return nil -// } +// Present adds the challenge certificate to the cache. +func (s tlsALPNSolver) Present(domain, token, keyAuth string) error { + cert, err := acme.TLSALPNChallengeCert(domain, keyAuth) + if err != nil { + return err + } + certHash := hashCertificateChain(cert.Certificate) + s.certCache.Lock() + s.certCache.cache[tlsALPNCertKeyName(domain)] = Certificate{ + Certificate: *cert, + Names: []string{domain}, + Hash: certHash, // perhaps not necesssary + } + s.certCache.Unlock() + return nil +} -// // CleanUp removes the challenge certificate from the cache. -// func (s tlsSNISolver) CleanUp(domain, token, keyAuth string) error { -// _, acmeDomain, err := acme.TLSSNI01ChallengeCert(keyAuth) -// if err != nil { -// return err -// } -// s.certCache.Lock() -// delete(s.certCache.cache, acmeDomain) -// s.certCache.Unlock() -// return nil -// } +// CleanUp removes the challenge certificate from the cache. +func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error { + s.certCache.Lock() + delete(s.certCache.cache, domain) + s.certCache.Unlock() + return nil +} -// distributedHTTPSolver allows the HTTP-01 challenge to be solved by -// an instance other than the one which initiated it. This is useful -// behind load balancers or in other cluster/fleet configurations. -// The only requirement is that this (the initiating) instance share -// the $CADDYPATH/acme folder with the instance that will complete -// the challenge. Mounting the folder locally should be sufficient. +// tlsALPNCertKeyName returns the key to use when caching a cert +// for use with the TLS-ALPN ACME challenge. It is simply to help +// avoid conflicts (although at time of writing, there shouldn't +// be, since the cert cache is keyed by hash of certificate chain). +func tlsALPNCertKeyName(sniName string) string { + return sniName + ":acme-tls-alpn" +} + +// distributedSolver allows the ACME HTTP-01 and TLS-ALPN challenges +// to be solved by an instance other than the one which initiated it. +// This is useful behind load balancers or in other cluster/fleet +// configurations. The only requirement is that this (the initiating) +// instance share the $CADDYPATH/acme folder with the instance that +// will complete the challenge. Mounting the folder locally should be +// sufficient. // // Obviously, the instance which completes the challenge must be -// serving on the HTTPChallengePort to receive and handle the request. -// The HTTP server which receives it must check if a file exists, e.g.: -// $CADDYPATH/acme/challenge_tokens/example.com.json, and if so, -// decode it and use it to serve up the correct response. Caddy's HTTP -// server does this by default. +// serving on the HTTPChallengePort for the HTTP-01 challenge or the +// TLSALPNChallengePort for the TLS-ALPN-01 challenge (or have all +// the packets port-forwarded) to receive and handle the request. The +// server which receives the challenge must handle it by checking to +// see if a file exists, e.g.: +// $CADDYPATH/acme/challenge_tokens/example.com.json +// and if so, decode it and use it to serve up the correct response. +// Caddy's HTTP server does this by default (for HTTP-01) and so does +// its TLS package (for TLS-ALPN-01). // // So as long as the folder is shared, this will just work. There are // no other requirements. The instances may be on other machines or @@ -155,29 +163,18 @@ func Revoke(host string) error { // This solver works by persisting the token and keyauth information // to disk in the shared folder when the authorization is presented, // and then deletes it when it is cleaned up. -type distributedHTTPSolver struct { - // The distributed HTTPS solver only works if an instance (either - // this one or another one) is already listening and serving on the - // HTTPChallengePort. If not -- for example: if this is the only - // instance, and it is just starting up and hasn't started serving - // yet -- then we still need a listener open with an HTTP server - // to handle the challenge request. Set this field to have the - // standard HTTPProviderServer open its listener for the duration - // of the challenge. Make sure to configure its listen address - // correctly. - httpProviderServer *acme.HTTPProviderServer -} - -type challengeInfo struct { - Domain, Token, KeyAuth string +type distributedSolver struct { + // As the distributedSolver is only a wrapper over the actual + // solver, place the actual solver here + providerServer ChallengeProvider } // Present adds the challenge certificate to the cache. -func (dhs distributedHTTPSolver) Present(domain, token, keyAuth string) error { - if dhs.httpProviderServer != nil { - err := dhs.httpProviderServer.Present(domain, token, keyAuth) +func (dhs distributedSolver) Present(domain, token, keyAuth string) error { + if dhs.providerServer != nil { + err := dhs.providerServer.Present(domain, token, keyAuth) if err != nil { - return fmt.Errorf("presenting with standard HTTP provider server: %v", err) + return fmt.Errorf("presenting with standard provider server: %v", err) } } @@ -199,25 +196,29 @@ func (dhs distributedHTTPSolver) Present(domain, token, keyAuth string) error { } // CleanUp removes the challenge certificate from the cache. -func (dhs distributedHTTPSolver) CleanUp(domain, token, keyAuth string) error { - if dhs.httpProviderServer != nil { - err := dhs.httpProviderServer.CleanUp(domain, token, keyAuth) +func (dhs distributedSolver) CleanUp(domain, token, keyAuth string) error { + if dhs.providerServer != nil { + err := dhs.providerServer.CleanUp(domain, token, keyAuth) if err != nil { - log.Printf("[ERROR] Cleaning up standard HTTP provider server: %v", err) + log.Printf("[ERROR] Cleaning up standard provider server: %v", err) } } return os.Remove(dhs.challengeTokensPath(domain)) } -func (dhs distributedHTTPSolver) challengeTokensPath(domain string) string { - domainFile := strings.Replace(strings.ToLower(domain), "*", "wildcard_", -1) +func (dhs distributedSolver) challengeTokensPath(domain string) string { + domainFile := fileSafe(domain) return filepath.Join(dhs.challengeTokensBasePath(), domainFile+".json") } -func (dhs distributedHTTPSolver) challengeTokensBasePath() string { +func (dhs distributedSolver) challengeTokensBasePath() string { return filepath.Join(caddy.AssetsPath(), "acme", "challenge_tokens") } +type challengeInfo struct { + Domain, Token, KeyAuth string +} + // ConfigHolder is any type that has a Config; it presumably is // connected to a hostname and port on which it is serving. type ConfigHolder interface { @@ -297,8 +298,8 @@ var ( // DisableHTTPChallenge will disable all HTTP challenges. DisableHTTPChallenge bool - // DisableTLSSNIChallenge will disable all TLS-SNI challenges. - DisableTLSSNIChallenge bool + // DisableTLSALPNChallenge will disable all TLS-ALPN challenges. + DisableTLSALPNChallenge bool ) var storageProviders = make(map[string]StorageConstructor) diff --git a/caddytls/tls_test.go b/caddytls/tls_test.go index 0d06f1adb..89f0f5e54 100644 --- a/caddytls/tls_test.go +++ b/caddytls/tls_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) func TestHostQualifies(t *testing.T) { @@ -116,7 +116,7 @@ func TestSaveCertResource(t *testing.T) { "certStableUrl": "https://example.com/cert/stable" }` - cert := acme.CertificateResource{ + cert := &acme.CertificateResource{ Domain: domain, CertURL: "https://example.com/cert", CertStableURL: "https://example.com/cert/stable", @@ -164,7 +164,7 @@ func TestExistingCertAndKey(t *testing.T) { t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain) } - err = saveCertResource(storage, acme.CertificateResource{ + err = saveCertResource(storage, &acme.CertificateResource{ Domain: domain, PrivateKey: []byte("key"), Certificate: []byte("cert"), diff --git a/caddytls/user.go b/caddytls/user.go index 35f00f0ab..e7a9646d3 100644 --- a/caddytls/user.go +++ b/caddytls/user.go @@ -27,7 +27,7 @@ import ( "os" "strings" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) // User represents a Let's Encrypt user account. diff --git a/caddytls/user_test.go b/caddytls/user_test.go index 8cd9a84dd..1fb1632df 100644 --- a/caddytls/user_test.go +++ b/caddytls/user_test.go @@ -27,7 +27,7 @@ import ( "os" - "github.com/xenolf/lego/acmev2" + "github.com/xenolf/lego/acme" ) func TestUser(t *testing.T) { diff --git a/vendor/github.com/xenolf/lego/acmev2/LICENSE b/vendor/github.com/xenolf/lego/acme/LICENSE similarity index 100% rename from vendor/github.com/xenolf/lego/acmev2/LICENSE rename to vendor/github.com/xenolf/lego/acme/LICENSE diff --git a/vendor/github.com/xenolf/lego/acmev2/challenges.go b/vendor/github.com/xenolf/lego/acme/challenges.go similarity index 80% rename from vendor/github.com/xenolf/lego/acmev2/challenges.go rename to vendor/github.com/xenolf/lego/acme/challenges.go index cf7bd7f75..1140b1073 100644 --- a/vendor/github.com/xenolf/lego/acmev2/challenges.go +++ b/vendor/github.com/xenolf/lego/acme/challenges.go @@ -10,4 +10,6 @@ const ( // DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns // Note: DNS01Record returns a DNS record which will fulfill this challenge DNS01 = Challenge("dns-01") + // TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01 + TLSALPN01 = Challenge("tls-alpn-01") ) diff --git a/vendor/github.com/xenolf/lego/acmev2/client.go b/vendor/github.com/xenolf/lego/acme/client.go similarity index 75% rename from vendor/github.com/xenolf/lego/acmev2/client.go rename to vendor/github.com/xenolf/lego/acme/client.go index 904f07fbc..b8daa7512 100644 --- a/vendor/github.com/xenolf/lego/acmev2/client.go +++ b/vendor/github.com/xenolf/lego/acme/client.go @@ -5,20 +5,17 @@ import ( "crypto" "crypto/x509" "encoding/base64" + "encoding/pem" "errors" "fmt" "io/ioutil" - "log" "net" "regexp" "strconv" "strings" "time" -) -var ( - // Logger is an optional custom logger. - Logger *log.Logger + "github.com/xenolf/lego/log" ) const ( @@ -31,16 +28,6 @@ const ( overallRequestLimit = 18 ) -// logf writes a log entry. It uses Logger if not -// nil, otherwise it uses the default log.Logger. -func logf(format string, args ...interface{}) { - if Logger != nil { - Logger.Printf(format, args...) - } else { - log.Printf(format, args...) - } -} - // User interface is to be implemented by users of this library. // It is used by the client type to get user specific information. type User interface { @@ -86,9 +73,6 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { if dir.NewOrderURL == "" { return nil, errors.New("directory missing new order URL") } - /*if dir.RevokeCertURL == "" { - return nil, errors.New("directory missing revoke certificate URL") - }*/ jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL} if reg := user.GetRegistration(); reg != nil { @@ -98,8 +82,10 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { // REVIEW: best possibility? // Add all available solvers with the right index as per ACME // spec to this map. Otherwise they won`t be found. - solvers := make(map[Challenge]solver) - solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} + solvers := map[Challenge]solver{ + HTTP01: &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}, + TLSALPN01: &tlsALPNChallenge{jws: jws, validate: validate, provider: &TLSALPNProviderServer{}}, + } return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil } @@ -111,8 +97,10 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p} case DNS01: c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p} + case TLSALPN01: + c.solvers[challenge] = &tlsALPNChallenge{jws: c.jws, validate: validate, provider: p} default: - return fmt.Errorf("Unknown challenge %v", challenge) + return fmt.Errorf("unknown challenge %v", challenge) } return nil } @@ -136,6 +124,24 @@ func (c *Client) SetHTTPAddress(iface string) error { return nil } +// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges. +// If this option is not used, the default port 443 and all interfaces will be used. +// To only specify a port and no interface use the ":port" notation. +// +// NOTE: This REPLACES any custom TLS-ALPN provider previously set by calling +// c.SetChallengeProvider with the default TLS-ALPN challenge provider. +func (c *Client) SetTLSAddress(iface string) error { + host, port, err := net.SplitHostPort(iface) + if err != nil { + return err + } + + if chlng, ok := c.solvers[TLSALPN01]; ok { + chlng.(*tlsALPNChallenge).provider = NewTLSALPNProviderServer(host, port) + } + return nil +} + // ExcludeChallenges explicitly removes challenges from the pool for solving. func (c *Client) ExcludeChallenges(challenges []Challenge) { // Loop through all challenges and delete the requested one if found. @@ -149,12 +155,17 @@ func (c *Client) GetToSURL() string { return c.directory.Meta.TermsOfService } +// GetExternalAccountRequired returns the External Account Binding requirement of the Directory +func (c *Client) GetExternalAccountRequired() bool { + return c.directory.Meta.ExternalAccountRequired +} + // Register the current account to the ACME server. func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { if c == nil || c.user == nil { return nil, errors.New("acme: cannot register a nil client or user") } - logf("[INFO] acme: Registering account for %s", c.user.GetEmail()) + log.Infof("acme: Registering account for %s", c.user.GetEmail()) accMsg := accountMessage{} if c.user.GetEmail() != "" { @@ -183,10 +194,58 @@ func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { return reg, nil } +// RegisterWithExternalAccountBinding Register the current account to the ACME server. +func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) { + if c == nil || c.user == nil { + return nil, errors.New("acme: cannot register a nil client or user") + } + log.Infof("acme: Registering account (EAB) for %s", c.user.GetEmail()) + + accMsg := accountMessage{} + if c.user.GetEmail() != "" { + accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} + } else { + accMsg.Contact = []string{} + } + accMsg.TermsOfServiceAgreed = tosAgreed + + hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) + if err != nil { + return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error()) + } + + eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac) + if err != nil { + return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error()) + } + + eabPayload := eabJWS.FullSerialize() + + accMsg.ExternalAccountBinding = []byte(eabPayload) + + var serverReg accountMessage + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) + if err != nil { + remoteErr, ok := err.(RemoteError) + if ok && remoteErr.StatusCode == 409 { + } else { + return nil, err + } + } + + reg := &RegistrationResource{ + URI: hdr.Get("Location"), + Body: serverReg, + } + c.jws.kid = reg.URI + + return reg, nil +} + // ResolveAccountByKey will attempt to look up an account using the given account key // and return its registration resource. func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { - logf("[INFO] acme: Trying to resolve account by key") + log.Infof("acme: Trying to resolve account by key") acc := accountMessage{OnlyReturnExisting: true} hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil) @@ -201,7 +260,7 @@ func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { var retAccount accountMessage c.jws.kid = accountLink - hdr, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount) + _, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount) if err != nil { return nil, err } @@ -215,18 +274,14 @@ func (c *Client) DeleteRegistration() error { if c == nil || c.user == nil { return errors.New("acme: cannot unregister a nil client or user") } - logf("[INFO] acme: Deleting account for %s", c.user.GetEmail()) + log.Infof("acme: Deleting account for %s", c.user.GetEmail()) accMsg := accountMessage{ Status: "deactivated", } _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil) - if err != nil { - return err - } - - return nil + return err } // QueryRegistration runs a POST request on the client's registration and @@ -239,7 +294,7 @@ func (c *Client) QueryRegistration() (*RegistrationResource, error) { return nil, errors.New("acme: cannot query the registration of a nil client or user") } // Log the URL here instead of the email as the email may not be set - logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI) + log.Infof("acme: Querying account for %s", c.user.GetRegistration().URI) accMsg := accountMessage{} @@ -265,7 +320,7 @@ func (c *Client) QueryRegistration() (*RegistrationResource, error) { // your issued certificate as a bundle. // This function will never return a partial certificate. If one domain in the list fails, // the whole certificate will fail. -func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, error) { +func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (*CertificateResource, error) { // figure out what domains it concerns // start with the common name domains := []string{csr.Subject.CommonName} @@ -285,14 +340,14 @@ DNSNames: } if bundle { - logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) + log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) } else { - logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) + log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) } order, err := c.createOrderForIdentifiers(domains) if err != nil { - return CertificateResource{}, err + return nil, err } authz, err := c.getAuthzForOrder(order) if err != nil { @@ -300,16 +355,16 @@ DNSNames: /*for _, auth := range authz { c.disableAuthz(auth) }*/ - return CertificateResource{}, err + return nil, err } err = c.solveChallengeForAuthz(authz) if err != nil { // If any challenge fails, return. Do not generate partial SAN certificates. - return CertificateResource{}, err + return nil, err } - logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) failures := make(ObtainError) cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil) @@ -339,20 +394,20 @@ DNSNames: // your issued certificate as a bundle. // This function will never return a partial certificate. If one domain in the list fails, // the whole certificate will fail. -func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) { +func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { if len(domains) == 0 { - return CertificateResource{}, errors.New("No domains to obtain a certificate for") + return nil, errors.New("No domains to obtain a certificate for") } if bundle { - logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) + log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { - logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) + log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) } order, err := c.createOrderForIdentifiers(domains) if err != nil { - return CertificateResource{}, err + return nil, err } authz, err := c.getAuthzForOrder(order) if err != nil { @@ -360,16 +415,16 @@ func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto /*for _, auth := range authz { c.disableAuthz(auth) }*/ - return CertificateResource{}, err + return nil, err } err = c.solveChallengeForAuthz(authz) if err != nil { // If any challenge fails, return. Do not generate partial SAN certificates. - return CertificateResource{}, err + return nil, err } - logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) failures := make(ObtainError) cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple) @@ -413,22 +468,22 @@ func (c *Client) RevokeCertificate(certificate []byte) error { // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. // For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. -func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (CertificateResource, error) { +func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (*CertificateResource, error) { // Input certificate is PEM encoded. Decode it here as we may need the decoded // cert later on in the renewal process. The input may be a bundle or a single certificate. certificates, err := parsePEMBundle(cert.Certificate) if err != nil { - return CertificateResource{}, err + return nil, err } x509Cert := certificates[0] if x509Cert.IsCA { - return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) + return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) } // This is just meant to be informal for the user. timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) - logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) + log.Infof("[%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) // We always need to request a new certificate to renew. // Start by checking to see if the certificate was based off a CSR, and @@ -436,7 +491,7 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b if len(cert.CSR) > 0 { csr, err := pemDecodeTox509CSR(cert.CSR) if err != nil { - return CertificateResource{}, err + return nil, err } newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) return newCert, failures @@ -446,7 +501,7 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b if cert.PrivateKey != nil { privKey, err = parsePEMPrivateKey(cert.PrivateKey) if err != nil { - return CertificateResource{}, err + return nil, err } } @@ -502,7 +557,7 @@ func (c *Client) solveChallengeForAuthz(authorizations []authorization) error { for _, authz := range authorizations { if authz.Status == "valid" { // Boulder might recycle recent validated authz (see issue #267) - logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) + log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) continue } @@ -533,7 +588,7 @@ func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) { if solver, ok := c.solvers[Challenge(challenge.Type)]; ok { return i, solver } - logf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type) + log.Infof("[%s] acme: Could not find solver for: %s", domain, challenge.Type) } return 0, nil } @@ -585,7 +640,7 @@ func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) func logAuthz(order orderResource) { for i, auth := range order.Authorizations { - logf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth) + log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth) } } @@ -596,44 +651,53 @@ func (c *Client) disableAuthz(authURL string) error { return err } -func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) { +func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { var err error if privKey == nil { privKey, err = generatePrivateKey(c.keyType) if err != nil { - return CertificateResource{}, err + return nil, err } } // determine certificate name(s) based on the authorization resources commonName := order.Domains[0] - var san []string + + // ACME draft Section 7.4 "Applying for Certificate Issuance" + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4 + // says: + // Clients SHOULD NOT make any assumptions about the sort order of + // "identifiers" or "authorizations" elements in the returned order + // object. + san := []string{commonName} for _, auth := range order.Identifiers { - san = append(san, auth.Value) + if auth.Value != commonName { + san = append(san, auth.Value) + } } // TODO: should the CSR be customizable? csr, err := generateCsr(privKey, commonName, san, mustStaple) if err != nil { - return CertificateResource{}, err + return nil, err } return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey)) } -func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) { +func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) { commonName := order.Domains[0] csrString := base64.RawURLEncoding.EncodeToString(csr) var retOrder orderMessage - _, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder) - if error != nil { - return CertificateResource{}, error + _, err := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder) + if err != nil { + return nil, err } if retOrder.Status == "invalid" { - return CertificateResource{}, error + return nil, err } certRes := CertificateResource{ @@ -646,33 +710,38 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr // if the certificate is available right away, short cut! ok, err := c.checkCertResponse(retOrder, &certRes, bundle) if err != nil { - return CertificateResource{}, err + return nil, err } if ok { - return certRes, nil + return &certRes, nil } } - maxChecks := 1000 - for i := 0; i < maxChecks; i++ { - _, err := getJSON(order.URL, &retOrder) - if err != nil { - return CertificateResource{}, err - } - done, err := c.checkCertResponse(retOrder, &certRes, bundle) - if err != nil { - return CertificateResource{}, err - } - if done { - break - } - if i == maxChecks-1 { - return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i) + stopTimer := time.NewTimer(30 * time.Second) + defer stopTimer.Stop() + retryTick := time.NewTicker(500 * time.Millisecond) + defer retryTick.Stop() + + for { + select { + case <-stopTimer.C: + return nil, errors.New("certificate polling timed out") + case <-retryTick.C: + _, err := getJSON(order.URL, &retOrder) + if err != nil { + return nil, err + } + + done, err := c.checkCertResponse(retOrder, &certRes, bundle) + if err != nil { + return nil, err + } + if done { + return &certRes, nil + } } } - - return certRes, nil } // checkCertResponse checks to see if the certificate is ready and a link is contained in the @@ -694,15 +763,16 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou return false, err } - // The issuer certificate link is always supplied via an "up" link - // in the response headers of a new certificate. + // The issuer certificate link may be supplied via an "up" link + // in the response headers of a new certificate. See + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2 links := parseLinks(resp.Header["Link"]) if link, ok := links["up"]; ok { issuerCert, err := c.getIssuerCertificate(link) if err != nil { // If we fail to acquire the issuer cert, return the issued certificate - do not fail. - logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err) + log.Warnf("[%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err) } else { issuerCert = pemEncode(derCertificateBytes(issuerCert)) @@ -714,26 +784,33 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou certRes.IssuerCertificate = issuerCert } + } else { + // Get issuerCert from bundled response from Let's Encrypt + // See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962 + _, rest := pem.Decode(cert) + if rest != nil { + certRes.IssuerCertificate = rest + } } certRes.Certificate = cert certRes.CertURL = order.Certificate certRes.CertStableURL = order.Certificate - logf("[INFO][%s] Server responded with a certificate.", certRes.Domain) + log.Infof("[%s] Server responded with a certificate.", certRes.Domain) return true, nil case "processing": return false, nil case "invalid": - return false, errors.New("Order has invalid state: invalid") + return false, errors.New("order has invalid state: invalid") + default: + return false, nil } - - return false, nil } // getIssuerCertificate requests the issuer certificate func (c *Client) getIssuerCertificate(url string) ([]byte, error) { - logf("[INFO] acme: Requesting issuer cert from %s", url) + log.Infof("acme: Requesting issuer cert from %s", url) resp, err := httpGet(url) if err != nil { return nil, err @@ -787,14 +864,14 @@ func validate(j *jws, domain, uri string, c challenge) error { for { switch chlng.Status { case "valid": - logf("[INFO][%s] The server validated our request", domain) + log.Infof("[%s] The server validated our request", domain) return nil case "pending": - break + case "processing": case "invalid": return handleChallengeError(chlng) default: - return errors.New("The server returned an unexpected state") + return errors.New("the server returned an unexpected state") } ra, err := strconv.Atoi(hdr.Get("Retry-After")) diff --git a/vendor/github.com/xenolf/lego/acmev2/crypto.go b/vendor/github.com/xenolf/lego/acme/crypto.go similarity index 93% rename from vendor/github.com/xenolf/lego/acmev2/crypto.go rename to vendor/github.com/xenolf/lego/acme/crypto.go index e50ca30d8..f5ebbf08e 100644 --- a/vendor/github.com/xenolf/lego/acmev2/crypto.go +++ b/vendor/github.com/xenolf/lego/acme/crypto.go @@ -9,6 +9,7 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/base64" "encoding/pem" "errors" @@ -19,8 +20,6 @@ import ( "net/http" "time" - "encoding/asn1" - "golang.org/x/crypto/ocsp" jose "gopkg.in/square/go-jose.v2" ) @@ -118,6 +117,10 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { defer req.Body.Close() ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024)) + if err != nil { + return nil, nil, err + } + ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) if err != nil { return nil, nil, err @@ -138,7 +141,7 @@ func getKeyAuthorization(token string, key interface{}) (string, error) { // Generate the Key Authorization for the challenge jwk := &jose.JSONWebKey{Key: publicKey} if jwk == nil { - return "", errors.New("Could not generate JWK from key") + return "", errors.New("could not generate JWK from key") } thumbBytes, err := jwk.Thumbprint(crypto.SHA256) if err != nil { @@ -173,7 +176,7 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { } if len(certificates) == 0 { - return nil, errors.New("No certificates were found while parsing the bundle") + return nil, errors.New("no certificates were found while parsing the bundle") } return certificates, nil @@ -188,7 +191,7 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { case "EC PRIVATE KEY": return x509.ParseECPrivateKey(keyBlock.Bytes) default: - return nil, errors.New("Unknown PEM header value") + return nil, errors.New("unknown PEM header value") } } @@ -207,14 +210,12 @@ func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, 8192) } - return nil, fmt.Errorf("Invalid KeyType: %s", keyType) + return nil, fmt.Errorf("invalid KeyType: %s", keyType) } func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { template := x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: domain, - }, + Subject: pkix.Name{CommonName: domain}, } if len(san) > 0 { @@ -239,10 +240,8 @@ func pemEncode(data interface{}) []byte { pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} case *rsa.PrivateKey: pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} - break case *x509.CertificateRequest: pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} - break case derCertificateBytes: pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} } @@ -302,8 +301,8 @@ func getCertExpiration(cert []byte) (time.Time, error) { return pCert.NotAfter, nil } -func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { - derBytes, err := generateDerCert(privKey, time.Time{}, domain) +func generatePemCert(privKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) { + derBytes, err := generateDerCert(privKey, time.Time{}, domain, extensions) if err != nil { return nil, err } @@ -311,7 +310,7 @@ func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil } -func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { +func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { @@ -333,6 +332,7 @@ func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain strin KeyUsage: x509.KeyUsageKeyEncipherment, BasicConstraintsValid: true, DNSNames: []string{domain}, + ExtraExtensions: extensions, } return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) diff --git a/vendor/github.com/xenolf/lego/acmev2/dns_challenge.go b/vendor/github.com/xenolf/lego/acme/dns_challenge.go similarity index 95% rename from vendor/github.com/xenolf/lego/acmev2/dns_challenge.go rename to vendor/github.com/xenolf/lego/acme/dns_challenge.go index d129dacc6..c8a35eb88 100644 --- a/vendor/github.com/xenolf/lego/acmev2/dns_challenge.go +++ b/vendor/github.com/xenolf/lego/acme/dns_challenge.go @@ -5,12 +5,12 @@ import ( "encoding/base64" "errors" "fmt" - "log" "net" "strings" "time" "github.com/miekg/dns" + "github.com/xenolf/lego/log" ) type preCheckDNSFunc func(fqdn, value string) (bool, error) @@ -72,10 +72,10 @@ type dnsChallenge struct { } func (s *dnsChallenge) Solve(chlng challenge, domain string) error { - logf("[INFO][%s] acme: Trying to solve DNS-01", domain) + log.Infof("[%s] acme: Trying to solve DNS-01", domain) if s.provider == nil { - return errors.New("No DNS Provider configured") + return errors.New("no DNS Provider configured") } // Generate the Key Authorization for the challenge @@ -86,18 +86,18 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { err = s.provider.Present(domain, chlng.Token, keyAuth) if err != nil { - return fmt.Errorf("Error presenting token: %s", err) + return fmt.Errorf("error presenting token: %s", err) } defer func() { err := s.provider.CleanUp(domain, chlng.Token, keyAuth) if err != nil { - log.Printf("Error cleaning up %s: %v ", domain, err) + log.Warnf("Error cleaning up %s: %v ", domain, err) } }() fqdn, value, _ := DNS01Record(domain, keyAuth) - logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) + log.Infof("[%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) var timeout, interval time.Duration switch provider := s.provider.(type) { diff --git a/vendor/github.com/xenolf/lego/acmev2/dns_challenge_manual.go b/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go similarity index 79% rename from vendor/github.com/xenolf/lego/acmev2/dns_challenge_manual.go rename to vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go index 240384e60..ca94fcac7 100644 --- a/vendor/github.com/xenolf/lego/acmev2/dns_challenge_manual.go +++ b/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go @@ -4,6 +4,8 @@ import ( "bufio" "fmt" "os" + + "github.com/xenolf/lego/log" ) const ( @@ -28,9 +30,9 @@ func (*DNSProviderManual) Present(domain, token, keyAuth string) error { return err } - logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone) - logf("[INFO] acme: %s", dnsRecord) - logf("[INFO] acme: Press 'Enter' when you are done") + log.Infof("acme: Please create the following TXT record in your %s zone:", authZone) + log.Infof("acme: %s", dnsRecord) + log.Infof("acme: Press 'Enter' when you are done") reader := bufio.NewReader(os.Stdin) _, _ = reader.ReadString('\n') @@ -47,7 +49,7 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { return err } - logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone) - logf("[INFO] acme: %s", dnsRecord) + log.Infof("acme: You can now remove this TXT record from your %s zone:", authZone) + log.Infof("acme: %s", dnsRecord) return nil } diff --git a/vendor/github.com/xenolf/lego/acmev2/error.go b/vendor/github.com/xenolf/lego/acme/error.go similarity index 100% rename from vendor/github.com/xenolf/lego/acmev2/error.go rename to vendor/github.com/xenolf/lego/acme/error.go diff --git a/vendor/github.com/xenolf/lego/acmev2/http.go b/vendor/github.com/xenolf/lego/acme/http.go similarity index 57% rename from vendor/github.com/xenolf/lego/acmev2/http.go rename to vendor/github.com/xenolf/lego/acme/http.go index b93e53445..8d7ee51ef 100644 --- a/vendor/github.com/xenolf/lego/acmev2/http.go +++ b/vendor/github.com/xenolf/lego/acme/http.go @@ -1,33 +1,45 @@ package acme import ( + "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" "io" + "io/ioutil" "net" "net/http" + "os" "runtime" "strings" "time" ) -// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. -var UserAgent string +var ( + // UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. + UserAgent string -// HTTPClient is an HTTP client with a reasonable timeout value. -var HTTPClient = http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 15 * time.Second, - ResponseHeaderTimeout: 15 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, -} + // HTTPClient is an HTTP client with a reasonable timeout value and + // potentially a custom *x509.CertPool based on the caCertificatesEnvVar + // environment variable (see the `initCertPool` function) + HTTPClient = http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 15 * time.Second, + ResponseHeaderTimeout: 15 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{ + ServerName: os.Getenv(caServerNameEnvVar), + RootCAs: initCertPool(), + }, + }, + } +) const ( // defaultGoUserAgent is the Go HTTP package user agent string. Too @@ -36,12 +48,46 @@ const ( // ourUserAgent is the User-Agent of this underlying library package. ourUserAgent = "xenolf-acme" + + // caCertificatesEnvVar is the environment variable name that can be used to + // specify the path to PEM encoded CA Certificates that can be used to + // authenticate an ACME server with a HTTPS certificate not issued by a CA in + // the system-wide trusted root list. + caCertificatesEnvVar = "LEGO_CA_CERTIFICATES" + + // caServerNameEnvVar is the environment variable name that can be used to + // specify the CA server name that can be used to + // authenticate an ACME server with a HTTPS certificate not issued by a CA in + // the system-wide trusted root list. + caServerNameEnvVar = "LEGO_CA_SERVER_NAME" ) +// initCertPool creates a *x509.CertPool populated with the PEM certificates +// found in the filepath specified in the caCertificatesEnvVar OS environment +// variable. If the caCertificatesEnvVar is not set then initCertPool will +// return nil. If there is an error creating a *x509.CertPool from the provided +// caCertificatesEnvVar value then initCertPool will panic. +func initCertPool() *x509.CertPool { + if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" { + customCAs, err := ioutil.ReadFile(customCACertsPath) + if err != nil { + panic(fmt.Sprintf("error reading %s=%q: %v", + caCertificatesEnvVar, customCACertsPath, err)) + } + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(customCAs); !ok { + panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v", + caCertificatesEnvVar, customCACertsPath, err)) + } + return certPool + } + return nil +} + // httpHead performs a HEAD request with a proper User-Agent string. // The response body (resp.Body) is already closed when this function returns. func httpHead(url string) (resp *http.Response, err error) { - req, err := http.NewRequest("HEAD", url, nil) + req, err := http.NewRequest(http.MethodHead, url, nil) if err != nil { return nil, fmt.Errorf("failed to head %q: %v", url, err) } @@ -59,7 +105,7 @@ func httpHead(url string) (resp *http.Response, err error) { // httpPost performs a POST request with a proper User-Agent string. // Callers should close resp.Body when done reading from it. func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { - req, err := http.NewRequest("POST", url, body) + req, err := http.NewRequest(http.MethodPost, url, body) if err != nil { return nil, fmt.Errorf("failed to post %q: %v", url, err) } @@ -72,7 +118,7 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, // httpGet performs a GET request with a proper User-Agent string. // Callers should close resp.Body when done reading from it. func httpGet(url string) (resp *http.Response, err error) { - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to get %q: %v", url, err) } @@ -155,6 +201,6 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e // userAgent builds and returns the User-Agent string to use in requests. func userAgent() string { - ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent) + ua := fmt.Sprintf("%s %s (%s; %s) %s", UserAgent, ourUserAgent, runtime.GOOS, runtime.GOARCH, defaultGoUserAgent) return strings.TrimSpace(ua) } diff --git a/vendor/github.com/xenolf/lego/acmev2/http_challenge.go b/vendor/github.com/xenolf/lego/acme/http_challenge.go similarity index 86% rename from vendor/github.com/xenolf/lego/acmev2/http_challenge.go rename to vendor/github.com/xenolf/lego/acme/http_challenge.go index b6c969fe2..77a8edd49 100644 --- a/vendor/github.com/xenolf/lego/acmev2/http_challenge.go +++ b/vendor/github.com/xenolf/lego/acme/http_challenge.go @@ -2,7 +2,8 @@ package acme import ( "fmt" - "log" + + "github.com/xenolf/lego/log" ) type httpChallenge struct { @@ -18,7 +19,7 @@ func HTTP01ChallengePath(token string) string { func (s *httpChallenge) Solve(chlng challenge, domain string) error { - logf("[INFO][%s] acme: Trying to solve HTTP-01", domain) + log.Infof("[%s] acme: Trying to solve HTTP-01", domain) // Generate the Key Authorization for the challenge keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) @@ -33,7 +34,7 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error { defer func() { err := s.provider.CleanUp(domain, chlng.Token, keyAuth) if err != nil { - log.Printf("[%s] error cleaning up: %v", domain, err) + log.Warnf("[%s] error cleaning up: %v", domain, err) } }() diff --git a/vendor/github.com/xenolf/lego/acmev2/http_challenge_server.go b/vendor/github.com/xenolf/lego/acme/http_challenge_server.go similarity index 86% rename from vendor/github.com/xenolf/lego/acmev2/http_challenge_server.go rename to vendor/github.com/xenolf/lego/acme/http_challenge_server.go index 64c6a8280..319e26185 100644 --- a/vendor/github.com/xenolf/lego/acmev2/http_challenge_server.go +++ b/vendor/github.com/xenolf/lego/acme/http_challenge_server.go @@ -5,6 +5,8 @@ import ( "net" "net/http" "strings" + + "github.com/xenolf/lego/log" ) // HTTPProviderServer implements ChallengeProvider for `http-01` challenge @@ -58,12 +60,12 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { // For validation it then writes the token the server returned with the challenge mux := http.NewServeMux() mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.Host, domain) && r.Method == "GET" { + if strings.HasPrefix(r.Host, domain) && r.Method == http.MethodGet { w.Header().Add("Content-Type", "text/plain") w.Write([]byte(keyAuth)) - logf("[INFO][%s] Served key authentication", domain) + log.Infof("[%s] Served key authentication", domain) } else { - logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method) + log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method) w.Write([]byte("TEST")) } }) diff --git a/vendor/github.com/xenolf/lego/acmev2/jws.go b/vendor/github.com/xenolf/lego/acme/jws.go similarity index 68% rename from vendor/github.com/xenolf/lego/acmev2/jws.go rename to vendor/github.com/xenolf/lego/acme/jws.go index 9b87e437a..bea762104 100644 --- a/vendor/github.com/xenolf/lego/acmev2/jws.go +++ b/vendor/github.com/xenolf/lego/acme/jws.go @@ -26,13 +26,13 @@ type jws struct { func (j *jws) post(url string, content []byte) (*http.Response, error) { signedContent, err := j.signContent(url, content) if err != nil { - return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) + return nil, fmt.Errorf("failed to sign content -> %s", err.Error()) } data := bytes.NewBuffer([]byte(signedContent.FullSerialize())) resp, err := httpPost(url, "application/jose+json", data) if err != nil { - return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error()) + return nil, fmt.Errorf("failed to HTTP POST to %s -> %s", url, err.Error()) } nonce, nonceErr := getNonceFromResponse(resp) @@ -77,16 +77,45 @@ func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, e signer, err := jose.NewSigner(signKey, &options) if err != nil { - return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error()) + return nil, fmt.Errorf("failed to create jose signer -> %s", err.Error()) } signed, err := signer.Sign(content) if err != nil { - return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) + return nil, fmt.Errorf("failed to sign content -> %s", err.Error()) } return signed, nil } +func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { + jwk := jose.JSONWebKey{Key: j.privKey} + jwkJSON, err := jwk.Public().MarshalJSON() + if err != nil { + return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error()) + } + + signer, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, + &jose.SignerOptions{ + EmbedJWK: false, + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": kid, + "url": url, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %s", err.Error()) + } + + signed, err := signer.Sign(jwkJSON) + if err != nil { + return nil, fmt.Errorf("failed to External Account Binding sign content -> %s", err.Error()) + } + + return signed, nil +} + func (j *jws) Nonce() (string, error) { if nonce, ok := j.nonces.Pop(); ok { return nonce, nil @@ -122,7 +151,7 @@ func (n *nonceManager) Push(nonce string) { func getNonce(url string) (string, error) { resp, err := httpHead(url) if err != nil { - return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error()) + return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %s", err.Error()) } return getNonceFromResponse(resp) @@ -131,7 +160,7 @@ func getNonce(url string) (string, error) { func getNonceFromResponse(resp *http.Response) (string, error) { nonce := resp.Header.Get("Replay-Nonce") if nonce == "" { - return "", fmt.Errorf("Server did not respond with a proper nonce header") + return "", fmt.Errorf("server did not respond with a proper nonce header") } return nonce, nil diff --git a/vendor/github.com/xenolf/lego/acmev2/messages.go b/vendor/github.com/xenolf/lego/acme/messages.go similarity index 86% rename from vendor/github.com/xenolf/lego/acmev2/messages.go rename to vendor/github.com/xenolf/lego/acme/messages.go index 0b7344372..6946cc15a 100644 --- a/vendor/github.com/xenolf/lego/acmev2/messages.go +++ b/vendor/github.com/xenolf/lego/acme/messages.go @@ -1,6 +1,7 @@ package acme import ( + "encoding/json" "time" ) @@ -26,11 +27,12 @@ type directory struct { } type accountMessage struct { - Status string `json:"status,omitempty"` - Contact []string `json:"contact,omitempty"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` - Orders string `json:"orders,omitempty"` - OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` + Status string `json:"status,omitempty"` + Contact []string `json:"contact,omitempty"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` + Orders string `json:"orders,omitempty"` + OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` + ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` } type orderResource struct { @@ -76,9 +78,6 @@ type csrMessage struct { Csr string `json:"csr"` } -type emptyObjectMessage struct { -} - type revokeCertMessage struct { Certificate string `json:"certificate"` } diff --git a/vendor/github.com/xenolf/lego/acmev2/provider.go b/vendor/github.com/xenolf/lego/acme/provider.go similarity index 100% rename from vendor/github.com/xenolf/lego/acmev2/provider.go rename to vendor/github.com/xenolf/lego/acme/provider.go diff --git a/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go b/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go new file mode 100644 index 000000000..d80351999 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go @@ -0,0 +1,104 @@ +package acme + +import ( + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + + "github.com/xenolf/lego/log" +) + +// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. +// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.1 +var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} + +type tlsALPNChallenge struct { + jws *jws + validate validateFunc + provider ChallengeProvider +} + +// Solve manages the provider to validate and solve the challenge. +func (t *tlsALPNChallenge) Solve(chlng challenge, domain string) error { + log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", domain) + + // Generate the Key Authorization for the challenge + keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey) + if err != nil { + return err + } + + err = t.provider.Present(domain, chlng.Token, keyAuth) + if err != nil { + return fmt.Errorf("[%s] error presenting token: %v", domain, err) + } + defer func() { + err := t.provider.CleanUp(domain, chlng.Token, keyAuth) + if err != nil { + log.Warnf("[%s] error cleaning up: %v", domain, err) + } + }() + + return t.validate(t.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) +} + +// TLSALPNChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension +// and domain name for the `tls-alpn-01` challenge. +func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) { + // Compute the SHA-256 digest of the key authorization. + zBytes := sha256.Sum256([]byte(keyAuth)) + + value, err := asn1.Marshal(zBytes[:sha256.Size]) + if err != nil { + return nil, nil, err + } + + // Add the keyAuth digest as the acmeValidation-v1 extension + // (marked as critical such that it won't be used by non-ACME software). + // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3 + extensions := []pkix.Extension{ + { + Id: idPeAcmeIdentifierV1, + Critical: true, + Value: value, + }, + } + + // Generate a new RSA key for the certificates. + tempPrivKey, err := generatePrivateKey(RSA2048) + if err != nil { + return nil, nil, err + } + + rsaPrivKey := tempPrivKey.(*rsa.PrivateKey) + + // Generate the PEM certificate using the provided private key, domain, and extra extensions. + tempCertPEM, err := generatePemCert(rsaPrivKey, domain, extensions) + if err != nil { + return nil, nil, err + } + + // Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair. + rsaPrivPEM := pemEncode(rsaPrivKey) + + return tempCertPEM, rsaPrivPEM, nil +} + +// TLSALPNChallengeCert returns a certificate with the acmeValidation-v1 extension +// and domain name for the `tls-alpn-01` challenge. +func TLSALPNChallengeCert(domain, keyAuth string) (*tls.Certificate, error) { + tempCertPEM, rsaPrivPEM, err := TLSALPNChallengeBlocks(domain, keyAuth) + if err != nil { + return nil, err + } + + certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) + if err != nil { + return nil, err + } + + return &certificate, nil +} diff --git a/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge_server.go b/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge_server.go new file mode 100644 index 000000000..8d33668e1 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acme/tls_alpn_challenge_server.go @@ -0,0 +1,86 @@ +package acme + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" +) + +const ( + // ACMETLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol. + ACMETLS1Protocol = "acme-tls/1" + + // defaultTLSPort is the port that the TLSALPNProviderServer will default to + // when no other port is provided. + defaultTLSPort = "443" +) + +// TLSALPNProviderServer implements ChallengeProvider for `TLS-ALPN-01` +// challenge. It may be instantiated without using the NewTLSALPNProviderServer +// if you want only to use the default values. +type TLSALPNProviderServer struct { + iface string + port string + listener net.Listener +} + +// NewTLSALPNProviderServer creates a new TLSALPNProviderServer on the selected +// interface and port. Setting iface and / or port to an empty string will make +// the server fall back to the "any" interface and port 443 respectively. +func NewTLSALPNProviderServer(iface, port string) *TLSALPNProviderServer { + return &TLSALPNProviderServer{iface: iface, port: port} +} + +// Present generates a certificate with a SHA-256 digest of the keyAuth provided +// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN +// spec. +func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error { + if t.port == "" { + // Fallback to port 443 if the port was not provided. + t.port = defaultTLSPort + } + + // Generate the challenge certificate using the provided keyAuth and domain. + cert, err := TLSALPNChallengeCert(domain, keyAuth) + if err != nil { + return err + } + + // Place the generated certificate with the extension into the TLS config + // so that it can serve the correct details. + tlsConf := new(tls.Config) + tlsConf.Certificates = []tls.Certificate{*cert} + + // We must set that the `acme-tls/1` application level protocol is supported + // so that the protocol negotiation can succeed. Reference: + // https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2 + tlsConf.NextProtos = []string{ACMETLS1Protocol} + + // Create the listener with the created tls.Config. + t.listener, err = tls.Listen("tcp", net.JoinHostPort(t.iface, t.port), tlsConf) + if err != nil { + return fmt.Errorf("could not start HTTPS server for challenge -> %v", err) + } + + // Shut the server down when we're finished. + go func() { + http.Serve(t.listener, nil) + }() + + return nil +} + +// CleanUp closes the HTTPS server. +func (t *TLSALPNProviderServer) CleanUp(domain, token, keyAuth string) error { + if t.listener == nil { + return nil + } + + // Server was created, close it. + if err := t.listener.Close(); err != nil && err != http.ErrServerClosed { + return err + } + + return nil +} diff --git a/vendor/github.com/xenolf/lego/acmev2/utils.go b/vendor/github.com/xenolf/lego/acme/utils.go similarity index 100% rename from vendor/github.com/xenolf/lego/acmev2/utils.go rename to vendor/github.com/xenolf/lego/acme/utils.go diff --git a/vendor/github.com/xenolf/lego/acmev2/pop_challenge.go b/vendor/github.com/xenolf/lego/acmev2/pop_challenge.go deleted file mode 100644 index 8d2a213b0..000000000 --- a/vendor/github.com/xenolf/lego/acmev2/pop_challenge.go +++ /dev/null @@ -1 +0,0 @@ -package acme diff --git a/vendor/github.com/xenolf/lego/log/LICENSE b/vendor/github.com/xenolf/lego/log/LICENSE new file mode 100644 index 000000000..270cba089 --- /dev/null +++ b/vendor/github.com/xenolf/lego/log/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2017 Sebastian Erhart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/xenolf/lego/log/logger.go b/vendor/github.com/xenolf/lego/log/logger.go new file mode 100644 index 000000000..101a2c993 --- /dev/null +++ b/vendor/github.com/xenolf/lego/log/logger.go @@ -0,0 +1,49 @@ +package log + +import ( + "log" + "os" +) + +// Logger is an optional custom logger. +var Logger = log.New(os.Stdout, "", log.LstdFlags) + +// Fatal writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Fatal(args ...interface{}) { + Logger.Fatal(args...) +} + +// Fatalf writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Fatalf(format string, args ...interface{}) { + Logger.Fatalf(format, args...) +} + +// Print writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Print(args ...interface{}) { + Logger.Print(args...) +} + +// Println writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Println(args ...interface{}) { + Logger.Println(args...) +} + +// Printf writes a log entry. +// It uses Logger if not nil, otherwise it uses the default log.Logger. +func Printf(format string, args ...interface{}) { + Logger.Printf(format, args...) +} + +// Warnf writes a log entry. +func Warnf(format string, args ...interface{}) { + Printf("[WARN] "+format, args...) +} + +// Infof writes a log entry. +func Infof(format string, args ...interface{}) { + Printf("[INFO] "+format, args...) +} diff --git a/vendor/manifest b/vendor/manifest index 494a59e4f..998f2cfac 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -167,12 +167,21 @@ "notests": true }, { - "importpath": "github.com/xenolf/lego/acmev2", + "importpath": "github.com/xenolf/lego/acme", "repository": "https://github.com/xenolf/lego", "vcs": "git", - "revision": "fad2257e11ae4ff31ed03739386873aa405dec2d", - "branch": "acmev2", - "path": "/acmev2", + "revision": "04e2d74406d42a3727e7a132c1a39735ac527f51", + "branch": "master", + "path": "/acme", + "notests": true + }, + { + "importpath": "github.com/xenolf/lego/log", + "repository": "https://github.com/xenolf/lego", + "vcs": "git", + "revision": "04e2d74406d42a3727e7a132c1a39735ac527f51", + "branch": "master", + "path": "log", "notests": true }, {