ipn/ipnlocal: add optional support for ACME Renewal Info (ARI) (#8599)

This commit is contained in:
Andrew Lytvynov
2023-07-13 14:29:59 -07:00
committed by GitHub
parent 354885a08d
commit 7a82fd8dbe
7 changed files with 88 additions and 36 deletions

View File

@@ -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