diff --git a/command/ca/sign.go b/command/ca/sign.go index fa5f3c75e..a5505c306 100644 --- a/command/ca/sign.go +++ b/command/ca/sign.go @@ -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)) if err != nil { return err } diff --git a/command/ca/token.go b/command/ca/token.go index 8212b6319..ced964196 100644 --- a/command/ca/token.go +++ b/command/ca/token.go @@ -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" @@ -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 { @@ -26,7 +29,7 @@ func tokenCommand() cli.Command { [**--password-file**=] [**--provisioner-password-file**=] [**--output-file**=] [**--kms**=uri] [**--key**=] [**--san**=] [**--offline**] [**--revoke**] [**--x5c-cert**=] [**--x5c-key**=] [**--x5c-insecure**] -[**--sshpop-cert**=] [**--sshpop-key**=] +[**--sshpop-cert**=] [**--sshpop-key**=] [**--cnf-file**=] [**--ssh**] [**--host**] [**--principal**=] [**--k8ssa-token-path**=] [**--ca-url**=] [**--root**=] [**--context**=]`, Description: `**step ca token** command generates a one-time token granting access to the @@ -186,6 +189,10 @@ multiple principals.`, flags.SSHPOPKey, flags.NebulaCert, flags.NebulaKey, + cli.StringFlag{ + Name: "cnf-file", + Usage: "The CSR or SSH public key to restrict this token for.", + }, cli.StringFlag{ Name: "key", Usage: `The private key used to sign the JWT. This is usually downloaded from @@ -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)) + } + } + // --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") @@ -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...) if err != nil { return err } diff --git a/token/options.go b/token/options.go index 3643dffd5..703518880 100644 --- a/token/options.go +++ b/token/options.go @@ -2,6 +2,7 @@ package token import ( "bytes" + "crypto" "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" @@ -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. @@ -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) + } + + kid, err := fingerprint.New(data, crypto.SHA256, fingerprint.Base64RawURLFingerprint) + if err != nil { + return err + } + c.Set(ConfirmationClaim, map[string]string{ + "kid": kid, + }) + return nil + } +} + // WithValidity validates boundary inputs and sets the 'nbf' (NotBefore) and // 'exp' (expiration) options. func WithValidity(notBefore, expiration time.Time) Options { diff --git a/token/token.go b/token/token.go index b8b225d97..770f939b7 100644 --- a/token/token.go +++ b/token/token.go @@ -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) diff --git a/utils/cautils/certificate_flow.go b/utils/cautils/certificate_flow.go index 81fe2caf0..ea87cbe63 100644 --- a/utils/cautils/certificate_flow.go +++ b/utils/cautils/certificate_flow.go @@ -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. @@ -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) +} + +func newFuncFlowOption(f func(fo *flowContext)) *funcFlowOption { + return &funcFlowOption{ + f: f, + } +} + +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 + }) +} + +// WithCertificateRequest sets the X509 certificate request used in the request. +func WithCertificateRequest(cr *x509.CertificateRequest) Option { + return newFuncFlowOption(func(fo *flowContext) { + fo.CertificateRequest = cr + }) +} // 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) { var err error var offlineClient *OfflineCA + // Add options to the shared context + for _, opt := range opts { + opt.apply(&sharedContext) + } + offline := ctx.Bool("offline") if offline { caConfig := ctx.String("ca-config") diff --git a/utils/cautils/token_flow.go b/utils/cautils/token_flow.go index 8d6a1dd97..185d91538 100644 --- a/utils/cautils/token_flow.go +++ b/utils/cautils/token_flow.go @@ -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) + } + // Get audience from ca-url audience, err := parseAudience(ctx, tokType) if err != nil { diff --git a/utils/cautils/token_generator.go b/utils/cautils/token_generator.go index 465dbcc18..4961fa8ec 100644 --- a/utils/cautils/token_generator.go +++ b/utils/cautils/token_generator.go @@ -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)) + } + return t.Token(sub, opts...) } @@ -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)) + } + return t.Token(sub, opts...) }