Skip to content

Commit

Permalink
Merge pull request #689 from smallstep/plugins
Browse files Browse the repository at this point in the history
Add initial support for executing command plugins
  • Loading branch information
maraino committed Jul 8, 2022
2 parents 36a075e + 3fbbd78 commit 81b6571
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 38 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -17,6 +17,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Security
---

## [Unreleased]
### Added
- Initial support for `step` plugins. A plugin is an executable file named with
the format step-`name`-plugin, located in the `$PATH` or the
`$STEPPATH/plugins` directory. These plugins will be executed using `step
name`.
- Integration of [`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin)
on `step certificate create` and `step certificate sign`.

## [0.21.0] - 2022-07-06
### Added
- Device Authorization Grant flow for input constrained devices needing OAuth
Expand Down
16 changes: 16 additions & 0 deletions cmd/step/main.go
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/smallstep/certificates/ca"
"github.com/smallstep/cli/command/version"
"github.com/smallstep/cli/internal/plugin"
"github.com/smallstep/cli/usage"
"github.com/urfave/cli"
"go.step.sm/cli-utils/command"
Expand Down Expand Up @@ -89,6 +90,21 @@ func main() {
Usage: "path to the config file to use for CLI flags",
})

// Action runs on `step` or `step <command>` if the command is not enabled.
app.Action = func(ctx *cli.Context) error {
args := ctx.Args()
if name := args.First(); name != "" {
if file, err := plugin.LookPath(name); err == nil {
return plugin.Run(ctx, file)
}
if u := plugin.GetURL(name); u != "" {
return fmt.Errorf("The plugin %q is not it in your system.\nDownload it from %s", name, u)
}
return cli.ShowCommandHelp(ctx, name)
}
return cli.ShowAppHelp(ctx)
}

// All non-successful output should be written to stderr
app.Writer = os.Stdout
app.ErrWriter = os.Stderr
Expand Down
75 changes: 56 additions & 19 deletions command/certificate/create.go
Expand Up @@ -7,14 +7,15 @@ import (
"time"

"github.com/pkg/errors"
"github.com/smallstep/cli/crypto/keys"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/flags"
"github.com/smallstep/cli/internal/cryptoutil"
"github.com/smallstep/cli/utils"
"github.com/urfave/cli"
"go.step.sm/cli-utils/command"
"go.step.sm/cli-utils/errs"
"go.step.sm/cli-utils/ui"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
)

Expand All @@ -40,7 +41,7 @@ func createCommand() cli.Command {
Action: command.ActionFunc(createAction),
Usage: "create a certificate or certificate signing request",
UsageText: `**step certificate create** <subject> <crt-file> <key-file>
[**--csr**] [**--profile**=<profile>] [**--template**=<file>]
[**--kms**=<uri>] [**--csr**] [**--profile**=<profile>] [**--template**=<file>]
[**--not-before**=<duration>] [**--not-after**=<duration>]
[**--password-file**=<file>] [**--ca**=<issuer-cert>]
[**--ca-key**=<issuer-key>] [**--ca-password-file**=<file>]
Expand Down Expand Up @@ -310,8 +311,35 @@ $ cat csr.tpl
}
$ step certificate create --csr --template csr.tpl --san coyote@acme.corp \
"Wile E. Coyote" coyote.csr coyote.key
'''`,
'''
Create a root certificate using <step-kms-plugin>:
'''
$ step kms create \
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
'pkcs11:id=4000;object=root-key'
$ step certificate create \
--profile root-ca \
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
--key 'pkcs11:id=4000' \
'KMS Root' root_ca.crt
'''
Create an intermediate certificate using <step-kms-plugin>:
'''
$ step kms create \
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
'pkcs11:id=4001;object=intermediate-key'
$ step certificate create \
--profile intermediate-ca \
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
--ca root_ca.crt --ca-key 'pkcs11:id=4000' \
--key 'pkcs11:id=4001' \
'My KMS Intermediate' intermediate_ca.crt
'''
`,
Flags: []cli.Flag{
flags.KMSUri,
cli.BoolFlag{
Name: "csr",
Usage: `Generate a certificate signing request (CSR) instead of a certificate.`,
Expand Down Expand Up @@ -636,7 +664,7 @@ func createAction(ctx *cli.Context) error {
}

// Save key and certificate request
if keyFile != "" {
if keyFile != "" && !cryptoutil.IsKMSSigner(priv) {
if err := savePrivateKey(ctx, keyFile, priv, noPass); err != nil {
return err
}
Expand All @@ -655,15 +683,18 @@ func createAction(ctx *cli.Context) error {
}

func parseOrCreateKey(ctx *cli.Context) (crypto.PublicKey, crypto.Signer, error) {
keyFile := ctx.String("key")
var (
kms = ctx.String("kms")
keyFile = ctx.String("key")
)

// Validate key parameters and generate key pair
if keyFile == "" {
kty, crv, size, err := utils.GetKeyDetailsFromCLI(ctx, ctx.Bool("insecure"), "kty", "curve", "size")
if err != nil {
return nil, nil, err
}
pub, priv, err := keys.GenerateKeyPair(kty, crv, size)
pub, priv, err := keyutil.GenerateKeyPair(kty, crv, size)
if err != nil {
return nil, nil, err
}
Expand All @@ -684,19 +715,22 @@ func parseOrCreateKey(ctx *cli.Context) (crypto.PublicKey, crypto.Signer, error)
return nil, nil, errs.IncompatibleFlag(ctx, "key", "size")
}

ops := []pemutil.Options{}
opts := []pemutil.Options{}
passFile := ctx.String("password-file")
if passFile != "" {
ops = append(ops, pemutil.WithPasswordFile(passFile))
opts = append(opts, pemutil.WithPasswordFile(passFile))
}
v, err := pemutil.Read(keyFile, ops...)

signer, err := cryptoutil.CreateSigner(kms, keyFile, opts...)
if err != nil {
return nil, nil, err
}
signer, ok := v.(crypto.Signer)
if !ok {
return nil, nil, errors.Errorf("file %s does not contain a valid private key", keyFile)

// Make sure we can sign X509 certificates with it.
if !cryptoutil.IsX509Signer(signer) {
return nil, nil, errs.InvalidFlagValueMsg(ctx, "key", keyFile, "the given key cannot sign X509 certificates")
}

return signer.Public(), signer, nil
}

Expand All @@ -709,6 +743,7 @@ func parseSigner(ctx *cli.Context, defaultSigner crypto.Signer) (*x509.Certifica
caKey = ctx.String("ca-key")
profile = ctx.String("profile")
template = ctx.String("template")
kms = ctx.String("kms")
)

// Check required flags when profile is used.
Expand Down Expand Up @@ -754,17 +789,19 @@ func parseSigner(ctx *cli.Context, defaultSigner crypto.Signer) (*x509.Certifica

// Parse --ca-key as a crypto.Signer.
passFile := ctx.String("ca-password-file")
ops := []pemutil.Options{}
opts := []pemutil.Options{}
if passFile != "" {
ops = append(ops, pemutil.WithPasswordFile(passFile))
opts = append(opts, pemutil.WithPasswordFile(passFile))
}
key, err := pemutil.Read(caKey, ops...)

signer, err := cryptoutil.CreateSigner(kms, caKey, opts...)
if err != nil {
return nil, nil, err
}
signer, ok := key.(crypto.Signer)
if !ok {
return nil, nil, errors.Errorf("invalid value '%s' for flag '--ca-key': file is not a valid private key", caKey)

// Make sure we can sign X509 certificates with it.
if !cryptoutil.IsX509Signer(signer) {
return nil, nil, errs.InvalidFlagValueMsg(ctx, "ca-key", caKey, "the given key cannot sign X509 certificates")
}

return cert, signer, nil
Expand Down
39 changes: 24 additions & 15 deletions command/certificate/sign.go
@@ -1,7 +1,6 @@
package certificate

import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
Expand All @@ -14,6 +13,7 @@ import (

"github.com/pkg/errors"
"github.com/smallstep/cli/flags"
"github.com/smallstep/cli/internal/cryptoutil"
"github.com/smallstep/cli/utils"
"github.com/urfave/cli"
"go.step.sm/cli-utils/errs"
Expand Down Expand Up @@ -110,8 +110,17 @@ $ cat coyote.tpl
}
$ step certificate create --csr coyote@acme.corp coyote.csr coyote.key
$ step certificate sign --template coyote.tpl coyote.csr issuer.crt issuer.key
'''`,
'''
Sign a CSR using <step-kms-plugin>:
'''
$ step certificate sign \
--kms 'pkcs11:module-path=/usr/local/lib/softhsm/libsofthsm2.so;token=smallstep?pin-value=password' \
leaf.csr issuer.crt 'pkcs11:id=4001'
'''
`,
Flags: []cli.Flag{
flags.KMSUri,
cli.StringFlag{
Name: "profile",
Value: profileLeaf,
Expand Down Expand Up @@ -188,24 +197,24 @@ func signAction(ctx *cli.Context) error {
if err != nil {
return err
}
ops := []pemutil.Options{}
opts := []pemutil.Options{}
passFile := ctx.String("password-file")
if passFile == "" {
ops = append(ops, pemutil.WithPasswordPrompt(
opts = append(opts, pemutil.WithPasswordPrompt(
fmt.Sprintf("Please enter the password to decrypt %s", keyFile),
func(s string) ([]byte, error) {
return ui.PromptPassword(s)
}))
} else {
ops = append(ops, pemutil.WithPasswordFile(passFile))
opts = append(opts, pemutil.WithPasswordFile(passFile))
}
key, err := pemutil.Read(keyFile, ops...)

signer, err := cryptoutil.CreateSigner(ctx.String("kms"), keyFile, opts...)
if err != nil {
return err
}
signer, ok := key.(crypto.Signer)
if !ok {
return errors.Errorf("key in %s does not satisfy the crypto.Signer interface", keyFile)
if !cryptoutil.IsX509Signer(signer) {
return errors.Errorf("the key %q cannot be used to sign X509 certificates", keyFile)
}
if err := validateIssuerKey(issuers[0], signer); err != nil {
return err
Expand Down Expand Up @@ -322,27 +331,27 @@ func signAction(ctx *cli.Context) error {
func validateIssuerKey(crt *x509.Certificate, signer crypto.Signer) error {
switch pub := crt.PublicKey.(type) {
case *rsa.PublicKey:
priv, ok := signer.(*rsa.PrivateKey)
pk, ok := signer.Public().(*rsa.PublicKey)
if !ok {
return errors.New("private key type does not match issuer public key type")
}
if pub.N.Cmp(priv.N) != 0 {
if !pub.Equal(pk) {
return errors.New("private key does not match issuer public key")
}
case *ecdsa.PublicKey:
priv, ok := signer.(*ecdsa.PrivateKey)
pk, ok := signer.Public().(*ecdsa.PublicKey)
if !ok {
return errors.New("private key type does not match issuer public key type")
}
if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 {
if !pub.Equal(pk) {
return errors.New("private key does not match issuer public key")
}
case ed25519.PublicKey:
priv, ok := signer.(ed25519.PrivateKey)
pk, ok := signer.Public().(ed25519.PublicKey)
if !ok {
return errors.New("private key type does not match issuer public key type")
}
if !bytes.Equal(priv.Public().(ed25519.PublicKey), pub) {
if !pub.Equal(pk) {
return errors.New("private key does not match issuer public key")
}
default:
Expand Down
9 changes: 7 additions & 2 deletions flags/flags.go
Expand Up @@ -379,13 +379,18 @@ flag exists so it can be configured in $STEPPATH/config/defaults.json.`,
// EABKeyID is a cli.Flag that points to an ACME EAB Key ID
EABKeyID = cli.StringFlag{
Name: "eab-key-id",
Usage: "An ACME EAB Key ID",
Usage: "An ACME EAB Key ID.",
}

// EABReference is a cli.Flag that points to an ACME EAB Key Reference
EABReference = cli.StringFlag{
Name: "eab-key-reference",
Usage: "An ACME EAB Key Reference",
Usage: "An ACME EAB Key Reference.",
}

KMSUri = cli.StringFlag{
Name: "kms",
Usage: "The <uri> to configure a Cloud KMS or an HSM.",
}
)

Expand Down

0 comments on commit 81b6571

Please sign in to comment.