acme/autocert: add support for tls-alpn-01
Because tls.Config now requires more fields to be set
in order for tls-alpn to work, Manager provides a new
TLSConfig method for easier setup.
This CL also adds a new internal package for end-to-end tests.
The package implements a simple ACME CA server.
Fixes golang/go#25013
Fixes golang/go#25901
Updates golang/go#17251
Change-Id: I2687ea8d5c445ddafad5ea2cdd36cd4e7d10bc86
Reviewed-on: https://go-review.googlesource.com/125495
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/acme/acme.go b/acme/acme.go
index e6d5202..ece9113 100644
--- a/acme/acme.go
+++ b/acme/acme.go
@@ -39,8 +39,17 @@
"time"
)
-// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
-const LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory"
+const (
+ // LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
+ LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory"
+
+ // ALPNProto is the ALPN protocol name used by a CA server when validating
+ // tls-alpn-01 challenges.
+ //
+ // Package users must ensure their servers can negotiate the ACME ALPN
+ // in order for tls-alpn-01 challenge verifications to succeed.
+ ALPNProto = "acme-tls/1"
+)
// idPeACMEIdentifierV1 is the OID for the ACME extension for the TLS-ALPN challenge.
var idPeACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go
index c8fa4e6..1a9d972 100644
--- a/acme/autocert/autocert.go
+++ b/acme/autocert/autocert.go
@@ -81,9 +81,9 @@
}
// Manager is a stateful certificate manager built on top of acme.Client.
-// It obtains and refreshes certificates automatically using "tls-sni-01",
-// "tls-sni-02" and "http-01" challenge types, as well as providing them
-// to a TLS server via tls.Config.
+// It obtains and refreshes certificates automatically using "tls-alpn-01",
+// "tls-sni-01", "tls-sni-02" and "http-01" challenge types,
+// as well as providing them to a TLS server via tls.Config.
//
// You must specify a cache implementation, such as DirCache,
// to reuse obtained certificates across program restarts.
@@ -177,9 +177,10 @@
// to be provisioned.
// The entries are stored for the duration of the authorization flow.
httpTokens map[string][]byte
- // certTokens contains temporary certificates for tls-sni challenges
+ // certTokens contains temporary certificates for tls-sni and tls-alpn challenges
// and is keyed by token domain name, which matches server name of ClientHello.
- // Keys always have ".acme.invalid" suffix.
+ // Keys always have ".acme.invalid" suffix for tls-sni. Otherwise, they are domain names
+ // for tls-alpn.
// The entries are stored for the duration of the authorization flow.
certTokens map[string]*tls.Certificate
}
@@ -188,7 +189,7 @@
type certKey struct {
domain string // without trailing dot
isRSA bool // RSA cert for legacy clients (as opposed to default ECDSA)
- isToken bool // tls-sni challenge token cert; key type is undefined regardless of isRSA
+ isToken bool // tls-based challenge token cert; key type is undefined regardless of isRSA
}
func (c certKey) String() string {
@@ -201,9 +202,22 @@
return c.domain
}
+// TLSConfig creates a new TLS config suitable for net/http.Server servers,
+// supporting HTTP/2 and the tls-alpn-01 ACME challenge type.
+func (m *Manager) TLSConfig() *tls.Config {
+ return &tls.Config{
+ GetCertificate: m.GetCertificate,
+ NextProtos: []string{
+ "h2", "http/1.1", // enable HTTP/2
+ acme.ALPNProto, // enable tls-alpn ACME challenges
+ },
+ }
+}
+
// GetCertificate implements the tls.Config.GetCertificate hook.
// It provides a TLS certificate for hello.ServerName host, including answering
-// *.acme.invalid (TLS-SNI) challenges. All other fields of hello are ignored.
+// tls-alpn-01 and *.acme.invalid (tls-sni-01 and tls-sni-02) challenges.
+// All other fields of hello are ignored.
//
// If m.HostPolicy is non-nil, GetCertificate calls the policy before requesting
// a new cert. A non-nil error returned from m.HostPolicy halts TLS negotiation.
@@ -230,10 +244,13 @@
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
- // check whether this is a token cert requested for TLS-SNI challenge
- if strings.HasSuffix(name, ".acme.invalid") {
+ // Check whether this is a token cert requested for TLS-SNI or TLS-ALPN challenge.
+ if wantsTokenCert(hello) {
m.tokensMu.RLock()
defer m.tokensMu.RUnlock()
+ // It's ok to use the same token cert key for both tls-sni and tls-alpn
+ // because there's always at most 1 token cert per on-going domain authorization.
+ // See m.verify for details.
if cert := m.certTokens[name]; cert != nil {
return cert, nil
}
@@ -269,6 +286,17 @@
return cert, nil
}
+// wantsTokenCert reports whether a TLS request with SNI is made by a CA server
+// for a challenge verification.
+func wantsTokenCert(hello *tls.ClientHelloInfo) bool {
+ // tls-alpn-01
+ if len(hello.SupportedProtos) == 1 && hello.SupportedProtos[0] == acme.ALPNProto {
+ return true
+ }
+ // tls-sni-xx
+ return strings.HasSuffix(hello.ServerName, ".acme.invalid")
+}
+
func supportsECDSA(hello *tls.ClientHelloInfo) bool {
// The "signature_algorithms" extension, if present, limits the key exchange
// algorithms allowed by the cipher suites. See RFC 5246, section 7.4.1.4.1.
@@ -635,7 +663,7 @@
func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string) error {
// The list of challenge types we'll try to fulfill
// in this specific order.
- challengeTypes := []string{"tls-sni-02", "tls-sni-01"}
+ challengeTypes := []string{"tls-alpn-01", "tls-sni-02", "tls-sni-01"}
m.tokensMu.RLock()
if m.tryHTTP01 {
challengeTypes = append(challengeTypes, "http-01")
@@ -691,7 +719,7 @@
}
return errors.New(errorMsg)
}
- cleanup, err := m.fulfill(ctx, client, chal)
+ cleanup, err := m.fulfill(ctx, client, chal, domain)
if err != nil {
errs[chal] = err
continue
@@ -714,8 +742,15 @@
// fulfill provisions a response to the challenge chal.
// The cleanup is non-nil only if provisioning succeeded.
-func (m *Manager) fulfill(ctx context.Context, client *acme.Client, chal *acme.Challenge) (cleanup func(), err error) {
+func (m *Manager) fulfill(ctx context.Context, client *acme.Client, chal *acme.Challenge, domain string) (cleanup func(), err error) {
switch chal.Type {
+ case "tls-alpn-01":
+ cert, err := client.TLSALPN01ChallengeCert(chal.Token, domain)
+ if err != nil {
+ return nil, err
+ }
+ m.putCertToken(ctx, domain, &cert)
+ return func() { go m.deleteCertToken(domain) }, nil
case "tls-sni-01":
cert, name, err := client.TLSSNI01ChallengeCert(chal.Token)
if err != nil {
diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go
index 48ccd35..ced1759 100644
--- a/acme/autocert/autocert_test.go
+++ b/acme/autocert/autocert_test.go
@@ -21,6 +21,7 @@
"fmt"
"html/template"
"io"
+ "io/ioutil"
"math/big"
"net/http"
"net/http/httptest"
@@ -31,6 +32,7 @@
"time"
"golang.org/x/crypto/acme"
+ "golang.org/x/crypto/acme/autocert/internal/acmetest"
)
var (
@@ -440,6 +442,7 @@
// startACMEServerStub runs an ACME server
// The domain argument is the expected domain name of a certificate request.
+// TODO: Drop this in favour of x/crypto/acme/autocert/internal/acmetest.
func startACMEServerStub(t *testing.T, getCertificate func(string) error, domain string) (url string, finish func()) {
// echo token-02 | shasum -a 256
// then divide result in 2 parts separated by dot
@@ -607,7 +610,7 @@
}
// ACME CA server stub, only the needed bits.
- // TODO: Merge this with startACMEServerStub, making it a configurable CA for testing.
+ // TODO: Replace this with x/crypto/acme/autocert/internal/acmetest.
var ca *httptest.Server
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
@@ -701,7 +704,7 @@
done := make(chan struct{}) // closed when revokeCount is 3
// ACME CA server stub, only the needed bits.
- // TODO: Merge this with startACMEServerStub, making it a configurable CA for testing.
+ // TODO: Replace this with x/crypto/acme/autocert/internal/acmetest.
var ca *httptest.Server
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
@@ -1128,3 +1131,59 @@
}
}
}
+
+// TODO: add same end-to-end for http-01 challenge type.
+func TestEndToEnd(t *testing.T) {
+ const domain = "example.org"
+
+ // ACME CA server
+ ca := acmetest.NewCAServer([]string{"tls-alpn-01"}, []string{domain})
+ defer ca.Close()
+
+ // User dummy server.
+ m := &Manager{
+ Prompt: AcceptTOS,
+ Client: &acme.Client{DirectoryURL: ca.URL},
+ }
+ us := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("OK"))
+ }))
+ us.TLS = &tls.Config{
+ NextProtos: []string{"http/1.1", acme.ALPNProto},
+ GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ cert, err := m.GetCertificate(hello)
+ if err != nil {
+ t.Errorf("m.GetCertificate: %v", err)
+ }
+ return cert, err
+ },
+ }
+ us.StartTLS()
+ defer us.Close()
+ // In TLS-ALPN challenge verification, CA connects to the domain:443 in question.
+ // Because the domain won't resolve in tests, we need to tell the CA
+ // where to dial to instead.
+ ca.Resolve(domain, strings.TrimPrefix(us.URL, "https://"))
+
+ // A client visiting user dummy server.
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{
+ RootCAs: ca.Roots,
+ ServerName: domain,
+ },
+ }
+ client := &http.Client{Transport: tr}
+ res, err := client.Get(us.URL)
+ if err != nil {
+ t.Logf("CA errors: %v", ca.Errors())
+ t.Fatal(err)
+ }
+ defer res.Body.Close()
+ b, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if v := string(b); v != "OK" {
+ t.Errorf("user server response: %q; want 'OK'", v)
+ }
+}
diff --git a/acme/autocert/example_test.go b/acme/autocert/example_test.go
index 552a625..89e2d83 100644
--- a/acme/autocert/example_test.go
+++ b/acme/autocert/example_test.go
@@ -5,7 +5,6 @@
package autocert_test
import (
- "crypto/tls"
"fmt"
"log"
"net/http"
@@ -27,10 +26,9 @@
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org"),
}
- go http.ListenAndServe(":http", m.HTTPHandler(nil))
s := &http.Server{
Addr: ":https",
- TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
+ TLSConfig: m.TLSConfig(),
}
s.ListenAndServeTLS("", "")
}
diff --git a/acme/autocert/internal/acmetest/ca.go b/acme/autocert/internal/acmetest/ca.go
new file mode 100644
index 0000000..acc486a
--- /dev/null
+++ b/acme/autocert/internal/acmetest/ca.go
@@ -0,0 +1,416 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package acmetest provides types for testing acme and autocert packages.
+//
+// TODO: Consider moving this to x/crypto/acme/internal/acmetest for acme tests as well.
+package acmetest
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math/big"
+ "net/http"
+ "net/http/httptest"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+)
+
+// CAServer is a simple test server which implements ACME spec bits needed for testing.
+type CAServer struct {
+ URL string // server URL after it has been started
+ Roots *x509.CertPool // CA root certificates; initialized in NewCAServer
+
+ rootKey crypto.Signer
+ rootCert []byte // DER encoding
+ rootTemplate *x509.Certificate
+
+ server *httptest.Server
+ challengeTypes []string // supported challenge types
+ domainsWhitelist []string // only these domains are valid for issuing, unless empty
+
+ mu sync.Mutex
+ certCount int // number of issued certs
+ domainAddr map[string]string // domain name to addr:port resolution
+ authorizations map[string]*authorization // keyed by domain name
+ errors []error // encountered client errors
+}
+
+// NewCAServer creates a new ACME test server and starts serving requests.
+// The returned CAServer issues certs signed with the CA roots
+// available in the Roots field.
+//
+// The challengeTypes argument defines the supported ACME challenge types
+// sent to a client in a response for a domain authorization.
+// If domainsWhitelist is non-empty, the certs will be issued only for the specified
+// list of domains. Otherwise, any domain name is allowed.
+func NewCAServer(challengeTypes []string, domainsWhitelist []string) *CAServer {
+ var whitelist []string
+ for _, name := range domainsWhitelist {
+ whitelist = append(whitelist, name)
+ }
+ sort.Strings(whitelist)
+ ca := &CAServer{
+ challengeTypes: challengeTypes,
+ domainsWhitelist: whitelist,
+ domainAddr: make(map[string]string),
+ authorizations: make(map[string]*authorization),
+ }
+
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err))
+ }
+ tmpl := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ Organization: []string{"Test Acme Co"},
+ CommonName: "Root CA",
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(365 * 24 * time.Hour),
+ KeyUsage: x509.KeyUsageCertSign,
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+ if err != nil {
+ panic(fmt.Sprintf("x509.CreateCertificate: %v", err))
+ }
+ cert, err := x509.ParseCertificate(der)
+ if err != nil {
+ panic(fmt.Sprintf("x509.ParseCertificate: %v", err))
+ }
+ ca.Roots = x509.NewCertPool()
+ ca.Roots.AddCert(cert)
+ ca.rootKey = key
+ ca.rootCert = der
+ ca.rootTemplate = tmpl
+
+ ca.server = httptest.NewServer(http.HandlerFunc(ca.handle))
+ ca.URL = ca.server.URL
+ return ca
+}
+
+// Close shuts down the server and blocks until all outstanding
+// requests on this server have completed.
+func (ca *CAServer) Close() {
+ ca.server.Close()
+}
+
+// Errors returns all client errors.
+func (ca *CAServer) Errors() []error {
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ return ca.errors
+}
+
+// Resolve adds a domain to address resolution for the ca to dial to
+// when validating challenges for the domain authorization.
+func (ca *CAServer) Resolve(domain, addr string) {
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ ca.domainAddr[domain] = addr
+}
+
+type discovery struct {
+ NewReg string `json:"new-reg"`
+ NewAuthz string `json:"new-authz"`
+ NewCert string `json:"new-cert"`
+}
+
+type challenge struct {
+ URI string `json:"uri"`
+ Type string `json:"type"`
+ Token string `json:"token"`
+}
+
+type authorization struct {
+ Status string `json:"status"`
+ Challenges []challenge `json:"challenges"`
+
+ id int
+ domain string
+}
+
+func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Replay-Nonce", "nonce")
+ if r.Method == "HEAD" {
+ // a nonce request
+ return
+ }
+
+ // TODO: Verify nonce header for all POST requests.
+
+ switch {
+ default:
+ err := fmt.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
+ ca.addError(err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+
+ // Discovery request.
+ case r.URL.Path == "/":
+ resp := &discovery{
+ NewReg: ca.serverURL("/new-reg"),
+ NewAuthz: ca.serverURL("/new-authz"),
+ NewCert: ca.serverURL("/new-cert"),
+ }
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ panic(fmt.Sprintf("discovery response: %v", err))
+ }
+
+ // Client key registration request.
+ case r.URL.Path == "/new-reg":
+ // TODO: Check the user account key against a ca.accountKeys?
+ w.Write([]byte("{}"))
+
+ // Domain authorization request.
+ case r.URL.Path == "/new-authz":
+ var req struct {
+ Identifier struct{ Value string }
+ }
+ if err := decodePayload(&req, r.Body); err != nil {
+ ca.addError(err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ authz, ok := ca.authorizations[req.Identifier.Value]
+ if !ok {
+ authz = &authorization{
+ domain: req.Identifier.Value,
+ Status: "pending",
+ }
+ for _, typ := range ca.challengeTypes {
+ authz.Challenges = append(authz.Challenges, challenge{
+ Type: typ,
+ URI: ca.serverURL("/challenge/%s/%s", typ, authz.domain),
+ Token: challengeToken(authz.domain, typ),
+ })
+ }
+ ca.authorizations[authz.domain] = authz
+ }
+ w.Header().Set("Location", ca.serverURL("/authz/%s", authz.domain))
+ w.WriteHeader(http.StatusCreated)
+ if err := json.NewEncoder(w).Encode(authz); err != nil {
+ panic(fmt.Sprintf("new authz response: %v", err))
+ }
+
+ // Accept tls-alpn-01 challenge type requests.
+ // TODO: Add http-01 and dns-01 handlers.
+ case strings.HasPrefix(r.URL.Path, "/challenge/tls-alpn-01/"):
+ domain := strings.TrimPrefix(r.URL.Path, "/challenge/tls-alpn-01/")
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ if _, ok := ca.authorizations[domain]; !ok {
+ err := fmt.Errorf("challenge accept: no authz for %q", domain)
+ ca.addError(err)
+ http.Error(w, err.Error(), http.StatusNotFound)
+ return
+ }
+ go func(domain string) {
+ err := ca.verifyALPNChallenge(domain)
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ authz := ca.authorizations[domain]
+ if err != nil {
+ authz.Status = "invalid"
+ return
+ }
+ authz.Status = "valid"
+
+ }(domain)
+ w.Write([]byte("{}"))
+
+ // Get authorization status requests.
+ case strings.HasPrefix(r.URL.Path, "/authz/"):
+ domain := strings.TrimPrefix(r.URL.Path, "/authz/")
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ authz, ok := ca.authorizations[domain]
+ if !ok {
+ http.Error(w, fmt.Sprintf("no authz for %q", domain), http.StatusNotFound)
+ return
+ }
+ if err := json.NewEncoder(w).Encode(authz); err != nil {
+ panic(fmt.Sprintf("get authz for %q response: %v", domain, err))
+ }
+
+ // Cert issuance request.
+ case r.URL.Path == "/new-cert":
+ var req struct {
+ CSR string `json:"csr"`
+ }
+ decodePayload(&req, r.Body)
+ b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
+ csr, err := x509.ParseCertificateRequest(b)
+ if err != nil {
+ ca.addError(err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ names := unique(append(csr.DNSNames, csr.Subject.CommonName))
+ if err := ca.matchWhitelist(names); err != nil {
+ ca.addError(err)
+ http.Error(w, err.Error(), http.StatusUnauthorized)
+ return
+ }
+ if err := ca.authorized(names); err != nil {
+ ca.addError(err)
+ http.Error(w, err.Error(), http.StatusUnauthorized)
+ return
+ }
+ der, err := ca.leafCert(csr)
+ if err != nil {
+ err = fmt.Errorf("new-cert response: ca.leafCert: %v", err)
+ ca.addError(err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ }
+ w.Header().Set("Link", fmt.Sprintf("<%s>; rel=up", ca.serverURL("/ca-cert")))
+ w.WriteHeader(http.StatusCreated)
+ w.Write(der)
+
+ // CA chain cert request.
+ case r.URL.Path == "/ca-cert":
+ w.Write(ca.rootCert)
+ }
+}
+
+func (ca *CAServer) addError(err error) {
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ ca.errors = append(ca.errors, err)
+}
+
+func (ca *CAServer) serverURL(format string, arg ...interface{}) string {
+ return ca.server.URL + fmt.Sprintf(format, arg...)
+}
+
+func (ca *CAServer) matchWhitelist(dnsNames []string) error {
+ if len(ca.domainsWhitelist) == 0 {
+ return nil
+ }
+ var nomatch []string
+ for _, name := range dnsNames {
+ i := sort.SearchStrings(ca.domainsWhitelist, name)
+ if i == len(ca.domainsWhitelist) || ca.domainsWhitelist[i] != name {
+ nomatch = append(nomatch, name)
+ }
+ }
+ if len(nomatch) > 0 {
+ return fmt.Errorf("matchWhitelist: some domains don't match: %q", nomatch)
+ }
+ return nil
+}
+
+func (ca *CAServer) authorized(dnsNames []string) error {
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ var noauthz []string
+ for _, name := range dnsNames {
+ authz, ok := ca.authorizations[name]
+ if !ok || authz.Status != "valid" {
+ noauthz = append(noauthz, name)
+ }
+ }
+ if len(noauthz) > 0 {
+ return fmt.Errorf("CAServer: no authz for %q", noauthz)
+ }
+ return nil
+}
+
+func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) {
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ ca.certCount++ // next leaf cert serial number
+ leaf := &x509.Certificate{
+ SerialNumber: big.NewInt(int64(ca.certCount)),
+ Subject: pkix.Name{Organization: []string{"Test Acme Co"}},
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(90 * 24 * time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ DNSNames: csr.DNSNames,
+ BasicConstraintsValid: true,
+ }
+ if len(csr.DNSNames) == 0 {
+ leaf.DNSNames = []string{csr.Subject.CommonName}
+ }
+ return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey)
+}
+
+func (ca *CAServer) addr(domain string) (string, error) {
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+ addr, ok := ca.domainAddr[domain]
+ if !ok {
+ return "", fmt.Errorf("CAServer: no addr resolution for %q", domain)
+ }
+ return addr, nil
+}
+
+func (ca *CAServer) verifyALPNChallenge(domain string) error {
+ const acmeALPNProto = "acme-tls/1"
+
+ addr, err := ca.addr(domain)
+ if err != nil {
+ return err
+ }
+ conn, err := tls.Dial("tcp", addr, &tls.Config{
+ ServerName: domain,
+ InsecureSkipVerify: true,
+ NextProtos: []string{acmeALPNProto},
+ })
+ if err != nil {
+ return err
+ }
+ if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto {
+ return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto)
+ }
+ if n := len(conn.ConnectionState().PeerCertificates); n != 1 {
+ return fmt.Errorf("len(PeerCertificates) = %d; want 1", n)
+ }
+ // TODO: verify conn.ConnectionState().PeerCertificates[0]
+ return nil
+}
+
+func decodePayload(v interface{}, r io.Reader) error {
+ var req struct{ Payload string }
+ if err := json.NewDecoder(r).Decode(&req); err != nil {
+ return err
+ }
+ payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(payload, v)
+}
+
+func challengeToken(domain, challType string) string {
+ return fmt.Sprintf("token-%s-%s", domain, challType)
+}
+
+func unique(a []string) []string {
+ seen := make(map[string]bool)
+ var res []string
+ for _, s := range a {
+ if s != "" && !seen[s] {
+ seen[s] = true
+ res = append(res, s)
+ }
+ }
+ return res
+}
diff --git a/acme/autocert/listener.go b/acme/autocert/listener.go
index d744df0..1e06981 100644
--- a/acme/autocert/listener.go
+++ b/acme/autocert/listener.go
@@ -72,11 +72,8 @@
// the Manager m's Prompt, Cache, HostPolicy, and other desired options.
func (m *Manager) Listener() net.Listener {
ln := &listener{
- m: m,
- conf: &tls.Config{
- GetCertificate: m.GetCertificate, // bonus: panic on nil m
- NextProtos: []string{"h2", "http/1.1"}, // Enable HTTP/2
- },
+ m: m,
+ conf: m.TLSConfig(),
}
ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443")
return ln