Skip to content

Commit

Permalink
Allow to add confirmation claims to tokens
Browse files Browse the repository at this point in the history
This commit allows passing confirmation claims to tokens to tie the
tokens with a provided CSR or SSH public key.

Fixes smallstep/certificates#1637
  • Loading branch information
maraino committed Dec 29, 2023
1 parent 937370a commit 8a7ccc8
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 8 deletions.
2 changes: 1 addition & 1 deletion command/ca/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func signCertificateAction(ctx *cli.Context) error {
}

// certificate flow unifies online and offline flows on a single api
flow, err := cautils.NewCertificateFlow(ctx)
flow, err := cautils.NewCertificateFlow(ctx, cautils.WithCertificateRequest(csr))

Check warning on line 178 in command/ca/sign.go

View check run for this annotation

Codecov / codecov/patch

command/ca/sign.go#L178

Added line #L178 was not covered by tests
if err != nil {
return err
}
Expand Down
34 changes: 32 additions & 2 deletions command/ca/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"

"github.com/pkg/errors"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/pki"
"github.com/smallstep/cli/flags"
Expand All @@ -12,6 +13,8 @@ import (
"github.com/urfave/cli"
"go.step.sm/cli-utils/command"
"go.step.sm/cli-utils/errs"
"go.step.sm/crypto/pemutil"
"golang.org/x/crypto/ssh"
)

