From 99a01f63f51b73f103cd1e094f1a8e7f35d9d30b Mon Sep 17 00:00:00 2001 From: Niall Sheridan Date: Fri, 10 Aug 2018 17:14:17 +0100 Subject: Fix LetsEncrypt support --- server/server.go | 6 +- vendor/golang.org/x/crypto/acme/acme.go | 19 ++++- .../golang.org/x/crypto/acme/autocert/autocert.go | 91 ++++++++++++++++------ .../golang.org/x/crypto/acme/autocert/listener.go | 7 +- .../golang.org/x/crypto/acme/autocert/renewal.go | 2 +- vendor/golang.org/x/crypto/acme/http.go | 19 +++-- vendor/vendor.json | 12 +-- 7 files changed, 109 insertions(+), 47 deletions(-) diff --git a/server/server.go b/server/server.go index 2995ead..42476f3 100644 --- a/server/server.go +++ b/server/server.go @@ -60,10 +60,12 @@ func Run(conf *config.Config) { if conf.Server.LetsEncryptServername != "" { m := autocert.Manager{ Prompt: autocert.AcceptTOS, - Cache: wkfscache.Cache(conf.Server.LetsEncryptCache), HostPolicy: autocert.HostWhitelist(conf.Server.LetsEncryptServername), } - tlsConfig.GetCertificate = m.GetCertificate + if conf.Server.LetsEncryptCache != "" { + m.Cache = wkfscache.Cache(conf.Server.LetsEncryptCache) + } + tlsConfig = m.TLSConfig() } else { if conf.Server.TLSCert == "" || conf.Server.TLSKey == "" { log.Fatal("TLS cert or key not specified in config") diff --git a/vendor/golang.org/x/crypto/acme/acme.go b/vendor/golang.org/x/crypto/acme/acme.go index 8257ffb..7df6476 100644 --- a/vendor/golang.org/x/crypto/acme/acme.go +++ b/vendor/golang.org/x/crypto/acme/acme.go @@ -39,8 +39,18 @@ 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. + // See the crypto/tls package's Config.NextProtos field. + 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} @@ -645,8 +655,9 @@ func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Accoun req.Agreement = acct.AgreedTerms } res, err := c.post(ctx, c.Key, url, req, wantStatus( - http.StatusOK, // updates and deletes - http.StatusCreated, // new account creation + http.StatusOK, // updates and deletes + http.StatusCreated, // new account creation + http.StatusAccepted, // Let's Encrypt divergent implementation )) if err != nil { return nil, err diff --git a/vendor/golang.org/x/crypto/acme/autocert/autocert.go b/vendor/golang.org/x/crypto/acme/autocert/autocert.go index c8fa4e6..4c2fc07 100644 --- a/vendor/golang.org/x/crypto/acme/autocert/autocert.go +++ b/vendor/golang.org/x/crypto/acme/autocert/autocert.go @@ -44,7 +44,7 @@ var createCertRetryAfter = time.Minute var pseudoRand *lockedMathRand func init() { - src := mathrand.NewSource(timeNow().UnixNano()) + src := mathrand.NewSource(time.Now().UnixNano()) pseudoRand = &lockedMathRand{rnd: mathrand.New(src)} } @@ -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,18 +177,22 @@ 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 + // nowFunc, if not nil, returns the current time. This may be set for + // testing purposes. + nowFunc func() time.Time } // certKey is the key by which certificates are tracked in state, renewal and cache. 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,14 +205,32 @@ 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. // The error is propagated back to the caller of GetCertificate and is user-visible. // This does not affect cached certs. See HostPolicy field description for more details. +// +// If GetCertificate is used directly, instead of via Manager.TLSConfig, package users will +// also have to add acme.ALPNProto to NextProtos for tls-alpn-01, or use HTTPHandler +// for http-01. (The tls-sni-* challenges have been deprecated by popular ACME providers +// due to security issues in the ecosystem.) func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { if m.Prompt == nil { return nil, errors.New("acme/autocert: Manager.Prompt not set") @@ -230,10 +252,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 +294,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. @@ -328,8 +364,8 @@ func supportsECDSA(hello *tls.ClientHelloInfo) bool { // Because the fallback handler is run with unencrypted port 80 requests, // the fallback should not serve TLS-only requests. // -// If HTTPHandler is never called, the Manager will only use TLS SNI -// challenges for domain verification. +// If HTTPHandler is never called, the Manager will only use the "tls-alpn-01" +// challenge for domain verification. func (m *Manager) HTTPHandler(fallback http.Handler) http.Handler { m.tokensMu.Lock() defer m.tokensMu.Unlock() @@ -447,7 +483,7 @@ func (m *Manager) cacheGet(ctx context.Context, ck certKey) (*tls.Certificate, e } // verify and create TLS cert - leaf, err := validCert(ck, pubDER, privKey) + leaf, err := validCert(ck, pubDER, privKey, m.now()) if err != nil { return nil, ErrCacheMiss } @@ -542,7 +578,7 @@ func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate, if !ok { return } - if _, err := validCert(ck, s.cert, s.key); err == nil { + if _, err := validCert(ck, s.cert, s.key, m.now()); err == nil { return } delete(m.state, ck) @@ -611,7 +647,7 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, ck cert if err != nil { return nil, nil, err } - leaf, err = validCert(ck, der, key) + leaf, err = validCert(ck, der, key, m.now()) if err != nil { return nil, nil, err } @@ -635,7 +671,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 +727,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 +750,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 { @@ -948,6 +991,13 @@ func (m *Manager) renewBefore() time.Duration { return 720 * time.Hour // 30 days } +func (m *Manager) now() time.Time { + if m.nowFunc != nil { + return m.nowFunc() + } + return time.Now() +} + // certState is ready when its mutex is unlocked for reading. type certState struct { sync.RWMutex @@ -1014,7 +1064,7 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) { // are valid. It doesn't do any revocation checking. // // The returned value is the verified leaf cert. -func validCert(ck certKey, der [][]byte, key crypto.Signer) (leaf *x509.Certificate, err error) { +func validCert(ck certKey, der [][]byte, key crypto.Signer, now time.Time) (leaf *x509.Certificate, err error) { // parse public part(s) var n int for _, b := range der { @@ -1031,7 +1081,6 @@ func validCert(ck certKey, der [][]byte, key crypto.Signer) (leaf *x509.Certific } // verify the leaf is not expired and matches the domain name leaf = x509Cert[0] - now := timeNow() if now.Before(leaf.NotBefore) { return nil, errors.New("acme/autocert: certificate is not valid yet") } @@ -1085,8 +1134,6 @@ func (r *lockedMathRand) int63n(max int64) int64 { // For easier testing. var ( - timeNow = time.Now - // Called when a state is removed. testDidRemoveState = func(certKey) {} ) diff --git a/vendor/golang.org/x/crypto/acme/autocert/listener.go b/vendor/golang.org/x/crypto/acme/autocert/listener.go index d744df0..1e06981 100644 --- a/vendor/golang.org/x/crypto/acme/autocert/listener.go +++ b/vendor/golang.org/x/crypto/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 diff --git a/vendor/golang.org/x/crypto/acme/autocert/renewal.go b/vendor/golang.org/x/crypto/acme/autocert/renewal.go index ef3e44e..665f870 100644 --- a/vendor/golang.org/x/crypto/acme/autocert/renewal.go +++ b/vendor/golang.org/x/crypto/acme/autocert/renewal.go @@ -128,7 +128,7 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) { } func (dr *domainRenewal) next(expiry time.Time) time.Duration { - d := expiry.Sub(timeNow()) - dr.m.renewBefore() + d := expiry.Sub(dr.m.now()) - dr.m.renewBefore() // add a bit of randomness to renew deadline n := pseudoRand.int63n(int64(renewJitter)) d -= time.Duration(n) diff --git a/vendor/golang.org/x/crypto/acme/http.go b/vendor/golang.org/x/crypto/acme/http.go index 56ba53a..a43ce6a 100644 --- a/vendor/golang.org/x/crypto/acme/http.go +++ b/vendor/golang.org/x/crypto/acme/http.go @@ -140,10 +140,13 @@ func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Respons case ok(res): return res, nil case isRetriable(res.StatusCode): - res.Body.Close() retry.inc() - if err := retry.backoff(ctx, req, res); err != nil { - return nil, err + resErr := responseError(res) + res.Body.Close() + // Ignore the error value from retry.backoff + // and return the one from last retry, as received from the CA. + if retry.backoff(ctx, req, res) != nil { + return nil, resErr } default: defer res.Body.Close() @@ -169,20 +172,22 @@ func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body i if ok(res) { return res, nil } - err = responseError(res) + resErr := responseError(res) res.Body.Close() switch { // Check for bad nonce before isRetriable because it may have been returned // with an unretriable response code such as 400 Bad Request. - case isBadNonce(err): + case isBadNonce(resErr): // Consider any previously stored nonce values to be invalid. c.clearNonces() case !isRetriable(res.StatusCode): - return nil, err + return nil, resErr } retry.inc() + // Ignore the error value from retry.backoff + // and return the one from last retry, as received from the CA. if err := retry.backoff(ctx, req, res); err != nil { - return nil, err + return nil, resErr } } } diff --git a/vendor/vendor.json b/vendor/vendor.json index 4471df2..aca060d 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -835,16 +835,16 @@ "revisionTime": "2018-04-17T22:48:46Z" }, { - "checksumSHA1": "1wzl062spqfhV985nlLdj/cngns=", + "checksumSHA1": "/pfkYB22L5MtzxrfIHQ0tWylnZw=", "path": "golang.org/x/crypto/acme", - "revision": "7f39a6fea4fe9364fb61e1def6a268a51b4f3a06", - "revisionTime": "2018-06-15T16:03:23Z" + "revision": "de0752318171da717af4ce24d0a2e8626afaeb11", + "revisionTime": "2018-08-08T16:52:45Z" }, { - "checksumSHA1": "JjpnjJabumVpCg8Sths68wWNv7Q=", + "checksumSHA1": "Ayt033DfS42Y57XUoOAZvfGUyaA=", "path": "golang.org/x/crypto/acme/autocert", - "revision": "7f39a6fea4fe9364fb61e1def6a268a51b4f3a06", - "revisionTime": "2018-06-15T16:03:23Z" + "revision": "de0752318171da717af4ce24d0a2e8626afaeb11", + "revisionTime": "2018-08-08T16:52:45Z" }, { "checksumSHA1": "IQkUIOnvlf0tYloFx9mLaXSvXWQ=", -- cgit v1.2.3