You've already forked step-ca-cli
mirror of
https://github.com/smallstep/cli.git
synced 2026-10-29 00:17:23 +03:00
* Remove unnecessary utils files in favor of cli-utils package * Remove nolint:revive directive from bcrypt-pbkdf file
452 lines
12 KiB
Go
452 lines
12 KiB
Go
package key
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"os"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/urfave/cli"
|
|
|
|
"github.com/smallstep/cli-utils/errs"
|
|
"github.com/smallstep/cli-utils/fileutil"
|
|
"github.com/smallstep/cli-utils/ui"
|
|
"go.step.sm/crypto/jose"
|
|
"go.step.sm/crypto/pemutil"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/smallstep/cli/flags"
|
|
"github.com/smallstep/cli/utils"
|
|
)
|
|
|
|
func formatCommand() cli.Command {
|
|
return cli.Command{
|
|
Name: "format",
|
|
Action: cli.ActionFunc(formatAction),
|
|
Usage: `reformat a public or private key`,
|
|
UsageText: `**step crypto key format** <key-file> [**--out**=<file>]`,
|
|
Description: `**step crypto key format** prints or writes the key in
|
|
a different format.
|
|
|
|
By default PEM formatted keys will be converted to DER with the following rules:
|
|
|
|
* ECDSA, RSA, AND Ed25519 public keys will use the DER-encoded PKIX format.
|
|
* ECDSA, AND RSA private keys will use the ASN.1, DER format.
|
|
* Ed25519 private keys will use the DER-encoded PKCS8 encoded form.
|
|
|
|
And DER encoded keys will be converted to PEM with the following rules:
|
|
|
|
* ECDSA, RSA, AND Ed25519 public keys will use the PEM-encoded PKIX format.
|
|
* ECDSA private keys will use the PEM-encoded format defined in RFC 5915 and
|
|
SEC1.
|
|
* RSA private keys will use the PEM-encoded PKCS#1 format.
|
|
* Ed25519 private keys will use the PEM-encoded PKCS#8 format.
|
|
|
|
The flags **--pkcs8**, **--pem**, **--der**, **--ssh**, and **--jwk** can be use
|
|
to change the previous defaults. For example we can use **--pkcs8** to save a
|
|
PKCS#1 RSA key to the PKCS#8 form. Or we can combine **--pem** and **--pkcs8**
|
|
to convert to PKCS#8 a PEM file.
|
|
|
|
## POSITIONAL ARGUMENTS
|
|
|
|
<key-file>
|
|
: Path to a file with a public or private key, or the public key of an
|
|
X.509 certificate.
|
|
|
|
## EXIT CODES
|
|
|
|
This command returns 0 on success and \>0 if any error occurs.
|
|
|
|
## EXAMPLES
|
|
|
|
Convert a PEM file to DER:
|
|
'''
|
|
$ step crypto key format key.pem
|
|
'''
|
|
|
|
Convert DER file to PEM:
|
|
'''
|
|
$ step crypto key format key.der
|
|
'''
|
|
|
|
Convert a PEM file to OpenSSH:
|
|
'''
|
|
$ step crypto key format --ssh key.pem
|
|
'''
|
|
|
|
Convert a PEM file to JWK:
|
|
'''
|
|
$ step crypto key format --jwk key.pem
|
|
'''
|
|
|
|
Convert PEM file to DER and write to disk:
|
|
'''
|
|
$ step crypto key format key.pem --out key.der
|
|
'''
|
|
|
|
Convert a PKCS#1 RSA private key to PKCS#8 using the PEM format:
|
|
'''
|
|
$ step crypto key format --pem --pkcs8 rsa.pem --out rsa-pkcs8.pem
|
|
'''
|
|
|
|
Convert PKCS#8 RSA private key to the PKCS#1 format:
|
|
'''
|
|
$ step crypto key format --pem rsa-pkcs8.pem --out rsa.pem
|
|
'''
|
|
|
|
Convert an ASN.1 DER format to the PEM-encoded PKCS#8 format:
|
|
'''
|
|
$ step crypto key format --pkcs8 key.der --out key-pkcs8.der
|
|
'''
|
|
|
|
Convert an ASN.1 DER format to the DER-encoded PKCS#8 format:
|
|
'''
|
|
$ step crypto key format --der --pkcs8 key.der --out key-pkcs8.der
|
|
'''`,
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "pkcs8",
|
|
Usage: "Convert RSA and ECDSA private keys to PKCS#8 PEM/DER format.",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "pem",
|
|
Usage: `Uses PEM as the result encoding format. If neither **--pem** nor **--der** nor
|
|
**--ssh** nor **--jwk** are set it will always switch to the DER format.`,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "der",
|
|
Usage: `Uses DER as the result enconfig format. If neither **--pem** nor **--der** nor
|
|
**--ssh** nor **--jwk** are set it will always switch to the PEM format.`,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "ssh",
|
|
Usage: `Uses OpenSSH as the result encoding format.`,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "jwk",
|
|
Usage: `Uses JSON Web Key as the result encoding format.`,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "out",
|
|
Usage: "Path to write the reformatted result.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "password-file",
|
|
Usage: "Location of file containing passphrase to decrypt private key.",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "no-password",
|
|
Usage: `Do not ask for a password to encrypt a private key with PEM format. Sensitive
|
|
key material will be written to disk unencrypted. This is not recommended.
|
|
Requires **--insecure** flag.`,
|
|
},
|
|
flags.Insecure,
|
|
flags.Force,
|
|
},
|
|
}
|
|
}
|
|
|
|
func formatAction(ctx *cli.Context) error {
|
|
if err := errs.MinMaxNumberOfArguments(ctx, 0, 1); err != nil {
|
|
return err
|
|
}
|
|
|
|
var keyFile string
|
|
switch ctx.NArg() {
|
|
case 0:
|
|
keyFile = "-"
|
|
case 1:
|
|
keyFile = ctx.Args().First()
|
|
default:
|
|
return errs.TooManyArguments(ctx)
|
|
}
|
|
|
|
var (
|
|
out = ctx.String("out")
|
|
toPEM = ctx.Bool("pem")
|
|
toDER = ctx.Bool("der")
|
|
toSSH = ctx.Bool("ssh")
|
|
toJWK = ctx.Bool("jwk")
|
|
noPassword = ctx.Bool("no-password")
|
|
insecure = ctx.Bool("insecure")
|
|
key interface{}
|
|
ob []byte
|
|
)
|
|
|
|
switch {
|
|
case toPEM && toDER:
|
|
return errs.IncompatibleFlagWithFlag(ctx, "pem", "der")
|
|
case toPEM && toSSH:
|
|
return errs.IncompatibleFlagWithFlag(ctx, "pem", "ssh")
|
|
case toPEM && toJWK:
|
|
return errs.IncompatibleFlagWithFlag(ctx, "pem", "jwk")
|
|
case toDER && toSSH:
|
|
return errs.IncompatibleFlagWithFlag(ctx, "der", "ssh")
|
|
case toDER && toJWK:
|
|
return errs.IncompatibleFlagWithFlag(ctx, "der", "jwk")
|
|
case toSSH && toJWK:
|
|
return errs.IncompatibleFlagWithFlag(ctx, "ssh", "jwk")
|
|
}
|
|
|
|
// --no-password requires --insecure
|
|
if noPassword && !insecure {
|
|
return errs.RequiredInsecureFlag(ctx, "no-password")
|
|
}
|
|
|
|
b, err := utils.ReadFile(keyFile)
|
|
if err != nil {
|
|
return errs.FileError(err, keyFile)
|
|
}
|
|
|
|
switch {
|
|
case bytes.HasPrefix(b, []byte("-----BEGIN ")): // PEM format:
|
|
opts := []pemutil.Options{pemutil.WithFilename(keyFile)}
|
|
if passFile := ctx.String("password-file"); passFile != "" {
|
|
opts = append(opts, pemutil.WithPasswordFile(passFile))
|
|
}
|
|
if key, err = pemutil.Parse(b, opts...); err != nil {
|
|
return err
|
|
}
|
|
// convert to DER if not specified
|
|
if !toPEM && !toDER && !toSSH && !toJWK {
|
|
toDER = true
|
|
}
|
|
case isSSHPublicKey(b):
|
|
if key, err = pemutil.ParseSSH(b); err != nil {
|
|
return err
|
|
}
|
|
// convert to PEM if not specified
|
|
if !toPEM && !toDER && !toSSH && !toJWK {
|
|
toPEM = true
|
|
}
|
|
case isJWK(b):
|
|
if key, err = parseJWK(ctx, b); err != nil {
|
|
return err
|
|
}
|
|
// convert to PEM if not specified
|
|
if !toPEM && !toDER && !toSSH && !toJWK {
|
|
toPEM = true
|
|
}
|
|
default: // assuming DER format
|
|
if key, err = pemutil.ParseDER(b); err != nil {
|
|
return err
|
|
}
|
|
// convert to PEM if not specified
|
|
if !toPEM && !toDER && !toSSH && !toJWK {
|
|
toPEM = true
|
|
}
|
|
}
|
|
|
|
// If it's a certificate grab it's public key
|
|
if cert, ok := key.(*x509.Certificate); ok {
|
|
key = cert.PublicKey
|
|
}
|
|
|
|
switch {
|
|
case toPEM:
|
|
if ob, err = convertToPEM(ctx, key); err != nil {
|
|
return err
|
|
}
|
|
case toDER:
|
|
if ob, err = convertToDER(ctx, key); err != nil {
|
|
return err
|
|
}
|
|
case toSSH:
|
|
if ob, err = convertToSSH(ctx, key); err != nil {
|
|
return err
|
|
}
|
|
case toJWK:
|
|
if ob, err = convertToJWK(ctx, key); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New("error formatting key: it should not get here")
|
|
}
|
|
|
|
if out == "" {
|
|
os.Stdout.Write(ob)
|
|
} else {
|
|
info, err := os.Stat(keyFile)
|
|
if err != nil {
|
|
return errs.FileError(err, keyFile)
|
|
}
|
|
if err := fileutil.WriteFile(out, ob, info.Mode()); err != nil {
|
|
return errs.FileError(err, out)
|
|
}
|
|
ui.Printf("Your key has been saved in %s.\n", out)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isSSHPublicKey(in []byte) bool {
|
|
switch {
|
|
case bytes.HasPrefix(in, []byte(ssh.KeyAlgoRSA)),
|
|
bytes.HasPrefix(in, []byte(ssh.InsecureKeyAlgoDSA)), //nolint:staticcheck // compatibility with older tooling
|
|
bytes.HasPrefix(in, []byte(ssh.KeyAlgoECDSA256)),
|
|
bytes.HasPrefix(in, []byte(ssh.KeyAlgoECDSA384)),
|
|
bytes.HasPrefix(in, []byte(ssh.KeyAlgoECDSA521)),
|
|
bytes.HasPrefix(in, []byte(ssh.KeyAlgoED25519)):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isJWK(in []byte) bool {
|
|
if bytes.HasPrefix(in, []byte("{")) {
|
|
return true
|
|
}
|
|
if _, err := jose.ParseEncrypted(string(in)); err == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseJWK(ctx *cli.Context, b []byte) (interface{}, error) {
|
|
// Decrypt key if encrypted.
|
|
if _, err := jose.ParseEncrypted(string(b)); err == nil {
|
|
opts := []jose.Option{
|
|
jose.WithPasswordPrompter("Please enter the password to decrypt the key", func(s string) ([]byte, error) {
|
|
return ui.PromptPassword(s)
|
|
}),
|
|
}
|
|
if passFile := ctx.String("password-file"); passFile != "" {
|
|
opts = append(opts, jose.WithPasswordFile(passFile))
|
|
}
|
|
b, err = jose.Decrypt(b, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Parse decrypted key
|
|
var jwk jose.JSONWebKey
|
|
if err := json.Unmarshal(b, &jwk); err != nil {
|
|
return nil, errors.Wrap(err, "error unmarshaling key")
|
|
}
|
|
if jwk.Key == nil {
|
|
return nil, errors.New("error parsing key: not found")
|
|
}
|
|
|
|
return jwk.Key, nil
|
|
}
|
|
|
|
func convertToPEM(ctx *cli.Context, key interface{}) (b []byte, err error) {
|
|
opts := []pemutil.Options{
|
|
pemutil.WithPKCS8(ctx.Bool("pkcs8")),
|
|
}
|
|
|
|
if !ctx.Bool("no-password") {
|
|
switch key.(type) {
|
|
case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey:
|
|
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
|
|
if passFile := ctx.String("password-file"); passFile != "" {
|
|
opts = append(opts, pemutil.WithPasswordFile(passFile))
|
|
} else {
|
|
opts = append(opts, pemutil.WithPasswordPrompt("Please enter the password to encrypt the private key", func(s string) ([]byte, error) {
|
|
return ui.PromptPassword(s, ui.WithValidateNotEmpty())
|
|
}))
|
|
}
|
|
default:
|
|
return nil, errors.Errorf("unsupported key type %T", key)
|
|
}
|
|
}
|
|
|
|
block, err := pemutil.Serialize(key, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pem.EncodeToMemory(block), nil
|
|
}
|
|
|
|
func convertToDER(ctx *cli.Context, key interface{}) (b []byte, err error) {
|
|
switch k := key.(type) {
|
|
case *rsa.PrivateKey:
|
|
if ctx.Bool("pkcs8") {
|
|
b, err = x509.MarshalPKCS8PrivateKey(key)
|
|
} else {
|
|
b = x509.MarshalPKCS1PrivateKey(k)
|
|
}
|
|
case *ecdsa.PrivateKey:
|
|
if ctx.Bool("pkcs8") {
|
|
b, err = x509.MarshalPKCS8PrivateKey(key)
|
|
} else {
|
|
b, err = x509.MarshalECPrivateKey(k)
|
|
}
|
|
case ed25519.PrivateKey: // always PKCS#8
|
|
b, err = x509.MarshalPKCS8PrivateKey(key)
|
|
case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey: // always PKIX
|
|
b, err = x509.MarshalPKIXPublicKey(key)
|
|
default:
|
|
return nil, errors.Errorf("unsupported key type %T", key)
|
|
}
|
|
return
|
|
}
|
|
|
|
func convertToSSH(ctx *cli.Context, key interface{}) ([]byte, error) {
|
|
switch key.(type) {
|
|
case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey:
|
|
k, err := ssh.NewPublicKey(key)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error converting public key")
|
|
}
|
|
return ssh.MarshalAuthorizedKey(k), nil
|
|
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
|
|
opts := []pemutil.Options{
|
|
pemutil.WithOpenSSH(true),
|
|
}
|
|
if !ctx.Bool("no-password") {
|
|
if passFile := ctx.String("password-file"); passFile != "" {
|
|
opts = append(opts, pemutil.WithPasswordFile(passFile))
|
|
} else {
|
|
opts = append(opts, pemutil.WithPasswordPrompt("Please enter the password to encrypt the private key", func(s string) ([]byte, error) {
|
|
return ui.PromptPassword(s, ui.WithValidateNotEmpty())
|
|
}))
|
|
}
|
|
}
|
|
block, err := pemutil.Serialize(key, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pem.EncodeToMemory(block), nil
|
|
default:
|
|
return nil, errors.Errorf("unsupported key type %T", key)
|
|
}
|
|
}
|
|
|
|
func convertToJWK(ctx *cli.Context, key interface{}) ([]byte, error) {
|
|
b, err := json.Marshal(&jose.JSONWebKey{Key: key})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch key.(type) {
|
|
case *ecdsa.PublicKey, *rsa.PublicKey, ed25519.PublicKey:
|
|
return b, nil
|
|
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
|
|
if ctx.Bool("no-password") {
|
|
return b, nil
|
|
}
|
|
|
|
opts := []jose.Option{
|
|
jose.WithContentType("jwk+json"),
|
|
}
|
|
if passFile := ctx.String("password-file"); passFile != "" {
|
|
opts = append(opts, jose.WithPasswordFile(passFile))
|
|
}
|
|
jwe, err := jose.Encrypt(b, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(jwe.FullSerialize()), nil
|
|
default:
|
|
return nil, errors.Errorf("unsupported key type %T", key)
|
|
}
|
|
}
|