diff --git a/acme/acme.go b/acme/acme.go index e6d5202f..ece9113f 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -39,8 +39,17 @@ import ( "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 c8fa4e66..1a9d972d 100644 --- a/acme/autocert/autocert.go +++ b/acme/autocert/autocert.go @@ -81,9 +81,9 @@ func defaultHostPolicy(context.Context, string) error { } // 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 @@ type Manager struct { // 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 Manager struct { 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 @@ func (c certKey) String() string { 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 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, 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 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, 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) revokePendingAuthz(ctx context.Context, uri []string) { 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 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string } 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 @@ func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string // 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 48ccd35b..ced1759e 100644 --- a/acme/autocert/autocert_test.go +++ b/acme/autocert/autocert_test.go @@ -21,6 +21,7 @@ import ( "fmt" "html/template" "io" + "io/ioutil" "math/big" "net/http" "net/http/httptest" @@ -31,6 +32,7 @@ import ( "time" "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert/internal/acmetest" ) var ( @@ -440,6 +442,7 @@ func getCertificateFromManager(man *Manager, ecdsaSupport bool) func(string) err // 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 @@ func TestVerifyHTTP01(t *testing.T) { } // 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 @@ func TestRevokeFailedAuthz(t *testing.T) { 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 @@ func TestSupportsECDSA(t *testing.T) { } } } + +// 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 552a6254..89e2d836 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 @@ func ExampleManager() { 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 00000000..acc486af --- /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 d744df0e..1e069818 100644 --- a/acme/autocert/listener.go +++ b/acme/autocert/listener.go @@ -72,11 +72,8 @@ func NewListener(domains ...string) net.Listener { // 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