Browse Source

acme/autocert: implement certificates renewal

A cert renewal loop is started when a certificate is fetched
from cache for the first time or a new one is created.
At most one renew loop is running per domain.

Closes #16851

Change-Id: I3d5821d8d76e9f2d9b551d0976ebc4cf91647092
Reviewed-on: https://go-review.googlesource.com/27611
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Alex Vaghin 9 năm trước cách đây
mục cha
commit
33b41827e6
4 tập tin đã thay đổi với 614 bổ sung169 xóa
  1. 202 100
      acme/autocert/autocert.go
  2. 137 69
      acme/autocert/autocert_test.go
  3. 87 0
      acme/autocert/renewal.go
  4. 188 0
      acme/autocert/renewal_test.go

+ 202 - 100
acme/autocert/autocert.go

@@ -20,8 +20,8 @@ import (
 	"encoding/pem"
 	"errors"
 	"fmt"
+	mathrand "math/rand"
 	"net/http"
-	"sort"
 	"strconv"
 	"strings"
 	"sync"
@@ -31,6 +31,14 @@ import (
 	"golang.org/x/net/context"
 )
 
+// pseudoRand is safe for concurrent use.
+var pseudoRand *lockedMathRand
+
+func init() {
+	src := mathrand.NewSource(timeNow().UnixNano())
+	pseudoRand = &lockedMathRand{rnd: mathrand.New(src)}
+}
+
 // AcceptTOS always returns true to indicate the acceptance of a CA Terms of Service
 // during account registration.
 func AcceptTOS(tosURL string) bool { return true }
@@ -110,6 +118,12 @@ type Manager struct {
 	// See GetCertificate for more details.
 	HostPolicy HostPolicy
 
+	// RenewBefore optionally specifies how early certificates should
+	// be renewed before they expire.
+	//
+	// If zero, they're renewed 1 week before expiration.
+	RenewBefore time.Duration
+
 	// Client is used to perform low-level operations, such as account registration
 	// and requesting new certificates.
 	// If Client is nil, a zero-value acme.Client is used with acme.LetsEncryptURL
@@ -135,6 +149,11 @@ type Manager struct {
 	// 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
 }
 
 // GetCertificate implements the tls.Config.GetCertificate hook.
@@ -192,8 +211,7 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
 // with the cached value.
 func (m *Manager) cert(name string) (*tls.Certificate, error) {
 	m.stateMu.Lock()
-	s, ok := m.state[name]
-	if ok {
+	if s, ok := m.state[name]; ok {
 		m.stateMu.Unlock()
 		s.RLock()
 		defer s.RUnlock()
@@ -211,11 +229,13 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) {
 	if m.state == nil {
 		m.state = make(map[string]*certState)
 	}
-	m.state[name] = &certState{
+	s := &certState{
 		key:  signer,
 		cert: cert.Certificate,
 		leaf: cert.Leaf,
 	}
+	m.state[name] = s
+	go m.renew(name, s.key, s.leaf.NotAfter)
 	return cert, nil
 }
 
@@ -242,65 +262,29 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
 	}
 
 	// public
-	var pubDER []byte
+	var pubDER [][]byte
 	for len(pub) > 0 {
 		var b *pem.Block
 		b, pub = pem.Decode(pub)
 		if b == nil {
 			break
 		}
-		pubDER = append(pubDER, b.Bytes...)
+		pubDER = append(pubDER, b.Bytes)
 	}
 	if len(pub) > 0 {
 		return nil, errors.New("acme/autocert: invalid public key")
 	}
 
-	// parse public part(s) and verify the leaf is not expired
-	// and corresponds to the private key
-	x509Cert, err := x509.ParseCertificates(pubDER)
-	if len(x509Cert) == 0 {
-		return nil, errors.New("acme/autocert: no public key found in cache")
-	}
-	leaf := x509Cert[0]
-	now := time.Now()
-	if now.Before(leaf.NotBefore) {
-		return nil, errors.New("acme/autocert: certificate is not valid yet")
-	}
-	if now.After(leaf.NotAfter) {
-		return nil, errors.New("acme/autocert: expired certificate")
-	}
-	if !domainMatch(leaf, domain) {
-		return nil, errors.New("acme/autocert: certificate does not match domain name")
-	}
-	switch pub := leaf.PublicKey.(type) {
-	case *rsa.PublicKey:
-		prv, ok := privKey.(*rsa.PrivateKey)
-		if !ok {
-			return nil, errors.New("acme/autocert: private key type does not match public key type")
-		}
-		if pub.N.Cmp(prv.N) != 0 {
-			return nil, errors.New("acme/autocert: private key does not match public key")
-		}
-	case *ecdsa.PublicKey:
-		prv, ok := privKey.(*ecdsa.PrivateKey)
-		if !ok {
-			return nil, errors.New("acme/autocert: private key type does not match public key type")
-		}
-		if pub.X.Cmp(prv.X) != 0 || pub.Y.Cmp(prv.Y) != 0 {
-			return nil, errors.New("acme/autocert: private key does not match public key")
-		}
-	default:
-		return nil, errors.New("acme/autocert: unknown public key algorithm")
+	// verify and create TLS cert
+	leaf, err := validCert(domain, pubDER, privKey)
+	if err != nil {
+		return nil, err
 	}
-
 	tlscert := &tls.Certificate{
-		Certificate: make([][]byte, len(x509Cert)),
+		Certificate: pubDER,
 		PrivateKey:  privKey,
 		Leaf:        leaf,
 	}
-	for i, crt := range x509Cert {
-		tlscert.Certificate[i] = crt.Raw
-	}
 	return tlscert, nil
 }
 
@@ -346,46 +330,92 @@ func (m *Manager) cachePut(domain string, tlscert *tls.Certificate) error {
 	return m.Cache.Put(ctx, domain, buf.Bytes())
 }
 
-// createCert starts domain ownership verification and returns a certificate for that domain
-// upon success.
+// createCert starts the domain ownership verification and returns a certificate
+// for that domain upon success.
 //
 // If the domain is already being verified, it waits for the existing verification to complete.
 // Either way, createCert blocks for the duration of the whole process.
 func (m *Manager) createCert(ctx context.Context, domain string) (*tls.Certificate, error) {
-	state, ok, err := m.certState(domain)
+	// TODO: maybe rewrite this whole piece using sync.Once
+	state, err := m.certState(domain)
 	if err != nil {
 		return nil, err
 	}
 	// state may exist if another goroutine is already working on it
 	// in which case just wait for it to finish
-	if ok {
+	if !state.locked {
 		state.RLock()
 		defer state.RUnlock()
 		return state.tlscert()
 	}
 
-	// We are the first.
+	// 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.
 	defer state.Unlock()
+	state.locked = false
+
+	der, leaf, err := m.authorizedCert(ctx, state.key, domain)
+	if err != nil {
+		return nil, err
+	}
+	state.cert = der
+	state.leaf = leaf
+	go m.renew(domain, state.key, state.leaf.NotAfter)
+	return state.tlscert()
+}
+
+// certState returns a new or existing certState.
+// If a new certState is returned, state.exist is false and the state is locked.
+// The returned error is non-nil only in the case where a new state could not be created.
+func (m *Manager) certState(domain string) (*certState, error) {
+	m.stateMu.Lock()
+	defer m.stateMu.Unlock()
+	if m.state == nil {
+		m.state = make(map[string]*certState)
+	}
+	// existing state
+	if state, ok := m.state[domain]; ok {
+		return state, nil
+	}
+	// new locked state
+	key, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return nil, err
+	}
+	state := &certState{
+		key:    key,
+		locked: true,
+	}
+	state.Lock() // will be unlocked by m.certState caller
+	m.state[domain] = state
+	return state, nil
+}
+
+// authorizedCert starts 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, err
+		return nil, nil, err
 	}
 	client, err := m.acmeClient(ctx)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	csr, err := certRequest(state.key, domain)
+	csr, err := certRequest(key, domain)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	der, _, err := client.CreateCert(ctx, csr, 0, true)
+	der, _, err = client.CreateCert(ctx, csr, 0, true)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	state.cert = der
-	return state.tlscert()
+	leaf, err = validCert(domain, der, key)
+	if err != nil {
+		return nil, nil, err
+	}
+	return der, leaf, nil
 }
 
 // verify starts a new identifier (domain) authorization flow.
@@ -457,29 +487,6 @@ func (m *Manager) verify(ctx context.Context, domain string) error {
 	return err
 }
 
-// certState returns existing state or creates a new one locked for read/write.
-// The boolean return value indicates whether the state was found in m.state.
-func (m *Manager) certState(domain string) (*certState, bool, error) {
-	m.stateMu.Lock()
-	defer m.stateMu.Unlock()
-	if m.state == nil {
-		m.state = make(map[string]*certState)
-	}
-	// existing state
-	if state, ok := m.state[domain]; ok {
-		return state, true, nil
-	}
-	// new locked state
-	key, err := rsa.GenerateKey(rand.Reader, 2048)
-	if err != nil {
-		return nil, false, err
-	}
-	state := &certState{key: key}
-	state.Lock()
-	m.state[domain] = state
-	return state, false, nil
-}
-
 // putTokenCert stores the cert under the named key in both m.tokenCert map
 // and m.Cache.
 func (m *Manager) putTokenCert(name string, cert *tls.Certificate) {
@@ -503,6 +510,29 @@ func (m *Manager) deleteTokenCert(name string) {
 	}
 }
 
+// renew starts a cert renewal timer loop, one per domain.
+//
+// The loop is scheduled in two cases:
+// - a cert was fetched from cache for the first time (wasn't in m.state)
+// - a new cert was created by m.createCert
+//
+// The key argument is a certificate private key.
+// The exp argument is the cert expiration time (NotAfter).
+func (m *Manager) renew(domain string, key crypto.Signer, exp time.Time) {
+	m.renewalMu.Lock()
+	defer m.renewalMu.Unlock()
+	if m.renewal[domain] != nil {
+		// another goroutine is already on it
+		return
+	}
+	if m.renewal == nil {
+		m.renewal = make(map[string]*domainRenewal)
+	}
+	dr := &domainRenewal{m: m, domain: domain, key: key}
+	m.renewal[domain] = dr
+	time.AfterFunc(dr.next(exp), dr.renew)
+}
+
 func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) {
 	m.clientMu.Lock()
 	defer m.clientMu.Unlock()
@@ -542,12 +572,20 @@ func (m *Manager) hostPolicy() HostPolicy {
 	return defaultHostPolicy
 }
 
+func (m *Manager) renewBefore() time.Duration {
+	if m.RenewBefore > maxRandRenew {
+		return m.RenewBefore
+	}
+	return 7 * 24 * time.Hour // 1 week
+}
+
 // certState is ready when its mutex is unlocked for reading.
 type certState struct {
 	sync.RWMutex
-	key  crypto.Signer
-	cert [][]byte          // DER encoding
-	leaf *x509.Certificate // parsed cert[0]; may be nil
+	locked bool              // locked for read/write
+	key    crypto.Signer     // private key for cert
+	cert   [][]byte          // DER encoding
+	leaf   *x509.Certificate // parsed cert[0]; always non-nil if cert != nil
 }
 
 // tlscert creates a tls.Certificate from s.key and s.cert.
@@ -559,7 +597,6 @@ func (s *certState) tlscert() (*tls.Certificate, error) {
 	if len(s.cert) == 0 {
 		return nil, errors.New("acme/autocert: missing certificate")
 	}
-	// TODO: compare pub.N with key.N or pub.{X,Y} for ECDSA?
 	return &tls.Certificate{
 		PrivateKey:  s.key,
 		Certificate: s.cert,
@@ -581,17 +618,19 @@ func certRequest(key crypto.Signer, cn string, san ...string) ([]byte, error) {
 // PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
 // OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
 //
-// Copied from crypto/tls/tls.go.
-func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
+// Inspired by parsePrivateKey in crypto/tls/tls.go.
+func parsePrivateKey(der []byte) (crypto.Signer, error) {
 	if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
 		return key, nil
 	}
 	if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
 		switch key := key.(type) {
-		case *rsa.PrivateKey, *ecdsa.PrivateKey:
+		case *rsa.PrivateKey:
+			return key, nil
+		case *ecdsa.PrivateKey:
 			return key, nil
 		default:
-			return nil, errors.New("acme/autocert: found unknown private key type in PKCS#8 wrapping")
+			return nil, errors.New("acme/autocert: unknown private key type in PKCS#8 wrapping")
 		}
 	}
 	if key, err := x509.ParseECPrivateKey(der); err == nil {
@@ -601,15 +640,60 @@ func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
 	return nil, errors.New("acme/autocert: failed to parse private key")
 }
 
-// domainMatch matches cert against the specified domain name.
-// It doesn't support wildcard.
-func domainMatch(cert *x509.Certificate, name string) bool {
-	if cert.Subject.CommonName == name {
-		return true
+// validCert parses a cert chain provided as der argument and verifies the leaf, der[0],
+// corresponds to the private key, as well as the domain match and expiration dates.
+// It doesn't do any revocation checking.
+//
+// The returned value is the verified leaf cert.
+func validCert(domain string, der [][]byte, key crypto.Signer) (leaf *x509.Certificate, err error) {
+	// parse public part(s)
+	var n int
+	for _, b := range der {
+		n += len(b)
+	}
+	pub := make([]byte, n)
+	n = 0
+	for _, b := range der {
+		n += copy(pub[n:], b)
+	}
+	x509Cert, err := x509.ParseCertificates(pub)
+	if len(x509Cert) == 0 {
+		return nil, errors.New("acme/autocert: no public key found")
 	}
-	sort.Strings(cert.DNSNames)
-	i := sort.SearchStrings(cert.DNSNames, name)
-	return i < len(cert.DNSNames) && cert.DNSNames[i] == name
+	// 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")
+	}
+	if now.After(leaf.NotAfter) {
+		return nil, errors.New("acme/autocert: expired certificate")
+	}
+	if err := leaf.VerifyHostname(domain); err != nil {
+		return nil, err
+	}
+	// ensure the leaf corresponds to the private key
+	switch pub := leaf.PublicKey.(type) {
+	case *rsa.PublicKey:
+		prv, ok := key.(*rsa.PrivateKey)
+		if !ok {
+			return nil, errors.New("acme/autocert: private key type does not match public key type")
+		}
+		if pub.N.Cmp(prv.N) != 0 {
+			return nil, errors.New("acme/autocert: private key does not match public key")
+		}
+	case *ecdsa.PublicKey:
+		prv, ok := key.(*ecdsa.PrivateKey)
+		if !ok {
+			return nil, errors.New("acme/autocert: private key type does not match public key type")
+		}
+		if pub.X.Cmp(prv.X) != 0 || pub.Y.Cmp(prv.Y) != 0 {
+			return nil, errors.New("acme/autocert: private key does not match public key")
+		}
+	default:
+		return nil, errors.New("acme/autocert: unknown public key algorithm")
+	}
+	return leaf, nil
 }
 
 func retryAfter(v string) time.Duration {
@@ -617,7 +701,25 @@ func retryAfter(v string) time.Duration {
 		return time.Duration(i) * time.Second
 	}
 	if t, err := http.ParseTime(v); err == nil {
-		return t.Sub(time.Now())
+		return t.Sub(timeNow())
 	}
 	return time.Second
 }
+
+type lockedMathRand struct {
+	sync.Mutex
+	rnd *mathrand.Rand
+}
+
+func (r *lockedMathRand) int63n(max int64) int64 {
+	r.Lock()
+	n := r.rnd.Int63n(max)
+	r.Unlock()
+	return n
+}
+
+// for easier testing
+var (
+	timeNow          = time.Now
+	testDidRenewLoop = func(next time.Duration, err error) {}
+)

+ 137 - 69
acme/autocert/autocert_test.go

@@ -5,16 +5,19 @@
 package autocert
 
 import (
-	"bytes"
+	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
+	"crypto/rsa"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	"encoding/pem"
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"html/template"
+	"io"
 	"math/big"
 	"net/http"
 	"net/http/httptest"
@@ -47,7 +50,31 @@ var authzTmpl = template.Must(template.New("authz").Parse(`{
 	]
 }`))
 
-func dummyCert(san ...string) ([]byte, error) {
+type memCache map[string][]byte
+
+func (m memCache) Get(ctx context.Context, key string) ([]byte, error) {
+	v, ok := m[key]
+	if !ok {
+		return nil, ErrCacheMiss
+	}
+	return v, nil
+}
+
+func (m memCache) Put(ctx context.Context, key string, data []byte) error {
+	m[key] = data
+	return nil
+}
+
+func (m memCache) Delete(ctx context.Context, key string) error {
+	delete(m, key)
+	return nil
+}
+
+func dummyCert(pub interface{}, san ...string) ([]byte, error) {
+	return dateDummyCert(pub, time.Now(), time.Now().Add(90*24*time.Hour), san...)
+}
+
+func dateDummyCert(pub interface{}, start, end time.Time, san ...string) ([]byte, error) {
 	// use EC key to run faster on 386
 	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 	if err != nil {
@@ -55,13 +82,28 @@ func dummyCert(san ...string) ([]byte, error) {
 	}
 	t := &x509.Certificate{
 		SerialNumber:          big.NewInt(1),
-		NotBefore:             time.Now(),
-		NotAfter:              time.Now().Add(24 * time.Hour),
+		NotBefore:             start,
+		NotAfter:              end,
 		BasicConstraintsValid: true,
 		KeyUsage:              x509.KeyUsageKeyEncipherment,
 		DNSNames:              san,
 	}
-	return x509.CreateCertificate(rand.Reader, t, t, &key.PublicKey, key)
+	if pub == nil {
+		pub = &key.PublicKey
+	}
+	return x509.CreateCertificate(rand.Reader, t, t, pub, key)
+}
+
+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 TestGetCertificate(t *testing.T) {
@@ -84,6 +126,11 @@ func TestGetCertificate(t *testing.T) {
 	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 "/":
@@ -109,7 +156,16 @@ func TestGetCertificate(t *testing.T) {
 			w.Write([]byte(`{"status": "valid"}`))
 		// cert request
 		case "/new-cert":
-			der, err := dummyCert(domain)
+			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 {
+				t.Fatalf("new-cert: CSR: %v", err)
+			}
+			der, err := dummyCert(csr.PublicKey, domain)
 			if err != nil {
 				t.Fatalf("new-cert: dummyCert: %v", err)
 			}
@@ -119,7 +175,7 @@ func TestGetCertificate(t *testing.T) {
 			w.Write(der)
 		// CA chain cert
 		case "/ca-cert":
-			der, err := dummyCert("ca")
+			der, err := dummyCert(nil, "ca")
 			if err != nil {
 				t.Fatalf("ca-cert: dummyCert: %v", err)
 			}
@@ -131,9 +187,9 @@ func TestGetCertificate(t *testing.T) {
 	defer ca.Close()
 
 	// use EC key to run faster on 386
-	key, kerr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if kerr != nil {
-		t.Fatal(kerr)
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
 	}
 	man.Client = &acme.Client{
 		Key:          key,
@@ -141,11 +197,8 @@ func TestGetCertificate(t *testing.T) {
 	}
 
 	// simulate tls.Config.GetCertificate
-	var (
-		tlscert *tls.Certificate
-		err     error
-		done    = make(chan struct{})
-	)
+	var tlscert *tls.Certificate
+	done := make(chan struct{})
 	go func() {
 		hello := &tls.ClientHelloInfo{ServerName: domain}
 		tlscert, err = man.GetCertificate(hello)
@@ -191,26 +244,6 @@ func TestGetCertificate(t *testing.T) {
 	}
 }
 
-type memCache map[string][]byte
-
-func (m memCache) Get(ctx context.Context, key string) ([]byte, error) {
-	v, ok := m[key]
-	if !ok {
-		return nil, ErrCacheMiss
-	}
-	return v, nil
-}
-
-func (m memCache) Put(ctx context.Context, key string, data []byte) error {
-	m[key] = data
-	return nil
-}
-
-func (m memCache) Delete(ctx context.Context, key string) error {
-	delete(m, key)
-	return nil
-}
-
 func TestCache(t *testing.T) {
 	privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 	if err != nil {
@@ -242,39 +275,6 @@ func TestCache(t *testing.T) {
 	if res == nil {
 		t.Fatal("res is nil")
 	}
-
-	priv, err := x509.MarshalECPrivateKey(privKey)
-	if err != nil {
-		t.Fatalf("MarshalECPrivateKey: %v", err)
-	}
-	dummy, err := dummyCert("dummy")
-	if err != nil {
-		t.Fatalf("dummyCert: %v", err)
-	}
-	tt := []struct {
-		key      string
-		prv, pub []byte
-	}{
-		{"dummy", priv, dummy},
-		{"bad1", priv, []byte{1}},
-		{"bad2", []byte{1}, pub},
-	}
-	for i, test := range tt {
-		var buf bytes.Buffer
-		pb := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: test.prv}
-		if err := pem.Encode(&buf, pb); err != nil {
-			t.Errorf("%d: pem.Encode: %v", i, err)
-		}
-		pb = &pem.Block{Type: "CERTIFICATE", Bytes: test.pub}
-		if err := pem.Encode(&buf, pb); err != nil {
-			t.Errorf("%d: pem.Encode: %v", i, err)
-		}
-
-		cache.Put(nil, test.key, buf.Bytes())
-		if _, err := man.cacheGet(test.key); err == nil {
-			t.Errorf("%d: err is nil", i)
-		}
-	}
 }
 
 func TestHostWhitelist(t *testing.T) {
@@ -300,3 +300,71 @@ func TestHostWhitelist(t *testing.T) {
 		}
 	}
 }
+
+func TestValidCert(t *testing.T) {
+	key1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	key2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	key3, err := rsa.GenerateKey(rand.Reader, 512)
+	if err != nil {
+		t.Fatal(err)
+	}
+	cert1, err := dummyCert(key1.Public(), "example.org")
+	if err != nil {
+		t.Fatal(err)
+	}
+	cert2, err := dummyCert(key2.Public(), "example.org")
+	if err != nil {
+		t.Fatal(err)
+	}
+	cert3, err := dummyCert(key3.Public(), "example.org")
+	if err != nil {
+		t.Fatal(err)
+	}
+	now := time.Now()
+	early, err := dateDummyCert(key1.Public(), now.Add(time.Hour), now.Add(2*time.Hour), "example.org")
+	if err != nil {
+		t.Fatal(err)
+	}
+	expired, err := dateDummyCert(key1.Public(), now.Add(-2*time.Hour), now.Add(-time.Hour), "example.org")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	tt := []struct {
+		domain string
+		key    crypto.Signer
+		cert   [][]byte
+		ok     bool
+	}{
+		{"example.org", key1, [][]byte{cert1}, true},
+		{"example.org", key3, [][]byte{cert3}, true},
+		{"example.org", key1, [][]byte{cert1, cert2, cert3}, true},
+		{"example.org", key1, [][]byte{cert1, {1}}, false},
+		{"example.org", key1, [][]byte{{1}}, false},
+		{"example.org", key1, [][]byte{cert2}, false},
+		{"example.org", key2, [][]byte{cert1}, false},
+		{"example.org", key1, [][]byte{cert3}, false},
+		{"example.org", key3, [][]byte{cert1}, false},
+		{"example.net", key1, [][]byte{cert1}, false},
+		{"example.org", key1, [][]byte{early}, false},
+		{"example.org", key1, [][]byte{expired}, false},
+	}
+	for i, test := range tt {
+		leaf, err := validCert(test.domain, test.cert, test.key)
+		if err != nil && test.ok {
+			t.Errorf("%d: err = %v", i, err)
+		}
+		if err == nil && !test.ok {
+			t.Errorf("%d: err is nil", i)
+		}
+		if err == nil && test.ok && leaf == nil {
+			t.Errorf("%d: leaf is nil", i)
+		}
+	}
+}

+ 87 - 0
acme/autocert/renewal.go

@@ -0,0 +1,87 @@
+// Copyright 2016 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 (
+	"context"
+	"crypto"
+	"time"
+)
+
+// maxRandRenew is a maximum deviation from Manager.RenewBefore.
+const maxRandRenew = time.Hour
+
+// domainRenewal tracks the state used by the periodic timers
+// renewing a single domain's cert.
+type domainRenewal struct {
+	m      *Manager
+	domain string
+	key    crypto.Signer
+}
+
+// renew is called periodically by a timer.
+// The first renew call is kicked off by dr.m.renew.
+func (dr *domainRenewal) renew() {
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
+	defer cancel()
+	// TODO: rotate dr.key at some point?
+	next, err := dr.do(ctx)
+	if err != nil {
+		next = maxRandRenew / 2
+		next += time.Duration(pseudoRand.int63n(int64(next)))
+	}
+	testDidRenewLoop(next, err)
+	time.AfterFunc(next, dr.renew)
+}
+
+// do is similar to Manager.createCert but it doesn't lock a Manager.state item.
+// Instead, it requests a new certificate independently and, upon success,
+// replaces dr.m.state item with a new one and updates cache for the given domain.
+//
+// It may return immediately if the expiration date of the currently cached cert
+// is far enough in the future.
+//
+// The returned value is a time interval after which the renewal should occur again.
+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 {
+		next := dr.next(tlscert.Leaf.NotAfter)
+		if next > dr.m.renewBefore()+maxRandRenew {
+			return next, nil
+		}
+	}
+
+	der, leaf, err := dr.m.authorizedCert(ctx, dr.key, dr.domain)
+	if err != nil {
+		return 0, err
+	}
+	state := &certState{
+		key:  dr.key,
+		cert: der,
+		leaf: leaf,
+	}
+	tlscert, err := state.tlscert()
+	if err != nil {
+		return 0, err
+	}
+	dr.m.cachePut(dr.domain, tlscert)
+	dr.m.stateMu.Lock()
+	defer dr.m.stateMu.Unlock()
+	// m.state is guaranteed to be non-nil at this point
+	dr.m.state[dr.domain] = state
+	return dr.next(leaf.NotAfter), nil
+}
+
+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))
+	d -= time.Duration(n)
+	if d < 0 {
+		return 0
+	}
+	return d
+}

+ 188 - 0
acme/autocert/renewal_test.go

@@ -0,0 +1,188 @@
+// Copyright 2016 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/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/base64"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"golang.org/x/crypto/acme/internal/acme"
+)
+
+func TestRenewalNext(t *testing.T) {
+	now := time.Now()
+	timeNow = func() time.Time { return now }
+	defer func() { timeNow = time.Now }()
+
+	man := &Manager{RenewBefore: 7 * 24 * time.Hour}
+	tt := []struct {
+		expiry   time.Time
+		min, max time.Duration
+	}{
+		{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - maxRandRenew, 83 * 24 * time.Hour},
+		{now.Add(time.Hour), 0, 1},
+		{now, 0, 1},
+		{now.Add(-time.Hour), 0, 1},
+	}
+
+	dr := &domainRenewal{m: man}
+	for i, test := range tt {
+		next := dr.next(test.expiry)
+		if next < test.min || test.max < next {
+			t.Errorf("%d: next = %v; want between %v and %v", i, next, test.min, test.max)
+		}
+	}
+}
+
+func TestRenewFromCache(t *testing.T) {
+	const domain = "example.org"
+
+	// 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")
+		if r.Method == "HEAD" {
+			// a nonce request
+			return
+		}
+
+		switch r.URL.Path {
+		// discovery
+		case "/":
+			if err := discoTmpl.Execute(w, ca.URL); err != nil {
+				t.Fatalf("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.WriteHeader(http.StatusCreated)
+			w.Write([]byte(`{"status": "valid"}`))
+		// cert request
+		case "/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 {
+				t.Fatalf("new-cert: CSR: %v", err)
+			}
+			der, err := dummyCert(csr.PublicKey, domain)
+			if err != nil {
+				t.Fatalf("new-cert: dummyCert: %v", err)
+			}
+			chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
+			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)
+			}
+			w.Write(der)
+		default:
+			t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
+		}
+	}))
+	defer ca.Close()
+
+	// use EC key to run faster on 386
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	man := &Manager{
+		Prompt:      AcceptTOS,
+		Cache:       make(memCache),
+		RenewBefore: 24 * time.Hour,
+		Client: &acme.Client{
+			Key:          key,
+			DirectoryURL: ca.URL,
+		},
+	}
+
+	// cache an almost expired cert
+	now := time.Now()
+	cert, err := dateDummyCert(key.Public(), now.Add(-2*time.Hour), now.Add(time.Minute), domain)
+	if err != nil {
+		t.Fatal(err)
+	}
+	tlscert := &tls.Certificate{PrivateKey: key, Certificate: [][]byte{cert}}
+	if err := man.cachePut(domain, tlscert); err != nil {
+		t.Fatal(err)
+	}
+
+	// veriy the renewal happened
+	defer func() {
+		testDidRenewLoop = func(next time.Duration, err error) {}
+	}()
+	done := make(chan struct{})
+	testDidRenewLoop = func(next time.Duration, err error) {
+		defer close(done)
+		if err != nil {
+			t.Errorf("testDidRenewLoop: %v", err)
+		}
+		// Next should be about 90 days:
+		// dummyCert creates 90days expiry + account for man.RenewBefore.
+		// Previous expiration was within 1 min.
+		future := 88 * 24 * time.Hour
+		if next < future {
+			t.Errorf("testDidRenewLoop: next = %v; want >= %v", next, future)
+		}
+
+		// ensure the new cert is cached
+		after := time.Now().Add(future)
+		tlscert, err := man.cacheGet(domain)
+		if err != nil {
+			t.Fatalf("man.cacheGet: %v", err)
+		}
+		if !tlscert.Leaf.NotAfter.After(after) {
+			t.Errorf("cache leaf.NotAfter = %v; want > %v", tlscert.Leaf.NotAfter, after)
+		}
+
+		// verify the old cert is also replaced in memory
+		man.stateMu.Lock()
+		defer man.stateMu.Unlock()
+		s := man.state[domain]
+		if s == nil {
+			t.Fatalf("m.state[%q] is nil", domain)
+		}
+		tlscert, err = s.tlscert()
+		if err != nil {
+			t.Fatalf("s.tlscert: %v", err)
+		}
+		if !tlscert.Leaf.NotAfter.After(after) {
+			t.Errorf("state leaf.NotAfter = %v; want > %v", tlscert.Leaf.NotAfter, after)
+		}
+	}
+
+	// trigger renew
+	hello := &tls.ClientHelloInfo{ServerName: domain}
+	if _, err := man.GetCertificate(hello); err != nil {
+		t.Fatal(err)
+	}
+
+	// wait for renew loop
+	select {
+	case <-time.After(10 * time.Second):
+		t.Fatal("renew took too long to occur")
+	case <-done:
+	}
+}