1
0
mirror of https://github.com/smallstep/cli.git synced 2026-10-29 00:17:23 +03:00
Files
step-ca-cli/command/crypto/key/format.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

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)
}
}