Skip to content

Commit d9c1476

Browse files
committed
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

Lines changed: 189 additions & 15 deletions
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

Lines changed: 28 additions & 10 deletions
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)