From bfda5a23295aa1c500d158524b0fd9e987d56195 Mon Sep 17 00:00:00 2001 From: z8674558 Date: Thu, 11 Nov 2021 18:33:06 +0900 Subject: [PATCH] support p12 extraction in format command --- command/certificate/format.go | 161 ++++++++++++++++++++++++++-- integration/certificate_p12_test.go | 94 ++++++++++++++++ 2 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 integration/certificate_p12_test.go diff --git a/command/certificate/format.go b/command/certificate/format.go index fc3e878f3..245560746 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,28 @@ import ( "github.com/smallstep/cli/ui" "github.com/smallstep/cli/utils" "github.com/urfave/cli" + + "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**=]`, 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, it extracts a certificate and private key 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 option. ## EXIT CODES @@ -51,12 +57,50 @@ 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 +''' + +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 +''' + +Get certificates from "trust store" for Java applications: + +''' +$ step certificate format trust.p12 --ca ca.crt +''' `, Flags: []cli.Flag{ + cli.StringFlag{ + Name: "crt", + Usage: `The destination path to the +to which a certificate will be extracted from .p12 file.`, + }, + cli.StringFlag{ + Name: "key", + Usage: `The destination path to the +to which a key will be extracted from .p12 file.`, + }, + cli.StringFlag{ + Name: "ca", + Usage: `The destination path to the +to which intermediate certificates will be extracted from .p12 file.`, + }, + cli.StringFlag{ + Name: "password-file", + Usage: `The path to the containing the password to decrypt the .p12 file.`, + }, cli.StringFlag{ Name: "out", Usage: `Path to write the reformatted result.`, }, + flags.NoPassword, flags.Force, }, } @@ -67,6 +111,16 @@ func formatAction(ctx *cli.Context) error { return err } + targetCrtFile := ctx.String("crt") + targetCAFile := ctx.String("ca") + // if --crt or --ca option are set, the input is .p12 file + if targetCrtFile != "" || targetCAFile != "" { + if err := formatP12Action(ctx); err != nil { + return err + } + return nil + } + var ( out = ctx.String("out") ob []byte @@ -151,3 +205,96 @@ func decodeCertificatePem(b []byte) ([]byte, error) { return nil, errors.Errorf("error decoding certificate: invalid PEM block") } + +func formatP12Action(ctx *cli.Context) error { + + p12File := ctx.Args().Get(0) + crtFile := ctx.String("crt") + keyFile := ctx.String("key") + caFile := ctx.String("ca") + + var err error + var password string + if passwordFile := ctx.String("password-file"); passwordFile != "" { + password, err = utils.ReadStringPasswordFromFile(passwordFile) + if err != nil { + return err + } + } + + if password == "" && !ctx.Bool("no-password") { + 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) + } + + if crtFile != "" && keyFile != "" { + // If we have a destination crt path and a key path, + // we are extracting those two from the .p12 file + key, crt, CAs, err := pkcs12.DecodeChain(p12Data, password) + if err != nil { + return errs.Wrap(err, "failed to decode PKCS12 data") + } + + _, err = pemutil.Serialize(key, pemutil.ToFile(keyFile, 0600)) + if err != nil { + return errs.Wrap(err, "failed to serialize private key") + } + + _, err = pemutil.Serialize(crt, pemutil.ToFile(crtFile, 0600)) + if err != nil { + return errs.Wrap(err, "failed to serialize certificate") + } + + if caFile != "" { + if err := extractCerts(CAs, caFile); err != nil { + return errs.Wrap(err, "failed to serialize CA certificates") + } + } + + } else { + // If we have only --ca flags, + // we are extracting from trust store + certs, err := pkcs12.DecodeTrustStore(p12Data, password) + if err != nil { + return errs.Wrap(err, "failed to decode trust store") + } + if err := extractCerts(certs, caFile); err != nil { + return errs.Wrap(err, "failed to serialize CA certificates") + } + } + + if crtFile != "" { + ui.Printf("Your certificate has been saved in %s.\n", crtFile) + } + if keyFile != "" { + ui.Printf("Your private key has been saved in %s.\n", keyFile) + } + if caFile != "" { + ui.Printf("Your CA certificate has been saved in %s.\n", caFile) + } + + return nil +} + +func extractCerts(certs []*x509.Certificate, filename string) error { + var data []byte + for _, cert := range certs { + pemblk, err := pemutil.Serialize(cert) + if err != nil { + return err + } + data = append(data, pem.EncodeToMemory(pemblk)...) + } + if err := utils.WriteFile(filename, data, 0600); err != nil { + return err + } + return nil +} diff --git a/integration/certificate_p12_test.go b/integration/certificate_p12_test.go new file mode 100644 index 000000000..4d8ba4d8b --- /dev/null +++ b/integration/certificate_p12_test.go @@ -0,0 +1,94 @@ +//go:build integration + +package integration + +import ( + "fmt" + "testing" + + "github.com/smallstep/assert" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/utils" +) + +func TestCertificateP12(t *testing.T) { + setup() + t.Run("extracted cert and key are equal to p12 inputs", func(t *testing.T) { + 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() + + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))). + setFlag("crt", temp("foo_out.crt")). + setFlag("key", temp("foo_out.key")). + setFlag("ca", temp("intermediate-ca_out0.crt")). + setFlag("no-password", ""). + run() + + foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt")) + foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_out.crt")) + assert.Equals(t, foo_crt, foo_crt_out) + + foo_key, _ := utils.ReadFile(temp("foo.key")) + foo_out_key, _ := utils.ReadFile(temp("foo_out.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("extracted trust store is equal to p12 input", func(t *testing.T) { + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate p12 %s", temp("truststore.p12"))). + setFlag("ca", temp("intermediate-ca.crt")). + setFlag("no-password", ""). + setFlag("insecure", ""). + run() + + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("truststore.p12"))). + setFlag("ca", temp("intermediate-ca_out1.crt")). + setFlag("no-password", ""). + run() + + ca, _ := pemutil.ReadCertificate(temp("intermediate-ca.crt")) + ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out1.crt")) + assert.Equals(t, ca, ca_out) + }) +} + +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() +} + +func temp(filename string) string { + return fmt.Sprintf("%s/%s", TempDirectory, filename) +}