Skip to content

Commit

Permalink
support p12 extraction in format command
Browse files Browse the repository at this point in the history
  • Loading branch information
6293 committed Nov 11, 2021
1 parent b30347b commit bfda5a2
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 7 deletions.
161 changes: 154 additions & 7 deletions command/certificate/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto/x509"
"encoding/pem"
"github.com/smallstep/cli/crypto/pemutil"
"os"

"github.com/pkg/errors"
Expand All @@ -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** <crt_file> [**--out**=<file>]`,
Name: "format",
Action: command.ActionFunc(formatAction),
Usage: `reformat certificate`,
UsageText: `**step certificate format** <src_file> [**--crt**=<file>] [**--key**=<file>]
[**--ca**=<file>] [**--out**=<file>]`,
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
<crt_file>
: Path to a certificate or CSR file.
<src_file>
: Path to a certificate or CSR file, or .p12 file when you specify --crt option.
## EXIT CODES
Expand All @@ -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 <file>
to which a certificate will be extracted from .p12 file.`,
},
cli.StringFlag{
Name: "key",
Usage: `The destination path to the <file>
to which a key will be extracted from .p12 file.`,
},
cli.StringFlag{
Name: "ca",
Usage: `The destination path to the <file>
to which intermediate certificates will be extracted from .p12 file.`,
},
cli.StringFlag{
Name: "password-file",
Usage: `The path to the <file> containing the password to decrypt the .p12 file.`,
},
cli.StringFlag{
Name: "out",
Usage: `Path to write the reformatted result.`,
},
flags.NoPassword,
flags.Force,
},
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
94 changes: 94 additions & 0 deletions integration/certificate_p12_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit bfda5a2

Please sign in to comment.