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 042033f

Browse files
committedNov 15, 2021
support p12 extraction in format command
1 parent b30347b commit 042033f

File tree

3 files changed

+467
-25
lines changed

3 files changed

+467
-25
lines changed
 

‎command/certificate/format.go

+256-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,32 @@ 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>] [**--format**=<format>]`,
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 as a positional argument, and the format is specified as "pem"/"der",
34+
it extracts a certificate and private key from the input.
35+
36+
If either PEM or ASN.1 DER is provided in "--crt", "--key" and "--ca", and the format is specified as "p12",
37+
it creates PFX / PKCS12 file from the input .
38+
2939
## POSITIONAL ARGUMENTS
3040
31-
<crt_file>
32-
: Path to a certificate or CSR file.
41+
<src_file>
42+
: Path to a certificate or CSR file, or .p12 file when you specify --crt/--ca option.
3343
3444
## EXIT CODES
3545
@@ -51,12 +61,60 @@ Convert PEM format to DER and write to disk:
5161
'''
5262
$ step certificate format foo.pem --out foo.der
5363
'''
64+
65+
Convert a .p12 file to a certificate and private key:
66+
67+
'''
68+
$ step certificate format foo.p12 --crt foo.crt --key foo.key --format pem
69+
'''
70+
71+
Convert a .p12 file to a certificate, private key and intermediate certificates:
72+
73+
'''
74+
$ step certificate format foo.p12 --crt foo.crt --key foo.key --ca intermediate.crt --format pem
75+
'''
76+
77+
Convert a certificate and private key to a .p12 file:
78+
79+
'''
80+
$ step certificate format foo.crt --crt foo.p12 --key foo.key --format p12
81+
'''
82+
83+
Convert a certificate, a private key, and intermediate certificates to a .p12 file:
84+
85+
'''
86+
$ step certificate format foo.crt --crt foo.p12 --key foo.key --ca intermediate.crt --format p12
87+
'''
5488
`,
5589
Flags: []cli.Flag{
90+
cli.StringFlag{
91+
Name: "format",
92+
Usage: `Target format.`,
93+
},
94+
cli.StringFlag{
95+
Name: "crt",
96+
Usage: `The path to a certificate.`,
97+
},
98+
cli.StringFlag{
99+
Name: "key",
100+
Usage: `The path to a private key.`,
101+
},
102+
cli.StringSliceFlag{
103+
Name: "ca",
104+
Usage: `The path a CA or intermediate certificate. When converting certificates
105+
to p12 file, Use the '--ca' flag multiple times to add
106+
multiple CAs or intermediates.`,
107+
},
56108
cli.StringFlag{
57109
Name: "out",
58110
Usage: `Path to write the reformatted result.`,
59111
},
112+
cli.StringFlag{
113+
Name: "password-file",
114+
Usage: `The path to the <file> containing the password to encrypt/decrypt the .p12 file.`,
115+
},
116+
flags.NoPassword,
117+
flags.Insecure,
60118
flags.Force,
61119
},
62120
}
@@ -67,15 +125,68 @@ func formatAction(ctx *cli.Context) error {
67125
return err
68126
}
69127

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

75-
var crtFile string
76-
if ctx.NArg() == 1 {
77-
crtFile = ctx.Args().First()
78-
} else {
138+
if out != "" {
139+
if crt != "" {
140+
return errs.IncompatibleFlagWithFlag(ctx, "out", "crt")
141+
}
142+
if key != "" {
143+
return errs.IncompatibleFlagWithFlag(ctx, "out", "key")
144+
}
145+
if len(ca) != 0 {
146+
return errs.IncompatibleFlagWithFlag(ctx, "out", "ca")
147+
}
148+
if format != "" {
149+
return errs.IncompatibleFlagWithFlag(ctx, "out", "format")
150+
}
151+
}
152+
153+
if passwordFile != "" && noPassword {
154+
return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file")
155+
}
156+
157+
switch {
158+
case format == "pem" || format == "der":
159+
if len(ca) > 1 {
160+
return errors.Errorf("--ca option specified for multiple times when the target format is pem/der")
161+
}
162+
caFile := ""
163+
if len(ca) == 1 {
164+
caFile = ca[0]
165+
}
166+
if err := fromP12(sourceFile, crt, key, caFile, passwordFile, noPassword, format); err != nil {
167+
return err
168+
}
169+
case format == "p12":
170+
if noPassword && !insecure {
171+
return errs.RequiredInsecureFlag(ctx, "no-password")
172+
}
173+
if err := ToP12(crt, sourceFile, key, ca, passwordFile, noPassword, insecure); err != nil {
174+
return err
175+
}
176+
case format == "":
177+
if err := interconvertPemAndDer(sourceFile, out); err != nil {
178+
return err
179+
}
180+
default:
181+
return errors.Errorf("unrecognized argument: --format %s", format)
182+
}
183+
return nil
184+
}
185+
186+
func interconvertPemAndDer(crtFile, out string) error {
187+
var ob []byte
188+
189+
if crtFile == "" {
79190
crtFile = "-"
80191
}
81192

@@ -144,10 +255,140 @@ func decodeCertificatePem(b []byte) ([]byte, error) {
144255
return nil, errors.Wrap(err, "error parsing certificate request")
145256
}
146257
return csr.Raw, nil
258+
case "RSA PRIVATE KEY":
259+
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
260+
if err != nil {
261+
return nil, errors.Wrap(err, "error parsing RSA private key")
262+
}
263+
keyBytes := x509.MarshalPKCS1PrivateKey(key)
264+
return keyBytes, nil
265+
case "EC PRIVATE KEY":
266+
key, err := x509.ParseECPrivateKey(block.Bytes)
267+
if err != nil {
268+
return nil, errors.Wrap(err, "error parsing EC private key")
269+
}
270+
keyBytes, err := x509.MarshalECPrivateKey(key)
271+
if err != nil {
272+
return nil, errors.Wrap(err, "error converting EC private key to DER format")
273+
}
274+
return keyBytes, nil
275+
case "PRIVATE KEY":
276+
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
277+
if err != nil {
278+
return nil, errors.Wrap(err, "error parsing private key")
279+
}
280+
keyBytes, err := x509.MarshalPKCS8PrivateKey(key)
281+
if err != nil {
282+
return nil, errors.Wrap(err, "error converting private key to DER format")
283+
}
284+
return keyBytes, nil
147285
default:
148286
continue
149287
}
150288
}
151289

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

0 commit comments

Comments
 (0)
Please sign in to comment.