44 "fmt"
55 "os"
66
7+ "github.com/pkg/errors"
78 "github.com/smallstep/certificates/api"
89 "github.com/smallstep/certificates/pki"
910 "github.com/smallstep/cli/flags"
@@ -12,6 +13,8 @@ import (
1213 "github.com/urfave/cli"
1314 "go.step.sm/cli-utils/command"
1415 "go.step.sm/cli-utils/errs"
16+ "go.step.sm/crypto/pemutil"
17+ "golang.org/x/crypto/ssh"
1518)
1619
1720func tokenCommand () cli.Command {
@@ -27,6 +30,7 @@ func tokenCommand() cli.Command {
2730[**--output-file**=<file>] [**--kms**=uri] [**--key**=<file>] [**--san**=<SAN>] [**--offline**]
2831[**--revoke**] [**--x5c-cert**=<file>] [**--x5c-key**=<file>] [**--x5c-insecure**]
2932[**--sshpop-cert**=<file>] [**--sshpop-key**=<file>]
33+ [**--cnf-file**=<file>] [**--cnf-kid**=<fingerprint>]
3034[**--ssh**] [**--host**] [**--principal**=<name>] [**--k8ssa-token-path**=<file>]
3135[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]` ,
3236 Description : `**step ca token** command generates a one-time token granting access to the
@@ -82,6 +86,11 @@ Get a new token that becomes valid in 30 minutes and expires 5 minutes after tha
8286$ step ca token --not-before 30m --not-after 35m internal.example.com
8387'''
8488
89+ Get a new token with a confirmation claim to enforce the use of a given CSR:
90+ '''
91+ step ca token --cnf-file internal.csr internal.smallstep.com
92+ '''
93+
8594Get a new token signed with the given private key, the public key must be
8695configured in the certificate authority:
8796'''
@@ -133,6 +142,11 @@ Get a new token for an SSH host certificate:
133142$ step ca token my-remote.hostname --ssh --host
134143'''
135144
145+ Get a new token with a confirmation claim to enforce the use of a given public key:
146+ '''
147+ step ca token --ssh --host --cnf-file internal.pub internal.smallstep.com
148+ '''
149+
136150Generate a renew token and use it in a renew after expiry request:
137151'''
138152$ TOKEN=$(step ca token --x5c-cert internal.crt --x5c-key internal.key --renew internal.example.com)
@@ -186,6 +200,8 @@ multiple principals.`,
186200 flags .SSHPOPKey ,
187201 flags .NebulaCert ,
188202 flags .NebulaKey ,
203+ flags .ConfirmationFile ,
204+ flags .ConfirmationKid ,
189205 cli.StringFlag {
190206 Name : "key" ,
191207 Usage : `The private key <file> used to sign the JWT. This is usually downloaded from
@@ -240,6 +256,9 @@ func tokenAction(ctx *cli.Context) error {
240256 isSSH := ctx .Bool ("ssh" )
241257 isHost := ctx .Bool ("host" )
242258 principals := ctx .StringSlice ("principal" )
259+ // confirmation claims
260+ cnfFile := ctx .String ("cnf-file" )
261+ cnfKid := ctx .String ("cnf-kid" )
243262
244263 switch {
245264 case isSSH && len (sans ) > 0 :
@@ -252,6 +271,8 @@ func tokenAction(ctx *cli.Context) error {
252271 return errs .RequiredWithFlag (ctx , "host" , "ssh" )
253272 case ! isSSH && len (principals ) > 0 :
254273 return errs .RequiredWithFlag (ctx , "principal" , "ssh" )
274+ case cnfFile != "" && cnfKid != "" :
275+ return errs .IncompatibleFlagWithFlag (ctx , "cnf-file" , "cnf-kid" )
255276 }
256277
257278 // Default token type is always a 'Sign' token.
@@ -295,6 +316,31 @@ func tokenAction(ctx *cli.Context) error {
295316 }
296317 }
297318
319+ // Add options to create a confirmation claim if a CSR or SSH public key is
320+ // passed.
321+ var tokenOpts []cautils.Option
322+ if cnfFile != "" {
323+ in , err := utils .ReadFile (cnfFile )
324+ if err != nil {
325+ return err
326+ }
327+ if isSSH {
328+ sshPub , _ , _ , _ , err := ssh .ParseAuthorizedKey (in )
329+ if err != nil {
330+ return errors .Wrap (err , "error parsing ssh public key" )
331+ }
332+ tokenOpts = append (tokenOpts , cautils .WithSSHPublicKey (sshPub ))
333+ } else {
334+ csr , err := pemutil .ParseCertificateRequest (in )
335+ if err != nil {
336+ return errors .Wrap (err , "error parsing certificate request" )
337+ }
338+ tokenOpts = append (tokenOpts , cautils .WithCertificateRequest (csr ))
339+ }
340+ } else if cnfKid != "" {
341+ tokenOpts = append (tokenOpts , cautils .WithConfirmationKid (cnfKid ))
342+ }
343+
298344 // --san and --type revoke are incompatible. Revocation tokens do not support SANs.
299345 if typ == cautils .RevokeType && len (sans ) > 0 {
300346 return errs .IncompatibleFlagWithFlag (ctx , "san" , "revoke" )
@@ -327,7 +373,7 @@ func tokenAction(ctx *cli.Context) error {
327373 return err
328374 }
329375 } else {
330- token , err = cautils .NewTokenFlow (ctx , typ , subject , sans , caURL , root , notBefore , notAfter , certNotBefore , certNotAfter )
376+ token , err = cautils .NewTokenFlow (ctx , typ , subject , sans , caURL , root , notBefore , notAfter , certNotBefore , certNotAfter , tokenOpts ... )
331377 if err != nil {
332378 return err
333379 }
0 commit comments