diff --git a/command/ca/token.go b/command/ca/token.go index 7a1e10021..785a6a020 100644 --- a/command/ca/token.go +++ b/command/ca/token.go @@ -34,7 +34,8 @@ func tokenCommand() cli.Command { [**--sshpop-cert**=] [**--sshpop-key**=] [**--cnf**=] [**--cnf-file**=] [**--ssh**] [**--host**] [**--principal**=] [**--k8ssa-token-path**=] -[**--ca-url**=] [**--root**=] [**--context**=]`, +[**--ca-url**=] [**--root**=] [**--context**=] +[**--set**=] [**--set-file**=]`, Description: `**step ca token** command generates a one-time token granting access to the certificates authority. @@ -174,6 +175,18 @@ add the intermediate and the root in the provisioner configuration: $ step ca token --kms yubikey:pin-value=123456 \ --x5c-cert yubikey:slot-id=82 --x5c-key yubikey:slot-id=82 \ internal.example.com +''' + +Generate a token with custom data in the "user" claim. The example below can be +accessed in a template as **{{ .Token.user.field }}**, rendering to the string +"value". + +This is distinct from **.Insecure.User**: any attributes set using this option +are added to a claim named "user" in the signed JWT produced by this command. +This data may therefore be considered trusted (insofar as the token itself is +trusted). +''' +$ step ca token --set field=value internal.example.com '''`, Flags: []cli.Flag{ provisionerKidFlag, @@ -244,6 +257,8 @@ be invalid for any other API request.`, flags.CaURL, flags.Root, flags.Context, + flags.TemplateSet, + flags.TemplateSetFile, }, } } @@ -350,11 +365,29 @@ func tokenAction(ctx *cli.Context) error { tokenOpts = append(tokenOpts, cautils.WithConfirmationFingerprint(cnf)) } + templateData, err := flags.GetTemplateData(ctx) + if err != nil { + return err + } + if templateData != nil { + tokenOpts = append(tokenOpts, cautils.WithCustomAttributes(templateData)) + } + // --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") } + // --offline doesn't support tokenOpts, so reject set/set-file + if offline { + if len(ctx.StringSlice("set")) > 0 { + return errs.IncompatibleFlagWithFlag(ctx, "offline", "set") + } + if ctx.String("set-file") != "" { + return errs.IncompatibleFlagWithFlag(ctx, "offline", "set-file") + } + } + // parse times or durations notBefore, ok := flags.ParseTimeOrDuration(ctx.String("not-before")) if !ok { diff --git a/token/options.go b/token/options.go index cf8b85979..3ceb1d1b2 100644 --- a/token/options.go +++ b/token/options.go @@ -80,6 +80,25 @@ func WithStep(v interface{}) Options { } } +// WithUserData returns an Option function that merges the provided map with the +// existing user claim in the payload. +func WithUserData(v map[string]interface{}) Options { + return func(c *Claims) error { + if _, ok := c.ExtraClaims[UserClaim]; !ok { + c.Set(UserClaim, make(map[string]interface{})) + } + s := c.ExtraClaims[UserClaim] + sm, ok := s.(map[string]interface{}) + if !ok { + return fmt.Errorf("%q claim is %T, not map[string]interface{}", UserClaim, s) + } + for k, val := range v { + sm[k] = val + } + return nil + } +} + // WithSSH returns an Options function that sets the step claim with the ssh // property in the value. func WithSSH(v interface{}) Options { diff --git a/token/token.go b/token/token.go index 770f939b7..e0a00c1ce 100644 --- a/token/token.go +++ b/token/token.go @@ -32,6 +32,9 @@ const SANSClaim = "sans" // StepClaim is the property name for a JWT claim the stores the custom information in the certificate. const StepClaim = "step" +// UserClaim is the property name for a JWT claim that stores user-provided custom information. +const UserClaim = "user" + // ConfirmationClaim is the property name for a JWT claim that stores a JSON // object used as Proof-Of-Possession. const ConfirmationClaim = "cnf" diff --git a/utils/cautils/certificate_flow.go b/utils/cautils/certificate_flow.go index 8403ef8ee..5e25ba73a 100644 --- a/utils/cautils/certificate_flow.go +++ b/utils/cautils/certificate_flow.go @@ -43,6 +43,7 @@ type flowContext struct { SSHPublicKey ssh.PublicKey CertificateRequest *x509.CertificateRequest ConfirmationFingerprint string + CustomAttributes map[string]interface{} } // sharedContext is used to share information between commands. @@ -88,6 +89,18 @@ func WithConfirmationFingerprint(fp string) Option { }) } +// WithCustomAttributes adds custom attributes to be set in the "user" claim. +func WithCustomAttributes(v map[string]interface{}) Option { + return newFuncFlowOption(func(fo *flowContext) { + if fo.CustomAttributes == nil { + fo.CustomAttributes = make(map[string]interface{}) + } + for k, val := range v { + fo.CustomAttributes[k] = val + } + }) +} + // NewCertificateFlow initializes a cli flow to get a new certificate. func NewCertificateFlow(ctx *cli.Context, opts ...Option) (*CertificateFlow, error) { var err error diff --git a/utils/cautils/token_generator.go b/utils/cautils/token_generator.go index 8aaaef314..41ada1c9d 100644 --- a/utils/cautils/token_generator.go +++ b/utils/cautils/token_generator.go @@ -108,6 +108,11 @@ func (t *TokenGenerator) SignToken(sub string, sans []string, opts ...token.Opti opts = append(opts, token.WithConfirmationFingerprint(sharedContext.ConfirmationFingerprint)) } + // Add custom user data, if set. + if sharedContext.CustomAttributes != nil { + opts = append(opts, token.WithUserData(sharedContext.CustomAttributes)) + } + return t.Token(sub, opts...) } @@ -126,6 +131,11 @@ func (t *TokenGenerator) SignSSHToken(sub, certType string, principals []string, ValidBefore: notAfter, })}, opts...) + // Add custom user data, if set. + if sharedContext.CustomAttributes != nil { + opts = append(opts, token.WithUserData(sharedContext.CustomAttributes)) + } + return t.Token(sub, opts...) }