Skip to content

Commit

Permalink
Merge pull request #111 from smallstep/cloud-identities
Browse files Browse the repository at this point in the history
Cloud identities
  • Loading branch information
maraino authored Jun 7, 2019
2 parents 52a50ed + 766b5ae commit 4111ddf
Show file tree
Hide file tree
Showing 11 changed files with 938 additions and 139 deletions.
12 changes: 6 additions & 6 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion command/ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ $STEPPATH/config/ca.json`,
}

provisionerIssuerFlag = cli.StringFlag{
Name: "issuer",
Name: "issuer,provisioner",
Usage: "The provisioner <name> to use.",
}

Expand Down
157 changes: 95 additions & 62 deletions command/ca/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"net"
"os"
"strings"
Expand All @@ -21,7 +22,7 @@ import (
"github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/errs"
"github.com/smallstep/cli/flags"
"github.com/smallstep/cli/jose"
"github.com/smallstep/cli/token"
"github.com/smallstep/cli/ui"
"github.com/smallstep/cli/utils"
"github.com/urfave/cli"
Expand All @@ -33,7 +34,7 @@ func certificateCommand() cli.Command {
Action: command.ActionFunc(certificateAction),
Usage: "generate a new private key and certificate signed by the root certificate",
UsageText: `**step ca certificate** <subject> <crt-file> <key-file>
[**--token**=<token>] [**--ca-url**=<uri>] [**--root**=<file>]
[**--token**=<token>] [**--issuer**=<name>] [**--ca-url**=<uri>] [**--root**=<file>]
[**--not-before**=<time|duration>] [**--not-after**=<time|duration>]
[**--san**=<SAN>]`,
Description: `**step ca certificate** command generates a new certificate pair
Expand Down Expand Up @@ -88,6 +89,7 @@ $ step ca certificate --token $(step oauth --oidc --bare) [email protected] joe.cr
'''`,
Flags: []cli.Flag{
tokenFlag,
provisionerIssuerFlag,
caURLFlag,
rootFlag,
notBeforeFlag,
Expand Down Expand Up @@ -115,13 +117,13 @@ func certificateAction(ctx *cli.Context) error {
args := ctx.Args()
subject := args.Get(0)
crtFile, keyFile := args.Get(1), args.Get(2)
token := ctx.String("token")
tok := ctx.String("token")
offline := ctx.Bool("offline")
sans := ctx.StringSlice("san")

// offline and token are incompatible because the token is generated before
// the start of the offline CA.
if offline && len(token) != 0 {
if offline && len(tok) != 0 {
return errs.IncompatibleFlagWithFlag(ctx, "offline", "token")
}

Expand All @@ -131,40 +133,54 @@ func certificateAction(ctx *cli.Context) error {
return err
}

var isStepToken bool
if len(token) == 0 {
if token, err = flow.GenerateToken(ctx, subject, sans); err != nil {
if len(tok) == 0 {
if tok, err = flow.GenerateToken(ctx, subject, sans); err != nil {
return err
}
isStepToken = isStepCertificatesToken(token)
} else {
isStepToken = isStepCertificatesToken(token)
if isStepToken && len(sans) > 0 {
return errs.MutuallyExclusiveFlags(ctx, "token", "san")
}
}

req, pk, err := flow.CreateSignRequest(token, sans)
req, pk, err := flow.CreateSignRequest(tok, sans)
if err != nil {
return err
}

if isStepToken {
// Validate that subject matches the CSR common name.
jwt, err := token.ParseInsecure(tok)
if err != nil {
return err
}

switch jwt.Payload.Type() {
case token.JWK: // Validate that subject matches the CSR common name.
if ctx.String("token") != "" && len(sans) > 0 {
return errs.MutuallyExclusiveFlags(ctx, "token", "san")
}
if strings.ToLower(subject) != strings.ToLower(req.CsrPEM.Subject.CommonName) {
return errors.Errorf("token subject '%s' and common name '%s' do not match", req.CsrPEM.Subject.CommonName, subject)
return errors.Errorf("token subject '%s' and argument '%s' do not match", req.CsrPEM.Subject.CommonName, subject)
}
} else {
// Validate that the subject matches an email SAN
case token.OIDC: // Validate that the subject matches an email SAN
if len(req.CsrPEM.EmailAddresses) == 0 {
return errors.New("unexpected token: payload does not contain an email claim")
}
if email := req.CsrPEM.EmailAddresses[0]; email != subject {
return errors.Errorf("token email '%s' and argument '%s' do not match", email, subject)
}
case token.AWS: // Validate that the subject matches the instance id
if strings.ToLower(subject) != strings.ToLower(req.CsrPEM.Subject.CommonName) {
return errors.Errorf("token subject '%s' and argument '%s' do not match", req.CsrPEM.Subject.CommonName, subject)
}
case token.GCP: // Validate that the subject matches the instance Name
if strings.ToLower(subject) != strings.ToLower(req.CsrPEM.Subject.CommonName) {
return errors.Errorf("token google.compute_engine.instance_name '%s' and argument '%s' do not match", req.CsrPEM.Subject.CommonName, subject)
}
case token.Azure: // Validate that the subject matches the virtual machine name
if strings.ToLower(subject) != strings.ToLower(req.CsrPEM.Subject.CommonName) {
return errors.Errorf("token virtual machine '%s' and argument '%s' do not match", req.CsrPEM.Subject.CommonName, subject)
}
default:
return errors.New("token is not supported")
}

if err := flow.Sign(ctx, token, req.CsrPEM, crtFile); err != nil {
if err := flow.Sign(ctx, tok, req.CsrPEM, crtFile); err != nil {
return err
}

Expand All @@ -178,25 +194,6 @@ func certificateAction(ctx *cli.Context) error {
return nil
}

type tokenClaims struct {
jose.Claims
SHA string `json:"sha"`
SANs []string `json:"sans"`
Email string `json:"email"`
}

func isStepCertificatesToken(token string) bool {
t, err := jose.ParseSigned(token)
if err != nil {
return false
}
var claims tokenClaims
if err := t.UnsafeClaimsWithoutVerification(&claims); err != nil {
return false
}
return len(claims.SHA) > 0 || len(claims.SANs) > 0
}

type certificateFlow struct {
offlineCA *offlineCA
offline bool
Expand Down Expand Up @@ -224,7 +221,7 @@ func newCertificateFlow(ctx *cli.Context) (*certificateFlow, error) {
}, nil
}

func (f *certificateFlow) getClient(ctx *cli.Context, subject, token string) (caClient, error) {
func (f *certificateFlow) getClient(ctx *cli.Context, subject, tok string) (caClient, error) {
if f.offline {
return f.offlineCA, nil
}
Expand All @@ -233,25 +230,38 @@ func (f *certificateFlow) getClient(ctx *cli.Context, subject, token string) (ca
root := ctx.String("root")
caURL := ctx.String("ca-url")

tok, err := jose.ParseSigned(token)
jwt, err := token.ParseInsecure(tok)
if err != nil {
return nil, errors.Wrap(err, "error parsing flag '--token'")
}
var claims tokenClaims
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, errors.Wrap(err, "error parsing flag '--token'")
}
if strings.ToLower(claims.Subject) != strings.ToLower(subject) {
return nil, errors.Errorf("token subject '%s' and CSR CommonName '%s' do not match", claims.Subject, subject)
switch jwt.Payload.Type() {
case token.AWS:
instanceID := jwt.Payload.Amazon.InstanceIdentityDocument.InstanceID
if strings.ToLower(instanceID) != strings.ToLower(subject) {
return nil, errors.Errorf("token amazon.document.instanceId '%s' and CSR CommonName '%s' do not match", instanceID, subject)
}
case token.GCP:
instanceName := jwt.Payload.Google.ComputeEngine.InstanceName
if strings.ToLower(instanceName) != strings.ToLower(subject) {
return nil, errors.Errorf("token google.compute_engine.instance_name '%s' and CSR CommonName '%s' do not match", instanceName, subject)
}
case token.Azure:
if strings.ToLower(jwt.Payload.Azure.VirtualMachine) != strings.ToLower(subject) {
return nil, errors.Errorf("token virtual machine '%s' and CSR CommonName '%s' do not match", jwt.Payload.Azure.VirtualMachine, subject)
}
default:
if strings.ToLower(jwt.Payload.Subject) != strings.ToLower(subject) {
return nil, errors.Errorf("token subject '%s' and CSR CommonName '%s' do not match", jwt.Payload.Subject, subject)
}
}

// Prepare client for bootstrap or provisioning tokens
var options []ca.ClientOption
if len(claims.SHA) > 0 && len(claims.Audience) > 0 && strings.HasPrefix(strings.ToLower(claims.Audience[0]), "http") {
if len(jwt.Payload.SHA) > 0 && len(jwt.Payload.Audience) > 0 && strings.HasPrefix(strings.ToLower(jwt.Payload.Audience[0]), "http") {
if len(caURL) == 0 {
caURL = claims.Audience[0]
caURL = jwt.Payload.Audience[0]
}
options = append(options, ca.WithRootSHA256(claims.SHA))
options = append(options, ca.WithRootSHA256(jwt.Payload.SHA))
} else {
if len(caURL) == 0 {
return nil, errs.RequiredFlag(ctx, "ca-url")
Expand Down Expand Up @@ -341,14 +351,10 @@ func (f *certificateFlow) Sign(ctx *cli.Context, token string, csr api.Certifica

// CreateSignRequest is a helper function that given an x509 OTT returns a
// simple but secure sign request as well as the private key used.
func (f *certificateFlow) CreateSignRequest(token string, sans []string) (*api.SignRequest, crypto.PrivateKey, error) {
tok, err := jose.ParseSigned(token)
func (f *certificateFlow) CreateSignRequest(tok string, sans []string) (*api.SignRequest, crypto.PrivateKey, error) {
jwt, err := token.ParseInsecure(tok)
if err != nil {
return nil, nil, errors.Wrap(err, "error parsing token")
}
var claims tokenClaims
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, nil, errors.Wrap(err, "error parsing token")
return nil, nil, err
}

pk, err := keys.GenerateDefaultKey()
Expand All @@ -357,14 +363,41 @@ func (f *certificateFlow) CreateSignRequest(token string, sans []string) (*api.S
}

var emails []string
dnsNames, ips := splitSANs(sans, claims.SANs)
if claims.Email != "" {
emails = append(emails, claims.Email)
dnsNames, ips := splitSANs(sans, jwt.Payload.SANs)
if jwt.Payload.Email != "" {
emails = append(emails, jwt.Payload.Email)
}

subject := jwt.Payload.Subject
switch jwt.Payload.Type() {
case token.AWS:
doc := jwt.Payload.Amazon.InstanceIdentityDocument
subject = doc.InstanceID
if len(ips) == 0 && len(dnsNames) == 0 {
ips = append(ips, net.ParseIP(doc.PrivateIP))
dnsNames = append(dnsNames,
fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region),
)
}
case token.GCP:
ce := jwt.Payload.Google.ComputeEngine
subject = ce.InstanceName
if len(dnsNames) == 0 {
dnsNames = append(dnsNames,
fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID),
fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID),
)
}
case token.Azure:
subject = jwt.Payload.Azure.VirtualMachine
if len(dnsNames) == 0 {
dnsNames = append(dnsNames, jwt.Payload.Azure.VirtualMachine)
}
}

template := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: claims.Subject,
CommonName: subject,
},
SignatureAlgorithm: keys.DefaultSignatureAlgorithm,
DNSNames: dnsNames,
Expand All @@ -385,7 +418,7 @@ func (f *certificateFlow) CreateSignRequest(token string, sans []string) (*api.S
}
return &api.SignRequest{
CsrPEM: api.CertificateRequest{CertificateRequest: cr},
OTT: token,
OTT: tok,
}, pk, nil
}

Expand Down
18 changes: 18 additions & 0 deletions command/ca/offline.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/pem"
"fmt"
"net/http"
"strings"
"time"

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

switch p := p.(type) {
case *provisioner.OIDC: // Run step oauth
out, err := exec.Step("oauth", "--oidc", "--bare",
"--provider", p.ConfigurationEndpoint,
"--client-id", p.ClientID, "--client-secret", p.ClientSecret)
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
case *provisioner.GCP: // Do the identity request to get the token
return p.GetIdentityToken(c.CaURL())
case *provisioner.AWS: // Do the identity request to get the token
return p.GetIdentityToken(c.CaURL())
case *provisioner.Azure: // Do the identity request to get the token
return p.GetIdentityToken()
}

// With OIDC just run step oauth
if p, ok := p.(*provisioner.OIDC); ok {
out, err := exec.Step("oauth", "--oidc", "--bare",
Expand Down
Loading

0 comments on commit 4111ddf

Please sign in to comment.