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**=<fingerprint>] [**--cnf-file**=<file>]
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,18 @@ 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 a given CSR fingerprint:
90+ '''
91+ $ step certificate fingerprint --format base64-url-raw internal.csr
92+ PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw
93+ $ step ca token --cnf PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw internal.smallstep.com
94+ '''
95+
96+ Get a new token with a confirmation claim to enforce the use of a given CSR:
97+ '''
98+ step ca token --cnf-file internal.csr internal.smallstep.com
99+ '''
100+
85101Get a new token signed with the given private key, the public key must be
86102configured in the certificate authority:
87103'''
@@ -133,6 +149,11 @@ Get a new token for an SSH host certificate:
133149$ step ca token my-remote.hostname --ssh --host
134150'''
135151
152+ Get a new token with a confirmation claim to enforce the use of a given public key:
153+ '''
154+ step ca token --ssh --host --cnf-file internal.pub internal.smallstep.com
155+ '''
156+
136157Generate a renew token and use it in a renew after expiry request:
137158'''
138159$ TOKEN=$(step ca token --x5c-cert internal.crt --x5c-key internal.key --renew internal.example.com)
@@ -186,6 +207,8 @@ multiple principals.`,
186207 flags .SSHPOPKey ,
187208 flags .NebulaCert ,
188209 flags .NebulaKey ,
210+ flags .Confirmation ,
211+ flags .ConfirmationFile ,
189212 cli.StringFlag {
190213 Name : "key" ,
191214 Usage : `The private key <file> used to sign the JWT. This is usually downloaded from
@@ -240,6 +263,9 @@ func tokenAction(ctx *cli.Context) error {
240263 isSSH := ctx .Bool ("ssh" )
241264 isHost := ctx .Bool ("host" )
242265 principals := ctx .StringSlice ("principal" )
266+ // confirmation claims
267+ cnfFile := ctx .String ("cnf-file" )
268+ cnf := ctx .String ("cnf" )
243269
244270 switch {
245271 case isSSH && len (sans ) > 0 :
@@ -252,6 +278,8 @@ func tokenAction(ctx *cli.Context) error {
252278 return errs .RequiredWithFlag (ctx , "host" , "ssh" )
253279 case ! isSSH && len (principals ) > 0 :
254280 return errs .RequiredWithFlag (ctx , "principal" , "ssh" )
281+ case cnfFile != "" && cnf != "" :
282+ return errs .IncompatibleFlagWithFlag (ctx , "cnf-file" , "cnf" )
255283 }
256284
257285 // Default token type is always a 'Sign' token.
@@ -295,6 +323,31 @@ func tokenAction(ctx *cli.Context) error {
295323 }
296324 }
297325
326+ // Add options to create a confirmation claim if a CSR or SSH public key is
327+ // passed.
328+ var tokenOpts []cautils.Option
329+ if cnfFile != "" {
330+ in , err := utils .ReadFile (cnfFile )
331+ if err != nil {
332+ return err
333+ }
334+ if isSSH {
335+ sshPub , _ , _ , _ , err := ssh .ParseAuthorizedKey (in )
336+ if err != nil {
337+ return errors .Wrap (err , "error parsing ssh public key" )
338+ }
339+ tokenOpts = append (tokenOpts , cautils .WithSSHPublicKey (sshPub ))
340+ } else {
341+ csr , err := pemutil .ParseCertificateRequest (in )
342+ if err != nil {
343+ return errors .Wrap (err , "error parsing certificate request" )
344+ }
345+ tokenOpts = append (tokenOpts , cautils .WithCertificateRequest (csr ))
346+ }
347+ } else if cnf != "" {
348+ tokenOpts = append (tokenOpts , cautils .WithConfirmationFingerprint (cnf ))
349+ }
350+
298351 // --san and --type revoke are incompatible. Revocation tokens do not support SANs.
299352 if typ == cautils .RevokeType && len (sans ) > 0 {
300353 return errs .IncompatibleFlagWithFlag (ctx , "san" , "revoke" )
@@ -327,7 +380,7 @@ func tokenAction(ctx *cli.Context) error {
327380 return err
328381 }
329382 } else {
330- token , err = cautils .NewTokenFlow (ctx , typ , subject , sans , caURL , root , notBefore , notAfter , certNotBefore , certNotAfter )
383+ token , err = cautils .NewTokenFlow (ctx , typ , subject , sans , caURL , root , notBefore , notAfter , certNotBefore , certNotAfter , tokenOpts ... )
331384 if err != nil {
332385 return err
333386 }
0 commit comments