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/urfave/cli"
1415 "go.step.sm/cli-utils/command"
1516 "go.step.sm/cli-utils/errs"
17+
18+ "software.sslmate.com/src/go-pkcs12"
1619)
1720
1821func 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** <crt-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
2731a 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
3141<crt-file>
32- : Path to a certificate or CSR 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