From 33b41827e673eff8a3b8c2f2089bf0d14d99e1ef Mon Sep 17 00:00:00 2001 From: Alex Vaghin Date: Mon, 22 Aug 2016 21:27:50 +0200 Subject: [PATCH] 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 Reviewed-by: Brad Fitzpatrick --- acme/autocert/autocert.go | 312 ++++++++++++++++++++++----------- acme/autocert/autocert_test.go | 206 ++++++++++++++-------- acme/autocert/renewal.go | 87 +++++++++ acme/autocert/renewal_test.go | 188 ++++++++++++++++++++ 4 files changed, 619 insertions(+), 174 deletions(-) create mode 100644 acme/autocert/renewal.go create mode 100644 acme/autocert/renewal_test.go diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index 2450cb6..d0e3ab6 100644 --- a/acme/autocert/autocert.go +++ b/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") + // 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) {} +) diff --git a/acme/autocert/autocert_test.go b/acme/autocert/autocert_test.go index c1e2dcf..b4d950b 100644 --- a/acme/autocert/autocert_test.go +++ b/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) + } + } +} diff --git a/acme/autocert/renewal.go b/acme/autocert/renewal.go new file mode 100644 index 0000000..3b62b54 --- /dev/null +++ b/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 +} diff --git a/acme/autocert/renewal_test.go b/acme/autocert/renewal_test.go new file mode 100644 index 0000000..c711f0f --- /dev/null +++ b/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: + } +}