8000 Renew after expiry by maraino · Pull Request #647 · smallstep/cli · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Renew after expiry #647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion command/ca/provisionerbeta/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func addCommand() cli.Command {
sshHostMaxDurFlag,
sshHostDefaultDurFlag,
disableRenewalFlag,
allowRenewalAfterExpiryFlag,
enableX509Flag,
enableSSHFlag,

Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion command/ca/provisionerbeta/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions command/ca/provisionerbeta/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ IID (AWS/GCP/Azure)
sshHostMaxDurFlag,
sshHostDefaultDurFlag,
disableRenewalFlag,
allowRenewalAfterExpiryFlag,
enableX509Flag,
enableSSHFlag,

Expand Down Expand Up @@ -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 {
Expand Down
71 changes: 61 additions & 10 deletions command/ca/renew.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 10 additions & 1 deletion command/ca/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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>]`,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -166,6 +172,7 @@ multiple principals.`,
flags.ProvisionerPasswordFile,
flags.X5cCert,
flags.X5cKey,
flags.X5cInsecure,
flags.SSHPOPCert,
flags.SSHPOPKey,
flags.NebulaCert,
Expand Down Expand Up @@ -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
}
Expand Down
26 changes: 15 additions & 11 deletions command/crypto/jwt/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -207,6 +208,7 @@ the **"kid"** member of one of the JWKs in the JWK Set.`,
},
flags.X5cCert,
flags.X5tCert,
flags.X5cInsecure,
},
}
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -352,8 +355,6 @@ func signAction(ctx *cli.Context) error {
}
}

headers := ctx.StringSlice("header")

// Add claims
c := &jose.Claims{
Issuer: ctx.String("iss"),
Expand Down Expand Up @@ -401,22 +402,25 @@ 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 {
certStrs, err := jose.ValidateX5C(x5cCertFile, jwk.Key)
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 {
Expand Down
7 changes: 7 additions & 0 deletions flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
0