From 7a82fd8dbe9e56bf04125c9f8084dd8e21290020 Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Thu, 13 Jul 2023 14:29:59 -0700 Subject: [PATCH] ipn/ipnlocal: add optional support for ACME Renewal Info (ARI) (#8599) --- cmd/derper/depaware.txt | 4 -- cmd/tailscale/depaware.txt | 4 +- cmd/tailscaled/depaware.txt | 7 +--- go.mod | 10 ++--- go.sum | 19 ++++----- ipn/ipnlocal/cert.go | 77 ++++++++++++++++++++++++++++++++----- ipn/ipnlocal/cert_test.go | 3 +- 7 files changed, 88 insertions(+), 36 deletions(-) diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 9bc358cc3..cff294079 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -208,7 +208,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa bytes from bufio+ compress/flate from compress/gzip+ compress/gzip from internal/profile+ - L compress/zlib from debug/elf container/list from crypto/tls+ context from crypto/tls+ crypto from crypto/ecdsa+ @@ -232,8 +231,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa crypto/tls from golang.org/x/crypto/acme+ crypto/x509 from crypto/tls+ crypto/x509/pkix from crypto/x509+ - L debug/dwarf from debug/elf - L debug/elf from golang.org/x/sys/unix embed from crypto/internal/nistec+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ @@ -249,7 +246,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa fmt from compress/flate+ go/token from google.golang.org/protobuf/internal/strs hash from crypto+ - L hash/adler32 from compress/zlib hash/crc32 from compress/gzip+ hash/fnv from google.golang.org/protobuf/internal/detrand hash/maphash from go4.org/mem diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index ae6bf4cb6..0c6255e6d 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -207,7 +207,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep bytes from bufio+ compress/flate from compress/gzip+ compress/gzip from net/http - compress/zlib from image/png+ + compress/zlib from image/png container/list from crypto/tls+ context from crypto/tls+ crypto from crypto/ecdsa+ @@ -232,8 +232,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep crypto/x509 from crypto/tls+ crypto/x509/pkix from crypto/x509+ database/sql/driver from github.com/google/uuid - L debug/dwarf from debug/elf - L debug/elf from golang.org/x/sys/unix embed from tailscale.com/cmd/tailscale/cli+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 90332549b..28d81eb73 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -129,6 +129,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de LD github.com/pkg/sftp from tailscale.com/ssh/tailssh LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient + github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20 LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+ @@ -363,7 +364,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine tailscale.com/wgengine/wglog from tailscale.com/wgengine W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router - golang.org/x/crypto/acme from tailscale.com/ipn/ipnlocal golang.org/x/crypto/argon2 from tailscale.com/tka golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+ golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+ @@ -414,7 +414,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de bytes from bufio+ compress/flate from compress/gzip+ compress/gzip from golang.org/x/net/http2+ - L compress/zlib from debug/elf container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp container/list from crypto/tls+ context from crypto/tls+ @@ -439,8 +438,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de crypto/tls from github.com/tcnksm/go-httpstat+ crypto/x509 from crypto/tls+ crypto/x509/pkix from crypto/x509+ - L debug/dwarf from debug/elf - L debug/elf from golang.org/x/sys/unix embed from tailscale.com+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ @@ -456,7 +453,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de flag from net/http/httptest+ fmt from compress/flate+ hash from crypto+ - hash/adler32 from tailscale.com/ipn/ipnlocal+ + hash/adler32 from tailscale.com/ipn/ipnlocal hash/crc32 from compress/gzip+ hash/fnv from tailscale.com/wgengine/magicsock+ hash/maphash from go4.org/mem diff --git a/go.mod b/go.mod index 4ef166db9..02acc7a71 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 - github.com/tailscale/golang-x-crypto v0.0.0-20221115211329-17a3db2c30d2 + github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 @@ -73,14 +73,14 @@ require ( go.uber.org/zap v1.24.0 go4.org/mem v0.0.0-20220726221520-4f986261bf13 go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 - golang.org/x/crypto v0.8.0 + golang.org/x/crypto v0.11.0 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 golang.org/x/mod v0.10.0 golang.org/x/net v0.10.0 golang.org/x/oauth2 v0.7.0 golang.org/x/sync v0.2.0 - golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a - golang.org/x/term v0.8.0 + golang.org/x/sys v0.10.0 + golang.org/x/term v0.10.0 golang.org/x/time v0.3.0 golang.org/x/tools v0.9.1 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 @@ -333,7 +333,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp/typeparams v0.0.0-20230425010034-47ecfdc1ba53 // indirect golang.org/x/image v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/text v0.11.0 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index 0b3f0e7b7..bd1a1d2f6 100644 --- a/go.sum +++ b/go.sum @@ -1054,8 +1054,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns= github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ= -github.com/tailscale/golang-x-crypto v0.0.0-20221115211329-17a3db2c30d2 h1:pBpqbsyX9H8c26oPYC2H+232HOdp1gDnCztoKmKWKDA= -github.com/tailscale/golang-x-crypto v0.0.0-20221115211329-17a3db2c30d2/go.mod h1:V2G8jyemEGZWKQ+3xNn4+bOx+FuoXU9Zc5GUsZMthBg= +github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ= +github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e/go.mod h1:DjoeCULdP6vTJ/xY+nzzR9LaUHprkbZEpNidX0aqEEk= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= @@ -1210,8 +1210,8 @@ golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1432,8 +1432,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a h1:qMsju+PNttu/NMbq8bQ9waDdxgJMu9QNoUDuhnBaYt0= -golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1444,8 +1444,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1460,8 +1460,9 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/ipn/ipnlocal/cert.go b/ipn/ipnlocal/cert.go index 33bbac8df..f5384276c 100644 --- a/ipn/ipnlocal/cert.go +++ b/ipn/ipnlocal/cert.go @@ -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 diff --git a/ipn/ipnlocal/cert_test.go b/ipn/ipnlocal/cert_test.go index d29a68776..52ba13453 100644 --- a/ipn/ipnlocal/cert_test.go +++ b/ipn/ipnlocal/cert_test.go @@ -173,11 +173,12 @@ func TestShouldStartDomainRenewal(t *testing.T) { want: false, }, } + b := new(LocalBackend) for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { reset() - ret, err := shouldStartDomainRenewal("example.com", now, mustMakePair(&x509.Certificate{ + ret, err := b.shouldStartDomainRenewalByExpiry(now, mustMakePair(&x509.Certificate{ SerialNumber: big.NewInt(2019), Subject: subject, NotBefore: tt.notBefore,