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>
This commit is contained in:
Alex Vaghin
2016-08-31 18:15:28 +00:00
parent 055d4bfb5c
commit 33b41827e6
4 changed files with 619 additions and 174 deletions
+207 -105
View File
@@ -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")
// verify and create TLS cert
leaf, err := validCert(domain, pubDER, privKey)
if err != nil {
return nil, err
}
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")
}
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,48 +330,94 @@ 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()
// TODO: make m.verify retry or retry m.verify calls here
if err := m.verify(ctx, domain); err != nil {
return nil, err
}
client, err := m.acmeClient(ctx)
if err != nil {
return nil, err
}
csr, err := certRequest(state.key, domain)
if err != nil {
return nil, err
}
der, _, err := client.CreateCert(ctx, csr, 0, true)
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, nil, err
}
client, err := m.acmeClient(ctx)
if err != nil {
return nil, nil, err
}
csr, err := certRequest(key, domain)
if err != nil {
return nil, nil, err
}
der, _, err = client.CreateCert(ctx, csr, 0, true)
if err != nil {
return nil, nil, err
}
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.
// It prepares a challenge response and then blocks until the authorization
// is marked as "completed" by the CA (either succeeded or failed).
@@ -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)
}
sort.Strings(cert.DNSNames)
i := sort.SearchStrings(cert.DNSNames, name)
return i < len(cert.DNSNames) && cert.DNSNames[i] == name
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")
}
// 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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
}
}