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