From b5eeb35645f62c394316c1b3ed4064c2cd65d6f1 Mon Sep 17 00:00:00 2001 From: z8674558 Date: Sat, 6 Nov 2021 23:54:27 +0900 Subject: [PATCH] add certificate extract command Fixes #487 --- command/certificate/certificate.go | 1 + command/certificate/extract.go | 161 ++++++++++++++++++++++++++++ integration/certificate_p12_test.go | 86 +++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 command/certificate/extract.go create mode 100644 integration/certificate_p12_test.go diff --git a/command/certificate/certificate.go b/command/certificate/certificate.go index 5b98ba7f4..faa5873a3 100644 --- a/command/certificate/certificate.go +++ b/command/certificate/certificate.go @@ -94,6 +94,7 @@ $ step certificate uninstall root-ca.crt installCommand(), uninstallCommand(), p12Command(), + extractCommand(), }, } diff --git a/command/certificate/extract.go b/command/certificate/extract.go new file mode 100644 index 000000000..fee188c93 --- /dev/null +++ b/command/certificate/extract.go @@ -0,0 +1,161 @@ +package certificate + +import ( + "crypto/x509" + "encoding/pem" + + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/ui" + "github.com/smallstep/cli/utils" + "github.com/urfave/cli" + + "software.sslmate.com/src/go-pkcs12" +) + +func extractCommand() cli.Command { + return cli.Command{ + Name: "extract", + Action: command.ActionFunc(extractAction), + Usage: `extract a .p12 file`, + UsageText: `step certificate extract [] [] +[**--ca**=] [**--password-file**=]`, + Description: `**step certificate extract** extracts a certificate and private key +from a .p12 (PFX / PKCS12) file. + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +Extract a certificate and a private key from a .p12 file: + +''' +$ step certificate extract foo.p12 foo.crt foo.key +''' + +Extract a certificate, private key and intermediate certidicates from a .p12 file: + +''' +$ step certificate extract foo.p12 foo.crt foo.key --ca intermediate.crt +''' + +Extract certificates from "trust store" for Java applications: + +''' +$ step certificate extract trust.p12 --ca ca.crt +'''`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "ca", + Usage: `The path to the containing a CA or intermediate certificate to +add to the .p12 file. Use the '--ca' flag multiple times to add +multiple CAs or intermediates.`, + }, + cli.StringFlag{ + Name: "password-file", + Usage: `The path to the containing the password to decrypt the .p12 file.`, + }, + flags.NoPassword, + }, + } +} + +func extractAction(ctx *cli.Context) error { + if err := errs.MinMaxNumberOfArguments(ctx, 1, 3); err != nil { + return err + } + + p12File := ctx.Args().Get(0) + crtFile := ctx.Args().Get(1) + keyFile := ctx.Args().Get(2) + 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 a .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..c62b161a9 --- /dev/null +++ b/integration/certificate_p12_test.go @@ -0,0 +1,86 @@ +//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("no-password", ""). + setFlag("insecure", ""). + run() + + NewCLICommand(). + setCommand(fmt.Sprintf("../bin/step certificate extract %s %s %s", temp("foo.p12"), temp("foo_out.crt"), temp("foo_out.key"))). + 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) + }) + + 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 extract %s", temp("truststore.p12"))). + setFlag("ca", temp("intermediate-ca_out.crt")). + setFlag("no-password", ""). + run() + + ca, _ := pemutil.ReadCertificate(temp("intermediate-ca.crt")) + ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out.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) +}