Skip to content

Commit 8afe8b7

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

File tree

3 files changed

+439
-25
lines changed

3 files changed

+439
-25
lines changed

command/certificate/format.go

+237-15
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/--ca option.
3339
3440
## EXIT CODES
3541
@@ -51,12 +57,60 @@ 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 --out foo.crt --out-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 --out foo.crt --out-key foo.key --out-ca intermediate.crt
71+
'''
72+
73+
Convert a certificate and private key to a .p12 file:
74+
75+
'''
76+
$ step certificate format foo.crt --out foo.p12 --key foo.key
77+
'''
78+
79+
Convert a certificate, a private key, and intermediate certificates to a .p12 file:
80+
81+
'''
82+
$ step certificate format foo.crt --out foo.p12 --key foo.key --ca intermediate.crt
83+
'''
5484
`,
5585
Flags: []cli.Flag{
86+
cli.StringFlag{
87+
Name: "format",
88+
Usage: `Target format.`,
89+
},
90+
cli.StringFlag{
91+
Name: "crt",
92+
Usage: `The path to a certificate.`,
93+
},
94+
cli.StringFlag{
95+
Name: "key",
96+
Usage: `The path to a private key.`,
97+
},
98+
cli.StringSliceFlag{
99+
Name: "ca",
100+
Usage: `The path a CA or intermediate certificate. When converting certificates
101+
to p12 file, Use the '--ca' flag multiple times to add
102+
multiple CAs or intermediates.`,
103+
},
56104
cli.StringFlag{
57105
Name: "out",
58106
Usage: `Path to write the reformatted result.`,
59107
},
108+
cli.StringFlag{
109+
Name: "password-file",
110+
Usage: `The path to the <file> containing the password to encrypt/decrypt the .p12 file.`,
111+
},
112+
flags.NoPassword,
113+
flags.Insecure,
60114
flags.Force,
61115
},
62116
}
@@ -67,15 +121,53 @@ func formatAction(ctx *cli.Context) error {
67121
return err
68122
}
69123

70-
var (
71-
out = ctx.String("out")
72-
ob []byte
73-
)
124+
sourceFile := ctx.Args().First()
125+
format := ctx.String("format")
126+
crt := ctx.String("crt")
127+
key := ctx.String("key")
128+
ca := ctx.StringSlice("ca")
129+
out := ctx.String("out")
130+
passwordFile := ctx.String("password-file")
131+
noPassword := ctx.Bool("no-password")
132+
insecure := ctx.Bool("insecure")
74133

75-
var crtFile string
76-
if ctx.NArg() == 1 {
77-
crtFile = ctx.Args().First()
78-
} else {
134+
if passwordFile != "" && noPassword {
135+
return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file")
136+
}
137+
138+
switch {
139+
case format == "pem" || format == "der":
140+
if len(ca) > 1 {
141+
return errors.Errorf("--ca option specified for multiple times when the target format is pem/der")
142+
}
143+
caFile := ""
144+
if len(ca) == 1 {
145+
caFile = ca[0]
146+
}
147+
if err := fromP12(sourceFile, crt, key, caFile, passwordFile, noPassword, format); err != nil {
148+
return err
149+
}
150+
case format == "p12":
151+
if noPassword && !insecure {
152+
return errs.RequiredInsecureFlag(ctx, "no-password")
153+
}
154+
if err := ToP12(crt, sourceFile, key, ca, passwordFile, noPassword, insecure); err != nil {
155+
return err
156+
}
157+
case format == "":
158+
if err := interconvertPemAndDer(sourceFile, out); err != nil {
159+
return err
160+
}
161+
default:
162+
return errors.Errorf("unrecognized argument: --format %s", format)
163+
}
164+
return nil
165+
}
166+
167+
func interconvertPemAndDer(crtFile, out string) error {
168+
var ob []byte
169+
170+
if crtFile == "" {
79171
crtFile = "-"
80172
}
81173

@@ -144,10 +236,140 @@ func decodeCertificatePem(b []byte) ([]byte, error) {
144236
return nil, errors.Wrap(err, "error parsing certificate request")
145237
}
146238
return csr.Raw, nil
239+
case "RSA PRIVATE KEY":
240+
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
241+
if err != nil {
242+
return nil, errors.Wrap(err, "error parsing RSA private key")
243+
}
244+
keyBytes := x509.MarshalPKCS1PrivateKey(key)
245+
return keyBytes, nil
246+
case "EC PRIVATE KEY":
247+
key, err := x509.ParseECPrivateKey(block.Bytes)
248+
if err != nil {
249+
return nil, errors.Wrap(err, "error parsing EC private key")
250+
}
251+
keyBytes, err := x509.MarshalECPrivateKey(key)
252+
if err != nil {
253+
return nil, errors.Wrap(err, "error converting EC private key to DER format")
254+
}
255+
return keyBytes, nil
256+
case "PRIVATE KEY":
257+
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
258+
if err != nil {
259+
return nil, errors.Wrap(err, "error parsing private key")
260+
}
261+
keyBytes, err := x509.MarshalPKCS8PrivateKey(key)
262+
if err != nil {
263+
return nil, errors.Wrap(err, "error converting private key to DER format")
264+
}
265+
return keyBytes, nil
147266
default:
148267
continue
149268
}
150269
}
151270

152271
return nil, errors.Errorf("error decoding certificate: invalid PEM block")
153272
}
273+
274+
func fromP12(p12File, crtFile, keyFile, caFile, passwordFile string, noPassword bool, format string) error {
275+
var err error
276+
var password string
277+
if passwordFile != "" {
278+
password, err = utils.ReadStringPasswordFromFile(passwordFile)
279+
if err != nil {
280+
return err
281+
}
282+
}
283+
284+
if password == "" && !noPassword {
285+
pass, err := ui.PromptPassword("Please enter a password to decrypt the .p12 file")
286+
if err != nil {
287+
return errs.Wrap(err, "error reading password")
288+
}
289+
password = string(pass)
290+
}
291+
292+
p12Data, err := utils.ReadFile(p12File)
293+
if err != nil {
294+
return errs.Wrap(err, "error reading file %s", p12File)
295+
}
296+
297+
key, crt, ca, err := pkcs12.DecodeChain(p12Data, password)
298+
if err != nil {
299+
return errs.Wrap(err, "failed to decode PKCS12 data")
300+
}
301+
302+
if err := write(crtFile, format, crt); err != nil {
303+
return err
304+
}
305+
306+
if err := writeCerts(caFile, format, ca); err != nil {
307+
return err
308+
}
309+
310+
if err := write(keyFile, format, key); err != nil {
311+
return err
312+
}
313+
314+
return nil
315+
}
316+
317+
func writeCerts(filename, format string, certs []*x509.Certificate) error {
318+
if len(certs) > 1 && format == "der" {
319+
return errors.Errorf("der format does not support a certificate bundle")
320+
}
321+
var data []byte
322+
for _, cert := range certs {
323+
b, err := toByte(cert, format)
324+
if err != nil {
325+
return err
326+
}
327+
data = append(data, b...)
328+
}
329+
if err := maybeWrite(filename, data); err != nil {
330+
return err
331+
}
332+
return nil
333+
}
334+
335+
func write(filename, format string, in interface{}) error {
336+
b, err := toByte(in, format)
337+
if err != nil {
338+
return err
339+
}
340+
if err := maybeWrite(filename, b); err != nil {
341+
return err
342+
}
343+
return nil
344+
}
345+
346+
func maybeWrite(filename string, out []byte) error {
347+
if filename == "" {
348+
os.Stdout.Write(out)
349+
} else {
350+
if err := utils.WriteFile(filename, out, 0600); err != nil {
351+
return err
352+
}
353+
}
354+
return nil
355+
}
356+
357+
func toByte(in interface{}, format string) ([]byte, error) {
358+
pemblk, err := pemutil.Serialize(in)
359+
if err != nil {
360+
return nil, err
361+
}
362+
pemByte := pem.EncodeToMemory(pemblk)
363+
switch format {
364+
case "der":
365+
derByte, err := decodeCertificatePem(pemByte)
366+
if err != nil {
367+
return nil, err
368+
}
369+
return derByte, nil
370+
case "pem", "":
371+
return pemByte, nil
372+
default:
373+
return nil, errors.Errorf("unsupported format: %s", format)
374+
}
375+
}

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
}

0 commit comments

Comments
 (0)