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