func tokenCommand() cli.Command {
Expand All @@ -26,7 +29,7 @@ func tokenCommand() cli.Command {
[**--password-file**=<file>] [**--provisioner-password-file**=<file>]
[**--output-file**=<file>] [**--kms**=uri] [**--key**=<file>] [**--san**=<SAN>] [**--offline**]
[**--revoke**] [**--x5c-cert**=<file>] [**--x5c-key**=<file>] [**--x5c-insecure**]
[**--sshpop-cert**=<file>] [**--sshpop-key**=<file>]
[**--sshpop-cert**=<file>] [**--sshpop-key**=<file>] [**--cnf-file**=<file>]
[**--ssh**] [**--host**] [**--principal**=<name>] [**--k8ssa-token-path**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
Description: `**step ca token** command generates a one-time token granting access to the
Expand Down Expand Up @@ -186,6 +189,10 @@ multiple principals.`,
flags.SSHPOPKey,
flags.NebulaCert,
flags.NebulaKey,
cli.StringFlag{
Name: "cnf-file",
Usage: "The CSR or SSH public key <file> to restrict this token for.",
},
cli.StringFlag{
Name: "key",
Usage: `The private key <file> used to sign the JWT. This is usually downloaded from
Expand Down Expand Up @@ -295,6 +302,29 @@ func tokenAction(ctx *cli.Context) error {
}
}

// Add options to create a confirmation claim if a CSR or SSH public key is
// passed.
var tokenOpts []cautils.Option
if filename := ctx.String("cnf-file"); filename != "" {
in, err := utils.ReadFile(filename)
if err != nil {
return err
}
if isSSH {
sshPub, _, _, _, err := ssh.ParseAuthorizedKey(in)
if err != nil {
return errors.Wrap(err, "error parsing ssh public key")
}
tokenOpts = append(tokenOpts, cautils.WithSSHPublicKey(sshPub))
} else {
csr, err := pemutil.ParseCertificateRequest(in)
if err != nil {
return errors.Wrap(err, "error parsing certificate request")
}
tokenOpts = append(tokenOpts, cautils.WithCertificateRequest(csr))

Check warning on line 324 in command/ca/token.go

View check run for this annotation

Codecov / codecov/patch

command/ca/token.go#L307-L324

Added lines #L307 - L324 were not covered by tests
}
}

// --san and --type revoke are incompatible. Revocation tokens do not support SANs.
if typ == cautils.RevokeType && len(sans) > 0 {
return errs.IncompatibleFlagWithFlag(ctx, "san", "revoke")
Expand Down Expand Up @@ -327,7 +357,7 @@ func tokenAction(ctx *cli.Context) error {
return err
}
} else {
token, err = cautils.NewTokenFlow(ctx, typ, subject, sans, caURL, root, notBefore, notAfter, certNotBefore, certNotAfter)
token, err = cautils.NewTokenFlow(ctx, typ, subject, sans, caURL, root, notBefore, notAfter, certNotBefore, certNotAfter, tokenOpts...)

Check warning on line 360 in command/ca/token.go

View check run for this annotation

Codecov / codecov/patch

command/ca/token.go#L360

Added line #L360 was not covered by tests
if err != nil {
return err
}
Expand Down
31 changes: 31 additions & 0 deletions token/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package token

import (
"bytes"
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
Expand All @@ -15,9 +16,11 @@ import (

"github.com/pkg/errors"
nebula "github.com/slackhq/nebula/cert"
"go.step.sm/crypto/fingerprint"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x25519"
"golang.org/x/crypto/ssh"
)

// Options is a function that set claims.
Expand Down Expand Up @@ -84,6 +87,34 @@ func WithSSH(v interface{}) Options {
})
}

// WithFingerprint returns an Options function that the cnf claims with the kid
// representing the fingerprint of the certificate request or the ssh public
// key.
func WithFingerprint(v interface{}) Options {
return func(c *Claims) error {
var data []byte
switch vv := v.(type) {
case *x509.CertificateRequest:
data = vv.Raw
case ssh.PublicKey:
data = vv.Marshal()
case []byte:
data = vv
default:
return fmt.Errorf("unsupported fingerprint for %T", vv)

Check warning on line 104 in token/options.go

View check run for this annotation

Codecov / codecov/patch

token/options.go#L93-L104

Added lines #L93 - L104 were not covered by tests
}

kid, err := fingerprint.New(data, crypto.SHA256, fingerprint.Base64RawURLFingerprint)
if err != nil {
return err
}
c.Set(ConfirmationClaim, map[string]string{
"kid": kid,
})
return nil

Check warning on line 114 in token/options.go

View check run for this annotation

Codecov / codecov/patch

token/options.go#L107-L114

Added lines #L107 - L114 were not covered by tests
}
}

// WithValidity validates boundary inputs and sets the 'nbf' (NotBefore) and
// 'exp' (expiration) options.
func WithValidity(notBefore, expiration time.Time) Options {
Expand Down
4 changes: 4 additions & 0 deletions token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const SANSClaim = "sans"
// StepClaim is the property name for a JWT claim the stores the custom information in the certificate.
const StepClaim = "step"

// ConfirmationClaim is the property name for a JWT claim that stores a JSON
// object used as Proof-Of-Possession.
const ConfirmationClaim = "cnf"

// Token interface which all token types should attempt to implement.
type Token interface {
SignedString(sigAlg string, priv interface{}) (string, error)
Expand Down
50 changes: 46 additions & 4 deletions utils/cautils/certificate_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
"golang.org/x/crypto/ssh"
)

// CertificateFlow manages the flow to retrieve a new certificate.
Expand All @@ -35,16 +36,57 @@ type CertificateFlow struct {
offline bool
}

type flowContext struct {
DisableCustomSANs bool
SSHPublicKey ssh.PublicKey
CertificateRequest *x509.CertificateRequest
}

// sharedContext is used to share information between commands.
var sharedContext = struct {
DisableCustomSANs bool
}{}
var sharedContext flowContext

type funcFlowOption struct {
f func(fo *flowContext)
}

func (ffo *funcFlowOption) apply(fo *flowContext) {
ffo.f(fo)

Check warning on line 53 in utils/cautils/certificate_flow.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/certificate_flow.go#L52-L53

Added lines #L52 - L53 were not covered by tests
}

func newFuncFlowOption(f func(fo *flowContext)) *funcFlowOption {
return &funcFlowOption{
f: f,
}

Check warning on line 59 in utils/cautils/certificate_flow.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/certificate_flow.go#L56-L59

Added lines #L56 - L59 were not covered by tests
}

type Option interface {
apply(fo *flowContext)
}

// WithSSHPublicKey sets the SSH public key used in the request.
func WithSSHPublicKey(key ssh.PublicKey) Option {
return newFuncFlowOption(func(fo *flowContext) {
fo.SSHPublicKey = key
})

Check warning on line 70 in utils/cautils/certificate_flow.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/certificate_flow.go#L67-L70

Added lines #L67 - L70 were not covered by tests
}

// WithCertificateRequest sets the X509 certificate request used in the request.
func WithCertificateRequest(cr *x509.CertificateRequest) Option {
return newFuncFlowOption(func(fo *flowContext) {
fo.CertificateRequest = cr
})

Check warning on line 77 in utils/cautils/certificate_flow.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/certificate_flow.go#L74-L77

Added lines #L74 - L77 were not covered by tests
}

// NewCertificateFlow initializes a cli flow to get a new certificate.
func NewCertificateFlow(ctx *cli.Context) (*CertificateFlow, error) {
func NewCertificateFlow(ctx *cli.Context, opts ...Option) (*CertificateFlow, error) {

Check warning on line 81 in utils/cautils/certificate_flow.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/certificate_flow.go#L81

Added line #L81 was not covered by tests
var err error
var offlineClient *OfflineCA

// Add options to the shared context
for _, opt := range opts {
opt.apply(&sharedContext)
}

Check warning on line 88 in utils/cautils/certificate_flow.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/certificate_flow.go#L85-L88

Added lines #L85 - L88 were not covered by tests

offline := ctx.Bool("offline")
if offline {
caConfig := ctx.String("ca-config")
Expand Down
7 changes: 6 additions & 1 deletion utils/cautils/token_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ func (e *ACMETokenError) Error() string {
}

// NewTokenFlow implements the common flow used to generate a token
func NewTokenFlow(ctx *cli.Context, tokType int, subject string, sans []string, caURL, root string, notBefore, notAfter time.Time, certNotBefore, certNotAfter provisioner.TimeDuration) (string, error) {
func NewTokenFlow(ctx *cli.Context, tokType int, subject string, sans []string, caURL, root string, notBefore, notAfter time.Time, certNotBefore, certNotAfter provisioner.TimeDuration, opts ...Option) (string, error) {
// Apply options to shared context
for _, opt := range opts {
opt.apply(&sharedContext)
}

Check warning on line 92 in utils/cautils/token_flow.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/token_flow.go#L88-L92

Added lines #L88 - L92 were not covered by tests

// Get audience from ca-url
audience, err := parseAudience(ctx, tokType)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions utils/cautils/token_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ func (t *TokenGenerator) SignToken(sub string, sans []string, opts ...token.Opti
sans = []string{sub}
}
opts = append(opts, token.WithSANS(sans))

// Tie certificate request to the token used in the JWK and X5C provisioners
if sharedContext.CertificateRequest != nil {
opts = append(opts, token.WithFingerprint(sharedContext.CertificateRequest))
}

Check warning on line 105 in utils/cautils/token_generator.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/token_generator.go#L101-L105

Added lines #L101 - L105 were not covered by tests

return t.Token(sub, opts...)
}

Expand All @@ -115,6 +121,12 @@ func (t *TokenGenerator) SignSSHToken(sub, certType string, principals []string,
ValidAfter: notBefore,
ValidBefore: notAfter,
})}, opts...)

// Tie SSH public key to the token used in the JWK and X5C provisioners
if sharedContext.SSHPublicKey != nil {
opts = append(opts, token.WithFingerprint(sharedContext.SSHPublicKey))
}

Check warning on line 128 in utils/cautils/token_generator.go

View check run for this annotation

Codecov / codecov/patch

utils/cautils/token_generator.go#L124-L128

Added lines #L124 - L128 were not covered by tests

return t.Token(sub, opts...)
}

Expand Down

0 comments on commit 8a7ccc8

Please sign in to comment.