mirror of
https://github.com/caddyserver/caddy.git
synced 2025-05-24 02:02:26 -04:00
Merge branch 'certmagic'
This commit is contained in:
commit
33f2b16a1b
62
caddy.go
62
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
|
||||
|
@ -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 {
|
||||
|
@ -16,9 +16,7 @@ package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@ -204,39 +202,3 @@ func TestIsInternal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerAddrEqual(t *testing.T) {
|
||||
ln1, err := net.Listen("tcp", "[::]:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln1.Close()
|
||||
ln1port := strconv.Itoa(ln1.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln2.Close()
|
||||
ln2port := strconv.Itoa(ln2.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
for i, test := range []struct {
|
||||
ln net.Listener
|
||||
addr string
|
||||
expect bool
|
||||
}{
|
||||
{ln1, ":" + ln2port, false},
|
||||
{ln1, "0.0.0.0:" + ln2port, false},
|
||||
{ln1, "0.0.0.0", false},
|
||||
{ln1, ":" + ln1port, true},
|
||||
{ln1, "0.0.0.0:" + ln1port, true},
|
||||
{ln2, ":" + ln2port, false},
|
||||
{ln2, "127.0.0.1:" + ln1port, false},
|
||||
{ln2, "127.0.0.1", false},
|
||||
{ln2, "127.0.0.1:" + ln2port, true},
|
||||
} {
|
||||
if got, want := listenerAddrEqual(test.ln, test.addr), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s == %s): expected %v but was %v", i, test.addr, test.ln.Addr().String(), want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
// ensure that the standard plugins are in fact plugged in
|
||||
// and registered properly; this is a quick/naive way to do it.
|
||||
func TestStandardPlugins(t *testing.T) {
|
||||
numStandardPlugins := 31 // importing caddyhttp plugs in this many plugins
|
||||
numStandardPlugins := 30 // importing caddyhttp plugs in this many plugins
|
||||
s := caddy.DescribePlugins()
|
||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
|
||||
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
|
||||
|
@ -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,14 @@ 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 == nil || 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 +120,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 +214,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,
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
@ -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{Managed: true, Manager: &certmagic.Config{}}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
@ -196,18 +197,18 @@ func TestEnableAutoHTTPS(t *testing.T) {
|
||||
func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
// TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list...
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: ""}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "localhost"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: ""}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "localhost"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manual: true}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "off"}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com"}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com", Manager: &certmagic.Config{}}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: newManagedConfig()},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
@ -224,3 +225,7 @@ func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
||||
|
||||
func newManagedConfig() *caddytls.Config {
|
||||
return &caddytls.Config{Manager: &certmagic.Config{}}
|
||||
}
|
||||
|
@ -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.
|
||||
@ -336,7 +346,11 @@ func GetConfig(c *caddy.Controller) *SiteConfig {
|
||||
// we should only get here during tests because directive
|
||||
// actions typically skip the server blocks where we make
|
||||
// the configs
|
||||
cfg := &SiteConfig{Root: Root, TLS: new(caddytls.Config), IndexPages: staticfiles.DefaultIndexPages}
|
||||
cfg := &SiteConfig{
|
||||
Root: Root,
|
||||
TLS: &caddytls.Config{Manager: certmagic.NewDefault()},
|
||||
IndexPages: staticfiles.DefaultIndexPages,
|
||||
}
|
||||
ctx.saveConfig(key, cfg)
|
||||
return cfg
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
}
|
@ -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
|
@ -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"
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
106
caddytls/selfsigned.go
Normal file
106
caddytls/selfsigned.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
251
caddytls/tls.go
251
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{})
|
||||
// }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
@ -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.
|
||||
|
21
vendor/github.com/codahale/aesnicheck/LICENSE
generated
vendored
Normal file
21
vendor/github.com/codahale/aesnicheck/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Coda Hale
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
9
vendor/github.com/codahale/aesnicheck/asm_amd64.s
generated
vendored
Normal file
9
vendor/github.com/codahale/aesnicheck/asm_amd64.s
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
// func HasAESNI() bool
|
||||
TEXT ·HasAESNI(SB),$0
|
||||
XORQ AX, AX
|
||||
INCL AX
|
||||
CPUID
|
||||
SHRQ $25, CX
|
||||
ANDQ $1, CX
|
||||
MOVB CX, ret+0(FP)
|
||||
RET
|
6
vendor/github.com/codahale/aesnicheck/check_asm.go
generated
vendored
Normal file
6
vendor/github.com/codahale/aesnicheck/check_asm.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// +build amd64
|
||||
|
||||
package aesnicheck
|
||||
|
||||
// HasAESNI returns whether AES-NI is supported by the CPU.
|
||||
func HasAESNI() bool
|
8
vendor/github.com/codahale/aesnicheck/check_generic.go
generated
vendored
Normal file
8
vendor/github.com/codahale/aesnicheck/check_generic.go
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
// +build !amd64
|
||||
|
||||
package aesnicheck
|
||||
|
||||
// HasAESNI returns whether AES-NI is supported by the CPU.
|
||||
func HasAESNI() bool {
|
||||
return false
|
||||
}
|
22
vendor/github.com/codahale/aesnicheck/cmd/aesnicheck/aesnicheck.go
generated
vendored
Normal file
22
vendor/github.com/codahale/aesnicheck/cmd/aesnicheck/aesnicheck.go
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
// Command aesnicheck queries the CPU for AES-NI support. If AES-NI is supported,
|
||||
// aesnicheck will print "supported" and exit with a status of 0. If AES-NI is
|
||||
// not supported, aesnicheck will print "unsupported" and exit with a status of
|
||||
// -1.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/codahale/aesnicheck"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if aesnicheck.HasAESNI() {
|
||||
fmt.Println("supported")
|
||||
os.Exit(0)
|
||||
} else {
|
||||
fmt.Println("unsupported")
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
9
vendor/github.com/codahale/aesnicheck/docs.go
generated
vendored
Normal file
9
vendor/github.com/codahale/aesnicheck/docs.go
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
// Package aesnicheck provides a simple check to see if crypto/aes is using
|
||||
// AES-NI instructions or if the AES transform is being done in software. AES-NI
|
||||
// is constant-time, which makes it impervious to cache-level timing attacks. For
|
||||
// security-conscious deployments on public cloud infrastructure (Amazon EC2,
|
||||
// Google Compute Engine, Microsoft Azure, etc.) this may be critical.
|
||||
//
|
||||
// See http://eprint.iacr.org/2014/248 for details on cross-VM timing attacks on
|
||||
// AES keys.
|
||||
package aesnicheck
|
201
vendor/github.com/mholt/certmagic/LICENSE.txt
generated
vendored
Normal file
201
vendor/github.com/mholt/certmagic/LICENSE.txt
generated
vendored
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
170
vendor/github.com/mholt/certmagic/cache.go
generated
vendored
Normal file
170
vendor/github.com/mholt/certmagic/cache.go
generated
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache is a structure that stores certificates in memory.
|
||||
// Generally, there should only be one per process. However,
|
||||
// complex applications that virtualize the concept of a
|
||||
// "process" (such as Caddy, which virtualizes processes as
|
||||
// "instances" so it can do graceful, in-memory reloads of
|
||||
// its configuration) may use more of these per OS process.
|
||||
//
|
||||
// Using just one cache per process avoids duplication of
|
||||
// certificates across multiple configurations and makes
|
||||
// maintenance easier.
|
||||
//
|
||||
// An empty cache is INVALID and must not be used.
|
||||
// Be sure to call NewCertificateCache to get one.
|
||||
//
|
||||
// These should be very long-lived values, and must not be
|
||||
// copied. Before all references leave scope to be garbage
|
||||
// collected, ensure you call Stop() to stop maintenance
|
||||
// maintenance on the certificates stored in this cache.
|
||||
type Cache struct {
|
||||
// How often to check certificates for renewal
|
||||
RenewInterval time.Duration
|
||||
|
||||
// How often to check if OCSP stapling needs updating
|
||||
OCSPInterval time.Duration
|
||||
|
||||
// The storage implementation
|
||||
storage Storage
|
||||
|
||||
// The cache is keyed by certificate hash
|
||||
cache map[string]Certificate
|
||||
|
||||
// Protects the cache map
|
||||
mu sync.RWMutex
|
||||
|
||||
// Close this channel to cancel asset maintenance
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NewCache returns a new, valid Cache backed by the
|
||||
// given storage implementation. It also begins a
|
||||
// maintenance goroutine for any managed certificates
|
||||
// stored in this cache.
|
||||
//
|
||||
// See the godoc for Cache to use it properly.
|
||||
//
|
||||
// Note that all processes running in a cluster
|
||||
// configuration must use the same storage value
|
||||
// in order to share certificates. (A single storage
|
||||
// value may be shared by multiple clusters as well.)
|
||||
func NewCache(storage Storage) *Cache {
|
||||
c := &Cache{
|
||||
RenewInterval: DefaultRenewInterval,
|
||||
OCSPInterval: DefaultOCSPInterval,
|
||||
storage: storage,
|
||||
cache: make(map[string]Certificate),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
go c.maintainAssets()
|
||||
return c
|
||||
}
|
||||
|
||||
// Stop stops the maintenance goroutine for
|
||||
// certificates in certCache.
|
||||
func (certCache *Cache) Stop() {
|
||||
close(certCache.stopChan)
|
||||
}
|
||||
|
||||
// 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 *Cache) replaceCertificate(oldCert, newCert Certificate) error {
|
||||
certCache.mu.Lock()
|
||||
defer certCache.mu.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 *Cache) 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
|
||||
}
|
||||
|
||||
// defaultCache is a convenient, default certificate cache for
|
||||
// use by this process when no other certificate cache is provided.
|
||||
var defaultCache = NewCache(DefaultStorage)
|
249
caddytls/certificates.go → vendor/github.com/mholt/certmagic/certificates.go
generated
vendored
249
caddytls/certificates.go → vendor/github.com/mholt/certmagic/certificates.go
generated
vendored
@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -12,114 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"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.
|
||||
@ -146,56 +53,99 @@ type Certificate struct {
|
||||
// This field will be populated by cacheCertificate.
|
||||
// Only meddle with it if you know what you're doing!
|
||||
configs []*Config
|
||||
|
||||
// whether this certificate is under our management
|
||||
managed bool
|
||||
}
|
||||
|
||||
// NeedsRenewal returns true if the certificate is
|
||||
// expiring soon or has expired.
|
||||
func (c Certificate) NeedsRenewal() bool {
|
||||
if c.NotAfter.IsZero() {
|
||||
return false
|
||||
}
|
||||
timeLeft := c.NotAfter.UTC().Sub(time.Now().UTC())
|
||||
renewDurationBefore := DefaultRenewDurationBefore
|
||||
if len(c.configs) > 0 && c.configs[0].RenewDurationBefore > 0 {
|
||||
renewDurationBefore = c.configs[0].RenewDurationBefore
|
||||
}
|
||||
return timeLeft < renewDurationBefore
|
||||
}
|
||||
|
||||
// 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 is a lower-level method; normally you'll call Manage() instead.
|
||||
//
|
||||
// This method is safe for concurrent use.
|
||||
func (cfg *Config) CacheManagedCertificate(domain string) (Certificate, error) {
|
||||
storage, err := cfg.StorageFor(cfg.CAUrl)
|
||||
certRes, err := cfg.loadCertResource(domain)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
siteData, err := storage.LoadSite(domain)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
cert, err := makeCertificateWithOCSP(siteData.Cert, siteData.Key)
|
||||
cert, err := cfg.makeCertificateWithOCSP(certRes.Certificate, certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
telemetry.Increment("tls_managed_cert_count")
|
||||
cert.managed = true
|
||||
if cfg.OnEvent != nil {
|
||||
cfg.OnEvent("cached_managed_cert", cert.Names)
|
||||
}
|
||||
return cfg.cacheCertificate(cert), nil
|
||||
}
|
||||
|
||||
// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
|
||||
// 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)
|
||||
// This method is safe for concurrent use.
|
||||
func (cfg *Config) CacheUnmanagedCertificatePEMFile(certFile, keyFile string) error {
|
||||
cert, err := cfg.makeCertificateFromDiskWithOCSP(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.cacheCertificate(cert)
|
||||
telemetry.Increment("tls_manual_cert_count")
|
||||
if cfg.OnEvent != nil {
|
||||
cfg.OnEvent("cached_unmanaged_cert", cert.Names)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
|
||||
// CacheUnmanagedTLSCertificate adds tlsCert to the certificate cache.
|
||||
// It staples OCSP if possible.
|
||||
//
|
||||
// This method is safe for concurrent use.
|
||||
func (cfg *Config) CacheUnmanagedTLSCertificate(tlsCert tls.Certificate) error {
|
||||
var cert Certificate
|
||||
err := fillCertFromLeaf(&cert, tlsCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cfg.certCache.stapleOCSP(&cert, nil)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Stapling OCSP: %v", err)
|
||||
}
|
||||
if cfg.OnEvent != nil {
|
||||
cfg.OnEvent("cached_unmanaged_cert", cert.Names)
|
||||
}
|
||||
cfg.cacheCertificate(cert)
|
||||
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)
|
||||
// This method is safe for concurrent use.
|
||||
func (cfg *Config) CacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error {
|
||||
cert, err := cfg.makeCertificateWithOCSP(certBytes, keyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.cacheCertificate(cert)
|
||||
telemetry.Increment("tls_manual_cert_count")
|
||||
if cfg.OnEvent != nil {
|
||||
cfg.OnEvent("cached_unmanaged_cert", cert.Names)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -203,7 +153,7 @@ func (cfg *Config) cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte)
|
||||
// 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) {
|
||||
func (cfg *Config) makeCertificateFromDiskWithOCSP(certFile, keyFile string) (Certificate, error) {
|
||||
certPEMBlock, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
@ -212,7 +162,7 @@ func makeCertificateFromDiskWithOCSP(certFile, keyFile string) (Certificate, err
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
return makeCertificateWithOCSP(certPEMBlock, keyPEMBlock)
|
||||
return cfg.makeCertificateWithOCSP(certPEMBlock, keyPEMBlock)
|
||||
}
|
||||
|
||||
// makeCertificate turns a certificate PEM bundle and a key PEM block into
|
||||
@ -220,7 +170,7 @@ func makeCertificateFromDiskWithOCSP(certFile, keyFile string) (Certificate, err
|
||||
// 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) {
|
||||
func (*Config) makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
|
||||
var cert Certificate
|
||||
|
||||
// Convert to a tls.Certificate
|
||||
@ -240,12 +190,12 @@ func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
|
||||
|
||||
// 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)
|
||||
func (cfg *Config) makeCertificateWithOCSP(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
|
||||
cert, err := cfg.makeCertificate(certPEMBlock, keyPEMBlock)
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
err = stapleOCSP(&cert, certPEMBlock)
|
||||
err = cfg.certCache.stapleOCSP(&cert, certPEMBlock)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Stapling OCSP: %v", err)
|
||||
}
|
||||
@ -255,7 +205,7 @@ func makeCertificateWithOCSP(certPEMBlock, keyPEMBlock []byte) (Certificate, err
|
||||
// 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")
|
||||
return fmt.Errorf("certificate is empty")
|
||||
}
|
||||
cert.Certificate = tlsCert
|
||||
|
||||
@ -284,7 +234,7 @@ func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
|
||||
}
|
||||
}
|
||||
if len(cert.Names) == 0 {
|
||||
return errors.New("certificate has no names")
|
||||
return fmt.Errorf("certificate has no names")
|
||||
}
|
||||
|
||||
// save the hash of this certificate (chain) and
|
||||
@ -295,17 +245,6 @@ func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error {
|
||||
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
|
||||
@ -319,15 +258,12 @@ 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)
|
||||
cfg := cert.configs[0]
|
||||
certRes, err := cfg.loadCertResource(cert.Names[0])
|
||||
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)
|
||||
tlsCert, err := tls.X509KeyPair(certRes.Certificate, certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -336,14 +272,14 @@ func managedCertInStorageExpiresSoon(cert Certificate) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
timeLeft := leaf.NotAfter.Sub(time.Now().UTC())
|
||||
return timeLeft < RenewDurationBefore, nil
|
||||
return timeLeft < cfg.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).
|
||||
// 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.
|
||||
//
|
||||
@ -352,8 +288,8 @@ func managedCertInStorageExpiresSoon(cert Certificate) (bool, error) {
|
||||
//
|
||||
// This function is safe for concurrent use.
|
||||
func (cfg *Config) cacheCertificate(cert Certificate) Certificate {
|
||||
cfg.certCache.Lock()
|
||||
defer cfg.certCache.Unlock()
|
||||
cfg.certCache.mu.Lock()
|
||||
defer cfg.certCache.mu.Unlock()
|
||||
|
||||
// if this certificate already exists in the cache,
|
||||
// use it instead of overwriting it -- very important!
|
||||
@ -380,7 +316,7 @@ func (cfg *Config) cacheCertificate(cert Certificate) Certificate {
|
||||
// (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
|
||||
cfg.certificates[name] = cert.Hash
|
||||
}
|
||||
|
||||
// store the certificate
|
||||
@ -388,3 +324,30 @@ func (cfg *Config) cacheCertificate(cert Certificate) Certificate {
|
||||
|
||||
return cert
|
||||
}
|
||||
|
||||
// HostQualifies returns true if the hostname alone
|
||||
// appears eligible for automagic TLS. 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
|
||||
}
|
511
vendor/github.com/mholt/certmagic/certmagic.go
generated
vendored
Normal file
511
vendor/github.com/mholt/certmagic/certmagic.go
generated
vendored
Normal file
@ -0,0 +1,511 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/certcrypto"
|
||||
"github.com/xenolf/lego/challenge"
|
||||
)
|
||||
|
||||
// HTTPS serves mux for all domainNames using the HTTP
|
||||
// and HTTPS ports, redirecting all HTTP requests to HTTPS.
|
||||
//
|
||||
// Calling this function signifies your acceptance to
|
||||
// the CA's Subscriber Agreement and/or Terms of Service.
|
||||
func HTTPS(domainNames []string, mux http.Handler) error {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
cfg, err := manageWithDefaultConfig(domainNames, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpWg.Add(1)
|
||||
defer httpWg.Done()
|
||||
|
||||
// if we haven't made listeners yet, do so now,
|
||||
// and clean them up when all servers are done
|
||||
lnMu.Lock()
|
||||
if httpLn == nil && httpsLn == nil {
|
||||
httpLn, err = net.Listen("tcp", fmt.Sprintf(":%d", HTTPPort))
|
||||
if err != nil {
|
||||
lnMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
httpsLn, err = tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig())
|
||||
if err != nil {
|
||||
httpLn.Close()
|
||||
httpLn = nil
|
||||
lnMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
httpWg.Wait()
|
||||
lnMu.Lock()
|
||||
httpLn.Close()
|
||||
httpsLn.Close()
|
||||
lnMu.Unlock()
|
||||
}()
|
||||
}
|
||||
hln, hsln := httpLn, httpsLn
|
||||
lnMu.Unlock()
|
||||
|
||||
httpHandler := cfg.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler))
|
||||
|
||||
log.Printf("%v Serving HTTP->HTTPS on %s and %s",
|
||||
domainNames, hln.Addr(), hsln.Addr())
|
||||
|
||||
go http.Serve(hln, httpHandler)
|
||||
return http.Serve(hsln, mux)
|
||||
}
|
||||
|
||||
func httpRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
toURL := "https://"
|
||||
|
||||
// since we redirect to the standard HTTPS port, we
|
||||
// do not need to include it in the redirect URL
|
||||
requestHost, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
requestHost = r.Host // host probably did not contain a port
|
||||
}
|
||||
|
||||
toURL += requestHost
|
||||
toURL += r.URL.RequestURI()
|
||||
|
||||
// get rid of this disgusting unencrypted HTTP connection 🤢
|
||||
w.Header().Set("Connection", "close")
|
||||
|
||||
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
// TLS enables management of certificates for domainNames
|
||||
// and returns a valid tls.Config.
|
||||
//
|
||||
// Because this is a convenience function that returns
|
||||
// only a tls.Config, it does not assume HTTP is being
|
||||
// served on the HTTP port, so the HTTP challenge is
|
||||
// disabled (no HTTPChallengeHandler is necessary).
|
||||
//
|
||||
// Calling this function signifies your acceptance to
|
||||
// the CA's Subscriber Agreement and/or Terms of Service.
|
||||
func TLS(domainNames []string) (*tls.Config, error) {
|
||||
cfg, err := manageWithDefaultConfig(domainNames, true)
|
||||
return cfg.TLSConfig(), err
|
||||
}
|
||||
|
||||
// Listen manages certificates for domainName and returns a
|
||||
// TLS listener.
|
||||
//
|
||||
// Because this convenience function returns only a TLS-enabled
|
||||
// listener and does not presume HTTP is also being served,
|
||||
// the HTTP challenge will be disabled.
|
||||
//
|
||||
// Calling this function signifies your acceptance to
|
||||
// the CA's Subscriber Agreement and/or Terms of Service.
|
||||
func Listen(domainNames []string) (net.Listener, error) {
|
||||
cfg, err := manageWithDefaultConfig(domainNames, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig())
|
||||
}
|
||||
|
||||
// Manage obtains certificates for domainNames and keeps them
|
||||
// renewed using the returned Config.
|
||||
//
|
||||
// You will need to ensure that you use a TLS config that gets
|
||||
// certificates from this Config and that the HTTP and TLS-ALPN
|
||||
// challenges can be solved. The easiest way to do this is to
|
||||
// use cfg.TLSConfig() as your TLS config and to wrap your
|
||||
// HTTP handler with cfg.HTTPChallengeHandler(). If you don't
|
||||
// have an HTTP server, you will need to disable the HTTP
|
||||
// challenge.
|
||||
//
|
||||
// If you already have a TLS config you want to use, you can
|
||||
// simply set its GetCertificate field to cfg.GetCertificate.
|
||||
//
|
||||
// Calling this function signifies your acceptance to
|
||||
// the CA's Subscriber Agreement and/or Terms of Service.
|
||||
func Manage(domainNames []string) (cfg *Config, err error) {
|
||||
return manageWithDefaultConfig(domainNames, false)
|
||||
}
|
||||
|
||||
// manageWithDefaultConfig returns a TLS configuration that
|
||||
// is fully managed for the given names, optionally
|
||||
// with the HTTP challenge disabled.
|
||||
func manageWithDefaultConfig(domainNames []string, disableHTTPChallenge bool) (*Config, error) {
|
||||
cfg := NewDefault()
|
||||
cfg.DisableHTTPChallenge = disableHTTPChallenge
|
||||
return cfg, cfg.Manage(domainNames)
|
||||
}
|
||||
|
||||
// Locker facilitates synchronization of certificate tasks across
|
||||
// machines and networks.
|
||||
type Locker interface {
|
||||
// TryLock will attempt to acquire the lock for key. If a
|
||||
// lock could be obtained, nil values are returned as no
|
||||
// waiting is required. If not (meaning another process is
|
||||
// already working on key), a Waiter value will be returned,
|
||||
// upon which you should Wait() until it is finished.
|
||||
//
|
||||
// The key should be a carefully-chosen value that uniquely
|
||||
// and precisely identifies the operation being locked. For
|
||||
// example, if it is for a certificate obtain or renew with
|
||||
// the ACME protocol to the same CA endpoint (remembering
|
||||
// that an obtain and renew are the same according to ACME,
|
||||
// thus both obtain and renew should share a lock key), a
|
||||
// good key would identify that operation by some name,
|
||||
// concatenated with the domain name and the CA endpoint.
|
||||
//
|
||||
// TryLock never blocks; it always returns without waiting.
|
||||
//
|
||||
// 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(key string) (Waiter, error)
|
||||
|
||||
// Unlock releases the lock for key. This method must ONLY be
|
||||
// called after a successful call to TryLock where no Waiter was
|
||||
// returned, and only after the operation requiring the lock is
|
||||
// finished, even if it returned an error or timed out. Unlock
|
||||
// should also clean up any unused resources allocated during
|
||||
// TryLock.
|
||||
Unlock(key string) error
|
||||
}
|
||||
|
||||
// Waiter is a type that can block until a lock is released.
|
||||
type Waiter interface {
|
||||
Wait()
|
||||
}
|
||||
|
||||
// OnDemandConfig contains some state relevant for providing
|
||||
// on-demand TLS.
|
||||
type OnDemandConfig struct {
|
||||
// If set, this function will be the absolute
|
||||
// authority on whether the hostname (according
|
||||
// to SNI) is allowed to try to get a cert.
|
||||
DecisionFunc func(name string) error
|
||||
|
||||
// If no DecisionFunc is set, this whitelist
|
||||
// is the absolute authority as to whether
|
||||
// a certificate should be allowed to be tried.
|
||||
// Names are compared against SNI value.
|
||||
HostWhitelist []string
|
||||
|
||||
// If no DecisionFunc or HostWhitelist are set,
|
||||
// then an HTTP request will be made to AskURL
|
||||
// to determine if a certificate should be
|
||||
// obtained. If the request fails or the response
|
||||
// is anything other than 2xx status code, the
|
||||
// issuance will be denied.
|
||||
AskURL *url.URL
|
||||
|
||||
// If no DecisionFunc, HostWhitelist, or AskURL
|
||||
// are set, then only this many certificates may
|
||||
// be obtained on-demand; this field is required
|
||||
// if all others are empty, otherwise, all cert
|
||||
// issuances will fail.
|
||||
MaxObtain int32
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Allowed returns whether the issuance for name is allowed according to o.
|
||||
func (o *OnDemandConfig) Allowed(name string) error {
|
||||
// The decision function has absolute authority, if set
|
||||
if o.DecisionFunc != nil {
|
||||
return o.DecisionFunc(name)
|
||||
}
|
||||
|
||||
// Otherwise, the host whitelist has decision authority
|
||||
if len(o.HostWhitelist) > 0 {
|
||||
return o.checkWhitelistForObtainingNewCerts(name)
|
||||
}
|
||||
|
||||
// Otherwise, a URL is checked for permission to issue this cert
|
||||
if o.AskURL != nil {
|
||||
return o.checkURLForObtainingNewCerts(name)
|
||||
}
|
||||
|
||||
// Otherwise use the limit defined by the "max_certs" setting
|
||||
return o.checkLimitsForObtainingNewCerts(name)
|
||||
}
|
||||
|
||||
func (o *OnDemandConfig) whitelistContains(name string) bool {
|
||||
for _, n := range o.HostWhitelist {
|
||||
if strings.ToLower(n) == strings.ToLower(name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *OnDemandConfig) checkWhitelistForObtainingNewCerts(name string) error {
|
||||
if !o.whitelistContains(name) {
|
||||
return fmt.Errorf("%s: name is not whitelisted", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OnDemandConfig) checkURLForObtainingNewCerts(name string) error {
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return fmt.Errorf("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 = *o.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", o.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, o.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 (o *OnDemandConfig) checkLimitsForObtainingNewCerts(name string) error {
|
||||
if o.MaxObtain == 0 {
|
||||
return fmt.Errorf("%s: no certificates allowed to be issued on-demand", name)
|
||||
}
|
||||
|
||||
// User can set hard limit for number of certs for the process to issue
|
||||
if o.MaxObtain > 0 &&
|
||||
atomic.LoadInt32(&o.obtainedCount) >= o.MaxObtain {
|
||||
return fmt.Errorf("%s: maximum certificates issued (%d)", name, o.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(&o.obtainedCount) >= 10 && since < 10*time.Minute {
|
||||
return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
|
||||
}
|
||||
|
||||
// Good to go 👍
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// isLoopback returns true if the hostname of addr looks
|
||||
// explicitly like a common local hostname. addr must only
|
||||
// be a host or a host:port combination.
|
||||
func isLoopback(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(strings.ToLower(addr))
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is only a hostname
|
||||
}
|
||||
return host == "localhost" ||
|
||||
strings.Trim(host, "[]") == "::1" ||
|
||||
strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// isInternal returns true if the IP of addr
|
||||
// belongs to a private network IP range. addr
|
||||
// must only be an IP or an IP:port combination.
|
||||
// Loopback addresses are considered false.
|
||||
func isInternal(addr string) bool {
|
||||
privateNetworks := []string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"fc00::/7",
|
||||
}
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is just a hostname, missing port
|
||||
// if we encounter an error, the brackets need to be stripped
|
||||
// because SplitHostPort didn't do it for us
|
||||
host = strings.Trim(host, "[]")
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
for _, privateNetwork := range privateNetworks {
|
||||
_, ipnet, _ := net.ParseCIDR(privateNetwork)
|
||||
if ipnet.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Package defaults
|
||||
var (
|
||||
// The endpoint of the directory for the ACME
|
||||
// CA we are to use
|
||||
CA = LetsEncryptProductionCA
|
||||
|
||||
// The email address to use when creating or
|
||||
// selecting an existing ACME server account
|
||||
Email string
|
||||
|
||||
// The synchronization implementation - all
|
||||
// instances of certmagic in a cluster must
|
||||
// use the same value here, otherwise some
|
||||
// cert operations will not be properly
|
||||
// coordinated
|
||||
Sync Locker
|
||||
|
||||
// Set to true if agreed to the CA's
|
||||
// subscriber agreement
|
||||
Agreed bool
|
||||
|
||||
// Disable all HTTP challenges
|
||||
DisableHTTPChallenge bool
|
||||
|
||||
// Disable all TLS-ALPN challenges
|
||||
DisableTLSALPNChallenge bool
|
||||
|
||||
// How long before expiration to renew certificates
|
||||
RenewDurationBefore = DefaultRenewDurationBefore
|
||||
|
||||
// How long before expiration to require a renewed
|
||||
// certificate when in interactive mode, like when
|
||||
// the program is first starting up (see
|
||||
// mholt/caddy#1680). A wider window between
|
||||
// RenewDurationBefore and this value will suppress
|
||||
// errors under duress (bad) but hopefully this duration
|
||||
// will give it enough time for the blockage to be
|
||||
// relieved.
|
||||
RenewDurationBeforeAtStartup = DefaultRenewDurationBeforeAtStartup
|
||||
|
||||
// An optional event callback clients can set
|
||||
// to subscribe to certain things happening
|
||||
// internally by this config; invocations are
|
||||
// synchronous, so make them return quickly!
|
||||
OnEvent func(event string, data interface{})
|
||||
|
||||
// The host (ONLY the host, not port) to listen
|
||||
// on if necessary to start a listener to solve
|
||||
// an ACME challenge
|
||||
ListenHost string
|
||||
|
||||
// The alternate port to use for the ACME HTTP
|
||||
// challenge; if non-empty, this port will be
|
||||
// used instead of HTTPChallengePort to spin up
|
||||
// a listener for the HTTP challenge
|
||||
AltHTTPPort int
|
||||
|
||||
// The alternate port to use for the ACME
|
||||
// TLS-ALPN challenge; the system must forward
|
||||
// TLSALPNChallengePort to this port for
|
||||
// challenge to succeed
|
||||
AltTLSALPNPort int
|
||||
|
||||
// The DNS provider to use when solving the
|
||||
// ACME DNS challenge
|
||||
DNSProvider challenge.Provider
|
||||
|
||||
// The type of key to use when generating
|
||||
// certificates
|
||||
KeyType = certcrypto.RSA2048
|
||||
|
||||
// The state needed to operate on-demand TLS
|
||||
OnDemand *OnDemandConfig
|
||||
|
||||
// Add the must staple TLS extension to the
|
||||
// CSR generated by lego/acme
|
||||
MustStaple bool
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
// Some well-known CA endpoints available to use.
|
||||
const (
|
||||
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
)
|
||||
|
||||
// Port variables must remain their defaults unless you
|
||||
// forward packets from the defaults to whatever these
|
||||
// are set to; otherwise ACME challenges will fail.
|
||||
var (
|
||||
// HTTPPort is the port on which to serve HTTP.
|
||||
HTTPPort = 80
|
||||
|
||||
// HTTPSPort is the port on which to serve HTTPS.
|
||||
HTTPSPort = 443
|
||||
)
|
||||
|
||||
// Variables for conveniently serving HTTPS
|
||||
var (
|
||||
httpLn, httpsLn net.Listener
|
||||
lnMu sync.Mutex
|
||||
httpWg sync.WaitGroup
|
||||
)
|
378
vendor/github.com/mholt/certmagic/client.go
generated
vendored
Normal file
378
vendor/github.com/mholt/certmagic/client.go
generated
vendored
Normal file
@ -0,0 +1,378 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/certificate"
|
||||
"github.com/xenolf/lego/challenge"
|
||||
"github.com/xenolf/lego/challenge/http01"
|
||||
"github.com/xenolf/lego/challenge/tlsalpn01"
|
||||
"github.com/xenolf/lego/lego"
|
||||
"github.com/xenolf/lego/registration"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
config *Config
|
||||
acmeClient *lego.Client
|
||||
}
|
||||
|
||||
// listenerAddressInUse returns true if a TCP connection
|
||||
// can be made to addr within a short time interval.
|
||||
func listenerAddressInUse(addr string) bool {
|
||||
conn, err := net.DialTimeout("tcp", addr, 250*time.Millisecond)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
}
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) {
|
||||
// look up or create the user account
|
||||
leUser, err := cfg.getUser(cfg.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure key type is set
|
||||
keyType := KeyType
|
||||
if cfg.KeyType != "" {
|
||||
keyType = cfg.KeyType
|
||||
}
|
||||
|
||||
// ensure CA URL (directory endpoint) is set
|
||||
caURL := CA
|
||||
if cfg.CA != "" {
|
||||
caURL = cfg.CA
|
||||
}
|
||||
|
||||
// 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" && !isLoopback(u.Host) && !isInternal(u.Host) {
|
||||
return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL)
|
||||
}
|
||||
|
||||
clientKey := caURL + leUser.Email + string(keyType)
|
||||
|
||||
// if an underlying client with this configuration already exists, reuse it
|
||||
cfg.acmeClientsMu.Lock()
|
||||
client, ok := cfg.acmeClients[clientKey]
|
||||
if !ok {
|
||||
// the client facilitates our communication with the CA server
|
||||
legoCfg := lego.NewConfig(&leUser)
|
||||
legoCfg.CADirURL = caURL
|
||||
legoCfg.KeyType = keyType
|
||||
legoCfg.UserAgent = UserAgent
|
||||
legoCfg.HTTPClient.Timeout = HTTPTimeout
|
||||
client, err = lego.NewClient(legoCfg)
|
||||
if err != nil {
|
||||
cfg.acmeClientsMu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
cfg.acmeClients[clientKey] = client
|
||||
}
|
||||
cfg.acmeClientsMu.Unlock()
|
||||
|
||||
// if not registered, the user must register an account
|
||||
// with the CA and agree to terms
|
||||
if leUser.Registration == nil {
|
||||
if interactive { // can't prompt a user who isn't there
|
||||
termsURL := client.GetToSURL()
|
||||
if !cfg.Agreed && termsURL != "" {
|
||||
cfg.Agreed = cfg.askUserAgreement(client.GetToSURL())
|
||||
}
|
||||
if !cfg.Agreed && termsURL != "" {
|
||||
return nil, fmt.Errorf("user must agree to CA terms")
|
||||
}
|
||||
}
|
||||
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: cfg.Agreed})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("registration error: %v", err)
|
||||
}
|
||||
leUser.Registration = reg
|
||||
|
||||
// persist the user to storage
|
||||
err = cfg.saveUser(leUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not save user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
c := &acmeClient{
|
||||
config: cfg,
|
||||
acmeClient: client,
|
||||
}
|
||||
|
||||
if cfg.DNSProvider == nil {
|
||||
// Use HTTP and TLS-ALPN challenges by default
|
||||
|
||||
// figure out which ports we'll be serving the challenges on
|
||||
useHTTPPort := HTTPChallengePort
|
||||
useTLSALPNPort := TLSALPNChallengePort
|
||||
if cfg.AltHTTPPort > 0 {
|
||||
useHTTPPort = cfg.AltHTTPPort
|
||||
}
|
||||
if cfg.AltTLSALPNPort > 0 {
|
||||
useTLSALPNPort = cfg.AltTLSALPNPort
|
||||
}
|
||||
|
||||
// If this machine is already listening on the HTTP or TLS-ALPN port
|
||||
// designated for the challenges, then we need to handle the challenges
|
||||
// a little differently: for HTTP, we will answer the challenge request
|
||||
// using our own HTTP handler (the HandleHTTPChallenge function - this
|
||||
// works only because challenge info is written to storage associated
|
||||
// with cfg when the challenge is initiated); for TLS-ALPN, we will add
|
||||
// the challenge cert to our cert cache and serve it up during the
|
||||
// handshake. As for the default solvers... we are careful to honor the
|
||||
// listener bind preferences by using cfg.ListenHost.
|
||||
var httpSolver, alpnSolver challenge.Provider
|
||||
httpSolver = http01.NewProviderServer(cfg.ListenHost, fmt.Sprintf("%d", useHTTPPort))
|
||||
alpnSolver = tlsalpn01.NewProviderServer(cfg.ListenHost, fmt.Sprintf("%d", useTLSALPNPort))
|
||||
if listenerAddressInUse(net.JoinHostPort(cfg.ListenHost, fmt.Sprintf("%d", useHTTPPort))) {
|
||||
httpSolver = nil
|
||||
}
|
||||
if listenerAddressInUse(net.JoinHostPort(cfg.ListenHost, fmt.Sprintf("%d", useTLSALPNPort))) {
|
||||
alpnSolver = tlsALPNSolver{certCache: cfg.certCache}
|
||||
}
|
||||
|
||||
// because of our nifty Storage interface, we can distribute the HTTP and
|
||||
// TLS-ALPN challenges across all instances that share the same storage -
|
||||
// in fact, this is required now for successful solving of the HTTP challenge
|
||||
// if the port is already in use, since we must write the challenge info
|
||||
// to storage for the HTTPChallengeHandler to solve it successfully
|
||||
c.acmeClient.Challenge.SetHTTP01Provider(distributedSolver{
|
||||
config: cfg,
|
||||
providerServer: httpSolver,
|
||||
})
|
||||
c.acmeClient.Challenge.SetTLSALPN01Provider(distributedSolver{
|
||||
config: cfg,
|
||||
providerServer: alpnSolver,
|
||||
})
|
||||
|
||||
// disable any challenges that should not be used
|
||||
var disabledChallenges []challenge.Type
|
||||
if cfg.DisableHTTPChallenge {
|
||||
disabledChallenges = append(disabledChallenges, challenge.HTTP01)
|
||||
}
|
||||
if cfg.DisableTLSALPNChallenge {
|
||||
disabledChallenges = append(disabledChallenges, challenge.TLSALPN01)
|
||||
}
|
||||
if len(disabledChallenges) > 0 {
|
||||
c.acmeClient.Challenge.Exclude(disabledChallenges)
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use DNS challenge exclusively
|
||||
c.acmeClient.Challenge.Exclude([]challenge.Type{challenge.HTTP01, challenge.TLSALPN01})
|
||||
c.acmeClient.Challenge.SetDNS01Provider(cfg.DNSProvider)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) lockKey(op, domainName string) string {
|
||||
return fmt.Sprintf("%s:%s:%s", op, domainName, cfg.CA)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if c.config.Sync != nil {
|
||||
lockKey := c.config.lockKey("cert_acme", name)
|
||||
waiter, err := c.config.Sync.TryLock(lockKey)
|
||||
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.config.Sync.Unlock(lockKey); err != nil {
|
||||
log.Printf("[ERROR] Unable to unlock obtain call for %s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{name},
|
||||
Bundle: true,
|
||||
MustStaple: c.config.MustStaple,
|
||||
}
|
||||
acmeMu.Lock()
|
||||
certificate, err := c.acmeClient.Certificate.Obtain(request)
|
||||
acmeMu.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] failed to obtain certificate: %s", name, err)
|
||||
}
|
||||
|
||||
// double-check that we actually got a certificate, in case there's a bug upstream (see issue mholt/caddy#2121)
|
||||
if certificate.Domain == "" || certificate.Certificate == nil {
|
||||
return fmt.Errorf("returned certificate was empty; probably an unchecked error obtaining it")
|
||||
}
|
||||
|
||||
// Success - immediately save the certificate resource
|
||||
err = c.config.saveCertResource(certificate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving assets for %v: %v", name, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if c.config.OnEvent != nil {
|
||||
c.config.OnEvent("acme_cert_obtained", name)
|
||||
}
|
||||
|
||||
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 {
|
||||
if c.config.Sync != nil {
|
||||
lockKey := c.config.lockKey("cert_acme", name)
|
||||
waiter, err := c.config.Sync.TryLock(lockKey)
|
||||
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.config.Sync.Unlock(lockKey); err != nil {
|
||||
log.Printf("[ERROR] Unable to unlock renew call for %s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Prepare for renewal (load PEM cert, key, and meta)
|
||||
certRes, err := c.config.loadCertResource(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform renewal and retry if necessary, but not too many times.
|
||||
var newCertMeta *certificate.Resource
|
||||
var success bool
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
acmeMu.Lock()
|
||||
newCertMeta, err = c.acmeClient.Certificate.Renew(certRes, true, c.config.MustStaple)
|
||||
acmeMu.Unlock()
|
||||
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 = fmt.Errorf("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 fmt.Errorf("too many renewal attempts; last error: %v", err)
|
||||
}
|
||||
|
||||
if c.config.OnEvent != nil {
|
||||
c.config.OnEvent("acme_cert_renewed", name)
|
||||
}
|
||||
|
||||
return c.config.saveCertResource(newCertMeta)
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for name and deletes
|
||||
// it from storage.
|
||||
func (c *acmeClient) Revoke(name string) error {
|
||||
if !c.config.certCache.storage.Exists(prefixSiteKey(c.config.CA, name)) {
|
||||
return fmt.Errorf("private key not found for %s", name)
|
||||
}
|
||||
|
||||
certRes, err := c.config.loadCertResource(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.acmeClient.Certificate.Revoke(certRes.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.config.OnEvent != nil {
|
||||
c.config.OnEvent("acme_cert_revoked", name)
|
||||
}
|
||||
|
||||
err = c.config.certCache.storage.Delete(prefixSiteCert(c.config.CA, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate revoked, but unable to delete certificate file: %v", err)
|
||||
}
|
||||
err = c.config.certCache.storage.Delete(prefixSiteKey(c.config.CA, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate revoked, but unable to delete private key: %v", err)
|
||||
}
|
||||
err = c.config.certCache.storage.Delete(prefixSiteMeta(c.config.CA, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate revoked, but unable to delete certificate metadata: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Some default values passed down to the underlying lego client.
|
||||
var (
|
||||
UserAgent string
|
||||
HTTPTimeout = 30 * time.Second
|
||||
)
|
363
vendor/github.com/mholt/certmagic/config.go
generated
vendored
Normal file
363
vendor/github.com/mholt/certmagic/config.go
generated
vendored
Normal file
@ -0,0 +1,363 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/certcrypto"
|
||||
"github.com/xenolf/lego/challenge"
|
||||
"github.com/xenolf/lego/challenge/tlsalpn01"
|
||||
"github.com/xenolf/lego/lego"
|
||||
)
|
||||
|
||||
// Config configures a certificate manager instance.
|
||||
// An empty Config is not valid: use New() to obtain
|
||||
// a valid Config.
|
||||
type Config struct {
|
||||
// The endpoint of the directory for the ACME
|
||||
// CA we are to use
|
||||
CA string
|
||||
|
||||
// The email address to use when creating or
|
||||
// selecting an existing ACME server account
|
||||
Email string
|
||||
|
||||
// The synchronization implementation - although
|
||||
// it is not strictly required to have a Sync
|
||||
// value in general, all instances running in
|
||||
// in a cluster for the same domain names must
|
||||
// specify a Sync and use the same one, otherwise
|
||||
// some cert operations will not be properly
|
||||
// coordinated
|
||||
Sync Locker
|
||||
|
||||
// Set to true if agreed to the CA's
|
||||
// subscriber agreement
|
||||
Agreed bool
|
||||
|
||||
// Disable all HTTP challenges
|
||||
DisableHTTPChallenge bool
|
||||
|
||||
// Disable all TLS-ALPN challenges
|
||||
DisableTLSALPNChallenge bool
|
||||
|
||||
// How long before expiration to renew certificates
|
||||
RenewDurationBefore time.Duration
|
||||
|
||||
// How long before expiration to require a renewed
|
||||
// certificate when in interactive mode, like when
|
||||
// the program is first starting up (see
|
||||
// mholt/caddy#1680). A wider window between
|
||||
// RenewDurationBefore and this value will suppress
|
||||
// errors under duress (bad) but hopefully this duration
|
||||
// will give it enough time for the blockage to be
|
||||
// relieved.
|
||||
RenewDurationBeforeAtStartup time.Duration
|
||||
|
||||
// An optional event callback clients can set
|
||||
// to subscribe to certain things happening
|
||||
// internally by this config; invocations are
|
||||
// synchronous, so make them return quickly!
|
||||
OnEvent func(event string, data interface{})
|
||||
|
||||
// The host (ONLY the host, not port) to listen
|
||||
// on if necessary to start a listener to solve
|
||||
// an ACME challenge
|
||||
ListenHost string
|
||||
|
||||
// The alternate port to use for the ACME HTTP
|
||||
// challenge; if non-empty, this port will be
|
||||
// used instead of HTTPChallengePort to spin up
|
||||
// a listener for the HTTP challenge
|
||||
AltHTTPPort int
|
||||
|
||||
// The alternate port to use for the ACME
|
||||
// TLS-ALPN challenge; the system must forward
|
||||
// TLSALPNChallengePort to this port for
|
||||
// challenge to succeed
|
||||
AltTLSALPNPort int
|
||||
|
||||
// The DNS provider to use when solving the
|
||||
// ACME DNS challenge
|
||||
DNSProvider challenge.Provider
|
||||
|
||||
// The type of key to use when generating
|
||||
// certificates
|
||||
KeyType certcrypto.KeyType
|
||||
|
||||
// The state needed to operate on-demand TLS
|
||||
OnDemand *OnDemandConfig
|
||||
|
||||
// Add the must staple TLS extension to the
|
||||
// CSR generated by lego/acme
|
||||
MustStaple bool
|
||||
|
||||
// Map of hostname to certificate hash; used
|
||||
// to complete handshakes and serve the right
|
||||
// certificate given SNI
|
||||
certificates map[string]string
|
||||
|
||||
// Pointer to the certificate store to use
|
||||
certCache *Cache
|
||||
|
||||
// Map of client config key to ACME clients
|
||||
// so they can be reused
|
||||
acmeClients map[string]*lego.Client
|
||||
acmeClientsMu *sync.Mutex
|
||||
}
|
||||
|
||||
// NewDefault returns a new, valid, default config.
|
||||
//
|
||||
// Calling this function signifies your acceptance to
|
||||
// the CA's Subscriber Agreement and/or Terms of Service.
|
||||
func NewDefault() *Config {
|
||||
return New(Config{Agreed: true})
|
||||
}
|
||||
|
||||
// New makes a valid config based on cfg and uses
|
||||
// a default certificate cache. All calls to
|
||||
// New() will use the same certificate cache.
|
||||
func New(cfg Config) *Config {
|
||||
return NewWithCache(defaultCache, cfg)
|
||||
}
|
||||
|
||||
// NewWithCache makes a valid new config based on cfg
|
||||
// and uses the provided certificate cache.
|
||||
func NewWithCache(certCache *Cache, cfg Config) *Config {
|
||||
// avoid nil pointers with sensible defaults
|
||||
if certCache == nil {
|
||||
certCache = defaultCache
|
||||
}
|
||||
if certCache.storage == nil {
|
||||
certCache.storage = DefaultStorage
|
||||
}
|
||||
|
||||
// fill in default values
|
||||
if cfg.CA == "" {
|
||||
cfg.CA = CA
|
||||
}
|
||||
if cfg.Email == "" {
|
||||
cfg.Email = Email
|
||||
}
|
||||
if cfg.OnDemand == nil {
|
||||
cfg.OnDemand = OnDemand
|
||||
}
|
||||
if !cfg.Agreed {
|
||||
cfg.Agreed = Agreed
|
||||
}
|
||||
if !cfg.DisableHTTPChallenge {
|
||||
cfg.DisableHTTPChallenge = DisableHTTPChallenge
|
||||
}
|
||||
if !cfg.DisableTLSALPNChallenge {
|
||||
cfg.DisableTLSALPNChallenge = DisableTLSALPNChallenge
|
||||
}
|
||||
if cfg.RenewDurationBefore == 0 {
|
||||
cfg.RenewDurationBefore = RenewDurationBefore
|
||||
}
|
||||
if cfg.RenewDurationBeforeAtStartup == 0 {
|
||||
cfg.RenewDurationBeforeAtStartup = RenewDurationBeforeAtStartup
|
||||
}
|
||||
if cfg.OnEvent == nil {
|
||||
cfg.OnEvent = OnEvent
|
||||
}
|
||||
if cfg.ListenHost == "" {
|
||||
cfg.ListenHost = ListenHost
|
||||
}
|
||||
if cfg.AltHTTPPort == 0 {
|
||||
cfg.AltHTTPPort = AltHTTPPort
|
||||
}
|
||||
if cfg.AltTLSALPNPort == 0 {
|
||||
cfg.AltTLSALPNPort = AltTLSALPNPort
|
||||
}
|
||||
if cfg.DNSProvider == nil {
|
||||
cfg.DNSProvider = DNSProvider
|
||||
}
|
||||
if cfg.KeyType == "" {
|
||||
cfg.KeyType = KeyType
|
||||
}
|
||||
if cfg.OnDemand == nil {
|
||||
cfg.OnDemand = OnDemand
|
||||
}
|
||||
if !cfg.MustStaple {
|
||||
cfg.MustStaple = MustStaple
|
||||
}
|
||||
|
||||
// if no sync facility is provided, we'll default to
|
||||
// a file system synchronizer backed by the storage
|
||||
// given to certCache (if it is one), or just a simple
|
||||
// in-memory sync facility otherwise (strictly speaking,
|
||||
// a sync is not required; only if running multiple
|
||||
// instances for the same domain names concurrently)
|
||||
if cfg.Sync == nil {
|
||||
if ccfs, ok := certCache.storage.(FileStorage); ok {
|
||||
cfg.Sync = NewFileStorageLocker(ccfs)
|
||||
} else {
|
||||
cfg.Sync = NewMemoryLocker()
|
||||
}
|
||||
}
|
||||
|
||||
// ensure the unexported fields are valid
|
||||
cfg.certificates = make(map[string]string)
|
||||
cfg.certCache = certCache
|
||||
cfg.acmeClients = make(map[string]*lego.Client)
|
||||
cfg.acmeClientsMu = new(sync.Mutex)
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
// Manage causes the certificates for domainNames to be managed
|
||||
// according to cfg.
|
||||
func (cfg *Config) Manage(domainNames []string) error {
|
||||
for _, domainName := range domainNames {
|
||||
// if on-demand is configured, simply whitelist this name
|
||||
if cfg.OnDemand != nil {
|
||||
if !cfg.OnDemand.whitelistContains(domainName) {
|
||||
cfg.OnDemand.HostWhitelist = append(cfg.OnDemand.HostWhitelist, domainName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// try loading an existing certificate; if it doesn't
|
||||
// exist yet, obtain one and try loading it again
|
||||
cert, err := cfg.CacheManagedCertificate(domainName)
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
// if it doesn't exist, get it, then try loading it again
|
||||
err := cfg.ObtainCert(domainName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: obtaining certificate: %v", domainName, err)
|
||||
}
|
||||
cert, err = cfg.CacheManagedCertificate(domainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: caching certificate after obtaining it: %v", domainName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s: caching certificate: %v", domainName, err)
|
||||
}
|
||||
|
||||
// for existing certificates, make sure it is renewed
|
||||
if cert.NeedsRenewal() {
|
||||
err := cfg.RenewCert(domainName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: renewing certificate: %v", domainName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for name using cfg, as long
|
||||
// as a certificate does not already exist in storage for that
|
||||
// name. The name must qualify and cfg must be flagged as Managed.
|
||||
// This function is a no-op if storage already has a certificate
|
||||
// for name.
|
||||
//
|
||||
// It only obtains and stores certificates (and their keys),
|
||||
// it does not load them into memory. If interactive is true,
|
||||
// the user may be shown a prompt.
|
||||
func (cfg *Config) ObtainCert(name string, interactive bool) error {
|
||||
skip, err := cfg.preObtainOrRenewChecks(name, interactive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we expect this to be a new site; if the
|
||||
// cert already exists, then no-op
|
||||
if cfg.certCache.storage.Exists(prefixSiteCert(cfg.CA, name)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := cfg.newACMEClient(interactive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Obtain(name)
|
||||
}
|
||||
|
||||
// RenewCert renews the certificate for name using cfg. It stows the
|
||||
// renewed certificate and its assets in storage if successful.
|
||||
func (cfg *Config) RenewCert(name string, interactive bool) error {
|
||||
skip, err := cfg.preObtainOrRenewChecks(name, interactive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
client, err := cfg.newACMEClient(interactive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Renew(name)
|
||||
}
|
||||
|
||||
// RevokeCert revokes the certificate for domain via ACME protocol.
|
||||
func (cfg *Config) RevokeCert(domain string, interactive bool) error {
|
||||
client, err := cfg.newACMEClient(interactive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Revoke(domain)
|
||||
}
|
||||
|
||||
// TLSConfig returns a TLS configuration that
|
||||
// can be used to configure TLS listeners. It
|
||||
// supports the TLS-ALPN challenge and serves
|
||||
// up certificates managed by cfg.
|
||||
func (cfg *Config) TLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
GetCertificate: cfg.GetCertificate,
|
||||
NextProtos: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol},
|
||||
}
|
||||
}
|
||||
|
||||
// RenewAllCerts triggers a renewal check of all
|
||||
// certificates in the cache. It only renews
|
||||
// certificates if they need to be renewed.
|
||||
// func (cfg *Config) RenewAllCerts(interactive bool) error {
|
||||
// return cfg.certCache.RenewManagedCertificates(interactive)
|
||||
// }
|
||||
|
||||
// preObtainOrRenewChecks perform a few simple checks before
|
||||
// obtaining or renewing a certificate with ACME, and returns
|
||||
// whether this name should be skipped (like if it's not
|
||||
// managed TLS) as well as any error. It ensures that the
|
||||
// config is Managed, that the name qualifies for a certificate,
|
||||
// and that an email address is available.
|
||||
func (cfg *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool, error) {
|
||||
if !HostQualifies(name) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if cfg.Email == "" {
|
||||
var err error
|
||||
cfg.Email, err = cfg.getEmail(allowPrompts)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
155
vendor/github.com/mholt/certmagic/crypto.go
generated
vendored
Normal file
155
vendor/github.com/mholt/certmagic/crypto.go
generated
vendored
Normal file
@ -0,0 +1,155 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/xenolf/lego/certificate"
|
||||
)
|
||||
|
||||
// encodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes.
|
||||
func encodePrivateKey(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
|
||||
}
|
||||
|
||||
// decodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
|
||||
func decodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) {
|
||||
keyBlock, _ := pem.Decode(keyPEMBytes)
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown private key type")
|
||||
}
|
||||
|
||||
// parseCertsFromPEMBundle parses a certificate bundle from top to bottom and returns
|
||||
// a slice of x509 certificates. This function will error if no certificates are found.
|
||||
func parseCertsFromPEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||
var certificates []*x509.Certificate
|
||||
var certDERBlock *pem.Block
|
||||
for {
|
||||
certDERBlock, bundle = pem.Decode(bundle)
|
||||
if certDERBlock == nil {
|
||||
break
|
||||
}
|
||||
if certDERBlock.Type == "CERTIFICATE" {
|
||||
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificates = append(certificates, cert)
|
||||
}
|
||||
}
|
||||
if len(certificates) == 0 {
|
||||
return nil, fmt.Errorf("no certificates found in bundle")
|
||||
}
|
||||
return certificates, nil
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// saveCertResource saves the certificate resource to disk. This
|
||||
// includes the certificate file itself, the private key, and the
|
||||
// metadata file.
|
||||
func (cfg *Config) saveCertResource(cert *certificate.Resource) error {
|
||||
metaBytes, err := json.MarshalIndent(&cert, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding certificate metadata: %v", err)
|
||||
}
|
||||
|
||||
all := []keyValue{
|
||||
{
|
||||
key: prefixSiteCert(cfg.CA, cert.Domain),
|
||||
value: cert.Certificate,
|
||||
},
|
||||
{
|
||||
key: prefixSiteKey(cfg.CA, cert.Domain),
|
||||
value: cert.PrivateKey,
|
||||
},
|
||||
{
|
||||
key: prefixSiteMeta(cfg.CA, cert.Domain),
|
||||
value: metaBytes,
|
||||
},
|
||||
}
|
||||
|
||||
return storeTx(cfg.certCache.storage, all)
|
||||
}
|
||||
|
||||
func (cfg *Config) loadCertResource(domain string) (certificate.Resource, error) {
|
||||
var certRes certificate.Resource
|
||||
certBytes, err := cfg.certCache.storage.Load(prefixSiteCert(cfg.CA, domain))
|
||||
if err != nil {
|
||||
return certRes, err
|
||||
}
|
||||
keyBytes, err := cfg.certCache.storage.Load(prefixSiteKey(cfg.CA, domain))
|
||||
if err != nil {
|
||||
return certRes, err
|
||||
}
|
||||
metaBytes, err := cfg.certCache.storage.Load(prefixSiteMeta(cfg.CA, domain))
|
||||
if err != nil {
|
||||
return certRes, err
|
||||
}
|
||||
err = json.Unmarshal(metaBytes, &certRes)
|
||||
if err != nil {
|
||||
return certRes, fmt.Errorf("decoding certificate metadata: %v", err)
|
||||
}
|
||||
certRes.Certificate = certBytes
|
||||
certRes.PrivateKey = keyBytes
|
||||
return certRes, 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))
|
||||
}
|
126
vendor/github.com/mholt/certmagic/filestorage.go
generated
vendored
Normal file
126
vendor/github.com/mholt/certmagic/filestorage.go
generated
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Exists returns true if key exists in fs.
|
||||
func (fs FileStorage) Exists(key string) bool {
|
||||
_, err := os.Stat(fs.filename(key))
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// Store saves value at key.
|
||||
func (fs FileStorage) Store(key string, value []byte) error {
|
||||
filename := fs.filename(key)
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filename, value, 0600)
|
||||
}
|
||||
|
||||
// Load retrieves the value at key.
|
||||
func (fs FileStorage) Load(key string) ([]byte, error) {
|
||||
contents, err := ioutil.ReadFile(fs.filename(key))
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrNotExist(err)
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
// Delete deletes the value at key.
|
||||
// TODO: Delete any empty folders caused by this operation
|
||||
func (fs FileStorage) Delete(key string) error {
|
||||
err := os.Remove(fs.filename(key))
|
||||
if os.IsNotExist(err) {
|
||||
return ErrNotExist(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// List returns all keys that match prefix.
|
||||
func (fs FileStorage) List(prefix string) ([]string, error) {
|
||||
d, err := os.Open(fs.filename(prefix))
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrNotExist(err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer d.Close()
|
||||
return d.Readdirnames(-1)
|
||||
}
|
||||
|
||||
// Stat returns information about key.
|
||||
func (fs FileStorage) Stat(key string) (KeyInfo, error) {
|
||||
fi, err := os.Stat(fs.filename(key))
|
||||
if os.IsNotExist(err) {
|
||||
return KeyInfo{}, ErrNotExist(err)
|
||||
}
|
||||
if err != nil {
|
||||
return KeyInfo{}, err
|
||||
}
|
||||
return KeyInfo{
|
||||
Key: key,
|
||||
Modified: fi.ModTime(),
|
||||
Size: fi.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fs FileStorage) filename(key string) string {
|
||||
return filepath.Join(fs.Path, filepath.FromSlash(key))
|
||||
}
|
||||
|
||||
// homeDir returns the best guess of the current user's home
|
||||
// directory from environment variables. If unknown, "." (the
|
||||
// current directory) is returned instead.
|
||||
func homeDir() string {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" && runtime.GOOS == "windows" {
|
||||
drive := os.Getenv("HOMEDRIVE")
|
||||
path := os.Getenv("HOMEPATH")
|
||||
home = drive + path
|
||||
if drive == "" || path == "" {
|
||||
home = os.Getenv("USERPROFILE")
|
||||
}
|
||||
}
|
||||
if home == "" {
|
||||
home = "."
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
||||
func dataDir() string {
|
||||
baseDir := filepath.Join(homeDir(), ".local", "share")
|
||||
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
|
||||
baseDir = xdgData
|
||||
}
|
||||
return filepath.Join(baseDir, "certmagic")
|
||||
}
|
||||
|
||||
var _ Storage = FileStorage{}
|
98
caddytls/filestoragesync.go → vendor/github.com/mholt/certmagic/filestoragesync.go
generated
vendored
98
caddytls/filestoragesync.go → vendor/github.com/mholt/certmagic/filestoragesync.go
generated
vendored
@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -12,59 +12,56 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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)
|
||||
}
|
||||
})
|
||||
// FileStorageLocker implements the Locker interface
|
||||
// using the file system. An empty value is NOT VALID,
|
||||
// so you must use NewFileStorageLocker() to get one.
|
||||
type FileStorageLocker struct {
|
||||
fs FileStorage
|
||||
}
|
||||
|
||||
// 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
|
||||
// NewFileStorageLocker returns a valid Locker backed by fs.
|
||||
func NewFileStorageLocker(fs FileStorage) *FileStorageLocker {
|
||||
return &FileStorageLocker{fs: fs}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func (l *FileStorageLocker) TryLock(name string) (Waiter, error) {
|
||||
fileStorageNameLocksMu.Lock()
|
||||
defer fileStorageNameLocksMu.Unlock()
|
||||
|
||||
// see if lock already exists within this process
|
||||
fw, ok := fileStorageNameLocks[s.caURL+name]
|
||||
fw, ok := fileStorageNameLocks[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 {
|
||||
lockDir := l.lockDir()
|
||||
if err := os.MkdirAll(lockDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fw = &FileStorageWaiter{
|
||||
filename: filepath.Join(lockDir, safeKey(name)+".lock"),
|
||||
wg: new(sync.WaitGroup),
|
||||
}
|
||||
|
||||
// create the file in a special mode such that an
|
||||
// error is returned if it already exists
|
||||
lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
@ -78,50 +75,59 @@ func (s *fileStorageLock) TryLock(name string) (Waiter, error) {
|
||||
|
||||
// looks like we get the lock
|
||||
fw.wg.Add(1)
|
||||
fileStorageNameLocks[s.caURL+name] = fw
|
||||
fileStorageNameLocks[name] = fw
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Unlock unlocks name.
|
||||
func (s *fileStorageLock) Unlock(name string) error {
|
||||
// Unlock releases the lock for name.
|
||||
func (l *FileStorageLocker) Unlock(name string) error {
|
||||
fileStorageNameLocksMu.Lock()
|
||||
defer fileStorageNameLocksMu.Unlock()
|
||||
fw, ok := fileStorageNameLocks[s.caURL+name]
|
||||
|
||||
fw, ok := fileStorageNameLocks[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("FileStorage: no lock to release for %s", name)
|
||||
return fmt.Errorf("FileStorageLocker: 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)
|
||||
dir, err := os.Open(l.lockDir()) // OK to ignore error here
|
||||
if err == nil {
|
||||
items, _ := dir.Readdirnames(3) // OK to ignore error here
|
||||
if len(items) == 0 {
|
||||
os.Remove(lockParentFolder)
|
||||
os.Remove(dir.Name())
|
||||
}
|
||||
dir.Close()
|
||||
}
|
||||
|
||||
// clean up in memory
|
||||
fw.wg.Done()
|
||||
delete(fileStorageNameLocks, s.caURL+name)
|
||||
delete(fileStorageNameLocks, 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 {
|
||||
func (l *FileStorageLocker) lockDir() string {
|
||||
return filepath.Join(l.fs.Path, "locks")
|
||||
}
|
||||
|
||||
// FileStorageWaiter waits for a file to disappear; it
|
||||
// polls the file system to check for the existence of
|
||||
// a file. It also uses a WaitGroup to optimize the
|
||||
// polling in the case when this process is the only
|
||||
// one waiting. (Other processes that are waiting
|
||||
// for the lock will still block, but must wait
|
||||
// for the poll intervals to get their answer.)
|
||||
type FileStorageWaiter struct {
|
||||
filename string
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// Wait waits until the lock is released.
|
||||
func (fw *fileWaiter) Wait() {
|
||||
func (fw *FileStorageWaiter) Wait() {
|
||||
start := time.Now()
|
||||
fw.wg.Wait()
|
||||
for time.Since(start) < 1*time.Hour {
|
||||
@ -133,8 +139,8 @@ func (fw *fileWaiter) Wait() {
|
||||
}
|
||||
}
|
||||
|
||||
var fileStorageNameLocks = make(map[string]*fileWaiter) // keyed by CA + name
|
||||
var fileStorageNameLocks = make(map[string]*FileStorageWaiter)
|
||||
var fileStorageNameLocksMu sync.Mutex
|
||||
|
||||
var _ Locker = &fileStorageLock{}
|
||||
var _ Waiter = &fileWaiter{}
|
||||
var _ Locker = &FileStorageLocker{}
|
||||
var _ Waiter = &FileStorageWaiter{}
|
400
vendor/github.com/mholt/certmagic/handshake.go
generated
vendored
Normal file
400
vendor/github.com/mholt/certmagic/handshake.go
generated
vendored
Normal file
@ -0,0 +1,400 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/challenge/tlsalpn01"
|
||||
)
|
||||
|
||||
// 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 cfg.OnEvent != nil {
|
||||
cfg.OnEvent("tls_handshake_started", clientHello)
|
||||
}
|
||||
|
||||
// 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 == tlsalpn01.ACMETLS1Protocol {
|
||||
cfg.certCache.mu.RLock()
|
||||
challengeCert, ok := cfg.certCache.cache[tlsALPNCertKeyName(clientHello.ServerName)]
|
||||
cfg.certCache.mu.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 && cfg.OnEvent != nil {
|
||||
cfg.OnEvent("tls_handshake_completed", clientHello)
|
||||
}
|
||||
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.mu.RLock()
|
||||
defer cfg.certCache.mu.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
|
||||
// mholt/caddy#2035 and mholt/caddy#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 != nil && 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, fmt.Errorf("hostname '%s' does not qualify for certificate", name)
|
||||
}
|
||||
|
||||
// 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 cfg.OnDemand == nil {
|
||||
return fmt.Errorf("not configured for on-demand certificate issuance")
|
||||
}
|
||||
return cfg.OnDemand.Allowed(name)
|
||||
}
|
||||
|
||||
// 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.OnDemand.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 < cfg.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 := cfg.certCache.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.mu.Lock()
|
||||
cfg.certCache.cache[cert.Hash] = cert
|
||||
cfg.certCache.mu.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) {
|
||||
tokenKey := distributedSolver{}.challengeTokensKey(clientHello.ServerName)
|
||||
chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey)
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
return Certificate{}, false, nil
|
||||
}
|
||||
return Certificate{}, false, fmt.Errorf("opening distributed challenge token file %s: %v", tokenKey, err)
|
||||
}
|
||||
|
||||
var chalInfo challengeInfo
|
||||
err = json.Unmarshal(chalInfoBytes, &chalInfo)
|
||||
if err != nil {
|
||||
return Certificate{}, false, fmt.Errorf("decoding challenge token file %s (corrupted?): %v", tokenKey, err)
|
||||
}
|
||||
|
||||
cert, err := tlsalpn01.ChallengeCert(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
|
||||
}
|
||||
|
||||
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
|
||||
var obtainCertWaitChans = make(map[string]chan struct{})
|
||||
var obtainCertWaitChansMu sync.Mutex
|
111
vendor/github.com/mholt/certmagic/httphandler.go
generated
vendored
Normal file
111
vendor/github.com/mholt/certmagic/httphandler.go
generated
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/xenolf/lego/challenge/http01"
|
||||
)
|
||||
|
||||
// HTTPChallengeHandler wraps h in a handler that can solve the ACME
|
||||
// HTTP challenge. cfg is required, and it must have a certificate
|
||||
// cache backed by a functional storage facility, since that is where
|
||||
// the challenge state is stored between initiation and solution.
|
||||
//
|
||||
// If a request is not an ACME HTTP challenge, h willl be invoked.
|
||||
func (cfg *Config) HTTPChallengeHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if cfg.HandleHTTPChallenge(w, r) {
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// HandleHTTPChallenge uses cfg to solve challenge requests from an ACME
|
||||
// server that were initiated by this instance or any other instance in
|
||||
// this cluster (being, any instances using the same storage cfg does).
|
||||
//
|
||||
// If the HTTP challenge is disabled, this function is a no-op.
|
||||
//
|
||||
// If cfg is nil or if cfg does not have a certificate cache backed by
|
||||
// usable storage, solving the HTTP challenge will fail.
|
||||
//
|
||||
// It returns true if it handled the request; if so, the response has
|
||||
// already been written. If false is returned, this call was a no-op and
|
||||
// the request has not been handled.
|
||||
func (cfg *Config) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
|
||||
if cfg == nil {
|
||||
return false
|
||||
}
|
||||
if cfg.DisableHTTPChallenge {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
||||
return false
|
||||
}
|
||||
return cfg.distributedHTTPChallengeSolver(w, r)
|
||||
}
|
||||
|
||||
// distributedHTTPChallengeSolver checks to see if this challenge
|
||||
// request was initiated by this or another instance which uses the
|
||||
// same storage as cfg does, and attempts to complete the challenge for
|
||||
// it. It returns true if the request was handled; false otherwise.
|
||||
func (cfg *Config) distributedHTTPChallengeSolver(w http.ResponseWriter, r *http.Request) bool {
|
||||
if cfg == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
tokenKey := distributedSolver{config: cfg}.challengeTokensKey(r.Host)
|
||||
chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey)
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); !ok {
|
||||
log.Printf("[ERROR][%s] Opening distributed HTTP challenge token file: %v", r.Host, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var chalInfo challengeInfo
|
||||
err = json.Unmarshal(chalInfoBytes, &chalInfo)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR][%s] Decoding challenge token file %s (corrupted?): %v", r.Host, tokenKey, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return answerHTTPChallenge(w, r, chalInfo)
|
||||
}
|
||||
|
||||
// answerHTTPChallenge solves the challenge with chalInfo.
|
||||
// Most of this code borrowed from xenolf/lego's built-in HTTP-01
|
||||
// challenge solver in March 2018.
|
||||
func answerHTTPChallenge(w http.ResponseWriter, r *http.Request, chalInfo challengeInfo) bool {
|
||||
challengeReqPath := http01.ChallengePath(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
|
||||
}
|
||||
|
||||
const challengeBasePath = "/.well-known/acme-challenge"
|
307
vendor/github.com/mholt/certmagic/maintain.go
generated
vendored
Normal file
307
vendor/github.com/mholt/certmagic/maintain.go
generated
vendored
Normal file
@ -0,0 +1,307 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// 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 (certCache *Cache) maintainAssets() {
|
||||
renewalTicker := time.NewTicker(certCache.RenewInterval)
|
||||
ocspTicker := time.NewTicker(certCache.OCSPInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-renewalTicker.C:
|
||||
log.Println("[INFO] Scanning for expiring certificates")
|
||||
err := certCache.RenewManagedCertificates(false)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Renewing managed certificates: %v", err)
|
||||
}
|
||||
log.Println("[INFO] Done checking certificates")
|
||||
case <-ocspTicker.C:
|
||||
log.Println("[INFO] Scanning for stale OCSP staples")
|
||||
certCache.updateOCSPStaples()
|
||||
certCache.deleteOldStapleFiles()
|
||||
log.Println("[INFO] Done checking OCSP staples")
|
||||
case <-certCache.stopChan:
|
||||
renewalTicker.Stop()
|
||||
ocspTicker.Stop()
|
||||
log.Println("[INFO] Stopped certificate maintenance routine")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RenewManagedCertificates renews managed certificates,
|
||||
// including ones loaded on-demand. Note that this is done
|
||||
// automatically on a regular basis; normally you will not
|
||||
// need to call this.
|
||||
func (certCache *Cache) RenewManagedCertificates(interactive bool) error {
|
||||
// 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.mu.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.managed {
|
||||
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
|
||||
if cert.NeedsRenewal() {
|
||||
// see if the certificate in storage has already been renewed, possibly by another
|
||||
// instance 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: 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.mu.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 interactive {
|
||||
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.
|
||||
renewName := oldCert.Names[0]
|
||||
|
||||
// perform renewal
|
||||
err := oldCert.configs[0].RenewCert(renewName, interactive)
|
||||
if err != nil {
|
||||
if interactive {
|
||||
// Certificate renewal failed and the operator is present. See a discussion about
|
||||
// this in issue mholt/caddy#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 withit right now.
|
||||
// Follow-up: See issue mholt/caddy#1680. Only fail in this case if the certificate
|
||||
// is dangerously close to expiration.
|
||||
timeLeft := oldCert.NotAfter.Sub(time.Now().UTC())
|
||||
if timeLeft < oldCert.configs[0].RenewDurationBeforeAtStartup {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Printf("[ERROR] %v", err)
|
||||
if oldCert.configs[0].OnDemand != nil {
|
||||
// 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 interactive {
|
||||
return err // operator is present, so report error immediately
|
||||
}
|
||||
log.Printf("[ERROR] %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Deletion queue
|
||||
for _, cert := range deleteQueue {
|
||||
certCache.mu.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.mu.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 (certCache *Cache) updateOCSPStaples() {
|
||||
// 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.mu.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 := certCache.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.mu.RUnlock()
|
||||
|
||||
// These write locks should be brief since we have all the info we need now.
|
||||
for certKey, update := range updated {
|
||||
certCache.mu.Lock()
|
||||
cert := certCache.cache[certKey]
|
||||
cert.OCSP = update.parsed
|
||||
cert.Certificate.OCSPStaple = update.rawBytes
|
||||
certCache.cache[certKey] = cert
|
||||
certCache.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// deleteOldStapleFiles deletes cached OCSP staples that have expired.
|
||||
// TODO: We should do this for long-expired certificates, too.
|
||||
func (certCache *Cache) deleteOldStapleFiles() {
|
||||
ocspKeys, err := certCache.storage.List(prefixOCSP)
|
||||
if err != nil {
|
||||
// maybe just hasn't been created yet; no big deal
|
||||
return
|
||||
}
|
||||
for _, key := range ocspKeys {
|
||||
ocspBytes, err := certCache.storage.Load(key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] While deleting old OCSP staples, unable to load staple file: %v", err)
|
||||
continue
|
||||
}
|
||||
resp, err := ocsp.ParseResponse(ocspBytes, nil)
|
||||
if err != nil {
|
||||
// contents are invalid; delete it
|
||||
err = certCache.storage.Delete(key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Purging corrupt staple file %s: %v", key, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if time.Now().After(resp.NextUpdate) {
|
||||
// response has expired; delete it
|
||||
err = certCache.storage.Delete(key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Purging expired staple file %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultRenewInterval is how often to check certificates for renewal.
|
||||
DefaultRenewInterval = 12 * time.Hour
|
||||
|
||||
// DefaultRenewDurationBefore is how long before expiration to renew certificates.
|
||||
DefaultRenewDurationBefore = (24 * time.Hour) * 30
|
||||
|
||||
// DefaultRenewDurationBeforeAtStartup is how long before expiration to require
|
||||
// a renewed certificate when the process is first starting up (see mholt/caddy#1680).
|
||||
DefaultRenewDurationBeforeAtStartup = (24 * time.Hour) * 7
|
||||
|
||||
// DefaultOCSPInterval is how often to check if OCSP stapling needs updating.
|
||||
DefaultOCSPInterval = 1 * time.Hour
|
||||
)
|
85
vendor/github.com/mholt/certmagic/memorysync.go
generated
vendored
Normal file
85
vendor/github.com/mholt/certmagic/memorysync.go
generated
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MemoryLocker implements the Locker interface
|
||||
// using memory. An empty value is NOT VALID,
|
||||
// so you must use NewMemoryLocker() to get one.
|
||||
type MemoryLocker struct {
|
||||
nameLocks map[string]*MemoryWaiter
|
||||
nameLocksMu *sync.Mutex
|
||||
}
|
||||
|
||||
// NewMemoryLocker returns a valid Locker backed by fs.
|
||||
func NewMemoryLocker() *MemoryLocker {
|
||||
return &MemoryLocker{
|
||||
nameLocks: make(map[string]*MemoryWaiter),
|
||||
nameLocksMu: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
// TryLock attempts to get a lock for name, otherwise it returns
|
||||
// a Waiter value to wait until the other process is finished.
|
||||
func (l *MemoryLocker) TryLock(name string) (Waiter, error) {
|
||||
l.nameLocksMu.Lock()
|
||||
defer l.nameLocksMu.Unlock()
|
||||
|
||||
// see if lock already exists within this process
|
||||
w, ok := l.nameLocks[name]
|
||||
if ok {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// we got the lock, so create it
|
||||
w = &MemoryWaiter{wg: new(sync.WaitGroup)}
|
||||
w.wg.Add(1)
|
||||
l.nameLocks[name] = w
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Unlock releases the lock for name.
|
||||
func (l *MemoryLocker) Unlock(name string) error {
|
||||
l.nameLocksMu.Lock()
|
||||
defer l.nameLocksMu.Unlock()
|
||||
|
||||
w, ok := l.nameLocks[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("MemoryLocker: no lock to release for %s", name)
|
||||
}
|
||||
|
||||
w.wg.Done()
|
||||
delete(l.nameLocks, name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MemoryWaiter implements Waiter in memory.
|
||||
type MemoryWaiter struct {
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// Wait waits until w.wg is done.
|
||||
func (w *MemoryWaiter) Wait() {
|
||||
w.Wait()
|
||||
}
|
||||
|
||||
var _ Locker = &MemoryLocker{}
|
||||
var _ Waiter = &MemoryWaiter{}
|
209
vendor/github.com/mholt/certmagic/ocsp.go
generated
vendored
Normal file
209
vendor/github.com/mholt/certmagic/ocsp.go
generated
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// 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 (certCache *Cache) stapleOCSP(cert *Certificate, pemBundle []byte) error {
|
||||
if pemBundle == nil {
|
||||
// we need a PEM encoding only for some function calls below
|
||||
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.
|
||||
ocspStapleKey := prefixOCSPStaple(cert, pemBundle)
|
||||
cachedOCSP, err := certCache.storage.Load(ocspStapleKey)
|
||||
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 := certCache.storage.Delete(ocspStapleKey)
|
||||
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 = 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 := certCache.storage.Store(ocspStapleKey, ocspBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write OCSP staple file for %v: %v", cert.Names, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response,
|
||||
// the parsed response, and an error, if any. The returned []byte can be passed directly
|
||||
// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the
|
||||
// issued certificate, this function will try to get the issuer certificate from the
|
||||
// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return
|
||||
// values are nil, the OCSP status may be assumed OCSPUnknown.
|
||||
//
|
||||
// Borrowed from github.com/xenolf/lego
|
||||
func getOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
||||
// TODO: Perhaps this should be synchronized too, with a Locker?
|
||||
|
||||
certificates, err := parseCertsFromPEMBundle(bundle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We expect the certificate slice to be ordered downwards the chain.
|
||||
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
|
||||
// which should always be the first two certificates. If there's no
|
||||
// OCSP server listed in the leaf cert, there's nothing to do. And if
|
||||
// we have only one certificate so far, we need to get the issuer cert.
|
||||
issuedCert := certificates[0]
|
||||
if len(issuedCert.OCSPServer) == 0 {
|
||||
return nil, nil, fmt.Errorf("no OCSP server specified in certificate")
|
||||
}
|
||||
if len(certificates) == 1 {
|
||||
if len(issuedCert.IssuingCertificateURL) == 0 {
|
||||
return nil, nil, fmt.Errorf("no URL to issuing certificate")
|
||||
}
|
||||
|
||||
resp, err := http.Get(issuedCert.IssuingCertificateURL[0])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("getting issuer certificate: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
issuerBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*1024))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading issuer certificate: %v", err)
|
||||
}
|
||||
|
||||
issuerCert, err := x509.ParseCertificate(issuerBytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("parsing issuer certificate: %v", err)
|
||||
}
|
||||
|
||||
// insert it into the slice on position 0;
|
||||
// we want it ordered right SRV CRT -> CA
|
||||
certificates = append(certificates, issuerCert)
|
||||
}
|
||||
|
||||
issuerCert := certificates[1]
|
||||
|
||||
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("creating OCSP request: %v", err)
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(ocspReq)
|
||||
req, err := http.Post(issuedCert.OCSPServer[0], "application/ocsp-request", reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("making OCSP request: %v", err)
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
ocspResBytes, err := ioutil.ReadAll(io.LimitReader(req.Body, 1024*1024))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading OCSP response: %v", err)
|
||||
}
|
||||
|
||||
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("parsing OCSP response: %v", err)
|
||||
}
|
||||
|
||||
return ocspResBytes, ocspRes, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
148
vendor/github.com/mholt/certmagic/solvers.go
generated
vendored
Normal file
148
vendor/github.com/mholt/certmagic/solvers.go
generated
vendored
Normal file
@ -0,0 +1,148 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/xenolf/lego/challenge"
|
||||
"github.com/xenolf/lego/challenge/tlsalpn01"
|
||||
)
|
||||
|
||||
// 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 *Cache
|
||||
}
|
||||
|
||||
// Present adds the challenge certificate to the cache.
|
||||
func (s tlsALPNSolver) Present(domain, token, keyAuth string) error {
|
||||
cert, err := tlsalpn01.ChallengeCert(domain, keyAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certHash := hashCertificateChain(cert.Certificate)
|
||||
s.certCache.mu.Lock()
|
||||
s.certCache.cache[tlsALPNCertKeyName(domain)] = Certificate{
|
||||
Certificate: *cert,
|
||||
Names: []string{domain},
|
||||
Hash: certHash, // perhaps not necesssary
|
||||
}
|
||||
s.certCache.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the challenge certificate from the cache.
|
||||
func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error {
|
||||
s.certCache.mu.Lock()
|
||||
delete(s.certCache.cache, domain)
|
||||
s.certCache.mu.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 the instance which
|
||||
// initiates the challenge shares the same storage and locker with
|
||||
// the others in the cluster. The storage backing the certificate
|
||||
// cache in distributedSolver.config is crucial.
|
||||
//
|
||||
// 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 the challenge token exists in storage, and if so, decode it
|
||||
// and use it to serve up the correct response. HTTPChallengeHandler
|
||||
// in this package as well as the GetCertificate method implemented
|
||||
// by a Config support and even require this behavior.
|
||||
//
|
||||
// In short: the only two requirements for cluster operation are
|
||||
// sharing sync and storage, and using the facilities provided by
|
||||
// this package for solving the challenges.
|
||||
type distributedSolver struct {
|
||||
// The config with a certificate cache
|
||||
// with a reference to the storage to
|
||||
// use which is shared among all the
|
||||
// instances in the cluster - REQUIRED.
|
||||
config *Config
|
||||
|
||||
// Since the distributedSolver is only a
|
||||
// wrapper over an actual solver, place
|
||||
// the actual solver here.
|
||||
providerServer challenge.Provider
|
||||
}
|
||||
|
||||
// Present invokes the underlying solver's Present method
|
||||
// and also stores domain, token, and keyAuth to the storage
|
||||
// backing the certificate cache of dhs.config.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
infoBytes, err := json.Marshal(challengeInfo{
|
||||
Domain: domain,
|
||||
Token: token,
|
||||
KeyAuth: keyAuth,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dhs.config.certCache.storage.Store(dhs.challengeTokensKey(domain), infoBytes)
|
||||
}
|
||||
|
||||
// CleanUp invokes the underlying solver's CleanUp method
|
||||
// and also cleans up any assets saved to storage.
|
||||
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 dhs.config.certCache.storage.Delete(dhs.challengeTokensKey(domain))
|
||||
}
|
||||
|
||||
// challengeTokensPrefix returns the key prefix for challenge info.
|
||||
func (dhs distributedSolver) challengeTokensPrefix() string {
|
||||
return filepath.Join(prefixCA(dhs.config.CA), "challenge_tokens")
|
||||
}
|
||||
|
||||
// challengeTokensKey returns the key to use to store and access
|
||||
// challenge info for domain.
|
||||
func (dhs distributedSolver) challengeTokensKey(domain string) string {
|
||||
return filepath.Join(dhs.challengeTokensPrefix(), safeKey(domain)+".json")
|
||||
}
|
||||
|
||||
type challengeInfo struct {
|
||||
Domain, Token, KeyAuth string
|
||||
}
|
212
vendor/github.com/mholt/certmagic/storage.go
generated
vendored
Normal file
212
vendor/github.com/mholt/certmagic/storage.go
generated
vendored
Normal file
@ -0,0 +1,212 @@
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Storage is a type that implements a key-value store.
|
||||
// Keys are prefix-based, with forward slash '/' as separators
|
||||
// and without a leading slash.
|
||||
//
|
||||
// Processes running in a cluster will wish to use the
|
||||
// same Storage value (its implementation and configuration)
|
||||
// in order to share certificates and other TLS resources
|
||||
// with the cluster.
|
||||
type Storage interface {
|
||||
// Exists returns true if the key exists
|
||||
// and there was no error checking.
|
||||
Exists(key string) bool
|
||||
|
||||
// Store puts value at key.
|
||||
Store(key string, value []byte) error
|
||||
|
||||
// Load retrieves the value at key.
|
||||
Load(key string) ([]byte, error)
|
||||
|
||||
// Delete deletes key.
|
||||
Delete(key string) error
|
||||
|
||||
// List returns all keys that match prefix.
|
||||
List(prefix string) ([]string, error)
|
||||
|
||||
// Stat returns information about key.
|
||||
Stat(key string) (KeyInfo, error)
|
||||
}
|
||||
|
||||
// KeyInfo holds information about a key in storage.
|
||||
type KeyInfo struct {
|
||||
Key string
|
||||
Modified time.Time
|
||||
Size int64
|
||||
}
|
||||
|
||||
// storeTx stores all the values or none at all.
|
||||
func storeTx(s Storage, all []keyValue) error {
|
||||
for i, kv := range all {
|
||||
err := s.Store(kv.key, kv.value)
|
||||
if err != nil {
|
||||
for j := i - 1; j >= 0; j-- {
|
||||
s.Delete(all[j].key)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// keyValue pairs a key and a value.
|
||||
type keyValue struct {
|
||||
key string
|
||||
value []byte
|
||||
}
|
||||
|
||||
const (
|
||||
prefixACME = "acme"
|
||||
prefixOCSP = "ocsp"
|
||||
)
|
||||
|
||||
func prefixCA(ca string) string {
|
||||
caURL, err := url.Parse(ca)
|
||||
if err != nil {
|
||||
caURL = &url.URL{Host: ca}
|
||||
}
|
||||
return path.Join(prefixACME, safeKey(caURL.Host))
|
||||
}
|
||||
|
||||
func prefixSite(ca, domain string) string {
|
||||
return path.Join(prefixCA(ca), "sites", safeKey(domain))
|
||||
}
|
||||
|
||||
// prefixSiteCert returns the path to the certificate file for domain.
|
||||
func prefixSiteCert(ca, domain string) string {
|
||||
return path.Join(prefixSite(ca, domain), safeKey(domain)+".crt")
|
||||
}
|
||||
|
||||
// prefixSiteKey returns the path to domain's private key file.
|
||||
func prefixSiteKey(ca, domain string) string {
|
||||
return path.Join(prefixSite(ca, domain), safeKey(domain)+".key")
|
||||
}
|
||||
|
||||
// prefixSiteMeta returns the path to the domain's asset metadata file.
|
||||
func prefixSiteMeta(ca, domain string) string {
|
||||
return path.Join(prefixSite(ca, domain), safeKey(domain)+".json")
|
||||
}
|
||||
|
||||
func prefixUsers(ca string) string {
|
||||
return path.Join(prefixCA(ca), "users")
|
||||
}
|
||||
|
||||
// prefixUser gets the account folder for the user with email
|
||||
func prefixUser(ca, email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
return path.Join(prefixUsers(ca), safeKey(email))
|
||||
}
|
||||
|
||||
// prefixUserReg gets the path to the registration file for the user with the
|
||||
// given email address.
|
||||
func prefixUserReg(ca, email string) string {
|
||||
return safeUserKey(ca, email, "registration", ".json")
|
||||
}
|
||||
|
||||
// prefixUserKey gets the path to the private key file for the user with the
|
||||
// given email address.
|
||||
func prefixUserKey(ca, email string) string {
|
||||
return safeUserKey(ca, email, "private", ".key")
|
||||
}
|
||||
|
||||
func prefixOCSPStaple(cert *Certificate, pemBundle []byte) string {
|
||||
var ocspFileName string
|
||||
if len(cert.Names) > 0 {
|
||||
firstName := safeKey(cert.Names[0])
|
||||
ocspFileName = firstName + "-"
|
||||
}
|
||||
ocspFileName += fastHash(pemBundle)
|
||||
return path.Join(prefixOCSP, ocspFileName)
|
||||
}
|
||||
|
||||
// safeUserKey returns a key for the given email,
|
||||
// with the default filename, and the filename
|
||||
// ending in the given extension.
|
||||
func safeUserKey(ca, email, defaultFilename, extension string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
email = strings.ToLower(email)
|
||||
filename := emailUsername(email)
|
||||
if filename == "" {
|
||||
filename = defaultFilename
|
||||
}
|
||||
filename = safeKey(filename)
|
||||
return path.Join(prefixUser(ca, email), filename+extension)
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// safeKey standardizes and sanitizes str for use in a file path.
|
||||
func safeKey(str string) string {
|
||||
str = strings.ToLower(str)
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
// replace a few specific characters
|
||||
repl := strings.NewReplacer(
|
||||
" ", "_",
|
||||
"+", "_plus_",
|
||||
"*", "wildcard_",
|
||||
"..", "", // prevent directory traversal (regex allows single dots)
|
||||
)
|
||||
str = repl.Replace(str)
|
||||
|
||||
// finally remove all non-word characters
|
||||
return safeKeyRE.ReplaceAllLiteralString(str, "")
|
||||
}
|
||||
|
||||
// safeKeyRE matches any undesirable characters in storage keys.
|
||||
// Note that this allows dots, so you'll have to strip ".." manually.
|
||||
var safeKeyRE = regexp.MustCompile(`[^\w@.-]`)
|
||||
|
||||
// ErrNotExist is returned by Storage implementations when
|
||||
// a resource is not found. It is similar to os.IsNotExist
|
||||
// except this is a type, not a variable.
|
||||
type ErrNotExist interface {
|
||||
error
|
||||
}
|
||||
|
||||
// defaultFileStorage is a convenient, default storage
|
||||
// implementation using the local file system.
|
||||
var defaultFileStorage = FileStorage{Path: dataDir()}
|
||||
|
||||
// DefaultStorage is the default Storage implementation.
|
||||
var DefaultStorage Storage = defaultFileStorage
|
||||
|
||||
// DefaultSync is a default sync to use.
|
||||
var DefaultSync Locker
|
145
caddytls/user.go → vendor/github.com/mholt/certmagic/user.go
generated
vendored
145
caddytls/user.go → vendor/github.com/mholt/certmagic/user.go
generated
vendored
@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
// Copyright 2015 Matthew Holt
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddytls
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -21,34 +21,35 @@ import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/lego"
|
||||
"github.com/xenolf/lego/registration"
|
||||
)
|
||||
|
||||
// User represents a Let's Encrypt user account.
|
||||
type User struct {
|
||||
// user represents a Let's Encrypt user account.
|
||||
type user struct {
|
||||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
// GetEmail gets u's email.
|
||||
func (u User) GetEmail() string {
|
||||
func (u user) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
// GetRegistration gets u's registration resource.
|
||||
func (u User) GetRegistration() *acme.RegistrationResource {
|
||||
func (u user) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
// GetPrivateKey gets u's private key.
|
||||
func (u User) GetPrivateKey() crypto.PrivateKey {
|
||||
func (u user) GetPrivateKey() crypto.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
|
||||
@ -57,11 +58,11 @@ func (u User) GetPrivateKey() crypto.PrivateKey {
|
||||
// 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}
|
||||
func (cfg *Config) 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())
|
||||
return user, fmt.Errorf("generating private key: %v", err)
|
||||
}
|
||||
user.key = privateKey
|
||||
return user, nil
|
||||
@ -77,19 +78,17 @@ func newUser(email string) (User, error) {
|
||||
// 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
|
||||
func (cfg *Config) getEmail(userPresent bool) (string, error) {
|
||||
// First try memory
|
||||
leEmail := cfg.Email
|
||||
if leEmail == "" {
|
||||
leEmail = Email
|
||||
}
|
||||
|
||||
// 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
|
||||
leEmail = cfg.mostRecentUserEmail()
|
||||
cfg.Email = leEmail // save for next time
|
||||
}
|
||||
|
||||
// Looks like there is no email address readily available,
|
||||
@ -98,7 +97,7 @@ func getEmail(cfg *Config, userPresent bool) (string, error) {
|
||||
// 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("")
|
||||
user, err := cfg.newUser("")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -108,11 +107,14 @@ func getEmail(cfg *Config, userPresent bool) (string, error) {
|
||||
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
|
||||
caURL := CA
|
||||
if cfg.CA != "" {
|
||||
caURL = cfg.CA
|
||||
}
|
||||
tempClient, err := acme.NewClient(caURL, user, "")
|
||||
legoConfig := lego.NewConfig(user)
|
||||
legoConfig.CADirURL = caURL
|
||||
legoConfig.UserAgent = UserAgent
|
||||
tempClient, err := lego.NewClient(legoConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("making ACME client to get ToS URL: %v", err)
|
||||
}
|
||||
@ -121,7 +123,7 @@ func getEmail(cfg *Config, userPresent bool) (string, error) {
|
||||
|
||||
// prompt the user for an email address and terms agreement
|
||||
reader := bufio.NewReader(stdin)
|
||||
promptUserAgreement(agreementURL)
|
||||
cfg.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: ")
|
||||
@ -130,12 +132,12 @@ func getEmail(cfg *Config, userPresent bool) (string, error) {
|
||||
return "", fmt.Errorf("reading email address: %v", err)
|
||||
}
|
||||
leEmail = strings.TrimSpace(leEmail)
|
||||
DefaultEmail = leEmail
|
||||
Agreed = true
|
||||
cfg.Email = leEmail
|
||||
cfg.Agreed = true
|
||||
|
||||
// save the new user to preserve this for next time
|
||||
user.Email = leEmail
|
||||
err = saveUser(storage, user)
|
||||
err = cfg.saveUser(user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -150,27 +152,31 @@ func getEmail(cfg *Config, userPresent bool) (string, error) {
|
||||
// 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
|
||||
func (cfg *Config) getUser(email string) (user, error) {
|
||||
var user user
|
||||
|
||||
// open user reg
|
||||
userData, err := storage.LoadUser(email)
|
||||
regBytes, err := cfg.certCache.storage.Load(prefixUserReg(cfg.CA, email))
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
// create a new user
|
||||
return newUser(email)
|
||||
return cfg.newUser(email)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
keyBytes, err := cfg.certCache.storage.Load(prefixUserKey(cfg.CA, email))
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
// create a new user
|
||||
return cfg.newUser(email)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
// load user information
|
||||
err = json.Unmarshal(userData.Reg, &user)
|
||||
err = json.Unmarshal(regBytes, &user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// load their private key
|
||||
user.key, err = loadPrivateKey(userData.Key)
|
||||
user.key, err = decodePrivateKey(keyBytes)
|
||||
return user, err
|
||||
}
|
||||
|
||||
@ -179,24 +185,34 @@ func getUser(storage Storage, email string) (User, error) {
|
||||
// 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")
|
||||
func (cfg *Config) saveUser(user user) error {
|
||||
regBytes, err := json.MarshalIndent(&user, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
err = storage.StoreUser(user.Email, userData)
|
||||
keyBytes, err := encodePrivateKey(user.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
|
||||
all := []keyValue{
|
||||
{
|
||||
key: prefixUserReg(cfg.CA, user.Email),
|
||||
value: regBytes,
|
||||
},
|
||||
{
|
||||
key: prefixUserKey(cfg.CA, user.Email),
|
||||
value: keyBytes,
|
||||
},
|
||||
}
|
||||
|
||||
return storeTx(cfg.certCache.storage, all)
|
||||
}
|
||||
|
||||
// promptUserAgreement simply outputs the standard user
|
||||
// agreement prompt with the given agreement URL.
|
||||
// It outputs a newline after the message.
|
||||
func promptUserAgreement(agreementURL string) {
|
||||
func (cfg *Config) 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)
|
||||
@ -205,8 +221,8 @@ By continuing, you agree to the Let's Encrypt Subscriber Agreement at:`
|
||||
// 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)
|
||||
func (cfg *Config) askUserAgreement(agreementURL string) bool {
|
||||
cfg.promptUserAgreement(agreementURL)
|
||||
fmt.Print("Do you agree to the terms? (y/n): ")
|
||||
|
||||
reader := bufio.NewReader(stdin)
|
||||
@ -219,6 +235,27 @@ func askUserAgreement(agreementURL string) bool {
|
||||
return answer == "y" || answer == "yes"
|
||||
}
|
||||
|
||||
// mostRecentUserEmail finds the most recently-written user file
|
||||
// in s. Since this is part of a complex sequence to get a user
|
||||
// account, errors here are discarded to simplify code flow in
|
||||
// the caller, and errors are not important here anyway.
|
||||
func (cfg *Config) mostRecentUserEmail() string {
|
||||
userList, err := cfg.certCache.storage.List(prefixUsers(cfg.CA))
|
||||
if err != nil || len(userList) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Slice(userList, func(i, j int) bool {
|
||||
iInfo, _ := cfg.certCache.storage.Stat(prefixUser(cfg.CA, userList[i]))
|
||||
jInfo, _ := cfg.certCache.storage.Stat(prefixUser(cfg.CA, userList[j]))
|
||||
return jInfo.Modified.Before(iInfo.Modified)
|
||||
})
|
||||
user, err := cfg.getUser(userList[0])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return user.Email
|
||||
}
|
||||
|
||||
// agreementTestURL is set during tests to skip requiring
|
||||
// setting up an entire ACME CA endpoint.
|
||||
var agreementTestURL string
|
69
vendor/github.com/xenolf/lego/acme/api/account.go
generated
vendored
Normal file
69
vendor/github.com/xenolf/lego/acme/api/account.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
type AccountService service
|
||||
|
||||
// New Creates a new account.
|
||||
func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
|
||||
var account acme.Account
|
||||
resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account)
|
||||
location := getLocation(resp)
|
||||
|
||||
if len(location) > 0 {
|
||||
a.core.jws.SetKid(location)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return acme.ExtendedAccount{Location: location}, err
|
||||
}
|
||||
|
||||
return acme.ExtendedAccount{Account: account, Location: location}, nil
|
||||
}
|
||||
|
||||
// NewEAB Creates a new account with an External Account Binding.
|
||||
func (a *AccountService) NewEAB(accMsg acme.Account, kid string, hmacEncoded string) (acme.ExtendedAccount, error) {
|
||||
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
|
||||
if err != nil {
|
||||
return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %v", err)
|
||||
}
|
||||
|
||||
eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)
|
||||
if err != nil {
|
||||
return acme.ExtendedAccount{}, fmt.Errorf("acme: error signing eab content: %v", err)
|
||||
}
|
||||
accMsg.ExternalAccountBinding = eabJWS
|
||||
|
||||
return a.New(accMsg)
|
||||
}
|
||||
|
||||
// Get Retrieves an account.
|
||||
func (a *AccountService) Get(accountURL string) (acme.Account, error) {
|
||||
if len(accountURL) == 0 {
|
||||
return acme.Account{}, errors.New("account[get]: empty URL")
|
||||
}
|
||||
|
||||
var account acme.Account
|
||||
_, err := a.core.post(accountURL, acme.Account{}, &account)
|
||||
if err != nil {
|
||||
return acme.Account{}, err
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// Deactivate Deactivates an account.
|
||||
func (a *AccountService) Deactivate(accountURL string) error {
|
||||
if len(accountURL) == 0 {
|
||||
return errors.New("account[deactivate]: empty URL")
|
||||
}
|
||||
|
||||
req := acme.Account{Status: acme.StatusDeactivated}
|
||||
_, err := a.core.post(accountURL, req, nil)
|
||||
return err
|
||||
}
|
151
vendor/github.com/xenolf/lego/acme/api/api.go
generated
vendored
Normal file
151
vendor/github.com/xenolf/lego/acme/api/api.go
generated
vendored
Normal file
@ -0,0 +1,151 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/acme/api/internal/nonces"
|
||||
"github.com/xenolf/lego/acme/api/internal/secure"
|
||||
"github.com/xenolf/lego/acme/api/internal/sender"
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
// Core ACME/LE core API.
|
||||
type Core struct {
|
||||
doer *sender.Doer
|
||||
nonceManager *nonces.Manager
|
||||
jws *secure.JWS
|
||||
directory acme.Directory
|
||||
HTTPClient *http.Client
|
||||
|
||||
common service // Reuse a single struct instead of allocating one for each service on the heap.
|
||||
Accounts *AccountService
|
||||
Authorizations *AuthorizationService
|
||||
Certificates *CertificateService
|
||||
Challenges *ChallengeService
|
||||
Orders *OrderService
|
||||
}
|
||||
|
||||
// New Creates a new Core.
|
||||
func New(httpClient *http.Client, userAgent string, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) {
|
||||
doer := sender.NewDoer(httpClient, userAgent)
|
||||
|
||||
dir, err := getDirectory(doer, caDirURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceManager := nonces.NewManager(doer, dir.NewNonceURL)
|
||||
|
||||
jws := secure.NewJWS(privateKey, kid, nonceManager)
|
||||
|
||||
c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir}
|
||||
|
||||
c.common.core = c
|
||||
c.Accounts = (*AccountService)(&c.common)
|
||||
c.Authorizations = (*AuthorizationService)(&c.common)
|
||||
c.Certificates = (*CertificateService)(&c.common)
|
||||
c.Challenges = (*ChallengeService)(&c.common)
|
||||
c.Orders = (*OrderService)(&c.common)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// post performs an HTTP POST request and parses the response body as JSON,
|
||||
// into the provided respBody object.
|
||||
func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) {
|
||||
content, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to marshal message")
|
||||
}
|
||||
|
||||
return a.retrievablePost(uri, content, response, 0)
|
||||
}
|
||||
|
||||
// postAsGet performs an HTTP POST ("POST-as-GET") request.
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-6.3
|
||||
func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) {
|
||||
return a.retrievablePost(uri, []byte{}, response, 0)
|
||||
}
|
||||
|
||||
func (a *Core) retrievablePost(uri string, content []byte, response interface{}, retry int) (*http.Response, error) {
|
||||
resp, err := a.signedPost(uri, content, response)
|
||||
if err != nil {
|
||||
// during tests, 5 retries allow to support ~50% of bad nonce.
|
||||
if retry >= 5 {
|
||||
log.Infof("too many retry on a nonce error, retry count: %d", retry)
|
||||
return resp, err
|
||||
}
|
||||
switch err.(type) {
|
||||
// Retry once if the nonce was invalidated
|
||||
case *acme.NonceError:
|
||||
log.Infof("nonce error retry: %s", err)
|
||||
resp, err = a.retrievablePost(uri, content, response, retry+1)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
default:
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) {
|
||||
signedContent, err := a.jws.SignContent(uri, content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to post JWS message -> failed to sign content -> %v", err)
|
||||
}
|
||||
|
||||
signedBody := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
|
||||
|
||||
resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response)
|
||||
|
||||
// nonceErr is ignored to keep the root error.
|
||||
nonce, nonceErr := nonces.GetFromResponse(resp)
|
||||
if nonceErr == nil {
|
||||
a.nonceManager.Push(nonce)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) {
|
||||
eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(eabJWS.FullSerialize()), nil
|
||||
}
|
||||
|
||||
// GetKeyAuthorization Gets the key authorization
|
||||
func (a *Core) GetKeyAuthorization(token string) (string, error) {
|
||||
return a.jws.GetKeyAuthorization(token)
|
||||
}
|
||||
|
||||
func (a *Core) GetDirectory() acme.Directory {
|
||||
return a.directory
|
||||
}
|
||||
|
||||
func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
|
||||
var dir acme.Directory
|
||||
if _, err := do.Get(caDirURL, &dir); err != nil {
|
||||
return dir, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
||||
}
|
||||
|
||||
if dir.NewAccountURL == "" {
|
||||
return dir, errors.New("directory missing new registration URL")
|
||||
}
|
||||
if dir.NewOrderURL == "" {
|
||||
return dir, errors.New("directory missing new order URL")
|
||||
}
|
||||
|
||||
return dir, nil
|
||||
}
|
34
vendor/github.com/xenolf/lego/acme/api/authorization.go
generated
vendored
Normal file
34
vendor/github.com/xenolf/lego/acme/api/authorization.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
type AuthorizationService service
|
||||
|
||||
// Get Gets an authorization.
|
||||
func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) {
|
||||
if len(authzURL) == 0 {
|
||||
return acme.Authorization{}, errors.New("authorization[get]: empty URL")
|
||||
}
|
||||
|
||||
var authz acme.Authorization
|
||||
_, err := c.core.postAsGet(authzURL, &authz)
|
||||
if err != nil {
|
||||
return acme.Authorization{}, err
|
||||
}
|
||||
return authz, nil
|
||||
}
|
||||
|
||||
// Deactivate Deactivates an authorization.
|
||||
func (c *AuthorizationService) Deactivate(authzURL string) error {
|
||||
if len(authzURL) == 0 {
|
||||
return errors.New("authorization[deactivate]: empty URL")
|
||||
}
|
||||
|
||||
var disabledAuth acme.Authorization
|
||||
_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)
|
||||
return err
|
||||
}
|
99
vendor/github.com/xenolf/lego/acme/api/certificate.go
generated
vendored
Normal file
99
vendor/github.com/xenolf/lego/acme/api/certificate.go
generated
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/certcrypto"
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
// maxBodySize is the maximum size of body that we will read.
|
||||
const maxBodySize = 1024 * 1024
|
||||
|
||||
type CertificateService service
|
||||
|
||||
// Get Returns the certificate and the issuer certificate.
|
||||
// 'bundle' is only applied if the issuer is provided by the 'up' link.
|
||||
func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) {
|
||||
cert, up, err := c.get(certURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Get issuerCert from bundled response from Let's Encrypt
|
||||
// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962
|
||||
_, issuer := pem.Decode(cert)
|
||||
if issuer != nil {
|
||||
return cert, issuer, nil
|
||||
}
|
||||
|
||||
issuer, err = c.getIssuerFromLink(up)
|
||||
if err != nil {
|
||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
||||
log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err)
|
||||
} else if len(issuer) > 0 {
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
// To do this, we append the issuer cert to the issued cert.
|
||||
if bundle {
|
||||
cert = append(cert, issuer...)
|
||||
}
|
||||
}
|
||||
|
||||
return cert, issuer, nil
|
||||
}
|
||||
|
||||
// Revoke Revokes a certificate.
|
||||
func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error {
|
||||
_, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// get Returns the certificate and the "up" link.
|
||||
func (c *CertificateService) get(certURL string) ([]byte, string, error) {
|
||||
if len(certURL) == 0 {
|
||||
return nil, "", errors.New("certificate[get]: empty URL")
|
||||
}
|
||||
|
||||
resp, err := c.core.postAsGet(certURL, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
cert, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// The issuer certificate link may be supplied via an "up" link
|
||||
// in the response headers of a new certificate.
|
||||
// See https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2
|
||||
up := getLink(resp.Header, "up")
|
||||
|
||||
return cert, up, err
|
||||
}
|
||||
|
||||
// getIssuerFromLink requests the issuer certificate
|
||||
func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) {
|
||||
if len(up) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Infof("acme: Requesting issuer cert from %s", up)
|
||||
|
||||
cert, _, err := c.get(up)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = x509.ParseCertificate(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert)), nil
|
||||
}
|
45
vendor/github.com/xenolf/lego/acme/api/challenge.go
generated
vendored
Normal file
45
vendor/github.com/xenolf/lego/acme/api/challenge.go
generated
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
type ChallengeService service
|
||||
|
||||
// New Creates a challenge.
|
||||
func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
|
||||
if len(chlgURL) == 0 {
|
||||
return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL")
|
||||
}
|
||||
|
||||
// Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`.
|
||||
// We use an empty struct instance as the postJSON payload here to achieve this result.
|
||||
var chlng acme.ExtendedChallenge
|
||||
resp, err := c.core.post(chlgURL, struct{}{}, &chlng)
|
||||
if err != nil {
|
||||
return acme.ExtendedChallenge{}, err
|
||||
}
|
||||
|
||||
chlng.AuthorizationURL = getLink(resp.Header, "up")
|
||||
chlng.RetryAfter = getRetryAfter(resp)
|
||||
return chlng, nil
|
||||
}
|
||||
|
||||
// Get Gets a challenge.
|
||||
func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
|
||||
if len(chlgURL) == 0 {
|
||||
return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL")
|
||||
}
|
||||
|
||||
var chlng acme.ExtendedChallenge
|
||||
resp, err := c.core.postAsGet(chlgURL, &chlng)
|
||||
if err != nil {
|
||||
return acme.ExtendedChallenge{}, err
|
||||
}
|
||||
|
||||
chlng.AuthorizationURL = getLink(resp.Header, "up")
|
||||
chlng.RetryAfter = getRetryAfter(resp)
|
||||
return chlng, nil
|
||||
}
|
78
vendor/github.com/xenolf/lego/acme/api/internal/nonces/nonce_manager.go
generated
vendored
Normal file
78
vendor/github.com/xenolf/lego/acme/api/internal/nonces/nonce_manager.go
generated
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
package nonces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/xenolf/lego/acme/api/internal/sender"
|
||||
)
|
||||
|
||||
// Manager Manages nonces.
|
||||
type Manager struct {
|
||||
do *sender.Doer
|
||||
nonceURL string
|
||||
nonces []string
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewManager Creates a new Manager.
|
||||
func NewManager(do *sender.Doer, nonceURL string) *Manager {
|
||||
return &Manager{
|
||||
do: do,
|
||||
nonceURL: nonceURL,
|
||||
}
|
||||
}
|
||||
|
||||
// Pop Pops a nonce.
|
||||
func (n *Manager) Pop() (string, bool) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
if len(n.nonces) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
nonce := n.nonces[len(n.nonces)-1]
|
||||
n.nonces = n.nonces[:len(n.nonces)-1]
|
||||
return nonce, true
|
||||
}
|
||||
|
||||
// Push Pushes a nonce.
|
||||
func (n *Manager) Push(nonce string) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
n.nonces = append(n.nonces, nonce)
|
||||
}
|
||||
|
||||
// Nonce implement jose.NonceSource
|
||||
func (n *Manager) Nonce() (string, error) {
|
||||
if nonce, ok := n.Pop(); ok {
|
||||
return nonce, nil
|
||||
}
|
||||
return n.getNonce()
|
||||
}
|
||||
|
||||
func (n *Manager) getNonce() (string, error) {
|
||||
resp, err := n.do.Head(n.nonceURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %v", err)
|
||||
}
|
||||
|
||||
return GetFromResponse(resp)
|
||||
}
|
||||
|
||||
// GetFromResponse Extracts a nonce from a HTTP response.
|
||||
func GetFromResponse(resp *http.Response) (string, error) {
|
||||
if resp == nil {
|
||||
return "", errors.New("nil response")
|
||||
}
|
||||
|
||||
nonce := resp.Header.Get("Replay-Nonce")
|
||||
if nonce == "" {
|
||||
return "", fmt.Errorf("server did not respond with a proper nonce header")
|
||||
}
|
||||
|
||||
return nonce, nil
|
||||
}
|
134
vendor/github.com/xenolf/lego/acme/api/internal/secure/jws.go
generated
vendored
Normal file
134
vendor/github.com/xenolf/lego/acme/api/internal/secure/jws.go
generated
vendored
Normal file
@ -0,0 +1,134 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/xenolf/lego/acme/api/internal/nonces"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
// JWS Represents a JWS.
|
||||
type JWS struct {
|
||||
privKey crypto.PrivateKey
|
||||
kid string // Key identifier
|
||||
nonces *nonces.Manager
|
||||
}
|
||||
|
||||
// NewJWS Create a new JWS.
|
||||
func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS {
|
||||
return &JWS{
|
||||
privKey: privateKey,
|
||||
nonces: nonceManager,
|
||||
kid: kid,
|
||||
}
|
||||
}
|
||||
|
||||
// SetKid Sets a key identifier.
|
||||
func (j *JWS) SetKid(kid string) {
|
||||
j.kid = kid
|
||||
}
|
||||
|
||||
// SignContent Signs a content with the JWS.
|
||||
func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) {
|
||||
var alg jose.SignatureAlgorithm
|
||||
switch k := j.privKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
alg = jose.RS256
|
||||
case *ecdsa.PrivateKey:
|
||||
if k.Curve == elliptic.P256() {
|
||||
alg = jose.ES256
|
||||
} else if k.Curve == elliptic.P384() {
|
||||
alg = jose.ES384
|
||||
}
|
||||
}
|
||||
|
||||
signKey := jose.SigningKey{
|
||||
Algorithm: alg,
|
||||
Key: jose.JSONWebKey{Key: j.privKey, KeyID: j.kid},
|
||||
}
|
||||
|
||||
options := jose.SignerOptions{
|
||||
NonceSource: j.nonces,
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"url": url,
|
||||
},
|
||||
}
|
||||
|
||||
if j.kid == "" {
|
||||
options.EmbedJWK = true
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(signKey, &options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create jose signer -> %v", err)
|
||||
}
|
||||
|
||||
signed, err := signer.Sign(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign content -> %v", err)
|
||||
}
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
// SignEABContent Signs an external account binding content with the JWS.
|
||||
func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
|
||||
jwk := jose.JSONWebKey{Key: j.privKey}
|
||||
jwkJSON, err := jwk.Public().MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme: error encoding eab jwk key: %v", err)
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(
|
||||
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
|
||||
&jose.SignerOptions{
|
||||
EmbedJWK: false,
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"kid": kid,
|
||||
"url": url,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %v", err)
|
||||
}
|
||||
|
||||
signed, err := signer.Sign(jwkJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to External Account Binding sign content -> %v", err)
|
||||
}
|
||||
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
// GetKeyAuthorization Gets the key authorization for a token.
|
||||
func (j *JWS) GetKeyAuthorization(token string) (string, error) {
|
||||
var publicKey crypto.PublicKey
|
||||
switch k := j.privKey.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
publicKey = k.Public()
|
||||
case *rsa.PrivateKey:
|
||||
publicKey = k.Public()
|
||||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
jwk := &jose.JSONWebKey{Key: publicKey}
|
||||
if jwk == nil {
|
||||
return "", errors.New("could not generate JWK from key")
|
||||
}
|
||||
|
||||
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// unpad the base64URL
|
||||
keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
|
||||
|
||||
return token + "." + keyThumb, nil
|
||||
}
|
146
vendor/github.com/xenolf/lego/acme/api/internal/sender/sender.go
generated
vendored
Normal file
146
vendor/github.com/xenolf/lego/acme/api/internal/sender/sender.go
generated
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
type RequestOption func(*http.Request) error
|
||||
|
||||
func contentType(ct string) RequestOption {
|
||||
return func(req *http.Request) error {
|
||||
req.Header.Set("Content-Type", ct)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type Doer struct {
|
||||
httpClient *http.Client
|
||||
userAgent string
|
||||
}
|
||||
|
||||
// NewDoer Creates a new Doer.
|
||||
func NewDoer(client *http.Client, userAgent string) *Doer {
|
||||
return &Doer{
|
||||
httpClient: client,
|
||||
userAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
// Get performs a GET request with a proper User-Agent string.
|
||||
// If "response" is not provided, callers should close resp.Body when done reading from it.
|
||||
func (d *Doer) Get(url string, response interface{}) (*http.Response, error) {
|
||||
req, err := d.newRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.do(req, response)
|
||||
}
|
||||
|
||||
// Head performs a HEAD request with a proper User-Agent string.
|
||||
// The response body (resp.Body) is already closed when this function returns.
|
||||
func (d *Doer) Head(url string) (*http.Response, error) {
|
||||
req, err := d.newRequest(http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.do(req, nil)
|
||||
}
|
||||
|
||||
// Post performs a POST request with a proper User-Agent string.
|
||||
// If "response" is not provided, callers should close resp.Body when done reading from it.
|
||||
func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) {
|
||||
req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.do(req, response)
|
||||
}
|
||||
|
||||
func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, uri, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", d.formatUserAgent())
|
||||
|
||||
for _, opt := range opts {
|
||||
err = opt(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) {
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = checkError(req, resp); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if response != nil {
|
||||
raw, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = json.Unmarshal(raw, response)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("failed to unmarshal %q to type %T: %v", raw, response, err)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// formatUserAgent builds and returns the User-Agent string to use in requests.
|
||||
func (d *Doer) formatUserAgent() string {
|
||||
ua := fmt.Sprintf("%s %s (%s; %s; %s)", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
|
||||
return strings.TrimSpace(ua)
|
||||
}
|
||||
|
||||
func checkError(req *http.Request, resp *http.Response) error {
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d :: %s :: %s :: %v", resp.StatusCode, req.Method, req.URL, err)
|
||||
}
|
||||
|
||||
var errorDetails *acme.ProblemDetails
|
||||
err = json.Unmarshal(body, &errorDetails)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d ::%s :: %s :: %v :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
|
||||
}
|
||||
|
||||
errorDetails.Method = req.Method
|
||||
errorDetails.URL = req.URL.String()
|
||||
|
||||
// Check for errors we handle specifically
|
||||
if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr {
|
||||
return &acme.NonceError{ProblemDetails: errorDetails}
|
||||
}
|
||||
|
||||
return errorDetails
|
||||
}
|
||||
return nil
|
||||
}
|
14
vendor/github.com/xenolf/lego/acme/api/internal/sender/useragent.go
generated
vendored
Normal file
14
vendor/github.com/xenolf/lego/acme/api/internal/sender/useragent.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package sender
|
||||
|
||||
// CODE GENERATED AUTOMATICALLY
|
||||
// THIS FILE MUST NOT BE EDITED BY HAND
|
||||
|
||||
const (
|
||||
// ourUserAgent is the User-Agent of this underlying library package.
|
||||
ourUserAgent = "xenolf-acme/1.2.1"
|
||||
|
||||
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
|
||||
// values: detach|release
|
||||
// NOTE: Update this with each tagged release.
|
||||
ourUserAgentComment = "detach"
|
||||
)
|
65
vendor/github.com/xenolf/lego/acme/api/order.go
generated
vendored
Normal file
65
vendor/github.com/xenolf/lego/acme/api/order.go
generated
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
type OrderService service
|
||||
|
||||
// New Creates a new order.
|
||||
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
|
||||
var identifiers []acme.Identifier
|
||||
for _, domain := range domains {
|
||||
identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain})
|
||||
}
|
||||
|
||||
orderReq := acme.Order{Identifiers: identifiers}
|
||||
|
||||
var order acme.Order
|
||||
resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
|
||||
if err != nil {
|
||||
return acme.ExtendedOrder{}, err
|
||||
}
|
||||
|
||||
return acme.ExtendedOrder{
|
||||
Location: resp.Header.Get("Location"),
|
||||
Order: order,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get Gets an order.
|
||||
func (o *OrderService) Get(orderURL string) (acme.Order, error) {
|
||||
if len(orderURL) == 0 {
|
||||
return acme.Order{}, errors.New("order[get]: empty URL")
|
||||
}
|
||||
|
||||
var order acme.Order
|
||||
_, err := o.core.postAsGet(orderURL, &order)
|
||||
if err != nil {
|
||||
return acme.Order{}, err
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// UpdateForCSR Updates an order for a CSR.
|
||||
func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.Order, error) {
|
||||
csrMsg := acme.CSRMessage{
|
||||
Csr: base64.RawURLEncoding.EncodeToString(csr),
|
||||
}
|
||||
|
||||
var order acme.Order
|
||||
_, err := o.core.post(orderURL, csrMsg, &order)
|
||||
if err != nil {
|
||||
return acme.Order{}, err
|
||||
}
|
||||
|
||||
if order.Status == acme.StatusInvalid {
|
||||
return acme.Order{}, order.Error
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
45
vendor/github.com/xenolf/lego/acme/api/service.go
generated
vendored
Normal file
45
vendor/github.com/xenolf/lego/acme/api/service.go
generated
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
core *Core
|
||||
}
|
||||
|
||||
// getLink get a rel into the Link header
|
||||
func getLink(header http.Header, rel string) string {
|
||||
var linkExpr = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`)
|
||||
|
||||
for _, link := range header["Link"] {
|
||||
for _, m := range linkExpr.FindAllStringSubmatch(link, -1) {
|
||||
if len(m) != 3 {
|
||||
continue
|
||||
}
|
||||
if m[2] == rel {
|
||||
return m[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getLocation get the value of the header Location
|
||||
func getLocation(resp *http.Response) string {
|
||||
if resp == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return resp.Header.Get("Location")
|
||||
}
|
||||
|
||||
// getRetryAfter get the value of the header Retry-After
|
||||
func getRetryAfter(resp *http.Response) string {
|
||||
if resp == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return resp.Header.Get("Retry-After")
|
||||
}
|
17
vendor/github.com/xenolf/lego/acme/challenges.go
generated
vendored
17
vendor/github.com/xenolf/lego/acme/challenges.go
generated
vendored
@ -1,17 +0,0 @@
|
||||
package acme
|
||||
|
||||
// Challenge is a string that identifies a particular type and version of ACME challenge.
|
||||
type Challenge string
|
||||
|
||||
const (
|
||||
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
|
||||
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
|
||||
HTTP01 = Challenge("http-01")
|
||||
|
||||
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
|
||||
// Note: DNS01Record returns a DNS record which will fulfill this challenge
|
||||
DNS01 = Challenge("dns-01")
|
||||
|
||||
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
|
||||
TLSALPN01 = Challenge("tls-alpn-01")
|
||||
)
|
957
vendor/github.com/xenolf/lego/acme/client.go
generated
vendored
957
vendor/github.com/xenolf/lego/acme/client.go
generated
vendored
@ -1,957 +0,0 @@
|
||||
// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers.
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxBodySize is the maximum size of body that we will read.
|
||||
maxBodySize = 1024 * 1024
|
||||
|
||||
// overallRequestLimit is the overall number of request per second limited on the
|
||||
// “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the
|
||||
// limitation is 20 requests per second, but using 20 as value doesn't work but 18 do
|
||||
overallRequestLimit = 18
|
||||
|
||||
statusValid = "valid"
|
||||
statusInvalid = "invalid"
|
||||
)
|
||||
|
||||
// User interface is to be implemented by users of this library.
|
||||
// It is used by the client type to get user specific information.
|
||||
type User interface {
|
||||
GetEmail() string
|
||||
GetRegistration() *RegistrationResource
|
||||
GetPrivateKey() crypto.PrivateKey
|
||||
}
|
||||
|
||||
// Interface for all challenge solvers to implement.
|
||||
type solver interface {
|
||||
Solve(challenge challenge, domain string) error
|
||||
}
|
||||
|
||||
// Interface for challenges like dns, where we can set a record in advance for ALL challenges.
|
||||
// This saves quite a bit of time vs creating the records and solving them serially.
|
||||
type preSolver interface {
|
||||
PreSolve(challenge challenge, domain string) error
|
||||
}
|
||||
|
||||
// Interface for challenges like dns, where we can solve all the challenges before to delete them.
|
||||
type cleanup interface {
|
||||
CleanUp(challenge challenge, domain string) error
|
||||
}
|
||||
|
||||
type validateFunc func(j *jws, domain, uri string, chlng challenge) error
|
||||
|
||||
// Client is the user-friendy way to ACME
|
||||
type Client struct {
|
||||
directory directory
|
||||
user User
|
||||
jws *jws
|
||||
keyType KeyType
|
||||
solvers map[Challenge]solver
|
||||
}
|
||||
|
||||
// NewClient creates a new ACME client on behalf of the user. The client will depend on
|
||||
// the ACME directory located at caDirURL for the rest of its actions. A private
|
||||
// key of type keyType (see KeyType contants) will be generated when requesting a new
|
||||
// certificate if one isn't provided.
|
||||
func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) {
|
||||
privKey := user.GetPrivateKey()
|
||||
if privKey == nil {
|
||||
return nil, errors.New("private key was nil")
|
||||
}
|
||||
|
||||
var dir directory
|
||||
if _, err := getJSON(caDirURL, &dir); err != nil {
|
||||
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
||||
}
|
||||
|
||||
if dir.NewAccountURL == "" {
|
||||
return nil, errors.New("directory missing new registration URL")
|
||||
}
|
||||
if dir.NewOrderURL == "" {
|
||||
return nil, errors.New("directory missing new order URL")
|
||||
}
|
||||
|
||||
jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL}
|
||||
if reg := user.GetRegistration(); reg != nil {
|
||||
jws.kid = reg.URI
|
||||
}
|
||||
|
||||
// REVIEW: best possibility?
|
||||
// Add all available solvers with the right index as per ACME
|
||||
// spec to this map. Otherwise they won`t be found.
|
||||
solvers := map[Challenge]solver{
|
||||
HTTP01: &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}},
|
||||
TLSALPN01: &tlsALPNChallenge{jws: jws, validate: validate, provider: &TLSALPNProviderServer{}},
|
||||
}
|
||||
|
||||
return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
|
||||
}
|
||||
|
||||
// SetChallengeProvider specifies a custom provider p that can solve the given challenge type.
|
||||
func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error {
|
||||
switch challenge {
|
||||
case HTTP01:
|
||||
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
|
||||
case DNS01:
|
||||
c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
|
||||
case TLSALPN01:
|
||||
c.solvers[challenge] = &tlsALPNChallenge{jws: c.jws, validate: validate, provider: p}
|
||||
default:
|
||||
return fmt.Errorf("unknown challenge %v", challenge)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges.
|
||||
// If this option is not used, the default port 80 and all interfaces will be used.
|
||||
// To only specify a port and no interface use the ":port" notation.
|
||||
//
|
||||
// NOTE: This REPLACES any custom HTTP provider previously set by calling
|
||||
// c.SetChallengeProvider with the default HTTP challenge provider.
|
||||
func (c *Client) SetHTTPAddress(iface string) error {
|
||||
host, port, err := net.SplitHostPort(iface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chlng, ok := c.solvers[HTTP01]; ok {
|
||||
chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges.
|
||||
// If this option is not used, the default port 443 and all interfaces will be used.
|
||||
// To only specify a port and no interface use the ":port" notation.
|
||||
//
|
||||
// NOTE: This REPLACES any custom TLS-ALPN provider previously set by calling
|
||||
// c.SetChallengeProvider with the default TLS-ALPN challenge provider.
|
||||
func (c *Client) SetTLSAddress(iface string) error {
|
||||
host, port, err := net.SplitHostPort(iface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chlng, ok := c.solvers[TLSALPN01]; ok {
|
||||
chlng.(*tlsALPNChallenge).provider = NewTLSALPNProviderServer(host, port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExcludeChallenges explicitly removes challenges from the pool for solving.
|
||||
func (c *Client) ExcludeChallenges(challenges []Challenge) {
|
||||
// Loop through all challenges and delete the requested one if found.
|
||||
for _, challenge := range challenges {
|
||||
delete(c.solvers, challenge)
|
||||
}
|
||||
}
|
||||
|
||||
// GetToSURL returns the current ToS URL from the Directory
|
||||
func (c *Client) GetToSURL() string {
|
||||
return c.directory.Meta.TermsOfService
|
||||
}
|
||||
|
||||
// GetExternalAccountRequired returns the External Account Binding requirement of the Directory
|
||||
func (c *Client) GetExternalAccountRequired() bool {
|
||||
return c.directory.Meta.ExternalAccountRequired
|
||||
}
|
||||
|
||||
// Register the current account to the ACME server.
|
||||
func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) {
|
||||
if c == nil || c.user == nil {
|
||||
return nil, errors.New("acme: cannot register a nil client or user")
|
||||
}
|
||||
log.Infof("acme: Registering account for %s", c.user.GetEmail())
|
||||
|
||||
accMsg := accountMessage{}
|
||||
if c.user.GetEmail() != "" {
|
||||
accMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
||||
} else {
|
||||
accMsg.Contact = []string{}
|
||||
}
|
||||
accMsg.TermsOfServiceAgreed = tosAgreed
|
||||
|
||||
var serverReg accountMessage
|
||||
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg)
|
||||
if err != nil {
|
||||
remoteErr, ok := err.(RemoteError)
|
||||
if ok && remoteErr.StatusCode == 409 {
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reg := &RegistrationResource{
|
||||
URI: hdr.Get("Location"),
|
||||
Body: serverReg,
|
||||
}
|
||||
c.jws.kid = reg.URI
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// RegisterWithExternalAccountBinding Register the current account to the ACME server.
|
||||
func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) {
|
||||
if c == nil || c.user == nil {
|
||||
return nil, errors.New("acme: cannot register a nil client or user")
|
||||
}
|
||||
log.Infof("acme: Registering account (EAB) for %s", c.user.GetEmail())
|
||||
|
||||
accMsg := accountMessage{}
|
||||
if c.user.GetEmail() != "" {
|
||||
accMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
|
||||
} else {
|
||||
accMsg.Contact = []string{}
|
||||
}
|
||||
accMsg.TermsOfServiceAgreed = tosAgreed
|
||||
|
||||
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error())
|
||||
}
|
||||
|
||||
eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error())
|
||||
}
|
||||
|
||||
eabPayload := eabJWS.FullSerialize()
|
||||
|
||||
accMsg.ExternalAccountBinding = []byte(eabPayload)
|
||||
|
||||
var serverReg accountMessage
|
||||
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg)
|
||||
if err != nil {
|
||||
remoteErr, ok := err.(RemoteError)
|
||||
if ok && remoteErr.StatusCode == 409 {
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reg := &RegistrationResource{
|
||||
URI: hdr.Get("Location"),
|
||||
Body: serverReg,
|
||||
}
|
||||
c.jws.kid = reg.URI
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// ResolveAccountByKey will attempt to look up an account using the given account key
|
||||
// and return its registration resource.
|
||||
func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) {
|
||||
log.Infof("acme: Trying to resolve account by key")
|
||||
|
||||
acc := accountMessage{OnlyReturnExisting: true}
|
||||
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountLink := hdr.Get("Location")
|
||||
if accountLink == "" {
|
||||
return nil, errors.New("Server did not return the account link")
|
||||
}
|
||||
|
||||
var retAccount accountMessage
|
||||
c.jws.kid = accountLink
|
||||
_, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RegistrationResource{URI: accountLink, Body: retAccount}, nil
|
||||
}
|
||||
|
||||
// DeleteRegistration deletes the client's user registration from the ACME
|
||||
// server.
|
||||
func (c *Client) DeleteRegistration() error {
|
||||
if c == nil || c.user == nil {
|
||||
return errors.New("acme: cannot unregister a nil client or user")
|
||||
}
|
||||
log.Infof("acme: Deleting account for %s", c.user.GetEmail())
|
||||
|
||||
accMsg := accountMessage{
|
||||
Status: "deactivated",
|
||||
}
|
||||
|
||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// QueryRegistration runs a POST request on the client's registration and
|
||||
// returns the result.
|
||||
//
|
||||
// This is similar to the Register function, but acting on an existing
|
||||
// registration link and resource.
|
||||
func (c *Client) QueryRegistration() (*RegistrationResource, error) {
|
||||
if c == nil || c.user == nil {
|
||||
return nil, errors.New("acme: cannot query the registration of a nil client or user")
|
||||
}
|
||||
// Log the URL here instead of the email as the email may not be set
|
||||
log.Infof("acme: Querying account for %s", c.user.GetRegistration().URI)
|
||||
|
||||
accMsg := accountMessage{}
|
||||
|
||||
var serverReg accountMessage
|
||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &serverReg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg := &RegistrationResource{Body: serverReg}
|
||||
|
||||
// Location: header is not returned so this needs to be populated off of
|
||||
// existing URI
|
||||
reg.URI = c.user.GetRegistration().URI
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it.
|
||||
// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key
|
||||
// for this CSR is not required.
|
||||
// If bundle is true, the []byte contains both the issuer certificate and
|
||||
// your issued certificate as a bundle.
|
||||
// This function will never return a partial certificate. If one domain in the list fails,
|
||||
// the whole certificate will fail.
|
||||
func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (*CertificateResource, error) {
|
||||
// figure out what domains it concerns
|
||||
// start with the common name
|
||||
domains := []string{csr.Subject.CommonName}
|
||||
|
||||
// loop over the SubjectAltName DNS names
|
||||
DNSNames:
|
||||
for _, sanName := range csr.DNSNames {
|
||||
for _, existingName := range domains {
|
||||
if existingName == sanName {
|
||||
// duplicate; skip this name
|
||||
continue DNSNames
|
||||
}
|
||||
}
|
||||
|
||||
// name is unique
|
||||
domains = append(domains, sanName)
|
||||
}
|
||||
|
||||
if bundle {
|
||||
log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||
} else {
|
||||
log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||
}
|
||||
|
||||
order, err := c.createOrderForIdentifiers(domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authz, err := c.getAuthzForOrder(order)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
/*for _, auth := range authz {
|
||||
c.disableAuthz(auth)
|
||||
}*/
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.solveChallengeForAuthz(authz)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
failures := make(ObtainError)
|
||||
cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil)
|
||||
if err != nil {
|
||||
for _, chln := range authz {
|
||||
failures[chln.Identifier.Value] = err
|
||||
}
|
||||
}
|
||||
|
||||
if cert != nil {
|
||||
// Add the CSR to the certificate so that it can be used for renewals.
|
||||
cert.CSR = pemEncode(&csr)
|
||||
}
|
||||
|
||||
// do not return an empty failures map, because
|
||||
// it would still be a non-nil error value
|
||||
if len(failures) > 0 {
|
||||
return cert, failures
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
|
||||
// The first domain in domains is used for the CommonName field of the certificate, all other
|
||||
// domains are added using the Subject Alternate Names extension. A new private key is generated
|
||||
// for every invocation of this function. If you do not want that you can supply your own private key
|
||||
// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one.
|
||||
// If bundle is true, the []byte contains both the issuer certificate and
|
||||
// your issued certificate as a bundle.
|
||||
// This function will never return a partial certificate. If one domain in the list fails,
|
||||
// the whole certificate will fail.
|
||||
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
|
||||
if len(domains) == 0 {
|
||||
return nil, errors.New("no domains to obtain a certificate for")
|
||||
}
|
||||
|
||||
if bundle {
|
||||
log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
|
||||
} else {
|
||||
log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
|
||||
}
|
||||
|
||||
order, err := c.createOrderForIdentifiers(domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authz, err := c.getAuthzForOrder(order)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
/*for _, auth := range authz {
|
||||
c.disableAuthz(auth)
|
||||
}*/
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.solveChallengeForAuthz(authz)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
failures := make(ObtainError)
|
||||
cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple)
|
||||
if err != nil {
|
||||
for _, auth := range authz {
|
||||
failures[auth.Identifier.Value] = err
|
||||
}
|
||||
}
|
||||
|
||||
// do not return an empty failures map, because
|
||||
// it would still be a non-nil error value
|
||||
if len(failures) > 0 {
|
||||
return cert, failures
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
||||
func (c *Client) RevokeCertificate(certificate []byte) error {
|
||||
certificates, err := parsePEMBundle(certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x509Cert := certificates[0]
|
||||
if x509Cert.IsCA {
|
||||
return fmt.Errorf("Certificate bundle starts with a CA certificate")
|
||||
}
|
||||
|
||||
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
|
||||
|
||||
_, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// RenewCertificate takes a CertificateResource and tries to renew the certificate.
|
||||
// If the renewal process succeeds, the new certificate will ge returned in a new CertResource.
|
||||
// Please be aware that this function will return a new certificate in ANY case that is not an error.
|
||||
// If the server does not provide us with a new cert on a GET request to the CertURL
|
||||
// this function will start a new-cert flow where a new certificate gets generated.
|
||||
// If bundle is true, the []byte contains both the issuer certificate and
|
||||
// your issued certificate as a bundle.
|
||||
// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil.
|
||||
func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (*CertificateResource, error) {
|
||||
// Input certificate is PEM encoded. Decode it here as we may need the decoded
|
||||
// cert later on in the renewal process. The input may be a bundle or a single certificate.
|
||||
certificates, err := parsePEMBundle(cert.Certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x509Cert := certificates[0]
|
||||
if x509Cert.IsCA {
|
||||
return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
|
||||
}
|
||||
|
||||
// This is just meant to be informal for the user.
|
||||
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
||||
log.Infof("[%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
|
||||
|
||||
// We always need to request a new certificate to renew.
|
||||
// Start by checking to see if the certificate was based off a CSR, and
|
||||
// use that if it's defined.
|
||||
if len(cert.CSR) > 0 {
|
||||
csr, errP := pemDecodeTox509CSR(cert.CSR)
|
||||
if errP != nil {
|
||||
return nil, errP
|
||||
}
|
||||
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
|
||||
return newCert, failures
|
||||
}
|
||||
|
||||
var privKey crypto.PrivateKey
|
||||
if cert.PrivateKey != nil {
|
||||
privKey, err = parsePEMPrivateKey(cert.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var domains []string
|
||||
// check for SAN certificate
|
||||
if len(x509Cert.DNSNames) > 1 {
|
||||
domains = append(domains, x509Cert.Subject.CommonName)
|
||||
for _, sanDomain := range x509Cert.DNSNames {
|
||||
if sanDomain == x509Cert.Subject.CommonName {
|
||||
continue
|
||||
}
|
||||
domains = append(domains, sanDomain)
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, x509Cert.Subject.CommonName)
|
||||
}
|
||||
|
||||
newCert, err := c.ObtainCertificate(domains, bundle, privKey, mustStaple)
|
||||
return newCert, err
|
||||
}
|
||||
|
||||
func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) {
|
||||
var identifiers []identifier
|
||||
for _, domain := range domains {
|
||||
identifiers = append(identifiers, identifier{Type: "dns", Value: domain})
|
||||
}
|
||||
|
||||
order := orderMessage{
|
||||
Identifiers: identifiers,
|
||||
}
|
||||
|
||||
var response orderMessage
|
||||
hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response)
|
||||
if err != nil {
|
||||
return orderResource{}, err
|
||||
}
|
||||
|
||||
orderRes := orderResource{
|
||||
URL: hdr.Get("Location"),
|
||||
Domains: domains,
|
||||
orderMessage: response,
|
||||
}
|
||||
return orderRes, nil
|
||||
}
|
||||
|
||||
// an authz with the solver we have chosen and the index of the challenge associated with it
|
||||
type selectedAuthSolver struct {
|
||||
authz authorization
|
||||
challengeIndex int
|
||||
solver solver
|
||||
}
|
||||
|
||||
// Looks through the challenge combinations to find a solvable match.
|
||||
// Then solves the challenges in series and returns.
|
||||
func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
|
||||
failures := make(ObtainError)
|
||||
|
||||
authSolvers := []*selectedAuthSolver{}
|
||||
|
||||
// loop through the resources, basically through the domains. First pass just selects a solver for each authz.
|
||||
for _, authz := range authorizations {
|
||||
if authz.Status == statusValid {
|
||||
// Boulder might recycle recent validated authz (see issue #267)
|
||||
log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
|
||||
continue
|
||||
}
|
||||
if i, solvr := c.chooseSolver(authz, authz.Identifier.Value); solvr != nil {
|
||||
authSolvers = append(authSolvers, &selectedAuthSolver{
|
||||
authz: authz,
|
||||
challengeIndex: i,
|
||||
solver: solvr,
|
||||
})
|
||||
} else {
|
||||
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// for all valid presolvers, first submit the challenges so they have max time to propagate
|
||||
for _, item := range authSolvers {
|
||||
authz := item.authz
|
||||
i := item.challengeIndex
|
||||
if presolver, ok := item.solver.(preSolver); ok {
|
||||
if err := presolver.PreSolve(authz.Challenges[i], authz.Identifier.Value); err != nil {
|
||||
failures[authz.Identifier.Value] = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// clean all created TXT records
|
||||
for _, item := range authSolvers {
|
||||
if clean, ok := item.solver.(cleanup); ok {
|
||||
if failures[item.authz.Identifier.Value] != nil {
|
||||
// already failed in previous loop
|
||||
continue
|
||||
}
|
||||
err := clean.CleanUp(item.authz.Challenges[item.challengeIndex], item.authz.Identifier.Value)
|
||||
if err != nil {
|
||||
log.Warnf("Error cleaning up %s: %v ", item.authz.Identifier.Value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// finally solve all challenges for real
|
||||
for _, item := range authSolvers {
|
||||
authz := item.authz
|
||||
i := item.challengeIndex
|
||||
if failures[authz.Identifier.Value] != nil {
|
||||
// already failed in previous loop
|
||||
continue
|
||||
}
|
||||
if err := item.solver.Solve(authz.Challenges[i], authz.Identifier.Value); err != nil {
|
||||
failures[authz.Identifier.Value] = err
|
||||
}
|
||||
}
|
||||
|
||||
// be careful not to return an empty failures map, for
|
||||
// even an empty ObtainError is a non-nil error value
|
||||
if len(failures) > 0 {
|
||||
return failures
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks all challenges from the server in order and returns the first matching solver.
|
||||
func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) {
|
||||
for i, challenge := range auth.Challenges {
|
||||
if solver, ok := c.solvers[Challenge(challenge.Type)]; ok {
|
||||
return i, solver
|
||||
}
|
||||
log.Infof("[%s] acme: Could not find solver for: %s", domain, challenge.Type)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Get the challenges needed to proof our identifier to the ACME server.
|
||||
func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) {
|
||||
resc, errc := make(chan authorization), make(chan domainError)
|
||||
|
||||
delay := time.Second / overallRequestLimit
|
||||
|
||||
for _, authzURL := range order.Authorizations {
|
||||
time.Sleep(delay)
|
||||
|
||||
go func(authzURL string) {
|
||||
var authz authorization
|
||||
_, err := postAsGet(c.jws, authzURL, &authz)
|
||||
if err != nil {
|
||||
errc <- domainError{Domain: authz.Identifier.Value, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
resc <- authz
|
||||
}(authzURL)
|
||||
}
|
||||
|
||||
var responses []authorization
|
||||
failures := make(ObtainError)
|
||||
for i := 0; i < len(order.Authorizations); i++ {
|
||||
select {
|
||||
case res := <-resc:
|
||||
responses = append(responses, res)
|
||||
case err := <-errc:
|
||||
failures[err.Domain] = err.Error
|
||||
}
|
||||
}
|
||||
|
||||
logAuthz(order)
|
||||
|
||||
close(resc)
|
||||
close(errc)
|
||||
|
||||
// be careful to not return an empty failures map;
|
||||
// even if empty, they become non-nil error values
|
||||
if len(failures) > 0 {
|
||||
return responses, failures
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func logAuthz(order orderResource) {
|
||||
for i, auth := range order.Authorizations {
|
||||
log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanAuthz loops through the passed in slice and disables any auths which are not "valid"
|
||||
func (c *Client) disableAuthz(authURL string) error {
|
||||
var disabledAuth authorization
|
||||
_, err := postJSON(c.jws, authURL, deactivateAuthMessage{Status: "deactivated"}, &disabledAuth)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
|
||||
|
||||
var err error
|
||||
if privKey == nil {
|
||||
privKey, err = generatePrivateKey(c.keyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// determine certificate name(s) based on the authorization resources
|
||||
commonName := order.Domains[0]
|
||||
|
||||
// ACME draft Section 7.4 "Applying for Certificate Issuance"
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
|
||||
// says:
|
||||
// Clients SHOULD NOT make any assumptions about the sort order of
|
||||
// "identifiers" or "authorizations" elements in the returned order
|
||||
// object.
|
||||
san := []string{commonName}
|
||||
for _, auth := range order.Identifiers {
|
||||
if auth.Value != commonName {
|
||||
san = append(san, auth.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
csr, err := generateCsr(privKey, commonName, san, mustStaple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey))
|
||||
}
|
||||
|
||||
func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) {
|
||||
commonName := order.Domains[0]
|
||||
|
||||
csrString := base64.RawURLEncoding.EncodeToString(csr)
|
||||
var retOrder orderMessage
|
||||
_, err := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if retOrder.Status == statusInvalid {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certRes := CertificateResource{
|
||||
Domain: commonName,
|
||||
CertURL: retOrder.Certificate,
|
||||
PrivateKey: privateKeyPem,
|
||||
}
|
||||
|
||||
if retOrder.Status == statusValid {
|
||||
// if the certificate is available right away, short cut!
|
||||
ok, err := c.checkCertResponse(retOrder, &certRes, bundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
return &certRes, nil
|
||||
}
|
||||
}
|
||||
|
||||
stopTimer := time.NewTimer(30 * time.Second)
|
||||
defer stopTimer.Stop()
|
||||
retryTick := time.NewTicker(500 * time.Millisecond)
|
||||
defer retryTick.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopTimer.C:
|
||||
return nil, errors.New("certificate polling timed out")
|
||||
case <-retryTick.C:
|
||||
_, err := postAsGet(c.jws, order.URL, &retOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done, err := c.checkCertResponse(retOrder, &certRes, bundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if done {
|
||||
return &certRes, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkCertResponse checks to see if the certificate is ready and a link is contained in the
|
||||
// response. if so, loads it into certRes and returns true. If the cert
|
||||
// is not yet ready, it returns false. The certRes input
|
||||
// should already have the Domain (common name) field populated. If bundle is
|
||||
// true, the certificate will be bundled with the issuer's cert.
|
||||
func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) {
|
||||
switch order.Status {
|
||||
case statusValid:
|
||||
resp, err := postAsGet(c.jws, order.Certificate, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// The issuer certificate link may be supplied via an "up" link
|
||||
// in the response headers of a new certificate. See
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2
|
||||
links := parseLinks(resp.Header["Link"])
|
||||
if link, ok := links["up"]; ok {
|
||||
issuerCert, err := c.getIssuerCertificate(link)
|
||||
|
||||
if err != nil {
|
||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
||||
log.Warnf("[%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
|
||||
} else {
|
||||
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
||||
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
// To do this, we append the issuer cert to the issued cert.
|
||||
if bundle {
|
||||
cert = append(cert, issuerCert...)
|
||||
}
|
||||
|
||||
certRes.IssuerCertificate = issuerCert
|
||||
}
|
||||
} else {
|
||||
// Get issuerCert from bundled response from Let's Encrypt
|
||||
// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962
|
||||
_, rest := pem.Decode(cert)
|
||||
if rest != nil {
|
||||
certRes.IssuerCertificate = rest
|
||||
}
|
||||
}
|
||||
|
||||
certRes.Certificate = cert
|
||||
certRes.CertURL = order.Certificate
|
||||
certRes.CertStableURL = order.Certificate
|
||||
log.Infof("[%s] Server responded with a certificate.", certRes.Domain)
|
||||
return true, nil
|
||||
|
||||
case "processing":
|
||||
return false, nil
|
||||
case statusInvalid:
|
||||
return false, errors.New("order has invalid state: invalid")
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// getIssuerCertificate requests the issuer certificate
|
||||
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
|
||||
log.Infof("acme: Requesting issuer cert from %s", url)
|
||||
resp, err := postAsGet(c.jws, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = x509.ParseCertificate(issuerBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return issuerBytes, err
|
||||
}
|
||||
|
||||
func parseLinks(links []string) map[string]string {
|
||||
aBrkt := regexp.MustCompile("[<>]")
|
||||
slver := regexp.MustCompile("(.+) *= *\"(.+)\"")
|
||||
linkMap := make(map[string]string)
|
||||
|
||||
for _, link := range links {
|
||||
|
||||
link = aBrkt.ReplaceAllString(link, "")
|
||||
parts := strings.Split(link, ";")
|
||||
|
||||
matches := slver.FindStringSubmatch(parts[1])
|
||||
if len(matches) > 0 {
|
||||
linkMap[matches[2]] = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
return linkMap
|
||||
}
|
||||
|
||||
// validate makes the ACME server start validating a
|
||||
// challenge response, only returning once it is done.
|
||||
func validate(j *jws, domain, uri string, c challenge) error {
|
||||
var chlng challenge
|
||||
|
||||
// Challenge initiation is done by sending a JWS payload containing the
|
||||
// trivial JSON object `{}`. We use an empty struct instance as the postJSON
|
||||
// payload here to achieve this result.
|
||||
hdr, err := postJSON(j, uri, struct{}{}, &chlng)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// After the path is sent, the ACME server will access our server.
|
||||
// Repeatedly check the server for an updated status on our request.
|
||||
for {
|
||||
switch chlng.Status {
|
||||
case statusValid:
|
||||
log.Infof("[%s] The server validated our request", domain)
|
||||
return nil
|
||||
case "pending":
|
||||
case "processing":
|
||||
case statusInvalid:
|
||||
return handleChallengeError(chlng)
|
||||
default:
|
||||
return errors.New("the server returned an unexpected state")
|
||||
}
|
||||
|
||||
ra, err := strconv.Atoi(hdr.Get("Retry-After"))
|
||||
if err != nil {
|
||||
// The ACME server MUST return a Retry-After.
|
||||
// If it doesn't, we'll just poll hard.
|
||||
ra = 5
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(ra) * time.Second)
|
||||
|
||||
resp, err := postAsGet(j, uri, &chlng)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp != nil {
|
||||
hdr = resp.Header
|
||||
}
|
||||
}
|
||||
}
|
284
vendor/github.com/xenolf/lego/acme/commons.go
generated
vendored
Normal file
284
vendor/github.com/xenolf/lego/acme/commons.go
generated
vendored
Normal file
@ -0,0 +1,284 @@
|
||||
// Package acme contains all objects related the ACME endpoints.
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Challenge statuses
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.6
|
||||
const (
|
||||
StatusPending = "pending"
|
||||
StatusInvalid = "invalid"
|
||||
StatusValid = "valid"
|
||||
StatusProcessing = "processing"
|
||||
StatusDeactivated = "deactivated"
|
||||
StatusExpired = "expired"
|
||||
StatusRevoked = "revoked"
|
||||
)
|
||||
|
||||
// Directory the ACME directory object.
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.1
|
||||
type Directory struct {
|
||||
NewNonceURL string `json:"newNonce"`
|
||||
NewAccountURL string `json:"newAccount"`
|
||||
NewOrderURL string `json:"newOrder"`
|
||||
NewAuthzURL string `json:"newAuthz"`
|
||||
RevokeCertURL string `json:"revokeCert"`
|
||||
KeyChangeURL string `json:"keyChange"`
|
||||
Meta Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// Meta the ACME meta object (related to Directory).
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.1
|
||||
type Meta struct {
|
||||
// termsOfService (optional, string):
|
||||
// A URL identifying the current terms of service.
|
||||
TermsOfService string `json:"termsOfService"`
|
||||
|
||||
// website (optional, string):
|
||||
// An HTTP or HTTPS URL locating a website providing more information about the ACME server.
|
||||
Website string `json:"website"`
|
||||
|
||||
// caaIdentities (optional, array of string):
|
||||
// The hostnames that the ACME server recognizes as referring to itself
|
||||
// for the purposes of CAA record validation as defined in [RFC6844].
|
||||
// Each string MUST represent the same sequence of ASCII code points
|
||||
// that the server will expect to see as the "Issuer Domain Name" in a CAA issue or issuewild property tag.
|
||||
// This allows clients to determine the correct issuer domain name to use when configuring CAA records.
|
||||
CaaIdentities []string `json:"caaIdentities"`
|
||||
|
||||
// externalAccountRequired (optional, boolean):
|
||||
// If this field is present and set to "true",
|
||||
// then the CA requires that all new- account requests include an "externalAccountBinding" field
|
||||
// associating the new account with an external account.
|
||||
ExternalAccountRequired bool `json:"externalAccountRequired"`
|
||||
}
|
||||
|
||||
// ExtendedAccount a extended Account.
|
||||
type ExtendedAccount struct {
|
||||
Account
|
||||
// Contains the value of the response header `Location`
|
||||
Location string `json:"-"`
|
||||
}
|
||||
|
||||
// Account the ACME account Object.
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.2
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.3
|
||||
type Account struct {
|
||||
// status (required, string):
|
||||
// The status of this account.
|
||||
// Possible values are: "valid", "deactivated", and "revoked".
|
||||
// The value "deactivated" should be used to indicate client-initiated deactivation
|
||||
// whereas "revoked" should be used to indicate server- initiated deactivation. (See Section 7.1.6)
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
// contact (optional, array of string):
|
||||
// An array of URLs that the server can use to contact the client for issues related to this account.
|
||||
// For example, the server may wish to notify the client about server-initiated revocation or certificate expiration.
|
||||
// For information on supported URL schemes, see Section 7.3
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
|
||||
// termsOfServiceAgreed (optional, boolean):
|
||||
// Including this field in a new-account request,
|
||||
// with a value of true, indicates the client's agreement with the terms of service.
|
||||
// This field is not updateable by the client.
|
||||
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
||||
|
||||
// orders (required, string):
|
||||
// A URL from which a list of orders submitted by this account can be fetched via a POST-as-GET request,
|
||||
// as described in Section 7.1.2.1.
|
||||
Orders string `json:"orders,omitempty"`
|
||||
|
||||
// onlyReturnExisting (optional, boolean):
|
||||
// If this field is present with the value "true",
|
||||
// then the server MUST NOT create a new account if one does not already exist.
|
||||
// This allows a client to look up an account URL based on an account key (see Section 7.3.1).
|
||||
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
|
||||
|
||||
// externalAccountBinding (optional, object):
|
||||
// An optional field for binding the new account with an existing non-ACME account (see Section 7.3.4).
|
||||
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
|
||||
}
|
||||
|
||||
// ExtendedOrder a extended Order.
|
||||
type ExtendedOrder struct {
|
||||
Order
|
||||
// The order URL, contains the value of the response header `Location`
|
||||
Location string `json:"-"`
|
||||
}
|
||||
|
||||
// Order the ACME order Object.
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.3
|
||||
type Order struct {
|
||||
// status (required, string):
|
||||
// The status of this order.
|
||||
// Possible values are: "pending", "ready", "processing", "valid", and "invalid".
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
// expires (optional, string):
|
||||
// The timestamp after which the server will consider this order invalid,
|
||||
// encoded in the format specified in RFC 3339 [RFC3339].
|
||||
// This field is REQUIRED for objects with "pending" or "valid" in the status field.
|
||||
Expires string `json:"expires,omitempty"`
|
||||
|
||||
// identifiers (required, array of object):
|
||||
// An array of identifier objects that the order pertains to.
|
||||
Identifiers []Identifier `json:"identifiers"`
|
||||
|
||||
// notBefore (optional, string):
|
||||
// The requested value of the notBefore field in the certificate,
|
||||
// in the date format defined in [RFC3339].
|
||||
NotBefore string `json:"notBefore,omitempty"`
|
||||
|
||||
// notAfter (optional, string):
|
||||
// The requested value of the notAfter field in the certificate,
|
||||
// in the date format defined in [RFC3339].
|
||||
NotAfter string `json:"notAfter,omitempty"`
|
||||
|
||||
// error (optional, object):
|
||||
// The error that occurred while processing the order, if any.
|
||||
// This field is structured as a problem document [RFC7807].
|
||||
Error *ProblemDetails `json:"error,omitempty"`
|
||||
|
||||
// authorizations (required, array of string):
|
||||
// For pending orders,
|
||||
// the authorizations that the client needs to complete before the requested certificate can be issued (see Section 7.5),
|
||||
// including unexpired authorizations that the client has completed in the past for identifiers specified in the order.
|
||||
// The authorizations required are dictated by server policy
|
||||
// and there may not be a 1:1 relationship between the order identifiers and the authorizations required.
|
||||
// For final orders (in the "valid" or "invalid" state), the authorizations that were completed.
|
||||
// Each entry is a URL from which an authorization can be fetched with a POST-as-GET request.
|
||||
Authorizations []string `json:"authorizations,omitempty"`
|
||||
|
||||
// finalize (required, string):
|
||||
// A URL that a CSR must be POSTed to once all of the order's authorizations are satisfied to finalize the order.
|
||||
// The result of a successful finalization will be the population of the certificate URL for the order.
|
||||
Finalize string `json:"finalize,omitempty"`
|
||||
|
||||
// certificate (optional, string):
|
||||
// A URL for the certificate that has been issued in response to this order
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
}
|
||||
|
||||
// Authorization the ACME authorization object.
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.4
|
||||
type Authorization struct {
|
||||
// status (required, string):
|
||||
// The status of this authorization.
|
||||
// Possible values are: "pending", "valid", "invalid", "deactivated", "expired", and "revoked".
|
||||
Status string `json:"status"`
|
||||
|
||||
// expires (optional, string):
|
||||
// The timestamp after which the server will consider this authorization invalid,
|
||||
// encoded in the format specified in RFC 3339 [RFC3339].
|
||||
// This field is REQUIRED for objects with "valid" in the "status" field.
|
||||
Expires time.Time `json:"expires,omitempty"`
|
||||
|
||||
// identifier (required, object):
|
||||
// The identifier that the account is authorized to represent
|
||||
Identifier Identifier `json:"identifier,omitempty"`
|
||||
|
||||
// challenges (required, array of objects):
|
||||
// For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier.
|
||||
// For valid authorizations, the challenge that was validated.
|
||||
// For invalid authorizations, the challenge that was attempted and failed.
|
||||
// Each array entry is an object with parameters required to validate the challenge.
|
||||
// A client should attempt to fulfill one of these challenges,
|
||||
// and a server should consider any one of the challenges sufficient to make the authorization valid.
|
||||
Challenges []Challenge `json:"challenges,omitempty"`
|
||||
|
||||
// wildcard (optional, boolean):
|
||||
// For authorizations created as a result of a newOrder request containing a DNS identifier
|
||||
// with a value that contained a wildcard prefix this field MUST be present, and true.
|
||||
Wildcard bool `json:"wildcard,omitempty"`
|
||||
}
|
||||
|
||||
// ExtendedChallenge a extended Challenge.
|
||||
type ExtendedChallenge struct {
|
||||
Challenge
|
||||
// Contains the value of the response header `Retry-After`
|
||||
RetryAfter string `json:"-"`
|
||||
// Contains the value of the response header `Link` rel="up"
|
||||
AuthorizationURL string `json:"-"`
|
||||
}
|
||||
|
||||
// Challenge the ACME challenge object.
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.5
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8
|
||||
type Challenge struct {
|
||||
// type (required, string):
|
||||
// The type of challenge encoded in the object.
|
||||
Type string `json:"type"`
|
||||
|
||||
// url (required, string):
|
||||
// The URL to which a response can be posted.
|
||||
URL string `json:"url"`
|
||||
|
||||
// status (required, string):
|
||||
// The status of this challenge. Possible values are: "pending", "processing", "valid", and "invalid".
|
||||
Status string `json:"status"`
|
||||
|
||||
// validated (optional, string):
|
||||
// The time at which the server validated this challenge,
|
||||
// encoded in the format specified in RFC 3339 [RFC3339].
|
||||
// This field is REQUIRED if the "status" field is "valid".
|
||||
Validated time.Time `json:"validated,omitempty"`
|
||||
|
||||
// error (optional, object):
|
||||
// Error that occurred while the server was validating the challenge, if any,
|
||||
// structured as a problem document [RFC7807].
|
||||
// Multiple errors can be indicated by using subproblems Section 6.7.1.
|
||||
// A challenge object with an error MUST have status equal to "invalid".
|
||||
Error *ProblemDetails `json:"error,omitempty"`
|
||||
|
||||
// token (required, string):
|
||||
// A random value that uniquely identifies the challenge.
|
||||
// This value MUST have at least 128 bits of entropy.
|
||||
// It MUST NOT contain any characters outside the base64url alphabet,
|
||||
// and MUST NOT include base64 padding characters ("=").
|
||||
// See [RFC4086] for additional information on randomness requirements.
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.3
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.4
|
||||
Token string `json:"token"`
|
||||
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.1
|
||||
KeyAuthorization string `json:"keyAuthorization"`
|
||||
}
|
||||
|
||||
// Identifier the ACME identifier object.
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-9.7.7
|
||||
type Identifier struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// CSRMessage Certificate Signing Request
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.4
|
||||
type CSRMessage struct {
|
||||
// csr (required, string):
|
||||
// A CSR encoding the parameters for the certificate being requested [RFC2986].
|
||||
// The CSR is sent in the base64url-encoded version of the DER format.
|
||||
// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.).
|
||||
Csr string `json:"csr"`
|
||||
}
|
||||
|
||||
// RevokeCertMessage a certificate revocation message
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.6
|
||||
// - https://tools.ietf.org/html/rfc5280#section-5.3.1
|
||||
type RevokeCertMessage struct {
|
||||
// certificate (required, string):
|
||||
// The certificate to be revoked, in the base64url-encoded version of the DER format.
|
||||
// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.)
|
||||
Certificate string `json:"certificate"`
|
||||
|
||||
// reason (optional, int):
|
||||
// One of the revocation reasonCodes defined in Section 5.3.1 of [RFC5280] to be used when generating OCSP responses and CRLs.
|
||||
// If this field is not set the server SHOULD omit the reasonCode CRL entry extension when generating OCSP responses and CRLs.
|
||||
// The server MAY disallow a subset of reasonCodes from being used by the user.
|
||||
// If a request contains a disallowed reasonCode the server MUST reject it with the error type "urn:ietf:params:acme:error:badRevocationReason".
|
||||
// The problem document detail SHOULD indicate which reasonCodes are allowed.
|
||||
Reason *uint `json:"reason,omitempty"`
|
||||
}
|
334
vendor/github.com/xenolf/lego/acme/crypto.go
generated
vendored
334
vendor/github.com/xenolf/lego/acme/crypto.go
generated
vendored
@ -1,334 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
// KeyType represents the key algo as well as the key size or curve to use.
|
||||
type KeyType string
|
||||
type derCertificateBytes []byte
|
||||
|
||||
// Constants for all key types we support.
|
||||
const (
|
||||
EC256 = KeyType("P256")
|
||||
EC384 = KeyType("P384")
|
||||
RSA2048 = KeyType("2048")
|
||||
RSA4096 = KeyType("4096")
|
||||
RSA8192 = KeyType("8192")
|
||||
)
|
||||
|
||||
const (
|
||||
// OCSPGood means that the certificate is valid.
|
||||
OCSPGood = ocsp.Good
|
||||
// OCSPRevoked means that the certificate has been deliberately revoked.
|
||||
OCSPRevoked = ocsp.Revoked
|
||||
// OCSPUnknown means that the OCSP responder doesn't know about the certificate.
|
||||
OCSPUnknown = ocsp.Unknown
|
||||
// OCSPServerFailed means that the OCSP responder failed to process the request.
|
||||
OCSPServerFailed = ocsp.ServerFailed
|
||||
)
|
||||
|
||||
// Constants for OCSP must staple
|
||||
var (
|
||||
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
||||
ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
||||
)
|
||||
|
||||
// GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response,
|
||||
// the parsed response, and an error, if any. The returned []byte can be passed directly
|
||||
// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the
|
||||
// issued certificate, this function will try to get the issuer certificate from the
|
||||
// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return
|
||||
// values are nil, the OCSP status may be assumed OCSPUnknown.
|
||||
func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
||||
certificates, err := parsePEMBundle(bundle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We expect the certificate slice to be ordered downwards the chain.
|
||||
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
|
||||
// which should always be the first two certificates. If there's no
|
||||
// OCSP server listed in the leaf cert, there's nothing to do. And if
|
||||
// we have only one certificate so far, we need to get the issuer cert.
|
||||
issuedCert := certificates[0]
|
||||
if len(issuedCert.OCSPServer) == 0 {
|
||||
return nil, nil, errors.New("no OCSP server specified in cert")
|
||||
}
|
||||
if len(certificates) == 1 {
|
||||
// TODO: build fallback. If this fails, check the remaining array entries.
|
||||
if len(issuedCert.IssuingCertificateURL) == 0 {
|
||||
return nil, nil, errors.New("no issuing certificate URL")
|
||||
}
|
||||
|
||||
resp, errC := httpGet(issuedCert.IssuingCertificateURL[0])
|
||||
if errC != nil {
|
||||
return nil, nil, errC
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
issuerBytes, errC := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
|
||||
if errC != nil {
|
||||
return nil, nil, errC
|
||||
}
|
||||
|
||||
issuerCert, errC := x509.ParseCertificate(issuerBytes)
|
||||
if errC != nil {
|
||||
return nil, nil, errC
|
||||
}
|
||||
|
||||
// Insert it into the slice on position 0
|
||||
// We want it ordered right SRV CRT -> CA
|
||||
certificates = append(certificates, issuerCert)
|
||||
}
|
||||
issuerCert := certificates[1]
|
||||
|
||||
// Finally kick off the OCSP request.
|
||||
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(ocspReq)
|
||||
req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return ocspResBytes, ocspRes, nil
|
||||
}
|
||||
|
||||
func getKeyAuthorization(token string, key interface{}) (string, error) {
|
||||
var publicKey crypto.PublicKey
|
||||
switch k := key.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
publicKey = k.Public()
|
||||
case *rsa.PrivateKey:
|
||||
publicKey = k.Public()
|
||||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
jwk := &jose.JSONWebKey{Key: publicKey}
|
||||
if jwk == nil {
|
||||
return "", errors.New("could not generate JWK from key")
|
||||
}
|
||||
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// unpad the base64URL
|
||||
keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
|
||||
|
||||
return token + "." + keyThumb, nil
|
||||
}
|
||||
|
||||
// parsePEMBundle parses a certificate bundle from top to bottom and returns
|
||||
// a slice of x509 certificates. This function will error if no certificates are found.
|
||||
func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||
var certificates []*x509.Certificate
|
||||
var certDERBlock *pem.Block
|
||||
|
||||
for {
|
||||
certDERBlock, bundle = pem.Decode(bundle)
|
||||
if certDERBlock == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if certDERBlock.Type == "CERTIFICATE" {
|
||||
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificates = append(certificates, cert)
|
||||
}
|
||||
}
|
||||
|
||||
if len(certificates) == 0 {
|
||||
return nil, errors.New("no certificates were found while parsing the bundle")
|
||||
}
|
||||
|
||||
return certificates, nil
|
||||
}
|
||||
|
||||
func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
|
||||
keyBlock, _ := pem.Decode(key)
|
||||
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
default:
|
||||
return nil, errors.New("unknown PEM header value")
|
||||
}
|
||||
}
|
||||
|
||||
func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
|
||||
|
||||
switch keyType {
|
||||
case EC256:
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
case EC384:
|
||||
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
case RSA2048:
|
||||
return rsa.GenerateKey(rand.Reader, 2048)
|
||||
case RSA4096:
|
||||
return rsa.GenerateKey(rand.Reader, 4096)
|
||||
case RSA8192:
|
||||
return rsa.GenerateKey(rand.Reader, 8192)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid KeyType: %s", keyType)
|
||||
}
|
||||
|
||||
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
|
||||
template := x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: domain},
|
||||
}
|
||||
|
||||
if len(san) > 0 {
|
||||
template.DNSNames = san
|
||||
}
|
||||
|
||||
if mustStaple {
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
|
||||
Id: tlsFeatureExtensionOID,
|
||||
Value: ocspMustStapleFeature,
|
||||
})
|
||||
}
|
||||
|
||||
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
|
||||
}
|
||||
|
||||
func pemEncode(data interface{}) []byte {
|
||||
var pemBlock *pem.Block
|
||||
switch key := data.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
keyBytes, _ := x509.MarshalECPrivateKey(key)
|
||||
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
||||
case *rsa.PrivateKey:
|
||||
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||
case *x509.CertificateRequest:
|
||||
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
||||
case derCertificateBytes:
|
||||
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(pemBlock)
|
||||
}
|
||||
|
||||
func pemDecode(data []byte) (*pem.Block, error) {
|
||||
pemBlock, _ := pem.Decode(data)
|
||||
if pemBlock == nil {
|
||||
return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?")
|
||||
}
|
||||
|
||||
return pemBlock, nil
|
||||
}
|
||||
|
||||
func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
|
||||
pemBlock, err := pemDecode(pem)
|
||||
if pemBlock == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pemBlock.Type != "CERTIFICATE REQUEST" {
|
||||
return nil, fmt.Errorf("PEM block is not a certificate request")
|
||||
}
|
||||
|
||||
return x509.ParseCertificateRequest(pemBlock.Bytes)
|
||||
}
|
||||
|
||||
// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate.
|
||||
// The certificate has to be PEM encoded. Any other encodings like DER will fail.
|
||||
func GetPEMCertExpiration(cert []byte) (time.Time, error) {
|
||||
pemBlock, err := pemDecode(cert)
|
||||
if pemBlock == nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
return getCertExpiration(pemBlock.Bytes)
|
||||
}
|
||||
|
||||
// getCertExpiration returns the "NotAfter" date of a DER encoded certificate.
|
||||
func getCertExpiration(cert []byte) (time.Time, error) {
|
||||
pCert, err := x509.ParseCertificate(cert)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
return pCert.NotAfter, nil
|
||||
}
|
||||
|
||||
func generatePemCert(privKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
|
||||
derBytes, err := generateDerCert(privKey, time.Time{}, domain, extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
|
||||
}
|
||||
|
||||
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if expiration.IsZero() {
|
||||
expiration = time.Now().Add(365)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "ACME Challenge TEMP",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: expiration,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{domain},
|
||||
ExtraExtensions: extensions,
|
||||
}
|
||||
|
||||
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||
}
|
||||
|
||||
func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser {
|
||||
return http.MaxBytesReader(nil, rd, numBytes)
|
||||
}
|
343
vendor/github.com/xenolf/lego/acme/dns_challenge.go
generated
vendored
343
vendor/github.com/xenolf/lego/acme/dns_challenge.go
generated
vendored
@ -1,343 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
type preCheckDNSFunc func(fqdn, value string) (bool, error)
|
||||
|
||||
var (
|
||||
// PreCheckDNS checks DNS propagation before notifying ACME that
|
||||
// the DNS challenge is ready.
|
||||
PreCheckDNS preCheckDNSFunc = checkDNSPropagation
|
||||
fqdnToZone = map[string]string{}
|
||||
muFqdnToZone sync.Mutex
|
||||
)
|
||||
|
||||
const defaultResolvConf = "/etc/resolv.conf"
|
||||
|
||||
const (
|
||||
// DefaultPropagationTimeout default propagation timeout
|
||||
DefaultPropagationTimeout = 60 * time.Second
|
||||
|
||||
// DefaultPollingInterval default polling interval
|
||||
DefaultPollingInterval = 2 * time.Second
|
||||
|
||||
// DefaultTTL default TTL
|
||||
DefaultTTL = 120
|
||||
)
|
||||
|
||||
var defaultNameservers = []string{
|
||||
"google-public-dns-a.google.com:53",
|
||||
"google-public-dns-b.google.com:53",
|
||||
}
|
||||
|
||||
// RecursiveNameservers are used to pre-check DNS propagation
|
||||
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
|
||||
|
||||
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
|
||||
var DNSTimeout = 10 * time.Second
|
||||
|
||||
// getNameservers attempts to get systems nameservers before falling back to the defaults
|
||||
func getNameservers(path string, defaults []string) []string {
|
||||
config, err := dns.ClientConfigFromFile(path)
|
||||
if err != nil || len(config.Servers) == 0 {
|
||||
return defaults
|
||||
}
|
||||
|
||||
systemNameservers := []string{}
|
||||
for _, server := range config.Servers {
|
||||
// ensure all servers have a port number
|
||||
if _, _, err := net.SplitHostPort(server); err != nil {
|
||||
systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53"))
|
||||
} else {
|
||||
systemNameservers = append(systemNameservers, server)
|
||||
}
|
||||
}
|
||||
return systemNameservers
|
||||
}
|
||||
|
||||
// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge
|
||||
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
|
||||
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
||||
// base64URL encoding without padding
|
||||
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
||||
ttl = DefaultTTL
|
||||
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
|
||||
return
|
||||
}
|
||||
|
||||
// dnsChallenge implements the dns-01 challenge according to ACME 7.5
|
||||
type dnsChallenge struct {
|
||||
jws *jws
|
||||
validate validateFunc
|
||||
provider ChallengeProvider
|
||||
}
|
||||
|
||||
// PreSolve just submits the txt record to the dns provider. It does not validate record propagation, or
|
||||
// do anything at all with the acme server.
|
||||
func (s *dnsChallenge) PreSolve(chlng challenge, domain string) error {
|
||||
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
|
||||
|
||||
if s.provider == nil {
|
||||
return errors.New("no DNS Provider configured")
|
||||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.provider.Present(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error presenting token: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fqdn, value, _ := DNS01Record(domain, keyAuth)
|
||||
|
||||
log.Infof("[%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
|
||||
|
||||
var timeout, interval time.Duration
|
||||
switch provider := s.provider.(type) {
|
||||
case ChallengeProviderTimeout:
|
||||
timeout, interval = provider.Timeout()
|
||||
default:
|
||||
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
|
||||
}
|
||||
|
||||
err = WaitFor(timeout, interval, func() (bool, error) {
|
||||
return PreCheckDNS(fqdn, value)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||
}
|
||||
|
||||
// CleanUp cleans the challenge
|
||||
func (s *dnsChallenge) CleanUp(chlng challenge, domain string) error {
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
}
|
||||
|
||||
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
||||
func checkDNSPropagation(fqdn, value string) (bool, error) {
|
||||
// Initial attempt to resolve at the recursive NS
|
||||
r, err := dnsQuery(fqdn, dns.TypeTXT, RecursiveNameservers, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if r.Rcode == dns.RcodeSuccess {
|
||||
// If we see a CNAME here then use the alias
|
||||
for _, rr := range r.Answer {
|
||||
if cn, ok := rr.(*dns.CNAME); ok {
|
||||
if cn.Hdr.Name == fqdn {
|
||||
fqdn = cn.Target
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
authoritativeNss, err := lookupNameservers(fqdn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
|
||||
}
|
||||
|
||||
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
|
||||
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
|
||||
for _, ns := range nameservers {
|
||||
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, rr := range r.Answer {
|
||||
if txt, ok := rr.(*dns.TXT); ok {
|
||||
if strings.Join(txt.Txt, "") == value {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s]", ns, fqdn)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// dnsQuery will query a nameserver, iterating through the supplied servers as it retries
|
||||
// The nameserver should include a port, to facilitate testing where we talk to a mock dns server.
|
||||
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(fqdn, rtype)
|
||||
m.SetEdns0(4096, false)
|
||||
|
||||
if !recursive {
|
||||
m.RecursionDesired = false
|
||||
}
|
||||
|
||||
// Will retry the request based on the number of servers (n+1)
|
||||
for i := 1; i <= len(nameservers)+1; i++ {
|
||||
ns := nameservers[i%len(nameservers)]
|
||||
udp := &dns.Client{Net: "udp", Timeout: DNSTimeout}
|
||||
in, _, err = udp.Exchange(m, ns)
|
||||
|
||||
if err == dns.ErrTruncated {
|
||||
tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout}
|
||||
// If the TCP request succeeds, the err will reset to nil
|
||||
in, _, err = tcp.Exchange(m, ns)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// lookupNameservers returns the authoritative nameservers for the given fqdn.
|
||||
func lookupNameservers(fqdn string) ([]string, error) {
|
||||
var authoritativeNss []string
|
||||
|
||||
zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine the zone: %v", err)
|
||||
}
|
||||
|
||||
r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rr := range r.Answer {
|
||||
if ns, ok := rr.(*dns.NS); ok {
|
||||
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
|
||||
}
|
||||
}
|
||||
|
||||
if len(authoritativeNss) > 0 {
|
||||
return authoritativeNss, nil
|
||||
}
|
||||
return nil, fmt.Errorf("could not determine authoritative nameservers")
|
||||
}
|
||||
|
||||
// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the
|
||||
// domain labels until the nameserver returns a SOA record in the answer section.
|
||||
func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
|
||||
muFqdnToZone.Lock()
|
||||
defer muFqdnToZone.Unlock()
|
||||
|
||||
// Do we have it cached?
|
||||
if zone, ok := fqdnToZone[fqdn]; ok {
|
||||
return zone, nil
|
||||
}
|
||||
|
||||
labelIndexes := dns.Split(fqdn)
|
||||
for _, index := range labelIndexes {
|
||||
domain := fqdn[index:]
|
||||
|
||||
in, err := dnsQuery(domain, dns.TypeSOA, nameservers, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Any response code other than NOERROR and NXDOMAIN is treated as error
|
||||
if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess {
|
||||
return "", fmt.Errorf("unexpected response code '%s' for %s",
|
||||
dns.RcodeToString[in.Rcode], domain)
|
||||
}
|
||||
|
||||
// Check if we got a SOA RR in the answer section
|
||||
if in.Rcode == dns.RcodeSuccess {
|
||||
|
||||
// CNAME records cannot/should not exist at the root of a zone.
|
||||
// So we skip a domain when a CNAME is found.
|
||||
if dnsMsgContainsCNAME(in) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ans := range in.Answer {
|
||||
if soa, ok := ans.(*dns.SOA); ok {
|
||||
zone := soa.Hdr.Name
|
||||
fqdnToZone[fqdn] = zone
|
||||
return zone, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find the start of authority")
|
||||
}
|
||||
|
||||
// dnsMsgContainsCNAME checks for a CNAME answer in msg
|
||||
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
|
||||
for _, ans := range msg.Answer {
|
||||
if _, ok := ans.(*dns.CNAME); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
|
||||
func ClearFqdnCache() {
|
||||
fqdnToZone = map[string]string{}
|
||||
}
|
||||
|
||||
// ToFqdn converts the name into a fqdn appending a trailing dot.
|
||||
func ToFqdn(name string) string {
|
||||
n := len(name)
|
||||
if n == 0 || name[n-1] == '.' {
|
||||
return name
|
||||
}
|
||||
return name + "."
|
||||
}
|
||||
|
||||
// UnFqdn converts the fqdn into a name removing the trailing dot.
|
||||
func UnFqdn(name string) string {
|
||||
n := len(name)
|
||||
if n != 0 && name[n-1] == '.' {
|
||||
return name[:n-1]
|
||||
}
|
||||
return name
|
||||
}
|
55
vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
generated
vendored
55
vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
generated
vendored
@ -1,55 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsTemplate = "%s %d IN TXT \"%s\""
|
||||
)
|
||||
|
||||
// DNSProviderManual is an implementation of the ChallengeProvider interface
|
||||
type DNSProviderManual struct{}
|
||||
|
||||
// NewDNSProviderManual returns a DNSProviderManual instance.
|
||||
func NewDNSProviderManual() (*DNSProviderManual, error) {
|
||||
return &DNSProviderManual{}, nil
|
||||
}
|
||||
|
||||
// Present prints instructions for manually creating the TXT record
|
||||
func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := DNS01Record(domain, keyAuth)
|
||||
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
|
||||
|
||||
authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("acme: Please create the following TXT record in your %s zone:", authZone)
|
||||
log.Infof("acme: %s", dnsRecord)
|
||||
log.Infof("acme: Press 'Enter' when you are done")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
_, _ = reader.ReadString('\n')
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp prints instructions for manually removing the TXT record
|
||||
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, ttl := DNS01Record(domain, keyAuth)
|
||||
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, "...")
|
||||
|
||||
authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("acme: You can now remove this TXT record from your %s zone:", authZone)
|
||||
log.Infof("acme: %s", dnsRecord)
|
||||
return nil
|
||||
}
|
91
vendor/github.com/xenolf/lego/acme/error.go
generated
vendored
91
vendor/github.com/xenolf/lego/acme/error.go
generated
vendored
@ -1,91 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
tosAgreementError = "Terms of service have changed"
|
||||
invalidNonceError = "urn:ietf:params:acme:error:badNonce"
|
||||
)
|
||||
|
||||
// RemoteError is the base type for all errors specific to the ACME protocol.
|
||||
type RemoteError struct {
|
||||
StatusCode int `json:"status,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
func (e RemoteError) Error() string {
|
||||
return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail)
|
||||
}
|
||||
|
||||
// TOSError represents the error which is returned if the user needs to
|
||||
// accept the TOS.
|
||||
// TODO: include the new TOS url if we can somehow obtain it.
|
||||
type TOSError struct {
|
||||
RemoteError
|
||||
}
|
||||
|
||||
// NonceError represents the error which is returned if the
|
||||
// nonce sent by the client was not accepted by the server.
|
||||
type NonceError struct {
|
||||
RemoteError
|
||||
}
|
||||
|
||||
type domainError struct {
|
||||
Domain string
|
||||
Error error
|
||||
}
|
||||
|
||||
// ObtainError is returned when there are specific errors available
|
||||
// per domain. For example in ObtainCertificate
|
||||
type ObtainError map[string]error
|
||||
|
||||
func (e ObtainError) Error() string {
|
||||
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
|
||||
for dom, err := range e {
|
||||
buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err))
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func handleHTTPError(resp *http.Response) error {
|
||||
var errorDetail RemoteError
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") {
|
||||
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
errorDetail.Detail = string(detailBytes)
|
||||
}
|
||||
|
||||
errorDetail.StatusCode = resp.StatusCode
|
||||
|
||||
// Check for errors we handle specifically
|
||||
if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError {
|
||||
return TOSError{errorDetail}
|
||||
}
|
||||
|
||||
if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError {
|
||||
return NonceError{errorDetail}
|
||||
}
|
||||
|
||||
return errorDetail
|
||||
}
|
||||
|
||||
func handleChallengeError(chlng challenge) error {
|
||||
return chlng.Error
|
||||
}
|
58
vendor/github.com/xenolf/lego/acme/errors.go
generated
vendored
Normal file
58
vendor/github.com/xenolf/lego/acme/errors.go
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Errors types
|
||||
const (
|
||||
errNS = "urn:ietf:params:acme:error:"
|
||||
BadNonceErr = errNS + "badNonce"
|
||||
)
|
||||
|
||||
// ProblemDetails the problem details object
|
||||
// - https://tools.ietf.org/html/rfc7807#section-3.1
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.3.3
|
||||
type ProblemDetails struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
HTTPStatus int `json:"status,omitempty"`
|
||||
Instance string `json:"instance,omitempty"`
|
||||
SubProblems []SubProblem `json:"subproblems,omitempty"`
|
||||
|
||||
// additional values to have a better error message (Not defined by the RFC)
|
||||
Method string `json:"method,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// SubProblem a "subproblems"
|
||||
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-6.7.1
|
||||
type SubProblem struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Identifier Identifier `json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProblemDetails) Error() string {
|
||||
msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus)
|
||||
if len(p.Method) != 0 || len(p.URL) != 0 {
|
||||
msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL)
|
||||
}
|
||||
msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail)
|
||||
|
||||
for _, sub := range p.SubProblems {
|
||||
msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail)
|
||||
}
|
||||
|
||||
if len(p.Instance) == 0 {
|
||||
msg += ", url: " + p.Instance
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// NonceError represents the error which is returned
|
||||
// if the nonce sent by the client was not accepted by the server.
|
||||
type NonceError struct {
|
||||
*ProblemDetails
|
||||
}
|
212
vendor/github.com/xenolf/lego/acme/http.go
generated
vendored
212
vendor/github.com/xenolf/lego/acme/http.go
generated
vendored
@ -1,212 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
|
||||
UserAgent string
|
||||
|
||||
// HTTPClient is an HTTP client with a reasonable timeout value and
|
||||
// potentially a custom *x509.CertPool based on the caCertificatesEnvVar
|
||||
// environment variable (see the `initCertPool` function)
|
||||
HTTPClient = http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 15 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
ServerName: os.Getenv(caServerNameEnvVar),
|
||||
RootCAs: initCertPool(),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// ourUserAgent is the User-Agent of this underlying library package.
|
||||
// NOTE: Update this with each tagged release.
|
||||
ourUserAgent = "xenolf-acme/1.2.1"
|
||||
|
||||
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
|
||||
// values: detach|release
|
||||
// NOTE: Update this with each tagged release.
|
||||
ourUserAgentComment = "detach"
|
||||
|
||||
// caCertificatesEnvVar is the environment variable name that can be used to
|
||||
// specify the path to PEM encoded CA Certificates that can be used to
|
||||
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
|
||||
// the system-wide trusted root list.
|
||||
caCertificatesEnvVar = "LEGO_CA_CERTIFICATES"
|
||||
|
||||
// caServerNameEnvVar is the environment variable name that can be used to
|
||||
// specify the CA server name that can be used to
|
||||
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
|
||||
// the system-wide trusted root list.
|
||||
caServerNameEnvVar = "LEGO_CA_SERVER_NAME"
|
||||
)
|
||||
|
||||
// initCertPool creates a *x509.CertPool populated with the PEM certificates
|
||||
// found in the filepath specified in the caCertificatesEnvVar OS environment
|
||||
// variable. If the caCertificatesEnvVar is not set then initCertPool will
|
||||
// return nil. If there is an error creating a *x509.CertPool from the provided
|
||||
// caCertificatesEnvVar value then initCertPool will panic.
|
||||
func initCertPool() *x509.CertPool {
|
||||
if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" {
|
||||
customCAs, err := ioutil.ReadFile(customCACertsPath)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error reading %s=%q: %v",
|
||||
caCertificatesEnvVar, customCACertsPath, err))
|
||||
}
|
||||
certPool := x509.NewCertPool()
|
||||
if ok := certPool.AppendCertsFromPEM(customCAs); !ok {
|
||||
panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v",
|
||||
caCertificatesEnvVar, customCACertsPath, err))
|
||||
}
|
||||
return certPool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// httpHead performs a HEAD request with a proper User-Agent string.
|
||||
// The response body (resp.Body) is already closed when this function returns.
|
||||
func httpHead(url string) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to head %q: %v", url, err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
resp, err = HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("failed to do head %q: %v", url, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// httpPost performs a POST request with a proper User-Agent string.
|
||||
// Callers should close resp.Body when done reading from it.
|
||||
func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest(http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to post %q: %v", url, err)
|
||||
}
|
||||
req.Header.Set("Content-Type", bodyType)
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
return HTTPClient.Do(req)
|
||||
}
|
||||
|
||||
// httpGet performs a GET request with a proper User-Agent string.
|
||||
// Callers should close resp.Body when done reading from it.
|
||||
func httpGet(url string) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get %q: %v", url, err)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
return HTTPClient.Do(req)
|
||||
}
|
||||
|
||||
// getJSON performs an HTTP GET request and parses the response body
|
||||
// as JSON, into the provided respBody object.
|
||||
func getJSON(uri string, respBody interface{}) (http.Header, error) {
|
||||
resp, err := httpGet(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get json %q: %v", uri, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return resp.Header, handleHTTPError(resp)
|
||||
}
|
||||
|
||||
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
|
||||
}
|
||||
|
||||
// postJSON performs an HTTP POST request and parses the response body
|
||||
// as JSON, into the provided respBody object.
|
||||
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
|
||||
jsonBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to marshal network message")
|
||||
}
|
||||
|
||||
resp, err := post(j, uri, jsonBytes, respBody)
|
||||
if resp == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
return resp.Header, err
|
||||
}
|
||||
|
||||
func postAsGet(j *jws, uri string, respBody interface{}) (*http.Response, error) {
|
||||
return post(j, uri, []byte{}, respBody)
|
||||
}
|
||||
|
||||
func post(j *jws, uri string, reqBody []byte, respBody interface{}) (*http.Response, error) {
|
||||
resp, err := j.post(uri, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to post JWS message. -> %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
err = handleHTTPError(resp)
|
||||
switch err.(type) {
|
||||
case NonceError:
|
||||
// Retry once if the nonce was invalidated
|
||||
|
||||
retryResp, errP := j.post(uri, reqBody)
|
||||
if errP != nil {
|
||||
return nil, fmt.Errorf("failed to post JWS message. -> %v", errP)
|
||||
}
|
||||
|
||||
if retryResp.StatusCode >= http.StatusBadRequest {
|
||||
return retryResp, handleHTTPError(retryResp)
|
||||
}
|
||||
|
||||
if respBody == nil {
|
||||
return retryResp, nil
|
||||
}
|
||||
|
||||
return retryResp, json.NewDecoder(retryResp.Body).Decode(respBody)
|
||||
default:
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
if respBody == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return resp, json.NewDecoder(resp.Body).Decode(respBody)
|
||||
}
|
||||
|
||||
// userAgent builds and returns the User-Agent string to use in requests.
|
||||
func userAgent() string {
|
||||
ua := fmt.Sprintf("%s %s (%s; %s; %s)", UserAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
|
||||
return strings.TrimSpace(ua)
|
||||
}
|
42
vendor/github.com/xenolf/lego/acme/http_challenge.go
generated
vendored
42
vendor/github.com/xenolf/lego/acme/http_challenge.go
generated
vendored
@ -1,42 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
type httpChallenge struct {
|
||||
jws *jws
|
||||
validate validateFunc
|
||||
provider ChallengeProvider
|
||||
}
|
||||
|
||||
// HTTP01ChallengePath returns the URL path for the `http-01` challenge
|
||||
func HTTP01ChallengePath(token string) string {
|
||||
return "/.well-known/acme-challenge/" + token
|
||||
}
|
||||
|
||||
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
|
||||
|
||||
log.Infof("[%s] acme: Trying to solve HTTP-01", domain)
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.provider.Present(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
||||
}
|
||||
defer func() {
|
||||
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
log.Warnf("[%s] error cleaning up: %v", domain, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||
}
|
167
vendor/github.com/xenolf/lego/acme/jws.go
generated
vendored
167
vendor/github.com/xenolf/lego/acme/jws.go
generated
vendored
@ -1,167 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
type jws struct {
|
||||
getNonceURL string
|
||||
privKey crypto.PrivateKey
|
||||
kid string
|
||||
nonces nonceManager
|
||||
}
|
||||
|
||||
// Posts a JWS signed message to the specified URL.
|
||||
// It does NOT close the response body, so the caller must
|
||||
// do that if no error was returned.
|
||||
func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
||||
signedContent, err := j.signContent(url, content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
|
||||
}
|
||||
|
||||
data := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
|
||||
resp, err := httpPost(url, "application/jose+json", data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to HTTP POST to %s -> %s", url, err.Error())
|
||||
}
|
||||
|
||||
nonce, nonceErr := getNonceFromResponse(resp)
|
||||
if nonceErr == nil {
|
||||
j.nonces.Push(nonce)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) {
|
||||
|
||||
var alg jose.SignatureAlgorithm
|
||||
switch k := j.privKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
alg = jose.RS256
|
||||
case *ecdsa.PrivateKey:
|
||||
if k.Curve == elliptic.P256() {
|
||||
alg = jose.ES256
|
||||
} else if k.Curve == elliptic.P384() {
|
||||
alg = jose.ES384
|
||||
}
|
||||
}
|
||||
|
||||
jsonKey := jose.JSONWebKey{
|
||||
Key: j.privKey,
|
||||
KeyID: j.kid,
|
||||
}
|
||||
|
||||
signKey := jose.SigningKey{
|
||||
Algorithm: alg,
|
||||
Key: jsonKey,
|
||||
}
|
||||
options := jose.SignerOptions{
|
||||
NonceSource: j,
|
||||
ExtraHeaders: make(map[jose.HeaderKey]interface{}),
|
||||
}
|
||||
options.ExtraHeaders["url"] = url
|
||||
if j.kid == "" {
|
||||
options.EmbedJWK = true
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(signKey, &options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create jose signer -> %s", err.Error())
|
||||
}
|
||||
|
||||
signed, err := signer.Sign(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
|
||||
}
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
|
||||
jwk := jose.JSONWebKey{Key: j.privKey}
|
||||
jwkJSON, err := jwk.Public().MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error())
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(
|
||||
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
|
||||
&jose.SignerOptions{
|
||||
EmbedJWK: false,
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"kid": kid,
|
||||
"url": url,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %s", err.Error())
|
||||
}
|
||||
|
||||
signed, err := signer.Sign(jwkJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to External Account Binding sign content -> %s", err.Error())
|
||||
}
|
||||
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
func (j *jws) Nonce() (string, error) {
|
||||
if nonce, ok := j.nonces.Pop(); ok {
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
return getNonce(j.getNonceURL)
|
||||
}
|
||||
|
||||
type nonceManager struct {
|
||||
nonces []string
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (n *nonceManager) Pop() (string, bool) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
if len(n.nonces) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
nonce := n.nonces[len(n.nonces)-1]
|
||||
n.nonces = n.nonces[:len(n.nonces)-1]
|
||||
return nonce, true
|
||||
}
|
||||
|
||||
func (n *nonceManager) Push(nonce string) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
n.nonces = append(n.nonces, nonce)
|
||||
}
|
||||
|
||||
func getNonce(url string) (string, error) {
|
||||
resp, err := httpHead(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %s", err.Error())
|
||||
}
|
||||
|
||||
return getNonceFromResponse(resp)
|
||||
}
|
||||
|
||||
func getNonceFromResponse(resp *http.Response) (string, error) {
|
||||
nonce := resp.Header.Get("Replay-Nonce")
|
||||
if nonce == "" {
|
||||
return "", fmt.Errorf("server did not respond with a proper nonce header")
|
||||
}
|
||||
|
||||
return nonce, nil
|
||||
}
|
103
vendor/github.com/xenolf/lego/acme/messages.go
generated
vendored
103
vendor/github.com/xenolf/lego/acme/messages.go
generated
vendored
@ -1,103 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RegistrationResource represents all important informations about a registration
|
||||
// of which the client needs to keep track itself.
|
||||
type RegistrationResource struct {
|
||||
Body accountMessage `json:"body,omitempty"`
|
||||
URI string `json:"uri,omitempty"`
|
||||
}
|
||||
|
||||
type directory struct {
|
||||
NewNonceURL string `json:"newNonce"`
|
||||
NewAccountURL string `json:"newAccount"`
|
||||
NewOrderURL string `json:"newOrder"`
|
||||
RevokeCertURL string `json:"revokeCert"`
|
||||
KeyChangeURL string `json:"keyChange"`
|
||||
Meta struct {
|
||||
TermsOfService string `json:"termsOfService"`
|
||||
Website string `json:"website"`
|
||||
CaaIdentities []string `json:"caaIdentities"`
|
||||
ExternalAccountRequired bool `json:"externalAccountRequired"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
|
||||
type accountMessage struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
||||
Orders string `json:"orders,omitempty"`
|
||||
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
|
||||
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
|
||||
}
|
||||
|
||||
type orderResource struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
orderMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type orderMessage struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
Expires string `json:"expires,omitempty"`
|
||||
Identifiers []identifier `json:"identifiers"`
|
||||
NotBefore string `json:"notBefore,omitempty"`
|
||||
NotAfter string `json:"notAfter,omitempty"`
|
||||
Authorizations []string `json:"authorizations,omitempty"`
|
||||
Finalize string `json:"finalize,omitempty"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
}
|
||||
|
||||
type authorization struct {
|
||||
Status string `json:"status"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Identifier identifier `json:"identifier"`
|
||||
Challenges []challenge `json:"challenges"`
|
||||
}
|
||||
|
||||
type identifier struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type challenge struct {
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Validated time.Time `json:"validated"`
|
||||
KeyAuthorization string `json:"keyAuthorization"`
|
||||
Error RemoteError `json:"error"`
|
||||
}
|
||||
|
||||
type csrMessage struct {
|
||||
Csr string `json:"csr"`
|
||||
}
|
||||
|
||||
type revokeCertMessage struct {
|
||||
Certificate string `json:"certificate"`
|
||||
}
|
||||
|
||||
type deactivateAuthMessage struct {
|
||||
Status string `jsom:"status"`
|
||||
}
|
||||
|
||||
// CertificateResource represents a CA issued certificate.
|
||||
// PrivateKey, Certificate and IssuerCertificate are all
|
||||
// already PEM encoded and can be directly written to disk.
|
||||
// Certificate may be a certificate bundle, depending on the
|
||||
// options supplied to create it.
|
||||
type CertificateResource struct {
|
||||
Domain string `json:"domain"`
|
||||
CertURL string `json:"certUrl"`
|
||||
CertStableURL string `json:"certStableUrl"`
|
||||
AccountRef string `json:"accountRef,omitempty"`
|
||||
PrivateKey []byte `json:"-"`
|
||||
Certificate []byte `json:"-"`
|
||||
IssuerCertificate []byte `json:"-"`
|
||||
CSR []byte `json:"-"`
|
||||
}
|
104
vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go
generated
vendored
104
vendor/github.com/xenolf/lego/acme/tls_alpn_challenge.go
generated
vendored
@ -1,104 +0,0 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.
|
||||
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1
|
||||
var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
|
||||
|
||||
type tlsALPNChallenge struct {
|
||||
jws *jws
|
||||
validate validateFunc
|
||||
provider ChallengeProvider
|
||||
}
|
||||
|
||||
// Solve manages the provider to validate and solve the challenge.
|
||||
func (t *tlsALPNChallenge) Solve(chlng challenge, domain string) error {
|
||||
log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", domain)
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.provider.Present(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
||||
}
|
||||
defer func() {
|
||||
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
log.Warnf("[%s] error cleaning up: %v", domain, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return t.validate(t.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||
}
|
||||
|
||||
// TLSALPNChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension
|
||||
// and domain name for the `tls-alpn-01` challenge.
|
||||
func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {
|
||||
// Compute the SHA-256 digest of the key authorization.
|
||||
zBytes := sha256.Sum256([]byte(keyAuth))
|
||||
|
||||
value, err := asn1.Marshal(zBytes[:sha256.Size])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Add the keyAuth digest as the acmeValidation-v1 extension
|
||||
// (marked as critical such that it won't be used by non-ACME software).
|
||||
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3
|
||||
extensions := []pkix.Extension{
|
||||
{
|
||||
Id: idPeAcmeIdentifierV1,
|
||||
Critical: true,
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
|
||||
// Generate a new RSA key for the certificates.
|
||||
tempPrivKey, err := generatePrivateKey(RSA2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
|
||||
|
||||
// Generate the PEM certificate using the provided private key, domain, and extra extensions.
|
||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain, extensions)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair.
|
||||
rsaPrivPEM := pemEncode(rsaPrivKey)
|
||||
|
||||
return tempCertPEM, rsaPrivPEM, nil
|
||||
}
|
||||
|
||||
// TLSALPNChallengeCert returns a certificate with the acmeValidation-v1 extension
|
||||
// and domain name for the `tls-alpn-01` challenge.
|
||||
func TLSALPNChallengeCert(domain, keyAuth string) (*tls.Certificate, error) {
|
||||
tempCertPEM, rsaPrivPEM, err := TLSALPNChallengeBlocks(domain, keyAuth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &certificate, nil
|
||||
}
|
21
vendor/github.com/xenolf/lego/certcrypto/LICENSE
generated
vendored
Normal file
21
vendor/github.com/xenolf/lego/certcrypto/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 Sebastian Erhart
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
252
vendor/github.com/xenolf/lego/certcrypto/crypto.go
generated
vendored
Normal file
252
vendor/github.com/xenolf/lego/certcrypto/crypto.go
generated
vendored
Normal file
@ -0,0 +1,252 @@
|
||||
package certcrypto
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// Constants for all key types we support.
|
||||
const (
|
||||
EC256 = KeyType("P256")
|
||||
EC384 = KeyType("P384")
|
||||
RSA2048 = KeyType("2048")
|
||||
RSA4096 = KeyType("4096")
|
||||
RSA8192 = KeyType("8192")
|
||||
)
|
||||
|
||||
const (
|
||||
// OCSPGood means that the certificate is valid.
|
||||
OCSPGood = ocsp.Good
|
||||
// OCSPRevoked means that the certificate has been deliberately revoked.
|
||||
OCSPRevoked = ocsp.Revoked
|
||||
// OCSPUnknown means that the OCSP responder doesn't know about the certificate.
|
||||
OCSPUnknown = ocsp.Unknown
|
||||
// OCSPServerFailed means that the OCSP responder failed to process the request.
|
||||
OCSPServerFailed = ocsp.ServerFailed
|
||||
)
|
||||
|
||||
// Constants for OCSP must staple
|
||||
var (
|
||||
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
||||
ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
||||
)
|
||||
|
||||
// KeyType represents the key algo as well as the key size or curve to use.
|
||||
type KeyType string
|
||||
|
||||
type DERCertificateBytes []byte
|
||||
|
||||
// ParsePEMBundle parses a certificate bundle from top to bottom and returns
|
||||
// a slice of x509 certificates. This function will error if no certificates are found.
|
||||
func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||
var certificates []*x509.Certificate
|
||||
var certDERBlock *pem.Block
|
||||
|
||||
for {
|
||||
certDERBlock, bundle = pem.Decode(bundle)
|
||||
if certDERBlock == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if certDERBlock.Type == "CERTIFICATE" {
|
||||
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificates = append(certificates, cert)
|
||||
}
|
||||
}
|
||||
|
||||
if len(certificates) == 0 {
|
||||
return nil, errors.New("no certificates were found while parsing the bundle")
|
||||
}
|
||||
|
||||
return certificates, nil
|
||||
}
|
||||
|
||||
func ParsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
|
||||
keyBlock, _ := pem.Decode(key)
|
||||
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
default:
|
||||
return nil, errors.New("unknown PEM header value")
|
||||
}
|
||||
}
|
||||
|
||||
func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
|
||||
switch keyType {
|
||||
case EC256:
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
case EC384:
|
||||
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
case RSA2048:
|
||||
return rsa.GenerateKey(rand.Reader, 2048)
|
||||
case RSA4096:
|
||||
return rsa.GenerateKey(rand.Reader, 4096)
|
||||
case RSA8192:
|
||||
return rsa.GenerateKey(rand.Reader, 8192)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid KeyType: %s", keyType)
|
||||
}
|
||||
|
||||
func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
|
||||
template := x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: domain},
|
||||
DNSNames: san,
|
||||
}
|
||||
|
||||
if mustStaple {
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
|
||||
Id: tlsFeatureExtensionOID,
|
||||
Value: ocspMustStapleFeature,
|
||||
})
|
||||
}
|
||||
|
||||
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
|
||||
}
|
||||
|
||||
func PEMEncode(data interface{}) []byte {
|
||||
var pemBlock *pem.Block
|
||||
switch key := data.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
keyBytes, _ := x509.MarshalECPrivateKey(key)
|
||||
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
||||
case *rsa.PrivateKey:
|
||||
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||
case *x509.CertificateRequest:
|
||||
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
||||
case DERCertificateBytes:
|
||||
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(DERCertificateBytes))}
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(pemBlock)
|
||||
}
|
||||
|
||||
func pemDecode(data []byte) (*pem.Block, error) {
|
||||
pemBlock, _ := pem.Decode(data)
|
||||
if pemBlock == nil {
|
||||
return nil, fmt.Errorf("PEM decode did not yield a valid block. Is the certificate in the right format?")
|
||||
}
|
||||
|
||||
return pemBlock, nil
|
||||
}
|
||||
|
||||
func PemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
|
||||
pemBlock, err := pemDecode(pem)
|
||||
if pemBlock == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pemBlock.Type != "CERTIFICATE REQUEST" {
|
||||
return nil, fmt.Errorf("PEM block is not a certificate request")
|
||||
}
|
||||
|
||||
return x509.ParseCertificateRequest(pemBlock.Bytes)
|
||||
}
|
||||
|
||||
// ParsePEMCertificate returns Certificate from a PEM encoded certificate.
|
||||
// The certificate has to be PEM encoded. Any other encodings like DER will fail.
|
||||
func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {
|
||||
pemBlock, err := pemDecode(cert)
|
||||
if pemBlock == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// from a DER encoded certificate
|
||||
return x509.ParseCertificate(pemBlock.Bytes)
|
||||
}
|
||||
|
||||
func ExtractDomains(cert *x509.Certificate) []string {
|
||||
domains := []string{cert.Subject.CommonName}
|
||||
|
||||
// Check for SAN certificate
|
||||
for _, sanDomain := range cert.DNSNames {
|
||||
if sanDomain == cert.Subject.CommonName {
|
||||
continue
|
||||
}
|
||||
domains = append(domains, sanDomain)
|
||||
}
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
|
||||
domains := []string{csr.Subject.CommonName}
|
||||
|
||||
// loop over the SubjectAltName DNS names
|
||||
for _, sanName := range csr.DNSNames {
|
||||
if containsSAN(domains, sanName) {
|
||||
// Duplicate; skip this name
|
||||
continue
|
||||
}
|
||||
|
||||
// Name is unique
|
||||
domains = append(domains, sanName)
|
||||
}
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
func containsSAN(domains []string, sanName string) bool {
|
||||
for _, existingName := range domains {
|
||||
if existingName == sanName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
|
||||
derBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
|
||||
}
|
||||
|
||||
func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if expiration.IsZero() {
|
||||
expiration = time.Now().Add(365)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "ACME Challenge TEMP",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: expiration,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{domain},
|
||||
ExtraExtensions: extensions,
|
||||
}
|
||||
|
||||
return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
}
|
21
vendor/github.com/xenolf/lego/certificate/LICENSE
generated
vendored
Normal file
21
vendor/github.com/xenolf/lego/certificate/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 Sebastian Erhart
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
69
vendor/github.com/xenolf/lego/certificate/authorization.go
generated
vendored
Normal file
69
vendor/github.com/xenolf/lego/certificate/authorization.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// overallRequestLimit is the overall number of request per second
|
||||
// limited on the "new-reg", "new-authz" and "new-cert" endpoints.
|
||||
// From the documentation the limitation is 20 requests per second,
|
||||
// but using 20 as value doesn't work but 18 do
|
||||
overallRequestLimit = 18
|
||||
)
|
||||
|
||||
func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) {
|
||||
resc, errc := make(chan acme.Authorization), make(chan domainError)
|
||||
|
||||
delay := time.Second / overallRequestLimit
|
||||
|
||||
for _, authzURL := range order.Authorizations {
|
||||
time.Sleep(delay)
|
||||
|
||||
go func(authzURL string) {
|
||||
authz, err := c.core.Authorizations.Get(authzURL)
|
||||
if err != nil {
|
||||
errc <- domainError{Domain: authz.Identifier.Value, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
resc <- authz
|
||||
}(authzURL)
|
||||
}
|
||||
|
||||
var responses []acme.Authorization
|
||||
failures := make(obtainError)
|
||||
for i := 0; i < len(order.Authorizations); i++ {
|
||||
select {
|
||||
case res := <-resc:
|
||||
responses = append(responses, res)
|
||||
case err := <-errc:
|
||||
failures[err.Domain] = err.Error
|
||||
}
|
||||
}
|
||||
|
||||
for i, auth := range order.Authorizations {
|
||||
log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth)
|
||||
}
|
||||
|
||||
close(resc)
|
||||
close(errc)
|
||||
|
||||
// be careful to not return an empty failures map;
|
||||
// even if empty, they become non-nil error values
|
||||
if len(failures) > 0 {
|
||||
return responses, failures
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder) {
|
||||
for _, auth := range order.Authorizations {
|
||||
if err := c.core.Authorizations.Deactivate(auth); err != nil {
|
||||
log.Infof("Unable to deactivated authorizations: %s", auth)
|
||||
}
|
||||
}
|
||||
}
|
493
vendor/github.com/xenolf/lego/certificate/certificates.go
generated
vendored
Normal file
493
vendor/github.com/xenolf/lego/certificate/certificates.go
generated
vendored
Normal file
@ -0,0 +1,493 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/acme/api"
|
||||
"github.com/xenolf/lego/certcrypto"
|
||||
"github.com/xenolf/lego/challenge"
|
||||
"github.com/xenolf/lego/log"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
// maxBodySize is the maximum size of body that we will read.
|
||||
const maxBodySize = 1024 * 1024
|
||||
|
||||
// Resource represents a CA issued certificate.
|
||||
// PrivateKey, Certificate and IssuerCertificate are all
|
||||
// already PEM encoded and can be directly written to disk.
|
||||
// Certificate may be a certificate bundle,
|
||||
// depending on the options supplied to create it.
|
||||
type Resource struct {
|
||||
Domain string `json:"domain"`
|
||||
CertURL string `json:"certUrl"`
|
||||
CertStableURL string `json:"certStableUrl"`
|
||||
PrivateKey []byte `json:"-"`
|
||||
Certificate []byte `json:"-"`
|
||||
IssuerCertificate []byte `json:"-"`
|
||||
CSR []byte `json:"-"`
|
||||
}
|
||||
|
||||
// ObtainRequest The request to obtain certificate.
|
||||
//
|
||||
// The first domain in domains is used for the CommonName field of the certificate,
|
||||
// all other domains are added using the Subject Alternate Names extension.
|
||||
//
|
||||
// A new private key is generated for every invocation of the function Obtain.
|
||||
// If you do not want that you can supply your own private key in the privateKey parameter.
|
||||
// If this parameter is non-nil it will be used instead of generating a new one.
|
||||
//
|
||||
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
|
||||
type ObtainRequest struct {
|
||||
Domains []string
|
||||
Bundle bool
|
||||
PrivateKey crypto.PrivateKey
|
||||
MustStaple bool
|
||||
}
|
||||
|
||||
type resolver interface {
|
||||
Solve(authorizations []acme.Authorization) error
|
||||
}
|
||||
|
||||
type Certifier struct {
|
||||
core *api.Core
|
||||
keyType certcrypto.KeyType
|
||||
resolver resolver
|
||||
}
|
||||
|
||||
func NewCertifier(core *api.Core, keyType certcrypto.KeyType, resolver resolver) *Certifier {
|
||||
return &Certifier{
|
||||
core: core,
|
||||
keyType: keyType,
|
||||
resolver: resolver,
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain tries to obtain a single certificate using all domains passed into it.
|
||||
//
|
||||
// This function will never return a partial certificate.
|
||||
// If one domain in the list fails, the whole certificate will fail.
|
||||
func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
|
||||
if len(request.Domains) == 0 {
|
||||
return nil, errors.New("no domains to obtain a certificate for")
|
||||
}
|
||||
|
||||
domains := sanitizeDomain(request.Domains)
|
||||
|
||||
if request.Bundle {
|
||||
log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
|
||||
} else {
|
||||
log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
|
||||
}
|
||||
|
||||
order, err := c.core.Orders.New(domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authz, err := c.getAuthorizations(order)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
c.deactivateAuthorizations(order)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.resolver.Solve(authz)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
failures := make(obtainError)
|
||||
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple)
|
||||
if err != nil {
|
||||
for _, auth := range authz {
|
||||
failures[challenge.GetTargetedDomain(auth)] = err
|
||||
}
|
||||
}
|
||||
|
||||
// Do not return an empty failures map, because
|
||||
// it would still be a non-nil error value
|
||||
if len(failures) > 0 {
|
||||
return cert, failures
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// ObtainForCSR tries to obtain a certificate matching the CSR passed into it.
|
||||
//
|
||||
// The domains are inferred from the CommonName and SubjectAltNames, if any.
|
||||
// The private key for this CSR is not required.
|
||||
//
|
||||
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
|
||||
//
|
||||
// This function will never return a partial certificate.
|
||||
// If one domain in the list fails, the whole certificate will fail.
|
||||
func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Resource, error) {
|
||||
// figure out what domains it concerns
|
||||
// start with the common name
|
||||
domains := certcrypto.ExtractDomainsCSR(&csr)
|
||||
|
||||
if bundle {
|
||||
log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||
} else {
|
||||
log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||
}
|
||||
|
||||
order, err := c.core.Orders.New(domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authz, err := c.getAuthorizations(order)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
c.deactivateAuthorizations(order)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.resolver.Solve(authz)
|
||||
if err != nil {
|
||||
// If any challenge fails, return. Do not generate partial SAN certificates.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
failures := make(obtainError)
|
||||
cert, err := c.getForCSR(domains, order, bundle, csr.Raw, nil)
|
||||
if err != nil {
|
||||
for _, auth := range authz {
|
||||
failures[challenge.GetTargetedDomain(auth)] = err
|
||||
}
|
||||
}
|
||||
|
||||
if cert != nil {
|
||||
// Add the CSR to the certificate so that it can be used for renewals.
|
||||
cert.CSR = certcrypto.PEMEncode(&csr)
|
||||
}
|
||||
|
||||
// Do not return an empty failures map,
|
||||
// because it would still be a non-nil error value
|
||||
if len(failures) > 0 {
|
||||
return cert, failures
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool) (*Resource, error) {
|
||||
if privateKey == nil {
|
||||
var err error
|
||||
privateKey, err = certcrypto.GeneratePrivateKey(c.keyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Determine certificate name(s) based on the authorization resources
|
||||
commonName := domains[0]
|
||||
|
||||
// ACME draft Section 7.4 "Applying for Certificate Issuance"
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
|
||||
// says:
|
||||
// Clients SHOULD NOT make any assumptions about the sort order of
|
||||
// "identifiers" or "authorizations" elements in the returned order
|
||||
// object.
|
||||
san := []string{commonName}
|
||||
for _, auth := range order.Identifiers {
|
||||
if auth.Value != commonName {
|
||||
san = append(san, auth.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey))
|
||||
}
|
||||
|
||||
func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr []byte, privateKeyPem []byte) (*Resource, error) {
|
||||
respOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commonName := domains[0]
|
||||
certRes := &Resource{
|
||||
Domain: commonName,
|
||||
CertURL: respOrder.Certificate,
|
||||
PrivateKey: privateKeyPem,
|
||||
}
|
||||
|
||||
if respOrder.Status == acme.StatusValid {
|
||||
// if the certificate is available right away, short cut!
|
||||
ok, err := c.checkResponse(respOrder, certRes, bundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
return certRes, nil
|
||||
}
|
||||
}
|
||||
|
||||
return c.waitForCertificate(certRes, order.Location, bundle)
|
||||
}
|
||||
|
||||
func (c *Certifier) waitForCertificate(certRes *Resource, orderURL string, bundle bool) (*Resource, error) {
|
||||
stopTimer := time.NewTimer(30 * time.Second)
|
||||
defer stopTimer.Stop()
|
||||
retryTick := time.NewTicker(500 * time.Millisecond)
|
||||
defer retryTick.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopTimer.C:
|
||||
return nil, errors.New("certificate polling timed out")
|
||||
case <-retryTick.C:
|
||||
order, err := c.core.Orders.Get(orderURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done, err := c.checkResponse(order, certRes, bundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if done {
|
||||
return certRes, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkResponse checks to see if the certificate is ready and a link is contained in the response.
|
||||
//
|
||||
// If so, loads it into certRes and returns true.
|
||||
// If the cert is not yet ready, it returns false.
|
||||
//
|
||||
// The certRes input should already have the Domain (common name) field populated.
|
||||
//
|
||||
// If bundle is true, the certificate will be bundled with the issuer's cert.
|
||||
func (c *Certifier) checkResponse(order acme.Order, certRes *Resource, bundle bool) (bool, error) {
|
||||
valid, err := checkOrderStatus(order)
|
||||
if err != nil || !valid {
|
||||
return valid, err
|
||||
}
|
||||
|
||||
cert, issuer, err := c.core.Certificates.Get(order.Certificate, bundle)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Infof("[%s] Server responded with a certificate.", certRes.Domain)
|
||||
|
||||
certRes.IssuerCertificate = issuer
|
||||
certRes.Certificate = cert
|
||||
certRes.CertURL = order.Certificate
|
||||
certRes.CertStableURL = order.Certificate
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
||||
func (c *Certifier) Revoke(cert []byte) error {
|
||||
certificates, err := certcrypto.ParsePEMBundle(cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x509Cert := certificates[0]
|
||||
if x509Cert.IsCA {
|
||||
return fmt.Errorf("certificate bundle starts with a CA certificate")
|
||||
}
|
||||
|
||||
revokeMsg := acme.RevokeCertMessage{
|
||||
Certificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw),
|
||||
}
|
||||
|
||||
return c.core.Certificates.Revoke(revokeMsg)
|
||||
}
|
||||
|
||||
// Renew takes a Resource and tries to renew the certificate.
|
||||
//
|
||||
// If the renewal process succeeds, the new certificate will ge returned in a new CertResource.
|
||||
// Please be aware that this function will return a new certificate in ANY case that is not an error.
|
||||
// If the server does not provide us with a new cert on a GET request to the CertURL
|
||||
// this function will start a new-cert flow where a new certificate gets generated.
|
||||
//
|
||||
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
|
||||
//
|
||||
// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
|
||||
func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool) (*Resource, error) {
|
||||
// Input certificate is PEM encoded.
|
||||
// Decode it here as we may need the decoded cert later on in the renewal process.
|
||||
// The input may be a bundle or a single certificate.
|
||||
certificates, err := certcrypto.ParsePEMBundle(certRes.Certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x509Cert := certificates[0]
|
||||
if x509Cert.IsCA {
|
||||
return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", certRes.Domain)
|
||||
}
|
||||
|
||||
// This is just meant to be informal for the user.
|
||||
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
||||
log.Infof("[%s] acme: Trying renewal with %d hours remaining", certRes.Domain, int(timeLeft.Hours()))
|
||||
|
||||
// We always need to request a new certificate to renew.
|
||||
// Start by checking to see if the certificate was based off a CSR,
|
||||
// and use that if it's defined.
|
||||
if len(certRes.CSR) > 0 {
|
||||
csr, errP := certcrypto.PemDecodeTox509CSR(certRes.CSR)
|
||||
if errP != nil {
|
||||
return nil, errP
|
||||
}
|
||||
|
||||
return c.ObtainForCSR(*csr, bundle)
|
||||
}
|
||||
|
||||
var privateKey crypto.PrivateKey
|
||||
if certRes.PrivateKey != nil {
|
||||
privateKey, err = certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
query := ObtainRequest{
|
||||
Domains: certcrypto.ExtractDomains(x509Cert),
|
||||
Bundle: bundle,
|
||||
PrivateKey: privateKey,
|
||||
MustStaple: mustStaple,
|
||||
}
|
||||
return c.Obtain(query)
|
||||
}
|
||||
|
||||
// GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response,
|
||||
// the parsed response, and an error, if any.
|
||||
//
|
||||
// The returned []byte can be passed directly into the OCSPStaple property of a tls.Certificate.
|
||||
// If the bundle only contains the issued certificate,
|
||||
// this function will try to get the issuer certificate from the IssuingCertificateURL in the certificate.
|
||||
//
|
||||
// If the []byte and/or ocsp.Response return values are nil, the OCSP status may be assumed OCSPUnknown.
|
||||
func (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) {
|
||||
certificates, err := certcrypto.ParsePEMBundle(bundle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We expect the certificate slice to be ordered downwards the chain.
|
||||
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
|
||||
// which should always be the first two certificates.
|
||||
// If there's no OCSP server listed in the leaf cert, there's nothing to do.
|
||||
// And if we have only one certificate so far, we need to get the issuer cert.
|
||||
|
||||
issuedCert := certificates[0]
|
||||
|
||||
if len(issuedCert.OCSPServer) == 0 {
|
||||
return nil, nil, errors.New("no OCSP server specified in cert")
|
||||
}
|
||||
|
||||
if len(certificates) == 1 {
|
||||
// TODO: build fallback. If this fails, check the remaining array entries.
|
||||
if len(issuedCert.IssuingCertificateURL) == 0 {
|
||||
return nil, nil, errors.New("no issuing certificate URL")
|
||||
}
|
||||
|
||||
resp, errC := c.core.HTTPClient.Get(issuedCert.IssuingCertificateURL[0])
|
||||
if errC != nil {
|
||||
return nil, nil, errC
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
issuerBytes, errC := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
|
||||
if errC != nil {
|
||||
return nil, nil, errC
|
||||
}
|
||||
|
||||
issuerCert, errC := x509.ParseCertificate(issuerBytes)
|
||||
if errC != nil {
|
||||
return nil, nil, errC
|
||||
}
|
||||
|
||||
// Insert it into the slice on position 0
|
||||
// We want it ordered right SRV CRT -> CA
|
||||
certificates = append(certificates, issuerCert)
|
||||
}
|
||||
|
||||
issuerCert := certificates[1]
|
||||
|
||||
// Finally kick off the OCSP request.
|
||||
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := c.core.HTTPClient.Post(issuedCert.OCSPServer[0], "application/ocsp-request", bytes.NewReader(ocspReq))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ocspResBytes, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return ocspResBytes, ocspRes, nil
|
||||
}
|
||||
|
||||
func checkOrderStatus(order acme.Order) (bool, error) {
|
||||
switch order.Status {
|
||||
case acme.StatusValid:
|
||||
return true, nil
|
||||
case acme.StatusInvalid:
|
||||
return false, order.Error
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.4
|
||||
// The domain name MUST be encoded
|
||||
// in the form in which it would appear in a certificate. That is, it
|
||||
// MUST be encoded according to the rules in Section 7 of [RFC5280].
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc5280#section-7
|
||||
func sanitizeDomain(domains []string) []string {
|
||||
var sanitizedDomains []string
|
||||
for _, domain := range domains {
|
||||
sanitizedDomain, err := idna.ToASCII(domain)
|
||||
if err != nil {
|
||||
log.Infof("skip domain %q: unable to sanitize (punnycode): %v", domain, err)
|
||||
} else {
|
||||
sanitizedDomains = append(sanitizedDomains, sanitizedDomain)
|
||||
}
|
||||
}
|
||||
return sanitizedDomains
|
||||
}
|
30
vendor/github.com/xenolf/lego/certificate/errors.go
generated
vendored
Normal file
30
vendor/github.com/xenolf/lego/certificate/errors.go
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// obtainError is returned when there are specific errors available per domain.
|
||||
type obtainError map[string]error
|
||||
|
||||
func (e obtainError) Error() string {
|
||||
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
|
||||
|
||||
var domains []string
|
||||
for domain := range e {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
sort.Strings(domains)
|
||||
|
||||
for _, domain := range domains {
|
||||
buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain]))
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
type domainError struct {
|
||||
Domain string
|
||||
Error error
|
||||
}
|
21
vendor/github.com/xenolf/lego/challenge/LICENSE
generated
vendored
Normal file
21
vendor/github.com/xenolf/lego/challenge/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 Sebastian Erhart
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
44
vendor/github.com/xenolf/lego/challenge/challenges.go
generated
vendored
Normal file
44
vendor/github.com/xenolf/lego/challenge/challenges.go
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Type is a string that identifies a particular challenge type and version of ACME challenge.
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// HTTP01 is the "http-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.3
|
||||
// Note: ChallengePath returns the URL path to fulfill this challenge
|
||||
HTTP01 = Type("http-01")
|
||||
|
||||
// DNS01 is the "dns-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.4
|
||||
// Note: GetRecord returns a DNS record which will fulfill this challenge
|
||||
DNS01 = Type("dns-01")
|
||||
|
||||
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05
|
||||
TLSALPN01 = Type("tls-alpn-01")
|
||||
)
|
||||
|
||||
func (t Type) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
func FindChallenge(chlgType Type, authz acme.Authorization) (acme.Challenge, error) {
|
||||
for _, chlg := range authz.Challenges {
|
||||
if chlg.Type == string(chlgType) {
|
||||
return chlg, nil
|
||||
}
|
||||
}
|
||||
|
||||
return acme.Challenge{}, fmt.Errorf("[%s] acme: unable to find challenge %s", GetTargetedDomain(authz), chlgType)
|
||||
}
|
||||
|
||||
func GetTargetedDomain(authz acme.Authorization) string {
|
||||
if authz.Wildcard {
|
||||
return "*." + authz.Identifier.Value
|
||||
}
|
||||
return authz.Identifier.Value
|
||||
}
|
174
vendor/github.com/xenolf/lego/challenge/dns01/dns_challenge.go
generated
vendored
Normal file
174
vendor/github.com/xenolf/lego/challenge/dns01/dns_challenge.go
generated
vendored
Normal file
@ -0,0 +1,174 @@
|
||||
package dns01
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/acme/api"
|
||||
"github.com/xenolf/lego/challenge"
|
||||
"github.com/xenolf/lego/log"
|
||||
"github.com/xenolf/lego/platform/wait"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultPropagationTimeout default propagation timeout
|
||||
DefaultPropagationTimeout = 60 * time.Second
|
||||
|
||||
// DefaultPollingInterval default polling interval
|
||||
DefaultPollingInterval = 2 * time.Second
|
||||
|
||||
// DefaultTTL default TTL
|
||||
DefaultTTL = 120
|
||||
)
|
||||
|
||||
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
|
||||
|
||||
type ChallengeOption func(*Challenge) error
|
||||
|
||||
// CondOption Conditional challenge option.
|
||||
func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
|
||||
if !condition {
|
||||
// NoOp options
|
||||
return func(*Challenge) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
// Challenge implements the dns-01 challenge
|
||||
type Challenge struct {
|
||||
core *api.Core
|
||||
validate ValidateFunc
|
||||
provider challenge.Provider
|
||||
preCheck preCheck
|
||||
dnsTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
|
||||
chlg := &Challenge{
|
||||
core: core,
|
||||
validate: validate,
|
||||
provider: provider,
|
||||
preCheck: newPreCheck(),
|
||||
dnsTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
err := opt(chlg)
|
||||
if err != nil {
|
||||
log.Infof("challenge option error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return chlg
|
||||
}
|
||||
|
||||
// PreSolve just submits the txt record to the dns provider.
|
||||
// It does not validate record propagation, or do anything at all with the acme server.
|
||||
func (c *Challenge) PreSolve(authz acme.Authorization) error {
|
||||
domain := challenge.GetTargetedDomain(authz)
|
||||
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
|
||||
|
||||
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.provider == nil {
|
||||
return fmt.Errorf("[%s] acme: no DNS Provider configured", domain)
|
||||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] acme: error presenting token: %s", domain, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Challenge) Solve(authz acme.Authorization) error {
|
||||
domain := challenge.GetTargetedDomain(authz)
|
||||
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
|
||||
|
||||
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fqdn, value := GetRecord(authz.Identifier.Value, keyAuth)
|
||||
|
||||
var timeout, interval time.Duration
|
||||
switch provider := c.provider.(type) {
|
||||
case challenge.ProviderTimeout:
|
||||
timeout, interval = provider.Timeout()
|
||||
default:
|
||||
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
|
||||
}
|
||||
|
||||
log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers)
|
||||
|
||||
err = wait.For(timeout, interval, func() (bool, error) {
|
||||
stop, errP := c.preCheck.call(fqdn, value)
|
||||
if !stop || errP != nil {
|
||||
log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
|
||||
}
|
||||
return stop, errP
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chlng.KeyAuthorization = keyAuth
|
||||
return c.validate(c.core, authz.Identifier.Value, chlng)
|
||||
}
|
||||
|
||||
// CleanUp cleans the challenge.
|
||||
func (c *Challenge) CleanUp(authz acme.Authorization) error {
|
||||
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
|
||||
}
|
||||
|
||||
func (c *Challenge) Sequential() (bool, time.Duration) {
|
||||
if p, ok := c.provider.(sequential); ok {
|
||||
return ok, p.Sequential()
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
type sequential interface {
|
||||
Sequential() time.Duration
|
||||
}
|
||||
|
||||
// GetRecord returns a DNS record which will fulfill the `dns-01` challenge
|
||||
func GetRecord(domain, keyAuth string) (fqdn string, value string) {
|
||||
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
||||
// base64URL encoding without padding
|
||||
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
||||
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
|
||||
return
|
||||
}
|
52
vendor/github.com/xenolf/lego/challenge/dns01/dns_challenge_manual.go
generated
vendored
Normal file
52
vendor/github.com/xenolf/lego/challenge/dns01/dns_challenge_manual.go
generated
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
package dns01
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsTemplate = `%s %d IN TXT "%s"`
|
||||
)
|
||||
|
||||
// DNSProviderManual is an implementation of the ChallengeProvider interface
|
||||
type DNSProviderManual struct{}
|
||||
|
||||
// NewDNSProviderManual returns a DNSProviderManual instance.
|
||||
func NewDNSProviderManual() (*DNSProviderManual, error) {
|
||||
return &DNSProviderManual{}, nil
|
||||
}
|
||||
|
||||
// Present prints instructions for manually creating the TXT record
|
||||
func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := GetRecord(domain, keyAuth)
|
||||
|
||||
authZone, err := FindZoneByFqdn(fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone)
|
||||
fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, value)
|
||||
fmt.Printf("lego: Press 'Enter' when you are done\n")
|
||||
|
||||
_, err = bufio.NewReader(os.Stdin).ReadBytes('\n')
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CleanUp prints instructions for manually removing the TXT record
|
||||
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _ := GetRecord(domain, keyAuth)
|
||||
|
||||
authZone, err := FindZoneByFqdn(fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone)
|
||||
fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, "...")
|
||||
|
||||
return nil
|
||||
}
|
19
vendor/github.com/xenolf/lego/challenge/dns01/fqdn.go
generated
vendored
Normal file
19
vendor/github.com/xenolf/lego/challenge/dns01/fqdn.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
package dns01
|
||||
|
||||
// ToFqdn converts the name into a fqdn appending a trailing dot.
|
||||
func ToFqdn(name string) string {
|
||||
n := len(name)
|
||||
if n == 0 || name[n-1] == '.' {
|
||||
return name
|
||||
}
|
||||
return name + "."
|
||||
}
|
||||
|
||||
// UnFqdn converts the fqdn into a name removing the trailing dot.
|
||||
func UnFqdn(name string) string {
|
||||
n := len(name)
|
||||
if n != 0 && name[n-1] == '.' {
|
||||
return name[:n-1]
|
||||
}
|
||||
return name
|
||||
}
|
232
vendor/github.com/xenolf/lego/challenge/dns01/nameserver.go
generated
vendored
Normal file
232
vendor/github.com/xenolf/lego/challenge/dns01/nameserver.go
generated
vendored
Normal file
@ -0,0 +1,232 @@
|
||||
package dns01
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const defaultResolvConf = "/etc/resolv.conf"
|
||||
|
||||
// dnsTimeout is used to override the default DNS timeout of 10 seconds.
|
||||
var dnsTimeout = 10 * time.Second
|
||||
|
||||
var (
|
||||
fqdnToZone = map[string]string{}
|
||||
muFqdnToZone sync.Mutex
|
||||
)
|
||||
|
||||
var defaultNameservers = []string{
|
||||
"google-public-dns-a.google.com:53",
|
||||
"google-public-dns-b.google.com:53",
|
||||
}
|
||||
|
||||
// recursiveNameservers are used to pre-check DNS propagation
|
||||
var recursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
|
||||
|
||||
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
|
||||
func ClearFqdnCache() {
|
||||
muFqdnToZone.Lock()
|
||||
fqdnToZone = map[string]string{}
|
||||
muFqdnToZone.Unlock()
|
||||
}
|
||||
|
||||
func AddDNSTimeout(timeout time.Duration) ChallengeOption {
|
||||
return func(_ *Challenge) error {
|
||||
dnsTimeout = timeout
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func AddRecursiveNameservers(nameservers []string) ChallengeOption {
|
||||
return func(_ *Challenge) error {
|
||||
recursiveNameservers = ParseNameservers(nameservers)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// getNameservers attempts to get systems nameservers before falling back to the defaults
|
||||
func getNameservers(path string, defaults []string) []string {
|
||||
config, err := dns.ClientConfigFromFile(path)
|
||||
if err != nil || len(config.Servers) == 0 {
|
||||
return defaults
|
||||
}
|
||||
|
||||
return ParseNameservers(config.Servers)
|
||||
}
|
||||
|
||||
func ParseNameservers(servers []string) []string {
|
||||
var resolvers []string
|
||||
for _, resolver := range servers {
|
||||
// ensure all servers have a port number
|
||||
if _, _, err := net.SplitHostPort(resolver); err != nil {
|
||||
resolvers = append(resolvers, net.JoinHostPort(resolver, "53"))
|
||||
} else {
|
||||
resolvers = append(resolvers, resolver)
|
||||
}
|
||||
}
|
||||
return resolvers
|
||||
}
|
||||
|
||||
// lookupNameservers returns the authoritative nameservers for the given fqdn.
|
||||
func lookupNameservers(fqdn string) ([]string, error) {
|
||||
var authoritativeNss []string
|
||||
|
||||
zone, err := FindZoneByFqdn(fqdn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine the zone: %v", err)
|
||||
}
|
||||
|
||||
r, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rr := range r.Answer {
|
||||
if ns, ok := rr.(*dns.NS); ok {
|
||||
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
|
||||
}
|
||||
}
|
||||
|
||||
if len(authoritativeNss) > 0 {
|
||||
return authoritativeNss, nil
|
||||
}
|
||||
return nil, fmt.Errorf("could not determine authoritative nameservers")
|
||||
}
|
||||
|
||||
// FindZoneByFqdn determines the zone apex for the given fqdn
|
||||
// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.
|
||||
func FindZoneByFqdn(fqdn string) (string, error) {
|
||||
return FindZoneByFqdnCustom(fqdn, recursiveNameservers)
|
||||
}
|
||||
|
||||
// FindZoneByFqdnCustom determines the zone apex for the given fqdn
|
||||
// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.
|
||||
func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
|
||||
muFqdnToZone.Lock()
|
||||
defer muFqdnToZone.Unlock()
|
||||
|
||||
// Do we have it cached?
|
||||
if zone, ok := fqdnToZone[fqdn]; ok {
|
||||
return zone, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var in *dns.Msg
|
||||
|
||||
labelIndexes := dns.Split(fqdn)
|
||||
for _, index := range labelIndexes {
|
||||
domain := fqdn[index:]
|
||||
|
||||
in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if in == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch in.Rcode {
|
||||
case dns.RcodeSuccess:
|
||||
// Check if we got a SOA RR in the answer section
|
||||
|
||||
if len(in.Answer) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// CNAME records cannot/should not exist at the root of a zone.
|
||||
// So we skip a domain when a CNAME is found.
|
||||
if dnsMsgContainsCNAME(in) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ans := range in.Answer {
|
||||
if soa, ok := ans.(*dns.SOA); ok {
|
||||
zone := soa.Hdr.Name
|
||||
fqdnToZone[fqdn] = zone
|
||||
return zone, nil
|
||||
}
|
||||
}
|
||||
case dns.RcodeNameError:
|
||||
// NXDOMAIN
|
||||
default:
|
||||
// Any response code other than NOERROR and NXDOMAIN is treated as error
|
||||
return "", fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain)
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err))
|
||||
}
|
||||
|
||||
// dnsMsgContainsCNAME checks for a CNAME answer in msg
|
||||
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
|
||||
for _, ans := range msg.Answer {
|
||||
if _, ok := ans.(*dns.CNAME); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {
|
||||
m := createDNSMsg(fqdn, rtype, recursive)
|
||||
|
||||
var in *dns.Msg
|
||||
var err error
|
||||
|
||||
for _, ns := range nameservers {
|
||||
in, err = sendDNSQuery(m, ns)
|
||||
if err == nil && len(in.Answer) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return in, err
|
||||
}
|
||||
|
||||
func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(fqdn, rtype)
|
||||
m.SetEdns0(4096, false)
|
||||
|
||||
if !recursive {
|
||||
m.RecursionDesired = false
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
|
||||
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
|
||||
in, _, err := udp.Exchange(m, ns)
|
||||
|
||||
if in != nil && in.Truncated {
|
||||
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
|
||||
// If the TCP request succeeds, the err will reset to nil
|
||||
in, _, err = tcp.Exchange(m, ns)
|
||||
}
|
||||
|
||||
return in, err
|
||||
}
|
||||
|
||||
func formatDNSError(msg *dns.Msg, err error) string {
|
||||
var parts []string
|
||||
|
||||
if msg != nil {
|
||||
parts = append(parts, dns.RcodeToString[msg.Rcode])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
parts = append(parts, fmt.Sprintf("%v", err))
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
return ": " + strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
110
vendor/github.com/xenolf/lego/challenge/dns01/precheck.go
generated
vendored
Normal file
110
vendor/github.com/xenolf/lego/challenge/dns01/precheck.go
generated
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
package dns01
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready.
|
||||
type PreCheckFunc func(fqdn, value string) (bool, error)
|
||||
|
||||
func AddPreCheck(preCheck PreCheckFunc) ChallengeOption {
|
||||
// Prevent race condition
|
||||
check := preCheck
|
||||
return func(chlg *Challenge) error {
|
||||
chlg.preCheck.checkFunc = check
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DisableCompletePropagationRequirement() ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
chlg.preCheck.requireCompletePropagation = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type preCheck struct {
|
||||
// checks DNS propagation before notifying ACME that the DNS challenge is ready.
|
||||
checkFunc PreCheckFunc
|
||||
// require the TXT record to be propagated to all authoritative name servers
|
||||
requireCompletePropagation bool
|
||||
}
|
||||
|
||||
func newPreCheck() preCheck {
|
||||
return preCheck{
|
||||
requireCompletePropagation: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (p preCheck) call(fqdn, value string) (bool, error) {
|
||||
if p.checkFunc == nil {
|
||||
return p.checkDNSPropagation(fqdn, value)
|
||||
}
|
||||
return p.checkFunc(fqdn, value)
|
||||
}
|
||||
|
||||
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
||||
func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) {
|
||||
// Initial attempt to resolve at the recursive NS
|
||||
r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !p.requireCompletePropagation {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if r.Rcode == dns.RcodeSuccess {
|
||||
// If we see a CNAME here then use the alias
|
||||
for _, rr := range r.Answer {
|
||||
if cn, ok := rr.(*dns.CNAME); ok {
|
||||
if cn.Hdr.Name == fqdn {
|
||||
fqdn = cn.Target
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
authoritativeNss, err := lookupNameservers(fqdn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
|
||||
}
|
||||
|
||||
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
|
||||
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
|
||||
for _, ns := range nameservers {
|
||||
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, rr := range r.Answer {
|
||||
if txt, ok := rr.(*dns.TXT); ok {
|
||||
if strings.Join(txt.Txt, "") == value {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s]", ns, fqdn)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
65
vendor/github.com/xenolf/lego/challenge/http01/http_challenge.go
generated
vendored
Normal file
65
vendor/github.com/xenolf/lego/challenge/http01/http_challenge.go
generated
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
package http01
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/acme/api"
|
||||
"github.com/xenolf/lego/challenge"
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
|
||||
|
||||
// ChallengePath returns the URL path for the `http-01` challenge
|
||||
func ChallengePath(token string) string {
|
||||
return "/.well-known/acme-challenge/" + token
|
||||
}
|
||||
|
||||
type Challenge struct {
|
||||
core *api.Core
|
||||
validate ValidateFunc
|
||||
provider challenge.Provider
|
||||
}
|
||||
|
||||
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
|
||||
return &Challenge{
|
||||
core: core,
|
||||
validate: validate,
|
||||
provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Challenge) SetProvider(provider challenge.Provider) {
|
||||
c.provider = provider
|
||||
}
|
||||
|
||||
func (c *Challenge) Solve(authz acme.Authorization) error {
|
||||
domain := challenge.GetTargetedDomain(authz)
|
||||
log.Infof("[%s] acme: Trying to solve HTTP-01", domain)
|
||||
|
||||
chlng, err := challenge.FindChallenge(challenge.HTTP01, authz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] acme: error presenting token: %v", domain, err)
|
||||
}
|
||||
defer func() {
|
||||
err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
log.Warnf("[%s] acme: error cleaning up: %v", domain, err)
|
||||
}
|
||||
}()
|
||||
|
||||
chlng.KeyAuthorization = keyAuth
|
||||
return c.validate(c.core, authz.Identifier.Value, chlng)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package acme
|
||||
package http01
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -9,31 +9,31 @@ import (
|
||||
"github.com/xenolf/lego/log"
|
||||
)
|
||||
|
||||
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
|
||||
// It may be instantiated without using the NewHTTPProviderServer function if
|
||||
// ProviderServer implements ChallengeProvider for `http-01` challenge
|
||||
// It may be instantiated without using the NewProviderServer function if
|
||||
// you want only to use the default values.
|
||||
type HTTPProviderServer struct {
|
||||
type ProviderServer struct {
|
||||
iface string
|
||||
port string
|
||||
done chan bool
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port.
|
||||
// NewProviderServer creates a new ProviderServer on the selected interface and port.
|
||||
// Setting iface and / or port to an empty string will make the server fall back to
|
||||
// the "any" interface and port 80 respectively.
|
||||
func NewHTTPProviderServer(iface, port string) *HTTPProviderServer {
|
||||
return &HTTPProviderServer{iface: iface, port: port}
|
||||
func NewProviderServer(iface, port string) *ProviderServer {
|
||||
return &ProviderServer{iface: iface, port: port}
|
||||
}
|
||||
|
||||
// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests.
|
||||
func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
|
||||
// Present starts a web server and makes the token available at `ChallengePath(token)` for web requests.
|
||||
func (s *ProviderServer) Present(domain, token, keyAuth string) error {
|
||||
if s.port == "" {
|
||||
s.port = "80"
|
||||
}
|
||||
|
||||
var err error
|
||||
s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
|
||||
s.listener, err = net.Listen("tcp", s.GetAddress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start HTTP server for challenge -> %v", err)
|
||||
}
|
||||
@ -43,8 +43,12 @@ func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)`
|
||||
func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error {
|
||||
func (s *ProviderServer) GetAddress() string {
|
||||
return net.JoinHostPort(s.iface, s.port)
|
||||
}
|
||||
|
||||
// CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`
|
||||
func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
@ -53,8 +57,8 @@ func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
|
||||
path := HTTP01ChallengePath(token)
|
||||
func (s *ProviderServer) serve(domain, token, keyAuth string) {
|
||||
path := ChallengePath(token)
|
||||
|
||||
// The handler validates the HOST header and request type.
|
||||
// For validation it then writes the token the server returned with the challenge
|
||||
@ -80,12 +84,12 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
|
||||
|
||||
httpServer := &http.Server{Handler: mux}
|
||||
|
||||
// Once httpServer is shut down we don't want any lingering
|
||||
// connections, so disable KeepAlives.
|
||||
// Once httpServer is shut down
|
||||
// we don't want any lingering connections, so disable KeepAlives.
|
||||
httpServer.SetKeepAlivesEnabled(false)
|
||||
|
||||
err := httpServer.Serve(s.listener)
|
||||
if err != nil {
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
log.Println(err)
|
||||
}
|
||||
s.done <- true
|
@ -1,28 +1,28 @@
|
||||
package acme
|
||||
package challenge
|
||||
|
||||
import "time"
|
||||
|
||||
// ChallengeProvider enables implementing a custom challenge
|
||||
// Provider enables implementing a custom challenge
|
||||
// provider. Present presents the solution to a challenge available to
|
||||
// be solved. CleanUp will be called by the challenge if Present ends
|
||||
// in a non-error state.
|
||||
type ChallengeProvider interface {
|
||||
type Provider interface {
|
||||
Present(domain, token, keyAuth string) error
|
||||
CleanUp(domain, token, keyAuth string) error
|
||||
}
|
||||
|
||||
// ChallengeProviderTimeout allows for implementing a
|
||||
// ChallengeProvider where an unusually long timeout is required when
|
||||
// ProviderTimeout allows for implementing a
|
||||
// Provider where an unusually long timeout is required when
|
||||
// waiting for an ACME challenge to be satisfied, such as when
|
||||
// checking for DNS record progagation. If an implementor of a
|
||||
// ChallengeProvider provides a Timeout method, then the return values
|
||||
// checking for DNS record propagation. If an implementor of a
|
||||
// Provider provides a Timeout method, then the return values
|
||||
// of the Timeout method will be used when appropriate by the acme
|
||||
// package. The interval value is the time between checks.
|
||||
//
|
||||
// The default values used for timeout and interval are 60 seconds and
|
||||
// 2 seconds respectively. These are used when no Timeout method is
|
||||
// defined for the ChallengeProvider.
|
||||
type ChallengeProviderTimeout interface {
|
||||
ChallengeProvider
|
||||
// defined for the Provider.
|
||||
type ProviderTimeout interface {
|
||||
Provider
|
||||
Timeout() (timeout, interval time.Duration)
|
||||
}
|
25
vendor/github.com/xenolf/lego/challenge/resolver/errors.go
generated
vendored
Normal file
25
vendor/github.com/xenolf/lego/challenge/resolver/errors.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// obtainError is returned when there are specific errors available per domain.
|
||||
type obtainError map[string]error
|
||||
|
||||
func (e obtainError) Error() string {
|
||||
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
|
||||
|
||||
var domains []string
|
||||
for domain := range e {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
sort.Strings(domains)
|
||||
|
||||
for _, domain := range domains {
|
||||
buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain]))
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user