1
0
mirror of https://github.com/smallstep/cli.git synced 2025-08-06 05:02:39 +03:00
Files
step-ca-cli/command/ca/token.go
Max 60a4eef3de Fix linter warnings (#1450)
* Remove unnecessary utils files in favor of cli-utils package
* Remove nolint:revive directive from bcrypt-pbkdf file
2025-07-10 11:15:28 -07:00

444 lines
14 KiB
Go

package ca
import (
"fmt"
"os"
"github.com/pkg/errors"
"github.com/urfave/cli"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/pki"
"github.com/smallstep/cli-utils/command"
"github.com/smallstep/cli-utils/errs"
"github.com/smallstep/cli-utils/fileutil"
"go.step.sm/crypto/pemutil"
"golang.org/x/crypto/ssh"
"github.com/smallstep/cli/flags"
"github.com/smallstep/cli/utils"
"github.com/smallstep/cli/utils/cautils"
)
func tokenCommand() cli.Command {
return cli.Command{
Name: "token",
Action: command.ActionFunc(tokenAction),
Usage: "generate an OTT granting access to the CA",
UsageText: `**step ca token** <subject>
[--**kid**=<kid>] [--**issuer**=<name>]
[**--cert-not-before**=<time|duration>] [**--cert-not-after**=<time|duration>]
[**--not-before**=<time|duration>] [**--not-after**=<time|duration>]
[**--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>]
[**--cnf**=<fingerprint>] [**--cnf-file**=<file>]
[**--ssh**] [**--host**] [**--principal**=<name>] [**--k8ssa-token-path**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]
[**--set**=<key=value>] [**--set-file**=<file>]`,
Description: `**step ca token** command generates a one-time token granting access to the
certificates authority.
## POSITIONAL ARGUMENTS
<subject>
: The Common Name, DNS Name, or IP address that will be set by the certificate authority.
When there are no additional Subject Alternative Names configured (via the
--san flag), the subject will be added as the only element of the 'sans' claim
on the token.
## EXAMPLES
Most of the following examples assumes that **--ca-url** and **--root** are
set using environment variables or the default configuration file in
<$STEPPATH/config/defaults.json>.
Get a new token for a DNS. Because there are no Subject Alternative Names
configured (via the '--san' flag), the 'sans' claim of the token will have a
default value of ['internal.example.com']:
'''
$ step ca token internal.example.com
'''
Get a new token for a 'Revoke' request:
'''
$ step ca token --revoke 146103349666685108195655980390445292315
'''
Get a new token for an IP address. Because there are no Subject Alternative Names
configured (via the '--san' flag), the 'sans' claim of the token will have a
default value of ['192.168.10.10']:
'''
$ step ca token 192.168.10.10
'''
Get a new token with custom Subject Alternative Names. The value of the 'sans'
claim of the token will be ['1.1.1.1', 'hello.example.com'] - 'foobar' will not
be in the 'sans' claim unless explicitly configured via the '--san' flag:
'''
$ step ca token foobar --san 1.1.1.1 --san hello.example.com
'''
Get a new token that expires in 30 minutes:
'''
$ step ca token --not-after 30m internal.example.com
'''
Get a new token that becomes valid in 30 minutes and expires 5 minutes after that:
'''
$ step ca token --not-before 30m --not-after 35m internal.example.com
'''
Get a new token with a confirmation claim to enforce a given CSR fingerprint:
'''
$ step certificate fingerprint --format base64-url-raw internal.csr
PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw
$ step ca token --cnf PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw internal.smallstep.com
'''
Get a new token with a confirmation claim to enforce the use of a given CSR:
'''
step ca token --cnf-file internal.csr internal.smallstep.com
'''
Get a new token signed with the given private key, the public key must be
configured in the certificate authority:
'''
$ step ca token internal.smallstep.com --key token.key
'''
Get a new token for a specific provisioner kid, ca-url and root:
'''
$ step ca token internal.example.com \
--kid 4vn46fbZT68Uxfs9LBwHkTvrjEvxQqx-W8nnE-qDjts \
--ca-url https://ca.example.com \
--root /path/to/root_ca.crt
'''
Get a new token using the simple offline mode, requires the configuration
files, certificates, and keys created with **step ca init**:
'''
$ step ca token internal.example.com --offline
'''
Get a new token using the offline mode with all the parameters:
'''
$ step ca token internal.example.com \
--offline \
--kid 4vn46fbZT68Uxfs9LBwHkTvrjEvxQqx-W8nnE-qDjts \
--issuer you@example.com \
--key provisioner.key \
--ca-url https://ca.example.com \
--root /path/to/root_ca.crt
'''
Get a new token for a 'Revoke' request:
'''
$ step ca token --revoke 146103349666685108195655980390445292315
'''
Get a new token in offline mode for a 'Revoke' request:
'''
$ step ca token --offline --revoke 146103349666685108195655980390445292315
'''
Get a new token for an SSH user certificate:
'''
$ step ca token max@smallstep.com --ssh
'''
Get a new token for an SSH host certificate:
'''
$ step ca token my-remote.hostname --ssh --host
'''
Get a new token with a confirmation claim to enforce the use of a given public key:
'''
step ca token --ssh --host --cnf-file internal.pub internal.smallstep.com
'''
Generate a renew token and use it in a renew after expiry request:
'''
$ TOKEN=$(step ca token --x5c-cert internal.crt --x5c-key internal.key --renew internal.example.com)
$ curl -X POST -H "Authorization: Bearer $TOKEN" https://ca.example.com/1.0/renew
'''
Generate a JWK provisioner token using a key in a YubiKey:
'''
$ step ca token --kms yubikey:pin-value=123456 --key yubikey:slot-id=82 internal.example.com
'''
Generate an X5C provisioner token using a certificate in a YubiKey. Note that a
YubiKey does not support storing a certificate bundle. To make it work, you must
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,
cli.StringSliceFlag{
Name: "san",
Usage: `Add <dns|ip|email|uri> Subject Alternative Name(s) (SANs)
that should be authorized. A certificate signing request using this token must
match the complete set of SANs in the token 1:1. Use the '--san' flag multiple
times to configure multiple SANs.`,
},
cli.StringSliceFlag{
Name: "principal,n",
Usage: `Add the principals (user or host <name>s) that the token is authorized to
request. The signing request using this token won't be able to add
extra names. Use the '--principal' flag multiple times to configure
multiple principals.`,
},
sshHostFlag,
flags.CaConfig,
flags.Force,
cli.StringFlag{
Name: "not-before",
Usage: `The <time|duration> when the token's validity period starts. If a <time> is
used it is expected to be in RFC 3339 format. If a <duration> is used, it is a
sequence of decimal numbers, each with optional fraction and a unit suffix, such
as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms",
"s", "m", "h".`,
},
cli.StringFlag{
Name: "not-after",
Usage: `The <time|duration> when the token's validity period ends. If a <time> is
used it is expected to be in RFC 3339 format. If a <duration> is used, it is a
sequence of decimal numbers, each with optional fraction and a unit suffix, such
as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms",
"s", "m", "h".`,
},
flags.CertNotAfter,
flags.CertNotBefore,
flags.Provisioner,
flags.PasswordFile,
flags.ProvisionerPasswordFile,
flags.KMSUri,
flags.X5cCert,
flags.X5cKey,
flags.X5cInsecure,
flags.SSHPOPCert,
flags.SSHPOPKey,
flags.NebulaCert,
flags.NebulaKey,
flags.Confirmation,
flags.ConfirmationFile,
cli.StringFlag{
Name: "key",
Usage: `The private key <file> used to sign the JWT. This is usually downloaded from
the certificate authority.`,
},
cli.StringFlag{
Name: "output-file",
Usage: "The destination <file> of the generated one-time token.",
},
cli.BoolFlag{
Name: "revoke",
Usage: `Create a token for authorizing 'Revoke' requests. The audience will
be invalid for any other API request.`,
},
cli.BoolFlag{
Name: "renew",
Usage: `Create a token for authorizing 'renew' requests. The audience will
be invalid for any other API request.`,
},
cli.BoolFlag{
Name: "rekey",
Usage: `Create a token for authorizing 'rekey' requests. The audience will
be invalid for any other API request.`,
},
cli.BoolFlag{
Name: "ssh",
Usage: `Create a token for authorizing an SSH certificate signing request.`,
},
flags.K8sSATokenPathFlag,
flags.Offline,
flags.CaURL,
flags.Root,
flags.Context,
flags.TemplateSet,
flags.TemplateSetFile,
},
}
}
func tokenAction(ctx *cli.Context) error {
if err := errs.NumberOfArguments(ctx, 1); err != nil {
return err
}
subject := ctx.Args().Get(0)
outputFile := ctx.String("output-file")
offline := ctx.Bool("offline")
// x.509 flags
sans := ctx.StringSlice("san")
isRevoke := ctx.Bool("revoke")
isRenew := ctx.Bool("renew")
isRekey := ctx.Bool("rekey")
// ssh flags
isSSH := ctx.Bool("ssh")
isHost := ctx.Bool("host")
principals := ctx.StringSlice("principal")
// confirmation claims
cnfFile := ctx.String("cnf-file")
cnf := ctx.String("cnf")
switch {
case isSSH && len(sans) > 0:
return errs.IncompatibleFlagWithFlag(ctx, "ssh", "san")
case isHost && len(sans) > 0:
return errs.IncompatibleFlagWithFlag(ctx, "host", "san")
case len(principals) > 0 && len(sans) > 0:
return errs.IncompatibleFlagWithFlag(ctx, "principal", "san")
case !isSSH && isHost:
return errs.RequiredWithFlag(ctx, "host", "ssh")
case !isSSH && len(principals) > 0:
return errs.RequiredWithFlag(ctx, "principal", "ssh")
case cnfFile != "" && cnf != "":
return errs.IncompatibleFlagWithFlag(ctx, "cnf-file", "cnf")
}
// Default token type is always a 'Sign' token.
var typ int
if isSSH {
switch {
case isRevoke:
typ = cautils.SSHRevokeType
case isRenew:
typ = cautils.SSHRenewType
case isRekey:
typ = cautils.SSHRekeyType
case isHost:
typ = cautils.SSHHostSignType
sans = principals
default:
typ = cautils.SSHUserSignType
sans = principals
}
} else {
switch {
case isRevoke:
typ = cautils.RevokeType
case isRenew:
typ = cautils.RenewType
default:
typ = cautils.SignType
}
}
caURL, err := flags.ParseCaURL(ctx)
if err != nil {
return err
}
root := ctx.String("root")
if root == "" {
root = pki.GetRootCAPath()
if _, err := os.Stat(root); err != nil {
return errs.RequiredFlag(ctx, "root")
}
}
// Add options to create a confirmation claim if a CSR or SSH public key is
// passed.
var tokenOpts []cautils.Option
if cnfFile != "" {
in, err := utils.ReadFile(cnfFile)
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))
}
} else if cnf != "" {
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 {
return errs.InvalidFlagValue(ctx, "not-before", ctx.String("not-before"), "")
}
notAfter, ok := flags.ParseTimeOrDuration(ctx.String("not-after"))
if !ok {
return errs.InvalidFlagValue(ctx, "not-after", ctx.String("not-after"), "")
}
// parse certificates durations
certNotBefore, err := api.ParseTimeDuration(ctx.String("cert-not-before"))
if err != nil {
return errs.InvalidFlagValue(ctx, "cert-not-before", ctx.String("cert-not-before"), "")
}
certNotAfter, err := api.ParseTimeDuration(ctx.String("cert-not-after"))
if err != nil {
return errs.InvalidFlagValue(ctx, "cert-not-after", ctx.String("cert-not-after"), "")
}
var token string
if offline {
token, err = cautils.OfflineTokenFlow(ctx, typ, subject, sans, notBefore, notAfter, certNotBefore, certNotAfter)
if err != nil {
return err
}
} else {
token, err = cautils.NewTokenFlow(ctx, typ, subject, sans, caURL, root, notBefore, notAfter, certNotBefore, certNotAfter, tokenOpts...)
if err != nil {
return err
}
}
if outputFile != "" {
return fileutil.WriteFile(outputFile, []byte(token), 0o600)
}
fmt.Println(token)
return nil
}