Skip to content

Commit b5eeb35

Browse files
committed
add certificate extract command
Fixes #487
1 parent b30347b commit b5eeb35

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

command/certificate/certificate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ $ step certificate uninstall root-ca.crt
9494
installCommand(),
9595
uninstallCommand(),
9696
p12Command(),
97+
extractCommand(),
9798
},
9899
}
99100

command/certificate/extract.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package certificate
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
7+
"github.com/smallstep/cli/command"
8+
"github.com/smallstep/cli/crypto/pemutil"
9+
"github.com/smallstep/cli/errs"
10+
"github.com/smallstep/cli/flags"
11+
"github.com/smallstep/cli/ui"
12+
"github.com/smallstep/cli/utils"
13+
"github.com/urfave/cli"
14+
15+
"software.sslmate.com/src/go-pkcs12"
16+
)
17+
18+
func extractCommand() cli.Command {
19+
return cli.Command{
20+
Name: "extract",
21+
Action: command.ActionFunc(extractAction),
22+
Usage: `extract a .p12 file`,
23+
UsageText: `step certificate extract <p12-path> [<crt-path>] [<key-path>]
24+
[**--ca**=<file>] [**--password-file**=<file>]`,
25+
Description: `**step certificate extract** extracts a certificate and private key
26+
from a .p12 (PFX / PKCS12) file.
27+
28+
## EXIT CODES
29+
30+
This command returns 0 on success and \>0 if any error occurs.
31+
32+
## EXAMPLES
33+
34+
Extract a certificate and a private key from a .p12 file:
35+
36+
'''
37+
$ step certificate extract foo.p12 foo.crt foo.key
38+
'''
39+
40+
Extract a certificate, private key and intermediate certidicates from a .p12 file:
41+
42+
'''
43+
$ step certificate extract foo.p12 foo.crt foo.key --ca intermediate.crt
44+
'''
45+
46+
Extract certificates from "trust store" for Java applications:
47+
48+
'''
49+
$ step certificate extract trust.p12 --ca ca.crt
50+
'''`,
51+
Flags: []cli.Flag{
52+
cli.StringFlag{
53+
Name: "ca",
54+
Usage: `The path to the <file> containing a CA or intermediate certificate to
55+
add to the .p12 file. Use the '--ca' flag multiple times to add
56+
multiple CAs or intermediates.`,
57+
},
58+
cli.StringFlag{
59+
Name: "password-file",
60+
Usage: `The path to the <file> containing the password to decrypt the .p12 file.`,
61+
},
62+
flags.NoPassword,
63+
},
64+
}
65+
}
66+
67+
func extractAction(ctx *cli.Context) error {
68+
if err := errs.MinMaxNumberOfArguments(ctx, 1, 3); err != nil {
69+
return err
70+
}
71+
72+
p12File := ctx.Args().Get(0)
73+
crtFile := ctx.Args().Get(1)
74+
keyFile := ctx.Args().Get(2)
75+
caFile := ctx.String("ca")
76+
77+
var err error
78+
var password string
79+
if passwordFile := ctx.String("password-file"); passwordFile != "" {
80+
password, err = utils.ReadStringPasswordFromFile(passwordFile)
81+
if err != nil {
82+
return err
83+
}
84+
}
85+
86+
if password == "" && !ctx.Bool("no-password") {
87+
pass, err := ui.PromptPassword("Please enter a password to decrypt the .p12 file")
88+
if err != nil {
89+
return errs.Wrap(err, "error reading password")
90+
}
91+
password = string(pass)
92+
}
93+
94+
p12Data, err := utils.ReadFile(p12File)
95+
if err != nil {
96+
return errs.Wrap(err, "error reading file %s", p12File)
97+
}
98+
99+
if crtFile != "" && keyFile != "" {
100+
// If we have a destination crt path and a key path,
101+
// we are extracting a .p12 file
102+
key, crt, CAs, err := pkcs12.DecodeChain(p12Data, password)
103+
if err != nil {
104+
return errs.Wrap(err, "failed to decode PKCS12 data")
105+
}
106+
107+
_, err = pemutil.Serialize(key, pemutil.ToFile(keyFile, 0600))
108+
if err != nil {
109+
return errs.Wrap(err, "failed to serialize private key")
110+
}
111+
112+
_, err = pemutil.Serialize(crt, pemutil.ToFile(crtFile, 0600))
113+
if err != nil {
114+
return errs.Wrap(err, "failed to serialize certificate")
115+
}
116+
117+
if caFile != "" {
118+
if err := extractCerts(CAs, caFile); err != nil {
119+
return errs.Wrap(err, "failed to serialize CA certificates")
120+
}
121+
}
122+
123+
} else {
124+
// If we have only --ca flags,
125+
// we are extracting from trust store
126+
certs, err := pkcs12.DecodeTrustStore(p12Data, password)
127+
if err != nil {
128+
return errs.Wrap(err, "failed to decode trust store")
129+
}
130+
if err := extractCerts(certs, caFile); err != nil {
131+
return errs.Wrap(err, "failed to serialize CA certificates")
132+
}
133+
}
134+
135+
if crtFile != "" {
136+
ui.Printf("Your certificate has been saved in %s.\n", crtFile)
137+
}
138+
if keyFile != "" {
139+
ui.Printf("Your private key has been saved in %s.\n", keyFile)
140+
}
141+
if caFile != "" {
142+
ui.Printf("Your CA certificate has been saved in %s.\n", caFile)
143+
}
144+
145+
return nil
146+
}
147+
148+
func extractCerts(certs []*x509.Certificate, filename string) error {
149+
var data []byte
150+
for _, cert := range certs {
151+
pemblk, err := pemutil.Serialize(cert)
152+
if err != nil {
153+
return err
154+
}
155+
data = append(data, pem.EncodeToMemory(pemblk)...)
156+
}
157+
if err := utils.WriteFile(filename, data, 0600); err != nil {
158+
return err
159+
}
160+
return nil
161+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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("no-password", "").
20+
setFlag("insecure", "").
21+
run()
22+
23+
NewCLICommand().
24+
setCommand(fmt.Sprintf("../bin/step certificate extract %s %s %s", temp("foo.p12"), temp("foo_out.crt"), temp("foo_out.key"))).
25+
setFlag("no-password", "").
26+
run()
27+
28+
foo_crt, _ := pemutil.ReadCertificate(temp("foo.crt"))
29+
foo_crt_out, _ := pemutil.ReadCertificate(temp("foo_out.crt"))
30+
assert.Equals(t, foo_crt, foo_crt_out)
31+
32+
foo_key, _ := utils.ReadFile(temp("foo.key"))
33+
foo_out_key, _ := utils.ReadFile(temp("foo_out.key"))
34+
assert.Equals(t, foo_key, foo_out_key)
35+
})
36+
37+
t.Run("extracted trust store is equal to p12 input", func(t *testing.T) {
38+
NewCLICommand().
39+
setCommand(fmt.Sprintf("../bin/step certificate p12 %s", temp("truststore.p12"))).
40+
setFlag("ca", temp("intermediate-ca.crt")).
41+
setFlag("no-password", "").
42+
setFlag("insecure", "").
43+
run()
44+
45+
NewCLICommand().
46+
setCommand(fmt.Sprintf("../bin/step certificate extract %s", temp("truststore.p12"))).
47+
setFlag("ca", temp("intermediate-ca_out.crt")).
48+
setFlag("no-password", "").
49+
run()
50+
51+
ca, _ := pemutil.ReadCertificate(temp("intermediate-ca.crt"))
52+
ca_out, _ := pemutil.ReadCertificate(temp("intermediate-ca_out.crt"))
53+
assert.Equals(t, ca, ca_out)
54+
})
55+
}
56+
57+
func setup() {
58+
NewCLICommand().
59+
setCommand(fmt.Sprintf("../bin/step certificate create root-ca %s %s", temp("root-ca.crt"), temp("root-ca.key"))).
60+
setFlag("profile", "root-ca").
61+
setFlag("no-password", "").
62+
setFlag("insecure", "").
63+
run()
64+
65+
NewCLICommand().
66+
setCommand(fmt.Sprintf("../bin/step certificate create intermediate-ca %s %s", temp("intermediate-ca.crt"), temp("intermediate-ca.key"))).
67+
setFlag("profile", "intermediate-ca").
68+
setFlag("ca", temp("root-ca.crt")).
69+
setFlag("ca-key", temp("root-ca.key")).
70+
setFlag("no-password", "").
71+
setFlag("insecure", "").
72+
run()
73+
74+
NewCLICommand().
75+
setCommand(fmt.Sprintf("../bin/step certificate create foo %s %s", temp("foo.crt"), temp("foo.key"))).
76+
setFlag("profile", "leaf").
77+
setFlag("ca", temp("intermediate-ca.crt")).
78+
setFlag("ca-key", temp("intermediate-ca.key")).
79+
setFlag("no-password", "").
80+
setFlag("insecure", "").
81+
run()
82+
}
83+
84+
func temp(filename string) string {
85+
return fmt.Sprintf("%s/%s", TempDirectory, filename)
86+
}

0 commit comments

Comments
 (0)