Skip to content

Commit bfda5a2

Browse files
committed
support p12 extraction in format command
1 parent b30347b commit bfda5a2

File tree

2 files changed

+248
-7
lines changed

2 files changed

+248
-7
lines changed

command/certificate/format.go

+154-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"crypto/x509"
66
"encoding/pem"
7+
"github.com/smallstep/cli/crypto/pemutil"
78
"os"
89

910
"github.com/pkg/errors"
@@ -13,23 +14,28 @@ import (
1314
"github.com/smallstep/cli/ui"
1415
"github.com/smallstep/cli/utils"
1516
"github.com/urfave/cli"
17+
18+
"software.sslmate.com/src/go-pkcs12"
1619
)
1720

1821
func formatCommand() cli.Command {
1922
return cli.Command{
20-
Name: "format",
21-
Action: command.ActionFunc(formatAction),
22-
Usage: `reformat certificate`,
23-
UsageText: `**step certificate format** <crt_file> [**--out**=<file>]`,
23+
Name: "format",
24+
Action: command.ActionFunc(formatAction),
25+
Usage: `reformat certificate`,
26+
UsageText: `**step certificate format** <src_file> [**--crt**=<file>] [**--key**=<file>]
27+
[**--ca**=<file>] [**--out**=<file>]`,
2428
Description: `**step certificate format** prints the certificate or CSR in a different format.
2529
26-
Only 2 formats are currently supported; PEM and ASN.1 DER. This tool will convert
30+
If either PEM or ASN.1 DER is provided as a positional argument, this tool will convert
2731
a certificate or CSR in one format to the other.
2832
33+
If PFX / PKCS12 file is provided, it extracts a certificate and private key from the input.
34+
2935
## POSITIONAL ARGUMENTS
3036
31-
<crt_file>
32-
: Path to a certificate or CSR file.
37+
<src_file>
38+
: Path to a certificate or CSR file, or .p12 file when you specify --crt option.
3339
3440
## EXIT CODES
3541
@@ -51,12 +57,50 @@ Convert PEM format to DER and write to disk:
5157
'''
5258
$ step certificate format foo.pem --out foo.der
5359
'''
60+
61+
Convert a .p12 file to a certificate and private key:
62+
63+
'''
64+
$ step certificate format foo.p12 --crt foo.crt --key foo.key
65+
'''
66+
67+
Convert a .p12 file to a certificate, private key and intermediate certificates:
68+
69+
'''
70+
$ step certificate format foo.p12 --crt foo.crt --key foo.key --ca intermediate.crt
71+
'''
72+
73+
Get certificates from "trust store" for Java applications:
74+
75+
'''
76+
$ step certificate format trust.p12 --ca ca.crt
77+
'''
5478
`,
5579
Flags: []cli.Flag{
80+
cli.StringFlag{
81+
Name: "crt",
82+
Usage: `The destination path to the <file>
83+
to which a certificate will be extracted from .p12 file.`,
84+
},
85+
cli.StringFlag{
86+
Name: "key",
87+
Usage: `The destination path to the <file>
88+
to which a key will be extracted from .p12 file.`,
89+
},
90+
cli.StringFlag{
91+
Name: "ca",
92+
Usage: `The destination path to the <file>
93+
to which intermediate certificates will be extracted from .p12 file.`,
94+
},
95+
cli.StringFlag{
96+
Name: "password-file",
97+
Usage: `The path to the <file> containing the password to decrypt the .p12 file.`,
98+
},
5699
cli.StringFlag{
57100
Name: "out",
58101
Usage: `Path to write the reformatted result.`,
59102
},
103+
flags.NoPassword,
60104
flags.Force,
61105
},
62106
}
@@ -67,6 +111,16 @@ func formatAction(ctx *cli.Context) error {
67111
return err
68112
}
69113

114+
targetCrtFile := ctx.String("crt")
115+
targetCAFile := ctx.String("ca")
116+
// if --crt or --ca option are set, the input is .p12 file
117+
if targetCrtFile != "" || targetCAFile != "" {
118+
if err := formatP12Action(ctx); err != nil {
119+
return err
120+
}
121+
return nil
122+
}
123+
70124
var (
71125
out = ctx.String("out")
72126
ob []byte
@@ -151,3 +205,96 @@ func decodeCertificatePem(b []byte) ([]byte, error) {
151205

152206
return nil, errors.Errorf("error decoding certificate: invalid PEM block")
153207
}
208+
209+
func formatP12Action(ctx *cli.Context) error {
210+
211+
p12File := ctx.Args().Get(0)
212+
crtFile := ctx.String("crt")
213+
keyFile := ctx.String("key")
214+
caFile := ctx.String("ca")
215+
216+
var err error
217+
var password string
218+
if passwordFile := ctx.String("password-file"); passwordFile != "" {
219+
password, err = utils.ReadStringPasswordFromFile(passwordFile)
220+
if err != nil {
221+
return err
222+
}
223+
}
224+
225+
if password == "" && !ctx.Bool("no-password") {
226+
pass, err := ui.PromptPassword("Please enter a password to decrypt the .p12 file")
227+
if err != nil {
228+
return errs.Wrap(err, "error reading password")
229+
}
230+
password = string(pass)
231+
}
232+
233+
p12Data, err := utils.ReadFile(p12File)
234+
if err != nil {
235+
return errs.Wrap(err, "error reading file %s", p12File)
236+
}
237+
238+
if crtFile != "" && keyFile != "" {
239+
// If we have a destination crt path and a key path,
240+
// we are extracting those two from the .p12 file
241+
key, crt, CAs, err := pkcs12.DecodeChain(p12Data, password)
242+
if err != nil {
243+
return errs.Wrap(err, "failed to decode PKCS12 data")
244+
}
245+
246+
_, err = pemutil.Serialize(key, pemutil.ToFile(keyFile, 0600))
247+
if err != nil {
248+
return errs.Wrap(err, "failed to serialize private key")
249+
}
250+
251+
_, err = pemutil.Serialize(crt, pemutil.ToFile(crtFile, 0600))
252+
if err != nil {
253+
return errs.Wrap(err, "failed to serialize certificate")
254+
}
255+
256+
if caFile != "" {
257+
if err := extractCerts(CAs, caFile); err != nil {
258+
return errs.Wrap(err, "failed to serialize CA certificates")
259+
}
260+
}
261+
262+
} else {
263+
// If we have only --ca flags,
264+
// we are extracting from trust store
265+
certs, err := pkcs12.DecodeTrustStore(p12Data, password)
266+
if err != nil {
267+
return errs.Wrap(err, "failed to decode trust store")
268+
}
269+
if err := extractCerts(certs, caFile); err != nil {
270+
return errs.Wrap(err, "failed to serialize CA certificates")
271+
}
272+
}
273+
274+
if crtFile != "" {
275+
ui.Printf("Your certificate has been saved in %s.\n", crtFile)
276+
}
277+
if keyFile != "" {
278+
ui.Printf("Your private key has been saved in %s.\n", keyFile)
279+
}
280+
if caFile != "" {
281+
ui.Printf("Your CA certificate has been saved in %s.\n", caFile)
282+
}
283+
284+
return nil
285+
}
286+
287+
func extractCerts(certs []*x509.Certificate, filename string) error {
288+
var data []byte
289+
for _, cert := range certs {
290+
pemblk, err := pemutil.Serialize(cert)
291+
if err != nil {
292+
return err
293+
}
294+
data = append(data, pem.EncodeToMemory(pemblk)...)
295+
}
296+
if err := utils.WriteFile(filename, data, 0600); err != nil {
297+
return err
298+
}
299+
return nil
300+
}

integration/certificate_p12_test.go

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//go:build integration
2+
3+
package integration
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
9+
"github.com/smallstep/assert"
10+
"github.com/smallstep/cli/crypto/pemutil"
11+
"github.com/smallstep/cli/utils"
12+
)
13+
14+
func TestCertificateP12(t *testing.T) {
15+
setup()
16+
t.Run("extracted cert and key are equal to p12 inputs", func(t *testing.T) {
17+
NewCLICommand().
18+
setCommand(fmt.Sprintf("../bin/step certificate p12 %s %s %s", temp("foo.p12"), temp("foo.crt"), temp("foo.key"))).
19+
setFlag("ca", temp("intermediate-ca.crt")).
20+
setFlag("no-password", "").
21+
setFlag("insecure", "").
22+
run()
23+
24+
NewCLICommand().
25+
setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))).
26+
setFlag("crt", temp("foo_out.crt")).
27+
setFlag("key", temp("foo_out.key")).
28+
setFlag("ca", temp("intermediate-ca_out0.crt")).
29+
setFlag("no-password", "").
30+
run()
31+
32+
foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt"))
33+
foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_out.crt"))
34+
assert.Equals(t, foo_crt, foo_crt_out)
35+
36+
foo_key, _ := utils.ReadFile(temp("foo.key"))
37+
foo_out_key, _ := utils.ReadFile(temp("foo_out.key"))
38+
assert.Equals(t, foo_key, foo_out_key)
39+
40+
foo_ca, _ := pemutil.ReadCertificate(temp("intermediate-ca_out0.crt"))
41+
foo_ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out0.crt"))
42+
assert.Equals(t, foo_ca, foo_ca_out)
43+
})
44+
45+
t.Run("extracted trust store is equal to p12 input", func(t *testing.T) {
46+
NewCLICommand().
47+
setCommand(fmt.Sprintf("../bin/step certificate p12 %s", temp("truststore.p12"))).
48+
setFlag("ca", temp("intermediate-ca.crt")).
49+
setFlag("no-password", "").
50+
setFlag("insecure", "").
51+
run()
52+
53+
NewCLICommand().
54+
setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("truststore.p12"))).
55+
setFlag("ca", temp("intermediate-ca_out1.crt")).
56+
setFlag("no-password", "").
57+
run()
58+
59+
ca, _ := pemutil.ReadCertificate(temp("intermediate-ca.crt"))
60+
ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out1.crt"))
61+
assert.Equals(t, ca, ca_out)
62+
})
63+
}
64+
65+
func setup() {
66+
NewCLICommand().
67+
setCommand(fmt.Sprintf("../bin/step certificate create root-ca %s %s", temp("root-ca.crt"), temp("root-ca.key"))).
68+
setFlag("profile", "root-ca").
69+
setFlag("no-password", "").
70+
setFlag("insecure", "").
71+
run()
72+
73+
NewCLICommand().
74+
setCommand(fmt.Sprintf("../bin/step certificate create intermediate-ca %s %s", temp("intermediate-ca.crt"), temp("intermediate-ca.key"))).
75+
setFlag("profile", "intermediate-ca").
76+
setFlag("ca", temp("root-ca.crt")).
77+
setFlag("ca-key", temp("root-ca.key")).
78+
setFlag("no-password", "").
79+
setFlag("insecure", "").
80+
run()
81+
82+
NewCLICommand().
83+
setCommand(fmt.Sprintf("../bin/step certificate create foo %s %s", temp("foo.crt"), temp("foo.key"))).
84+
setFlag("profile", "leaf").
85+
setFlag("ca", temp("intermediate-ca.crt")).
86+
setFlag("ca-key", temp("intermediate-ca.key")).
87+
setFlag("no-password", "").
88+
setFlag("insecure", "").
89+
run()
90+
}
91+
92+
func temp(filename string) string {
93+
return fmt.Sprintf("%s/%s", TempDirectory, filename)
94+
}

0 commit comments

Comments
 (0)