|
|
|
@ -22,6 +22,7 @@ import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"log"
|
|
|
|
|
insecurerand "math/rand"
|
|
|
|
|
"net"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
@ -30,7 +31,7 @@ import (
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/acme"
|
|
|
|
|
"github.com/tailscale/golang-x-crypto/acme"
|
|
|
|
|
"golang.org/x/exp/slices"
|
|
|
|
|
"tailscale.com/atomicfile"
|
|
|
|
|
"tailscale.com/envknob"
|
|
|
|
@ -101,7 +102,7 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
|
|
|
|
shouldRenew, err := shouldStartDomainRenewal(domain, now, pair)
|
|
|
|
|
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logf("error checking for certificate renewal: %v", err)
|
|
|
|
|
} else if shouldRenew {
|
|
|
|
@ -120,7 +121,7 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
|
|
|
|
return pair, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func shouldStartDomainRenewal(domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
|
|
|
|
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
|
|
|
|
renewMu.Lock()
|
|
|
|
|
defer renewMu.Unlock()
|
|
|
|
|
if last, ok := lastRenewCheck[domain]; ok && now.Sub(last) < time.Minute {
|
|
|
|
@ -130,6 +131,18 @@ func shouldStartDomainRenewal(domain string, now time.Time, pair *TLSCertKeyPair
|
|
|
|
|
}
|
|
|
|
|
lastRenewCheck[domain] = now
|
|
|
|
|
|
|
|
|
|
renew, err := b.shouldStartDomainRenewalByARI(cs, now, pair)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Log any ARI failure and fall back to checking for renewal by expiry.
|
|
|
|
|
b.logf("acme: ARI check failed: %v; falling back to expiry-based check", err)
|
|
|
|
|
} else {
|
|
|
|
|
return renew, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.shouldStartDomainRenewalByExpiry(now, pair)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) shouldStartDomainRenewalByExpiry(now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
|
|
|
|
block, _ := pem.Decode(pair.CertPEM)
|
|
|
|
|
if block == nil {
|
|
|
|
|
return false, fmt.Errorf("parsing certificate PEM")
|
|
|
|
@ -157,6 +170,42 @@ func shouldStartDomainRenewal(domain string, now time.Time, pair *TLSCertKeyPair
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) shouldStartDomainRenewalByARI(cs certStore, now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
|
|
|
|
var blocks []*pem.Block
|
|
|
|
|
rest := pair.CertPEM
|
|
|
|
|
for len(rest) > 0 {
|
|
|
|
|
var block *pem.Block
|
|
|
|
|
block, rest = pem.Decode(rest)
|
|
|
|
|
if block == nil {
|
|
|
|
|
return false, fmt.Errorf("parsing certificate PEM")
|
|
|
|
|
}
|
|
|
|
|
blocks = append(blocks, block)
|
|
|
|
|
}
|
|
|
|
|
if len(blocks) < 2 {
|
|
|
|
|
return false, fmt.Errorf("could not parse certificate chain from certStore, got %d PEM block(s)", len(blocks))
|
|
|
|
|
}
|
|
|
|
|
ac, err := acmeClient(cs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
ri, err := ac.FetchRenewalInfo(ctx, blocks[0].Bytes, blocks[1].Bytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, fmt.Errorf("failed to fetch renewal info from ACME server: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if acmeDebug() {
|
|
|
|
|
b.logf("acme: ARI response: %+v", ri)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Select a random time in the suggested window and renew if that time has
|
|
|
|
|
// passed. Time is randomized per recommendation in
|
|
|
|
|
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
|
|
|
|
|
start, end := ri.SuggestedWindow.Start, ri.SuggestedWindow.End
|
|
|
|
|
renewTime := start.Add(time.Duration(insecurerand.Int63n(int64(end.Sub(start)))))
|
|
|
|
|
return now.After(renewTime), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// certStore provides a way to perist and retrieve TLS certificates.
|
|
|
|
|
// As of 2023-02-01, we use store certs in directories on disk everywhere
|
|
|
|
|
// except on Kubernetes, where we use the state store.
|
|
|
|
@ -328,13 +377,9 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key, err := acmeKey(cs)
|
|
|
|
|
ac, err := acmeClient(cs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("acmeKey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
ac := &acme.Client{
|
|
|
|
|
Key: key,
|
|
|
|
|
UserAgent: "tailscaled/" + version.Long(),
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
|
|
|
|
@ -540,6 +585,20 @@ func acmeKey(cs certStore) (crypto.Signer, error) {
|
|
|
|
|
return privKey, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func acmeClient(cs certStore) (*acme.Client, error) {
|
|
|
|
|
key, err := acmeKey(cs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("acmeKey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
// Note: if we add support for additional ACME providers (other than
|
|
|
|
|
// LetsEncrypt), we should make sure that they support ARI extension (see
|
|
|
|
|
// shouldStartDomainRenewalARI).
|
|
|
|
|
return &acme.Client{
|
|
|
|
|
Key: key,
|
|
|
|
|
UserAgent: "tailscaled/" + version.Long(),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validCertPEM reports whether the given certificate is valid for domain at now.
|
|
|
|
|
//
|
|
|
|
|
// If roots != nil, it is used instead of the system root pool. This is meant
|
|
|
|
|