Skip to content

Commit 4111ddf

Browse files
authored
Merge pull request #111 from smallstep/cloud-identities
Cloud identities
2 parents 52a50ed + 766b5ae commit 4111ddf

File tree

11 files changed

+938
-139
lines changed

11 files changed

+938
-139
lines changed

Gopkg.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

command/ca/ca.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ $STEPPATH/config/ca.json`,
149149
}
150150

151151
provisionerIssuerFlag = cli.StringFlag{
152-
Name: "issuer",
152+
Name: "issuer,provisioner",
153153
Usage: "The provisioner <name> to use.",
154154
}
155155

command/ca/certificate.go

Lines changed: 95 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
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-
200197
type 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

command/ca/offline.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/pem"
88
"fmt"
99
"net/http"
10+
"strings"
1011
"time"
1112

1213
"github.com/pkg/errors"
@@ -228,6 +229,23 @@ func (c *offlineCA) GenerateToken(ctx *cli.Context, typ int, subject string, san
228229
return "", err
229230
}
230231

232+
switch p := p.(type) {
233+
case *provisioner.OIDC: // Run step oauth
234+
out, err := exec.Step("oauth", "--oidc", "--bare",
235+
"--provider", p.ConfigurationEndpoint,
236+
"--client-id", p.ClientID, "--client-secret", p.ClientSecret)
237+
if err != nil {
238+
return "", err
239+
}
240+
return strings.TrimSpace(string(out)), nil
241+
case *provisioner.GCP: // Do the identity request to get the token
242+
return p.GetIdentityToken(c.CaURL())
243+
case *provisioner.AWS: // Do the identity request to get the token
244+
return p.GetIdentityToken(c.CaURL())
245+
case *provisioner.Azure: // Do the identity request to get the token
246+
return p.GetIdentityToken()
247+
}
248+
231249
// With OIDC just run step oauth
232250
if p, ok := p.(*provisioner.OIDC); ok {
233251
out, err := exec.Step("oauth", "--oidc", "--bare",

0 commit comments

Comments
 (0)