From e0f1a02c37ed17b93285d49b443995b19f17431f Mon Sep 17 00:00:00 2001 From: Matthew Holt <mholt@users.noreply.github.com> Date: Mon, 10 Dec 2018 19:49:29 -0700 Subject: [PATCH] Extract most of caddytls core code into external CertMagic package All code relating to a caddytls.Config and setting it up from the Caddyfile is still intact; only the certificate management-related code was removed into a separate package. I don't expect this to build in CI successfully; updating dependencies and vendor is coming next. I've also removed the ad-hoc, half-baked storage plugins that we need to finish making first-class Caddy plugins (they were never documented anyway). The new certmagic package has a much better storage interface, and we can finally move toward making a new storage plugin type, but it shouldn't be configurable in the Caddyfile, I think, since it doesn't make sense for a Caddy instance to use more than one storage config... We also have the option of eliminating DNS provider plugins and just shipping all of lego's DNS providers by using a lego package (the caddytls/setup.go file has a comment describing how) -- but it doubles Caddy's binary size by 100% from about 19 MB to around 40 MB...! --- caddy.go | 62 +-- caddy/caddymain/run.go | 18 +- caddyhttp/bind/bind.go | 2 +- caddyhttp/bind/bind_test.go | 2 +- caddyhttp/httpserver/https.go | 26 +- caddyhttp/httpserver/https_test.go | 15 +- caddyhttp/httpserver/plugin.go | 26 +- caddyhttp/httpserver/server.go | 12 +- caddytls/certificates.go | 390 ----------------- caddytls/certificates_test.go | 82 ---- caddytls/client.go | 429 ------------------- caddytls/client_test.go | 17 - caddytls/config.go | 282 ++----------- caddytls/config_test.go | 119 ------ caddytls/crypto.go | 258 +---------- caddytls/crypto_test.go | 72 ---- caddytls/filestorage.go | 305 ------------- caddytls/filestorage_test.go | 84 ---- caddytls/filestoragesync.go | 140 ------ caddytls/handshake.go | 470 --------------------- caddytls/handshake_test.go | 76 ---- caddytls/httphandler.go | 120 ------ caddytls/httphandler_test.go | 84 ---- caddytls/maintain.go | 365 ---------------- caddytls/selfsigned.go | 106 +++++ caddytls/setup.go | 167 +++++--- caddytls/setup_test.go | 53 +-- caddytls/storage.go | 127 ------ caddytls/storagetest/memorystorage.go | 148 ------- caddytls/storagetest/memorystorage_test.go | 26 -- caddytls/storagetest/storagetest.go | 306 -------------- caddytls/storagetest/storagetest_test.go | 54 --- caddytls/tls.go | 251 ++--------- caddytls/tls_test.go | 139 +----- caddytls/user.go | 233 ---------- caddytls/user_test.go | 221 ---------- controller.go | 12 +- 37 files changed, 403 insertions(+), 4896 deletions(-) delete mode 100644 caddytls/certificates.go delete mode 100644 caddytls/certificates_test.go delete mode 100644 caddytls/client.go delete mode 100644 caddytls/client_test.go delete mode 100644 caddytls/filestorage.go delete mode 100644 caddytls/filestorage_test.go delete mode 100644 caddytls/filestoragesync.go delete mode 100644 caddytls/handshake_test.go delete mode 100644 caddytls/httphandler.go delete mode 100644 caddytls/httphandler_test.go delete mode 100644 caddytls/maintain.go create mode 100644 caddytls/selfsigned.go delete mode 100644 caddytls/storage.go delete mode 100644 caddytls/storagetest/memorystorage.go delete mode 100644 caddytls/storagetest/memorystorage_test.go delete mode 100644 caddytls/storagetest/storagetest.go delete mode 100644 caddytls/storagetest/storagetest_test.go delete mode 100644 caddytls/user.go delete mode 100644 caddytls/user_test.go diff --git a/caddy.go b/caddy.go index 3a2a0ccef..08eacf4ac 100644 --- a/caddy.go +++ b/caddy.go @@ -108,12 +108,12 @@ type Instance struct { servers []ServerListener // these callbacks execute when certain events occur - onFirstStartup []func() error // starting, not as part of a restart - onStartup []func() error // starting, even as part of a restart - onRestart []func() error // before restart commences - onRestartFailed []func() error // if restart failed - onShutdown []func() error // stopping, even as part of a restart - onFinalShutdown []func() error // stopping, not as part of a restart + OnFirstStartup []func() error // starting, not as part of a restart + OnStartup []func() error // starting, even as part of a restart + OnRestart []func() error // before restart commences + OnRestartFailed []func() error // if restart failed + OnShutdown []func() error // stopping, even as part of a restart + OnFinalShutdown []func() error // stopping, not as part of a restart // storing values on an instance is preferable to // global state because these will get garbage- @@ -163,13 +163,13 @@ func (i *Instance) Stop() error { // the rest. All the non-nil errors will be returned. func (i *Instance) ShutdownCallbacks() []error { var errs []error - for _, shutdownFunc := range i.onShutdown { + for _, shutdownFunc := range i.OnShutdown { err := shutdownFunc() if err != nil { errs = append(errs, err) } } - for _, finalShutdownFunc := range i.onFinalShutdown { + for _, finalShutdownFunc := range i.OnFinalShutdown { err := finalShutdownFunc() if err != nil { errs = append(errs, err) @@ -192,7 +192,7 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) { defer func() { r := recover() if err != nil || r != nil { - for _, fn := range i.onRestartFailed { + for _, fn := range i.OnRestartFailed { err = fn() if err != nil { log.Printf("[ERROR] restart failed: %v", err) @@ -205,7 +205,7 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) { }() // run restart callbacks - for _, fn := range i.onRestart { + for _, fn := range i.OnRestart { err = fn() if err != nil { return i, err @@ -252,7 +252,7 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) { if err != nil { return i, err } - for _, shutdownFunc := range i.onShutdown { + for _, shutdownFunc := range i.OnShutdown { err = shutdownFunc() if err != nil { return i, err @@ -274,42 +274,6 @@ func (i *Instance) SaveServer(s Server, ln net.Listener) { i.servers = append(i.servers, ServerListener{server: s, listener: ln}) } -// HasListenerWithAddress returns whether this package is -// tracking a server using a listener with the address -// addr. -func HasListenerWithAddress(addr string) bool { - instancesMu.Lock() - defer instancesMu.Unlock() - for _, inst := range instances { - for _, sln := range inst.servers { - if listenerAddrEqual(sln.listener, addr) { - return true - } - } - } - return false -} - -// listenerAddrEqual compares a listener's address with -// addr. Extra care is taken to match addresses with an -// empty hostname portion, as listeners tend to report -// [::]:80, for example, when the matching address that -// created the listener might be simply :80. -func listenerAddrEqual(ln net.Listener, addr string) bool { - lnAddr := ln.Addr().String() - hostname, port, err := net.SplitHostPort(addr) - if err != nil { - return lnAddr == addr - } - if lnAddr == net.JoinHostPort("::", port) { - return true - } - if lnAddr == net.JoinHostPort("0.0.0.0", port) { - return true - } - return hostname != "" && lnAddr == addr -} - // TCPServer is a type that can listen and serve connections. // A TCPServer must associate with exactly zero or one net.Listeners. type TCPServer interface { @@ -551,14 +515,14 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r // run startup callbacks if !IsUpgrade() && restartFds == nil { // first startup means not a restart or upgrade - for _, firstStartupFunc := range inst.onFirstStartup { + for _, firstStartupFunc := range inst.OnFirstStartup { err = firstStartupFunc() if err != nil { return err } } } - for _, startupFunc := range inst.onStartup { + for _, startupFunc := range inst.OnStartup { err = startupFunc() if err != nil { return err diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 106b7cd9c..c0d1ba494 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -33,8 +33,8 @@ import ( "github.com/mholt/caddy" "github.com/mholt/caddy/caddytls" "github.com/mholt/caddy/telemetry" - "github.com/xenolf/lego/acme" - "gopkg.in/natefinch/lumberjack.v2" + "github.com/mholt/certmagic" + lumberjack "gopkg.in/natefinch/lumberjack.v2" _ "github.com/mholt/caddy/caddyhttp" // plug in the HTTP server type // This is where other plugins get plugged in (imported) @@ -44,17 +44,17 @@ func init() { caddy.TrapSignals() setVersion() - 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.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", caddytls.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge") + 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.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.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") flag.StringVar(&envFile, "env", "", "Path to file with environment variables to load in KEY=VALUE format") flag.BoolVar(&plugins, "plugins", false, "List installed plugins") - flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address") - flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout") + flag.StringVar(&certmagic.Email, "email", "", "Default ACME CA account email address") + flag.DurationVar(&certmagic.HTTPTimeout, "catimeout", certmagic.HTTPTimeout, "Default ACME CA HTTP timeout") flag.StringVar(&logfile, "log", "", "Process log file") flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file") flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") @@ -73,7 +73,7 @@ func Run() { caddy.AppName = appName caddy.AppVersion = appVersion - acme.UserAgent = appName + "/" + appVersion + certmagic.UserAgent = appName + "/" + appVersion // Set up process log before anything bad happens switch logfile { diff --git a/caddyhttp/bind/bind.go b/caddyhttp/bind/bind.go index 7ab0a645e..79489fe4a 100644 --- a/caddyhttp/bind/bind.go +++ b/caddyhttp/bind/bind.go @@ -32,7 +32,7 @@ func setupBind(c *caddy.Controller) error { if !c.Args(&config.ListenHost) { return c.ArgErr() } - config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309 + config.TLS.Manager.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309 } return nil } diff --git a/caddyhttp/bind/bind_test.go b/caddyhttp/bind/bind_test.go index 70221a060..27d7fdc7d 100644 --- a/caddyhttp/bind/bind_test.go +++ b/caddyhttp/bind/bind_test.go @@ -32,7 +32,7 @@ func TestSetupBind(t *testing.T) { if got, want := cfg.ListenHost, "1.2.3.4"; got != want { t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got) } - if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want { + if got, want := cfg.TLS.Manager.ListenHost, "1.2.3.4"; got != want { t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got) } } diff --git a/caddyhttp/httpserver/https.go b/caddyhttp/httpserver/https.go index 86c15547a..f81fde1da 100644 --- a/caddyhttp/httpserver/https.go +++ b/caddyhttp/httpserver/https.go @@ -21,6 +21,7 @@ import ( "github.com/mholt/caddy" "github.com/mholt/caddy/caddytls" + "github.com/mholt/certmagic" ) func activateHTTPS(cctx caddy.Context) error { @@ -37,10 +38,10 @@ func activateHTTPS(cctx caddy.Context) error { // place certificates and keys on disk for _, c := range ctx.siteConfigs { - if c.TLS.OnDemand { + if c.TLS.Manager.OnDemand != nil { continue // obtain these certificates on-demand instead } - err := c.TLS.ObtainCert(c.TLS.Hostname, operatorPresent) + err := c.TLS.Manager.ObtainCert(c.TLS.Hostname, operatorPresent) if err != nil { return err } @@ -62,9 +63,14 @@ func activateHTTPS(cctx caddy.Context) error { // on the ports we'd need to do ACME before we finish starting; parent process // already running renewal ticker, so renewal won't be missed anyway.) if !caddy.IsUpgrade() { - err = caddytls.RenewManagedCertificates(true) - if err != nil { - return err + ctx.instance.StorageMu.RLock() + certCache, ok := ctx.instance.Storage[caddytls.CertCacheInstStorageKey].(*certmagic.Cache) + ctx.instance.StorageMu.RUnlock() + if ok && certCache != nil { + err = certCache.RenewManagedCertificates(operatorPresent) + if err != nil { + return err + } } } @@ -95,13 +101,13 @@ func markQualifiedForAutoHTTPS(configs []*SiteConfig) { // value will always be nil. func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error { for _, cfg := range configs { - if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed || cfg.TLS.OnDemand { + if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed || cfg.TLS.Manager.OnDemand != nil { continue } cfg.TLS.Enabled = true cfg.Addr.Scheme = "https" - if loadCertificates && caddytls.HostQualifies(cfg.TLS.Hostname) { - _, err := cfg.TLS.CacheManagedCertificate(cfg.TLS.Hostname) + if loadCertificates && certmagic.HostQualifies(cfg.TLS.Hostname) { + _, err := cfg.TLS.Manager.CacheManagedCertificate(cfg.TLS.Hostname) if err != nil { return err } @@ -113,7 +119,7 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error { // Set default port of 443 if not explicitly set if cfg.Addr.Port == "" && cfg.TLS.Enabled && - (!cfg.TLS.Manual || cfg.TLS.OnDemand) && + (!cfg.TLS.Manual || cfg.TLS.Manager.OnDemand != nil) && cfg.Addr.Host != "localhost" { cfg.Addr.Port = HTTPSPort } @@ -207,7 +213,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, AltTLSALPNPort: cfg.TLS.AltTLSALPNPort}, + TLS: &caddytls.Config{Manager: cfg.TLS.Manager}, Timeouts: cfg.Timeouts, } } diff --git a/caddyhttp/httpserver/https_test.go b/caddyhttp/httpserver/https_test.go index 043249445..7ee443097 100644 --- a/caddyhttp/httpserver/https_test.go +++ b/caddyhttp/httpserver/https_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/mholt/caddy/caddytls" + "github.com/mholt/certmagic" ) func TestRedirPlaintextHost(t *testing.T) { @@ -150,18 +151,18 @@ func TestHostHasOtherPort(t *testing.T) { func TestMakePlaintextRedirects(t *testing.T) { configs := []*SiteConfig{ // Happy path = standard redirect from 80 to 443 - {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}}, + {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manager: &certmagic.Config{Managed: true}}}, // Host on port 80 already defined; don't change it (no redirect) {Addr: Address{Host: "sub1.example.com", Port: "80", Scheme: "http"}, TLS: new(caddytls.Config)}, - {Addr: Address{Host: "sub1.example.com"}, TLS: &caddytls.Config{Managed: true}}, + {Addr: Address{Host: "sub1.example.com"}, TLS: &caddytls.Config{Manager: &certmagic.Config{Managed: true}}}, // Redirect from port 80 to port 5000 in this case - {Addr: Address{Host: "sub2.example.com", Port: "5000"}, TLS: &caddytls.Config{Managed: true}}, + {Addr: Address{Host: "sub2.example.com", Port: "5000"}, TLS: &caddytls.Config{Manager: &certmagic.Config{Managed: true}}}, // Can redirect from 80 to either 443 or 5001, but choose 443 - {Addr: Address{Host: "sub3.example.com", Port: "443"}, TLS: &caddytls.Config{Managed: true}}, - {Addr: Address{Host: "sub3.example.com", Port: "5001", Scheme: "https"}, TLS: &caddytls.Config{Managed: true}}, + {Addr: Address{Host: "sub3.example.com", Port: "443"}, TLS: &caddytls.Config{Manager: &certmagic.Config{Managed: true}}}, + {Addr: Address{Host: "sub3.example.com", Port: "5001", Scheme: "https"}, TLS: &caddytls.Config{Manager: &certmagic.Config{Managed: true}}}, } result := makePlaintextRedirects(configs) @@ -175,7 +176,7 @@ func TestMakePlaintextRedirects(t *testing.T) { func TestEnableAutoHTTPS(t *testing.T) { configs := []*SiteConfig{ - {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}}, + {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manager: &certmagic.Config{Managed: true}}}, {}, // not managed - no changes! } @@ -215,7 +216,7 @@ func TestMarkQualifiedForAutoHTTPS(t *testing.T) { count := 0 for _, cfg := range configs { - if cfg.TLS.Managed { + if cfg.TLS.Manager.Managed { count++ } } diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 32af0f722..92dc80593 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -23,6 +23,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" "time" @@ -31,6 +32,7 @@ import ( "github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddytls" "github.com/mholt/caddy/telemetry" + "github.com/mholt/certmagic" ) const serverType = "http" @@ -169,12 +171,20 @@ 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, altTLSALPNPort string + var altHTTPPort, altTLSALPNPort int if HTTPPort != DefaultHTTPPort { - altHTTPPort = HTTPPort + portInt, err := strconv.Atoi(HTTPPort) + if err != nil { + return nil, err + } + altHTTPPort = portInt } if HTTPSPort != DefaultHTTPSPort { - altTLSALPNPort = HTTPSPort + portInt, err := strconv.Atoi(HTTPSPort) + if err != nil { + return nil, err + } + altTLSALPNPort = portInt } // Make our caddytls.Config, which has a pointer to the @@ -182,8 +192,8 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd // to use automatic HTTPS when the time comes caddytlsConfig := caddytls.NewConfig(h.instance) caddytlsConfig.Hostname = addr.Host - caddytlsConfig.AltHTTPPort = altHTTPPort - caddytlsConfig.AltTLSALPNPort = altTLSALPNPort + caddytlsConfig.Manager.AltHTTPPort = altHTTPPort + caddytlsConfig.Manager.AltTLSALPNPort = altTLSALPNPort // Save the config to our master list, and key it for lookups cfg := &SiteConfig{ @@ -221,7 +231,7 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { // trusted CA (obviously not a perfect hueristic) var looksLikeProductionCA bool for _, publicCAEndpoint := range caddytls.KnownACMECAs { - if strings.Contains(caddytls.DefaultCAUrl, publicCAEndpoint) { + if strings.Contains(certmagic.CA, publicCAEndpoint) { looksLikeProductionCA = true break } @@ -243,7 +253,7 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { if !caddy.IsLoopback(cfg.Addr.Host) && !caddy.IsLoopback(cfg.ListenHost) && (caddytls.QualifiesForManagedTLS(cfg) || - caddytls.HostQualifies(cfg.Addr.Host)) { + certmagic.HostQualifies(cfg.Addr.Host)) { atLeastOneSiteLooksLikeProduction = true } } @@ -264,7 +274,7 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { // is incorrect for this site. cfg.Addr.Scheme = "https" } - if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.OnDemand) { + if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.Manager.OnDemand != nil) { // this is vital, otherwise the function call below that // sets the listener address will use the default port // instead of 443 because it doesn't know about TLS. diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 800f921de..142272f07 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -402,24 +402,26 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error) if vhost == nil { // check for ACME challenge even if vhost is nil; - // could be a new host coming online soon - if caddytls.HTTPChallengeHandler(w, r, "localhost") { + // could be a new host coming online soon - choose any + // vhost's cert manager configuration, I guess + if len(s.sites) > 0 && s.sites[0].TLS.Manager.HandleHTTPChallenge(w, r) { return 0, nil } + // otherwise, log the error and write a message to the client remoteHost, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { remoteHost = r.RemoteAddr } - WriteSiteNotFound(w, r) // don't add headers outside of this function + WriteSiteNotFound(w, r) // don't add headers outside of this function (http.forwardproxy) log.Printf("[INFO] %s - No such site at %s (Remote: %s, Referer: %s)", hostname, s.Server.Addr, remoteHost, r.Header.Get("Referer")) return 0, nil } // we still check for ACME challenge if the vhost exists, - // because we must apply its HTTP challenge config settings - if caddytls.HTTPChallengeHandler(w, r, vhost.ListenHost) { + // because the HTTP challenge might be disabled by its config + if vhost.TLS.Manager.HandleHTTPChallenge(w, r) { return 0, nil } diff --git a/caddytls/certificates.go b/caddytls/certificates.go deleted file mode 100644 index 7df4e11d6..000000000 --- a/caddytls/certificates.go +++ /dev/null @@ -1,390 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "crypto/sha256" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io/ioutil" - "log" - "strings" - "sync" - "time" - - "github.com/mholt/caddy/telemetry" - "golang.org/x/crypto/ocsp" -) - -// certificateCache is to be an instance-wide cache of certs -// that site-specific TLS configs can refer to. Using a -// central map like this avoids duplication of certs in -// memory when the cert is used by multiple sites, and makes -// maintenance easier. Because these are not to be global, -// the cache will get garbage collected after a config reload -// (a new instance will take its place). -type certificateCache struct { - sync.RWMutex - cache map[string]Certificate // keyed by certificate hash -} - -// replaceCertificate replaces oldCert with newCert in the cache, and -// updates all configs that are pointing to the old certificate to -// point to the new one instead. newCert must already be loaded into -// the cache (this method does NOT load it into the cache). -// -// Note that all the names on the old certificate will be deleted -// from the name lookup maps of each config, then all the names on -// the new certificate will be added to the lookup maps as long as -// they do not overwrite any entries. -// -// The newCert may be modified and its cache entry updated. -// -// This method is safe for concurrent use. -func (certCache *certificateCache) replaceCertificate(oldCert, newCert Certificate) error { - certCache.Lock() - defer certCache.Unlock() - - // have all the configs that are pointing to the old - // certificate point to the new certificate instead - for _, cfg := range oldCert.configs { - // first delete all the name lookup entries that - // pointed to the old certificate - for name, certKey := range cfg.Certificates { - if certKey == oldCert.Hash { - delete(cfg.Certificates, name) - } - } - - // then add name lookup entries for the names - // on the new certificate, but don't overwrite - // entries that may already exist, not only as - // a courtesy, but importantly: because if we - // overwrote a value here, and this config no - // longer pointed to a certain certificate in - // the cache, that certificate's list of configs - // referring to it would be incorrect; so just - // insert entries, don't overwrite any - for _, name := range newCert.Names { - if _, ok := cfg.Certificates[name]; !ok { - cfg.Certificates[name] = newCert.Hash - } - } - } - - // since caching a new certificate attaches only the config - // that loaded it, the new certificate needs to be given the - // list of all the configs that use it, so copy the list - // over from the old certificate to the new certificate - // in the cache - newCert.configs = oldCert.configs - certCache.cache[newCert.Hash] = newCert - - // finally, delete the old certificate from the cache - delete(certCache.cache, oldCert.Hash) - - return nil -} - -// reloadManagedCertificate reloads the certificate corresponding to the name(s) -// on oldCert into the cache, from storage. This also replaces the old certificate -// with the new one, so that all configurations that used the old cert now point -// to the new cert. -func (certCache *certificateCache) reloadManagedCertificate(oldCert Certificate) error { - // get the certificate from storage and cache it - newCert, err := oldCert.configs[0].CacheManagedCertificate(oldCert.Names[0]) - if err != nil { - return fmt.Errorf("unable to reload certificate for %v into cache: %v", oldCert.Names, err) - } - - // and replace the old certificate with the new one - err = certCache.replaceCertificate(oldCert, newCert) - if err != nil { - return fmt.Errorf("replacing certificate %v: %v", oldCert.Names, err) - } - - return nil -} - -// Certificate is a tls.Certificate with associated metadata tacked on. -// Even if the metadata can be obtained by parsing the certificate, -// we are more efficient by extracting the metadata onto this struct. -type Certificate struct { - tls.Certificate - - // Names is the list of names this certificate is written for. - // The first is the CommonName (if any), the rest are SAN. - Names []string - - // NotAfter is when the certificate expires. - NotAfter time.Time - - // OCSP contains the certificate's parsed OCSP response. - OCSP *ocsp.Response - - // The hex-encoded hash of this cert's chain's bytes. - Hash string - - // configs is the list of configs that use or refer to - // The first one is assumed to be the config that is - // "in charge" of this certificate (i.e. determines - // whether it is managed, how it is managed, etc). - // This field will be populated by cacheCertificate. - // Only meddle with it if you know what you're doing! - configs []*Config -} - -// CacheManagedCertificate loads the certificate for domain into the -// cache, from the TLS storage for managed certificates. It returns a -// copy of the Certificate that was put into the cache. -// -// This method is safe for concurrent use. -func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error) { - storage, err := cfg.StorageFor(cfg.CAUrl) - if err != nil { - return Certificate{}, err - } - siteData, err := storage.LoadSite(domain) - if err != nil { - return Certificate{}, err - } - cert, err := makeCertificateWithOCSP(siteData.Cert, siteData.Key) - if err != nil { - return cert, err - } - telemetry.Increment("tls_managed_cert_count") - return cfg.cacheCertificate(cert), nil -} - -// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile -// and keyFile, which must be in PEM format. It stores the certificate in -// the in-memory cache. -// -// This function is safe for concurrent use. -func (cfg *Config) cacheUnmanagedCertificatePEMFile(certFile, keyFile string) error { - cert, err := makeCertificateFromDiskWithOCSP(certFile, keyFile) - if err != nil { - return err - } - cfg.cacheCertificate(cert) - telemetry.Increment("tls_manual_cert_count") - return nil -} - -// cacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes -// of the certificate and key, then caches it in memory. -// -// This function is safe for concurrent use. -func (cfg *Config) cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error { - cert, err := makeCertificateWithOCSP(certBytes, keyBytes) - if err != nil { - return err - } - cfg.cacheCertificate(cert) - telemetry.Increment("tls_manual_cert_count") - return nil -} - -// makeCertificateFromDiskWithOCSP makes a Certificate by loading the -// certificate and key files. It fills out all the fields in -// the certificate except for the Managed and OnDemand flags. -// (It is up to the caller to set those.) It staples OCSP. -func makeCertificateFromDiskWithOCSP(certFile, keyFile string) (Certificate, error) { - certPEMBlock, err := ioutil.ReadFile(certFile) - if err != nil { - return Certificate{}, err - } - keyPEMBlock, err := ioutil.ReadFile(keyFile) - if err != nil { - return Certificate{}, err - } - return makeCertificateWithOCSP(certPEMBlock, keyPEMBlock) -} - -// makeCertificate turns a certificate PEM bundle and a key PEM block into -// a Certificate with necessary metadata from parsing its bytes filled into -// its struct fields for convenience (except for the OnDemand and Managed -// flags; it is up to the caller to set those properties!). This function -// does NOT staple OCSP. -func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { - var cert Certificate - - // Convert to a tls.Certificate - tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) - if err != nil { - return cert, err - } - - // Extract necessary metadata - err = fillCertFromLeaf(&cert, tlsCert) - if err != nil { - return cert, err - } - - return cert, nil -} - -// makeCertificateWithOCSP is the same as makeCertificate except that it also -// staples OCSP to the certificate. -func makeCertificateWithOCSP(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { - cert, err := makeCertificate(certPEMBlock, keyPEMBlock) - if err != nil { - return cert, err - } - err = stapleOCSP(&cert, certPEMBlock) - if err != nil { - log.Printf("[WARNING] Stapling OCSP: %v", err) - } - return cert, nil -} - -// fillCertFromLeaf populates metadata fields on cert from tlsCert. -func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error { - if len(tlsCert.Certificate) == 0 { - return errors.New("certificate is empty") - } - cert.Certificate = tlsCert - - // the leaf cert should be the one for the site; it has what we need - leaf, err := x509.ParseCertificate(tlsCert.Certificate[0]) - if err != nil { - return err - } - - if leaf.Subject.CommonName != "" { // TODO: CommonName is deprecated - cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)} - } - for _, name := range leaf.DNSNames { - if name != leaf.Subject.CommonName { // TODO: CommonName is deprecated - cert.Names = append(cert.Names, strings.ToLower(name)) - } - } - for _, ip := range leaf.IPAddresses { - if ipStr := ip.String(); ipStr != leaf.Subject.CommonName { // TODO: CommonName is deprecated - cert.Names = append(cert.Names, strings.ToLower(ipStr)) - } - } - for _, email := range leaf.EmailAddresses { - if email != leaf.Subject.CommonName { // TODO: CommonName is deprecated - cert.Names = append(cert.Names, strings.ToLower(email)) - } - } - if len(cert.Names) == 0 { - return errors.New("certificate has no names") - } - - // save the hash of this certificate (chain) and - // expiration date, for necessity and efficiency - cert.Hash = hashCertificateChain(cert.Certificate.Certificate) - cert.NotAfter = leaf.NotAfter - - return nil -} - -// hashCertificateChain computes the unique hash of certChain, -// which is the chain of DER-encoded bytes. It returns the -// hex encoding of the hash. -func hashCertificateChain(certChain [][]byte) string { - h := sha256.New() - for _, certInChain := range certChain { - h.Write(certInChain) - } - return fmt.Sprintf("%x", h.Sum(nil)) -} - -// managedCertInStorageExpiresSoon returns true if cert (being a -// managed certificate) is expiring within RenewDurationBefore. -// It returns false if there was an error checking the expiration -// of the certificate as found in storage, or if the certificate -// in storage is NOT expiring soon. A certificate that is expiring -// soon in our cache but is not expiring soon in storage probably -// means that another instance renewed the certificate in the -// meantime, and it would be a good idea to simply load the cert -// into our cache rather than repeating the renewal process again. -func managedCertInStorageExpiresSoon(cert Certificate) (bool, error) { - if len(cert.configs) == 0 { - return false, fmt.Errorf("no configs for certificate") - } - storage, err := cert.configs[0].StorageFor(cert.configs[0].CAUrl) - if err != nil { - return false, err - } - siteData, err := storage.LoadSite(cert.Names[0]) - if err != nil { - return false, err - } - tlsCert, err := tls.X509KeyPair(siteData.Cert, siteData.Key) - if err != nil { - return false, err - } - leaf, err := x509.ParseCertificate(tlsCert.Certificate[0]) - if err != nil { - return false, err - } - timeLeft := leaf.NotAfter.Sub(time.Now().UTC()) - return timeLeft < RenewDurationBefore, nil -} - -// cacheCertificate adds cert to the in-memory cache. If a certificate -// with the same hash is already cached, it is NOT overwritten; instead, -// cfg is added to the existing certificate's list of configs if not -// already in the list. Then all the names on cert are used to add -// entries to cfg.Certificates (the config's name lookup map). -// Then the certificate is stored/updated in the cache. It returns -// a copy of the certificate that ends up being stored in the cache. -// -// It is VERY important, even for some test cases, that the Hash field -// of the cert be set properly. -// -// This function is safe for concurrent use. -func (cfg *Config) cacheCertificate(cert Certificate) Certificate { - cfg.certCache.Lock() - defer cfg.certCache.Unlock() - - // if this certificate already exists in the cache, - // use it instead of overwriting it -- very important! - if existingCert, ok := cfg.certCache.cache[cert.Hash]; ok { - cert = existingCert - } - - // attach this config to the certificate so we know which - // configs are referencing/using the certificate, but don't - // duplicate entries - var found bool - for _, c := range cert.configs { - if c == cfg { - found = true - break - } - } - if !found { - cert.configs = append(cert.configs, cfg) - } - - // key the certificate by all its names for this config only, - // this is how we find the certificate during handshakes - // (yes, if certs overlap in the names they serve, one will - // overwrite another here, but that's just how it goes) - for _, name := range cert.Names { - cfg.Certificates[name] = cert.Hash - } - - // store the certificate - cfg.certCache.cache[cert.Hash] = cert - - return cert -} diff --git a/caddytls/certificates_test.go b/caddytls/certificates_test.go deleted file mode 100644 index 1a5332ca0..000000000 --- a/caddytls/certificates_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import "testing" - -func TestUnexportedGetCertificate(t *testing.T) { - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} - - // When cache is empty - if _, matched, defaulted := cfg.getCertificate("example.com"); matched || defaulted { - t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted) - } - - // When cache has one certificate in it - firstCert := Certificate{Names: []string{"example.com"}} - certCache.cache["0xdeadbeef"] = firstCert - cfg.Certificates["example.com"] = "0xdeadbeef" - if cert, matched, defaulted := cfg.getCertificate("Example.com"); !matched || defaulted || cert.Names[0] != "example.com" { - t.Errorf("Didn't get a cert for 'Example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) - } - if cert, matched, defaulted := cfg.getCertificate("example.com"); !matched || defaulted || cert.Names[0] != "example.com" { - t.Errorf("Didn't get a cert for 'example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) - } - - // When retrieving wildcard certificate - certCache.cache["0xb01dface"] = Certificate{Names: []string{"*.example.com"}} - cfg.Certificates["*.example.com"] = "0xb01dface" - if cert, matched, defaulted := cfg.getCertificate("sub.example.com"); !matched || defaulted || cert.Names[0] != "*.example.com" { - t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) - } - - // When no certificate matches and SNI is provided, return no certificate (should be TLS alert) - if cert, matched, defaulted := cfg.getCertificate("nomatch"); matched || defaulted { - t.Errorf("Expected matched=false, defaulted=false; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert) - } -} - -func TestCacheCertificate(t *testing.T) { - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} - - cfg.cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}, Hash: "foobar"}) - if len(certCache.cache) != 1 { - t.Errorf("Expected length of certificate cache to be 1") - } - if _, ok := certCache.cache["foobar"]; !ok { - t.Error("Expected first cert to be cached by key 'foobar', but it wasn't") - } - if _, ok := cfg.Certificates["example.com"]; !ok { - t.Error("Expected first cert to be keyed by 'example.com', but it wasn't") - } - if _, ok := cfg.Certificates["sub.example.com"]; !ok { - t.Error("Expected first cert to be keyed by 'sub.example.com', but it wasn't") - } - - // different config, but using same cache; and has cert with overlapping name, - // but different hash - cfg2 := &Config{Certificates: make(map[string]string), certCache: certCache} - cfg2.cacheCertificate(Certificate{Names: []string{"example.com"}, Hash: "barbaz"}) - if _, ok := certCache.cache["barbaz"]; !ok { - t.Error("Expected second cert to be cached by key 'barbaz.com', but it wasn't") - } - if hash, ok := cfg2.Certificates["example.com"]; !ok { - t.Error("Expected second cert to be keyed by 'example.com', but it wasn't") - } else if hash != "barbaz" { - t.Errorf("Expected second cert to map to 'barbaz' but it was %s instead", hash) - } -} diff --git a/caddytls/client.go b/caddytls/client.go deleted file mode 100644 index 3c971e48a..000000000 --- a/caddytls/client.go +++ /dev/null @@ -1,429 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "encoding/json" - "errors" - "fmt" - "log" - "net" - "net/url" - "strings" - "sync" - "time" - - "github.com/mholt/caddy" - "github.com/mholt/caddy/telemetry" - "github.com/xenolf/lego/acme" -) - -// acmeMu ensures that only one ACME challenge occurs at a time. -var acmeMu sync.Mutex - -// ACMEClient is a wrapper over acme.Client with -// some custom state attached. It is used to obtain, -// renew, and revoke certificates with ACME. -type ACMEClient struct { - AllowPrompts bool - config *Config - acmeClient *acme.Client - storage Storage -} - -// newACMEClient creates a new ACMEClient given an email and whether -// prompting the user is allowed. It's a variable so we can mock in tests. -var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) { - storage, err := config.StorageFor(config.CAUrl) - if err != nil { - return nil, err - } - - // Look up or create the LE user account - leUser, err := getUser(storage, config.ACMEEmail) - if err != nil { - return nil, err - } - - // ensure key type is set - keyType := DefaultKeyType - if config.KeyType != "" { - keyType = config.KeyType - } - - // ensure CA URL (directory endpoint) is set - caURL := DefaultCAUrl - if config.CAUrl != "" { - caURL = config.CAUrl - } - - // ensure endpoint is secure (assume HTTPS if scheme is missing) - if !strings.Contains(caURL, "://") { - caURL = "https://" + caURL - } - u, err := url.Parse(caURL) - if err != nil { - return nil, err - } - if u.Scheme != "https" && !caddy.IsLoopback(u.Host) && !caddy.IsInternal(u.Host) { - return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL) - } - - // The client facilitates our communication with the CA server. - client, err := acme.NewClient(caURL, &leUser, keyType) - if err != nil { - return nil, err - } - - // If not registered, the user must register an account with the CA - // and agree to terms - if leUser.Registration == nil { - if allowPrompts { // can't prompt a user who isn't there - termsURL := client.GetToSURL() - if !Agreed && termsURL != "" { - Agreed = askUserAgreement(client.GetToSURL()) - } - if !Agreed && termsURL != "" { - return nil, errors.New("user must agree to CA terms (use -agree flag)") - } - } - - reg, err := client.Register(Agreed) - if err != nil { - return nil, errors.New("registration error: " + err.Error()) - } - leUser.Registration = reg - - // save user to the file system - err = saveUser(storage, leUser) - if err != nil { - return nil, errors.New("could not save user: " + err.Error()) - } - } - - c := &ACMEClient{ - AllowPrompts: allowPrompts, - config: config, - acmeClient: client, - storage: storage, - } - - if config.DNSProvider == "" { - // Use HTTP and TLS-ALPN challenges by default - - // 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 - } - - // 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 { - useDistributedSolver = true - } - } - 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(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 - } - } - - // 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) - } - if DisableTLSALPNChallenge { - disabledChallenges = append(disabledChallenges, acme.TLSALPN01) - } - if len(disabledChallenges) > 0 { - c.acmeClient.ExcludeChallenges(disabledChallenges) - } - } else { - // Otherwise, use DNS challenge exclusively - - // Load provider constructor function - provFn, ok := dnsProviders[config.DNSProvider] - if !ok { - return nil, errors.New("unknown DNS provider by name '" + config.DNSProvider + "'") - } - - // We could pass credentials to create the provider, but for now - // just let the solver package get them from the environment - prov, err := provFn() - if err != nil { - return nil, err - } - - // Use the DNS challenge exclusively - c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSALPN01}) - c.acmeClient.SetChallengeProvider(acme.DNS01, prov) - } - - return c, nil -} - -// Obtain obtains a single certificate for name. It stores the certificate -// on the disk if successful. This function is safe for concurrent use. -// -// Right now our storage mechanism only supports one name per certificate, -// so this function (along with Renew and Revoke) only accepts one domain -// as input. It can be easily modified to support SAN certificates if our -// storage mechanism is upgraded later. -// -// Callers who have access to a Config value should use the ObtainCert -// method on that instead of this lower-level method. -func (c *ACMEClient) Obtain(name string) error { - waiter, err := c.storage.TryLock(name) - if err != nil { - return err - } - if waiter != nil { - log.Printf("[INFO] Certificate for %s is already being obtained elsewhere and stored; waiting", name) - waiter.Wait() - return nil // we assume the process with the lock succeeded, rather than hammering this execution path again - } - defer func() { - if err := c.storage.Unlock(name); err != nil { - log.Printf("[ERROR] Unable to unlock obtain call for %s: %v", name, err) - } - }() - - for attempts := 0; attempts < 2; attempts++ { - namesObtaining.Add([]string{name}) - acmeMu.Lock() - certificate, err := c.acmeClient.ObtainCertificate([]string{name}, true, nil, c.config.MustStaple) - acmeMu.Unlock() - namesObtaining.Remove([]string{name}) - if err != nil { - // for a certain kind of error, we can enumerate the error per-domain - if failures, ok := err.(acme.ObtainError); ok && len(failures) > 0 { - var errMsg string // combine all the failures into a single error message - for errDomain, obtainErr := range failures { - if obtainErr == nil { - continue - } - errMsg += fmt.Sprintf("[%s] failed to get certificate: %v\n", errDomain, obtainErr) - } - return errors.New(errMsg) - } - - return fmt.Errorf("[%s] failed to obtain certificate: %v", name, err) - } - - // double-check that we actually got a certificate, in case there's a bug upstream (see issue #2121) - if certificate.Domain == "" || certificate.Certificate == nil { - return errors.New("returned certificate was empty; probably an unchecked error obtaining it") - } - - // Success - immediately save the certificate resource - err = saveCertResource(c.storage, certificate) - if err != nil { - return fmt.Errorf("error saving assets for %v: %v", name, err) - } - - break - } - - go telemetry.Increment("tls_acme_certs_obtained") - - return nil -} - -// Renew renews the managed certificate for name. It puts the renewed -// certificate into storage (not the cache). This function is safe for -// concurrent use. -// -// Callers who have access to a Config value should use the RenewCert -// method on that instead of this lower-level method. -func (c *ACMEClient) Renew(name string) error { - waiter, err := c.storage.TryLock(name) - if err != nil { - return err - } - if waiter != nil { - log.Printf("[INFO] Certificate for %s is already being renewed elsewhere and stored; waiting", name) - waiter.Wait() - return nil // assume that the worker that renewed the cert succeeded; avoid hammering this path over and over - } - defer func() { - if err := c.storage.Unlock(name); err != nil { - log.Printf("[ERROR] Unable to unlock renew call for %s: %v", name, err) - } - }() - - // Prepare for renewal (load PEM cert, key, and meta) - siteData, err := c.storage.LoadSite(name) - if err != nil { - return err - } - var certMeta acme.CertificateResource - err = json.Unmarshal(siteData.Meta, &certMeta) - certMeta.Certificate = siteData.Cert - certMeta.PrivateKey = siteData.Key - - // Perform renewal and retry if necessary, but not too many times. - var newCertMeta *acme.CertificateResource - var success bool - for attempts := 0; attempts < 2; attempts++ { - namesObtaining.Add([]string{name}) - acmeMu.Lock() - newCertMeta, err = c.acmeClient.RenewCertificate(certMeta, true, c.config.MustStaple) - acmeMu.Unlock() - namesObtaining.Remove([]string{name}) - if err == 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 - break - } - } - - // wait a little bit and try again - wait := 10 * time.Second - log.Printf("[ERROR] Renewing [%v]: %v; trying again in %s", name, err, wait) - time.Sleep(wait) - } - - if !success { - return errors.New("too many renewal attempts; last error: " + err.Error()) - } - - caddy.EmitEvent(caddy.CertRenewEvent, name) - go telemetry.Increment("tls_acme_certs_renewed") - - return saveCertResource(c.storage, newCertMeta) -} - -// Revoke revokes the certificate for name and deletes -// it from storage. -func (c *ACMEClient) Revoke(name string) error { - siteExists, err := c.storage.SiteExists(name) - if err != nil { - return err - } - - if !siteExists { - return errors.New("no certificate and key for " + name) - } - - siteData, err := c.storage.LoadSite(name) - if err != nil { - return err - } - - err = c.acmeClient.RevokeCertificate(siteData.Cert) - if err != nil { - return err - } - - go telemetry.Increment("tls_acme_certs_revoked") - - err = c.storage.DeleteSite(name) - if err != nil { - return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error()) - } - - return nil -} - -// namesObtaining is a set of hostnames with thread-safe -// methods. A name should be in this set only while this -// package is in the process of obtaining a certificate -// for the name. ACME challenges that are received for -// names which are not in this set were not initiated by -// this package and probably should not be handled by -// this package. -var namesObtaining = nameCoordinator{names: make(map[string]struct{})} - -type nameCoordinator struct { - names map[string]struct{} - mu sync.RWMutex -} - -// Add adds names to c. It is safe for concurrent use. -func (c *nameCoordinator) Add(names []string) { - c.mu.Lock() - for _, name := range names { - c.names[strings.ToLower(name)] = struct{}{} - } - c.mu.Unlock() -} - -// Remove removes names from c. It is safe for concurrent use. -func (c *nameCoordinator) Remove(names []string) { - c.mu.Lock() - for _, name := range names { - delete(c.names, strings.ToLower(name)) - } - c.mu.Unlock() -} - -// Has returns true if c has name. It is safe for concurrent use. -func (c *nameCoordinator) Has(name string) bool { - hostname, _, err := net.SplitHostPort(name) - if err != nil { - hostname = name - } - c.mu.RLock() - _, ok := c.names[strings.ToLower(hostname)] - c.mu.RUnlock() - return ok -} - -// KnownACMECAs is a list of ACME directory endpoints of -// known, public, and trusted ACME-compatible certificate -// authorities. -var KnownACMECAs = []string{ - "https://acme-v02.api.letsencrypt.org/directory", -} diff --git a/caddytls/client_test.go b/caddytls/client_test.go deleted file mode 100644 index 5b699053a..000000000 --- a/caddytls/client_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -// TODO diff --git a/caddytls/config.go b/caddytls/config.go index e093d3f0e..539eed6e6 100644 --- a/caddytls/config.go +++ b/caddytls/config.go @@ -17,16 +17,15 @@ package caddytls import ( "crypto/tls" "crypto/x509" - "errors" "fmt" "io/ioutil" - "net/url" - "strings" + "github.com/xenolf/lego/challenge/tlsalpn01" "github.com/klauspost/cpuid" "github.com/mholt/caddy" - "github.com/xenolf/lego/acme" + "github.com/mholt/certmagic" + "github.com/xenolf/lego/certcrypto" ) // Config describes how TLS should be configured and used. @@ -64,102 +63,31 @@ type Config struct { // Manual means user provides own certs and keys Manual bool - // Managed means config qualifies for implicit, - // automatic, managed TLS; as opposed to the user - // providing and managing the certificate manually + // Managed means this config should be managed + // by the CertMagic Config (Manager field) Managed bool - // OnDemand means the class of hostnames this - // config applies to may obtain and manage - // certificates at handshake-time (as opposed - // to pre-loaded at startup); OnDemand certs - // will be managed the same way as preloaded - // ones, however, if an OnDemand cert fails to - // renew, it is removed from the in-memory - // cache; if this is true, Managed must - // necessarily be true - OnDemand bool + // Manager is how certificates are managed + Manager *certmagic.Config // SelfSigned means that this hostname is // served with a self-signed certificate // that we generated in memory for convenience SelfSigned bool - // The endpoint of the directory for the ACME - // CA we are to use - CAUrl string - - // The host (ONLY the host, not port) to listen - // on if necessary to start a listener to solve - // an ACME challenge - ListenHost string - - // The alternate port (ONLY port, not host) to - // use for the ACME HTTP challenge; if non-empty, - // this port will be used instead of - // HTTPChallengePort to spin up a listener for - // the HTTP challenge - AltHTTPPort string - - // The alternate port (ONLY port, not host) - // to use for the ACME TLS-ALPN challenge; - // the system must forward TLSALPNChallengePort - // to this port for challenge to succeed - AltTLSALPNPort string - - // The string identifier of the DNS provider - // to use when solving the ACME DNS challenge - DNSProvider string - // The email address to use when creating or // using an ACME account (fun fact: if this // is set to "off" then this config will not // qualify for managed TLS) ACMEEmail string - // The type of key to use when generating - // certificates - KeyType acme.KeyType - - // The storage creator; use StorageFor() to get a guaranteed - // non-nil Storage instance. Note, Caddy may call this frequently - // so implementors are encouraged to cache any heavy instantiations. - StorageProvider string - - // The state needed to operate on-demand TLS - OnDemandState OnDemandState - - // Add the must staple TLS extension to the CSR generated by lego/acme - MustStaple bool - // The list of protocols to choose from for Application Layer // Protocol Negotiation (ALPN). ALPN []string - // The map of hostname to certificate hash. This is used to complete - // handshakes and serve the right certificate given the SNI. - Certificates map[string]string - - certCache *certificateCache // pointer to the Instance's certificate store - tlsConfig *tls.Config // the final tls.Config created with buildStandardTLSConfig() -} - -// OnDemandState contains some state relevant for providing -// on-demand TLS. -type OnDemandState struct { - // The number of certificates that have been issued on-demand - // by this config. It is only safe to modify this count atomically. - // If it reaches MaxObtain, on-demand issuances must fail. - ObtainedCount int32 - - // Set from max_certs in tls config, it specifies the - // maximum number of certificates that can be issued. - MaxObtain int32 - - // The url to call to check if an on-demand tls certificate should - // be issued. If a request to the URL fails or returns a non 2xx - // status on-demand issuances must fail. - AskURL *url.URL + // The final tls.Config created with + // buildStandardTLSConfig() + tlsConfig *tls.Config } // NewConfig returns a new Config with a pointer to the instance's @@ -167,149 +95,21 @@ type OnDemandState struct { // the returned Config for successful practical use. func NewConfig(inst *caddy.Instance) *Config { inst.StorageMu.RLock() - certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certificateCache) + certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certmagic.Cache) inst.StorageMu.RUnlock() if !ok || certCache == nil { - certCache = &certificateCache{cache: make(map[string]Certificate)} + certCache = certmagic.NewCache(certmagic.FileStorage{Path: caddy.AssetsPath()}) + inst.OnShutdown = append(inst.OnShutdown, func() error { + certCache.Stop() + return nil + }) inst.StorageMu.Lock() inst.Storage[CertCacheInstStorageKey] = certCache inst.StorageMu.Unlock() } - cfg := new(Config) - cfg.Certificates = make(map[string]string) - cfg.certCache = certCache - return cfg -} - -// ObtainCert obtains a certificate for name using c, as long -// as a certificate does not already exist in storage for that -// name. The name must qualify and c must be flagged as Managed. -// This function is a no-op if storage already has a certificate -// for name. -// -// It only obtains and stores certificates (and their keys), -// it does not load them into memory. If allowPrompts is true, -// the user may be shown a prompt. -func (c *Config) ObtainCert(name string, allowPrompts bool) error { - skip, err := c.preObtainOrRenewChecks(name, allowPrompts) - if err != nil { - return err + return &Config{ + Manager: certmagic.NewWithCache(certCache, certmagic.Config{}), // TODO } - if skip { - return nil - } - - // we expect this to be a new (non-existent) site - storage, err := c.StorageFor(c.CAUrl) - if err != nil { - return err - } - siteExists, err := storage.SiteExists(name) - if err != nil { - return err - } - if siteExists { - return nil - } - - client, err := newACMEClient(c, allowPrompts) - if err != nil { - return err - } - return client.Obtain(name) -} - -// RenewCert renews the certificate for name using c. It stows the -// renewed certificate and its assets in storage if successful. -func (c *Config) RenewCert(name string, allowPrompts bool) error { - skip, err := c.preObtainOrRenewChecks(name, allowPrompts) - if err != nil { - return err - } - if skip { - return nil - } - - client, err := newACMEClient(c, allowPrompts) - if err != nil { - return err - } - return client.Renew(name) -} - -// preObtainOrRenewChecks perform a few simple checks before -// obtaining or renewing a certificate with ACME, and returns -// whether this name should be skipped (like if it's not -// managed TLS) as well as any error. It ensures that the -// config is Managed, that the name qualifies for a certificate, -// and that an email address is available. -func (c *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool, error) { - if !c.Managed || !HostQualifies(name) { - return true, nil - } - - // wildcard certificates require DNS challenge (as of March 2018) - if strings.Contains(name, "*") && c.DNSProvider == "" { - return false, fmt.Errorf("wildcard domain name (%s) requires DNS challenge; use dns subdirective to configure it", name) - } - - if c.ACMEEmail == "" { - var err error - c.ACMEEmail, err = getEmail(c, allowPrompts) - if err != nil { - return false, err - } - } - - return false, nil -} - -// StorageFor obtains a TLS Storage instance for the given CA URL which should -// be unique for every different ACME CA. If a StorageCreator is set on this -// Config, it will be used. Otherwise the default file storage implementation -// is used. When the error is nil, this is guaranteed to return a non-nil -// Storage instance. -func (c *Config) StorageFor(caURL string) (Storage, error) { - // Validate CA URL - if caURL == "" { - caURL = DefaultCAUrl - } - if caURL == "" { - return nil, fmt.Errorf("cannot create storage without CA URL") - } - caURL = strings.ToLower(caURL) - - // scheme required or host will be parsed as path (as of Go 1.6) - if !strings.Contains(caURL, "://") { - caURL = "https://" + caURL - } - - u, err := url.Parse(caURL) - if err != nil { - return nil, fmt.Errorf("%s: unable to parse CA URL: %v", caURL, err) - } - - if u.Host == "" { - return nil, fmt.Errorf("%s: no host in CA URL", caURL) - } - - // Create the storage based on the URL - var s Storage - if c.StorageProvider == "" { - c.StorageProvider = "file" - } - - creator, ok := storageProviders[c.StorageProvider] - if !ok { - return nil, fmt.Errorf("%s: Unknown storage: %v", caURL, c.StorageProvider) - } - - s, err = creator(u) - if err != nil { - return nil, fmt.Errorf("%s: unable to create custom storage '%v': %v", caURL, c.StorageProvider, err) - } - - return s, nil } // buildStandardTLSConfig converts cfg (*caddytls.Config) to a *tls.Config @@ -346,20 +146,20 @@ 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 { + if a == tlsalpn01.ACMETLS1Protocol { alpnFound = true break } } if !alpnFound { - c.ALPN = append(c.ALPN, acme.ACMETLS1Protocol) + c.ALPN = append(c.ALPN, tlsalpn01.ACMETLS1Protocol) } config.MinVersion = c.ProtocolMinVersion config.MaxVersion = c.ProtocolMaxVersion config.ClientAuth = c.ClientAuth config.NextProtos = c.ALPN - config.GetCertificate = c.GetCertificate + config.GetCertificate = c.Manager.GetCertificate // set up client authentication if enabled if config.ClientAuth != tls.NoClientCert { @@ -580,12 +380,12 @@ func SetDefaultTLSParams(config *Config) { } // Map of supported key types -var supportedKeyTypes = map[string]acme.KeyType{ - "P384": acme.EC384, - "P256": acme.EC256, - "RSA8192": acme.RSA8192, - "RSA4096": acme.RSA4096, - "RSA2048": acme.RSA2048, +var supportedKeyTypes = map[string]certcrypto.KeyType{ + "P384": certcrypto.EC384, + "P256": certcrypto.EC256, + "RSA8192": certcrypto.RSA8192, + "RSA4096": certcrypto.RSA4096, + "RSA2048": certcrypto.RSA2048, } // SupportedProtocols is a map of supported protocols. @@ -605,7 +405,7 @@ func GetSupportedProtocolName(protocol uint16) (string, error) { } } - return "", errors.New("name: unsuported protocol") + return "", fmt.Errorf("name: unsuported protocol") } // SupportedCiphersMap has supported ciphers, used only for parsing config. @@ -643,7 +443,7 @@ func GetSupportedCipherName(cipher uint16) (string, error) { } } - return "", errors.New("name: unsuported cipher") + return "", fmt.Errorf("name: unsuported cipher") } // List of all the ciphers we want to use by default @@ -706,24 +506,6 @@ var defaultCurves = []tls.CurveID{ tls.CurveP256, } -const ( - // HTTPChallengePort is the officially-designated port for - // the HTTP challenge according to the ACME spec. - HTTPChallengePort = "80" - - // 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. - // If this alternate port is used instead of the default - // port, then whatever is listening on the default port must - // be capable of proxying or forwarding the request to this - // alternate port. - DefaultHTTPAlternatePort = "5033" - - // CertCacheInstStorageKey is the name of the key for - // accessing the certificate storage on the *caddy.Instance. - CertCacheInstStorageKey = "tls_cert_cache" -) +// CertCacheInstStorageKey is the name of the key for +// accessing the certificate storage on the *caddy.Instance. +const CertCacheInstStorageKey = "tls_cert_cache" diff --git a/caddytls/config_test.go b/caddytls/config_test.go index c69f2d550..a0af13740 100644 --- a/caddytls/config_test.go +++ b/caddytls/config_test.go @@ -16,8 +16,6 @@ package caddytls import ( "crypto/tls" - "errors" - "net/url" "reflect" "testing" @@ -110,120 +108,3 @@ func TestGetPreferredDefaultCiphers(t *testing.T) { } } } - -func TestStorageForNoURL(t *testing.T) { - c := &Config{} - if _, err := c.StorageFor(""); err == nil { - t.Fatal("Expected error on empty URL") - } -} - -func TestStorageForLowercasesAndPrefixesScheme(t *testing.T) { - resultStr := "" - RegisterStorageProvider("fake-TestStorageForLowercasesAndPrefixesScheme", func(caURL *url.URL) (Storage, error) { - resultStr = caURL.String() - return nil, nil - }) - c := &Config{ - StorageProvider: "fake-TestStorageForLowercasesAndPrefixesScheme", - } - if _, err := c.StorageFor("EXAMPLE.COM/BLAH"); err != nil { - t.Fatal(err) - } - if resultStr != "https://example.com/blah" { - t.Fatalf("Unexpected CA URL string: %v", resultStr) - } -} - -func TestStorageForBadURL(t *testing.T) { - c := &Config{} - if _, err := c.StorageFor("http://192.168.0.%31/"); err == nil { - t.Fatal("Expected error for bad URL") - } -} - -func TestStorageForDefault(t *testing.T) { - c := &Config{} - s, err := c.StorageFor("example.com") - if err != nil { - t.Fatal(err) - } - if _, ok := s.(*FileStorage); !ok { - t.Fatalf("Unexpected storage type: %#v", s) - } -} - -func TestStorageForCustom(t *testing.T) { - storage := fakeStorage("fake-TestStorageForCustom") - RegisterStorageProvider("fake-TestStorageForCustom", func(caURL *url.URL) (Storage, error) { return storage, nil }) - c := &Config{ - StorageProvider: "fake-TestStorageForCustom", - } - s, err := c.StorageFor("example.com") - if err != nil { - t.Fatal(err) - } - if s != storage { - t.Fatal("Unexpected storage") - } -} - -func TestStorageForCustomError(t *testing.T) { - RegisterStorageProvider("fake-TestStorageForCustomError", func(caURL *url.URL) (Storage, error) { return nil, errors.New("some error") }) - c := &Config{ - StorageProvider: "fake-TestStorageForCustomError", - } - if _, err := c.StorageFor("example.com"); err == nil { - t.Fatal("Expecting error") - } -} - -func TestStorageForCustomNil(t *testing.T) { - // Should fall through to the default - c := &Config{StorageProvider: ""} - s, err := c.StorageFor("example.com") - if err != nil { - t.Fatal(err) - } - if _, ok := s.(*FileStorage); !ok { - t.Fatalf("Unexpected storage type: %#v", s) - } -} - -type fakeStorage string - -func (s fakeStorage) SiteExists(domain string) (bool, error) { - panic("no impl") -} - -func (s fakeStorage) LoadSite(domain string) (*SiteData, error) { - panic("no impl") -} - -func (s fakeStorage) StoreSite(domain string, data *SiteData) error { - panic("no impl") -} - -func (s fakeStorage) DeleteSite(domain string) error { - panic("no impl") -} - -func (s fakeStorage) TryLock(domain string) (Waiter, error) { - panic("no impl") -} - -func (s fakeStorage) Unlock(domain string) error { - panic("no impl") -} - -func (s fakeStorage) LoadUser(email string) (*UserData, error) { - panic("no impl") -} - -func (s fakeStorage) StoreUser(email string, data *UserData) error { - panic("no impl") -} - -func (s fakeStorage) MostRecentUserEmail() string { - panic("no impl") -} diff --git a/caddytls/crypto.go b/caddytls/crypto.go index 3f24b78a3..ff2e9f912 100644 --- a/caddytls/crypto.go +++ b/caddytls/crypto.go @@ -15,265 +15,20 @@ package caddytls import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "errors" - "fmt" - "hash/fnv" "io" - "io/ioutil" - "log" - "math/big" - "net" - "os" - "path/filepath" - "strings" "sync" "time" - - "golang.org/x/crypto/ocsp" - - "github.com/mholt/caddy" - "github.com/xenolf/lego/acme" ) -// loadPrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes. -func loadPrivateKey(keyBytes []byte) (crypto.PrivateKey, error) { - keyBlock, _ := pem.Decode(keyBytes) - - switch keyBlock.Type { - case "RSA PRIVATE KEY": - return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) - case "EC PRIVATE KEY": - return x509.ParseECPrivateKey(keyBlock.Bytes) - } - - return nil, errors.New("unknown private key type") -} - -// savePrivateKey saves a PEM-encoded ECC/RSA private key to an array of bytes. -func savePrivateKey(key crypto.PrivateKey) ([]byte, error) { - var pemType string - var keyBytes []byte - switch key := key.(type) { - case *ecdsa.PrivateKey: - var err error - pemType = "EC" - keyBytes, err = x509.MarshalECPrivateKey(key) - if err != nil { - return nil, err - } - case *rsa.PrivateKey: - pemType = "RSA" - keyBytes = x509.MarshalPKCS1PrivateKey(key) - } - - pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} - return pem.EncodeToMemory(&pemKey), nil -} - -// stapleOCSP staples OCSP information to cert for hostname name. -// If you have it handy, you should pass in the PEM-encoded certificate -// bundle; otherwise the DER-encoded cert will have to be PEM-encoded. -// If you don't have the PEM blocks already, just pass in nil. -// -// Errors here are not necessarily fatal, it could just be that the -// certificate doesn't have an issuer URL. -func stapleOCSP(cert *Certificate, pemBundle []byte) error { - if pemBundle == nil { - // The function in the acme package that gets OCSP requires a PEM-encoded cert - bundle := new(bytes.Buffer) - for _, derBytes := range cert.Certificate.Certificate { - pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - } - pemBundle = bundle.Bytes() - } - - var ocspBytes []byte - var ocspResp *ocsp.Response - var ocspErr error - var gotNewOCSP bool - - // First try to load OCSP staple from storage and see if - // we can still use it. - // TODO: Use Storage interface instead of disk directly - var ocspFileNamePrefix string - if len(cert.Names) > 0 { - firstName := strings.Replace(cert.Names[0], "*", "wildcard_", -1) - ocspFileNamePrefix = firstName + "-" - } - ocspFileName := ocspFileNamePrefix + fastHash(pemBundle) - ocspCachePath := filepath.Join(ocspFolder, ocspFileName) - cachedOCSP, err := ioutil.ReadFile(ocspCachePath) - if err == nil { - resp, err := ocsp.ParseResponse(cachedOCSP, nil) - if err == nil { - if freshOCSP(resp) { - // staple is still fresh; use it - ocspBytes = cachedOCSP - ocspResp = resp - } - } else { - // invalid contents; delete the file - // (we do this independently of the maintenance routine because - // in this case we know for sure this should be a staple file - // because we loaded it by name, whereas the maintenance routine - // just iterates the list of files, even if somehow a non-staple - // file gets in the folder. in this case we are sure it is corrupt.) - err := os.Remove(ocspCachePath) - if err != nil { - log.Printf("[WARNING] Unable to delete invalid OCSP staple file: %v", err) - } - } - } - - // If we couldn't get a fresh staple by reading the cache, - // then we need to request it from the OCSP responder - if ocspResp == nil || len(ocspBytes) == 0 { - ocspBytes, ocspResp, ocspErr = acme.GetOCSPForCert(pemBundle) - if ocspErr != nil { - // An error here is not a problem because a certificate may simply - // not contain a link to an OCSP server. But we should log it anyway. - // There's nothing else we can do to get OCSP for this certificate, - // so we can return here with the error. - return fmt.Errorf("no OCSP stapling for %v: %v", cert.Names, ocspErr) - } - gotNewOCSP = true - } - - // By now, we should have a response. If good, staple it to - // the certificate. If the OCSP response was not loaded from - // storage, we persist it for next time. - if ocspResp.Status == ocsp.Good { - if ocspResp.NextUpdate.After(cert.NotAfter) { - // uh oh, this OCSP response expires AFTER the certificate does, that's kinda bogus. - // it was the reason a lot of Symantec-validated sites (not Caddy) went down - // in October 2017. https://twitter.com/mattiasgeniar/status/919432824708648961 - return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)", - cert.Names, cert.NotAfter.Sub(ocspResp.NextUpdate)) - } - cert.Certificate.OCSPStaple = ocspBytes - cert.OCSP = ocspResp - if gotNewOCSP { - err := os.MkdirAll(filepath.Join(caddy.AssetsPath(), "ocsp"), 0700) - if err != nil { - return fmt.Errorf("unable to make OCSP staple path for %v: %v", cert.Names, err) - } - err = ioutil.WriteFile(ocspCachePath, ocspBytes, 0644) - if err != nil { - return fmt.Errorf("unable to write OCSP staple file for %v: %v", cert.Names, err) - } - } - } - - return nil -} - -func makeSelfSignedCertWithCustomSAN(sans []string, config *Config) (Certificate, error) { - // start by generating private key - var privKey interface{} - var err error - switch config.KeyType { - case "", acme.EC256: - privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case acme.EC384: - privKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case acme.RSA2048: - privKey, err = rsa.GenerateKey(rand.Reader, 2048) - case acme.RSA4096: - privKey, err = rsa.GenerateKey(rand.Reader, 4096) - case acme.RSA8192: - privKey, err = rsa.GenerateKey(rand.Reader, 8192) - default: - return Certificate{}, fmt.Errorf("cannot generate private key; unknown key type %v", config.KeyType) - } - if err != nil { - return Certificate{}, fmt.Errorf("failed to generate private key: %v", err) - } - - // create certificate structure with proper values - notBefore := time.Now() - notAfter := notBefore.Add(24 * time.Hour * 7) - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return Certificate{}, fmt.Errorf("failed to generate serial number: %v", err) - } - cert := &x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{Organization: []string{"Caddy Self-Signed"}}, - NotBefore: notBefore, - NotAfter: notAfter, - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - } - if len(sans) == 0 { - sans = []string{""} - } - var names []string - for _, san := range sans { - if ip := net.ParseIP(san); ip != nil { - names = append(names, strings.ToLower(ip.String())) - cert.IPAddresses = append(cert.IPAddresses, ip) - } else { - names = append(names, strings.ToLower(san)) - cert.DNSNames = append(cert.DNSNames, strings.ToLower(san)) - } - } - - publicKey := func(privKey interface{}) interface{} { - switch k := privKey.(type) { - case *rsa.PrivateKey: - return &k.PublicKey - case *ecdsa.PrivateKey: - return &k.PublicKey - default: - return errors.New("unknown key type") - } - } - derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey(privKey), privKey) - if err != nil { - return Certificate{}, fmt.Errorf("could not create certificate: %v", err) - } - - chain := [][]byte{derBytes} - - return Certificate{ - Certificate: tls.Certificate{ - Certificate: chain, - PrivateKey: privKey, - Leaf: cert, - }, - Names: names, - NotAfter: cert.NotAfter, - Hash: hashCertificateChain(chain), - }, nil -} - -// makeSelfSignedCertForConfig makes a self-signed certificate according -// to the parameters in config and caches the new cert in config directly. -func makeSelfSignedCertForConfig(config *Config) error { - cert, err := makeSelfSignedCertWithCustomSAN([]string{config.Hostname}, config) - if err != nil { - return err - } - config.cacheCertificate(cert) - return nil -} - // RotateSessionTicketKeys rotates the TLS session ticket keys // on cfg every TicketRotateInterval. It spawns a new goroutine so // this function does NOT block. It returns a channel you should // close when you are ready to stop the key rotation, like when the // server using cfg is no longer running. +// +// TODO: See about moving this into CertMagic and using its Storage func RotateSessionTicketKeys(cfg *tls.Config) chan struct{} { ch := make(chan struct{}) ticker := time.NewTicker(TicketRotateInterval) @@ -347,15 +102,6 @@ func standaloneTLSTicketKeyRotation(c *tls.Config, ticker *time.Ticker, exitChan } } -// fastHash hashes input using a hashing algorithm that -// is fast, and returns the hash as a hex-encoded string. -// Do not use this for cryptographic purposes. -func fastHash(input []byte) string { - h := fnv.New32a() - h.Write(input) - return fmt.Sprintf("%x", h.Sum32()) -} - const ( // NumTickets is how many tickets to hold and consider // to decrypt TLS sessions. diff --git a/caddytls/crypto_test.go b/caddytls/crypto_test.go index 957086f68..a8d59e040 100644 --- a/caddytls/crypto_test.go +++ b/caddytls/crypto_test.go @@ -15,83 +15,11 @@ package caddytls import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" "crypto/tls" - "crypto/x509" "testing" "time" ) -func TestSaveAndLoadRSAPrivateKey(t *testing.T) { - privateKey, err := rsa.GenerateKey(rand.Reader, 128) // make tests faster; small key size OK for testing - if err != nil { - t.Fatal(err) - } - - // test save - savedBytes, err := savePrivateKey(privateKey) - if err != nil { - t.Fatal("error saving private key:", err) - } - - // test load - loadedKey, err := loadPrivateKey(savedBytes) - if err != nil { - t.Error("error loading private key:", err) - } - - // verify loaded key is correct - if !PrivateKeysSame(privateKey, loadedKey) { - t.Error("Expected key bytes to be the same, but they weren't") - } -} - -func TestSaveAndLoadECCPrivateKey(t *testing.T) { - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - t.Fatal(err) - } - - // test save - savedBytes, err := savePrivateKey(privateKey) - if err != nil { - t.Fatal("error saving private key:", err) - } - - // test load - loadedKey, err := loadPrivateKey(savedBytes) - if err != nil { - t.Error("error loading private key:", err) - } - - // verify loaded key is correct - if !PrivateKeysSame(privateKey, loadedKey) { - t.Error("Expected key bytes to be the same, but they weren't") - } -} - -// PrivateKeysSame compares the bytes of a and b and returns true if they are the same. -func PrivateKeysSame(a, b crypto.PrivateKey) bool { - return bytes.Equal(PrivateKeyBytes(a), PrivateKeyBytes(b)) -} - -// PrivateKeyBytes returns the bytes of DER-encoded key. -func PrivateKeyBytes(key crypto.PrivateKey) []byte { - var keyBytes []byte - switch key := key.(type) { - case *rsa.PrivateKey: - keyBytes = x509.MarshalPKCS1PrivateKey(key) - case *ecdsa.PrivateKey: - keyBytes, _ = x509.MarshalECPrivateKey(key) - } - return keyBytes -} - func TestStandaloneTLSTicketKeyRotation(t *testing.T) { type syncPkt struct { ticketKey [32]byte diff --git a/caddytls/filestorage.go b/caddytls/filestorage.go deleted file mode 100644 index 4d5aebdde..000000000 --- a/caddytls/filestorage.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "fmt" - "io/ioutil" - "log" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/mholt/caddy" -) - -func init() { - RegisterStorageProvider("file", NewFileStorage) -} - -// NewFileStorage is a StorageConstructor function that creates a new -// Storage instance backed by the local disk. The resulting Storage -// instance is guaranteed to be non-nil if there is no error. -func NewFileStorage(caURL *url.URL) (Storage, error) { - // storageBasePath is the root path in which all TLS/ACME assets are - // stored. Do not change this value during the lifetime of the program. - storageBasePath := filepath.Join(caddy.AssetsPath(), "acme") - - storage := &FileStorage{Path: filepath.Join(storageBasePath, caURL.Host)} - storage.Locker = &fileStorageLock{caURL: caURL.Host, storage: storage} - return storage, nil -} - -// FileStorage facilitates forming file paths derived from a root -// directory. It is used to get file paths in a consistent, -// cross-platform way or persisting ACME assets on the file system. -type FileStorage struct { - Path string - Locker -} - -// sites gets the directory that stores site certificate and keys. -func (s *FileStorage) sites() string { - return filepath.Join(s.Path, "sites") -} - -// site returns the path to the folder containing assets for domain. -func (s *FileStorage) site(domain string) string { - domain = fileSafe(domain) - return filepath.Join(s.sites(), domain) -} - -// siteCertFile returns the path to the certificate file for domain. -func (s *FileStorage) siteCertFile(domain string) string { - domain = fileSafe(domain) - return filepath.Join(s.site(domain), domain+".crt") -} - -// siteKeyFile returns the path to domain's private key file. -func (s *FileStorage) siteKeyFile(domain string) string { - domain = fileSafe(domain) - return filepath.Join(s.site(domain), domain+".key") -} - -// siteMetaFile returns the path to the domain's asset metadata file. -func (s *FileStorage) siteMetaFile(domain string) string { - domain = fileSafe(domain) - return filepath.Join(s.site(domain), domain+".json") -} - -// users gets the directory that stores account folders. -func (s *FileStorage) users() string { - return filepath.Join(s.Path, "users") -} - -// user gets the account folder for the user with email -func (s *FileStorage) user(email string) string { - if email == "" { - email = emptyEmail - } - email = fileSafe(email) - return filepath.Join(s.users(), email) -} - -// emailUsername returns the username portion of an email address (part before -// '@') or the original input if it can't find the "@" symbol. -func emailUsername(email string) string { - at := strings.Index(email, "@") - if at == -1 { - return email - } else if at == 0 { - return email[1:] - } - return email[:at] -} - -// userRegFile gets the path to the registration file for the user with the -// given email address. -func (s *FileStorage) userRegFile(email string) string { - if email == "" { - email = emptyEmail - } - email = strings.ToLower(email) - fileName := emailUsername(email) - if fileName == "" { - fileName = "registration" - } - fileName = fileSafe(fileName) - return filepath.Join(s.user(email), fileName+".json") -} - -// userKeyFile gets the path to the private key file for the user with the -// given email address. -func (s *FileStorage) userKeyFile(email string) string { - if email == "" { - email = emptyEmail - } - email = strings.ToLower(email) - fileName := emailUsername(email) - if fileName == "" { - fileName = "private" - } - fileName = fileSafe(fileName) - return filepath.Join(s.user(email), fileName+".key") -} - -// readFile abstracts a simple ioutil.ReadFile, making sure to return an -// ErrNotExist instance when the file is not found. -func (s *FileStorage) readFile(file string) ([]byte, error) { - b, err := ioutil.ReadFile(file) - if os.IsNotExist(err) { - return nil, ErrNotExist(err) - } - return b, err -} - -// SiteExists implements Storage.SiteExists by checking for the presence of -// cert and key files. -func (s *FileStorage) SiteExists(domain string) (bool, error) { - _, err := os.Stat(s.siteCertFile(domain)) - if os.IsNotExist(err) { - return false, nil - } else if err != nil { - return false, err - } - - _, err = os.Stat(s.siteKeyFile(domain)) - if err != nil { - return false, err - } - return true, nil -} - -// LoadSite implements Storage.LoadSite by loading it from disk. If it is not -// present, an instance of ErrNotExist is returned. -func (s *FileStorage) LoadSite(domain string) (*SiteData, error) { - var err error - siteData := new(SiteData) - siteData.Cert, err = s.readFile(s.siteCertFile(domain)) - if err != nil { - return nil, err - } - siteData.Key, err = s.readFile(s.siteKeyFile(domain)) - if err != nil { - return nil, err - } - siteData.Meta, err = s.readFile(s.siteMetaFile(domain)) - if err != nil { - return nil, err - } - return siteData, nil -} - -// StoreSite implements Storage.StoreSite by writing it to disk. The base -// directories needed for the file are automatically created as needed. -func (s *FileStorage) StoreSite(domain string, data *SiteData) error { - err := os.MkdirAll(s.site(domain), 0700) - if err != nil { - return fmt.Errorf("making site directory: %v", err) - } - err = ioutil.WriteFile(s.siteCertFile(domain), data.Cert, 0600) - if err != nil { - return fmt.Errorf("writing certificate file: %v", err) - } - err = ioutil.WriteFile(s.siteKeyFile(domain), data.Key, 0600) - if err != nil { - return fmt.Errorf("writing key file: %v", err) - } - err = ioutil.WriteFile(s.siteMetaFile(domain), data.Meta, 0600) - if err != nil { - return fmt.Errorf("writing cert meta file: %v", err) - } - log.Printf("[INFO][%v] Certificate written to disk: %v", domain, s.siteCertFile(domain)) - return nil -} - -// DeleteSite implements Storage.DeleteSite by deleting just the cert from -// disk. If it is not present, an instance of ErrNotExist is returned. -func (s *FileStorage) DeleteSite(domain string) error { - err := os.Remove(s.siteCertFile(domain)) - if err != nil { - if os.IsNotExist(err) { - return ErrNotExist(err) - } - return err - } - return nil -} - -// LoadUser implements Storage.LoadUser by loading it from disk. If it is not -// present, an instance of ErrNotExist is returned. -func (s *FileStorage) LoadUser(email string) (*UserData, error) { - var err error - userData := new(UserData) - userData.Reg, err = s.readFile(s.userRegFile(email)) - if err != nil { - return nil, err - } - userData.Key, err = s.readFile(s.userKeyFile(email)) - if err != nil { - return nil, err - } - return userData, nil -} - -// StoreUser implements Storage.StoreUser by writing it to disk. The base -// directories needed for the file are automatically created as needed. -func (s *FileStorage) StoreUser(email string, data *UserData) error { - err := os.MkdirAll(s.user(email), 0700) - if err != nil { - return fmt.Errorf("making user directory: %v", err) - } - err = ioutil.WriteFile(s.userRegFile(email), data.Reg, 0600) - if err != nil { - return fmt.Errorf("writing user registration file: %v", err) - } - err = ioutil.WriteFile(s.userKeyFile(email), data.Key, 0600) - if err != nil { - return fmt.Errorf("writing user key file: %v", err) - } - return nil -} - -// MostRecentUserEmail implements Storage.MostRecentUserEmail by finding the -// most recently written sub directory in the users' directory. It is named -// after the email address. This corresponds to the most recent call to -// StoreUser. -func (s *FileStorage) MostRecentUserEmail() string { - userDirs, err := ioutil.ReadDir(s.users()) - if err != nil { - return "" - } - var mostRecent os.FileInfo - for _, dir := range userDirs { - if !dir.IsDir() { - continue - } - if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) { - mostRecent = dir - } - } - if mostRecent != nil { - return mostRecent.Name() - } - return "" -} - -// fileSafe standardizes and sanitizes str for use in a file path. -func fileSafe(str string) string { - str = strings.ToLower(str) - str = strings.TrimSpace(str) - repl := strings.NewReplacer( - "..", "", - "/", "", - "\\", "", - // TODO: Consider also replacing "@" with "_at_" (but migrate existing accounts...) - "+", "_plus_", - "*", "wildcard_", - "%", "", - "$", "", - "`", "", - "~", "", - ":", "", - ";", "", - "=", "", - "!", "", - "#", "", - "&", "", - "|", "", - `"`, "", - "'", "") - return repl.Replace(str) -} diff --git a/caddytls/filestorage_test.go b/caddytls/filestorage_test.go deleted file mode 100644 index e28dfc403..000000000 --- a/caddytls/filestorage_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "path/filepath" - "testing" -) - -// *********************************** NOTE ******************************** -// Due to circular package dependencies with the storagetest sub package and -// the fact that we want to use that harness to test file storage, most of -// the tests for file storage are done in the storagetest package. - -func TestPathBuilders(t *testing.T) { - fs := FileStorage{Path: "test"} - - for i, testcase := range []struct { - in, folder, certFile, keyFile, metaFile string - }{ - { - in: "example.com", - folder: filepath.Join("test", "sites", "example.com"), - certFile: filepath.Join("test", "sites", "example.com", "example.com.crt"), - keyFile: filepath.Join("test", "sites", "example.com", "example.com.key"), - metaFile: filepath.Join("test", "sites", "example.com", "example.com.json"), - }, - { - in: "*.example.com", - folder: filepath.Join("test", "sites", "wildcard_.example.com"), - certFile: filepath.Join("test", "sites", "wildcard_.example.com", "wildcard_.example.com.crt"), - keyFile: filepath.Join("test", "sites", "wildcard_.example.com", "wildcard_.example.com.key"), - metaFile: filepath.Join("test", "sites", "wildcard_.example.com", "wildcard_.example.com.json"), - }, - { - // prevent directory traversal! very important, esp. with on-demand TLS - // see issue #2092 - in: "a/../../../foo", - folder: filepath.Join("test", "sites", "afoo"), - certFile: filepath.Join("test", "sites", "afoo", "afoo.crt"), - keyFile: filepath.Join("test", "sites", "afoo", "afoo.key"), - metaFile: filepath.Join("test", "sites", "afoo", "afoo.json"), - }, - { - in: "b\\..\\..\\..\\foo", - folder: filepath.Join("test", "sites", "bfoo"), - certFile: filepath.Join("test", "sites", "bfoo", "bfoo.crt"), - keyFile: filepath.Join("test", "sites", "bfoo", "bfoo.key"), - metaFile: filepath.Join("test", "sites", "bfoo", "bfoo.json"), - }, - { - in: "c/foo", - folder: filepath.Join("test", "sites", "cfoo"), - certFile: filepath.Join("test", "sites", "cfoo", "cfoo.crt"), - keyFile: filepath.Join("test", "sites", "cfoo", "cfoo.key"), - metaFile: filepath.Join("test", "sites", "cfoo", "cfoo.json"), - }, - } { - if actual := fs.site(testcase.in); actual != testcase.folder { - t.Errorf("Test %d: site folder: Expected '%s' but got '%s'", i, testcase.folder, actual) - } - if actual := fs.siteCertFile(testcase.in); actual != testcase.certFile { - t.Errorf("Test %d: site cert file: Expected '%s' but got '%s'", i, testcase.certFile, actual) - } - if actual := fs.siteKeyFile(testcase.in); actual != testcase.keyFile { - t.Errorf("Test %d: site key file: Expected '%s' but got '%s'", i, testcase.keyFile, actual) - } - if actual := fs.siteMetaFile(testcase.in); actual != testcase.metaFile { - t.Errorf("Test %d: site meta file: Expected '%s' but got '%s'", i, testcase.metaFile, actual) - } - } -} diff --git a/caddytls/filestoragesync.go b/caddytls/filestoragesync.go deleted file mode 100644 index 251f8861f..000000000 --- a/caddytls/filestoragesync.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "fmt" - "os" - "sync" - "time" - - "github.com/mholt/caddy" -) - -func init() { - // be sure to remove lock files when exiting the process! - caddy.OnProcessExit = append(caddy.OnProcessExit, func() { - fileStorageNameLocksMu.Lock() - defer fileStorageNameLocksMu.Unlock() - for key, fw := range fileStorageNameLocks { - os.Remove(fw.filename) - delete(fileStorageNameLocks, key) - } - }) -} - -// fileStorageLock facilitates ACME-related locking by using -// the associated FileStorage, so multiple processes can coordinate -// renewals on the certificates on a shared file system. -type fileStorageLock struct { - caURL string - storage *FileStorage -} - -// TryLock attempts to get a lock for name, otherwise it returns -// a Waiter value to wait until the other process is finished. -func (s *fileStorageLock) TryLock(name string) (Waiter, error) { - fileStorageNameLocksMu.Lock() - defer fileStorageNameLocksMu.Unlock() - - // see if lock already exists within this process - fw, ok := fileStorageNameLocks[s.caURL+name] - if ok { - // lock already created within process, let caller wait on it - return fw, nil - } - - // attempt to persist lock to disk by creating lock file - fw = &fileWaiter{ - filename: s.storage.siteCertFile(name) + ".lock", - wg: new(sync.WaitGroup), - } - // parent dir must exist - if err := os.MkdirAll(s.storage.site(name), 0700); err != nil { - return nil, err - } - lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644) - if err != nil { - if os.IsExist(err) { - // another process has the lock; use it to wait - return fw, nil - } - // otherwise, this was some unexpected error - return nil, err - } - lf.Close() - - // looks like we get the lock - fw.wg.Add(1) - fileStorageNameLocks[s.caURL+name] = fw - - return nil, nil -} - -// Unlock unlocks name. -func (s *fileStorageLock) Unlock(name string) error { - fileStorageNameLocksMu.Lock() - defer fileStorageNameLocksMu.Unlock() - fw, ok := fileStorageNameLocks[s.caURL+name] - if !ok { - return fmt.Errorf("FileStorage: no lock to release for %s", name) - } - // remove lock file - os.Remove(fw.filename) - - // if parent folder is now empty, remove it too to keep it tidy - lockParentFolder := s.storage.site(name) - dir, err := os.Open(lockParentFolder) - if err == nil { - items, _ := dir.Readdirnames(3) // OK to ignore error here - if len(items) == 0 { - os.Remove(lockParentFolder) - } - dir.Close() - } - - fw.wg.Done() - delete(fileStorageNameLocks, s.caURL+name) - return nil -} - -// fileWaiter waits for a file to disappear; it polls -// the file system to check for the existence of a file. -// It also has a WaitGroup which will be faster than -// polling, for when locking need only happen within this -// process. -type fileWaiter struct { - filename string - wg *sync.WaitGroup -} - -// Wait waits until the lock is released. -func (fw *fileWaiter) Wait() { - start := time.Now() - fw.wg.Wait() - for time.Since(start) < 1*time.Hour { - _, err := os.Stat(fw.filename) - if os.IsNotExist(err) { - return - } - time.Sleep(1 * time.Second) - } -} - -var fileStorageNameLocks = make(map[string]*fileWaiter) // keyed by CA + name -var fileStorageNameLocksMu sync.Mutex - -var _ Locker = &fileStorageLock{} -var _ Waiter = &fileWaiter{} diff --git a/caddytls/handshake.go b/caddytls/handshake.go index 86bf656df..b90aab117 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -16,20 +16,10 @@ 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 @@ -89,451 +79,6 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls return nil, nil } -// GetCertificate gets a certificate to satisfy clientHello. In getting -// the certificate, it abides the rules and settings defined in the -// Config that matches clientHello.ServerName. It first checks the in- -// memory cache, then, if the config enables "OnDemand", it accesses -// disk, then accesses the network if it must obtain a new certificate -// via ACME. -// -// This method is safe for use as a tls.Config.GetCertificate callback. -func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - if ClientHelloTelemetry && len(clientHello.SupportedVersions) > 0 { - // If no other plugin (such as the HTTP server type) is implementing ClientHello telemetry, we do it. - // NOTE: The values in the Go standard lib's ClientHelloInfo aren't guaranteed to be in order. - info := ClientHelloInfo{ - Version: clientHello.SupportedVersions[0], // report the highest - CipherSuites: clientHello.CipherSuites, - ExtensionsUnknown: true, // no extension info... :( - CompressionMethodsUnknown: true, // no compression methods... :( - Curves: clientHello.SupportedCurves, - Points: clientHello.SupportedPoints, - // We also have, but do not yet use: SignatureSchemes, ServerName, and SupportedProtos (ALPN) - // because the standard lib parses some extensions, but our MITM detector generally doesn't. - } - 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 { - go telemetry.Increment("tls_handshake_count") // TODO: This is a "best guess" for now, we need something listener-level - } - return &cert.Certificate, err -} - -// getCertificate gets a certificate that matches name (a server name) -// from the in-memory cache, according to the lookup table associated with -// cfg. The lookup then points to a certificate in the Instance certificate -// cache. -// -// If there is no exact match for name, it will be checked against names of -// the form '*.example.com' (wildcard certificates) according to RFC 6125. -// If a match is found, matched will be true. If no matches are found, matched -// will be false and a "default" certificate will be returned with defaulted -// set to true. If defaulted is false, then no certificates were available. -// -// The logic in this function is adapted from the Go standard library, -// which is by the Go Authors. -// -// This function is safe for concurrent use. -func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defaulted bool) { - var certKey string - var ok bool - - // Not going to trim trailing dots here since RFC 3546 says, - // "The hostname is represented ... without a trailing dot." - // Just normalize to lowercase. - name = strings.ToLower(name) - - cfg.certCache.RLock() - defer cfg.certCache.RUnlock() - - // exact match? great, let's use it - if certKey, ok = cfg.Certificates[name]; ok { - cert = cfg.certCache.cache[certKey] - matched = true - return - } - - // try replacing labels in the name with wildcards until we get a match - labels := strings.Split(name, ".") - for i := range labels { - labels[i] = "*" - candidate := strings.Join(labels, ".") - if certKey, ok = cfg.Certificates[candidate]; ok { - cert = cfg.certCache.cache[certKey] - matched = true - return - } - } - - // check the certCache directly to see if the SNI name is - // 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...) - if directCert, ok := cfg.certCache.cache[name]; ok { - cert = directCert - matched = true - return - } - - // if nothing matches, use a "default" certificate - // (See issues 2035 and 1303; any change to this behavior - // must account for hosts defined like ":443" or - // "0.0.0.0:443" where the hostname is empty or a catch-all - // IP or something.) - if certKey, ok := cfg.Certificates[""]; ok { - cert = cfg.certCache.cache[certKey] - defaulted = true - return - } - - return -} - -// getCertDuringHandshake will get a certificate for name. It first tries -// the in-memory cache. If no certificate for name is in the cache, the -// config most closely corresponding to name will be loaded. If that config -// allows it (OnDemand==true) and if loadIfNecessary == true, it goes to disk -// to load it into the cache and serve it. If it's not on disk and if -// obtainIfNecessary == true, the certificate will be obtained from the CA, -// cached, and served. If obtainIfNecessary is true, then loadIfNecessary -// must also be set to true. An error will be returned if and only if no -// certificate is available. -// -// This function is safe for concurrent use. -func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) { - // First check our in-memory cache to see if we've already loaded it - cert, matched, defaulted := cfg.getCertificate(name) - if matched { - return cert, nil - } - - // If OnDemand is enabled, then we might be able to load or - // obtain a needed certificate - if cfg.OnDemand && loadIfNecessary { - // Then check to see if we have one on disk - loadedCert, err := cfg.CacheManagedCertificate(name) - if err == nil { - loadedCert, err = cfg.handshakeMaintenance(name, loadedCert) - if err != nil { - log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err) - } - return loadedCert, nil - } - if obtainIfNecessary { - // By this point, we need to ask the CA for a certificate - - name = strings.ToLower(name) - - // Make sure the certificate should be obtained based on config - err := cfg.checkIfCertShouldBeObtained(name) - if err != nil { - return Certificate{}, err - } - - // Name has to qualify for a certificate - if !HostQualifies(name) { - return cert, errors.New("hostname '" + name + "' does not qualify for certificate") - } - - // Obtain certificate from the CA - return cfg.obtainOnDemandCertificate(name) - } - } - - // Fall back to the default certificate if there is one - if defaulted { - return cert, nil - } - - return Certificate{}, fmt.Errorf("no certificate available for %s", name) -} - -// checkIfCertShouldBeObtained checks to see if an on-demand tls certificate -// should be obtained for a given domain based upon the config settings. If -// a non-nil error is returned, do not issue a new certificate for name. -func (cfg *Config) checkIfCertShouldBeObtained(name string) error { - // If the "ask" URL is defined in the config, use to determine if a - // cert should obtained - if cfg.OnDemandState.AskURL != nil { - return cfg.checkURLForObtainingNewCerts(name) - } - - // Otherwise use the limit defined by the "max_certs" setting - return cfg.checkLimitsForObtainingNewCerts(name) -} - -func (cfg *Config) checkURLForObtainingNewCerts(name string) error { - client := http.Client{ - Timeout: 10 * time.Second, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return errors.New("following http redirects is not allowed") - }, - } - - // Copy the URL from the config in order to modify it for this request - askURL := new(url.URL) - *askURL = *cfg.OnDemandState.AskURL - - query := askURL.Query() - query.Set("domain", name) - askURL.RawQuery = query.Encode() - - resp, err := client.Get(askURL.String()) - if err != nil { - return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", cfg.OnDemandState.AskURL, name, err) - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("certificate for hostname '%s' not allowed, non-2xx status code %d returned from %v", name, resp.StatusCode, cfg.OnDemandState.AskURL) - } - - return nil -} - -// checkLimitsForObtainingNewCerts checks to see if name can be issued right -// now according the maximum count defined in the configuration. If a non-nil -// error is returned, do not issue a new certificate for name. -func (cfg *Config) checkLimitsForObtainingNewCerts(name string) error { - // User can set hard limit for number of certs for the process to issue - if cfg.OnDemandState.MaxObtain > 0 && - atomic.LoadInt32(&cfg.OnDemandState.ObtainedCount) >= cfg.OnDemandState.MaxObtain { - return fmt.Errorf("%s: maximum certificates issued (%d)", name, cfg.OnDemandState.MaxObtain) - } - - // Make sure name hasn't failed a challenge recently - failedIssuanceMu.RLock() - when, ok := failedIssuance[name] - failedIssuanceMu.RUnlock() - if ok { - return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String()) - } - - // Make sure, if we've issued a few certificates already, that we haven't - // issued any recently - lastIssueTimeMu.Lock() - since := time.Since(lastIssueTime) - lastIssueTimeMu.Unlock() - if atomic.LoadInt32(&cfg.OnDemandState.ObtainedCount) >= 10 && since < 10*time.Minute { - return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since) - } - - // Good to go 👍 - return nil -} - -// obtainOnDemandCertificate obtains a certificate for name for the given -// name. If another goroutine has already started obtaining a cert for -// name, it will wait and use what the other goroutine obtained. -// -// This function is safe for use by multiple concurrent goroutines. -func (cfg *Config) obtainOnDemandCertificate(name string) (Certificate, error) { - // We must protect this process from happening concurrently, so synchronize. - obtainCertWaitChansMu.Lock() - wait, ok := obtainCertWaitChans[name] - if ok { - // lucky us -- another goroutine is already obtaining the certificate. - // wait for it to finish obtaining the cert and then we'll use it. - obtainCertWaitChansMu.Unlock() - <-wait - return cfg.getCertDuringHandshake(name, true, false) - } - - // looks like it's up to us to do all the work and obtain the cert. - // make a chan others can wait on if needed - wait = make(chan struct{}) - obtainCertWaitChans[name] = wait - obtainCertWaitChansMu.Unlock() - - // obtain the certificate - log.Printf("[INFO] Obtaining new certificate for %s", name) - err := cfg.ObtainCert(name, false) - - // immediately unblock anyone waiting for it; doing this in - // a defer would risk deadlock because of the recursive call - // to getCertDuringHandshake below when we return! - obtainCertWaitChansMu.Lock() - close(wait) - delete(obtainCertWaitChans, name) - obtainCertWaitChansMu.Unlock() - - if err != nil { - // Failed to solve challenge, so don't allow another on-demand - // issue for this name to be attempted for a little while. - failedIssuanceMu.Lock() - failedIssuance[name] = time.Now() - go func(name string) { - time.Sleep(5 * time.Minute) - failedIssuanceMu.Lock() - delete(failedIssuance, name) - failedIssuanceMu.Unlock() - }(name) - failedIssuanceMu.Unlock() - return Certificate{}, err - } - - // Success - update counters and stuff - atomic.AddInt32(&cfg.OnDemandState.ObtainedCount, 1) - lastIssueTimeMu.Lock() - lastIssueTime = time.Now() - lastIssueTimeMu.Unlock() - - // certificate is already on disk; now just start over to load it and serve it - return cfg.getCertDuringHandshake(name, true, false) -} - -// handshakeMaintenance performs a check on cert for expiration and OCSP -// validity. -// -// This function is safe for use by multiple concurrent goroutines. -func (cfg *Config) handshakeMaintenance(name string, cert Certificate) (Certificate, error) { - // Check cert expiration - timeLeft := cert.NotAfter.Sub(time.Now().UTC()) - if timeLeft < RenewDurationBefore { - log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft) - return cfg.renewDynamicCertificate(name, cert) - } - - // Check OCSP staple validity - if cert.OCSP != nil { - refreshTime := cert.OCSP.ThisUpdate.Add(cert.OCSP.NextUpdate.Sub(cert.OCSP.ThisUpdate) / 2) - if time.Now().After(refreshTime) { - err := stapleOCSP(&cert, nil) - if err != nil { - // 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. - log.Printf("[ERROR] Getting OCSP for %s: %v", name, err) - } - cfg.certCache.Lock() - cfg.certCache.cache[cert.Hash] = cert - cfg.certCache.Unlock() - } - } - - return cert, nil -} - -// renewDynamicCertificate renews the certificate for name using cfg. It returns the -// certificate to use and an error, if any. name should already be lower-cased before -// calling this function. name is the name obtained directly from the handshake's -// ClientHello. -// -// This function is safe for use by multiple concurrent goroutines. -func (cfg *Config) renewDynamicCertificate(name string, currentCert Certificate) (Certificate, error) { - obtainCertWaitChansMu.Lock() - wait, ok := obtainCertWaitChans[name] - if ok { - // lucky us -- another goroutine is already renewing the certificate. - // wait for it to finish, then we'll use the new one. - obtainCertWaitChansMu.Unlock() - <-wait - return cfg.getCertDuringHandshake(name, true, false) - } - - // looks like it's up to us to do all the work and renew the cert - wait = make(chan struct{}) - obtainCertWaitChans[name] = wait - obtainCertWaitChansMu.Unlock() - - // renew and reload the certificate - log.Printf("[INFO] Renewing certificate for %s", name) - err := cfg.RenewCert(name, false) - if err == nil { - // even though the recursive nature of the dynamic cert loading - // would just call this function anyway, we do it here to - // make the replacement as atomic as possible. - newCert, err := currentCert.configs[0].CacheManagedCertificate(name) - if err != nil { - log.Printf("[ERROR] loading renewed certificate for %s: %v", name, err) - } else { - // replace the old certificate with the new one - err = cfg.certCache.replaceCertificate(currentCert, newCert) - if err != nil { - log.Printf("[ERROR] Replacing certificate for %s: %v", name, err) - } - } - } - - // immediately unblock anyone waiting for it; doing this in - // a defer would risk deadlock because of the recursive call - // to getCertDuringHandshake below when we return! - obtainCertWaitChansMu.Lock() - close(wait) - delete(obtainCertWaitChans, name) - obtainCertWaitChansMu.Unlock() - - if err != nil { - return Certificate{}, err - } - - 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 @@ -570,21 +115,6 @@ func (info ClientHelloInfo) Key() string { compressionMethods, info.Curves, info.Points))) } -// obtainCertWaitChans is used to coordinate obtaining certs for each hostname. -var obtainCertWaitChans = make(map[string]chan struct{}) -var obtainCertWaitChansMu sync.Mutex - -// failedIssuance is a set of names that we recently failed to get a -// certificate for from the ACME CA. They are removed after some time. -// When a name is in this map, do not issue a certificate for it on-demand. -var failedIssuance = make(map[string]time.Time) -var failedIssuanceMu sync.RWMutex - -// lastIssueTime records when we last obtained a certificate successfully. -// If this value is recent, do not make any on-demand certificate requests. -var lastIssueTime time.Time -var lastIssueTimeMu sync.Mutex - // ClientHelloTelemetry determines whether to report // TLS ClientHellos to telemetry. Disable if doing // it from a different package. diff --git a/caddytls/handshake_test.go b/caddytls/handshake_test.go deleted file mode 100644 index b32d58cc1..000000000 --- a/caddytls/handshake_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "crypto/tls" - "crypto/x509" - "testing" -) - -func TestGetCertificate(t *testing.T) { - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} - - hello := &tls.ClientHelloInfo{ServerName: "example.com"} - helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"} - helloNoSNI := &tls.ClientHelloInfo{} - helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"} // TODO (see below) - - // When cache is empty - if cert, err := cfg.GetCertificate(hello); err == nil { - t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert) - } - if cert, err := cfg.GetCertificate(helloNoSNI); err == nil { - t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert) - } - - // When cache has one certificate in it - firstCert := Certificate{Names: []string{"example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}} - cfg.cacheCertificate(firstCert) - if cert, err := cfg.GetCertificate(hello); err != nil { - t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err) - } else if cert.Leaf.DNSNames[0] != "example.com" { - t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert) - } - if _, err := cfg.GetCertificate(helloNoSNI); err != nil { - t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err) - } - - // When retrieving wildcard certificate - wildcardCert := Certificate{ - Names: []string{"*.example.com"}, - Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}, - Hash: "(don't overwrite the first one)", - } - cfg.cacheCertificate(wildcardCert) - if cert, err := cfg.GetCertificate(helloSub); err != nil { - t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err) - } else if cert.Leaf.DNSNames[0] != "*.example.com" { - t.Errorf("Got wrong certificate, expected wildcard: %v", cert) - } - - // When cache is NOT empty but there's no SNI - if cert, err := cfg.GetCertificate(helloNoSNI); err != nil { - t.Errorf("Expected random certificate with no error when no SNI, got err: %v", err) - } else if cert == nil || len(cert.Leaf.DNSNames) == 0 { - t.Errorf("Expected random cert with no matches, got: %v", cert) - } - - // When no certificate matches, raise an alert - if _, err := cfg.GetCertificate(helloNoMatch); err == nil { - t.Errorf("Expected an error when no certificate matched the SNI, got: %v", err) - } -} diff --git a/caddytls/httphandler.go b/caddytls/httphandler.go deleted file mode 100644 index fc63f3f1d..000000000 --- a/caddytls/httphandler.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "crypto/tls" - "encoding/json" - "fmt" - "log" - "net/http" - "net/http/httputil" - "net/url" - "os" - "strings" - - "github.com/xenolf/lego/acme" -) - -const challengeBasePath = "/.well-known/acme-challenge" - -// HTTPChallengeHandler proxies challenge requests to ACME client if the -// request path starts with challengeBasePath, if the HTTP challenge is not -// disabled, and if we are known to be obtaining a certificate for the name. -// It returns true if it handled the request and no more needs to be done; -// it returns false if this call was a no-op and the request still needs handling. -func HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, listenHost string) bool { - if !strings.HasPrefix(r.URL.Path, challengeBasePath) { - return false - } - if DisableHTTPChallenge { - return false - } - - // see if another instance started the HTTP challenge for this name - if tryDistributedChallengeSolver(w, r) { - return true - } - - // otherwise, if we aren't getting the name, then ignore this challenge - if !namesObtaining.Has(r.Host) { - return false - } - - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - - if listenHost == "" { - listenHost = "localhost" - } - - // always proxy to the DefaultHTTPAlternatePort because obviously the - // ACME challenge request already got into one of our HTTP handlers, so - // it means we must have started a HTTP listener on the alternate - // port instead; which is only accessible via listenHost - upstream, err := url.Parse(fmt.Sprintf("%s://%s:%s", scheme, listenHost, DefaultHTTPAlternatePort)) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Printf("[ERROR] ACME proxy handler: %v", err) - return true - } - - proxy := httputil.NewSingleHostReverseProxy(upstream) - proxy.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - proxy.ServeHTTP(w, r) - - return true -} - -// tryDistributedChallengeSolver checks to see if this challenge -// request was initiated by another instance that shares file -// 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 := distributedSolver{}.challengeTokensPath(r.Host) - f, err := os.Open(filePath) - if err != nil { - if !os.IsNotExist(err) { - log.Printf("[ERROR][%s] Opening distributed challenge token file: %v", r.Host, err) - } - return false - } - defer f.Close() - - var chalInfo challengeInfo - err = json.NewDecoder(f).Decode(&chalInfo) - if err != nil { - log.Printf("[ERROR][%s] Decoding challenge token file %s (corrupted?): %v", r.Host, filePath, err) - return false - } - - // this part borrowed from xenolf/lego's built-in HTTP-01 challenge solver (March 2018) - challengeReqPath := acme.HTTP01ChallengePath(chalInfo.Token) - if r.URL.Path == challengeReqPath && - strings.HasPrefix(r.Host, chalInfo.Domain) && - r.Method == "GET" { - w.Header().Add("Content-Type", "text/plain") - w.Write([]byte(chalInfo.KeyAuth)) - r.Close = true - log.Printf("[INFO][%s] Served key authentication (distributed)", chalInfo.Domain) - return true - } - - return false -} diff --git a/caddytls/httphandler_test.go b/caddytls/httphandler_test.go deleted file mode 100644 index cae65ac8c..000000000 --- a/caddytls/httphandler_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "net" - "net/http" - "net/http/httptest" - "testing" -) - -func TestHTTPChallengeHandlerNoOp(t *testing.T) { - namesObtaining.Add([]string{"localhost"}) - - // try base paths and host names that aren't - // handled by this handler - for _, url := range []string{ - "http://localhost/", - "http://localhost/foo.html", - "http://localhost/.git", - "http://localhost/.well-known/", - "http://localhost/.well-known/acme-challenging", - "http://other/.well-known/acme-challenge/foo", - } { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - t.Fatalf("Could not craft request, got error: %v", err) - } - rw := httptest.NewRecorder() - if HTTPChallengeHandler(rw, req, "") { - t.Errorf("Got true with this URL, but shouldn't have: %s", url) - } - } -} - -func TestHTTPChallengeHandlerSuccess(t *testing.T) { - expectedPath := challengeBasePath + "/asdf" - - // Set up fake acme handler backend to make sure proxying succeeds - var proxySuccess bool - ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - proxySuccess = true - if r.URL.Path != expectedPath { - t.Errorf("Expected path '%s' but got '%s' instead", expectedPath, r.URL.Path) - } - })) - - // Custom listener that uses the port we expect - ln, err := net.Listen("tcp", "127.0.0.1:"+DefaultHTTPAlternatePort) - if err != nil { - t.Fatalf("Unable to start test server listener: %v", err) - } - ts.Listener = ln - - // Tell this package that we are handling a challenge for 127.0.0.1 - namesObtaining.Add([]string{"127.0.0.1"}) - - // Start our engines and run the test - ts.Start() - defer ts.Close() - req, err := http.NewRequest("GET", "http://127.0.0.1:"+DefaultHTTPAlternatePort+expectedPath, nil) - if err != nil { - t.Fatalf("Could not craft request, got error: %v", err) - } - rw := httptest.NewRecorder() - - HTTPChallengeHandler(rw, req, "") - - if !proxySuccess { - t.Fatal("Expected request to be proxied, but it wasn't") - } -} diff --git a/caddytls/maintain.go b/caddytls/maintain.go deleted file mode 100644 index c3f21f921..000000000 --- a/caddytls/maintain.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "io/ioutil" - "log" - "os" - "path/filepath" - "time" - - "github.com/mholt/caddy" - - "golang.org/x/crypto/ocsp" -) - -func init() { - // maintain assets while this package is imported, which is - // always. we don't ever stop it, since we need it running. - go maintainAssets(make(chan struct{})) -} - -const ( - // RenewInterval is how often to check certificates for renewal. - RenewInterval = 12 * time.Hour - - // RenewDurationBefore is how long before expiration to renew certificates. - RenewDurationBefore = (24 * time.Hour) * 30 - - // RenewDurationBeforeAtStartup is how long before expiration to require - // a renewed certificate when the process is first starting up (see #1680). - // A wider window between RenewDurationBefore and this value will allow - // Caddy to start under duress but hopefully this duration will give it - // enough time for the blockage to be relieved. - RenewDurationBeforeAtStartup = (24 * time.Hour) * 7 - - // OCSPInterval is how often to check if OCSP stapling needs updating. - OCSPInterval = 1 * time.Hour -) - -// maintainAssets is a permanently-blocking function -// that loops indefinitely and, on a regular schedule, checks -// certificates for expiration and initiates a renewal of certs -// that are expiring soon. It also updates OCSP stapling and -// performs other maintenance of assets. It should only be -// called once per process. -// -// You must pass in the channel which you'll close when -// maintenance should stop, to allow this goroutine to clean up -// after itself and unblock. (Not that you HAVE to stop it...) -func maintainAssets(stopChan chan struct{}) { - renewalTicker := time.NewTicker(RenewInterval) - ocspTicker := time.NewTicker(OCSPInterval) - - for { - select { - case <-renewalTicker.C: - log.Println("[INFO] Scanning for expiring certificates") - RenewManagedCertificates(false) - log.Println("[INFO] Done checking certificates") - case <-ocspTicker.C: - log.Println("[INFO] Scanning for stale OCSP staples") - UpdateOCSPStaples() - DeleteOldStapleFiles() - log.Println("[INFO] Done checking OCSP staples") - case <-stopChan: - renewalTicker.Stop() - ocspTicker.Stop() - log.Println("[INFO] Stopped background maintenance routine") - return - } - } -} - -// RenewManagedCertificates renews managed certificates, -// including ones loaded on-demand. -func RenewManagedCertificates(allowPrompts bool) (err error) { - for _, inst := range caddy.Instances() { - inst.StorageMu.RLock() - certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certificateCache) - inst.StorageMu.RUnlock() - if !ok || certCache == nil { - continue - } - - // we use the queues for a very important reason: to do any and all - // operations that could require an exclusive write lock outside - // of the read lock! otherwise we get a deadlock, yikes. in other - // words, our first iteration through the certificate cache does NOT - // perform any operations--only queues them--so that more fine-grained - // write locks may be obtained during the actual operations. - var renewQueue, reloadQueue, deleteQueue []Certificate - - certCache.RLock() - for certKey, cert := range certCache.cache { - if len(cert.configs) == 0 { - // this is bad if this happens, probably a programmer error (oops) - log.Printf("[ERROR] No associated TLS config for certificate with names %v; unable to manage", cert.Names) - continue - } - if !cert.configs[0].Managed || cert.configs[0].SelfSigned { - continue - } - - // the list of names on this cert should never be empty... programmer error? - if cert.Names == nil || len(cert.Names) == 0 { - log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v - removing from cache", certKey, cert.Names) - deleteQueue = append(deleteQueue, cert) - continue - } - - // if time is up or expires soon, we need to try to renew it - timeLeft := cert.NotAfter.Sub(time.Now().UTC()) - if timeLeft < RenewDurationBefore { - // see if the certificate in storage has already been renewed, possibly by another - // instance of Caddy that didn't coordinate with this one; if so, just load it (this - // might happen if another instance already renewed it - kinda sloppy but checking disk - // first is a simple way to possibly drastically reduce rate limit problems) - storedCertExpiring, err := managedCertInStorageExpiresSoon(cert) - if err != nil { - // hmm, weird, but not a big deal, maybe it was deleted or something - log.Printf("[NOTICE] Error while checking if certificate for %v in storage is also expiring soon: %v", - cert.Names, err) - } else if !storedCertExpiring { - // if the certificate is NOT expiring soon and there was no error, then we - // are good to just reload the certificate from storage instead of repeating - // a likely-unnecessary renewal procedure - reloadQueue = append(reloadQueue, cert) - continue - } - - // the certificate in storage has not been renewed yet, so we will do it - // NOTE 1: This is not correct 100% of the time, if multiple Caddy instances - // 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-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) - } - } - certCache.RUnlock() - - // Reload certificates that merely need to be updated in memory - for _, oldCert := range reloadQueue { - timeLeft := oldCert.NotAfter.Sub(time.Now().UTC()) - log.Printf("[INFO] Certificate for %v expires in %v, but is already renewed in storage; reloading stored certificate", - oldCert.Names, timeLeft) - - err = certCache.reloadManagedCertificate(oldCert) - if err != nil { - if allowPrompts { - return err // operator is present, so report error immediately - } - log.Printf("[ERROR] Loading renewed certificate: %v", err) - } - } - - // Renewal queue - for _, oldCert := range renewQueue { - timeLeft := oldCert.NotAfter.Sub(time.Now().UTC()) - log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", oldCert.Names, timeLeft) - - // Get the name which we should use to renew this certificate; - // we only support managing certificates with one name per cert, - // so this should be easy. We can't rely on cert.Config.Hostname - // because it may be a wildcard value from the Caddyfile (e.g. - // *.something.com) which, as of Jan. 2017, is not supported by ACME. - // TODO: ^ ^ ^ (wildcards) - renewName := oldCert.Names[0] - - // perform renewal - err := oldCert.configs[0].RenewCert(renewName, allowPrompts) - if err != nil { - if allowPrompts { - // Certificate renewal failed and the operator is present. See a discussion - // about this in issue 642. For a while, we only stopped if the certificate - // was expired, but in reality, there is no difference between reporting - // it now versus later, except that there's somebody present to deal with - // it right now. Follow-up: See issue 1680. Only fail in this case if the - // certificate is dangerously close to expiration. - timeLeft := oldCert.NotAfter.Sub(time.Now().UTC()) - if timeLeft < RenewDurationBeforeAtStartup { - return err - } - } - log.Printf("[ERROR] %v", err) - if oldCert.configs[0].OnDemand { - // loaded dynamically, remove dynamically - deleteQueue = append(deleteQueue, oldCert) - } - continue - } - - // successful renewal, so update in-memory cache by loading - // renewed certificate so it will be used with handshakes - err = certCache.reloadManagedCertificate(oldCert) - if err != nil { - if allowPrompts { - return err // operator is present, so report error immediately - } - log.Printf("[ERROR] %v", err) - } - } - - // Deletion queue - for _, cert := range deleteQueue { - certCache.Lock() - // remove any pointers to this certificate from Configs - for _, cfg := range cert.configs { - for name, certKey := range cfg.Certificates { - if certKey == cert.Hash { - delete(cfg.Certificates, name) - } - } - } - // then delete the certificate from the cache - delete(certCache.cache, cert.Hash) - certCache.Unlock() - } - } - - return nil -} - -// UpdateOCSPStaples updates the OCSP stapling in all -// eligible, cached certificates. -// -// OCSP maintenance strives to abide the relevant points on -// Ryan Sleevi's recommendations for good OCSP support: -// https://gist.github.com/sleevi/5efe9ef98961ecfb4da8 -func UpdateOCSPStaples() { - for _, inst := range caddy.Instances() { - inst.StorageMu.RLock() - certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certificateCache) - inst.StorageMu.RUnlock() - if !ok || certCache == nil { - continue - } - - // Create a temporary place to store updates - // until we release the potentially long-lived - // read lock and use a short-lived write lock - // on the certificate cache. - type ocspUpdate struct { - rawBytes []byte - parsed *ocsp.Response - } - updated := make(map[string]ocspUpdate) - - certCache.RLock() - for certHash, cert := range certCache.cache { - // no point in updating OCSP for expired certificates - if time.Now().After(cert.NotAfter) { - continue - } - - var lastNextUpdate time.Time - if cert.OCSP != nil { - lastNextUpdate = cert.OCSP.NextUpdate - if freshOCSP(cert.OCSP) { - continue // no need to update staple if ours is still fresh - } - } - - err := stapleOCSP(&cert, nil) - if err != nil { - if cert.OCSP != nil { - // if there was no staple before, that's fine; otherwise we should log the error - log.Printf("[ERROR] Checking OCSP: %v", err) - } - continue - } - - // By this point, we've obtained the latest OCSP response. - // If there was no staple before, or if the response is updated, make - // sure we apply the update to all names on the certificate. - if cert.OCSP != nil && (lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate) { - log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s", - cert.Names, lastNextUpdate, cert.OCSP.NextUpdate) - updated[certHash] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP} - } - } - certCache.RUnlock() - - // These write locks should be brief since we have all the info we need now. - for certKey, update := range updated { - certCache.Lock() - cert := certCache.cache[certKey] - cert.OCSP = update.parsed - cert.Certificate.OCSPStaple = update.rawBytes - certCache.cache[certKey] = cert - certCache.Unlock() - } - } -} - -// DeleteOldStapleFiles deletes cached OCSP staples that have expired. -// TODO: Should we do this for certificates too? -func DeleteOldStapleFiles() { - // TODO: Upgrade caddytls.Storage to support OCSP operations too - files, err := ioutil.ReadDir(ocspFolder) - if err != nil { - // maybe just hasn't been created yet; no big deal - return - } - for _, file := range files { - if file.IsDir() { - // weird, what's a folder doing inside the OCSP cache? - continue - } - stapleFile := filepath.Join(ocspFolder, file.Name()) - ocspBytes, err := ioutil.ReadFile(stapleFile) - if err != nil { - continue - } - resp, err := ocsp.ParseResponse(ocspBytes, nil) - if err != nil { - // contents are invalid; delete it - err = os.Remove(stapleFile) - if err != nil { - log.Printf("[ERROR] Purging corrupt staple file %s: %v", stapleFile, err) - } - continue - } - if time.Now().After(resp.NextUpdate) { - // response has expired; delete it - err = os.Remove(stapleFile) - if err != nil { - log.Printf("[ERROR] Purging expired staple file %s: %v", stapleFile, err) - } - } - } -} - -// freshOCSP returns true if resp is still fresh, -// meaning that it is not expedient to get an -// updated response from the OCSP server. -func freshOCSP(resp *ocsp.Response) bool { - nextUpdate := resp.NextUpdate - // If there is an OCSP responder certificate, and it expires before the - // OCSP response, use its expiration date as the end of the OCSP - // response's validity period. - if resp.Certificate != nil && resp.Certificate.NotAfter.Before(nextUpdate) { - nextUpdate = resp.Certificate.NotAfter - } - // start checking OCSP staple about halfway through validity period for good measure - refreshTime := resp.ThisUpdate.Add(nextUpdate.Sub(resp.ThisUpdate) / 2) - return time.Now().Before(refreshTime) -} - -var ocspFolder = filepath.Join(caddy.AssetsPath(), "ocsp") diff --git a/caddytls/selfsigned.go b/caddytls/selfsigned.go new file mode 100644 index 000000000..367cd7362 --- /dev/null +++ b/caddytls/selfsigned.go @@ -0,0 +1,106 @@ +package caddytls + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "net" + "strings" + "time" + + "github.com/xenolf/lego/certcrypto" +) + +// newSelfSignedCertificate returns a new self-signed certificate. +func newSelfSignedCertificate(ssconfig selfSignedConfig) (tls.Certificate, error) { + // start by generating private key + var privKey interface{} + var err error + switch ssconfig.KeyType { + case "", certcrypto.EC256: + privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case certcrypto.EC384: + privKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case certcrypto.RSA2048: + privKey, err = rsa.GenerateKey(rand.Reader, 2048) + case certcrypto.RSA4096: + privKey, err = rsa.GenerateKey(rand.Reader, 4096) + case certcrypto.RSA8192: + privKey, err = rsa.GenerateKey(rand.Reader, 8192) + default: + return tls.Certificate{}, fmt.Errorf("cannot generate private key; unknown key type %v", ssconfig.KeyType) + } + if err != nil { + return tls.Certificate{}, fmt.Errorf("failed to generate private key: %v", err) + } + + // create certificate structure with proper values + notBefore := time.Now() + notAfter := ssconfig.Expire + if notAfter.IsZero() || notAfter.Before(notBefore) { + notAfter = notBefore.Add(24 * time.Hour * 7) + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return tls.Certificate{}, fmt.Errorf("failed to generate serial number: %v", err) + } + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{Organization: []string{"Caddy Self-Signed"}}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + if len(ssconfig.SAN) == 0 { + ssconfig.SAN = []string{""} + } + var names []string + for _, san := range ssconfig.SAN { + if ip := net.ParseIP(san); ip != nil { + names = append(names, strings.ToLower(ip.String())) + cert.IPAddresses = append(cert.IPAddresses, ip) + } else { + names = append(names, strings.ToLower(san)) + cert.DNSNames = append(cert.DNSNames, strings.ToLower(san)) + } + } + + // generate the associated public key + publicKey := func(privKey interface{}) interface{} { + switch k := privKey.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return fmt.Errorf("unknown key type") + } + } + derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey(privKey), privKey) + if err != nil { + return tls.Certificate{}, fmt.Errorf("could not create certificate: %v", err) + } + + chain := [][]byte{derBytes} + + return tls.Certificate{ + Certificate: chain, + PrivateKey: privKey, + Leaf: cert, + }, nil +} + +// selfSignedConfig configures a self-signed certificate. +type selfSignedConfig struct { + SAN []string + KeyType certcrypto.KeyType + Expire time.Time +} diff --git a/caddytls/setup.go b/caddytls/setup.go index 7baf5b7f1..9ba09b9b3 100644 --- a/caddytls/setup.go +++ b/caddytls/setup.go @@ -29,17 +29,20 @@ import ( "github.com/mholt/caddy" "github.com/mholt/caddy/telemetry" + "github.com/mholt/certmagic" ) func init() { caddy.RegisterPlugin("tls", caddy.Plugin{Action: setupTLS}) + + // ensure TLS assets are stored and accessed from the CADDYPATH + certmagic.DefaultStorage = certmagic.FileStorage{Path: caddy.AssetsPath()} } // setupTLS sets up the TLS configuration and installs certificates that // are specified by the user in the config file. All the automatic HTTPS // stuff comes later outside of this function. func setupTLS(c *caddy.Controller) error { - // obtain the configGetter, which loads the config we're, uh, configuring configGetter, ok := configGetters[c.ServerType()] if !ok { return fmt.Errorf("no caddytls.ConfigGetter for %s server type; must call RegisterConfigGetter", c.ServerType()) @@ -49,18 +52,68 @@ func setupTLS(c *caddy.Controller) error { return fmt.Errorf("no caddytls.Config to set up for %s", c.Key) } - // the certificate cache is tied to the current caddy.Instance; get a pointer to it - certCache, ok := c.Get(CertCacheInstStorageKey).(*certificateCache) + 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 = &certificateCache{cache: make(map[string]Certificate)} + certCache = certmagic.NewCache(certmagic.FileStorage{Path: caddy.AssetsPath()}) + c.OnShutdown(func() error { + certCache.Stop() + return nil + }) c.Set(CertCacheInstStorageKey, certCache) } - config.certCache = certCache + config.Manager = certmagic.NewWithCache(certCache, certmagic.Config{}) - config.Enabled = true + // we use certmagic events to collect metrics for telemetry + config.Manager.OnEvent = func(event string, data interface{}) { + switch event { + case "tls_handshake_started": + clientHello := data.(*tls.ClientHelloInfo) + if ClientHelloTelemetry && len(clientHello.SupportedVersions) > 0 { + // If no other plugin (such as the HTTP server type) is implementing ClientHello telemetry, we do it. + // NOTE: The values in the Go standard lib's ClientHelloInfo aren't guaranteed to be in order. + info := ClientHelloInfo{ + Version: clientHello.SupportedVersions[0], // report the highest + CipherSuites: clientHello.CipherSuites, + ExtensionsUnknown: true, // no extension info... :( + CompressionMethodsUnknown: true, // no compression methods... :( + Curves: clientHello.SupportedCurves, + Points: clientHello.SupportedPoints, + // We also have, but do not yet use: SignatureSchemes, ServerName, and SupportedProtos (ALPN) + // because the standard lib parses some extensions, but our MITM detector generally doesn't. + } + go telemetry.SetNested("tls_client_hello", info.Key(), info) + } + + case "tls_handshake_completed": + // TODO: This is a "best guess" for now - at this point, we only gave a + // certificate to the client; we need something listener-level to be sure + go telemetry.Increment("tls_handshake_count") + + case "acme_cert_obtained": + go telemetry.Increment("tls_acme_certs_obtained") + + case "acme_cert_renewed": + name := data.(string) + caddy.EmitEvent(caddy.CertRenewEvent, name) + go telemetry.Increment("tls_acme_certs_renewed") + + case "acme_cert_revoked": + telemetry.Increment("acme_certs_revoked") + + case "cached_managed_cert": + telemetry.Increment("tls_managed_cert_count") + + case "cached_unmanaged_cert": + telemetry.Increment("tls_unmanaged_cert_count") + } + } for c.Next() { var certificateFile, keyFile, loadDir, maxCerts, askURL string + var onDemand bool args := c.RemainingArgs() switch len(args) { @@ -96,14 +149,14 @@ func setupTLS(c *caddy.Controller) error { if len(arg) != 1 { return c.ArgErr() } - config.CAUrl = arg[0] + config.Manager.CA = arg[0] case "key_type": arg := c.RemainingArgs() value, ok := supportedKeyTypes[strings.ToUpper(arg[0])] if !ok { return c.Errf("Wrong key type name or key type not supported: '%s'", c.Val()) } - config.KeyType = value + config.Manager.KeyType = value case "protocols": args := c.RemainingArgs() if len(args) == 1 { @@ -111,7 +164,6 @@ func setupTLS(c *caddy.Controller) error { if !ok { return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) } - config.ProtocolMinVersion, config.ProtocolMaxVersion = value, value } else { value, ok := SupportedProtocols[strings.ToLower(args[0])] @@ -174,32 +226,44 @@ func setupTLS(c *caddy.Controller) error { config.Manual = true case "max_certs": c.Args(&maxCerts) - config.OnDemand = true - telemetry.Increment("tls_on_demand_count") + onDemand = true case "ask": c.Args(&askURL) - config.OnDemand = true - telemetry.Increment("tls_on_demand_count") + onDemand = true case "dns": args := c.RemainingArgs() if len(args) != 1 { return c.ArgErr() } + // TODO: we can get rid of DNS provider plugins with this one line + // of code; however, currently (Dec. 2018) this adds about 20 MB + // of bloat to the Caddy binary, doubling its size to ~40 MB...! + // dnsProv, err := dns.NewDNSChallengeProviderByName(args[0]) + // if err != nil { + // return c.Errf("Configuring DNS provider '%s': %v", args[0], err) + // } dnsProvName := args[0] - if _, ok := dnsProviders[dnsProvName]; !ok { - return c.Errf("Unsupported DNS provider '%s'", args[0]) + dnsProvConstructor, ok := dnsProviders[dnsProvName] + if !ok { + return c.Errf("Unknown DNS provider by name '%s'", dnsProvName) } - config.DNSProvider = args[0] - case "storage": - args := c.RemainingArgs() - if len(args) != 1 { - return c.ArgErr() + dnsProv, err := dnsProvConstructor() + if err != nil { + return c.Errf("Setting up DNS provider '%s': %v", dnsProvName, err) } - storageProvName := args[0] - if _, ok := storageProviders[storageProvName]; !ok { - return c.Errf("Unsupported Storage provider '%s'", args[0]) - } - config.StorageProvider = args[0] + config.Manager.DNSProvider = dnsProv + // TODO + // case "storage": + // args := c.RemainingArgs() + // if len(args) != 1 { + // return c.ArgErr() + // } + // storageProvName := args[0] + // storageProvConstr, ok := storageProviders[storageProvName] + // if !ok { + // return c.Errf("Unsupported Storage provider '%s'", args[0]) + // } + // config.Manager.Storage = storageProvConstr case "alpn": args := c.RemainingArgs() if len(args) == 0 { @@ -209,9 +273,9 @@ func setupTLS(c *caddy.Controller) error { config.ALPN = append(config.ALPN, arg) } case "must_staple": - config.MustStaple = true + config.Manager.MustStaple = true case "wildcard": - if !HostQualifies(config.Hostname) { + if !certmagic.HostQualifies(config.Hostname) { return c.Errf("Hostname '%s' does not qualify for managed TLS, so cannot manage wildcard certificate for it", config.Hostname) } if strings.Contains(config.Hostname, "*") { @@ -233,26 +297,26 @@ func setupTLS(c *caddy.Controller) error { return c.ArgErr() } - // set certificate limit if on-demand TLS is enabled - if maxCerts != "" { - maxCertsNum, err := strconv.Atoi(maxCerts) - if err != nil || maxCertsNum < 1 { - return c.Err("max_certs must be a positive integer") + // configure on-demand TLS, if enabled + if onDemand { + config.Manager.OnDemand = new(certmagic.OnDemandConfig) + if maxCerts != "" { + maxCertsNum, err := strconv.Atoi(maxCerts) + if err != nil || maxCertsNum < 1 { + return c.Err("max_certs must be a positive integer") + } + config.Manager.OnDemand.MaxObtain = int32(maxCertsNum) } - config.OnDemandState.MaxObtain = int32(maxCertsNum) - } - - if askURL != "" { - parsedURL, err := url.Parse(askURL) - if err != nil { - return c.Err("ask must be a valid url") + if askURL != "" { + parsedURL, err := url.Parse(askURL) + if err != nil { + return c.Err("ask must be a valid url") + } + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return c.Err("ask URL must use http or https") + } + config.Manager.OnDemand.AskURL = parsedURL } - - if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { - return c.Err("ask URL must use http or https") - } - - config.OnDemandState.AskURL = parsedURL } // don't try to load certificates unless we're supposed to @@ -262,7 +326,7 @@ func setupTLS(c *caddy.Controller) error { // load a single certificate and key, if specified if certificateFile != "" && keyFile != "" { - err := config.cacheUnmanagedCertificatePEMFile(certificateFile, keyFile) + err := config.Manager.CacheUnmanagedCertificatePEMFile(certificateFile, keyFile) if err != nil { return c.Errf("Unable to load certificate and key files for '%s': %v", c.Key, err) } @@ -282,7 +346,14 @@ func setupTLS(c *caddy.Controller) error { // generate self-signed cert if needed if config.SelfSigned { - err := makeSelfSignedCertForConfig(config) + ssCert, err := newSelfSignedCertificate(selfSignedConfig{ + SAN: []string{config.Hostname}, + KeyType: config.Manager.KeyType, + }) + if err != nil { + return fmt.Errorf("self-signed certificate generation: %v", err) + } + err = config.Manager.CacheUnmanagedTLSCertificate(ssCert) if err != nil { return fmt.Errorf("self-signed: %v", err) } @@ -362,7 +433,7 @@ func loadCertsInDir(cfg *Config, c *caddy.Controller, dir string) error { return c.Errf("%s: no private key block found", path) } - err = cfg.cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes) + err = cfg.Manager.CacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes) if err != nil { return c.Errf("%s: failed to load cert and key for '%s': %v", path, c.Key, err) } diff --git a/caddytls/setup_test.go b/caddytls/setup_test.go index a47d71720..81b2cb84d 100644 --- a/caddytls/setup_test.go +++ b/caddytls/setup_test.go @@ -22,7 +22,8 @@ import ( "testing" "github.com/mholt/caddy" - "github.com/xenolf/lego/acme" + "github.com/mholt/certmagic" + "github.com/xenolf/lego/certcrypto" ) func TestMain(m *testing.M) { @@ -46,8 +47,7 @@ func TestMain(m *testing.M) { } func TestSetupParseBasic(t *testing.T) { - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, certCache := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", `tls `+certFile+` `+keyFile+``) @@ -127,8 +127,7 @@ func TestSetupParseWithOptionalParams(t *testing.T) { must_staple alpn http/1.1 }` - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, certCache := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", params) @@ -151,7 +150,7 @@ func TestSetupParseWithOptionalParams(t *testing.T) { t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(cfg.Ciphers)-1) } - if !cfg.MustStaple { + if !cfg.Manager.MustStaple { t.Error("Expected must staple to be true") } @@ -164,8 +163,7 @@ func TestSetupDefaultWithOptionalParams(t *testing.T) { params := `tls { ciphers RSA-3DES-EDE-CBC-SHA }` - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, certCache := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", params) c.Set(CertCacheInstStorageKey, certCache) @@ -184,8 +182,7 @@ func TestSetupParseWithWrongOptionalParams(t *testing.T) { params := `tls ` + certFile + ` ` + keyFile + ` { protocols ssl tls }` - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, certCache := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", params) c.Set(CertCacheInstStorageKey, certCache) @@ -239,8 +236,7 @@ func TestSetupParseWithClientAuth(t *testing.T) { params := `tls ` + certFile + ` ` + keyFile + ` { clients }` - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, _ := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", params) err := setupTLS(c) @@ -273,8 +269,8 @@ func TestSetupParseWithClientAuth(t *testing.T) { clients verify_if_given }`, tls.VerifyClientCertIfGiven, true, noCAs}, } { - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + certCache := certmagic.NewCache(certmagic.DefaultStorage) + cfg := &Config{Manager: certmagic.NewWithCache(certCache, certmagic.Config{})} RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", caseData.params) c.Set(CertCacheInstStorageKey, certCache) @@ -327,8 +323,8 @@ func TestSetupParseWithCAUrl(t *testing.T) { ca 1 2 }`, true, ""}, } { - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + certCache := certmagic.NewCache(certmagic.DefaultStorage) + cfg := &Config{Manager: certmagic.NewWithCache(certCache, certmagic.Config{})} RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", caseData.params) c.Set(CertCacheInstStorageKey, certCache) @@ -343,8 +339,8 @@ func TestSetupParseWithCAUrl(t *testing.T) { t.Errorf("In case %d: Expected no errors, got: %v", caseNumber, err) } - if cfg.CAUrl != caseData.expectedCAUrl { - t.Errorf("Expected '%v' as CAUrl, got %#v", caseData.expectedCAUrl, cfg.CAUrl) + if cfg.Manager.CA != caseData.expectedCAUrl { + t.Errorf("Expected '%v' as CAUrl, got %#v", caseData.expectedCAUrl, cfg.Manager.CA) } } } @@ -353,8 +349,7 @@ func TestSetupParseWithKeyType(t *testing.T) { params := `tls { key_type p384 }` - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, certCache := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", params) c.Set(CertCacheInstStorageKey, certCache) @@ -364,8 +359,8 @@ func TestSetupParseWithKeyType(t *testing.T) { t.Errorf("Expected no errors, got: %v", err) } - if cfg.KeyType != acme.EC384 { - t.Errorf("Expected 'P384' as KeyType, got %#v", cfg.KeyType) + if cfg.Manager.KeyType != certcrypto.EC384 { + t.Errorf("Expected 'P384' as KeyType, got %#v", cfg.Manager.KeyType) } } @@ -373,8 +368,7 @@ func TestSetupParseWithCurves(t *testing.T) { params := `tls { curves x25519 p256 p384 p521 }` - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, certCache := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", params) c.Set(CertCacheInstStorageKey, certCache) @@ -402,8 +396,7 @@ func TestSetupParseWithOneTLSProtocol(t *testing.T) { params := `tls { protocols tls1.2 }` - certCache := &certificateCache{cache: make(map[string]Certificate)} - cfg := &Config{Certificates: make(map[string]string), certCache: certCache} + cfg, certCache := testConfigForTLSSetup() RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) c := caddy.NewTestController("", params) c.Set(CertCacheInstStorageKey, certCache) @@ -422,6 +415,14 @@ func TestSetupParseWithOneTLSProtocol(t *testing.T) { } } +func testConfigForTLSSetup() (*Config, *certmagic.Cache) { + certCache := certmagic.NewCache(nil) + certCache.Stop() + return &Config{ + Manager: certmagic.NewWithCache(certCache, certmagic.Config{}), + }, certCache +} + const ( certFile = "test_cert.pem" keyFile = "test_key.pem" diff --git a/caddytls/storage.go b/caddytls/storage.go deleted file mode 100644 index c0bc5bc86..000000000 --- a/caddytls/storage.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import "net/url" - -// StorageConstructor is a function type that is used in the Config to -// instantiate a new Storage instance. This function can return a nil -// Storage even without an error. -type StorageConstructor func(caURL *url.URL) (Storage, error) - -// SiteData contains persisted items pertaining to an individual site. -type SiteData struct { - // Cert is the public cert byte array. - Cert []byte - // Key is the private key byte array. - Key []byte - // Meta is metadata about the site used by Caddy. - Meta []byte -} - -// UserData contains persisted items pertaining to a user. -type UserData struct { - // Reg is the user registration byte array. - Reg []byte - // Key is the user key byte array. - Key []byte -} - -// Locker provides support for mutual exclusion -type Locker interface { - // TryLock will return immediatedly with or without acquiring the lock. - // If a lock could be obtained, (nil, nil) is returned and you may - // continue normally. If not (meaning another process is already - // working on that name), a Waiter value will be returned upon - // which you can Wait() until it is finished, and then return - // when it unblocks. If waiting, do not unlock! - // - // To prevent deadlocks, all implementations (where this concern - // is relevant) should put a reasonable expiration on the lock in - // case Unlock is unable to be called due to some sort of storage - // system failure or crash. - TryLock(name string) (Waiter, error) - - // Unlock unlocks the mutex for name. Only callers of TryLock who - // successfully obtained the lock (no Waiter value was returned) - // should call this method, and it should be called only after - // the obtain/renew and store are finished, even if there was - // an error (or a timeout). Unlock should also clean up any - // unused resources allocated during TryLock. - Unlock(name string) error -} - -// Storage is an interface abstracting all storage used by Caddy's TLS -// subsystem. Implementations of this interface store both site and -// user data. -type Storage interface { - // SiteExists returns true if this site exists in storage. - // Site data is considered present when StoreSite has been called - // successfully (without DeleteSite having been called, of course). - SiteExists(domain string) (bool, error) - - // LoadSite obtains the site data from storage for the given domain and - // returns it. If data for the domain does not exist, an error value - // of type ErrNotExist is returned. For multi-server storage, care - // should be taken to make this load atomic to prevent race conditions - // that happen with multiple data loads. - LoadSite(domain string) (*SiteData, error) - - // StoreSite persists the given site data for the given domain in - // storage. For multi-server storage, care should be taken to make this - // call atomic to prevent half-written data on failure of an internal - // intermediate storage step. Implementers can trust that at runtime - // this function will only be invoked after LockRegister and before - // UnlockRegister of the same domain. - StoreSite(domain string, data *SiteData) error - - // DeleteSite deletes the site for the given domain from storage. - // Multi-server implementations should attempt to make this atomic. If - // the site does not exist, an error value of type ErrNotExist is returned. - DeleteSite(domain string) error - - // LoadUser obtains user data from storage for the given email and - // returns it. If data for the email does not exist, an error value - // of type ErrNotExist is returned. Multi-server implementations - // should take care to make this operation atomic for all loaded - // data items. - LoadUser(email string) (*UserData, error) - - // StoreUser persists the given user data for the given email in - // storage. Multi-server implementations should take care to make this - // operation atomic for all stored data items. - StoreUser(email string, data *UserData) error - - // MostRecentUserEmail provides the most recently used email parameter - // in StoreUser. The result is an empty string if there are no - // persisted users in storage. - MostRecentUserEmail() string - - // Locker is necessary because synchronizing certificate maintenance - // depends on how storage is implemented. - Locker -} - -// ErrNotExist is returned by Storage implementations when -// a resource is not found. It is similar to os.ErrNotExist -// except this is a type, not a variable. -type ErrNotExist interface { - error -} - -// Waiter is a type that can block until a storage lock is released. -type Waiter interface { - Wait() -} diff --git a/caddytls/storagetest/memorystorage.go b/caddytls/storagetest/memorystorage.go deleted file mode 100644 index 0f41a73a3..000000000 --- a/caddytls/storagetest/memorystorage.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package storagetest - -import ( - "errors" - "net/url" - "sync" - - "github.com/mholt/caddy/caddytls" -) - -// memoryMutex is a mutex used to control access to memoryStoragesByCAURL. -var memoryMutex sync.Mutex - -// memoryStoragesByCAURL is a map keyed by a CA URL string with values of -// instantiated memory stores. Do not access this directly, it is used by -// InMemoryStorageCreator. -var memoryStoragesByCAURL = make(map[string]*InMemoryStorage) - -// InMemoryStorageCreator is a caddytls.Storage.StorageCreator to create -// InMemoryStorage instances for testing. -func InMemoryStorageCreator(caURL *url.URL) (caddytls.Storage, error) { - urlStr := caURL.String() - memoryMutex.Lock() - defer memoryMutex.Unlock() - storage := memoryStoragesByCAURL[urlStr] - if storage == nil { - storage = NewInMemoryStorage() - memoryStoragesByCAURL[urlStr] = storage - } - return storage, nil -} - -// InMemoryStorage is a caddytls.Storage implementation for use in testing. -// It simply stores information in runtime memory. -type InMemoryStorage struct { - // Sites are exposed for testing purposes. - Sites map[string]*caddytls.SiteData - // Users are exposed for testing purposes. - Users map[string]*caddytls.UserData - // LastUserEmail is exposed for testing purposes. - LastUserEmail string -} - -// NewInMemoryStorage constructs an InMemoryStorage instance. For use with -// caddytls, the InMemoryStorageCreator should be used instead. -func NewInMemoryStorage() *InMemoryStorage { - return &InMemoryStorage{ - Sites: make(map[string]*caddytls.SiteData), - Users: make(map[string]*caddytls.UserData), - } -} - -// SiteExists implements caddytls.Storage.SiteExists in memory. -func (s *InMemoryStorage) SiteExists(domain string) (bool, error) { - _, siteExists := s.Sites[domain] - return siteExists, nil -} - -// Clear completely clears all values associated with this storage. -func (s *InMemoryStorage) Clear() { - s.Sites = make(map[string]*caddytls.SiteData) - s.Users = make(map[string]*caddytls.UserData) - s.LastUserEmail = "" -} - -// LoadSite implements caddytls.Storage.LoadSite in memory. -func (s *InMemoryStorage) LoadSite(domain string) (*caddytls.SiteData, error) { - siteData, ok := s.Sites[domain] - if !ok { - return nil, caddytls.ErrNotExist(errors.New("not found")) - } - return siteData, nil -} - -func copyBytes(from []byte) []byte { - copiedBytes := make([]byte, len(from)) - copy(copiedBytes, from) - return copiedBytes -} - -// StoreSite implements caddytls.Storage.StoreSite in memory. -func (s *InMemoryStorage) StoreSite(domain string, data *caddytls.SiteData) error { - copiedData := new(caddytls.SiteData) - copiedData.Cert = copyBytes(data.Cert) - copiedData.Key = copyBytes(data.Key) - copiedData.Meta = copyBytes(data.Meta) - s.Sites[domain] = copiedData - return nil -} - -// DeleteSite implements caddytls.Storage.DeleteSite in memory. -func (s *InMemoryStorage) DeleteSite(domain string) error { - if _, ok := s.Sites[domain]; !ok { - return caddytls.ErrNotExist(errors.New("not found")) - } - delete(s.Sites, domain) - return nil -} - -// TryLock implements Storage.TryLock by returning nil values because it -// is not a multi-server storage implementation. -func (s *InMemoryStorage) TryLock(domain string) (caddytls.Waiter, error) { - return nil, nil -} - -// Unlock implements Storage.Unlock as a no-op because it is -// not a multi-server storage implementation. -func (s *InMemoryStorage) Unlock(domain string) error { - return nil -} - -// LoadUser implements caddytls.Storage.LoadUser in memory. -func (s *InMemoryStorage) LoadUser(email string) (*caddytls.UserData, error) { - userData, ok := s.Users[email] - if !ok { - return nil, caddytls.ErrNotExist(errors.New("not found")) - } - return userData, nil -} - -// StoreUser implements caddytls.Storage.StoreUser in memory. -func (s *InMemoryStorage) StoreUser(email string, data *caddytls.UserData) error { - copiedData := new(caddytls.UserData) - copiedData.Reg = copyBytes(data.Reg) - copiedData.Key = copyBytes(data.Key) - s.Users[email] = copiedData - s.LastUserEmail = email - return nil -} - -// MostRecentUserEmail implements caddytls.Storage.MostRecentUserEmail in memory. -func (s *InMemoryStorage) MostRecentUserEmail() string { - return s.LastUserEmail -} diff --git a/caddytls/storagetest/memorystorage_test.go b/caddytls/storagetest/memorystorage_test.go deleted file mode 100644 index 456f472df..000000000 --- a/caddytls/storagetest/memorystorage_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package storagetest - -import "testing" - -func TestMemoryStorage(t *testing.T) { - storage := NewInMemoryStorage() - storageTest := &StorageTest{ - Storage: storage, - PostTest: storage.Clear, - } - storageTest.Test(t, false) -} diff --git a/caddytls/storagetest/storagetest.go b/caddytls/storagetest/storagetest.go deleted file mode 100644 index fbb63cca1..000000000 --- a/caddytls/storagetest/storagetest.go +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package storagetest provides utilities to assist in testing caddytls.Storage -// implementations. -package storagetest - -import ( - "bytes" - "errors" - "fmt" - "testing" - - "github.com/mholt/caddy/caddytls" -) - -// StorageTest is a test harness that contains tests to execute all exposed -// parts of a Storage implementation. -type StorageTest struct { - // Storage is the implementation to use during tests. This must be - // present. - caddytls.Storage - - // PreTest, if present, is called before every test. Any error returned - // is returned from the test and the test does not continue. - PreTest func() error - - // PostTest, if present, is executed after every test via defer which - // means it executes even on failure of the test (but not on failure of - // PreTest). - PostTest func() - - // AfterUserEmailStore, if present, is invoked during - // TestMostRecentUserEmail after each storage just in case anything - // needs to be mocked. - AfterUserEmailStore func(email string) error -} - -// TestFunc holds information about a test. -type TestFunc struct { - // Name is the friendly name of the test. - Name string - - // Fn is the function that is invoked for the test. - Fn func() error -} - -// runPreTest runs the PreTest function if present. -func (s *StorageTest) runPreTest() error { - if s.PreTest != nil { - return s.PreTest() - } - return nil -} - -// runPostTest runs the PostTest function if present. -func (s *StorageTest) runPostTest() { - if s.PostTest != nil { - s.PostTest() - } -} - -// AllFuncs returns all test functions that are part of this harness. -func (s *StorageTest) AllFuncs() []TestFunc { - return []TestFunc{ - {"TestSiteInfoExists", s.TestSiteExists}, - {"TestSite", s.TestSite}, - {"TestUser", s.TestUser}, - {"TestMostRecentUserEmail", s.TestMostRecentUserEmail}, - } -} - -// Test executes the entire harness using the testing package. Failures are -// reported via T.Fatal. If eagerFail is true, the first failure causes all -// testing to stop immediately. -func (s *StorageTest) Test(t *testing.T, eagerFail bool) { - if errs := s.TestAll(eagerFail); len(errs) > 0 { - ifaces := make([]interface{}, len(errs)) - for i, err := range errs { - ifaces[i] = err - } - t.Fatal(ifaces...) - } -} - -// TestAll executes the entire harness and returns the results as an array of -// errors. If eagerFail is true, the first failure causes all testing to stop -// immediately. -func (s *StorageTest) TestAll(eagerFail bool) (errs []error) { - for _, fn := range s.AllFuncs() { - if err := fn.Fn(); err != nil { - errs = append(errs, fmt.Errorf("%v failed: %v", fn.Name, err)) - if eagerFail { - return - } - } - } - return -} - -var simpleSiteData = &caddytls.SiteData{ - Cert: []byte("foo"), - Key: []byte("bar"), - Meta: []byte("baz"), -} -var simpleSiteDataAlt = &caddytls.SiteData{ - Cert: []byte("qux"), - Key: []byte("quux"), - Meta: []byte("corge"), -} - -// TestSiteExists tests Storage.SiteExists. -func (s *StorageTest) TestSiteExists() error { - if err := s.runPreTest(); err != nil { - return err - } - defer s.runPostTest() - - // Should not exist at first - siteExists, err := s.SiteExists("example.com") - if err != nil { - return err - } - - if siteExists { - return errors.New("Site should not exist") - } - - // Should exist after we store it - if err := s.StoreSite("example.com", simpleSiteData); err != nil { - return err - } - - siteExists, err = s.SiteExists("example.com") - if err != nil { - return err - } - - if !siteExists { - return errors.New("Expected site to exist") - } - - // Site should no longer exist after we delete it - if err := s.DeleteSite("example.com"); err != nil { - return err - } - - siteExists, err = s.SiteExists("example.com") - if err != nil { - return err - } - - if siteExists { - return errors.New("Site should not exist after delete") - } - return nil -} - -// TestSite tests Storage.LoadSite, Storage.StoreSite, and Storage.DeleteSite. -func (s *StorageTest) TestSite() error { - if err := s.runPreTest(); err != nil { - return err - } - defer s.runPostTest() - - // Should be a not-found error at first - _, err := s.LoadSite("example.com") - if _, ok := err.(caddytls.ErrNotExist); !ok { - return fmt.Errorf("Expected caddytls.ErrNotExist from load, got %T: %v", err, err) - } - - // Delete should also be a not-found error at first - err = s.DeleteSite("example.com") - if _, ok := err.(caddytls.ErrNotExist); !ok { - return fmt.Errorf("Expected ErrNotExist from delete, got: %v", err) - } - - // Should store successfully and then load just fine - if err := s.StoreSite("example.com", simpleSiteData); err != nil { - return err - } - if siteData, err := s.LoadSite("example.com"); err != nil { - return err - } else if !bytes.Equal(siteData.Cert, simpleSiteData.Cert) { - return errors.New("Unexpected cert returned after store") - } else if !bytes.Equal(siteData.Key, simpleSiteData.Key) { - return errors.New("Unexpected key returned after store") - } else if !bytes.Equal(siteData.Meta, simpleSiteData.Meta) { - return errors.New("Unexpected meta returned after store") - } - - // Overwrite should work just fine - if err := s.StoreSite("example.com", simpleSiteDataAlt); err != nil { - return err - } - if siteData, err := s.LoadSite("example.com"); err != nil { - return err - } else if !bytes.Equal(siteData.Cert, simpleSiteDataAlt.Cert) { - return errors.New("Unexpected cert returned after overwrite") - } - - // It should delete fine and then not be there - if err := s.DeleteSite("example.com"); err != nil { - return err - } - _, err = s.LoadSite("example.com") - if _, ok := err.(caddytls.ErrNotExist); !ok { - return fmt.Errorf("Expected caddytls.ErrNotExist after delete, got %T: %v", err, err) - } - - return nil -} - -var simpleUserData = &caddytls.UserData{ - Reg: []byte("foo"), - Key: []byte("bar"), -} -var simpleUserDataAlt = &caddytls.UserData{ - Reg: []byte("baz"), - Key: []byte("qux"), -} - -// TestUser tests Storage.LoadUser and Storage.StoreUser. -func (s *StorageTest) TestUser() error { - if err := s.runPreTest(); err != nil { - return err - } - defer s.runPostTest() - - // Should be a not-found error at first - _, err := s.LoadUser("foo@example.com") - if _, ok := err.(caddytls.ErrNotExist); !ok { - return fmt.Errorf("Expected caddytls.ErrNotExist from load, got %T: %v", err, err) - } - - // Should store successfully and then load just fine - if err := s.StoreUser("foo@example.com", simpleUserData); err != nil { - return err - } - if userData, err := s.LoadUser("foo@example.com"); err != nil { - return err - } else if !bytes.Equal(userData.Reg, simpleUserData.Reg) { - return errors.New("Unexpected reg returned after store") - } else if !bytes.Equal(userData.Key, simpleUserData.Key) { - return errors.New("Unexpected key returned after store") - } - - // Overwrite should work just fine - if err := s.StoreUser("foo@example.com", simpleUserDataAlt); err != nil { - return err - } - if userData, err := s.LoadUser("foo@example.com"); err != nil { - return err - } else if !bytes.Equal(userData.Reg, simpleUserDataAlt.Reg) { - return errors.New("Unexpected reg returned after overwrite") - } - - return nil -} - -// TestMostRecentUserEmail tests Storage.MostRecentUserEmail. -func (s *StorageTest) TestMostRecentUserEmail() error { - if err := s.runPreTest(); err != nil { - return err - } - defer s.runPostTest() - - // Should be empty on first run - if e := s.MostRecentUserEmail(); e != "" { - return fmt.Errorf("Expected empty most recent user on first run, got: %v", e) - } - - // If we store user, then that one should be returned - if err := s.StoreUser("foo1@example.com", simpleUserData); err != nil { - return err - } - if s.AfterUserEmailStore != nil { - s.AfterUserEmailStore("foo1@example.com") - } - if e := s.MostRecentUserEmail(); e != "foo1@example.com" { - return fmt.Errorf("Unexpected most recent email after first store: %v", e) - } - - // If we store another user, then that one should be returned - if err := s.StoreUser("foo2@example.com", simpleUserDataAlt); err != nil { - return err - } - if s.AfterUserEmailStore != nil { - s.AfterUserEmailStore("foo2@example.com") - } - if e := s.MostRecentUserEmail(); e != "foo2@example.com" { - return fmt.Errorf("Unexpected most recent email after user key: %v", e) - } - return nil -} diff --git a/caddytls/storagetest/storagetest_test.go b/caddytls/storagetest/storagetest_test.go deleted file mode 100644 index e4d848c5d..000000000 --- a/caddytls/storagetest/storagetest_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package storagetest - -import ( - "fmt" - "os" - "path/filepath" - "testing" - "time" - - "github.com/mholt/caddy/caddytls" -) - -// TestFileStorage tests the file storage set with the test harness in this -// package. -func TestFileStorage(t *testing.T) { - emailCounter := 0 - storageTest := &StorageTest{ - Storage: &caddytls.FileStorage{Path: "./testdata"}, // nameLocks isn't made here, but it's okay because the tests don't call TryLock or Unlock - PostTest: func() { os.RemoveAll("./testdata") }, - AfterUserEmailStore: func(email string) error { - // We need to change the dir mod time to show a - // that certain dirs are newer. - emailCounter++ - fp := filepath.Join("./testdata", "users", email) - - // What we will do is subtract 10 days from today and - // then add counter * seconds to make the later - // counters newer. We accept that this isn't exactly - // how the file storage works because it only changes - // timestamps on *newly seen* users, but it achieves - // the result that the harness expects. - chTime := time.Now().AddDate(0, 0, -10).Add(time.Duration(emailCounter) * time.Second) - if err := os.Chtimes(fp, chTime, chTime); err != nil { - return fmt.Errorf("Unable to change file time for %v: %v", fp, err) - } - return nil - }, - } - storageTest.Test(t, false) -} diff --git a/caddytls/tls.go b/caddytls/tls.go index 36554c401..9c39989b2 100644 --- a/caddytls/tls.go +++ b/caddytls/tls.go @@ -15,7 +15,7 @@ // Package caddytls facilitates the management of TLS assets and integrates // Let's Encrypt functionality into Caddy with first-class support for // creating and renewing certificates automatically. It also implements -// the tls directive. +// the tls directive. It's mostly powered by the CertMagic package. // // This package is meant to be used by Caddy server types. To use the // tls directive, a server type must import this package and call @@ -29,196 +29,11 @@ package caddytls import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net" - "os" - "path/filepath" - "strings" - "github.com/mholt/caddy" - "github.com/xenolf/lego/acme" + "github.com/mholt/certmagic" + "github.com/xenolf/lego/challenge" ) -// HostQualifies returns true if the hostname alone -// appears eligible for automatic HTTPS. For example: -// localhost, empty hostname, and IP addresses are -// not eligible because we cannot obtain certificates -// for those names. Wildcard names are allowed, as long -// as they conform to CABF requirements (only one wildcard -// label, and it must be the left-most label). -func HostQualifies(hostname string) bool { - return hostname != "localhost" && // localhost is ineligible - - // hostname must not be empty - strings.TrimSpace(hostname) != "" && - - // only one wildcard label allowed, and it must be left-most - (!strings.Contains(hostname, "*") || - (strings.Count(hostname, "*") == 1 && - strings.HasPrefix(hostname, "*."))) && - - // must not start or end with a dot - !strings.HasPrefix(hostname, ".") && - !strings.HasSuffix(hostname, ".") && - - // cannot be an IP address, see - // https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt - net.ParseIP(hostname) == nil -} - -// 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 { - // Save cert, private key, and metadata - siteData := &SiteData{ - Cert: cert.Certificate, - Key: cert.PrivateKey, - } - var err error - siteData.Meta, err = json.MarshalIndent(&cert, "", "\t") - if err == nil { - err = storage.StoreSite(cert.Domain, siteData) - } - return err -} - -// Revoke revokes the certificate for host via ACME protocol. -// It assumes the certificate was obtained from the -// CA at DefaultCAUrl. -func Revoke(host string) error { - client, err := newACMEClient(new(Config), true) - if err != nil { - return err - } - return client.Revoke(host) -} - -// 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 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 tlsALPNSolver) CleanUp(domain, token, keyAuth string) error { - s.certCache.Lock() - delete(s.certCache.cache, domain) - s.certCache.Unlock() - return nil -} - -// 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 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 -// even other networks, as long as they share the folder as part of -// the local file system. -// -// 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 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 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 provider server: %v", err) - } - } - - err := os.MkdirAll(dhs.challengeTokensBasePath(), 0755) - if err != nil { - return err - } - - infoBytes, err := json.Marshal(challengeInfo{ - Domain: domain, - Token: token, - KeyAuth: keyAuth, - }) - if err != nil { - return err - } - - return ioutil.WriteFile(dhs.challengeTokensPath(domain), infoBytes, 0644) -} - -// CleanUp removes the challenge certificate from the cache. -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 provider server: %v", err) - } - } - return os.Remove(dhs.challengeTokensPath(domain)) -} - -func (dhs distributedSolver) challengeTokensPath(domain string) string { - domainFile := fileSafe(domain) - return filepath.Join(dhs.challengeTokensBasePath(), domainFile+".json") -} - -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 { @@ -240,11 +55,12 @@ func QualifiesForManagedTLS(c ConfigHolder) bool { return false } tlsConfig := c.TLSConfig() - if tlsConfig == nil { + if tlsConfig == nil || tlsConfig.Manager == nil { return false } + onDemand := tlsConfig.Manager.OnDemand != nil - return (!tlsConfig.Manual || tlsConfig.OnDemand) && // user might provide own cert and key + return (!tlsConfig.Manual || onDemand) && // user might provide own cert and key // if self-signed, we've already generated one to use !tlsConfig.SelfSigned && @@ -255,17 +71,30 @@ func QualifiesForManagedTLS(c ConfigHolder) bool { // we get can't certs for some kinds of hostnames, but // on-demand TLS allows empty hostnames at startup - (HostQualifies(c.Host()) || tlsConfig.OnDemand) + (certmagic.HostQualifies(c.Host()) || onDemand) +} + +// Revoke revokes the certificate fro host via the ACME protocol. +// It assumes the certificate was obtained from certmagic.CA. +func Revoke(domainName string) error { + return certmagic.NewDefault().RevokeCert(domainName, true) +} + +// KnownACMECAs is a list of ACME directory endpoints of +// known, public, and trusted ACME-compatible certificate +// authorities. +var KnownACMECAs = []string{ + "https://acme-v02.api.letsencrypt.org/directory", } // ChallengeProvider defines an own type that should be used in Caddy plugins -// over acme.ChallengeProvider. Using acme.ChallengeProvider causes version mismatches +// over challenge.Provider. Using challenge.Provider causes version mismatches // with vendored dependencies (see https://github.com/mattfarina/golang-broken-vendor) // -// acme.ChallengeProvider is an interface that allows the implementation of custom +// challenge.Provider is an interface that allows the implementation of custom // challenge providers. For more details, see: // https://godoc.org/github.com/xenolf/lego/acme#ChallengeProvider -type ChallengeProvider acme.ChallengeProvider +type ChallengeProvider challenge.Provider // DNSProviderConstructor is a function that takes credentials and // returns a type that can solve the ACME DNS challenges. @@ -280,32 +109,12 @@ func RegisterDNSProvider(name string, provider DNSProviderConstructor) { caddy.RegisterPlugin("tls.dns."+name, caddy.Plugin{}) } -var ( - // DefaultEmail represents the Let's Encrypt account email to use if none provided. - DefaultEmail string +// TODO... - // Agreed indicates whether user has agreed to the Let's Encrypt SA. - Agreed bool +// var storageProviders = make(map[string]StorageConstructor) - // DefaultCAUrl is the default URL to the CA's ACME directory endpoint. - // It's very important to set this unless you set it in every Config. - DefaultCAUrl string - - // DefaultKeyType is used as the type of key for new certificates - // when no other key type is specified. - DefaultKeyType = acme.RSA2048 - - // DisableHTTPChallenge will disable all HTTP challenges. - DisableHTTPChallenge bool - - // DisableTLSALPNChallenge will disable all TLS-ALPN challenges. - DisableTLSALPNChallenge bool -) - -var storageProviders = make(map[string]StorageConstructor) - -// RegisterStorageProvider registers provider by name for storing tls data -func RegisterStorageProvider(name string, provider StorageConstructor) { - storageProviders[name] = provider - caddy.RegisterPlugin("tls.storage."+name, caddy.Plugin{}) -} +// // RegisterStorageProvider registers provider by name for storing tls data +// func RegisterStorageProvider(name string, provider StorageConstructor) { +// storageProviders[name] = provider +// caddy.RegisterPlugin("tls.storage."+name, caddy.Plugin{}) +// } diff --git a/caddytls/tls_test.go b/caddytls/tls_test.go index 89f0f5e54..d7cfc999b 100644 --- a/caddytls/tls_test.go +++ b/caddytls/tls_test.go @@ -15,49 +15,11 @@ package caddytls import ( - "os" "testing" - "github.com/xenolf/lego/acme" + "github.com/mholt/certmagic" ) -func TestHostQualifies(t *testing.T) { - for i, test := range []struct { - host string - expect bool - }{ - {"example.com", true}, - {"sub.example.com", true}, - {"Sub.Example.COM", true}, - {"127.0.0.1", false}, - {"127.0.1.5", false}, - {"69.123.43.94", false}, - {"::1", false}, - {"::", false}, - {"0.0.0.0", false}, - {"", false}, - {" ", false}, - {"*.example.com", true}, - {"*.*.example.com", false}, - {"sub.*.example.com", false}, - {"*sub.example.com", false}, - {".com", false}, - {"example.com.", false}, - {"localhost", false}, - {"local", true}, - {"devsite", true}, - {"192.168.1.3", false}, - {"10.0.2.1", false}, - {"169.112.53.4", false}, - } { - actual := HostQualifies(test.host) - if actual != test.expect { - t.Errorf("Test %d: Expected HostQualifies(%s)=%v, but got %v", - i, test.host, test.expect, actual) - } - } -} - type holder struct { host, port string cfg *Config @@ -79,17 +41,17 @@ func TestQualifiesForManagedTLS(t *testing.T) { {holder{host: "", cfg: new(Config)}, false}, {holder{host: "localhost", cfg: new(Config)}, false}, {holder{host: "123.44.3.21", cfg: new(Config)}, false}, - {holder{host: "example.com", cfg: new(Config)}, true}, - {holder{host: "*.example.com", cfg: new(Config)}, true}, + {holder{host: "example.com", cfg: &Config{Manager: &certmagic.Config{}}}, true}, + {holder{host: "*.example.com", cfg: &Config{Manager: &certmagic.Config{}}}, true}, {holder{host: "*.*.example.com", cfg: new(Config)}, false}, {holder{host: "*sub.example.com", cfg: new(Config)}, false}, {holder{host: "sub.*.example.com", cfg: new(Config)}, false}, - {holder{host: "example.com", cfg: &Config{Manual: true}}, false}, - {holder{host: "example.com", cfg: &Config{ACMEEmail: "off"}}, false}, - {holder{host: "example.com", cfg: &Config{ACMEEmail: "foo@bar.com"}}, true}, + {holder{host: "example.com", cfg: &Config{Manager: &certmagic.Config{}, Manual: true}}, false}, + {holder{host: "example.com", cfg: &Config{Manager: &certmagic.Config{}, ACMEEmail: "off"}}, false}, + {holder{host: "example.com", cfg: &Config{Manager: &certmagic.Config{}, ACMEEmail: "foo@bar.com"}}, true}, {holder{host: "example.com", port: "80"}, false}, - {holder{host: "example.com", port: "1234", cfg: new(Config)}, true}, - {holder{host: "example.com", port: "443", cfg: new(Config)}, true}, + {holder{host: "example.com", port: "1234", cfg: &Config{Manager: &certmagic.Config{}}}, true}, + {holder{host: "example.com", port: "443", cfg: &Config{Manager: &certmagic.Config{}}}, true}, {holder{host: "example.com", port: "80"}, false}, } { if got, want := QualifiesForManagedTLS(test.cfg), test.expect; got != want { @@ -97,88 +59,3 @@ func TestQualifiesForManagedTLS(t *testing.T) { } } } - -func TestSaveCertResource(t *testing.T) { - storage := &FileStorage{Path: "./le_test_save"} - defer func() { - err := os.RemoveAll(storage.Path) - if err != nil { - t.Fatalf("Could not remove temporary storage directory (%s): %v", storage.Path, err) - } - }() - - domain := "example.com" - certContents := "certificate" - keyContents := "private key" - metaContents := `{ - "domain": "example.com", - "certUrl": "https://example.com/cert", - "certStableUrl": "https://example.com/cert/stable" -}` - - cert := &acme.CertificateResource{ - Domain: domain, - CertURL: "https://example.com/cert", - CertStableURL: "https://example.com/cert/stable", - PrivateKey: []byte(keyContents), - Certificate: []byte(certContents), - } - - err := saveCertResource(storage, cert) - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - siteData, err := storage.LoadSite(domain) - if err != nil { - t.Errorf("Expected no error reading site, got: %v", err) - } - if string(siteData.Cert) != certContents { - t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(siteData.Cert)) - } - if string(siteData.Key) != keyContents { - t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(siteData.Key)) - } - if string(siteData.Meta) != metaContents { - t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(siteData.Meta)) - } -} - -func TestExistingCertAndKey(t *testing.T) { - storage := &FileStorage{Path: "./le_test_existing"} - defer func() { - err := os.RemoveAll(storage.Path) - if err != nil { - t.Fatalf("Could not remove temporary storage directory (%s): %v", storage.Path, err) - } - }() - - domain := "example.com" - - siteExists, err := storage.SiteExists(domain) - if err != nil { - t.Fatalf("Could not determine whether site exists: %v", err) - } - - if siteExists { - t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain) - } - - err = saveCertResource(storage, &acme.CertificateResource{ - Domain: domain, - PrivateKey: []byte("key"), - Certificate: []byte("cert"), - }) - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - siteExists, err = storage.SiteExists(domain) - if err != nil { - t.Fatalf("Could not determine whether site exists: %v", err) - } - - if !siteExists { - t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain) - } -} diff --git a/caddytls/user.go b/caddytls/user.go deleted file mode 100644 index e7a9646d3..000000000 --- a/caddytls/user.go +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "bufio" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "strings" - - "github.com/xenolf/lego/acme" -) - -// User represents a Let's Encrypt user account. -type User struct { - Email string - Registration *acme.RegistrationResource - key crypto.PrivateKey -} - -// GetEmail gets u's email. -func (u User) GetEmail() string { - return u.Email -} - -// GetRegistration gets u's registration resource. -func (u User) GetRegistration() *acme.RegistrationResource { - return u.Registration -} - -// GetPrivateKey gets u's private key. -func (u User) GetPrivateKey() crypto.PrivateKey { - return u.key -} - -// newUser creates a new User for the given email address -// with a new private key. This function does NOT save the -// user to disk or register it via ACME. If you want to use -// a user account that might already exist, call getUser -// instead. It does NOT prompt the user. -func newUser(email string) (User, error) { - user := User{Email: email} - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - return user, errors.New("error generating private key: " + err.Error()) - } - user.key = privateKey - return user, nil -} - -// getEmail does everything it can to obtain an email address -// from the user within the scope of memory and storage to use -// for ACME TLS. If it cannot get an email address, it returns -// empty string. (If user is present, it will warn the user of -// the consequences of an empty email.) This function MAY prompt -// the user for input. If userPresent is false, the operator -// will NOT be prompted and an empty email may be returned. -// If the user is prompted, a new User will be created and -// stored in storage according to the email address they -// provided (which might be blank). -func getEmail(cfg *Config, userPresent bool) (string, error) { - storage, err := cfg.StorageFor(cfg.CAUrl) - if err != nil { - return "", err - } - - // First try memory (command line flag or typed by user previously) - leEmail := DefaultEmail - - // Then try to get most recent user email from storage - if leEmail == "" { - leEmail = storage.MostRecentUserEmail() - DefaultEmail = leEmail // save for next time - } - - // Looks like there is no email address readily available, - // so we will have to ask the user if we can. - if leEmail == "" && userPresent { - // evidently, no User data was present in storage; - // thus we must make a new User so that we can get - // the Terms of Service URL via our ACME client, phew! - user, err := newUser("") - if err != nil { - return "", err - } - - // get the agreement URL - agreementURL := agreementTestURL - if agreementURL == "" { - // we call acme.NewClient directly because newACMEClient - // would require that we already know the user's email - caURL := DefaultCAUrl - if cfg.CAUrl != "" { - caURL = cfg.CAUrl - } - tempClient, err := acme.NewClient(caURL, user, "") - if err != nil { - return "", fmt.Errorf("making ACME client to get ToS URL: %v", err) - } - agreementURL = tempClient.GetToSURL() - } - - // prompt the user for an email address and terms agreement - reader := bufio.NewReader(stdin) - promptUserAgreement(agreementURL) - fmt.Println("Please enter your email address to signify agreement and to be notified") - fmt.Println("in case of issues. You can leave it blank, but we don't recommend it.") - fmt.Print(" Email address: ") - leEmail, err = reader.ReadString('\n') - if err != nil && err != io.EOF { - return "", fmt.Errorf("reading email address: %v", err) - } - leEmail = strings.TrimSpace(leEmail) - DefaultEmail = leEmail - Agreed = true - - // save the new user to preserve this for next time - user.Email = leEmail - err = saveUser(storage, user) - if err != nil { - return "", err - } - } - - // lower-casing the email is important for consistency - return strings.ToLower(leEmail), nil -} - -// getUser loads the user with the given email from disk -// using the provided storage. If the user does not exist, -// it will create a new one, but it does NOT save new -// users to the disk or register them via ACME. It does -// NOT prompt the user. -func getUser(storage Storage, email string) (User, error) { - var user User - - // open user reg - userData, err := storage.LoadUser(email) - if err != nil { - if _, ok := err.(ErrNotExist); ok { - // create a new user - return newUser(email) - } - return user, err - } - - // load user information - err = json.Unmarshal(userData.Reg, &user) - if err != nil { - return user, err - } - - // load their private key - user.key, err = loadPrivateKey(userData.Key) - return user, err -} - -// saveUser persists a user's key and account registration -// to the file system. It does NOT register the user via ACME -// or prompt the user. You must also pass in the storage -// wherein the user should be saved. It should be the storage -// for the CA with which user has an account. -func saveUser(storage Storage, user User) error { - // Save the private key and registration - userData := new(UserData) - var err error - userData.Key, err = savePrivateKey(user.key) - if err == nil { - userData.Reg, err = json.MarshalIndent(&user, "", "\t") - } - if err == nil { - err = storage.StoreUser(user.Email, userData) - } - return err -} - -// promptUserAgreement simply outputs the standard user -// agreement prompt with the given agreement URL. -// It outputs a newline after the message. -func promptUserAgreement(agreementURL string) { - const userAgreementPrompt = `Your sites will be served over HTTPS automatically using Let's Encrypt. -By continuing, you agree to the Let's Encrypt Subscriber Agreement at:` - fmt.Printf("\n\n%s\n %s\n", userAgreementPrompt, agreementURL) -} - -// askUserAgreement prompts the user to agree to the agreement -// at the given agreement URL via stdin. It returns whether the -// user agreed or not. -func askUserAgreement(agreementURL string) bool { - promptUserAgreement(agreementURL) - fmt.Print("Do you agree to the terms? (y/n): ") - - reader := bufio.NewReader(stdin) - answer, err := reader.ReadString('\n') - if err != nil { - return false - } - answer = strings.ToLower(strings.TrimSpace(answer)) - - return answer == "y" || answer == "yes" -} - -// agreementTestURL is set during tests to skip requiring -// setting up an entire ACME CA endpoint. -var agreementTestURL string - -// stdin is used to read the user's input if prompted; -// this is changed by tests during tests. -var stdin = io.ReadWriter(os.Stdin) - -// The name of the folder for accounts where the email -// address was not provided; default 'username' if you will, -// but only for local/storage use, not with the CA. -const emptyEmail = "default" diff --git a/caddytls/user_test.go b/caddytls/user_test.go deleted file mode 100644 index 1fb1632df..000000000 --- a/caddytls/user_test.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2015 Light Code Labs, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "bytes" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "io" - "path/filepath" - "strings" - "testing" - "time" - - "os" - - "github.com/xenolf/lego/acme" -) - -func TestUser(t *testing.T) { - defer testStorage.clean() - - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - t.Fatalf("Could not generate test private key: %v", err) - } - u := User{ - Email: "me@mine.com", - Registration: new(acme.RegistrationResource), - key: privateKey, - } - - if expected, actual := "me@mine.com", u.GetEmail(); actual != expected { - t.Errorf("Expected email '%s' but got '%s'", expected, actual) - } - if u.GetRegistration() == nil { - t.Error("Expected a registration resource, but got nil") - } - if expected, actual := privateKey, u.GetPrivateKey(); actual != expected { - t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual) - } -} - -func TestNewUser(t *testing.T) { - email := "me@foobar.com" - user, err := newUser(email) - if err != nil { - t.Fatalf("Error creating user: %v", err) - } - if user.key == nil { - t.Error("Private key is nil") - } - if user.Email != email { - t.Errorf("Expected email to be %s, but was %s", email, user.Email) - } - if user.Registration != nil { - t.Error("New user already has a registration resource; it shouldn't") - } -} - -func TestSaveUser(t *testing.T) { - defer testStorage.clean() - - email := "me@foobar.com" - user, err := newUser(email) - if err != nil { - t.Fatalf("Error creating user: %v", err) - } - - err = saveUser(testStorage, user) - if err != nil { - t.Fatalf("Error saving user: %v", err) - } - _, err = testStorage.LoadUser(email) - if err != nil { - t.Errorf("Cannot access user data, error: %v", err) - } -} - -func TestGetUserDoesNotAlreadyExist(t *testing.T) { - defer testStorage.clean() - - user, err := getUser(testStorage, "user_does_not_exist@foobar.com") - if err != nil { - t.Fatalf("Error getting user: %v", err) - } - - if user.key == nil { - t.Error("Expected user to have a private key, but it was nil") - } -} - -func TestGetUserAlreadyExists(t *testing.T) { - defer testStorage.clean() - - email := "me@foobar.com" - - // Set up test - user, err := newUser(email) - if err != nil { - t.Fatalf("Error creating user: %v", err) - } - err = saveUser(testStorage, user) - if err != nil { - t.Fatalf("Error saving user: %v", err) - } - - // Expect to load user from disk - user2, err := getUser(testStorage, email) - if err != nil { - t.Fatalf("Error getting user: %v", err) - } - - // Assert keys are the same - if !PrivateKeysSame(user.key, user2.key) { - t.Error("Expected private key to be the same after loading, but it wasn't") - } - - // Assert emails are the same - if user.Email != user2.Email { - t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email) - } -} - -func TestGetEmail(t *testing.T) { - // ensure storage (via StorageFor) uses the local testdata folder that we delete later - origCaddypath := os.Getenv("CADDYPATH") - os.Setenv("CADDYPATH", "./testdata") - defer os.Setenv("CADDYPATH", origCaddypath) - - agreementTestURL = "(none - testing)" - defer func() { agreementTestURL = "" }() - - // let's not clutter up the output - origStdout := os.Stdout - os.Stdout = nil - defer func() { os.Stdout = origStdout }() - - defer testStorage.clean() - DefaultEmail = "test2@foo.com" - - // Test1: Use default email from flag (or user previously typing it) - actual, err := getEmail(testConfig, true) - if err != nil { - t.Fatalf("getEmail (1) error: %v", err) - } - if actual != DefaultEmail { - t.Errorf("Did not get correct email from memory; expected '%s' but got '%s'", DefaultEmail, actual) - } - - // Test2: Get input from user - DefaultEmail = "" - stdin = new(bytes.Buffer) - _, err = io.Copy(stdin, strings.NewReader("test3@foo.com\n")) - if err != nil { - t.Fatalf("Could not simulate user input, error: %v", err) - } - actual, err = getEmail(testConfig, true) - if err != nil { - t.Fatalf("getEmail (2) error: %v", err) - } - if actual != "test3@foo.com" { - t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual) - } - - // Test3: Get most recent email from before (in storage) - DefaultEmail = "" - for i, eml := range []string{ - "test4-1@foo.com", - "test4-2@foo.com", - "TEST4-3@foo.com", // test case insensitivity - } { - u, err := newUser(eml) - if err != nil { - t.Fatalf("Error creating user %d: %v", i, err) - } - err = saveUser(testStorage, u) - if err != nil { - t.Fatalf("Error saving user %d: %v", i, err) - } - - // Change modified time so they're all different and the test becomes more deterministic - f, err := os.Stat(testStorage.user(eml)) - if err != nil { - t.Fatalf("Could not access user folder for '%s': %v", eml, err) - } - chTime := f.ModTime().Add(time.Duration(i) * time.Hour) // 1 second isn't always enough space! - if err := os.Chtimes(testStorage.user(eml), chTime, chTime); err != nil { - t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err) - } - } - actual, err = getEmail(testConfig, true) - if err != nil { - t.Fatalf("getEmail (3) error: %v", err) - } - if actual != "test4-3@foo.com" { - t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual) - } -} - -var ( - testStorageBase = "./testdata" // ephemeral folder that gets deleted after tests finish - testCAHost = "localhost" - testConfig = &Config{CAUrl: "http://" + testCAHost + "/directory", StorageProvider: "file"} - testStorage = &FileStorage{Path: filepath.Join(testStorageBase, "acme", testCAHost)} -) - -func (s *FileStorage) clean() error { return os.RemoveAll(testStorageBase) } diff --git a/controller.go b/controller.go index f63cebe00..af40a23a4 100644 --- a/controller.go +++ b/controller.go @@ -71,37 +71,37 @@ func (c *Controller) ServerType() string { // OnFirstStartup adds fn to the list of callback functions to execute // when the server is about to be started NOT as part of a restart. func (c *Controller) OnFirstStartup(fn func() error) { - c.instance.onFirstStartup = append(c.instance.onFirstStartup, fn) + c.instance.OnFirstStartup = append(c.instance.OnFirstStartup, fn) } // OnStartup adds fn to the list of callback functions to execute // when the server is about to be started (including restarts). func (c *Controller) OnStartup(fn func() error) { - c.instance.onStartup = append(c.instance.onStartup, fn) + c.instance.OnStartup = append(c.instance.OnStartup, fn) } // OnRestart adds fn to the list of callback functions to execute // when the server is about to be restarted. func (c *Controller) OnRestart(fn func() error) { - c.instance.onRestart = append(c.instance.onRestart, fn) + c.instance.OnRestart = append(c.instance.OnRestart, fn) } // OnRestartFailed adds fn to the list of callback functions to execute // if the server failed to restart. func (c *Controller) OnRestartFailed(fn func() error) { - c.instance.onRestartFailed = append(c.instance.onRestartFailed, fn) + c.instance.OnRestartFailed = append(c.instance.OnRestartFailed, fn) } // OnShutdown adds fn to the list of callback functions to execute // when the server is about to be shut down (including restarts). func (c *Controller) OnShutdown(fn func() error) { - c.instance.onShutdown = append(c.instance.onShutdown, fn) + c.instance.OnShutdown = append(c.instance.OnShutdown, fn) } // OnFinalShutdown adds fn to the list of callback functions to execute // when the server is about to be shut down NOT as part of a restart. func (c *Controller) OnFinalShutdown(fn func() error) { - c.instance.onFinalShutdown = append(c.instance.onFinalShutdown, fn) + c.instance.OnFinalShutdown = append(c.instance.OnFinalShutdown, fn) } // Context gets the context associated with the instance associated with c.