diff --git a/command/certificate/format.go b/command/certificate/format.go index 313dab967..4446f47a3 100644 --- a/command/certificate/format.go +++ b/command/certificate/format.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/x509" "encoding/pem" + "github.com/smallstep/cli/crypto/pemutil" "os" "github.com/pkg/errors" @@ -13,23 +14,32 @@ import ( "github.com/urfave/cli" "go.step.sm/cli-utils/command" "go.step.sm/cli-utils/errs" + + "software.sslmate.com/src/go-pkcs12" ) func formatCommand() cli.Command { return cli.Command{ - Name: "format", - Action: command.ActionFunc(formatAction), - Usage: `reformat certificate`, - UsageText: `**step certificate format** [**--out**=]`, + Name: "format", + Action: command.ActionFunc(formatAction), + Usage: `reformat certificate`, + UsageText: `**step certificate format** [**--crt**=] [**--key**=] +[**--ca**=] [**--out**=] [**--format**=]`, Description: `**step certificate format** prints the certificate or CSR in a different format. -Only 2 formats are currently supported; PEM and ASN.1 DER. This tool will convert +If either PEM or ASN.1 DER is provided as a positional argument, this tool will convert a certificate or CSR in one format to the other. +If PFX / PKCS12 file is provided as a positional argument, and the format is specified as "pem"/"der", +it extracts a certificate and private key from the input. + +If either PEM or ASN.1 DER is provided in "--crt", "--key" and "--ca", and the format is specified as "p12", +it creates PFX / PKCS12 file from the input . + ## POSITIONAL ARGUMENTS -: Path to a certificate or CSR file. +: Path to a certificate or CSR file, or .p12 file when you specify --crt/--ca option. ## EXIT CODES @@ -51,12 +61,60 @@ Convert PEM format to DER and write to disk: ''' $ step certificate format foo.pem --out foo.der ''' + +Convert a .p12 file to a certificate and private key: + +''' +$ step certificate format foo.p12 --crt foo.crt --key foo.key --format pem +''' + +Convert a .p12 file to a certificate, private key and intermediate certificates: + +''' +$ step certificate format foo.p12 --crt foo.crt --key foo.key --ca intermediate.crt --format pem +''' + +Convert a certificate and private key to a .p12 file: + +''' +$ step certificate format foo.crt --crt foo.p12 --key foo.key --format p12 +''' + +Convert a certificate, a private key, and intermediate certificates to a .p12 file: + +''' +$ step certificate format foo.crt --crt foo.p12 --key foo.key --ca intermediate.crt --format p12 +''' `, Flags: []cli.Flag{ + cli.StringFlag{ + Name: "format", + Usage: `Target format.`, + }, + cli.StringFlag{ + Name: "crt", + Usage: `The path to a certificate.`, + }, + cli.StringFlag{ + Name: "key", + Usage: `The path to a private key.`, + }, + cli.StringSliceFlag{ + Name: "ca", + Usage: `The path a CA or intermediate certificate. When converting certificates +to p12 file, Use the '--ca' flag multiple times to add +multiple CAs or intermediates.`, + }, cli.StringFlag{ Name: "out", Usage: `Path to write the reformatted result.`, }, + cli.StringFlag{ + Name: "password-file", + Usage: `The path to the containing the password to encrypt/decrypt the .p12 file.`, + }, + flags.NoPassword, + flags.Insecure, flags.Force, }, } @@ -67,15 +125,68 @@ func formatAction(ctx *cli.Context) error { return err } - var ( - out = ctx.String("out") - ob []byte - ) + sourceFile := ctx.Args().First() + format := ctx.String("format") + crt := ctx.String("crt") + key := ctx.String("key") + ca := ctx.StringSlice("ca") + out := ctx.String("out") + passwordFile := ctx.String("password-file") + noPassword := ctx.Bool("no-password") + insecure := ctx.Bool("insecure") - var crtFile string - if ctx.NArg() == 1 { - crtFile = ctx.Args().First() - } else { + if out != "" { + if crt != "" { + return errs.IncompatibleFlagWithFlag(ctx, "out", "crt") + } + if key != "" { + return errs.IncompatibleFlagWithFlag(ctx, "out", "key") + } + if len(ca) != 0 { + return errs.IncompatibleFlagWithFlag(ctx, "out", "ca") + } + if format != "" { + return errs.IncompatibleFlagWithFlag(ctx, "out", "format") + } + } + + if passwordFile != "" && noPassword { + return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file") + } + + switch { + case format == "pem" || format == "der": + if len(ca) > 1 { + return errors.Errorf("--ca option specified for multiple times when the target format is pem/der") + } + caFile := "" + if len(ca) == 1 { + caFile = ca[0] + } + if err := fromP12(sourceFile, crt, key, caFile, passwordFile, noPassword, format); err != nil { + return err + } + case format == "p12": + if noPassword && !insecure { + return errs.RequiredInsecureFlag(ctx, "no-password") + } + if err := ToP12(crt, sourceFile, key, ca, passwordFile, noPassword, insecure); err != nil { + return err + } + case format == "": + if err := interconvertPemAndDer(sourceFile, out); err != nil { + return err + } + default: + return errors.Errorf("unrecognized argument: --format %s", format) + } + return nil +} + +func interconvertPemAndDer(crtFile, out string) error { + var ob []byte + + if crtFile == "" { crtFile = "-" } @@ -144,6 +255,33 @@ func decodeCertificatePem(b []byte) ([]byte, error) { return nil, errors.Wrap(err, "error parsing certificate request") } return csr.Raw, nil + case "RSA PRIVATE KEY": + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing RSA private key") + } + keyBytes := x509.MarshalPKCS1PrivateKey(key) + return keyBytes, nil + case "EC PRIVATE KEY": + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing EC private key") + } + keyBytes, err := x509.MarshalECPrivateKey(key) + if err != nil { + return nil, errors.Wrap(err, "error converting EC private key to DER format") + } + return keyBytes, nil + case "PRIVATE KEY": + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing private key") + } + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, errors.Wrap(err, "error converting private key to DER format") + } + return keyBytes, nil default: continue } @@ -151,3 +289,106 @@ func decodeCertificatePem(b []byte) ([]byte, error) { return nil, errors.Errorf("error decoding certificate: invalid PEM block") } + +func fromP12(p12File, crtFile, keyFile, caFile, passwordFile string, noPassword bool, format string) error { + var err error + var password string + if passwordFile != "" { + password, err = utils.ReadStringPasswordFromFile(passwordFile) + if err != nil { + return err + } + } + + if password == "" && !noPassword { + pass, err := ui.PromptPassword("Please enter a password to decrypt the .p12 file") + if err != nil { + return errs.Wrap(err, "error reading password") + } + password = string(pass) + } + + p12Data, err := utils.ReadFile(p12File) + if err != nil { + return errs.Wrap(err, "error reading file %s", p12File) + } + + key, crt, ca, err := pkcs12.DecodeChain(p12Data, password) + if err != nil { + return errs.Wrap(err, "failed to decode PKCS12 data") + } + + if err := write(crtFile, format, crt); err != nil { + return err + } + + if err := writeCerts(caFile, format, ca); err != nil { + return err + } + + if err := write(keyFile, format, key); err != nil { + return err + } + + return nil +} + +func writeCerts(filename, format string, certs []*x509.Certificate) error { + if len(certs) > 1 && format == "der" { + return errors.Errorf("der format does not support a certificate bundle") + } + var data []byte + for _, cert := range certs { + b, err := toByte(cert, format) + if err != nil { + return err + } + data = append(data, b...) + } + if err := maybeWrite(filename, data); err != nil { + return err + } + return nil +} + +func write(filename, format string, in interface{}) error { + b, err := toByte(in, format) + if err != nil { + return err + } + if err := maybeWrite(filename, b); err != nil { + return err + } + return nil +} + +func maybeWrite(filename string, out []byte) error { + if filename == "" { + os.Stdout.Write(out) + } else { + if err := utils.WriteFile(filename, out, 0600); err != nil { + return err + } + } + return nil +} + +func toByte(in interface{}, format string) ([]byte, error) { + pemblk, err := pemutil.Serialize(in) + if err != nil { + return nil, err + } + pemByte := pem.EncodeToMemory(pemblk) + switch format { + case "der": + derByte, err := decodeCertificatePem(pemByte) + if err != nil { + return nil, err + } + return derByte, nil + case "pem", "": + return pemByte, nil + default: + return nil, errors.Errorf("unsupported format: %s", format) + } +} diff --git a/command/certificate/p12.go b/command/certificate/p12.go index cf5c4123d..7ce3c42d7 100644 --- a/command/certificate/p12.go +++ b/command/certificate/p12.go @@ -3,6 +3,7 @@ package certificate import ( "crypto/rand" "crypto/x509" + "os" "github.com/pkg/errors" "github.com/smallstep/cli/crypto/pemutil" @@ -85,6 +86,10 @@ func p12Action(ctx *cli.Context) error { caFiles := ctx.StringSlice("ca") hasKeyAndCert := crtFile != "" && keyFile != "" + passwordFile := ctx.String("password-file") + noPassword := ctx.Bool("no-password") + insecure := ctx.Bool("insecure") + // If either key or cert are provided, both must be provided if !hasKeyAndCert && (crtFile != "" || keyFile != "") { return errs.MissingArguments(ctx, "key_file") @@ -97,13 +102,20 @@ func p12Action(ctx *cli.Context) error { // Validate flags switch { - case ctx.String("password-file") != "" && ctx.Bool("no-password"): + case passwordFile != "" && noPassword: return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file") - case ctx.Bool("no-password") && !ctx.Bool("insecure"): + case noPassword && !insecure: return errs.RequiredInsecureFlag(ctx, "no-password") } - x509CAs := []*x509.Certificate{} + if err := ToP12(p12File, crtFile, keyFile, caFiles, passwordFile, noPassword, insecure); err != nil { + return err + } + return nil +} + +func ToP12(p12File, crtFile, keyFile string, caFiles []string, passwordFile string, noPassword, insecure bool) error { + var x509CAs []*x509.Certificate for _, caFile := range caFiles { x509Bundle, err := pemutil.ReadCertificateBundle(caFile) if err != nil { @@ -114,8 +126,8 @@ func p12Action(ctx *cli.Context) error { var err error var password string - if !ctx.Bool("no-password") { - if passwordFile := ctx.String("password-file"); passwordFile != "" { + if !noPassword { + if passwordFile != "" { password, err = utils.ReadStringPasswordFromFile(passwordFile) if err != nil { return err @@ -132,7 +144,7 @@ func p12Action(ctx *cli.Context) error { } var pkcs12Data []byte - if hasKeyAndCert { + if crtFile != "" && keyFile != "" { // If we have a key and certificate, we're making an identity store x509CertBundle, err := pemutil.ReadCertificateBundle(crtFile) if err != nil { @@ -146,7 +158,7 @@ func p12Action(ctx *cli.Context) error { // The first certificate in the bundle will be our server cert x509Cert := x509CertBundle[0] - // Any remaning certs will be intermediates for the server + // Any remaining certs will be intermediates for the server x509CAs = append(x509CAs, x509CertBundle[1:]...) pkcs12Data, err = pkcs12.Encode(rand.Reader, key, x509Cert, x509CAs, password) @@ -161,10 +173,16 @@ func p12Action(ctx *cli.Context) error { } } - if err := utils.WriteFile(p12File, pkcs12Data, 0600); err != nil { - return err + if p12File != "" { + if err := utils.WriteFile(p12File, pkcs12Data, 0600); err != nil { + return err + } + ui.Printf("Your .p12 bundle has been saved as %s.\n", p12File) + } else { + if _, err := os.Stdout.Write(pkcs12Data); err != nil { + return err + } } - ui.Printf("Your .p12 bundle has been saved as %s.\n", p12File) return nil } diff --git a/integration/certificate_format_test.go b/integration/certificate_format_test.go new file mode 100644 index 000000000..2a6de2f37 --- /dev/null +++ b/integration/certificate_format_test.go @@ -0,0 +1,183 @@ +//go:build integration + +package integration + +import ( + "fmt" + "testing" + + "github.com/smallstep/assert" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/utils" +) + +func TestCertificateFormat(t *testing.T) { + setup() + t.Run("validate cert and key extraction from p12", func(t *testing.T) { + _, err := NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))). + setFlag("crt", temp("foo_out0.crt")). + setFlag("key", temp("foo_out0.key")). + setFlag("ca", temp("intermediate-ca_out0.crt")). + setFlag("format", "pem"). + setFlag("no-password", ""). + run() + assert.Nil(t, err) + + foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt")) + foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_out0.crt")) + assert.Equals(t, foo_crt, foo_crt_out) + + foo_key, _ := utils.ReadFile(temp("foo.key")) + foo_out_key, _ := utils.ReadFile(temp("foo_out0.key")) + assert.Equals(t, foo_key, foo_out_key) + + foo_ca, _ := pemutil.ReadCertificate(temp("intermediate-ca_out0.crt")) + foo_ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out0.crt")) + assert.Equals(t, foo_ca, foo_ca_out) + }) + + t.Run("validate cert and key packaging to p12", func(t *testing.T) { + _, err := NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.crt"))). + setFlag("crt", temp("foo_format.p12")). + setFlag("key", temp("foo.key")). + setFlag("ca", temp("intermediate-ca.crt")). + setFlag("format", "p12"). + setFlag("no-password", ""). + setFlag("insecure", ""). + run() + assert.Nil(t, err) + + _, err = NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo_format.p12"))). + setFlag("crt", temp("foo_out1.crt")). + setFlag("key", temp("foo_out1.key")). + setFlag("ca", temp("intermediate-ca_out1.crt")). + setFlag("format", "pem"). + setFlag("no-password", ""). + run() + + assert.Nil(t, err) + + foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt")) + foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_out1.crt")) + assert.Equals(t, foo_crt, foo_crt_out) + + foo_key, _ := utils.ReadFile(temp("foo.key")) + foo_out_key, _ := utils.ReadFile(temp("foo_out1.key")) + assert.Equals(t, foo_key, foo_out_key) + + foo_ca, _ := pemutil.ReadCertificate(temp("intermediate-ca.crt")) + foo_ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out1.crt")) + assert.Equals(t, foo_ca, foo_ca_out) + }) + + t.Run("validate stdout output", func(t *testing.T) { + output, err := NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))). + setFlag("no-password", ""). + setFlag("key", temp("temp.key")). + setFlag("ca", temp("temp.crt")). + setFlag("format", "pem"). + run() + assert.Nil(t, err) + + foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt")) + foo_crt_out, _ := pemutil.Parse([]byte(output.stdout)) + assert.Equals(t, foo_crt, foo_crt_out) + }) + + t.Run("compare der format", func(t *testing.T) { + _, err := NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format")). + setArguments(temp("foo.crt")). + setFlag("out", temp("foo.der")). + run() + assert.Nil(t, err) + + + _, err = NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))). + setFlag("no-password", ""). + setFlag("format", "der"). + setFlag("crt", temp("foo_cmp.der")). + run() + + assert.Nil(t, err) + + foo_crt, _ := pemutil.ReadCertificate(temp("foo.der")) + foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_cmp.der")) + assert.Equals(t, foo_crt, foo_crt_out) + }) + + t.Run("validate interconversion between PEM and DER", func(t *testing.T) { + _, err := NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format")). + setArguments(temp("foo.crt")). + setFlag("out", temp("foo_inter.der")). + run() + assert.Nil(t, err) + + _, err = NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format")). + setArguments(temp("foo_inter.der")). + setFlag("out", temp("foo_inter.crt")). + run() + assert.Nil(t, err) + + assert.Nil(t, err) + + foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt")) + foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_inter.crt")) + assert.Equals(t, foo_crt, foo_crt_out) + }) + + t.Run("assert incompatible flag", func(t *testing.T) { + output, _ := NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))). + setFlag("out", temp("some")). + setFlag("key", temp("some")). + run() + assert.Equals(t, "flag '--out' is incompatible with '--key'\n", output.stderr) + }) + +} + +func setup() { + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate create root-ca %s %s", temp("root-ca.crt"), temp("root-ca.key"))). + setFlag("profile", "root-ca"). + setFlag("no-password", ""). + setFlag("insecure", ""). + run() + + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate create intermediate-ca %s %s", temp("intermediate-ca.crt"), temp("intermediate-ca.key"))). + setFlag("profile", "intermediate-ca"). + setFlag("ca", temp("root-ca.crt")). + setFlag("ca-key", temp("root-ca.key")). + setFlag("no-password", ""). + setFlag("insecure", ""). + run() + + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate create foo %s %s", temp("foo.crt"), temp("foo.key"))). + setFlag("profile", "leaf"). + setFlag("ca", temp("intermediate-ca.crt")). + setFlag("ca-key", temp("intermediate-ca.key")). + setFlag("no-password", ""). + setFlag("insecure", ""). + run() + + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate p12 %s %s %s", temp("foo.p12"), temp("foo.crt"), temp("foo.key"))). + setFlag("ca", temp("intermediate-ca.crt")). + setFlag("no-password", ""). + setFlag("insecure", ""). + run() +} + +func temp(filename string) string { + return fmt.Sprintf("%s/%s", TempDirectory, filename) +}