Commit 09188981 authored by Matt Holt's avatar Matt Holt Committed by GitHub

tls: Add support for the tls-alpn-01 challenge (#2201)

* tls: Add support for the tls-alpn-01 challenge

Also updates lego/acme to latest on master.

TODO: This implementation of the tls-alpn challenge is not yet solvable
in a distributed Caddy cluster like the http challenge is.

* build: Allow building with the race detector

* tls: Support distributed solving of the TLS-ALPN-01 challenge

* Update vendor and add a todo in MITM checker
parent ae5f013a
...@@ -42,11 +42,13 @@ import ( ...@@ -42,11 +42,13 @@ import (
) )
var goos, goarch, goarm string var goos, goarch, goarm string
var race bool
func init() { func init() {
flag.StringVar(&goos, "goos", "", "GOOS for which to build") flag.StringVar(&goos, "goos", "", "GOOS for which to build")
flag.StringVar(&goarch, "goarch", "", "GOARCH for which to build") flag.StringVar(&goarch, "goarch", "", "GOARCH for which to build")
flag.StringVar(&goarm, "goarm", "", "GOARM for which to build") flag.StringVar(&goarm, "goarm", "", "GOARM for which to build")
flag.BoolVar(&race, "race", false, "Enable race detector")
} }
func main() { func main() {
...@@ -67,6 +69,9 @@ func main() { ...@@ -67,6 +69,9 @@ func main() {
args := []string{"build", "-ldflags", ldflags} args := []string{"build", "-ldflags", ldflags}
args = append(args, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath)) args = append(args, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
args = append(args, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath)) args = append(args, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
if race {
args = append(args, "-race")
}
cmd := exec.Command("go", args...) cmd := exec.Command("go", args...)
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
...@@ -77,6 +82,9 @@ func main() { ...@@ -77,6 +82,9 @@ func main() {
"GOARCH=" + goarch, "GOARCH=" + goarch,
"GOARM=" + goarm, "GOARM=" + goarm,
} { } {
if race && env == "CGO_ENABLED=0" {
continue
}
cmd.Env = append(cmd.Env, env) cmd.Env = append(cmd.Env, env)
} }
......
...@@ -33,7 +33,7 @@ import ( ...@@ -33,7 +33,7 @@ import (
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddytls" "github.com/mholt/caddy/caddytls"
"github.com/mholt/caddy/telemetry" "github.com/mholt/caddy/telemetry"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
_ "github.com/mholt/caddy/caddyhttp" // plug in the HTTP server type _ "github.com/mholt/caddy/caddyhttp" // plug in the HTTP server type
...@@ -47,7 +47,7 @@ func init() { ...@@ -47,7 +47,7 @@ func init() {
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement") 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.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.DisableHTTPChallenge, "disable-http-challenge", caddytls.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
flag.BoolVar(&caddytls.DisableTLSSNIChallenge, "disable-tls-sni-challenge", caddytls.DisableTLSSNIChallenge, "Disable the ACME TLS-SNI challenge") flag.BoolVar(&caddytls.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", caddytls.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable") flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")") flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap") flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
......
...@@ -207,7 +207,7 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig { ...@@ -207,7 +207,7 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
Addr: Address{Original: addr, Host: host, Port: port}, Addr: Address{Original: addr, Host: host, Port: port},
ListenHost: cfg.ListenHost, ListenHost: cfg.ListenHost,
middleware: []Middleware{redirMiddleware}, middleware: []Middleware{redirMiddleware},
TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort, AltTLSSNIPort: cfg.TLS.AltTLSSNIPort}, TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort, AltTLSALPNPort: cfg.TLS.AltTLSALPNPort},
Timeouts: cfg.Timeouts, Timeouts: cfg.Timeouts,
} }
} }
...@@ -74,6 +74,7 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -74,6 +74,7 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values) if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values)
r.Header.Get("X-FCCKV2") != "" || // Fortinet r.Header.Get("X-FCCKV2") != "" || // Fortinet
info.advertisesHeartbeatSupport() { // no major browsers have ever implemented Heartbeat info.advertisesHeartbeatSupport() { // no major browsers have ever implemented Heartbeat
// TODO: Move the heartbeat check into each "looksLike" function...
checked = true checked = true
mitm = true mitm = true
} else if strings.Contains(ua, "Edge") || strings.Contains(ua, "MSIE") || } else if strings.Contains(ua, "Edge") || strings.Contains(ua, "MSIE") ||
......
...@@ -169,12 +169,12 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd ...@@ -169,12 +169,12 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
// If default HTTP or HTTPS ports have been customized, // If default HTTP or HTTPS ports have been customized,
// make sure the ACME challenge ports match // make sure the ACME challenge ports match
var altHTTPPort, altTLSSNIPort string var altHTTPPort, altTLSALPNPort string
if HTTPPort != DefaultHTTPPort { if HTTPPort != DefaultHTTPPort {
altHTTPPort = HTTPPort altHTTPPort = HTTPPort
} }
if HTTPSPort != DefaultHTTPSPort { if HTTPSPort != DefaultHTTPSPort {
altTLSSNIPort = HTTPSPort altTLSALPNPort = HTTPSPort
} }
// Make our caddytls.Config, which has a pointer to the // Make our caddytls.Config, which has a pointer to the
...@@ -183,7 +183,7 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd ...@@ -183,7 +183,7 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
caddytlsConfig := caddytls.NewConfig(h.instance) caddytlsConfig := caddytls.NewConfig(h.instance)
caddytlsConfig.Hostname = addr.Host caddytlsConfig.Hostname = addr.Host
caddytlsConfig.AltHTTPPort = altHTTPPort caddytlsConfig.AltHTTPPort = altHTTPPort
caddytlsConfig.AltTLSSNIPort = altTLSSNIPort caddytlsConfig.AltTLSALPNPort = altTLSALPNPort
// Save the config to our master list, and key it for lookups // Save the config to our master list, and key it for lookups
cfg := &SiteConfig{ cfg := &SiteConfig{
......
...@@ -27,7 +27,7 @@ import ( ...@@ -27,7 +27,7 @@ import (
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/telemetry" "github.com/mholt/caddy/telemetry"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
// acmeMu ensures that only one ACME challenge occurs at a time. // acmeMu ensures that only one ACME challenge occurs at a time.
...@@ -121,68 +121,69 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) ...@@ -121,68 +121,69 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
} }
if config.DNSProvider == "" { if config.DNSProvider == "" {
// Use HTTP and TLS-SNI challenges by default // Use HTTP and TLS-ALPN challenges by default
// See if HTTP challenge needs to be proxied // figure out which ports we'll be serving the challenges on
useHTTPPort := HTTPChallengePort useHTTPPort := HTTPChallengePort
useTLSALPNPort := TLSALPNChallengePort
if config.AltHTTPPort != "" { if config.AltHTTPPort != "" {
useHTTPPort = config.AltHTTPPort useHTTPPort = config.AltHTTPPort
} }
if config.AltTLSALPNPort != "" {
useTLSALPNPort = config.AltTLSALPNPort
}
if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) { if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) {
useHTTPPort = DefaultHTTPAlternatePort useHTTPPort = DefaultHTTPAlternatePort
} }
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return // if using file storage, we can distribute the HTTP or TLS-ALPN challenge
// See which port TLS-SNI challenges will be accomplished on // across all instances sharing the acme folder; either way, we must still
// useTLSSNIPort := TLSSNIChallengePort // set the address for the default provider server
// if config.AltTLSSNIPort != "" { var useDistributedSolver bool
// useTLSSNIPort = config.AltTLSSNIPort
// }
// err := c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort))
// if err != nil {
// return nil, err
// }
// if using file storage, we can distribute the HTTP challenge across
// all instances sharing the acme folder; either way, we must still set
// the address for the default HTTP provider server
var useDistributedHTTPSolver bool
if storage, err := c.config.StorageFor(c.config.CAUrl); err == nil { if storage, err := c.config.StorageFor(c.config.CAUrl); err == nil {
if _, ok := storage.(*FileStorage); ok { if _, ok := storage.(*FileStorage); ok {
useDistributedHTTPSolver = true useDistributedSolver = true
} }
} }
if useDistributedHTTPSolver { if useDistributedSolver {
c.acmeClient.SetChallengeProvider(acme.HTTP01, distributedHTTPSolver{ // ... being careful to respect user's listener bind preferences
// being careful to respect user's listener bind preferences c.acmeClient.SetChallengeProvider(acme.HTTP01, distributedSolver{
httpProviderServer: acme.NewHTTPProviderServer(config.ListenHost, useHTTPPort), providerServer: acme.NewHTTPProviderServer(config.ListenHost, useHTTPPort),
})
c.acmeClient.SetChallengeProvider(acme.TLSALPN01, distributedSolver{
providerServer: acme.NewTLSALPNProviderServer(config.ListenHost, useTLSALPNPort),
}) })
} else { } else {
// Always respect user's bind preferences by using config.ListenHost. // Always respect user's bind preferences by using config.ListenHost.
// NOTE(Sep'16): At time of writing, SetHTTPAddress() and SetTLSAddress() // NOTE(Nov'18): At time of writing, SetHTTPAddress() and SetTLSAddress()
// must be called before SetChallengeProvider() (see above), since they reset // reset the challenge provider back to the default one, overriding
// the challenge provider back to the default one! (still true in March 2018) // 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)) err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, useTLSALPNPort))
if err != nil {
return nil, err
}
} }
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return // if this server is already listening on the TLS-ALPN port we're supposed to use,
// See if TLS challenge needs to be handled by our own facilities // then wire up this config's ACME client to use our own facilities for solving
// if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort)) { // the challenge: our own certificate cache, since we already have a listener
// c.acmeClient.SetChallengeProvider(acme.TLSSNI01, tlsSNISolver{certCache: config.certCache}) 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 // Disable any challenges that should not be used
var disabledChallenges []acme.Challenge var disabledChallenges []acme.Challenge
if DisableHTTPChallenge { if DisableHTTPChallenge {
disabledChallenges = append(disabledChallenges, acme.HTTP01) disabledChallenges = append(disabledChallenges, acme.HTTP01)
} }
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return if DisableTLSALPNChallenge {
// if DisableTLSSNIChallenge { disabledChallenges = append(disabledChallenges, acme.TLSALPN01)
// disabledChallenges = append(disabledChallenges, acme.TLSSNI01) }
// }
if len(disabledChallenges) > 0 { if len(disabledChallenges) > 0 {
c.acmeClient.ExcludeChallenges(disabledChallenges) c.acmeClient.ExcludeChallenges(disabledChallenges)
} }
...@@ -203,9 +204,7 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) ...@@ -203,9 +204,7 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
} }
// Use the DNS challenge exclusively // Use the DNS challenge exclusively
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSALPN01})
// c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01})
c.acmeClient.SetChallengeProvider(acme.DNS01, prov) c.acmeClient.SetChallengeProvider(acme.DNS01, prov)
} }
...@@ -312,7 +311,7 @@ func (c *ACMEClient) Renew(name string) error { ...@@ -312,7 +311,7 @@ func (c *ACMEClient) Renew(name string) error {
certMeta.PrivateKey = siteData.Key certMeta.PrivateKey = siteData.Key
// Perform renewal and retry if necessary, but not too many times. // Perform renewal and retry if necessary, but not too many times.
var newCertMeta acme.CertificateResource var newCertMeta *acme.CertificateResource
var success bool var success bool
for attempts := 0; attempts < 2; attempts++ { for attempts := 0; attempts < 2; attempts++ {
namesObtaining.Add([]string{name}) namesObtaining.Add([]string{name})
...@@ -321,10 +320,8 @@ func (c *ACMEClient) Renew(name string) error { ...@@ -321,10 +320,8 @@ func (c *ACMEClient) Renew(name string) error {
acmeMu.Unlock() acmeMu.Unlock()
namesObtaining.Remove([]string{name}) namesObtaining.Remove([]string{name})
if err == nil { if err == nil {
// double-check that we actually got a certificate; check a couple fields // double-check that we actually got a certificate; check a couple fields, just in case
// TODO: This is a temporary workaround for what I think is a bug in the acmev2 package (March 2018) if newCertMeta == nil || newCertMeta.Domain == "" || newCertMeta.Certificate == nil {
// but it might not hurt to keep this extra check in place
if newCertMeta.Domain == "" || newCertMeta.Certificate == nil {
err = errors.New("returned certificate was empty; probably an unchecked error renewing it") err = errors.New("returned certificate was empty; probably an unchecked error renewing it")
} else { } else {
success = true success = true
......
...@@ -26,7 +26,7 @@ import ( ...@@ -26,7 +26,7 @@ import (
"github.com/klauspost/cpuid" "github.com/klauspost/cpuid"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
// Config describes how TLS should be configured and used. // Config describes how TLS should be configured and used.
...@@ -102,10 +102,10 @@ type Config struct { ...@@ -102,10 +102,10 @@ type Config struct {
AltHTTPPort string AltHTTPPort string
// The alternate port (ONLY port, not host) // The alternate port (ONLY port, not host)
// to use for the ACME TLS-SNI challenge. // to use for the ACME TLS-ALPN challenge;
// The system must forward TLSSNIChallengePort // the system must forward TLSALPNChallengePort
// to this port for challenge to succeed // to this port for challenge to succeed
AltTLSSNIPort string AltTLSALPNPort string
// The string identifier of the DNS provider // The string identifier of the DNS provider
// to use when solving the ACME DNS challenge // to use when solving the ACME DNS challenge
...@@ -343,6 +343,18 @@ func (c *Config) buildStandardTLSConfig() error { ...@@ -343,6 +343,18 @@ func (c *Config) buildStandardTLSConfig() error {
} }
} }
// ensure ALPN includes the ACME TLS-ALPN protocol
var alpnFound bool
for _, a := range c.ALPN {
if a == acme.ACMETLS1Protocol {
alpnFound = true
break
}
}
if !alpnFound {
c.ALPN = append(c.ALPN, acme.ACMETLS1Protocol)
}
config.MinVersion = c.ProtocolMinVersion config.MinVersion = c.ProtocolMinVersion
config.MaxVersion = c.ProtocolMaxVersion config.MaxVersion = c.ProtocolMaxVersion
config.ClientAuth = c.ClientAuth config.ClientAuth = c.ClientAuth
...@@ -695,13 +707,13 @@ var defaultCurves = []tls.CurveID{ ...@@ -695,13 +707,13 @@ var defaultCurves = []tls.CurveID{
} }
const ( const (
// HTTPChallengePort is the officially designated port for // HTTPChallengePort is the officially-designated port for
// the HTTP challenge according to the ACME spec. // the HTTP challenge according to the ACME spec.
HTTPChallengePort = "80" HTTPChallengePort = "80"
// TLSSNIChallengePort is the officially designated port for // TLSALPNChallengePort is the officially-designated port for
// the TLS-SNI challenge according to the ACME spec. // the TLS-ALPN challenge according to the ACME spec.
TLSSNIChallengePort = "443" TLSALPNChallengePort = "443"
// DefaultHTTPAlternatePort is the port on which the ACME // DefaultHTTPAlternatePort is the port on which the ACME
// client will open a listener and solve the HTTP challenge. // client will open a listener and solve the HTTP challenge.
......
...@@ -42,7 +42,7 @@ import ( ...@@ -42,7 +42,7 @@ import (
"golang.org/x/crypto/ocsp" "golang.org/x/crypto/ocsp"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
// loadPrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes. // loadPrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
......
...@@ -281,11 +281,13 @@ func (s *FileStorage) MostRecentUserEmail() string { ...@@ -281,11 +281,13 @@ func (s *FileStorage) MostRecentUserEmail() string {
func fileSafe(str string) string { func fileSafe(str string) string {
str = strings.ToLower(str) str = strings.ToLower(str)
str = strings.TrimSpace(str) str = strings.TrimSpace(str)
repl := strings.NewReplacer("..", "", repl := strings.NewReplacer(
"..", "",
"/", "", "/", "",
"\\", "", "\\", "",
// TODO: Consider also replacing "@" with "_at_" (but migrate existing accounts...) // TODO: Consider also replacing "@" with "_at_" (but migrate existing accounts...)
"+", "_plus_", "+", "_plus_",
"*", "wildcard_",
"%", "", "%", "",
"$", "", "$", "",
"`", "", "`", "",
...@@ -297,8 +299,7 @@ func fileSafe(str string) string { ...@@ -297,8 +299,7 @@ func fileSafe(str string) string {
"#", "", "#", "",
"&", "", "&", "",
"|", "", "|", "",
"\"", "", `"`, "",
"'", "", "'", "")
"*", "wildcard_")
return repl.Replace(str) return repl.Replace(str)
} }
...@@ -16,17 +16,20 @@ package caddytls ...@@ -16,17 +16,20 @@ package caddytls
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/mholt/caddy/telemetry" "github.com/mholt/caddy/telemetry"
"github.com/xenolf/lego/acme"
) )
// configGroup is a type that keys configs by their hostname // configGroup is a type that keys configs by their hostname
...@@ -111,6 +114,32 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif ...@@ -111,6 +114,32 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif
go telemetry.SetNested("tls_client_hello", info.Key(), info) 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 // get the certificate and serve it up
cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true)
if err == nil { if err == nil {
...@@ -166,17 +195,12 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau ...@@ -166,17 +195,12 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau
} }
// check the certCache directly to see if the SNI name is // check the certCache directly to see if the SNI name is
// already the key of the certificate it wants! this is vital // already the key of the certificate it wants; this implies
// for supporting the TLS-SNI challenge, since the tlsSNISolver // that the SNI can contain the hash of a specific cert
// just puts the temporary certificate in the instance cache, // (chain) it wants and we will still be able to serveit up
// with no regard for configs; this also means that the SNI
// can contain the hash of a specific cert (chain) it wants
// and we will still be able to serve it up
// (this behavior, by the way, could be controversial as to // (this behavior, by the way, could be controversial as to
// whether it complies with RFC 6066 about SNI, but I think // whether it complies with RFC 6066 about SNI, but I think
// it does soooo...) // it does, soooo...)
// NOTE/TODO: TLS-SNI challenge is changing, as of Jan. 2018
// but what will be different, if it ever returns, is unclear
if directCert, ok := cfg.certCache.cache[name]; ok { if directCert, ok := cfg.certCache.cache[name]; ok {
cert = directCert cert = directCert
matched = true matched = true
...@@ -477,6 +501,39 @@ func (cfg *Config) renewDynamicCertificate(name string, currentCert Certificate) ...@@ -477,6 +501,39 @@ func (cfg *Config) renewDynamicCertificate(name string, currentCert Certificate)
return cfg.getCertDuringHandshake(name, true, false) 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 // ClientHelloInfo is our own version of the standard lib's
// tls.ClientHelloInfo. As of May 2018, any fields populated // tls.ClientHelloInfo. As of May 2018, any fields populated
// by the Go standard library are not guaranteed to have their // by the Go standard library are not guaranteed to have their
......
...@@ -25,7 +25,7 @@ import ( ...@@ -25,7 +25,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
const challengeBasePath = "/.well-known/acme-challenge" const challengeBasePath = "/.well-known/acme-challenge"
...@@ -87,7 +87,7 @@ func HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, listenHost str ...@@ -87,7 +87,7 @@ func HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, listenHost str
// storage, and attempts to complete the challenge for it. It // storage, and attempts to complete the challenge for it. It
// returns true if the challenge was handled; false otherwise. // returns true if the challenge was handled; false otherwise.
func tryDistributedChallengeSolver(w http.ResponseWriter, r *http.Request) bool { func tryDistributedChallengeSolver(w http.ResponseWriter, r *http.Request) bool {
filePath := distributedHTTPSolver{}.challengeTokensPath(r.Host) filePath := distributedSolver{}.challengeTokensPath(r.Host)
f, err := os.Open(filePath) f, err := os.Open(filePath)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
...@@ -112,7 +112,7 @@ func tryDistributedChallengeSolver(w http.ResponseWriter, r *http.Request) bool ...@@ -112,7 +112,7 @@ func tryDistributedChallengeSolver(w http.ResponseWriter, r *http.Request) bool
w.Header().Add("Content-Type", "text/plain") w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(chalInfo.KeyAuth)) w.Write([]byte(chalInfo.KeyAuth))
r.Close = true r.Close = true
log.Printf("[INFO][%s] Served key authentication", chalInfo.Domain) log.Printf("[INFO][%s] Served key authentication (distributed)", chalInfo.Domain)
return true return true
} }
......
...@@ -146,7 +146,7 @@ func RenewManagedCertificates(allowPrompts bool) (err error) { ...@@ -146,7 +146,7 @@ func RenewManagedCertificates(allowPrompts bool) (err error) {
// happen to run their maintenance checks at approximately the same times; // 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 // 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. // will overwrite the other. Hence TLS storage plugins. This is sort of a TODO.
// NOTE 2: It is super-important to note that the TLS-SNI challenge requires // NOTE 2: It is super-important to note that the TLS-ALPN challenge requires
// a write lock on the cache in order to complete its challenge, so it is extra // 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! // vital that this renew operation does not happen inside our read lock!
renewQueue = append(renewQueue, cert) renewQueue = append(renewQueue, cert)
......
...@@ -22,7 +22,7 @@ import ( ...@@ -22,7 +22,7 @@ import (
"testing" "testing"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
......
...@@ -39,7 +39,7 @@ import ( ...@@ -39,7 +39,7 @@ import (
"strings" "strings"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
// HostQualifies returns true if the hostname alone // HostQualifies returns true if the hostname alone
...@@ -72,7 +72,7 @@ func HostQualifies(hostname string) bool { ...@@ -72,7 +72,7 @@ func HostQualifies(hostname string) bool {
// saveCertResource saves the certificate resource to disk. This // saveCertResource saves the certificate resource to disk. This
// includes the certificate file itself, the private key, and the // includes the certificate file itself, the private key, and the
// metadata file. // metadata file.
func saveCertResource(storage Storage, cert acme.CertificateResource) error { func saveCertResource(storage Storage, cert *acme.CertificateResource) error {
// Save cert, private key, and metadata // Save cert, private key, and metadata
siteData := &SiteData{ siteData := &SiteData{
Cert: cert.Certificate, Cert: cert.Certificate,
...@@ -97,55 +97,63 @@ func Revoke(host string) error { ...@@ -97,55 +97,63 @@ func Revoke(host string) error {
return client.Revoke(host) return client.Revoke(host)
} }
// TODO: tls-sni challenge was removed in January 2018, but a variant of it might return // tlsALPNSolver is a type that can solve TLS-ALPN challenges using
// // tlsSNISolver is a type that can solve TLS-SNI challenges using // an existing listener and our custom, in-memory certificate cache.
// // an existing listener and our custom, in-memory certificate cache. type tlsALPNSolver struct {
// type tlsSNISolver struct { certCache *certificateCache
// certCache *certificateCache }
// }
// Present adds the challenge certificate to the cache.
// // Present adds the challenge certificate to the cache. func (s tlsALPNSolver) Present(domain, token, keyAuth string) error {
// func (s tlsSNISolver) Present(domain, token, keyAuth string) error { cert, err := acme.TLSALPNChallengeCert(domain, keyAuth)
// cert, acmeDomain, err := acme.TLSSNI01ChallengeCert(keyAuth) if err != nil {
// if err != nil { return err
// return err }
// } certHash := hashCertificateChain(cert.Certificate)
// certHash := hashCertificateChain(cert.Certificate) s.certCache.Lock()
// s.certCache.Lock() s.certCache.cache[tlsALPNCertKeyName(domain)] = Certificate{
// s.certCache.cache[acmeDomain] = Certificate{ Certificate: *cert,
// Certificate: cert, Names: []string{domain},
// Names: []string{acmeDomain}, Hash: certHash, // perhaps not necesssary
// Hash: certHash, // perhaps not necesssary }
// } s.certCache.Unlock()
// s.certCache.Unlock() return nil
// return nil }
// }
// CleanUp removes the challenge certificate from the cache.
// // CleanUp removes the challenge certificate from the cache. func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error {
// func (s tlsSNISolver) CleanUp(domain, token, keyAuth string) error { s.certCache.Lock()
// _, acmeDomain, err := acme.TLSSNI01ChallengeCert(keyAuth) delete(s.certCache.cache, domain)
// if err != nil { s.certCache.Unlock()
// return err return nil
// } }
// s.certCache.Lock()
// delete(s.certCache.cache, acmeDomain) // tlsALPNCertKeyName returns the key to use when caching a cert
// s.certCache.Unlock() // for use with the TLS-ALPN ACME challenge. It is simply to help
// return nil // 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 {
// distributedHTTPSolver allows the HTTP-01 challenge to be solved by return sniName + ":acme-tls-alpn"
// 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 // distributedSolver allows the ACME HTTP-01 and TLS-ALPN challenges
// the $CADDYPATH/acme folder with the instance that will complete // to be solved by an instance other than the one which initiated it.
// the challenge. Mounting the folder locally should be sufficient. // 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 // Obviously, the instance which completes the challenge must be
// serving on the HTTPChallengePort to receive and handle the request. // serving on the HTTPChallengePort for the HTTP-01 challenge or the
// The HTTP server which receives it must check if a file exists, e.g.: // TLSALPNChallengePort for the TLS-ALPN-01 challenge (or have all
// $CADDYPATH/acme/challenge_tokens/example.com.json, and if so, // the packets port-forwarded) to receive and handle the request. The
// decode it and use it to serve up the correct response. Caddy's HTTP // server which receives the challenge must handle it by checking to
// server does this by default. // 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 // 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 // no other requirements. The instances may be on other machines or
...@@ -155,29 +163,18 @@ func Revoke(host string) error { ...@@ -155,29 +163,18 @@ func Revoke(host string) error {
// This solver works by persisting the token and keyauth information // This solver works by persisting the token and keyauth information
// to disk in the shared folder when the authorization is presented, // to disk in the shared folder when the authorization is presented,
// and then deletes it when it is cleaned up. // and then deletes it when it is cleaned up.
type distributedHTTPSolver struct { type distributedSolver struct {
// The distributed HTTPS solver only works if an instance (either // As the distributedSolver is only a wrapper over the actual
// this one or another one) is already listening and serving on the // solver, place the actual solver here
// HTTPChallengePort. If not -- for example: if this is the only providerServer ChallengeProvider
// instance, and it is just starting up and hasn't started serving
// yet -- then we still need a listener open with an HTTP server
// to handle the challenge request. Set this field to have the
// standard HTTPProviderServer open its listener for the duration
// of the challenge. Make sure to configure its listen address
// correctly.
httpProviderServer *acme.HTTPProviderServer
}
type challengeInfo struct {
Domain, Token, KeyAuth string
} }
// Present adds the challenge certificate to the cache. // Present adds the challenge certificate to the cache.
func (dhs distributedHTTPSolver) Present(domain, token, keyAuth string) error { func (dhs distributedSolver) Present(domain, token, keyAuth string) error {
if dhs.httpProviderServer != nil { if dhs.providerServer != nil {
err := dhs.httpProviderServer.Present(domain, token, keyAuth) err := dhs.providerServer.Present(domain, token, keyAuth)
if err != nil { if err != nil {
return fmt.Errorf("presenting with standard HTTP provider server: %v", err) return fmt.Errorf("presenting with standard provider server: %v", err)
} }
} }
...@@ -199,25 +196,29 @@ func (dhs distributedHTTPSolver) Present(domain, token, keyAuth string) error { ...@@ -199,25 +196,29 @@ func (dhs distributedHTTPSolver) Present(domain, token, keyAuth string) error {
} }
// CleanUp removes the challenge certificate from the cache. // CleanUp removes the challenge certificate from the cache.
func (dhs distributedHTTPSolver) CleanUp(domain, token, keyAuth string) error { func (dhs distributedSolver) CleanUp(domain, token, keyAuth string) error {
if dhs.httpProviderServer != nil { if dhs.providerServer != nil {
err := dhs.httpProviderServer.CleanUp(domain, token, keyAuth) err := dhs.providerServer.CleanUp(domain, token, keyAuth)
if err != nil { if err != nil {
log.Printf("[ERROR] Cleaning up standard HTTP provider server: %v", err) log.Printf("[ERROR] Cleaning up standard provider server: %v", err)
} }
} }
return os.Remove(dhs.challengeTokensPath(domain)) return os.Remove(dhs.challengeTokensPath(domain))
} }
func (dhs distributedHTTPSolver) challengeTokensPath(domain string) string { func (dhs distributedSolver) challengeTokensPath(domain string) string {
domainFile := strings.Replace(strings.ToLower(domain), "*", "wildcard_", -1) domainFile := fileSafe(domain)
return filepath.Join(dhs.challengeTokensBasePath(), domainFile+".json") return filepath.Join(dhs.challengeTokensBasePath(), domainFile+".json")
} }
func (dhs distributedHTTPSolver) challengeTokensBasePath() string { func (dhs distributedSolver) challengeTokensBasePath() string {
return filepath.Join(caddy.AssetsPath(), "acme", "challenge_tokens") 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 // ConfigHolder is any type that has a Config; it presumably is
// connected to a hostname and port on which it is serving. // connected to a hostname and port on which it is serving.
type ConfigHolder interface { type ConfigHolder interface {
...@@ -297,8 +298,8 @@ var ( ...@@ -297,8 +298,8 @@ var (
// DisableHTTPChallenge will disable all HTTP challenges. // DisableHTTPChallenge will disable all HTTP challenges.
DisableHTTPChallenge bool DisableHTTPChallenge bool
// DisableTLSSNIChallenge will disable all TLS-SNI challenges. // DisableTLSALPNChallenge will disable all TLS-ALPN challenges.
DisableTLSSNIChallenge bool DisableTLSALPNChallenge bool
) )
var storageProviders = make(map[string]StorageConstructor) var storageProviders = make(map[string]StorageConstructor)
......
...@@ -18,7 +18,7 @@ import ( ...@@ -18,7 +18,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
func TestHostQualifies(t *testing.T) { func TestHostQualifies(t *testing.T) {
...@@ -116,7 +116,7 @@ func TestSaveCertResource(t *testing.T) { ...@@ -116,7 +116,7 @@ func TestSaveCertResource(t *testing.T) {
"certStableUrl": "https://example.com/cert/stable" "certStableUrl": "https://example.com/cert/stable"
}` }`
cert := acme.CertificateResource{ cert := &acme.CertificateResource{
Domain: domain, Domain: domain,
CertURL: "https://example.com/cert", CertURL: "https://example.com/cert",
CertStableURL: "https://example.com/cert/stable", CertStableURL: "https://example.com/cert/stable",
...@@ -164,7 +164,7 @@ func TestExistingCertAndKey(t *testing.T) { ...@@ -164,7 +164,7 @@ func TestExistingCertAndKey(t *testing.T) {
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain) t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
} }
err = saveCertResource(storage, acme.CertificateResource{ err = saveCertResource(storage, &acme.CertificateResource{
Domain: domain, Domain: domain,
PrivateKey: []byte("key"), PrivateKey: []byte("key"),
Certificate: []byte("cert"), Certificate: []byte("cert"),
......
...@@ -27,7 +27,7 @@ import ( ...@@ -27,7 +27,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
// User represents a Let's Encrypt user account. // User represents a Let's Encrypt user account.
......
...@@ -27,7 +27,7 @@ import ( ...@@ -27,7 +27,7 @@ import (
"os" "os"
"github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/acme"
) )
func TestUser(t *testing.T) { func TestUser(t *testing.T) {
......
...@@ -10,4 +10,6 @@ const ( ...@@ -10,4 +10,6 @@ const (
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns // 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 // Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01") 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")
) )
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1"
"encoding/base64" "encoding/base64"
"encoding/pem" "encoding/pem"
"errors" "errors"
...@@ -19,8 +20,6 @@ import ( ...@@ -19,8 +20,6 @@ import (
"net/http" "net/http"
"time" "time"
"encoding/asn1"
"golang.org/x/crypto/ocsp" "golang.org/x/crypto/ocsp"
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
) )
...@@ -118,6 +117,10 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { ...@@ -118,6 +117,10 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
defer req.Body.Close() defer req.Body.Close()
ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024)) ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024))
if err != nil {
return nil, nil, err
}
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
...@@ -138,7 +141,7 @@ func getKeyAuthorization(token string, key interface{}) (string, error) { ...@@ -138,7 +141,7 @@ func getKeyAuthorization(token string, key interface{}) (string, error) {
// Generate the Key Authorization for the challenge // Generate the Key Authorization for the challenge
jwk := &jose.JSONWebKey{Key: publicKey} jwk := &jose.JSONWebKey{Key: publicKey}
if jwk == nil { if jwk == nil {
return "", errors.New("Could not generate JWK from key") return "", errors.New("could not generate JWK from key")
} }
thumbBytes, err := jwk.Thumbprint(crypto.SHA256) thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
if err != nil { if err != nil {
...@@ -173,7 +176,7 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { ...@@ -173,7 +176,7 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
} }
if len(certificates) == 0 { if len(certificates) == 0 {
return nil, errors.New("No certificates were found while parsing the bundle") return nil, errors.New("no certificates were found while parsing the bundle")
} }
return certificates, nil return certificates, nil
...@@ -188,7 +191,7 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { ...@@ -188,7 +191,7 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
case "EC PRIVATE KEY": case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(keyBlock.Bytes) return x509.ParseECPrivateKey(keyBlock.Bytes)
default: default:
return nil, errors.New("Unknown PEM header value") return nil, errors.New("unknown PEM header value")
} }
} }
...@@ -207,14 +210,12 @@ func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { ...@@ -207,14 +210,12 @@ func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, 8192) return rsa.GenerateKey(rand.Reader, 8192)
} }
return nil, fmt.Errorf("Invalid KeyType: %s", keyType) return nil, fmt.Errorf("invalid KeyType: %s", keyType)
} }
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
template := x509.CertificateRequest{ template := x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{CommonName: domain},
CommonName: domain,
},
} }
if len(san) > 0 { if len(san) > 0 {
...@@ -239,10 +240,8 @@ func pemEncode(data interface{}) []byte { ...@@ -239,10 +240,8 @@ func pemEncode(data interface{}) []byte {
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
case *rsa.PrivateKey: case *rsa.PrivateKey:
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
break
case *x509.CertificateRequest: case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
break
case derCertificateBytes: case derCertificateBytes:
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
} }
...@@ -302,8 +301,8 @@ func getCertExpiration(cert []byte) (time.Time, error) { ...@@ -302,8 +301,8 @@ func getCertExpiration(cert []byte) (time.Time, error) {
return pCert.NotAfter, nil return pCert.NotAfter, nil
} }
func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { func generatePemCert(privKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
derBytes, err := generateDerCert(privKey, time.Time{}, domain) derBytes, err := generateDerCert(privKey, time.Time{}, domain, extensions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -311,7 +310,7 @@ func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { ...@@ -311,7 +310,7 @@ func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
} }
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil { if err != nil {
...@@ -333,6 +332,7 @@ func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain strin ...@@ -333,6 +332,7 @@ func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain strin
KeyUsage: x509.KeyUsageKeyEncipherment, KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true, BasicConstraintsValid: true,
DNSNames: []string{domain}, DNSNames: []string{domain},
ExtraExtensions: extensions,
} }
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
......
...@@ -5,12 +5,12 @@ import ( ...@@ -5,12 +5,12 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"log"
"net" "net"
"strings" "strings"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/xenolf/lego/log"
) )
type preCheckDNSFunc func(fqdn, value string) (bool, error) type preCheckDNSFunc func(fqdn, value string) (bool, error)
...@@ -72,10 +72,10 @@ type dnsChallenge struct { ...@@ -72,10 +72,10 @@ type dnsChallenge struct {
} }
func (s *dnsChallenge) Solve(chlng challenge, domain string) error { func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve DNS-01", domain) log.Infof("[%s] acme: Trying to solve DNS-01", domain)
if s.provider == nil { if s.provider == nil {
return errors.New("No DNS Provider configured") return errors.New("no DNS Provider configured")
} }
// Generate the Key Authorization for the challenge // Generate the Key Authorization for the challenge
...@@ -86,18 +86,18 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { ...@@ -86,18 +86,18 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
err = s.provider.Present(domain, chlng.Token, keyAuth) err = s.provider.Present(domain, chlng.Token, keyAuth)
if err != nil { if err != nil {
return fmt.Errorf("Error presenting token: %s", err) return fmt.Errorf("error presenting token: %s", err)
} }
defer func() { defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth) err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil { if err != nil {
log.Printf("Error cleaning up %s: %v ", domain, err) log.Warnf("Error cleaning up %s: %v ", domain, err)
} }
}() }()
fqdn, value, _ := DNS01Record(domain, keyAuth) fqdn, value, _ := DNS01Record(domain, keyAuth)
logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) log.Infof("[%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
var timeout, interval time.Duration var timeout, interval time.Duration
switch provider := s.provider.(type) { switch provider := s.provider.(type) {
......
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"github.com/xenolf/lego/log"
) )
const ( const (
...@@ -28,9 +30,9 @@ func (*DNSProviderManual) Present(domain, token, keyAuth string) error { ...@@ -28,9 +30,9 @@ func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
return err return err
} }
logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone) log.Infof("acme: Please create the following TXT record in your %s zone:", authZone)
logf("[INFO] acme: %s", dnsRecord) log.Infof("acme: %s", dnsRecord)
logf("[INFO] acme: Press 'Enter' when you are done") log.Infof("acme: Press 'Enter' when you are done")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
_, _ = reader.ReadString('\n') _, _ = reader.ReadString('\n')
...@@ -47,7 +49,7 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { ...@@ -47,7 +49,7 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
return err return err
} }
logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone) log.Infof("acme: You can now remove this TXT record from your %s zone:", authZone)
logf("[INFO] acme: %s", dnsRecord) log.Infof("acme: %s", dnsRecord)
return nil return nil
} }
package acme package acme
import ( import (
"crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os"
"runtime" "runtime"
"strings" "strings"
"time" "time"
) )
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. var (
var UserAgent string // UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
UserAgent string
// HTTPClient is an HTTP client with a reasonable timeout value.
var HTTPClient = http.Client{ // HTTPClient is an HTTP client with a reasonable timeout value and
Transport: &http.Transport{ // potentially a custom *x509.CertPool based on the caCertificatesEnvVar
Proxy: http.ProxyFromEnvironment, // environment variable (see the `initCertPool` function)
Dial: (&net.Dialer{ HTTPClient = http.Client{
Timeout: 30 * time.Second, Transport: &http.Transport{
KeepAlive: 30 * time.Second, Proxy: http.ProxyFromEnvironment,
}).Dial, DialContext: (&net.Dialer{
TLSHandshakeTimeout: 15 * time.Second, Timeout: 30 * time.Second,
ResponseHeaderTimeout: 15 * time.Second, KeepAlive: 30 * time.Second,
ExpectContinueTimeout: 1 * 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 ( const (
// defaultGoUserAgent is the Go HTTP package user agent string. Too // defaultGoUserAgent is the Go HTTP package user agent string. Too
...@@ -36,12 +48,46 @@ const ( ...@@ -36,12 +48,46 @@ const (
// ourUserAgent is the User-Agent of this underlying library package. // ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme" ourUserAgent = "xenolf-acme"
// caCertificatesEnvVar is the environment variable name that can be used to
// specify the path to PEM encoded CA Certificates that can be used to
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list.
caCertificatesEnvVar = "LEGO_CA_CERTIFICATES"
// caServerNameEnvVar is the environment variable name that can be used to
// specify the CA server name that can be used to
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list.
caServerNameEnvVar = "LEGO_CA_SERVER_NAME"
) )
// initCertPool creates a *x509.CertPool populated with the PEM certificates
// found in the filepath specified in the caCertificatesEnvVar OS environment
// variable. If the caCertificatesEnvVar is not set then initCertPool will
// return nil. If there is an error creating a *x509.CertPool from the provided
// caCertificatesEnvVar value then initCertPool will panic.
func initCertPool() *x509.CertPool {
if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" {
customCAs, err := ioutil.ReadFile(customCACertsPath)
if err != nil {
panic(fmt.Sprintf("error reading %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
}
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(customCAs); !ok {
panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
}
return certPool
}
return nil
}
// httpHead performs a HEAD request with a proper User-Agent string. // httpHead performs a HEAD request with a proper User-Agent string.
// The response body (resp.Body) is already closed when this function returns. // The response body (resp.Body) is already closed when this function returns.
func httpHead(url string) (resp *http.Response, err error) { func httpHead(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("HEAD", url, nil) req, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to head %q: %v", url, err) return nil, fmt.Errorf("failed to head %q: %v", url, err)
} }
...@@ -59,7 +105,7 @@ func httpHead(url string) (resp *http.Response, err error) { ...@@ -59,7 +105,7 @@ func httpHead(url string) (resp *http.Response, err error) {
// httpPost performs a POST request with a proper User-Agent string. // httpPost performs a POST request with a proper User-Agent string.
// Callers should close resp.Body when done reading from it. // Callers should close resp.Body when done reading from it.
func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
req, err := http.NewRequest("POST", url, body) req, err := http.NewRequest(http.MethodPost, url, body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to post %q: %v", url, err) return nil, fmt.Errorf("failed to post %q: %v", url, err)
} }
...@@ -72,7 +118,7 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, ...@@ -72,7 +118,7 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response,
// httpGet performs a GET request with a proper User-Agent string. // httpGet performs a GET request with a proper User-Agent string.
// Callers should close resp.Body when done reading from it. // Callers should close resp.Body when done reading from it.
func httpGet(url string) (resp *http.Response, err error) { func httpGet(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get %q: %v", url, err) return nil, fmt.Errorf("failed to get %q: %v", url, err)
} }
...@@ -155,6 +201,6 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e ...@@ -155,6 +201,6 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e
// userAgent builds and returns the User-Agent string to use in requests. // userAgent builds and returns the User-Agent string to use in requests.
func userAgent() string { func userAgent() string {
ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent) ua := fmt.Sprintf("%s %s (%s; %s) %s", UserAgent, ourUserAgent, runtime.GOOS, runtime.GOARCH, defaultGoUserAgent)
return strings.TrimSpace(ua) return strings.TrimSpace(ua)
} }
...@@ -2,7 +2,8 @@ package acme ...@@ -2,7 +2,8 @@ package acme
import ( import (
"fmt" "fmt"
"log"
"github.com/xenolf/lego/log"
) )
type httpChallenge struct { type httpChallenge struct {
...@@ -18,7 +19,7 @@ func HTTP01ChallengePath(token string) string { ...@@ -18,7 +19,7 @@ func HTTP01ChallengePath(token string) string {
func (s *httpChallenge) Solve(chlng challenge, domain string) error { func (s *httpChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain) log.Infof("[%s] acme: Trying to solve HTTP-01", domain)
// Generate the Key Authorization for the challenge // Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
...@@ -33,7 +34,7 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error { ...@@ -33,7 +34,7 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error {
defer func() { defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth) err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil { if err != nil {
log.Printf("[%s] error cleaning up: %v", domain, err) log.Warnf("[%s] error cleaning up: %v", domain, err)
} }
}() }()
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"net" "net"
"net/http" "net/http"
"strings" "strings"
"github.com/xenolf/lego/log"
) )
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge // HTTPProviderServer implements ChallengeProvider for `http-01` challenge
...@@ -58,12 +60,12 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { ...@@ -58,12 +60,12 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
// For validation it then writes the token the server returned with the challenge // For validation it then writes the token the server returned with the challenge
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" { if strings.HasPrefix(r.Host, domain) && r.Method == http.MethodGet {
w.Header().Add("Content-Type", "text/plain") w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(keyAuth)) w.Write([]byte(keyAuth))
logf("[INFO][%s] Served key authentication", domain) log.Infof("[%s] Served key authentication", domain)
} else { } else {
logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method) log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method)
w.Write([]byte("TEST")) w.Write([]byte("TEST"))
} }
}) })
......
...@@ -26,13 +26,13 @@ type jws struct { ...@@ -26,13 +26,13 @@ type jws struct {
func (j *jws) post(url string, content []byte) (*http.Response, error) { func (j *jws) post(url string, content []byte) (*http.Response, error) {
signedContent, err := j.signContent(url, content) signedContent, err := j.signContent(url, content)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
} }
data := bytes.NewBuffer([]byte(signedContent.FullSerialize())) data := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
resp, err := httpPost(url, "application/jose+json", data) resp, err := httpPost(url, "application/jose+json", data)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error()) return nil, fmt.Errorf("failed to HTTP POST to %s -> %s", url, err.Error())
} }
nonce, nonceErr := getNonceFromResponse(resp) nonce, nonceErr := getNonceFromResponse(resp)
...@@ -77,16 +77,45 @@ func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, e ...@@ -77,16 +77,45 @@ func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, e
signer, err := jose.NewSigner(signKey, &options) signer, err := jose.NewSigner(signKey, &options)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error()) return nil, fmt.Errorf("failed to create jose signer -> %s", err.Error())
} }
signed, err := signer.Sign(content) signed, err := signer.Sign(content)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
} }
return signed, nil 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) { func (j *jws) Nonce() (string, error) {
if nonce, ok := j.nonces.Pop(); ok { if nonce, ok := j.nonces.Pop(); ok {
return nonce, nil return nonce, nil
...@@ -122,7 +151,7 @@ func (n *nonceManager) Push(nonce string) { ...@@ -122,7 +151,7 @@ func (n *nonceManager) Push(nonce string) {
func getNonce(url string) (string, error) { func getNonce(url string) (string, error) {
resp, err := httpHead(url) resp, err := httpHead(url)
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error()) return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %s", err.Error())
} }
return getNonceFromResponse(resp) return getNonceFromResponse(resp)
...@@ -131,7 +160,7 @@ func getNonce(url string) (string, error) { ...@@ -131,7 +160,7 @@ func getNonce(url string) (string, error) {
func getNonceFromResponse(resp *http.Response) (string, error) { func getNonceFromResponse(resp *http.Response) (string, error) {
nonce := resp.Header.Get("Replay-Nonce") nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" { if nonce == "" {
return "", fmt.Errorf("Server did not respond with a proper nonce header") return "", fmt.Errorf("server did not respond with a proper nonce header")
} }
return nonce, nil return nonce, nil
......
package acme package acme
import ( import (
"encoding/json"
"time" "time"
) )
...@@ -26,11 +27,12 @@ type directory struct { ...@@ -26,11 +27,12 @@ type directory struct {
} }
type accountMessage struct { type accountMessage struct {
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Contact []string `json:"contact,omitempty"` Contact []string `json:"contact,omitempty"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
Orders string `json:"orders,omitempty"` Orders string `json:"orders,omitempty"`
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
} }
type orderResource struct { type orderResource struct {
...@@ -76,9 +78,6 @@ type csrMessage struct { ...@@ -76,9 +78,6 @@ type csrMessage struct {
Csr string `json:"csr"` Csr string `json:"csr"`
} }
type emptyObjectMessage struct {
}
type revokeCertMessage struct { type revokeCertMessage struct {
Certificate string `json:"certificate"` Certificate string `json:"certificate"`
} }
......
package acme
import (
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"github.com/xenolf/lego/log"
)
// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.1
var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
type tlsALPNChallenge struct {
jws *jws
validate validateFunc
provider ChallengeProvider
}
// Solve manages the provider to validate and solve the challenge.
func (t *tlsALPNChallenge) Solve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
if err != nil {
return err
}
err = t.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
}
defer func() {
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Warnf("[%s] error cleaning up: %v", domain, err)
}
}()
return t.validate(t.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
// TLSALPNChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {
// Compute the SHA-256 digest of the key authorization.
zBytes := sha256.Sum256([]byte(keyAuth))
value, err := asn1.Marshal(zBytes[:sha256.Size])
if err != nil {
return nil, nil, err
}
// Add the keyAuth digest as the acmeValidation-v1 extension
// (marked as critical such that it won't be used by non-ACME software).
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3
extensions := []pkix.Extension{
{
Id: idPeAcmeIdentifierV1,
Critical: true,
Value: value,
},
}
// Generate a new RSA key for the certificates.
tempPrivKey, err := generatePrivateKey(RSA2048)
if err != nil {
return nil, nil, err
}
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
// Generate the PEM certificate using the provided private key, domain, and extra extensions.
tempCertPEM, err := generatePemCert(rsaPrivKey, domain, extensions)
if err != nil {
return nil, nil, err
}
// Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair.
rsaPrivPEM := pemEncode(rsaPrivKey)
return tempCertPEM, rsaPrivPEM, nil
}
// TLSALPNChallengeCert returns a certificate with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func TLSALPNChallengeCert(domain, keyAuth string) (*tls.Certificate, error) {
tempCertPEM, rsaPrivPEM, err := TLSALPNChallengeBlocks(domain, keyAuth)
if err != nil {
return nil, err
}
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
if err != nil {
return nil, err
}
return &certificate, nil
}
package acme
import (
"crypto/tls"
"fmt"
"net"
"net/http"
)
const (
// ACMETLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol.
ACMETLS1Protocol = "acme-tls/1"
// defaultTLSPort is the port that the TLSALPNProviderServer will default to
// when no other port is provided.
defaultTLSPort = "443"
)
// TLSALPNProviderServer implements ChallengeProvider for `TLS-ALPN-01`
// challenge. It may be instantiated without using the NewTLSALPNProviderServer
// if you want only to use the default values.
type TLSALPNProviderServer struct {
iface string
port string
listener net.Listener
}
// NewTLSALPNProviderServer creates a new TLSALPNProviderServer on the selected
// interface and port. Setting iface and / or port to an empty string will make
// the server fall back to the "any" interface and port 443 respectively.
func NewTLSALPNProviderServer(iface, port string) *TLSALPNProviderServer {
return &TLSALPNProviderServer{iface: iface, port: port}
}
// Present generates a certificate with a SHA-256 digest of the keyAuth provided
// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN
// spec.
func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error {
if t.port == "" {
// Fallback to port 443 if the port was not provided.
t.port = defaultTLSPort
}
// Generate the challenge certificate using the provided keyAuth and domain.
cert, err := TLSALPNChallengeCert(domain, keyAuth)
if err != nil {
return err
}
// Place the generated certificate with the extension into the TLS config
// so that it can serve the correct details.
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{*cert}
// We must set that the `acme-tls/1` application level protocol is supported
// so that the protocol negotiation can succeed. Reference:
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2
tlsConf.NextProtos = []string{ACMETLS1Protocol}
// Create the listener with the created tls.Config.
t.listener, err = tls.Listen("tcp", net.JoinHostPort(t.iface, t.port), tlsConf)
if err != nil {
return fmt.Errorf("could not start HTTPS server for challenge -> %v", err)
}
// Shut the server down when we're finished.
go func() {
http.Serve(t.listener, nil)
}()
return nil
}
// CleanUp closes the HTTPS server.
func (t *TLSALPNProviderServer) CleanUp(domain, token, keyAuth string) error {
if t.listener == nil {
return nil
}
// Server was created, close it.
if err := t.listener.Close(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
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.
package log
import (
"log"
"os"
)
// Logger is an optional custom logger.
var Logger = log.New(os.Stdout, "", log.LstdFlags)
// Fatal writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatal(args ...interface{}) {
Logger.Fatal(args...)
}
// Fatalf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatalf(format string, args ...interface{}) {
Logger.Fatalf(format, args...)
}
// Print writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Print(args ...interface{}) {
Logger.Print(args...)
}
// Println writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Println(args ...interface{}) {
Logger.Println(args...)
}
// Printf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Printf(format string, args ...interface{}) {
Logger.Printf(format, args...)
}
// Warnf writes a log entry.
func Warnf(format string, args ...interface{}) {
Printf("[WARN] "+format, args...)
}
// Infof writes a log entry.
func Infof(format string, args ...interface{}) {
Printf("[INFO] "+format, args...)
}
...@@ -167,12 +167,21 @@ ...@@ -167,12 +167,21 @@
"notests": true "notests": true
}, },
{ {
"importpath": "github.com/xenolf/lego/acmev2", "importpath": "github.com/xenolf/lego/acme",
"repository": "https://github.com/xenolf/lego", "repository": "https://github.com/xenolf/lego",
"vcs": "git", "vcs": "git",
"revision": "fad2257e11ae4ff31ed03739386873aa405dec2d", "revision": "04e2d74406d42a3727e7a132c1a39735ac527f51",
"branch": "acmev2", "branch": "master",
"path": "/acmev2", "path": "/acme",
"notests": true
},
{
"importpath": "github.com/xenolf/lego/log",
"repository": "https://github.com/xenolf/lego",
"vcs": "git",
"revision": "04e2d74406d42a3727e7a132c1a39735ac527f51",
"branch": "master",
"path": "log",
"notests": true "notests": true
}, },
{ {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment