diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert.go index 12c9010d..453e7229 100644 --- a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert.go +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert.go @@ -10,6 +10,7 @@ package autocert import ( "bytes" + "context" "crypto" "crypto/ecdsa" "crypto/elliptic" @@ -23,16 +24,23 @@ import ( "fmt" "io" mathrand "math/rand" + "net" "net/http" + "path" "strconv" "strings" "sync" "time" "golang.org/x/crypto/acme" - "golang.org/x/net/context" ) +// createCertRetryAfter is how much time to wait before removing a failed state +// entry due to an unsuccessful createCert call. +// This is a variable instead of a const for testing. +// TODO: Consider making it configurable or an exp backoff? +var createCertRetryAfter = time.Minute + // pseudoRand is safe for concurrent use. var pseudoRand *lockedMathRand @@ -41,8 +49,9 @@ func init() { pseudoRand = &lockedMathRand{rnd: mathrand.New(src)} } -// AcceptTOS always returns true to indicate the acceptance of a CA Terms of Service -// during account registration. +// AcceptTOS is a Manager.Prompt function that always returns true to +// indicate acceptance of the CA's Terms of Service during account +// registration. func AcceptTOS(tosURL string) bool { return true } // HostPolicy specifies which host names the Manager is allowed to respond to. @@ -73,23 +82,14 @@ func defaultHostPolicy(context.Context, string) error { } // Manager is a stateful certificate manager built on top of acme.Client. -// It obtains and refreshes certificates automatically, -// as well as providing them to a TLS server via tls.Config. -// -// A simple usage example: +// 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. // -// m := autocert.Manager{ -// Prompt: autocert.AcceptTOS, -// HostPolicy: autocert.HostWhitelist("example.org"), -// } -// s := &http.Server{ -// Addr: ":https", -// TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, -// } -// s.ListenAndServeTLS("", "") -// -// To preserve issued certificates and improve overall performance, -// use a cache implementation of Cache. For instance, DirCache. +// You must specify a cache implementation, such as DirCache, +// to reuse obtained certificates across program restarts. +// Otherwise your server is very likely to exceed the certificate +// issuer's request rate limits. type Manager struct { // Prompt specifies a callback function to conditionally accept a CA's Terms of Service (TOS). // The registration may require the caller to agree to the CA's TOS. @@ -123,7 +123,7 @@ type Manager struct { // RenewBefore optionally specifies how early certificates should // be renewed before they expire. // - // If zero, they're renewed 1 week before expiration. + // If zero, they're renewed 30 days before expiration. RenewBefore time.Duration // Client is used to perform low-level operations, such as account registration @@ -141,21 +141,38 @@ type Manager struct { // If the Client's account key is already registered, Email is not used. Email string + // ForceRSA makes the Manager generate certificates with 2048-bit RSA keys. + // + // If false, a default is used. Currently the default + // is EC-based keys using the P-256 curve. + ForceRSA bool + clientMu sync.Mutex client *acme.Client // initialized by acmeClient method stateMu sync.Mutex state map[string]*certState // keyed by domain name - // tokenCert is keyed by token domain name, which matches server name - // of ClientHello. Keys always have ".acme.invalid" suffix. - tokenCertMu sync.RWMutex - tokenCert map[string]*tls.Certificate - // renewal tracks the set of domains currently running renewal timers. // It is keyed by domain name. renewalMu sync.Mutex renewal map[string]*domainRenewal + + // tokensMu guards the rest of the fields: tryHTTP01, certTokens and httpTokens. + tokensMu sync.RWMutex + // tryHTTP01 indicates whether the Manager should try "http-01" challenge type + // during the authorization flow. + tryHTTP01 bool + // httpTokens contains response body values for http-01 challenges + // and is keyed by the URL path at which a challenge response is expected + // 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 + // and is keyed by token domain name, which matches server name of ClientHello. + // Keys always have ".acme.invalid" suffix. + // The entries are stored for the duration of the authorization flow. + certTokens map[string]*tls.Certificate } // GetCertificate implements the tls.Config.GetCertificate hook. @@ -167,19 +184,34 @@ type Manager struct { // 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. func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if m.Prompt == nil { + return nil, errors.New("acme/autocert: Manager.Prompt not set") + } + name := hello.ServerName if name == "" { return nil, errors.New("acme/autocert: missing server name") } + if !strings.Contains(strings.Trim(name, "."), ".") { + return nil, errors.New("acme/autocert: server name component count invalid") + } + if strings.ContainsAny(name, `/\`) { + return nil, errors.New("acme/autocert: server name contains invalid character") + } + + // In the worst-case scenario, the timeout needs to account for caching, host policy, + // domain ownership verification and certificate issuance. + 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") { - m.tokenCertMu.RLock() - defer m.tokenCertMu.RUnlock() - if cert := m.tokenCert[name]; cert != nil { + m.tokensMu.RLock() + defer m.tokensMu.RUnlock() + if cert := m.certTokens[name]; cert != nil { return cert, nil } - if cert, err := m.cacheGet(name); err == nil { + if cert, err := m.cacheGet(ctx, name); err == nil { return cert, nil } // TODO: cache error results? @@ -187,7 +219,8 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, } // regular domain - cert, err := m.cert(name) + name = strings.TrimSuffix(name, ".") // golang.org/issue/18114 + cert, err := m.cert(ctx, name) if err == nil { return cert, nil } @@ -196,7 +229,6 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, } // first-time - ctx := context.Background() // TODO: use a deadline? if err := m.hostPolicy()(ctx, name); err != nil { return nil, err } @@ -204,14 +236,76 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, if err != nil { return nil, err } - m.cachePut(name, cert) + m.cachePut(ctx, name, cert) return cert, nil } +// HTTPHandler configures the Manager to provision ACME "http-01" challenge responses. +// It returns an http.Handler that responds to the challenges and must be +// running on port 80. If it receives a request that is not an ACME challenge, +// it delegates the request to the optional fallback handler. +// +// If fallback is nil, the returned handler redirects all GET and HEAD requests +// to the default TLS port 443 with 302 Found status code, preserving the original +// request path and query. It responds with 400 Bad Request to all other HTTP methods. +// The fallback is not protected by the optional HostPolicy. +// +// 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. +func (m *Manager) HTTPHandler(fallback http.Handler) http.Handler { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + m.tryHTTP01 = true + + if fallback == nil { + fallback = http.HandlerFunc(handleHTTPRedirect) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { + fallback.ServeHTTP(w, r) + return + } + // A reasonable context timeout for cache and host policy only, + // because we don't wait for a new certificate issuance here. + ctx, cancel := context.WithTimeout(r.Context(), time.Minute) + defer cancel() + if err := m.hostPolicy()(ctx, r.Host); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + data, err := m.httpToken(ctx, r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + w.Write(data) + }) +} + +func handleHTTPRedirect(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" && r.Method != "HEAD" { + http.Error(w, "Use HTTPS", http.StatusBadRequest) + return + } + target := "https://" + stripPort(r.Host) + r.URL.RequestURI() + http.Redirect(w, r, target, http.StatusFound) +} + +func stripPort(hostport string) string { + host, _, err := net.SplitHostPort(hostport) + if err != nil { + return hostport + } + return net.JoinHostPort(host, "443") +} + // cert returns an existing certificate either from m.state or cache. // If a certificate is found in cache but not in m.state, the latter will be filled // with the cached value. -func (m *Manager) cert(name string) (*tls.Certificate, error) { +func (m *Manager) cert(ctx context.Context, name string) (*tls.Certificate, error) { m.stateMu.Lock() if s, ok := m.state[name]; ok { m.stateMu.Unlock() @@ -220,7 +314,7 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) { return s.tlscert() } defer m.stateMu.Unlock() - cert, err := m.cacheGet(name) + cert, err := m.cacheGet(ctx, name) if err != nil { return nil, err } @@ -242,12 +336,11 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) { } // cacheGet always returns a valid certificate, or an error otherwise. -func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) { +// If a cached certficate exists but is not valid, ErrCacheMiss is returned. +func (m *Manager) cacheGet(ctx context.Context, domain string) (*tls.Certificate, error) { if m.Cache == nil { return nil, ErrCacheMiss } - // TODO: might want to define a cache timeout on m - ctx := context.Background() data, err := m.Cache.Get(ctx, domain) if err != nil { return nil, err @@ -256,7 +349,7 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) { // private priv, pub := pem.Decode(data) if priv == nil || !strings.Contains(priv.Type, "PRIVATE") { - return nil, errors.New("acme/autocert: no private key found in cache") + return nil, ErrCacheMiss } privKey, err := parsePrivateKey(priv.Bytes) if err != nil { @@ -274,13 +367,14 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) { pubDER = append(pubDER, b.Bytes) } if len(pub) > 0 { - return nil, errors.New("acme/autocert: invalid public key") + // Leftover content not consumed by pem.Decode. Corrupt. Ignore. + return nil, ErrCacheMiss } // verify and create TLS cert leaf, err := validCert(domain, pubDER, privKey) if err != nil { - return nil, err + return nil, ErrCacheMiss } tlscert := &tls.Certificate{ Certificate: pubDER, @@ -290,7 +384,7 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) { return tlscert, nil } -func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error { +func (m *Manager) cachePut(ctx context.Context, domain string, tlscert *tls.Certificate) error { if m.Cache == nil { return nil } @@ -322,8 +416,6 @@ func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error { } } - // TODO: might want to define a cache timeout on m - ctx := context.Background() return m.Cache.Put(ctx, domain, buf.Bytes()) } @@ -357,12 +449,29 @@ func (m *Manager) createCert(ctx context.Context, domain string) (*tls.Certifica // We are the first; state is locked. // Unblock the readers when domain ownership is verified - // and the we got the cert or the process failed. + // and we got the cert or the process failed. defer state.Unlock() state.locked = false der, leaf, err := m.authorizedCert(ctx, state.key, domain) if err != nil { + // Remove the failed state after some time, + // making the manager call createCert again on the following TLS hello. + time.AfterFunc(createCertRetryAfter, func() { + defer testDidRemoveState(domain) + m.stateMu.Lock() + defer m.stateMu.Unlock() + // Verify the state hasn't changed and it's still invalid + // before deleting. + s, ok := m.state[domain] + if !ok { + return + } + if _, err := validCert(domain, s.cert, s.key); err == nil { + return + } + delete(m.state, domain) + }) return nil, err } state.cert = der @@ -384,11 +493,21 @@ func (m *Manager) certState(domain string) (*certState, error) { if state, ok := m.state[domain]; ok { return state, nil } + // new locked state - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + var ( + err error + key crypto.Signer + ) + if m.ForceRSA { + key, err = rsa.GenerateKey(rand.Reader, 2048) + } else { + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } if err != nil { return nil, err } + state := &certState{ key: key, locked: true, @@ -398,17 +517,17 @@ func (m *Manager) certState(domain string) (*certState, error) { return state, nil } -// authorizedCert starts domain ownership verification process and requests a new cert upon success. +// authorizedCert starts the domain ownership verification process and requests a new cert upon success. // The key argument is the certificate private key. func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain string) (der [][]byte, leaf *x509.Certificate, err error) { - // TODO: make m.verify retry or retry m.verify calls here - if err := m.verify(ctx, domain); err != nil { - return nil, nil, err - } client, err := m.acmeClient(ctx) if err != nil { return nil, nil, err } + + if err := m.verify(ctx, client, domain); err != nil { + return nil, nil, err + } csr, err := certRequest(key, domain) if err != nil { return nil, nil, err @@ -424,98 +543,171 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain return der, leaf, nil } -// verify starts a new identifier (domain) authorization flow. -// It prepares a challenge response and then blocks until the authorization -// is marked as "completed" by the CA (either succeeded or failed). -// -// verify returns nil iff the verification was successful. -func (m *Manager) verify(ctx context.Context, domain string) error { - client, err := m.acmeClient(ctx) - if err != nil { - return err - } - - // start domain authorization and get the challenge - authz, err := client.Authorize(ctx, domain) - if err != nil { - return err - } - // maybe don't need to at all - if authz.Status == acme.StatusValid { - return nil - } +// verify runs the identifier (domain) authorization flow +// using each applicable ACME challenge type. +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"} + m.tokensMu.RLock() + if m.tryHTTP01 { + challengeTypes = append(challengeTypes, "http-01") + } + m.tokensMu.RUnlock() + + var nextTyp int // challengeType index of the next challenge type to try + for { + // Start domain authorization and get the challenge. + authz, err := client.Authorize(ctx, domain) + if err != nil { + return err + } + // No point in accepting challenges if the authorization status + // is in a final state. + switch authz.Status { + case acme.StatusValid: + return nil // already authorized + case acme.StatusInvalid: + return fmt.Errorf("acme/autocert: invalid authorization %q", authz.URI) + } - // pick a challenge: prefer tls-sni-02 over tls-sni-01 - // TODO: consider authz.Combinations - var chal *acme.Challenge - for _, c := range authz.Challenges { - if c.Type == "tls-sni-02" { - chal = c - break + // Pick the next preferred challenge. + var chal *acme.Challenge + for chal == nil && nextTyp < len(challengeTypes) { + chal = pickChallenge(challengeTypes[nextTyp], authz.Challenges) + nextTyp++ } - if c.Type == "tls-sni-01" { - chal = c + if chal == nil { + return fmt.Errorf("acme/autocert: unable to authorize %q; tried %q", domain, challengeTypes) + } + cleanup, err := m.fulfill(ctx, client, chal) + if err != nil { + continue + } + defer cleanup() + if _, err := client.Accept(ctx, chal); err != nil { + continue + } + + // A challenge is fulfilled and accepted: wait for the CA to validate. + if _, err := client.WaitAuthorization(ctx, authz.URI); err == nil { + return nil } } - if chal == nil { - return errors.New("acme/autocert: no supported challenge type found") - } +} - // create a token cert for the challenge response - var ( - cert tls.Certificate - name 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) { switch chal.Type { case "tls-sni-01": - cert, name, err = client.TLSSNI01ChallengeCert(chal.Token) + cert, name, err := client.TLSSNI01ChallengeCert(chal.Token) + if err != nil { + return nil, err + } + m.putCertToken(ctx, name, &cert) + return func() { go m.deleteCertToken(name) }, nil case "tls-sni-02": - cert, name, err = client.TLSSNI02ChallengeCert(chal.Token) - default: - err = fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type) - } - if err != nil { - return err + cert, name, err := client.TLSSNI02ChallengeCert(chal.Token) + if err != nil { + return nil, err + } + m.putCertToken(ctx, name, &cert) + return func() { go m.deleteCertToken(name) }, nil + case "http-01": + resp, err := client.HTTP01ChallengeResponse(chal.Token) + if err != nil { + return nil, err + } + p := client.HTTP01ChallengePath(chal.Token) + m.putHTTPToken(ctx, p, resp) + return func() { go m.deleteHTTPToken(p) }, nil } - m.putTokenCert(name, &cert) - defer func() { - // verification has ended at this point - // don't need token cert anymore - go m.deleteTokenCert(name) - }() + return nil, fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type) +} - // ready to fulfill the challenge - if _, err := client.Accept(ctx, chal); err != nil { - return err +func pickChallenge(typ string, chal []*acme.Challenge) *acme.Challenge { + for _, c := range chal { + if c.Type == typ { + return c + } } - // wait for the CA to validate - _, err = client.WaitAuthorization(ctx, authz.URI) - return err + return nil } -// putTokenCert stores the cert under the named key in both m.tokenCert map +// putCertToken stores the cert under the named key in both m.certTokens map // and m.Cache. -func (m *Manager) putTokenCert(name string, cert *tls.Certificate) { - m.tokenCertMu.Lock() - defer m.tokenCertMu.Unlock() - if m.tokenCert == nil { - m.tokenCert = make(map[string]*tls.Certificate) - } - m.tokenCert[name] = cert - m.cachePut(name, cert) -} - -// deleteTokenCert removes the token certificate for the specified domain name -// from both m.tokenCert map and m.Cache. -func (m *Manager) deleteTokenCert(name string) { - m.tokenCertMu.Lock() - defer m.tokenCertMu.Unlock() - delete(m.tokenCert, name) +func (m *Manager) putCertToken(ctx context.Context, name string, cert *tls.Certificate) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + if m.certTokens == nil { + m.certTokens = make(map[string]*tls.Certificate) + } + m.certTokens[name] = cert + m.cachePut(ctx, name, cert) +} + +// deleteCertToken removes the token certificate for the specified domain name +// from both m.certTokens map and m.Cache. +func (m *Manager) deleteCertToken(name string) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + delete(m.certTokens, name) if m.Cache != nil { m.Cache.Delete(context.Background(), name) } } +// httpToken retrieves an existing http-01 token value from an in-memory map +// or the optional cache. +func (m *Manager) httpToken(ctx context.Context, tokenPath string) ([]byte, error) { + m.tokensMu.RLock() + defer m.tokensMu.RUnlock() + if v, ok := m.httpTokens[tokenPath]; ok { + return v, nil + } + if m.Cache == nil { + return nil, fmt.Errorf("acme/autocert: no token at %q", tokenPath) + } + return m.Cache.Get(ctx, httpTokenCacheKey(tokenPath)) +} + +// putHTTPToken stores an http-01 token value using tokenPath as key +// in both in-memory map and the optional Cache. +// +// It ignores any error returned from Cache.Put. +func (m *Manager) putHTTPToken(ctx context.Context, tokenPath, val string) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + if m.httpTokens == nil { + m.httpTokens = make(map[string][]byte) + } + b := []byte(val) + m.httpTokens[tokenPath] = b + if m.Cache != nil { + m.Cache.Put(ctx, httpTokenCacheKey(tokenPath), b) + } +} + +// deleteHTTPToken removes an http-01 token value from both in-memory map +// and the optional Cache, ignoring any error returned from the latter. +// +// If m.Cache is non-nil, it blocks until Cache.Delete returns without a timeout. +func (m *Manager) deleteHTTPToken(tokenPath string) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + delete(m.httpTokens, tokenPath) + if m.Cache != nil { + m.Cache.Delete(context.Background(), httpTokenCacheKey(tokenPath)) + } +} + +// httpTokenCacheKey returns a key at which an http-01 token value may be stored +// in the Manager's optional Cache. +func httpTokenCacheKey(tokenPath string) string { + return "http-01-" + path.Base(tokenPath) +} + // renew starts a cert renewal timer loop, one per domain. // // The loop is scheduled in two cases: @@ -627,10 +819,10 @@ func (m *Manager) hostPolicy() HostPolicy { } func (m *Manager) renewBefore() time.Duration { - if m.RenewBefore > maxRandRenew { + if m.RenewBefore > renewJitter { return m.RenewBefore } - return 7 * 24 * time.Hour // 1 week + return 720 * time.Hour // 30 days } // certState is ready when its mutex is unlocked for reading. @@ -772,5 +964,10 @@ func (r *lockedMathRand) int63n(max int64) int64 { return n } -// for easier testing -var timeNow = time.Now +// For easier testing. +var ( + timeNow = time.Now + + // Called when a state is removed. + testDidRemoveState = func(domain string) {} +) diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert_test.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert_test.go index 3a9daa10..2da1912e 100644 --- a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert_test.go +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/autocert_test.go @@ -5,6 +5,7 @@ package autocert import ( + "context" "crypto" "crypto/ecdsa" "crypto/elliptic" @@ -22,11 +23,12 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" + "sync" "testing" "time" "golang.org/x/crypto/acme" - "golang.org/x/net/context" ) var discoTmpl = template.Must(template.New("disco").Parse(`{ @@ -47,30 +49,58 @@ var authzTmpl = template.Must(template.New("authz").Parse(`{ "uri": "{{.}}/challenge/2", "type": "tls-sni-02", "token": "token-02" + }, + { + "uri": "{{.}}/challenge/dns-01", + "type": "dns-01", + "token": "token-dns-01" + }, + { + "uri": "{{.}}/challenge/http-01", + "type": "http-01", + "token": "token-http-01" } ] }`)) -type memCache map[string][]byte +type memCache struct { + mu sync.Mutex + keyData map[string][]byte +} + +func (m *memCache) Get(ctx context.Context, key string) ([]byte, error) { + m.mu.Lock() + defer m.mu.Unlock() -func (m memCache) Get(ctx context.Context, key string) ([]byte, error) { - v, ok := m[key] + v, ok := m.keyData[key] if !ok { return nil, ErrCacheMiss } return v, nil } -func (m memCache) Put(ctx context.Context, key string, data []byte) error { - m[key] = data +func (m *memCache) Put(ctx context.Context, key string, data []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.keyData[key] = data return nil } -func (m memCache) Delete(ctx context.Context, key string) error { - delete(m, key) +func (m *memCache) Delete(ctx context.Context, key string) error { + m.mu.Lock() + defer m.mu.Unlock() + + delete(m.keyData, key) return nil } +func newMemCache() *memCache { + return &memCache{ + keyData: make(map[string][]byte), + } +} + func dummyCert(pub interface{}, san ...string) ([]byte, error) { return dateDummyCert(pub, time.Now(), time.Now().Add(90*24*time.Hour), san...) } @@ -108,10 +138,142 @@ func decodePayload(v interface{}, r io.Reader) error { } func TestGetCertificate(t *testing.T) { - const domain = "example.org" man := &Manager{Prompt: AcceptTOS} defer man.stopRenew() + hello := &tls.ClientHelloInfo{ServerName: "example.org"} + testGetCertificate(t, man, "example.org", hello) +} + +func TestGetCertificate_trailingDot(t *testing.T) { + man := &Manager{Prompt: AcceptTOS} + defer man.stopRenew() + hello := &tls.ClientHelloInfo{ServerName: "example.org."} + testGetCertificate(t, man, "example.org", hello) +} + +func TestGetCertificate_ForceRSA(t *testing.T) { + man := &Manager{ + Prompt: AcceptTOS, + Cache: newMemCache(), + ForceRSA: true, + } + defer man.stopRenew() + hello := &tls.ClientHelloInfo{ServerName: "example.org"} + testGetCertificate(t, man, "example.org", hello) + + cert, err := man.cacheGet(context.Background(), "example.org") + if err != nil { + t.Fatalf("man.cacheGet: %v", err) + } + if _, ok := cert.PrivateKey.(*rsa.PrivateKey); !ok { + t.Errorf("cert.PrivateKey is %T; want *rsa.PrivateKey", cert.PrivateKey) + } +} + +func TestGetCertificate_nilPrompt(t *testing.T) { + man := &Manager{} + defer man.stopRenew() + url, finish := startACMEServerStub(t, man, "example.org") + defer finish() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + man.Client = &acme.Client{ + Key: key, + DirectoryURL: url, + } + hello := &tls.ClientHelloInfo{ServerName: "example.org"} + if _, err := man.GetCertificate(hello); err == nil { + t.Error("got certificate for example.org; wanted error") + } +} + +func TestGetCertificate_expiredCache(t *testing.T) { + // Make an expired cert and cache it. + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "example.org"}, + NotAfter: time.Now(), + } + pub, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &pk.PublicKey, pk) + if err != nil { + t.Fatal(err) + } + tlscert := &tls.Certificate{ + Certificate: [][]byte{pub}, + PrivateKey: pk, + } + + man := &Manager{Prompt: AcceptTOS, Cache: newMemCache()} + defer man.stopRenew() + if err := man.cachePut(context.Background(), "example.org", tlscert); err != nil { + t.Fatalf("man.cachePut: %v", err) + } + // The expired cached cert should trigger a new cert issuance + // and return without an error. + hello := &tls.ClientHelloInfo{ServerName: "example.org"} + testGetCertificate(t, man, "example.org", hello) +} + +func TestGetCertificate_failedAttempt(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer ts.Close() + + const example = "example.org" + d := createCertRetryAfter + f := testDidRemoveState + defer func() { + createCertRetryAfter = d + testDidRemoveState = f + }() + createCertRetryAfter = 0 + done := make(chan struct{}) + testDidRemoveState = func(domain string) { + if domain != example { + t.Errorf("testDidRemoveState: domain = %q; want %q", domain, example) + } + close(done) + } + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + man := &Manager{ + Prompt: AcceptTOS, + Client: &acme.Client{ + Key: key, + DirectoryURL: ts.URL, + }, + } + defer man.stopRenew() + hello := &tls.ClientHelloInfo{ServerName: example} + if _, err := man.GetCertificate(hello); err == nil { + t.Error("GetCertificate: err is nil") + } + select { + case <-time.After(5 * time.Second): + t.Errorf("took too long to remove the %q state", example) + case <-done: + man.stateMu.Lock() + defer man.stateMu.Unlock() + if v, exist := man.state[example]; exist { + t.Errorf("state exists for %q: %+v", example, v) + } + } +} + +// startACMEServerStub runs an ACME server +// The domain argument is the expected domain name of a certificate request. +func startACMEServerStub(t *testing.T, man *Manager, domain string) (url string, finish func()) { // echo token-02 | shasum -a 256 // then divide result in 2 parts separated by dot tokenCertName := "4e8eb87631187e9ff2153b56b13a4dec.13a35d002e485d60ff37354b32f665d9.token.acme.invalid" @@ -127,7 +289,7 @@ func TestGetCertificate(t *testing.T) { // ACME CA server stub var ca *httptest.Server ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("replay-nonce", "nonce") + w.Header().Set("Replay-Nonce", "nonce") if r.Method == "HEAD" { // a nonce request return @@ -137,17 +299,17 @@ func TestGetCertificate(t *testing.T) { // discovery case "/": if err := discoTmpl.Execute(w, ca.URL); err != nil { - t.Fatalf("discoTmpl: %v", err) + t.Errorf("discoTmpl: %v", err) } // client key registration case "/new-reg": w.Write([]byte("{}")) // domain authorization case "/new-authz": - w.Header().Set("location", ca.URL+"/authz/1") + w.Header().Set("Location", ca.URL+"/authz/1") w.WriteHeader(http.StatusCreated) if err := authzTmpl.Execute(w, ca.URL); err != nil { - t.Fatalf("authzTmpl: %v", err) + t.Errorf("authzTmpl: %v", err) } // accept tls-sni-02 challenge case "/challenge/2": @@ -165,28 +327,68 @@ func TestGetCertificate(t *testing.T) { b, _ := base64.RawURLEncoding.DecodeString(req.CSR) csr, err := x509.ParseCertificateRequest(b) if err != nil { - t.Fatalf("new-cert: CSR: %v", err) + t.Errorf("new-cert: CSR: %v", err) + } + if csr.Subject.CommonName != domain { + t.Errorf("CommonName in CSR = %q; want %q", csr.Subject.CommonName, domain) } der, err := dummyCert(csr.PublicKey, domain) if err != nil { - t.Fatalf("new-cert: dummyCert: %v", err) + t.Errorf("new-cert: dummyCert: %v", err) } chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL) - w.Header().Set("link", chainUp) + w.Header().Set("Link", chainUp) w.WriteHeader(http.StatusCreated) w.Write(der) // CA chain cert case "/ca-cert": der, err := dummyCert(nil, "ca") if err != nil { - t.Fatalf("ca-cert: dummyCert: %v", err) + t.Errorf("ca-cert: dummyCert: %v", err) } w.Write(der) default: t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) } })) - defer ca.Close() + finish = func() { + ca.Close() + + // make sure token cert was removed + cancel := make(chan struct{}) + done := make(chan struct{}) + go func() { + defer close(done) + tick := time.NewTicker(100 * time.Millisecond) + defer tick.Stop() + for { + hello := &tls.ClientHelloInfo{ServerName: tokenCertName} + if _, err := man.GetCertificate(hello); err != nil { + return + } + select { + case <-tick.C: + case <-cancel: + return + } + } + }() + select { + case <-done: + case <-time.After(5 * time.Second): + close(cancel) + t.Error("token cert was not removed") + <-done + } + } + return ca.URL, finish +} + +// tests man.GetCertificate flow using the provided hello argument. +// The domain argument is the expected domain name of a certificate request. +func testGetCertificate(t *testing.T, man *Manager, domain string, hello *tls.ClientHelloInfo) { + url, finish := startACMEServerStub(t, man, domain) + defer finish() // use EC key to run faster on 386 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -195,14 +397,13 @@ func TestGetCertificate(t *testing.T) { } man.Client = &acme.Client{ Key: key, - DirectoryURL: ca.URL, + DirectoryURL: url, } // simulate tls.Config.GetCertificate var tlscert *tls.Certificate done := make(chan struct{}) go func() { - hello := &tls.ClientHelloInfo{ServerName: domain} tlscert, err = man.GetCertificate(hello) close(done) }() @@ -227,28 +428,150 @@ func TestGetCertificate(t *testing.T) { t.Errorf("cert.DNSNames = %v; want %q", cert.DNSNames, domain) } - // make sure token cert was removed - done = make(chan struct{}) - go func() { - for { - hello := &tls.ClientHelloInfo{ServerName: tokenCertName} - if _, err := man.GetCertificate(hello); err != nil { - break +} + +func TestVerifyHTTP01(t *testing.T) { + var ( + http01 http.Handler + + authzCount int // num. of created authorizations + didAcceptHTTP01 bool + ) + + verifyHTTPToken := func() { + r := httptest.NewRequest("GET", "/.well-known/acme-challenge/token-http-01", nil) + w := httptest.NewRecorder() + http01.ServeHTTP(w, r) + if w.Code != http.StatusOK { + t.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK) + } + if v := string(w.Body.Bytes()); !strings.HasPrefix(v, "token-http-01.") { + t.Errorf("http token value = %q; want 'token-http-01.' prefix", v) + } + } + + // ACME CA server stub, only the needed bits. + // TODO: Merge this with startACMEServerStub, making it a configurable CA for testing. + var ca *httptest.Server + ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Replay-Nonce", "nonce") + if r.Method == "HEAD" { + // a nonce request + return + } + + switch r.URL.Path { + // Discovery. + case "/": + if err := discoTmpl.Execute(w, ca.URL); err != nil { + t.Errorf("discoTmpl: %v", err) } - time.Sleep(100 * time.Millisecond) + // Client key registration. + case "/new-reg": + w.Write([]byte("{}")) + // New domain authorization. + case "/new-authz": + authzCount++ + w.Header().Set("Location", fmt.Sprintf("%s/authz/%d", ca.URL, authzCount)) + w.WriteHeader(http.StatusCreated) + if err := authzTmpl.Execute(w, ca.URL); err != nil { + t.Errorf("authzTmpl: %v", err) + } + // Accept tls-sni-02. + case "/challenge/2": + w.Write([]byte("{}")) + // Reject tls-sni-01. + case "/challenge/1": + http.Error(w, "won't accept tls-sni-01", http.StatusBadRequest) + // Should not accept dns-01. + case "/challenge/dns-01": + t.Errorf("dns-01 challenge was accepted") + http.Error(w, "won't accept dns-01", http.StatusBadRequest) + // Accept http-01. + case "/challenge/http-01": + didAcceptHTTP01 = true + verifyHTTPToken() + w.Write([]byte("{}")) + // Authorization statuses. + // Make tls-sni-xxx invalid. + case "/authz/1", "/authz/2": + w.Write([]byte(`{"status": "invalid"}`)) + case "/authz/3", "/authz/4": + w.Write([]byte(`{"status": "valid"}`)) + default: + http.NotFound(w, r) + t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path) + } + })) + defer ca.Close() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + m := &Manager{ + Client: &acme.Client{ + Key: key, + DirectoryURL: ca.URL, + }, + } + http01 = m.HTTPHandler(nil) + if err := m.verify(context.Background(), m.Client, "example.org"); err != nil { + t.Errorf("m.verify: %v", err) + } + // Only tls-sni-01, tls-sni-02 and http-01 must be accepted + // The dns-01 challenge is unsupported. + if authzCount != 3 { + t.Errorf("authzCount = %d; want 3", authzCount) + } + if !didAcceptHTTP01 { + t.Error("did not accept http-01 challenge") + } +} + +func TestHTTPHandlerDefaultFallback(t *testing.T) { + tt := []struct { + method, url string + wantCode int + wantLocation string + }{ + {"GET", "http://example.org", 302, "https://example.org/"}, + {"GET", "http://example.org/foo", 302, "https://example.org/foo"}, + {"GET", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"}, + {"GET", "http://example.org/?a=b", 302, "https://example.org/?a=b"}, + {"GET", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"}, + {"GET", "http://example.org:80/foo?a=b", 302, "https://example.org:443/foo?a=b"}, + {"GET", "http://example.org:80/foo%20bar", 302, "https://example.org:443/foo%20bar"}, + {"GET", "http://[2602:d1:xxxx::c60a]:1234", 302, "https://[2602:d1:xxxx::c60a]:443/"}, + {"GET", "http://[2602:d1:xxxx::c60a]", 302, "https://[2602:d1:xxxx::c60a]/"}, + {"GET", "http://[2602:d1:xxxx::c60a]/foo?a=b", 302, "https://[2602:d1:xxxx::c60a]/foo?a=b"}, + {"HEAD", "http://example.org", 302, "https://example.org/"}, + {"HEAD", "http://example.org/foo", 302, "https://example.org/foo"}, + {"HEAD", "http://example.org/foo/bar/", 302, "https://example.org/foo/bar/"}, + {"HEAD", "http://example.org/?a=b", 302, "https://example.org/?a=b"}, + {"HEAD", "http://example.org/foo?a=b", 302, "https://example.org/foo?a=b"}, + {"POST", "http://example.org", 400, ""}, + {"PUT", "http://example.org", 400, ""}, + {"GET", "http://example.org/.well-known/acme-challenge/x", 404, ""}, + } + var m Manager + h := m.HTTPHandler(nil) + for i, test := range tt { + r := httptest.NewRequest(test.method, test.url, nil) + w := httptest.NewRecorder() + h.ServeHTTP(w, r) + if w.Code != test.wantCode { + t.Errorf("%d: w.Code = %d; want %d", i, w.Code, test.wantCode) + t.Errorf("%d: body: %s", i, w.Body.Bytes()) + } + if v := w.Header().Get("Location"); v != test.wantLocation { + t.Errorf("%d: Location = %q; want %q", i, v, test.wantLocation) } - close(done) - }() - select { - case <-time.After(5 * time.Second): - t.Error("token cert was not removed") - case <-done: } } func TestAccountKeyCache(t *testing.T) { - cache := make(memCache) - m := Manager{Cache: cache} + m := Manager{Cache: newMemCache()} ctx := context.Background() k1, err := m.accountKey(ctx) if err != nil { @@ -282,13 +605,13 @@ func TestCache(t *testing.T) { PrivateKey: privKey, } - cache := make(memCache) - man := &Manager{Cache: cache} + man := &Manager{Cache: newMemCache()} defer man.stopRenew() - if err := man.cachePut("example.org", tlscert); err != nil { + ctx := context.Background() + if err := man.cachePut(ctx, "example.org", tlscert); err != nil { t.Fatalf("man.cachePut: %v", err) } - res, err := man.cacheGet("example.org") + res, err := man.cacheGet(ctx, "example.org") if err != nil { t.Fatalf("man.cacheGet: %v", err) } @@ -388,3 +711,47 @@ func TestValidCert(t *testing.T) { } } } + +type cacheGetFunc func(ctx context.Context, key string) ([]byte, error) + +func (f cacheGetFunc) Get(ctx context.Context, key string) ([]byte, error) { + return f(ctx, key) +} + +func (f cacheGetFunc) Put(ctx context.Context, key string, data []byte) error { + return fmt.Errorf("unsupported Put of %q = %q", key, data) +} + +func (f cacheGetFunc) Delete(ctx context.Context, key string) error { + return fmt.Errorf("unsupported Delete of %q", key) +} + +func TestManagerGetCertificateBogusSNI(t *testing.T) { + m := Manager{ + Prompt: AcceptTOS, + Cache: cacheGetFunc(func(ctx context.Context, key string) ([]byte, error) { + return nil, fmt.Errorf("cache.Get of %s", key) + }), + } + tests := []struct { + name string + wantErr string + }{ + {"foo.com", "cache.Get of foo.com"}, + {"foo.com.", "cache.Get of foo.com"}, + {`a\b.com`, "acme/autocert: server name contains invalid character"}, + {`a/b.com`, "acme/autocert: server name contains invalid character"}, + {"", "acme/autocert: missing server name"}, + {"foo", "acme/autocert: server name component count invalid"}, + {".foo", "acme/autocert: server name component count invalid"}, + {"foo.", "acme/autocert: server name component count invalid"}, + {"fo.o", "cache.Get of fo.o"}, + } + for _, tt := range tests { + _, err := m.GetCertificate(&tls.ClientHelloInfo{ServerName: tt.name}) + got := fmt.Sprint(err) + if got != tt.wantErr { + t.Errorf("GetCertificate(SNI = %q) = %q; want %q", tt.name, got, tt.wantErr) + } + } +} diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache.go index 1c67f6ce..61a5fd23 100644 --- a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache.go +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache.go @@ -5,12 +5,11 @@ package autocert import ( + "context" "errors" "io/ioutil" "os" "path/filepath" - - "golang.org/x/net/context" ) // ErrCacheMiss is returned when a certificate is not found in cache. @@ -27,7 +26,7 @@ type Cache interface { Get(ctx context.Context, key string) ([]byte, error) // Put stores the data in the cache under the specified key. - // Inderlying implementations may use any data storage format, + // Underlying implementations may use any data storage format, // as long as the reverse operation, Get, results in the original data. Put(ctx context.Context, key string, data []byte) error @@ -78,12 +77,13 @@ func (d DirCache) Put(ctx context.Context, name string, data []byte) error { if tmp, err = d.writeTempFile(name, data); err != nil { return } - // prevent overwriting the file if the context was cancelled - if ctx.Err() != nil { - return // no need to set err + select { + case <-ctx.Done(): + // Don't overwrite the file if the context was canceled. + default: + newName := filepath.Join(string(d), name) + err = os.Rename(tmp, newName) } - name = filepath.Join(string(d), name) - err = os.Rename(tmp, name) }() select { case <-ctx.Done(): diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache_test.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache_test.go index ad6d4a46..653b05be 100644 --- a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache_test.go +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/cache_test.go @@ -5,13 +5,12 @@ package autocert import ( + "context" "io/ioutil" "os" "path/filepath" "reflect" "testing" - - "golang.org/x/net/context" ) // make sure DirCache satisfies Cache interface @@ -22,6 +21,7 @@ func TestDirCache(t *testing.T) { if err != nil { t.Fatal(err) } + defer os.RemoveAll(dir) dir = filepath.Join(dir, "certs") // a nonexistent dir cache := DirCache(dir) ctx := context.Background() diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/example_test.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/example_test.go new file mode 100644 index 00000000..552a6254 --- /dev/null +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/example_test.go @@ -0,0 +1,36 @@ +// Copyright 2017 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 autocert_test + +import ( + "crypto/tls" + "fmt" + "log" + "net/http" + + "golang.org/x/crypto/acme/autocert" +) + +func ExampleNewListener() { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, TLS user! Your config: %+v", r.TLS) + }) + log.Fatal(http.Serve(autocert.NewListener("example.com"), mux)) +} + +func ExampleManager() { + m := &autocert.Manager{ + Cache: autocert.DirCache("secret-dir"), + 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}, + } + s.ListenAndServeTLS("", "") +} diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/listener.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/listener.go new file mode 100644 index 00000000..d744df0e --- /dev/null +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/listener.go @@ -0,0 +1,160 @@ +// Copyright 2017 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 autocert + +import ( + "crypto/tls" + "log" + "net" + "os" + "path/filepath" + "runtime" + "time" +) + +// NewListener returns a net.Listener that listens on the standard TLS +// port (443) on all interfaces and returns *tls.Conn connections with +// LetsEncrypt certificates for the provided domain or domains. +// +// It enables one-line HTTPS servers: +// +// log.Fatal(http.Serve(autocert.NewListener("example.com"), handler)) +// +// NewListener is a convenience function for a common configuration. +// More complex or custom configurations can use the autocert.Manager +// type instead. +// +// Use of this function implies acceptance of the LetsEncrypt Terms of +// Service. If domains is not empty, the provided domains are passed +// to HostWhitelist. If domains is empty, the listener will do +// LetsEncrypt challenges for any requested domain, which is not +// recommended. +// +// Certificates are cached in a "golang-autocert" directory under an +// operating system-specific cache or temp directory. This may not +// be suitable for servers spanning multiple machines. +// +// The returned listener uses a *tls.Config that enables HTTP/2, and +// should only be used with servers that support HTTP/2. +// +// The returned Listener also enables TCP keep-alives on the accepted +// connections. The returned *tls.Conn are returned before their TLS +// handshake has completed. +func NewListener(domains ...string) net.Listener { + m := &Manager{ + Prompt: AcceptTOS, + } + if len(domains) > 0 { + m.HostPolicy = HostWhitelist(domains...) + } + dir := cacheDir() + if err := os.MkdirAll(dir, 0700); err != nil { + log.Printf("warning: autocert.NewListener not using a cache: %v", err) + } else { + m.Cache = DirCache(dir) + } + return m.Listener() +} + +// Listener listens on the standard TLS port (443) on all interfaces +// and returns a net.Listener returning *tls.Conn connections. +// +// The returned listener uses a *tls.Config that enables HTTP/2, and +// should only be used with servers that support HTTP/2. +// +// The returned Listener also enables TCP keep-alives on the accepted +// connections. The returned *tls.Conn are returned before their TLS +// handshake has completed. +// +// Unlike NewListener, it is the caller's responsibility to initialize +// 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 + }, + } + ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443") + return ln +} + +type listener struct { + m *Manager + conf *tls.Config + + tcpListener net.Listener + tcpListenErr error +} + +func (ln *listener) Accept() (net.Conn, error) { + if ln.tcpListenErr != nil { + return nil, ln.tcpListenErr + } + conn, err := ln.tcpListener.Accept() + if err != nil { + return nil, err + } + tcpConn := conn.(*net.TCPConn) + + // Because Listener is a convenience function, help out with + // this too. This is not possible for the caller to set once + // we return a *tcp.Conn wrapping an inaccessible net.Conn. + // If callers don't want this, they can do things the manual + // way and tweak as needed. But this is what net/http does + // itself, so copy that. If net/http changes, we can change + // here too. + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(3 * time.Minute) + + return tls.Server(tcpConn, ln.conf), nil +} + +func (ln *listener) Addr() net.Addr { + if ln.tcpListener != nil { + return ln.tcpListener.Addr() + } + // net.Listen failed. Return something non-nil in case callers + // call Addr before Accept: + return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 443} +} + +func (ln *listener) Close() error { + if ln.tcpListenErr != nil { + return ln.tcpListenErr + } + return ln.tcpListener.Close() +} + +func homeDir() string { + if runtime.GOOS == "windows" { + return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + } + if h := os.Getenv("HOME"); h != "" { + return h + } + return "/" +} + +func cacheDir() string { + const base = "golang-autocert" + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir(), "Library", "Caches", base) + case "windows": + for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} { + if v := os.Getenv(ev); v != "" { + return filepath.Join(v, base) + } + } + // Worst case: + return filepath.Join(homeDir(), base) + } + if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { + return filepath.Join(xdg, base) + } + return filepath.Join(homeDir(), ".cache", base) +} diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal.go index 1a5018c8..6c5da2bc 100644 --- a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal.go +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal.go @@ -5,15 +5,14 @@ package autocert import ( + "context" "crypto" "sync" "time" - - "golang.org/x/net/context" ) -// maxRandRenew is a maximum deviation from Manager.RenewBefore. -const maxRandRenew = time.Hour +// renewJitter is the maximum deviation from Manager.RenewBefore. +const renewJitter = time.Hour // domainRenewal tracks the state used by the periodic timers // renewing a single domain's cert. @@ -65,7 +64,7 @@ func (dr *domainRenewal) renew() { // TODO: rotate dr.key at some point? next, err := dr.do(ctx) if err != nil { - next = maxRandRenew / 2 + next = renewJitter / 2 next += time.Duration(pseudoRand.int63n(int64(next))) } dr.timer = time.AfterFunc(next, dr.renew) @@ -83,9 +82,9 @@ func (dr *domainRenewal) renew() { func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) { // a race is likely unavoidable in a distributed environment // but we try nonetheless - if tlscert, err := dr.m.cacheGet(dr.domain); err == nil { + if tlscert, err := dr.m.cacheGet(ctx, dr.domain); err == nil { next := dr.next(tlscert.Leaf.NotAfter) - if next > dr.m.renewBefore()+maxRandRenew { + if next > dr.m.renewBefore()+renewJitter { return next, nil } } @@ -103,7 +102,7 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) { if err != nil { return 0, err } - dr.m.cachePut(dr.domain, tlscert) + dr.m.cachePut(ctx, dr.domain, tlscert) dr.m.stateMu.Lock() defer dr.m.stateMu.Unlock() // m.state is guaranteed to be non-nil at this point @@ -114,7 +113,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() // add a bit of randomness to renew deadline - n := pseudoRand.int63n(int64(maxRandRenew)) + n := pseudoRand.int63n(int64(renewJitter)) d -= time.Duration(n) if d < 0 { return 0 diff --git a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal_test.go b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal_test.go index d1ec52f4..11d40ff5 100644 --- a/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal_test.go +++ b/cmd/ponzu/vendor/golang.org/x/crypto/autocert/renewal_test.go @@ -5,6 +5,7 @@ package autocert import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -31,7 +32,7 @@ func TestRenewalNext(t *testing.T) { expiry time.Time min, max time.Duration }{ - {now.Add(90 * 24 * time.Hour), 83*24*time.Hour - maxRandRenew, 83 * 24 * time.Hour}, + {now.Add(90 * 24 * time.Hour), 83*24*time.Hour - renewJitter, 83 * 24 * time.Hour}, {now.Add(time.Hour), 0, 1}, {now, 0, 1}, {now.Add(-time.Hour), 0, 1}, @@ -52,7 +53,7 @@ func TestRenewFromCache(t *testing.T) { // ACME CA server stub var ca *httptest.Server ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("replay-nonce", "nonce") + w.Header().Set("Replay-Nonce", "nonce") if r.Method == "HEAD" { // a nonce request return @@ -69,7 +70,7 @@ func TestRenewFromCache(t *testing.T) { w.Write([]byte("{}")) // domain authorization case "/new-authz": - w.Header().Set("location", ca.URL+"/authz/1") + w.Header().Set("Location", ca.URL+"/authz/1") w.WriteHeader(http.StatusCreated) w.Write([]byte(`{"status": "valid"}`)) // cert request @@ -88,7 +89,7 @@ func TestRenewFromCache(t *testing.T) { t.Fatalf("new-cert: dummyCert: %v", err) } chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL) - w.Header().Set("link", chainUp) + w.Header().Set("Link", chainUp) w.WriteHeader(http.StatusCreated) w.Write(der) // CA chain cert @@ -111,7 +112,7 @@ func TestRenewFromCache(t *testing.T) { } man := &Manager{ Prompt: AcceptTOS, - Cache: make(memCache), + Cache: newMemCache(), RenewBefore: 24 * time.Hour, Client: &acme.Client{ Key: key, @@ -127,7 +128,7 @@ func TestRenewFromCache(t *testing.T) { t.Fatal(err) } tlscert := &tls.Certificate{PrivateKey: key, Certificate: [][]byte{cert}} - if err := man.cachePut(domain, tlscert); err != nil { + if err := man.cachePut(context.Background(), domain, tlscert); err != nil { t.Fatal(err) } @@ -151,7 +152,7 @@ func TestRenewFromCache(t *testing.T) { // ensure the new cert is cached after := time.Now().Add(future) - tlscert, err := man.cacheGet(domain) + tlscert, err := man.cacheGet(context.Background(), domain) if err != nil { t.Fatalf("man.cacheGet: %v", err) } diff --git a/system/tls/enable.go b/system/tls/enable.go index a90f2d1a..9fc95f98 100644 --- a/system/tls/enable.go +++ b/system/tls/enable.go @@ -16,10 +16,9 @@ import ( "golang.org/x/crypto/acme/autocert" ) -var m autocert.Manager - -// setup attempts to locate or create the cert cache directory and the certs for TLS encryption -func setup() { +// newManager attempts to locate or create the cert cache directory and the +// certs for TLS encryption and returns an autocert.Manager +func newManager() autocert.Manager { pwd, err := os.Getwd() if err != nil { log.Fatalln("Couldn't find working directory to locate or save certificates.") @@ -57,25 +56,27 @@ func setup() { } fmt.Println("Using", string(email), "as contact email for certificate...") - m = autocert.Manager{ + return autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: cache, HostPolicy: autocert.HostWhitelist(string(host)), RenewBefore: time.Hour * 24 * 30, Email: string(email), } - } // Enable runs the setup for creating or locating production certificates and // starts the TLS server func Enable() { - setup() + m := newManager() server := &http.Server{ Addr: fmt.Sprintf(":%s", db.ConfigCache("https_port").(string)), TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, } + // launch http listener for "http-01" ACME challenge + go http.ListenAndServe(":http", m.HTTPHandler(nil)) + log.Fatalln(server.ListenAndServeTLS("", "")) }