66 "crypto/x509"
77 "crypto/x509/pkix"
88 "encoding/pem"
9+ "fmt"
910 "net"
1011 "os"
1112 "strings"
@@ -21,7 +22,7 @@ import (
2122 "github.com/smallstep/cli/crypto/x509util"
2223 "github.com/smallstep/cli/errs"
2324 "github.com/smallstep/cli/flags"
24- "github.com/smallstep/cli/jose "
25+ "github.com/smallstep/cli/token "
2526 "github.com/smallstep/cli/ui"
2627 "github.com/smallstep/cli/utils"
2728 "github.com/urfave/cli"
@@ -33,7 +34,7 @@ func certificateCommand() cli.Command {
3334 Action : command .ActionFunc (certificateAction ),
3435 Usage : "generate a new private key and certificate signed by the root certificate" ,
3536 UsageText : `**step ca certificate** <subject> <crt-file> <key-file>
36- [**--token**=<token>] [**--ca-url**=<uri>] [**--root**=<file>]
37+ [**--token**=<token>] [**--issuer**=<name>] [**--ca-url**=<uri>] [**--root**=<file>]
3738 [**--not-before**=<time|duration>] [**--not-after**=<time|duration>]
3839 [**--san**=<SAN>]` ,
3940 Description : `**step ca certificate** command generates a new certificate pair
@@ -88,6 +89,7 @@ $ step ca certificate --token $(step oauth --oidc --bare)
[email protected] joe.cr
8889'''` ,
8990 Flags : []cli.Flag {
9091 tokenFlag ,
92+ provisionerIssuerFlag ,
9193 caURLFlag ,
9294 rootFlag ,
9395 notBeforeFlag ,
@@ -115,13 +117,13 @@ func certificateAction(ctx *cli.Context) error {
115117 args := ctx .Args ()
116118 subject := args .Get (0 )
117119 crtFile , keyFile := args .Get (1 ), args .Get (2 )
118- token := ctx .String ("token" )
120+ tok := ctx .String ("token" )
119121 offline := ctx .Bool ("offline" )
120122 sans := ctx .StringSlice ("san" )
121123
122124 // offline and token are incompatible because the token is generated before
123125 // the start of the offline CA.
124- if offline && len (token ) != 0 {
126+ if offline && len (tok ) != 0 {
125127 return errs .IncompatibleFlagWithFlag (ctx , "offline" , "token" )
126128 }
127129
@@ -131,40 +133,54 @@ func certificateAction(ctx *cli.Context) error {
131133 return err
132134 }
133135
134- var isStepToken bool
135- if len (token ) == 0 {
136- if token , err = flow .GenerateToken (ctx , subject , sans ); err != nil {
136+ if len (tok ) == 0 {
137+ if tok , err = flow .GenerateToken (ctx , subject , sans ); err != nil {
137138 return err
138139 }
139- isStepToken = isStepCertificatesToken (token )
140- } else {
141- isStepToken = isStepCertificatesToken (token )
142- if isStepToken && len (sans ) > 0 {
143- return errs .MutuallyExclusiveFlags (ctx , "token" , "san" )
144- }
145140 }
146141
147- req , pk , err := flow .CreateSignRequest (token , sans )
142+ req , pk , err := flow .CreateSignRequest (tok , sans )
148143 if err != nil {
149144 return err
150145 }
151146
152- if isStepToken {
153- // Validate that subject matches the CSR common name.
147+ jwt , err := token .ParseInsecure (tok )
148+ if err != nil {
149+ return err
150+ }
151+
152+ switch jwt .Payload .Type () {
153+ case token .JWK : // Validate that subject matches the CSR common name.
154+ if ctx .String ("token" ) != "" && len (sans ) > 0 {
155+ return errs .MutuallyExclusiveFlags (ctx , "token" , "san" )
156+ }
154157 if strings .ToLower (subject ) != strings .ToLower (req .CsrPEM .Subject .CommonName ) {
155- return errors .Errorf ("token subject '%s' and common name '%s' do not match" , req .CsrPEM .Subject .CommonName , subject )
158+ return errors .Errorf ("token subject '%s' and argument '%s' do not match" , req .CsrPEM .Subject .CommonName , subject )
156159 }
157- } else {
158- // Validate that the subject matches an email SAN
160+ case token .OIDC : // Validate that the subject matches an email SAN
159161 if len (req .CsrPEM .EmailAddresses ) == 0 {
160162 return errors .New ("unexpected token: payload does not contain an email claim" )
161163 }
162164 if email := req .CsrPEM .EmailAddresses [0 ]; email != subject {
163165 return errors .Errorf ("token email '%s' and argument '%s' do not match" , email , subject )
164166 }
167+ case token .AWS : // Validate that the subject matches the instance id
168+ if strings .ToLower (subject ) != strings .ToLower (req .CsrPEM .Subject .CommonName ) {
169+ return errors .Errorf ("token subject '%s' and argument '%s' do not match" , req .CsrPEM .Subject .CommonName , subject )
170+ }
171+ case token .GCP : // Validate that the subject matches the instance Name
172+ if strings .ToLower (subject ) != strings .ToLower (req .CsrPEM .Subject .CommonName ) {
173+ return errors .Errorf ("token google.compute_engine.instance_name '%s' and argument '%s' do not match" , req .CsrPEM .Subject .CommonName , subject )
174+ }
175+ case token .Azure : // Validate that the subject matches the virtual machine name
176+ if strings .ToLower (subject ) != strings .ToLower (req .CsrPEM .Subject .CommonName ) {
177+ return errors .Errorf ("token virtual machine '%s' and argument '%s' do not match" , req .CsrPEM .Subject .CommonName , subject )
178+ }
179+ default :
180+ return errors .New ("token is not supported" )
165181 }
166182
167- if err := flow .Sign (ctx , token , req .CsrPEM , crtFile ); err != nil {
183+ if err := flow .Sign (ctx , tok , req .CsrPEM , crtFile ); err != nil {
168184 return err
169185 }
170186
@@ -178,25 +194,6 @@ func certificateAction(ctx *cli.Context) error {
178194 return nil
179195}
180196
181- type tokenClaims struct {
182- jose.Claims
183- SHA string `json:"sha"`
184- SANs []string `json:"sans"`
185- Email string `json:"email"`
186- }
187-
188- func isStepCertificatesToken (token string ) bool {
189- t , err := jose .ParseSigned (token )
190- if err != nil {
191- return false
192- }
193- var claims tokenClaims
194- if err := t .UnsafeClaimsWithoutVerification (& claims ); err != nil {
195- return false
196- }
197- return len (claims .SHA ) > 0 || len (claims .SANs ) > 0
198- }
199-
200197type certificateFlow struct {
201198 offlineCA * offlineCA
202199 offline bool
@@ -224,7 +221,7 @@ func newCertificateFlow(ctx *cli.Context) (*certificateFlow, error) {
224221 }, nil
225222}
226223
227- func (f * certificateFlow ) getClient (ctx * cli.Context , subject , token string ) (caClient , error ) {
224+ func (f * certificateFlow ) getClient (ctx * cli.Context , subject , tok string ) (caClient , error ) {
228225 if f .offline {
229226 return f .offlineCA , nil
230227 }
@@ -233,25 +230,38 @@ func (f *certificateFlow) getClient(ctx *cli.Context, subject, token string) (ca
233230 root := ctx .String ("root" )
234231 caURL := ctx .String ("ca-url" )
235232
236- tok , err := jose . ParseSigned ( token )
233+ jwt , err := token . ParseInsecure ( tok )
237234 if err != nil {
238235 return nil , errors .Wrap (err , "error parsing flag '--token'" )
239236 }
240- var claims tokenClaims
241- if err := tok .UnsafeClaimsWithoutVerification (& claims ); err != nil {
242- return nil , errors .Wrap (err , "error parsing flag '--token'" )
243- }
244- if strings .ToLower (claims .Subject ) != strings .ToLower (subject ) {
245- return nil , errors .Errorf ("token subject '%s' and CSR CommonName '%s' do not match" , claims .Subject , subject )
237+ switch jwt .Payload .Type () {
238+ case token .AWS :
239+ instanceID := jwt .Payload .Amazon .InstanceIdentityDocument .InstanceID
240+ if strings .ToLower (instanceID ) != strings .ToLower (subject ) {
241+ return nil , errors .Errorf ("token amazon.document.instanceId '%s' and CSR CommonName '%s' do not match" , instanceID , subject )
242+ }
243+ case token .GCP :
244+ instanceName := jwt .Payload .Google .ComputeEngine .InstanceName
245+ if strings .ToLower (instanceName ) != strings .ToLower (subject ) {
246+ return nil , errors .Errorf ("token google.compute_engine.instance_name '%s' and CSR CommonName '%s' do not match" , instanceName , subject )
247+ }
248+ case token .Azure :
249+ if strings .ToLower (jwt .Payload .Azure .VirtualMachine ) != strings .ToLower (subject ) {
250+ return nil , errors .Errorf ("token virtual machine '%s' and CSR CommonName '%s' do not match" , jwt .Payload .Azure .VirtualMachine , subject )
251+ }
252+ default :
253+ if strings .ToLower (jwt .Payload .Subject ) != strings .ToLower (subject ) {
254+ return nil , errors .Errorf ("token subject '%s' and CSR CommonName '%s' do not match" , jwt .Payload .Subject , subject )
255+ }
246256 }
247257
248258 // Prepare client for bootstrap or provisioning tokens
249259 var options []ca.ClientOption
250- if len (claims . SHA ) > 0 && len (claims . Audience ) > 0 && strings .HasPrefix (strings .ToLower (claims .Audience [0 ]), "http" ) {
260+ if len (jwt . Payload . SHA ) > 0 && len (jwt . Payload . Audience ) > 0 && strings .HasPrefix (strings .ToLower (jwt . Payload .Audience [0 ]), "http" ) {
251261 if len (caURL ) == 0 {
252- caURL = claims .Audience [0 ]
262+ caURL = jwt . Payload .Audience [0 ]
253263 }
254- options = append (options , ca .WithRootSHA256 (claims .SHA ))
264+ options = append (options , ca .WithRootSHA256 (jwt . Payload .SHA ))
255265 } else {
256266 if len (caURL ) == 0 {
257267 return nil , errs .RequiredFlag (ctx , "ca-url" )
@@ -341,14 +351,10 @@ func (f *certificateFlow) Sign(ctx *cli.Context, token string, csr api.Certifica
341351
342352// CreateSignRequest is a helper function that given an x509 OTT returns a
343353// simple but secure sign request as well as the private key used.
344- func (f * certificateFlow ) CreateSignRequest (token string , sans []string ) (* api.SignRequest , crypto.PrivateKey , error ) {
345- tok , err := jose . ParseSigned ( token )
354+ func (f * certificateFlow ) CreateSignRequest (tok string , sans []string ) (* api.SignRequest , crypto.PrivateKey , error ) {
355+ jwt , err := token . ParseInsecure ( tok )
346356 if err != nil {
347- return nil , nil , errors .Wrap (err , "error parsing token" )
348- }
349- var claims tokenClaims
350- if err := tok .UnsafeClaimsWithoutVerification (& claims ); err != nil {
351- return nil , nil , errors .Wrap (err , "error parsing token" )
357+ return nil , nil , err
352358 }
353359
354360 pk , err := keys .GenerateDefaultKey ()
@@ -357,14 +363,41 @@ func (f *certificateFlow) CreateSignRequest(token string, sans []string) (*api.S
357363 }
358364
359365 var emails []string
360- dnsNames , ips := splitSANs (sans , claims .SANs )
361- if claims .Email != "" {
362- emails = append (emails , claims .Email )
366+ dnsNames , ips := splitSANs (sans , jwt .Payload .SANs )
367+ if jwt .Payload .Email != "" {
368+ emails = append (emails , jwt .Payload .Email )
369+ }
370+
371+ subject := jwt .Payload .Subject
372+ switch jwt .Payload .Type () {
373+ case token .AWS :
374+ doc := jwt .Payload .Amazon .InstanceIdentityDocument
375+ subject = doc .InstanceID
376+ if len (ips ) == 0 && len (dnsNames ) == 0 {
377+ ips = append (ips , net .ParseIP (doc .PrivateIP ))
378+ dnsNames = append (dnsNames ,
379+ fmt .Sprintf ("ip-%s.%s.compute.internal" , strings .Replace (doc .PrivateIP , "." , "-" , - 1 ), doc .Region ),
380+ )
381+ }
382+ case token .GCP :
383+ ce := jwt .Payload .Google .ComputeEngine
384+ subject = ce .InstanceName
385+ if len (dnsNames ) == 0 {
386+ dnsNames = append (dnsNames ,
387+ fmt .Sprintf ("%s.c.%s.internal" , ce .InstanceName , ce .ProjectID ),
388+ fmt .Sprintf ("%s.%s.c.%s.internal" , ce .InstanceName , ce .Zone , ce .ProjectID ),
389+ )
390+ }
391+ case token .Azure :
392+ subject = jwt .Payload .Azure .VirtualMachine
393+ if len (dnsNames ) == 0 {
394+ dnsNames = append (dnsNames , jwt .Payload .Azure .VirtualMachine )
395+ }
363396 }
364397
365398 template := & x509.CertificateRequest {
366399 Subject : pkix.Name {
367- CommonName : claims . Subject ,
400+ CommonName : subject ,
368401 },
369402 SignatureAlgorithm : keys .DefaultSignatureAlgorithm ,
370403 DNSNames : dnsNames ,
@@ -385,7 +418,7 @@ func (f *certificateFlow) CreateSignRequest(token string, sans []string) (*api.S
385418 }
386419 return & api.SignRequest {
387420 CsrPEM : api.CertificateRequest {CertificateRequest : cr },
388- OTT : token ,
421+ OTT : tok ,
389422 }, pk , nil
390423}
391424
0 commit comments