Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d9c1476

Browse files
committedNov 12, 2021
support p12 extraction in format command
1 parent b30347b commit d9c1476

File tree

3 files changed

+389
-25
lines changed

3 files changed

+389
-25
lines changed
 

‎command/certificate/format.go

+189-15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"bytes"
55
"crypto/x509"
66
"encoding/pem"
7+
"fmt"
8+
"github.com/smallstep/cli/crypto/pemutil"
79
"os"
810

911
"github.com/pkg/errors"
@@ -13,23 +15,28 @@ import (
1315
"github.com/smallstep/cli/ui"
1416
"github.com/smallstep/cli/utils"
1517
"github.com/urfave/cli"
18+
19+
"software.sslmate.com/src/go-pkcs12"
1620
)
1721

1822
func formatCommand() cli.Command {
1923
return cli.Command{
20-
Name: "format",
21-
Action: command.ActionFunc(formatAction),
22-
Usage: `reformat certificate`,
23-
UsageText: `**step certificate format** <crt_file> [**--out**=<file>]`,
24+
Name: "format",
25+
Action: command.ActionFunc(formatAction),
26+
Usage: `reformat certificate`,
27+
UsageText: `**step certificate format** <src_file> [**--crt**=<file>] [**--key**=<file>]
28+
[**--ca**=<file>] [**--out**=<file>]`,
2429
Description: `**step certificate format** prints the certificate or CSR in a different format.
2530
26-
Only 2 formats are currently supported; PEM and ASN.1 DER. This tool will convert
31+
If either PEM or ASN.1 DER is provided as a positional argument, this tool will convert
2732
a certificate or CSR in one format to the other.
2833
34+
If PFX / PKCS12 file is provided, it extracts a certificate and private key from the input.
35+
2936
## POSITIONAL ARGUMENTS
3037
31-
<crt_file>
32-
: Path to a certificate or CSR file.
38+
<src_file>
39+
: Path to a certificate or CSR file, or .p12 file when you specify --crt/--ca option.
3340
3441
## EXIT CODES
3542
@@ -51,12 +58,64 @@ Convert PEM format to DER and write to disk:
5158
'''
5259
$ step certificate format foo.pem --out foo.der
5360
'''
61+
62+
Convert a .p12 file to a certificate and private key:
63+
64+
'''
65+
$ step certificate format foo.p12 --out foo.crt --out-key foo.key
66+
'''
67+
68+
Convert a .p12 file to a certificate, private key and intermediate certificates:
69+
70+
'''
71+
$ step certificate format foo.p12 --out foo.crt --out-key foo.key --out-ca intermediate.crt
72+
'''
73+
74+
Convert a certificate and private key to a .p12 file:
75+
76+
'''
77+
$ step certificate format foo.crt --out foo.p12 --key foo.key
78+
'''
79+
80+
Convert a certificate, a private key, and intermediate certificates to a .p12 file:
81+
82+
'''
83+
$ step certificate format foo.crt --out foo.p12 --key foo.key --ca intermediate.crt
84+
'''
5485
`,
5586
Flags: []cli.Flag{
87+
cli.StringFlag{
88+
Name: "format",
89+
Usage: `Target format.`,
90+
},
91+
cli.StringFlag{
92+
Name: "key",
93+
Usage: `The path to the <file> containing a private key to add to the .p12 file.`,
94+
},
95+
cli.StringSliceFlag{
96+
Name: "ca",
97+
Usage: `The path to the <file> containing a CA or intermediate certificate to
98+
add to the .p12 file. Use the '--ca' flag multiple times to add
99+
multiple CAs or intermediates.`,
100+
},
56101
cli.StringFlag{
57102
Name: "out",
58103
Usage: `Path to write the reformatted result.`,
59104
},
105+
cli.StringFlag{
106+
Name: "out-key",
107+
Usage: `Path to write the private key which is extracted from p12 file.`,
108+
},
109+
cli.StringFlag{
110+
Name: "out-ca",
111+
Usage: `Path to write the intermediate certificates which are extracted from p12 file.`,
112+
},
113+
cli.StringFlag{
114+
Name: "password-file",
115+
Usage: `The path to the <file> containing the password to encrypt/decrypt the .p12 file.`,
116+
},
117+
flags.NoPassword,
118+
flags.Insecure,
60119
flags.Force,
61120
},
62121
}
@@ -67,15 +126,51 @@ func formatAction(ctx *cli.Context) error {
67126
return err
68127
}
69128

70-
var (
71-
out = ctx.String("out")
72-
ob []byte
73-
)
129+
sourceFile := ctx.Args().First()
130+
format := ctx.String("format")
131+
key := ctx.String("key")
132+
ca := ctx.StringSlice("ca")
133+
out := ctx.String("out")
134+
outKey := ctx.String("out-key")
135+
outCA := ctx.String("out-ca")
136+
passwordFile := ctx.String("password-file")
137+
noPassword := ctx.Bool("no-password")
138+
insecure := ctx.Bool("insecure")
74139

75-
var crtFile string
76-
if ctx.NArg() == 1 {
77-
crtFile = ctx.Args().First()
78-
} else {
140+
if passwordFile != "" && noPassword {
141+
return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file")
142+
}
143+
144+
switch {
145+
// Format P12 to pem/der
146+
// We can default source format to p12 if --out-key or --out-ca are passed
147+
case format == "pem" || format == "der" || outKey != "":
148+
if err := fromP12(sourceFile, out, outKey, outCA, passwordFile, noPassword); err != nil {
149+
return err
150+
}
151+
// Format PEM to P12
152+
// We can default target format to p12 if --key or --ca are passed
153+
case format == "p12" || key != "" || len(ca) != 0:
154+
if noPassword && !insecure {
155+
return errs.RequiredInsecureFlag(ctx, "no-password")
156+
}
157+
if err := ToP12(out, sourceFile, key, ca, passwordFile, noPassword, insecure); err != nil {
158+
return err
159+
}
160+
case format == "":
161+
if err := interconvertPemAndDer(sourceFile, out); err != nil {
162+
return err
163+
}
164+
default:
165+
return errors.Errorf("unrecognized argument: --format %s", format)
166+
}
167+
return nil
168+
}
169+
170+
func interconvertPemAndDer(crtFile, out string) error {
171+
var ob []byte
172+
173+
if crtFile == "" {
79174
crtFile = "-"
80175
}
81176

@@ -151,3 +246,82 @@ func decodeCertificatePem(b []byte) ([]byte, error) {
151246

152247
return nil, errors.Errorf("error decoding certificate: invalid PEM block")
153248
}
249+
250+
func fromP12(p12File, crtFile, keyFile, caFile, passwordFile string, noPassword bool) error {
251+
var err error
252+
var password string
253+
if passwordFile != "" {
254+
password, err = utils.ReadStringPasswordFromFile(passwordFile)
255+
if err != nil {
256+
return err
257+
}
258+
}
259+
260+
if password == "" && !noPassword {
261+
pass, err := ui.PromptPassword("Please enter a password to decrypt the .p12 file")
262+
if err != nil {
263+
return errs.Wrap(err, "error reading password")
264+
}
265+
password = string(pass)
266+
}
267+
268+
p12Data, err := utils.ReadFile(p12File)
269+
if err != nil {
270+
return errs.Wrap(err, "error reading file %s", p12File)
271+
}
272+
273+
key, crt, ca, err := pkcs12.DecodeChain(p12Data, password)
274+
if err != nil {
275+
return errs.Wrap(err, "failed to decode PKCS12 data")
276+
}
277+
278+
if err := write(crtFile, fmt.Sprintf("Your certificate has been saved in %s.\n", crtFile), crt); err != nil {
279+
return err
280+
}
281+
282+
if err := writeCerts(caFile, fmt.Sprintf("Your CA certificates have been saved in %s.\n", caFile), ca); err != nil {
283+
return err
284+
}
285+
286+
if err := write(keyFile, fmt.Sprintf("Your private key has been saved in %s.\n", keyFile), key); err != nil {
287+
return err
288+
}
289+
290+
return nil
291+
}
292+
293+
func writeCerts(filename, msg string, certs []*x509.Certificate) error {
294+
var data []byte
295+
for _, cert := range certs {
296+
pemblk, err := pemutil.Serialize(cert)
297+
if err != nil {
298+
return err
299+
}
300+
data = append(data, pem.EncodeToMemory(pemblk)...)
301+
}
302+
if filename == "" {
303+
os.Stdout.Write(data)
304+
} else {
305+
if err := utils.WriteFile(filename, data, 0600); err != nil {
306+
return err
307+
}
308+
ui.Printf(msg)
309+
}
310+
return nil
311+
}
312+
313+
func write(filename, msg string, in interface{}) error {
314+
if filename != "" {
315+
if _, err := pemutil.Serialize(in, pemutil.ToFile(filename, 0600)); err != nil {
316+
return err
317+
}
318+
ui.Printf(msg)
319+
} else {
320+
pemblk, err := pemutil.Serialize(in)
321+
if err != nil {
322+
return err
323+
}
324+
pem.Encode(os.Stdout, pemblk)
325+
}
326+
return nil
327+
}

‎command/certificate/p12.go

+28-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package certificate
33
import (
44
"crypto/rand"
55
"crypto/x509"
6+
"os"
67

78
"github.com/pkg/errors"
89
"github.com/smallstep/cli/command"
@@ -85,6 +86,10 @@ func p12Action(ctx *cli.Context) error {
8586
caFiles := ctx.StringSlice("ca")
8687
hasKeyAndCert := crtFile != "" && keyFile != ""
8788

89+
passwordFile := ctx.String("password-file")
90+
noPassword := ctx.Bool("no-password")
91+
insecure := ctx.Bool("insecure")
92+
8893
// If either key or cert are provided, both must be provided
8994
if !hasKeyAndCert && (crtFile != "" || keyFile != "") {
9095
return errs.MissingArguments(ctx, "key_file")
@@ -97,13 +102,20 @@ func p12Action(ctx *cli.Context) error {
97102

98103
// Validate flags
99104
switch {
100-
case ctx.String("password-file") != "" && ctx.Bool("no-password"):
105+
case passwordFile != "" && noPassword:
101106
return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file")
102-
case ctx.Bool("no-password") && !ctx.Bool("insecure"):
107+
case noPassword && !insecure:
103108
return errs.RequiredInsecureFlag(ctx, "no-password")
104109
}
105110

106-
x509CAs := []*x509.Certificate{}
111+
if err := ToP12(p12File, crtFile, keyFile, caFiles, passwordFile, noPassword, insecure); err != nil {
112+
return err
113+
}
114+
return nil
115+
}
116+
117+
func ToP12(p12File, crtFile, keyFile string, caFiles []string, passwordFile string, noPassword, insecure bool) error {
118+
var x509CAs []*x509.Certificate
107119
for _, caFile := range caFiles {
108120
x509Bundle, err := pemutil.ReadCertificateBundle(caFile)
109121
if err != nil {
@@ -114,8 +126,8 @@ func p12Action(ctx *cli.Context) error {
114126

115127
var err error
116128
var password string
117-
if !ctx.Bool("no-password") {
118-
if passwordFile := ctx.String("password-file"); passwordFile != "" {
129+
if !noPassword {
130+
if passwordFile != "" {
119131
password, err = utils.ReadStringPasswordFromFile(passwordFile)
120132
if err != nil {
121133
return err
@@ -132,7 +144,7 @@ func p12Action(ctx *cli.Context) error {
132144
}
133145

134146
var pkcs12Data []byte
135-
if hasKeyAndCert {
147+
if crtFile != "" && keyFile != "" {
136148
// If we have a key and certificate, we're making an identity store
137149
x509CertBundle, err := pemutil.ReadCertificateBundle(crtFile)
138150
if err != nil {
@@ -146,7 +158,7 @@ func p12Action(ctx *cli.Context) error {
146158

147159
// The first certificate in the bundle will be our server cert
148160
x509Cert := x509CertBundle[0]
149-
// Any remaning certs will be intermediates for the server
161+
// Any remaining certs will be intermediates for the server
150162
x509CAs = append(x509CAs, x509CertBundle[1:]...)
151163

152164
pkcs12Data, err = pkcs12.Encode(rand.Reader, key, x509Cert, x509CAs, password)
@@ -161,10 +173,16 @@ func p12Action(ctx *cli.Context) error {
161173
}
162174
}
163175

164-
if err := utils.WriteFile(p12File, pkcs12Data, 0600); err != nil {
165-
return err
176+
if p12File != "" {
177+
if err := utils.WriteFile(p12File, pkcs12Data, 0600); err != nil {
178+
return err
179+
}
180+
ui.Printf("Your .p12 bundle has been saved as %s.\n", p12File)
181+
} else {
182+
if _, err := os.Stdout.Write(pkcs12Data); err != nil {
183+
return err
184+
}
166185
}
167186

168-
ui.Printf("Your .p12 bundle has been saved as %s.\n", p12File)
169187
return nil
170188
}
+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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 TestCertificateFormat(t *testing.T) {
15+
setup()
16+
t.Run("validate cert and key extraction from p12", func(t *testing.T) {
17+
_, err := NewCLICommand().
18+
setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))).
19+
setFlag("out", temp("foo_out0.crt")).
20+
setFlag("out-key", temp("foo_out0.key")).
21+
setFlag("out-ca", temp("intermediate-ca_out0.crt")).
22+
setFlag("no-password", "").
23+
run()
24+
assert.Nil(t, err)
25+
26+
foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt"))
27+
foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_out0.crt"))
28+
assert.Equals(t, foo_crt, foo_crt_out)
29+
30+
foo_key, _ := utils.ReadFile(temp("foo.key"))
31+
foo_out_key, _ := utils.ReadFile(temp("foo_out0.key"))
32+
assert.Equals(t, foo_key, foo_out_key)
33+
34+
foo_ca, _ := pemutil.ReadCertificate(temp("intermediate-ca_out0.crt"))
35+
foo_ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out0.crt"))
36+
assert.Equals(t, foo_ca, foo_ca_out)
37+
})
38+
39+
t.Run("validate cert and key packaging to p12", func(t *testing.T) {
40+
_, err := NewCLICommand().
41+
setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.crt"))).
42+
setFlag("out", temp("foo_format.p12")).
43+
setFlag("key", temp("foo.key")).
44+
setFlag("ca", temp("intermediate-ca.crt")).
45+
setFlag("no-password", "").
46+
setFlag("insecure", "").
47+
run()
48+
assert.Nil(t, err)
49+
50+
_, err = NewCLICommand().
51+
setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo_format.p12"))).
52+
setFlag("out", temp("foo_out1.crt")).
53+
setFlag("out-key", temp("foo_out1.key")).
54+
setFlag("out-ca", temp("intermediate-ca_out1.crt")).
55+
setFlag("no-password", "").
56+
run()
57+
58+
assert.Nil(t, err)
59+
60+
foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt"))
61+
foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_out1.crt"))
62+
assert.Equals(t, foo_crt, foo_crt_out)
63+
64+
foo_key, _ := utils.ReadFile(temp("foo.key"))
65+
foo_out_key, _ := utils.ReadFile(temp("foo_out1.key"))
66+
assert.Equals(t, foo_key, foo_out_key)
67+
68+
foo_ca, _ := pemutil.ReadCertificate(temp("intermediate-ca.crt"))
69+
foo_ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out1.crt"))
70+
assert.Equals(t, foo_ca, foo_ca_out)
71+
})
72+
73+
t.Run("validate stdout output", func(t *testing.T) {
74+
output, err := NewCLICommand().
75+
setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))).
76+
setFlag("no-password", "").
77+
setFlag("format", "pem").
78+
setFlag("out-key", temp("temp.key")).
79+
setFlag("out-ca", temp("temp.pem")).
80+
run()
81+
82+
assert.Nil(t, err)
83+
84+
foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt"))
85+
foo_crt_out, _ := pemutil.Parse([]byte(output.stdout))
86+
assert.Equals(t, foo_crt, foo_crt_out)
87+
})
88+
89+
t.Run("compare der format", func(t *testing.T) {
90+
_, err := NewCLICommand().
91+
setCommand(fmt.Sprintf("../bin/step certificate format")).
92+
setArguments(temp("foo.crt")).
93+
setFlag("out", temp("foo.der")).
94+
run()
95+
assert.Nil(t, err)
96+
97+
98+
_, err = NewCLICommand().
99+
setCommand(fmt.Sprintf("../bin/step certificate format %s", temp("foo.p12"))).
100+
setFlag("no-password", "").
101+
setFlag("out", temp("foo_cmp.der")).
102+
setFlag("format", "der").
103+
run()
104+
105+
assert.Nil(t, err)
106+
107+
foo_crt, _ := pemutil.ReadCertificate(temp("foo.der"))
108+
foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_cmp.der"))
109+
assert.Equals(t, foo_crt, foo_crt_out)
110+
})
111+
112+
t.Run("validate interconversion between PEM and DER", func(t *testing.T) {
113+
_, err := NewCLICommand().
114+
setCommand(fmt.Sprintf("../bin/step certificate format")).
115+
setArguments(temp("foo.crt")).
116+
setFlag("out", temp("foo_inter.der")).
117+
run()
118+
assert.Nil(t, err)
119+
120+
_, err = NewCLICommand().
121+
setCommand(fmt.Sprintf("../bin/step certificate format")).
122+
setArguments(temp("foo_inter.der")).
123+
setFlag("out", temp("foo_inter.crt")).
124+
run()
125+
assert.Nil(t, err)
126+
127+
assert.Nil(t, err)
128+
129+
foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt"))
130+
foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_inter.crt"))
131+
assert.Equals(t, foo_crt, foo_crt_out)
132+
})
133+
134+
}
135+
136+
func setup() {
137+
NewCLICommand().
138+
setCommand(fmt.Sprintf("../bin/step certificate create root-ca %s %s", temp("root-ca.crt"), temp("root-ca.key"))).
139+
setFlag("profile", "root-ca").
140+
setFlag("no-password", "").
141+
setFlag("insecure", "").
142+
run()
143+
144+
NewCLICommand().
145+
setCommand(fmt.Sprintf("../bin/step certificate create intermediate-ca %s %s", temp("intermediate-ca.crt"), temp("intermediate-ca.key"))).
146+
setFlag("profile", "intermediate-ca").
147+
setFlag("ca", temp("root-ca.crt")).
148+
setFlag("ca-key", temp("root-ca.key")).
149+
setFlag("no-password", "").
150+
setFlag("insecure", "").
151+
run()
152+
153+
NewCLICommand().
154+
setCommand(fmt.Sprintf("../bin/step certificate create foo %s %s", temp("foo.crt"), temp("foo.key"))).
155+
setFlag("profile", "leaf").
156+
setFlag("ca", temp("intermediate-ca.crt")).
157+
setFlag("ca-key", temp("intermediate-ca.key")).
158+
setFlag("no-password", "").
159+
setFlag("insecure", "").
160+
run()
161+
162+
NewCLICommand().
163+
setCommand(fmt.Sprintf("../bin/step certificate p12 %s %s %s", temp("foo.p12"), temp("foo.crt"), temp("foo.key"))).
164+
setFlag("ca", temp("intermediate-ca.crt")).
165+
setFlag("no-password", "").
166+
setFlag("insecure", "").
167+
run()
168+
}
169+
170+
func temp(filename string) string {
171+
return fmt.Sprintf("%s/%s", TempDirectory, filename)
172+
}

0 commit comments

Comments
 (0)
Please sign in to comment.