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:
+207
-105
@@ -20,8 +20,8 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
mathrand "math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -31,6 +31,14 @@ import (
|
|||||||
"golang.org/x/net/context"
|
"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
|
// AcceptTOS always returns true to indicate the acceptance of a CA Terms of Service
|
||||||
// during account registration.
|
// during account registration.
|
||||||
func AcceptTOS(tosURL string) bool { return true }
|
func AcceptTOS(tosURL string) bool { return true }
|
||||||
@@ -110,6 +118,12 @@ type Manager struct {
|
|||||||
// See GetCertificate for more details.
|
// See GetCertificate for more details.
|
||||||
HostPolicy HostPolicy
|
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
|
// Client is used to perform low-level operations, such as account registration
|
||||||
// and requesting new certificates.
|
// and requesting new certificates.
|
||||||
// If Client is nil, a zero-value acme.Client is used with acme.LetsEncryptURL
|
// 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.
|
// of ClientHello. Keys always have ".acme.invalid" suffix.
|
||||||
tokenCertMu sync.RWMutex
|
tokenCertMu sync.RWMutex
|
||||||
tokenCert map[string]*tls.Certificate
|
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.
|
// GetCertificate implements the tls.Config.GetCertificate hook.
|
||||||
@@ -192,8 +211,7 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
|
|||||||
// with the cached value.
|
// with the cached value.
|
||||||
func (m *Manager) cert(name string) (*tls.Certificate, error) {
|
func (m *Manager) cert(name string) (*tls.Certificate, error) {
|
||||||
m.stateMu.Lock()
|
m.stateMu.Lock()
|
||||||
s, ok := m.state[name]
|
if s, ok := m.state[name]; ok {
|
||||||
if ok {
|
|
||||||
m.stateMu.Unlock()
|
m.stateMu.Unlock()
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
@@ -211,11 +229,13 @@ func (m *Manager) cert(name string) (*tls.Certificate, error) {
|
|||||||
if m.state == nil {
|
if m.state == nil {
|
||||||
m.state = make(map[string]*certState)
|
m.state = make(map[string]*certState)
|
||||||
}
|
}
|
||||||
m.state[name] = &certState{
|
s := &certState{
|
||||||
key: signer,
|
key: signer,
|
||||||
cert: cert.Certificate,
|
cert: cert.Certificate,
|
||||||
leaf: cert.Leaf,
|
leaf: cert.Leaf,
|
||||||
}
|
}
|
||||||
|
m.state[name] = s
|
||||||
|
go m.renew(name, s.key, s.leaf.NotAfter)
|
||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,65 +262,29 @@ func (m *Manager) cacheGet(domain string) (*tls.Certificate, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// public
|
// public
|
||||||
var pubDER []byte
|
var pubDER [][]byte
|
||||||
for len(pub) > 0 {
|
for len(pub) > 0 {
|
||||||
var b *pem.Block
|
var b *pem.Block
|
||||||
b, pub = pem.Decode(pub)
|
b, pub = pem.Decode(pub)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
pubDER = append(pubDER, b.Bytes...)
|
pubDER = append(pubDER, b.Bytes)
|
||||||
}
|
}
|
||||||
if len(pub) > 0 {
|
if len(pub) > 0 {
|
||||||
return nil, errors.New("acme/autocert: invalid public key")
|
return nil, errors.New("acme/autocert: invalid public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse public part(s) and verify the leaf is not expired
|
// verify and create TLS cert
|
||||||
// and corresponds to the private key
|
leaf, err := validCert(domain, pubDER, privKey)
|
||||||
x509Cert, err := x509.ParseCertificates(pubDER)
|
if err != nil {
|
||||||
if len(x509Cert) == 0 {
|
return nil, err
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
tlscert := &tls.Certificate{
|
tlscert := &tls.Certificate{
|
||||||
Certificate: make([][]byte, len(x509Cert)),
|
Certificate: pubDER,
|
||||||
PrivateKey: privKey,
|
PrivateKey: privKey,
|
||||||
Leaf: leaf,
|
Leaf: leaf,
|
||||||
}
|
}
|
||||||
for i, crt := range x509Cert {
|
|
||||||
tlscert.Certificate[i] = crt.Raw
|
|
||||||
}
|
|
||||||
return tlscert, nil
|
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())
|
return m.Cache.Put(ctx, domain, buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// createCert starts domain ownership verification and returns a certificate for that domain
|
// createCert starts the domain ownership verification and returns a certificate
|
||||||
// upon success.
|
// for that domain upon success.
|
||||||
//
|
//
|
||||||
// If the domain is already being verified, it waits for the existing verification to complete.
|
// 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.
|
// Either way, createCert blocks for the duration of the whole process.
|
||||||
func (m *Manager) createCert(ctx context.Context, domain string) (*tls.Certificate, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// state may exist if another goroutine is already working on it
|
// state may exist if another goroutine is already working on it
|
||||||
// in which case just wait for it to finish
|
// in which case just wait for it to finish
|
||||||
if ok {
|
if !state.locked {
|
||||||
state.RLock()
|
state.RLock()
|
||||||
defer state.RUnlock()
|
defer state.RUnlock()
|
||||||
return state.tlscert()
|
return state.tlscert()
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are the first.
|
// We are the first; state is locked.
|
||||||
// Unblock the readers when domain ownership is verified
|
// Unblock the readers when domain ownership is verified
|
||||||
// and the we got the cert or the process failed.
|
// and the we got the cert or the process failed.
|
||||||
defer state.Unlock()
|
defer state.Unlock()
|
||||||
// TODO: make m.verify retry or retry m.verify calls here
|
state.locked = false
|
||||||
if err := m.verify(ctx, domain); err != nil {
|
|
||||||
return nil, err
|
der, leaf, err := m.authorizedCert(ctx, state.key, domain)
|
||||||
}
|
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
state.cert = der
|
state.cert = der
|
||||||
|
state.leaf = leaf
|
||||||
|
go m.renew(domain, state.key, state.leaf.NotAfter)
|
||||||
return state.tlscert()
|
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.
|
// verify starts a new identifier (domain) authorization flow.
|
||||||
// It prepares a challenge response and then blocks until the authorization
|
// It prepares a challenge response and then blocks until the authorization
|
||||||
// is marked as "completed" by the CA (either succeeded or failed).
|
// 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
|
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
|
// putTokenCert stores the cert under the named key in both m.tokenCert map
|
||||||
// and m.Cache.
|
// and m.Cache.
|
||||||
func (m *Manager) putTokenCert(name string, cert *tls.Certificate) {
|
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) {
|
func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) {
|
||||||
m.clientMu.Lock()
|
m.clientMu.Lock()
|
||||||
defer m.clientMu.Unlock()
|
defer m.clientMu.Unlock()
|
||||||
@@ -542,12 +572,20 @@ func (m *Manager) hostPolicy() HostPolicy {
|
|||||||
return defaultHostPolicy
|
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.
|
// certState is ready when its mutex is unlocked for reading.
|
||||||
type certState struct {
|
type certState struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
key crypto.Signer
|
locked bool // locked for read/write
|
||||||
cert [][]byte // DER encoding
|
key crypto.Signer // private key for cert
|
||||||
leaf *x509.Certificate // parsed cert[0]; may be nil
|
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.
|
// 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 {
|
if len(s.cert) == 0 {
|
||||||
return nil, errors.New("acme/autocert: missing certificate")
|
return nil, errors.New("acme/autocert: missing certificate")
|
||||||
}
|
}
|
||||||
// TODO: compare pub.N with key.N or pub.{X,Y} for ECDSA?
|
|
||||||
return &tls.Certificate{
|
return &tls.Certificate{
|
||||||
PrivateKey: s.key,
|
PrivateKey: s.key,
|
||||||
Certificate: s.cert,
|
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.
|
// 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.
|
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
||||||
//
|
//
|
||||||
// Copied from crypto/tls/tls.go.
|
// Inspired by parsePrivateKey in crypto/tls/tls.go.
|
||||||
func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
|
func parsePrivateKey(der []byte) (crypto.Signer, error) {
|
||||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||||
switch key := key.(type) {
|
switch key := key.(type) {
|
||||||
case *rsa.PrivateKey, *ecdsa.PrivateKey:
|
case *rsa.PrivateKey:
|
||||||
|
return key, nil
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
return key, nil
|
return key, nil
|
||||||
default:
|
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 {
|
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")
|
return nil, errors.New("acme/autocert: failed to parse private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// domainMatch matches cert against the specified domain name.
|
// validCert parses a cert chain provided as der argument and verifies the leaf, der[0],
|
||||||
// It doesn't support wildcard.
|
// corresponds to the private key, as well as the domain match and expiration dates.
|
||||||
func domainMatch(cert *x509.Certificate, name string) bool {
|
// It doesn't do any revocation checking.
|
||||||
if cert.Subject.CommonName == name {
|
//
|
||||||
return true
|
// 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)
|
pub := make([]byte, n)
|
||||||
i := sort.SearchStrings(cert.DNSNames, name)
|
n = 0
|
||||||
return i < len(cert.DNSNames) && cert.DNSNames[i] == name
|
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 {
|
func retryAfter(v string) time.Duration {
|
||||||
@@ -617,7 +701,25 @@ func retryAfter(v string) time.Duration {
|
|||||||
return time.Duration(i) * time.Second
|
return time.Duration(i) * time.Second
|
||||||
}
|
}
|
||||||
if t, err := http.ParseTime(v); err == nil {
|
if t, err := http.ParseTime(v); err == nil {
|
||||||
return t.Sub(time.Now())
|
return t.Sub(timeNow())
|
||||||
}
|
}
|
||||||
return time.Second
|
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
@@ -5,16 +5,19 @@
|
|||||||
package autocert
|
package autocert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"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
|
// use EC key to run faster on 386
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,13 +82,28 @@ func dummyCert(san ...string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
t := &x509.Certificate{
|
t := &x509.Certificate{
|
||||||
SerialNumber: big.NewInt(1),
|
SerialNumber: big.NewInt(1),
|
||||||
NotBefore: time.Now(),
|
NotBefore: start,
|
||||||
NotAfter: time.Now().Add(24 * time.Hour),
|
NotAfter: end,
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
KeyUsage: x509.KeyUsageKeyEncipherment,
|
||||||
DNSNames: san,
|
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) {
|
func TestGetCertificate(t *testing.T) {
|
||||||
@@ -84,6 +126,11 @@ func TestGetCertificate(t *testing.T) {
|
|||||||
var ca *httptest.Server
|
var ca *httptest.Server
|
||||||
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
// discovery
|
// discovery
|
||||||
case "/":
|
case "/":
|
||||||
@@ -109,7 +156,16 @@ func TestGetCertificate(t *testing.T) {
|
|||||||
w.Write([]byte(`{"status": "valid"}`))
|
w.Write([]byte(`{"status": "valid"}`))
|
||||||
// cert request
|
// cert request
|
||||||
case "/new-cert":
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("new-cert: dummyCert: %v", err)
|
t.Fatalf("new-cert: dummyCert: %v", err)
|
||||||
}
|
}
|
||||||
@@ -119,7 +175,7 @@ func TestGetCertificate(t *testing.T) {
|
|||||||
w.Write(der)
|
w.Write(der)
|
||||||
// CA chain cert
|
// CA chain cert
|
||||||
case "/ca-cert":
|
case "/ca-cert":
|
||||||
der, err := dummyCert("ca")
|
der, err := dummyCert(nil, "ca")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ca-cert: dummyCert: %v", err)
|
t.Fatalf("ca-cert: dummyCert: %v", err)
|
||||||
}
|
}
|
||||||
@@ -131,9 +187,9 @@ func TestGetCertificate(t *testing.T) {
|
|||||||
defer ca.Close()
|
defer ca.Close()
|
||||||
|
|
||||||
// use EC key to run faster on 386
|
// use EC key to run faster on 386
|
||||||
key, kerr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if kerr != nil {
|
if err != nil {
|
||||||
t.Fatal(kerr)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
man.Client = &acme.Client{
|
man.Client = &acme.Client{
|
||||||
Key: key,
|
Key: key,
|
||||||
@@ -141,11 +197,8 @@ func TestGetCertificate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// simulate tls.Config.GetCertificate
|
// simulate tls.Config.GetCertificate
|
||||||
var (
|
var tlscert *tls.Certificate
|
||||||
tlscert *tls.Certificate
|
done := make(chan struct{})
|
||||||
err error
|
|
||||||
done = make(chan struct{})
|
|
||||||
)
|
|
||||||
go func() {
|
go func() {
|
||||||
hello := &tls.ClientHelloInfo{ServerName: domain}
|
hello := &tls.ClientHelloInfo{ServerName: domain}
|
||||||
tlscert, err = man.GetCertificate(hello)
|
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) {
|
func TestCache(t *testing.T) {
|
||||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -242,39 +275,6 @@ func TestCache(t *testing.T) {
|
|||||||
if res == nil {
|
if res == nil {
|
||||||
t.Fatal("res is 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) {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user