Repository: smallstep/cli
Pull request #647 (2022-03-09 03:27:18)

Renew after expiry
### Description

This PR adds the necessary changes to step to support the renewal of certificates after expiry.

Requires:
- [x] smallstep/certificates#849


Patch:
diff --git a/command/ca/provisionerbeta/add.go b/command/ca/provisionerbeta/add.go
index 0b095966..84883d6a 100644
--- a/command/ca/provisionerbeta/add.go
+++ b/command/ca/provisionerbeta/add.go
@@ -141,6 +141,7 @@ func addCommand() cli.Command {
 			sshHostMaxDurFlag,
 			sshHostDefaultDurFlag,
 			disableRenewalFlag,
+			allowRenewalAfterExpiryFlag,
 			enableX509Flag,
 			enableSSHFlag,
 
@@ -404,7 +405,8 @@ func addAction(ctx *cli.Context) (err error) {
 			},
 			Enabled: !(ctx.IsSet("ssh") && !ctx.Bool("ssh")),
 		},
-		DisableRenewal: ctx.Bool("disable-renewal"),
+		DisableRenewal:          ctx.Bool("disable-renewal"),
+		AllowRenewalAfterExpiry: ctx.Bool("allow-renewal-after-expiry"),
 	}
 
 	switch linkedca.Provisioner_Type(typ) {
diff --git a/command/ca/provisionerbeta/provisioner.go b/command/ca/provisionerbeta/provisioner.go
index f5cc6b6a..6e55eb05 100644
--- a/command/ca/provisionerbeta/provisioner.go
+++ b/command/ca/provisionerbeta/provisioner.go
@@ -57,6 +57,8 @@ with the following properties:
     by default.
   * **disableRenewal**: whether or not to disable certificate renewal, set to false
     by default.
+  * **allowRenewalAfterExpiry**: whether or not to allow certificate renewal of
+    expired certificates, set to false by default.
 
 ## EXAMPLES
 
@@ -158,7 +160,11 @@ var (
 	}
 	disableRenewalFlag = cli.BoolFlag{
 		Name:  "disable-renewal",
-		Usage: `Disable renewal for all certificates generated by this provisioner`,
+		Usage: `Disable renewal for all certificates generated by this provisioner.`,
+	}
+	allowRenewalAfterExpiryFlag = cli.BoolFlag{
+		Name:  "allow-renewal-after-expiry",
+		Usage: `Allow renewals for expired certificates generated by this provisioner.`,
 	}
 	enableX509Flag = cli.BoolFlag{
 		Name:  "x509",
diff --git a/command/ca/provisionerbeta/update.go b/command/ca/provisionerbeta/update.go
index 570b6635..d3832c5d 100644
--- a/command/ca/provisionerbeta/update.go
+++ b/command/ca/provisionerbeta/update.go
@@ -108,6 +108,7 @@ IID (AWS/GCP/Azure)
 			sshHostMaxDurFlag,
 			sshHostDefaultDurFlag,
 			disableRenewalFlag,
+			allowRenewalAfterExpiryFlag,
 			enableX509Flag,
 			enableSSHFlag,
 
@@ -425,6 +426,9 @@ func updateClaims(ctx *cli.Context, p *linkedca.Provisioner) {
 	if ctx.IsSet("disable-renewal") {
 		p.Claims.DisableRenewal = ctx.Bool("disable-renewal")
 	}
+	if ctx.IsSet("allow-renewal-after-expiry") {
+		p.Claims.AllowRenewalAfterExpiry = ctx.Bool("allow-renewal-after-expiry")
+	}
 	claims := p.Claims
 
 	if claims.X509 == nil {
diff --git a/command/ca/renew.go b/command/ca/renew.go
index 9949dd19..931b7642 100644
--- a/command/ca/renew.go
+++ b/command/ca/renew.go
@@ -5,10 +5,12 @@ import (
 	cryptoRand "crypto/rand"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/base64"
 	"encoding/pem"
 	"log"
 	"math/rand"
 	"net/http"
+	"net/url"
 	"os"
 	"os/exec"
 	"os/signal"
@@ -24,6 +26,8 @@ import (
 	"github.com/smallstep/cli/crypto/pemutil"
 	"github.com/smallstep/cli/crypto/x509util"
 	"github.com/smallstep/cli/flags"
+	"github.com/smallstep/cli/jose"
+	"github.com/smallstep/cli/token"
 	"github.com/smallstep/cli/utils"
 	"github.com/smallstep/cli/utils/cautils"
 	"github.com/smallstep/cli/utils/sysutils"
@@ -269,12 +273,8 @@ func renewCertificateAction(ctx *cli.Context) error {
 	if err != nil {
 		return err
 	}
-	leaf := cert.Leaf
 
-	if leaf.NotAfter.Before(time.Now()) {
-		return errors.New("cannot renew an expired certificate")
-	}
-	cvp := leaf.NotAfter.Sub(leaf.NotBefore)
+	cvp := cert.Leaf.NotAfter.Sub(cert.Leaf.NotBefore)
 	if renewPeriod > 0 && renewPeriod >= cvp {
 		return errors.Errorf("flag '--renew-period' must be within (lower than) the certificate "+
 			"validity period; renew-period=%v, cert-validity-period=%v", renewPeriod, cvp)
@@ -293,14 +293,14 @@ func renewCertificateAction(ctx *cli.Context) error {
 	if isDaemon {
 		// Force is always enabled when daemon mode is used
 		ctx.Set("force", "true")
-		next := nextRenewDuration(leaf, expiresIn, renewPeriod)
+		next := nextRenewDuration(cert.Leaf, expiresIn, renewPeriod)
 		return renewer.Daemon(outFile, next, expiresIn, renewPeriod, afterRenew)
 	}
 
 	// Do not renew if (cert.notAfter - now) > (expiresIn + jitter)
 	if expiresIn > 0 {
 		jitter := rand.Int63n(int64(expiresIn / 20))
-		if d := time.Until(leaf.NotAfter); d > expiresIn+time.Duration(jitter) {
+		if d := time.Until(cert.Leaf.NotAfter); d > expiresIn+time.Duration(jitter) {
 			ui.Printf("certificate not renewed: expires in %s\n", d.Round(time.Second))
 			return nil
 		}
@@ -377,6 +377,8 @@ type renewer struct {
 	transport *http.Transport
 	key       crypto.PrivateKey
 	offline   bool
+	cert      tls.Certificate
+	caURL     *url.URL
 }
 
 func newRenewer(ctx *cli.Context, caURL string, cert tls.Certificate, rootFile string) (*renewer, error) {
@@ -392,12 +394,15 @@ func newRenewer(ctx *cli.Context, caURL string, cert tls.Certificate, rootFile s
 	tr := &http.Transport{
 		Proxy: http.ProxyFromEnvironment,
 		TLSClientConfig: &tls.Config{
-			Certificates:             []tls.Certificate{cert},
 			RootCAs:                  rootCAs,
 			PreferServerCipherSuites: true,
 		},
 	}
 
+	if time.Now().Before(cert.Leaf.NotAfter) {
+		tr.TLSClientConfig.Certificates = []tls.Certificate{cert}
+	}
+
 	var client cautils.CaClient
 	offline := ctx.Bool("offline")
 	if offline {
@@ -416,16 +421,27 @@ func newRenewer(ctx *cli.Context, caURL string, cert tls.Certificate, rootFile s
 		}
 	}
 
+	u, err := url.Parse(client.GetCaURL())
+	if err != nil {
+		return nil, errors.Errorf("error parsing CA URL: %s", client.GetCaURL())
+	}
+
 	return &renewer{
 		client:    client,
 		transport: tr,
 		key:       cert.PrivateKey,
 		offline:   offline,
+		cert:      cert,
+		caURL:     u,
 	}, nil
 }
 
-func (r *renewer) Renew(outFile string) (*api.SignResponse, error) {
-	resp, err := r.client.Renew(r.transport)
+func (r *renewer) Renew(outFile string) (resp *api.SignResponse, err error) {
+	if time.Now().After(r.cert.Leaf.NotAfter) {
+		resp, err = r.RenewAfterExpiry(r.cert)
+	} else {
+		resp, err = r.client.Renew(r.transport)
+	}
 	if err != nil {
 		return nil, errors.Wrap(err, "error renewing certificate")
 	}
@@ -515,6 +531,7 @@ func (r *renewer) RenewAndPrepareNext(outFile string, expiresIn, renewPeriod tim
 	}
 
 	// Prepare next transport
+	r.cert = cert
 	r.transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
 
 	// Get next renew duration
@@ -558,6 +575,40 @@ func (r *renewer) Daemon(outFile string, next, expiresIn, renewPeriod time.Durat
 	}
 }
 
+// RenewAfterExpiry creates an authorization token with the given certificate
+// and attempts to renew the expired certificate.
+func (r *renewer) RenewAfterExpiry(cert tls.Certificate) (*api.SignResponse, error) {
+	claims, err := token.NewClaims(
+		token.WithAudience(r.caURL.ResolveReference(&url.URL{Path: "/renew"}).String()),
+		token.WithIssuer("step-ca-client/1.0"),
+		token.WithSubject(cert.Leaf.Subject.CommonName),
+	)
+	if err != nil {
+		return nil, errors.Wrap(err, "error creating authorization token")
+	}
+	var x5c []string
+	for _, b := range cert.Certificate {
+		x5c = append(x5c, base64.StdEncoding.EncodeToString(b))
+	}
+	if claims.ExtraHeaders == nil {
+		claims.ExtraHeaders = make(map[string]interface{})
+	}
+	claims.ExtraHeaders[jose.X5cInsecureKey] = x5c
+
+	tok, err := claims.Sign("", cert.PrivateKey)
+	if err != nil {
+		return nil, errors.Wrap(err, "error signing authorization token")
+	}
+
+	// Remove existing certificate from the transport. And close keep-alive
+	// connections. When daemon is used we don't want to re-use the connection
+	// that did not include a certificate.
+	r.transport.TLSClientConfig.Certificates = nil
+	defer r.transport.CloseIdleConnections()
+
+	return r.client.RenewWithToken(tok)
+}
+
 func tlsLoadX509KeyPair(certFile, keyFile, passFile string) (tls.Certificate, error) {
 	x509Chain, err := pemutil.ReadCertificateBundle(certFile)
 	if err != nil {
diff --git a/command/ca/token.go b/command/ca/token.go
index f854042e..96a089b3 100644
--- a/command/ca/token.go
+++ b/command/ca/token.go
@@ -31,7 +31,7 @@ func tokenCommand() cli.Command {
 [**--not-before**=<time|duration>] [**--not-after**=<time|duration>]
 [**--password-file**=<file>] [**--provisioner-password-file**=<file>]
 [**--output-file**=<file>] [**--key**=<file>] [**--san**=<SAN>] [**--offline**]
-[**--revoke**] [**--x5c-cert**=<file>] [**--x5c-key**=<file>]
+[**--revoke**] [**--x5c-cert**=<file>] [**--x5c-key**=<file>] [**--x5c-insecure**]
 [**--sshpop-cert**=<file>] [**--sshpop-key**=<file>]
 [**--ssh**] [**--host**] [**--principal**=<name>] [**--k8ssa-token-path**=<file>]
 [**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
@@ -137,6 +137,12 @@ $ step ca token max@smallstep.com --ssh
 Get a new token for an SSH host certificate:
 '''
 $ step ca token my-remote.hostname --ssh --host
+'''
+
+Generate a renew token and use it in a renew after expiry request:
+'''
+$ TOKEN=$(step ca token --x5c-cert internal.crt --x5c-key internal.key --renew internal.example.com)
+$ curl -X POST -H "Authorization: Bearer $TOKEN" https://ca.example.com/1.0/renew
 '''`,
 		Flags: []cli.Flag{
 			certNotAfterFlag,
@@ -166,6 +172,7 @@ multiple principals.`,
 			flags.ProvisionerPasswordFile,
 			flags.X5cCert,
 			flags.X5cKey,
+			flags.X5cInsecure,
 			flags.SSHPOPCert,
 			flags.SSHPOPKey,
 			flags.NebulaCert,
@@ -259,6 +266,8 @@ func tokenAction(ctx *cli.Context) error {
 		switch {
 		case isRevoke:
 			typ = cautils.RevokeType
+		case isRenew:
+			typ = cautils.RenewType
 		default:
 			typ = cautils.SignType
 		}
diff --git a/command/crypto/jwt/sign.go b/command/crypto/jwt/sign.go
index 2ab64d0c..2177a0f6 100644
--- a/command/crypto/jwt/sign.go
+++ b/command/crypto/jwt/sign.go
@@ -27,7 +27,8 @@ func signCommand() cli.Command {
 [**--exp**=<expiration>] [**--iat**=<issued_at>] [**--nbf**=<not-before>]
 [**--key**=<file>] [**--jwks**=<jwks>] [**--kid**=<kid>] [**--jti**=<jti>]
 [**--header=<key=value>**] [**--password-file**=<file>]
-[**--x5c-cert**=<file>] [**--x5c-key**=<file>] [**--x5t-cert**=<file>] [**--x5t-key**=<file>]`,
+[**--x5c-cert**=<file>] [**--x5c-key**=<file>] [**--x5c-insecure**]
+[**--x5t-cert**=<file>] [**--x5t-key**=<file>]`,
 		Description: `**step crypto jwt sign** command generates a signed JSON Web Token (JWT) by
 computing a digital signature or message authentication code for a JSON
 payload. By default, the payload to sign is read from STDIN and the JWT will
@@ -207,6 +208,7 @@ the **"kid"** member of one of the JWKs in the JWK Set.`,
 			},
 			flags.X5cCert,
 			flags.X5tCert,
+			flags.X5cInsecure,
 		},
 	}
 }
@@ -234,6 +236,7 @@ func signAction(ctx *cli.Context) error {
 
 	x5cCertFile, x5cKeyFile := ctx.String("x5c-cert"), ctx.String("x5c-key")
 	x5tCertFile, x5tKeyFile := ctx.String("x5t-cert"), ctx.String("x5t-key")
+
 	key := ctx.String("key")
 	jwks := ctx.String("jwks")
 	kid := ctx.String("kid")
@@ -352,8 +355,6 @@ func signAction(ctx *cli.Context) error {
 		}
 	}
 
-	headers := ctx.StringSlice("header")
-
 	// Add claims
 	c := &jose.Claims{
 		Issuer:    ctx.String("iss"),
@@ -401,14 +402,13 @@ func signAction(ctx *cli.Context) error {
 		so.WithHeader("kid", jwk.KeyID)
 	}
 
-	if len(headers) > 0 {
-		for _, s := range headers {
-			i := strings.Index(s, "=")
-			if i == -1 {
-				return errs.InvalidFlagValue(ctx, "set", s, "")
-			}
-			so.WithHeader(jose.HeaderKey(s[:i]), s[i+1:])
+	// Add extra headers. Currently only string headers are supported.
+	for _, s := range ctx.StringSlice("header") {
+		i := strings.Index(s, "=")
+		if i == -1 {
+			return errs.InvalidFlagValue(ctx, "header", s, "")
 		}
+		so.WithHeader(jose.HeaderKey(s[:i]), s[i+1:])
 	}
 
 	if isX5C {
@@ -416,7 +416,11 @@ func signAction(ctx *cli.Context) error {
 		if err != nil {
 			return errors.Wrap(err, "error validating x5c certificate chain and key for use in x5c header")
 		}
-		so.WithHeader("x5c", certStrs)
+		if ctx.Bool("x5c-insecure") {
+			so.WithHeader("x5cInsecure", certStrs)
+		} else {
+			so.WithHeader("x5c", certStrs)
+		}
 	}
 
 	if isX5T {
diff --git a/flags/flags.go b/flags/flags.go
index a8cac99b..cfb4fcf4 100644
--- a/flags/flags.go
+++ b/flags/flags.go
@@ -269,6 +269,13 @@ be stored in the 'x5c' header.`,
 be stored in the 'x5c' header.`,
 	}
 
+	// X5cInsecure is a cli.Flag used to set the JWT header x5cInsecure instead
+	// of x5c when --x5c-cert is used.
+	X5cInsecure = cli.BoolFlag{
+		Name:  "x5c-insecure",
+		Usage: "Use the JWT header 'x5cInsecure' instead of 'x5c'.",
+	}
+
 	// X5tCert is a cli.Flag used to pass the x5t header certificate thumbprint
 	// for a JWS or JWT.
 	X5tCert = cli.StringFlag{
diff --git a/go.mod b/go.mod
index 3f7c3201..9c913a6e 100644
--- a/go.mod
+++ b/go.mod
@@ -17,20 +17,20 @@ require (
 	github.com/shurcooL/sanitized_anchor_name v1.0.0
 	github.com/slackhq/nebula v1.5.2
 	github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262
-	github.com/smallstep/certificates v0.18.2
+	github.com/smallstep/certificates v0.18.3-0.20220413221949-6331041b2b62
 	github.com/smallstep/certinfo v1.6.0
 	github.com/smallstep/truststore v0.11.0
 	github.com/smallstep/zcrypto v0.0.0-20210924233136-66c2600f6e71
 	github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.7.1
 	github.com/urfave/cli v1.22.5
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
 	go.step.sm/cli-utils v0.7.2
-	go.step.sm/crypto v0.15.0
-	go.step.sm/linkedca v0.10.0
+	go.step.sm/crypto v0.16.1
+	go.step.sm/linkedca v0.15.0
 	golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
-	golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d
-	golang.org/x/sys v0.0.0-20220114195835-da31bd327af9
+	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
+	golang.org/x/sys v0.0.0-20220209214540-3681064d5158
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
 	google.golang.org/protobuf v1.27.1
 	gopkg.in/square/go-jose.v2 v2.6.0
diff --git a/go.sum b/go.sum
index 5d7de96d..71ef3081 100644
--- a/go.sum
+++ b/go.sum
@@ -23,21 +23,39 @@ cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPT
 cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
 cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
 cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0 h1:bAMqZidYkmIsUqe6PtkEPT7Q+vfizScn+jfNA6jwK9c=
 cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
+cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
+cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
+cloud.google.com/go/compute v1.3.0 h1:mPL/MzDDYHsh5tHRS9mhmhWlcgClCrCa6ApQCU6wnHI=
+cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/iam v0.1.0 h1:W2vbGCrE3Z7J/x3WXLxxGl9LMSB2uhsAA7Ss/6u/qRY=
+cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=
+cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo=
+cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
 cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/security v1.3.0 h1:BhCl33x+KQI4qiZnFrfr2gAGhb2aZ0ZvKB3Y4QlEfgo=
+cloud.google.com/go/security v1.3.0/go.mod h1:pQsnLAXfMzuWVJdctBs8BV3tGd3Jr0SMYu6KK3QXYAs=
 cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs=
 cloud.google.com/go/spanner v1.18.0/go.mod h1:LvAjUXPeJRGNuGpikMULjhLj/t9cRvdc+fxRoLiugXA=
 cloud.google.com/go/spanner v1.20.0/go.mod h1:ajR/W06cMHQu7nqQ4irRGplPNoWgejGJlEhlB8xBTKk=
@@ -414,8 +432,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
 github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -442,6 +461,8 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
 github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
@@ -458,8 +479,10 @@ github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs167
 github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
 github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
+github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
 github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ=
@@ -860,12 +883,12 @@ github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIp
 github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE=
 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
-github.com/smallstep/certificates v0.18.2 h1:3A5aOGO/Cghht2FhRwAahPHPCwKUJ1ofRRDwlk0z2nE=
-github.com/smallstep/certificates v0.18.2/go.mod h1:GfaGvnmQXSR6JxG7ZkpDRixKyQPpWdw7GFD9zKs04nc=
+github.com/smallstep/certificates v0.18.3-0.20220413221949-6331041b2b62 h1:2raZ4R3y1B6mG3HCyoNiL27tNUdPMV7Ky6YxTmVXw+c=
+github.com/smallstep/certificates v0.18.3-0.20220413221949-6331041b2b62/go.mod h1:nWRPVeHnIPrjODR+zlfc5ryrSjCjCxnyBrFriPbasDI=
 github.com/smallstep/certinfo v1.6.0 h1:o1eS9+iE6OPLRdnRFiYqAtXYR2FioNUt8q4CIj7X3Nk=
 github.com/smallstep/certinfo v1.6.0/go.mod h1:DsKAlSDLWsywdiVBCfqqVdRuny77wqiI+NFskLM7Ods=
-github.com/smallstep/nosql v0.3.10 h1:Xs7nueSl250GYb5XdfbzR8w+xPbvF6/oSw6pryY7gJI=
-github.com/smallstep/nosql v0.3.10/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo=
+github.com/smallstep/nosql v0.4.0 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M=
+github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo=
 github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk=
 github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM=
 github.com/smallstep/zcrypto v0.0.0-20210924233136-66c2600f6e71 h1:q0IDrQpquiWcU1nJmjwEremPwG+pT2AAGsDatPgg3Kw=
@@ -918,8 +941,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
 github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
@@ -1045,10 +1069,10 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/
 go.step.sm/cli-utils v0.7.2 h1:kUNNhGRWAad3bLkhvbLjVr3Dqs5DgxCZQcUspWaQCIQ=
 go.step.sm/cli-utils v0.7.2/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E=
 go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
-go.step.sm/crypto v0.15.0 h1:VioBln+x3+RoejgeBhvxkLGVYdWRy6PFiAaUUN29/E0=
-go.step.sm/crypto v0.15.0/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
-go.step.sm/linkedca v0.10.0 h1:+bqymMRulHYkVde4l16FnqFVskoS6HCWJN5Z5cxAqF8=
-go.step.sm/linkedca v0.10.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
+go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk=
+go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
+go.step.sm/linkedca v0.15.0 h1:lEkGRDY+u7FudGKt8yEo7nBy5OzceO9s3rl+/sZVL5M=
+go.step.sm/linkedca v0.15.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -1195,8 +1219,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs=
-golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1213,8 +1237,12 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1307,13 +1335,22 @@ golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
-golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
@@ -1407,6 +1444,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1447,8 +1486,18 @@ google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk
 google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA=
 google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I=
 google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0 h1:RDAPWfNFY06dffEXfn7hZF5Fr1ZbnChzfQZAPyBd1+I=
 google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
+google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
+google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
+google.golang.org/api v0.70.0 h1:67zQnAE0T2rB0A3CwLSas0K+SbVzSxP+zTLkQLexeiw=
+google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1515,8 +1564,28 @@ google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQ
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q=
-google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
+google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -1546,9 +1615,12 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=
-google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg=
+google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
diff --git a/utils/cautils/client.go b/utils/cautils/client.go
index 072c124e..41699a19 100644
--- a/utils/cautils/client.go
+++ b/utils/cautils/client.go
@@ -26,6 +26,7 @@ import (
 type CaClient interface {
 	Sign(req *api.SignRequest) (*api.SignResponse, error)
 	Renew(tr http.RoundTripper) (*api.SignResponse, error)
+	RenewWithToken(ott string) (*api.SignResponse, error)
 	Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error)
 	Rekey(req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error)
 	SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error)
@@ -40,6 +41,7 @@ type CaClient interface {
 	SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse, error)
 	Version() (*api.VersionResponse, error)
 	GetRootCAs() *x509.CertPool
+	GetCaURL() string
 }
 
 // NewClient returns a client of an online or offline CA. Requires the flags
diff --git a/utils/cautils/offline.go b/utils/cautils/offline.go
index 82c50b95..18ce403d 100644
--- a/utils/cautils/offline.go
+++ b/utils/cautils/offline.go
@@ -78,6 +78,11 @@ func NewOfflineCA(ctx *cli.Context, configFile string) (*OfflineCA, error) {
 	return offlineInstance, nil
 }
 
+// GetCaURL returns the configured CA url.
+func (c *OfflineCA) GetCaURL() string {
+	return "https://" + c.config.DNSNames[0]
+}
+
 // GetRootCAs return the cert pool for the ca, as it's an offline ca, a pool is
 // not required and it always returns nil.
 func (c *OfflineCA) GetRootCAs() *x509.CertPool {
@@ -143,6 +148,8 @@ func (c *OfflineCA) Audience(tokType int) string {
 		return fmt.Sprintf("https://%s/ssh/revoke", toHostname(c.config.DNSNames[0]))
 	case SSHRekeyType:
 		return fmt.Sprintf("https://%s/ssh/rekey", toHostname(c.config.DNSNames[0]))
+	case RenewType:
+		return fmt.Sprintf("https://%s/renew", toHostname(c.config.DNSNames[0]))
 	default:
 		return fmt.Sprintf("https://%s/sign", toHostname(c.config.DNSNames[0]))
 	}
@@ -240,6 +247,31 @@ func (c *OfflineCA) Renew(rt http.RoundTripper) (*api.SignResponse, error) {
 	}, nil
 }
 
+// RenewWithToken is a wrapper on top of certificates AuthorizeRenew and Renew
+// method. It returns an api.SignResponse with the requested certificate and the
+// intermediate.
+func (c *OfflineCA) RenewWithToken(ott string) (*api.SignResponse, error) {
+	cert, err := c.authority.AuthorizeRenewToken(context.Background(), ott)
+	if err != nil {
+		return nil, err
+	}
+	certChain, err := c.authority.Renew(cert)
+	if err != nil {
+		return nil, err
+	}
+	certChainPEM := certChainToPEM(certChain)
+	var caPEM api.Certificate
+	if len(certChainPEM) > 1 {
+		caPEM = certChainPEM[1]
+	}
+	return &api.SignResponse{
+		ServerPEM:    certChainPEM[0],
+		CaPEM:        caPEM,
+		CertChainPEM: certChainPEM,
+		TLSOptions:   c.authority.GetTLSOptions(),
+	}, nil
+}
+
 // Revoke is a wrapper on top of certificates Revoke method. It returns an
 // api.RevokeResponse.
 func (c *OfflineCA) Revoke(req *api.RevokeRequest, rt http.RoundTripper) (*api.RevokeResponse, error) {
@@ -507,6 +539,11 @@ func (c *OfflineCA) GenerateToken(ctx *cli.Context, tokType int, subject string,
 	root := c.Root()
 	audience := c.Audience(tokType)
 
+	// All provisioners use the same type of tokens to do a X.509 renewal.
+	if tokType == RenewType {
+		return generateRenewToken(ctx, audience, subject)
+	}
+
 	// Get provisioner to use
 	provisioners := c.Provisioners()
 	p, err := provisionerPrompt(ctx, provisioners)
diff --git a/utils/cautils/token_flow.go b/utils/cautils/token_flow.go
index 75125a80..2e8fb6cf 100644
--- a/utils/cautils/token_flow.go
+++ b/utils/cautils/token_flow.go
@@ -30,6 +30,7 @@ const (
 	SSHRevokeType
 	SSHRenewType
 	SSHRekeyType
+	RenewType
 )
 
 // parseAudience creates the ca audience url from the ca-url
@@ -47,10 +48,10 @@ func parseAudience(ctx *cli.Context, tokType int) (string, error) {
 	case "https", "":
 		var path string
 		switch tokType {
-		// default
 		case SignType:
 			path = "/1.0/sign"
-		// revocation token
+		case RenewType:
+			path = "/1.0/renew"
 		case RevokeType:
 			path = "/1.0/revoke"
 		case SSHUserSignType, SSHHostSignType:
@@ -91,6 +92,11 @@ func NewTokenFlow(ctx *cli.Context, tokType int, subject string, sans []string,
 		return "", err
 	}
 
+	// All provisioners use the same type of tokens to do a X.509 renewal.
+	if tokType == RenewType {
+		return generateRenewToken(ctx, audience, subject)
+	}
+
 	provisioners, err := pki.GetProvisioners(caURL, root)
 	if err != nil {
 		return "", err
diff --git a/utils/cautils/token_generator.go b/utils/cautils/token_generator.go
index 76c3945c..91ea373b 100644
--- a/utils/cautils/token_generator.go
+++ b/utils/cautils/token_generator.go
@@ -13,6 +13,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/smallstep/certificates/authority/provisioner"
 	"github.com/smallstep/certificates/pki"
+	"github.com/smallstep/cli/crypto/pemutil"
 	"github.com/smallstep/cli/crypto/randutil"
 	"github.com/smallstep/cli/exec"
 	"github.com/smallstep/cli/token"
@@ -176,22 +177,31 @@ func generateX5CToken(ctx *cli.Context, p *provisioner.X5C, tokType int, tokAttr
 	if err != nil {
 		return "", err
 	}
+
 	tokenGen := NewTokenGenerator(jwk.KeyID, p.Name,
 		fmt.Sprintf("%s#%s", tokAttrs.audience, p.GetIDForToken()), tokAttrs.root,
 		tokAttrs.notBefore, tokAttrs.notAfter, jwk)
+
+	var tokenOpts []token.Options
+	if ctx.Bool("x5c-insecure") {
+		tokenOpts = append(tokenOpts, token.WithX5CInsecureFile(x5cCertFile, jwk.Key))
+	} else {
+		tokenOpts = append(tokenOpts, token.WithX5CFile(x5cCertFile, jwk.Key))
+	}
+
 	switch tokType {
 	case SignType:
-		return tokenGen.SignToken(tokAttrs.subject, tokAttrs.sans, token.WithX5CFile(x5cCertFile, jwk.Key))
+		return tokenGen.SignToken(tokAttrs.subject, tokAttrs.sans, tokenOpts...)
 	case RevokeType:
-		return tokenGen.RevokeToken(tokAttrs.subject, token.WithX5CFile(x5cCertFile, jwk.Key))
+		return tokenGen.RevokeToken(tokAttrs.subject, tokenOpts...)
 	case SSHUserSignType:
 		return tokenGen.SignSSHToken(tokAttrs.subject, provisioner.SSHUserCert, tokAttrs.sans,
-			tokAttrs.certNotBefore, tokAttrs.certNotAfter, token.WithX5CFile(x5cCertFile, jwk.Key))
+			tokAttrs.certNotBefore, tokAttrs.certNotAfter, tokenOpts...)
 	case SSHHostSignType:
 		return tokenGen.SignSSHToken(tokAttrs.subject, provisioner.SSHHostCert, tokAttrs.sans,
-			tokAttrs.certNotBefore, tokAttrs.certNotAfter, token.WithX5CFile(x5cCertFile, jwk.Key))
+			tokAttrs.certNotBefore, tokAttrs.certNotAfter, tokenOpts...)
 	default:
-		return tokenGen.Token(tokAttrs.subject, token.WithX5CFile(x5cCertFile, jwk.Key))
+		return tokenGen.Token(tokAttrs.subject, tokenOpts...)
 	}
 }
 
@@ -387,3 +397,51 @@ func generateJWKToken(ctx *cli.Context, p *provisioner.JWK, tokType int, tokAttr
 		return tokenGen.Token(tokAttrs.subject)
 	}
 }
+
+func generateRenewToken(ctx *cli.Context, aud, sub string) (string, error) {
+	renewCert := ctx.String("x5c-cert")
+	if renewCert == "" {
+		return "", errs.RequiredFlag(ctx, "x5c-cert")
+	}
+	renewKey := ctx.String("x5c-key")
+	if renewKey == "" {
+		return "", errs.RequiredFlag(ctx, "x5c-key")
+	}
+
+	bundle, err := pemutil.ReadCertificateBundle(renewCert)
+	if err != nil {
+		return "", err
+	}
+	if len(bundle) == 0 {
+		return "", errs.InvalidFlagValueMsg(ctx, "--x5c-cert", renewCert, "certificate not found")
+	}
+	key, err := pemutil.Read(renewKey)
+	if err != nil {
+		return "", err
+	}
+	if sub != "" && sub != bundle[0].Subject.CommonName {
+		return "", errors.Errorf("positional argument <subject> must match the certificate common name")
+	}
+	claims, err := token.NewClaims(
+		token.WithAudience(aud),
+		token.WithIssuer("step-ca-client/1.0"),
+		token.WithSubject(bundle[0].Subject.CommonName),
+	)
+	if err != nil {
+		return "", errors.Wrap(err, "error creating renew token")
+	}
+	var x5c []string
+	for _, crt := range bundle {
+		x5c = append(x5c, base64.StdEncoding.EncodeToString(crt.Raw))
+	}
+	if claims.ExtraHeaders == nil {
+		claims.ExtraHeaders = make(map[string]interface{})
+	}
+	claims.ExtraHeaders[jose.X5cInsecureKey] = x5c
+
+	tok, err := claims.Sign("", key)
+	if err != nil {
+		return "", errors.Wrap(err, "error creating renew token")
+	}
+	return tok, nil
+}


Test patch:
diff --git a/utils/cautils/offline_test.go b/utils/cautils/offline_test.go
index 3883f0a5..27814096 100644
--- a/utils/cautils/offline_test.go
+++ b/utils/cautils/offline_test.go
@@ -84,6 +84,14 @@ func TestOfflineCA_Audience(t *testing.T) {
 			tokType: SSHRekeyType,
 			want:    "https://ca.smallstep.com/ssh/rekey",
 		},
+		{
+			name: "ok/dns-renew",
+			config: config.Config{
+				DNSNames: []string{"ca.smallstep.com"},
+			},
+			tokType: RenewType,
+			want:    "https://ca.smallstep.com/renew",
+		},
 		{
 			name: "ok/ipv4-sign",
 			config: config.Config{
@@ -120,3 +128,26 @@ func TestOfflineCA_Audience(t *testing.T) {
 		})
 	}
 }
+
+func TestOfflineCA_GetCaURL(t *testing.T) {
+	type fields struct {
+		config config.Config
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   string
+	}{
+		{"ok", fields{config.Config{DNSNames: []string{"ca.com"}}}, "https://ca.com"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &OfflineCA{
+				config: tt.fields.config,
+			}
+			if got := c.GetCaURL(); got != tt.want {
+				t.Errorf("OfflineCA.GetCaURL() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}