-
Notifications
You must be signed in to change notification settings - Fork 280
Add certificate extract command for conversion between P12, PEM, and DER #589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dopey
wants to merge
9
commits into
master
Choose a base branch
from
max/extract
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
10e9785
support p12 extraction in format command
6293 1b8e030
Update some documentation syntax, grammar, and content
dopey b4ef290
require pemutil from go.step.sm
dopey 60baaec
inputs -> formats for clarity in description
dopey b8b593e
grammar - clarify flag error condition
dopey 28b6b7e
don't re-marshal keys when decoding PEM for verification
dopey 29cb746
Attempt to detect input file for code clarity
dopey 9f49147
renaming some input flags
dopey 1f5d33b
fix linter errors
dopey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,30 +6,43 @@ import ( | |
"encoding/pem" | ||
"os" | ||
|
||
"github.com/smallstep/cli/crypto/pemutil" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/smallstep/cli/flags" | ||
"github.com/smallstep/cli/ui" | ||
"github.com/smallstep/cli/utils" | ||
"github.com/urfave/cli" | ||
"go.step.sm/cli-utils/command" | ||
"go.step.sm/cli-utils/errs" | ||
|
||
"software.sslmate.com/src/go-pkcs12" | ||
) | ||
|
||
func formatCommand() cli.Command { | ||
return cli.Command{ | ||
Name: "format", | ||
Action: command.ActionFunc(formatAction), | ||
Usage: `reformat certificate`, | ||
UsageText: `**step certificate format** <crt-file> [**--out**=<file>]`, | ||
Name: "format", | ||
Action: command.ActionFunc(formatAction), | ||
Usage: `reformat certificate`, | ||
UsageText: `**step certificate format** <crt-file> [**--crt**=<file>] [**--key**=<file>] | ||
[**--ca**=<file>] [**--out**=<file>] [**--format**=<format>]`, | ||
Description: `**step certificate format** prints the certificate or CSR in a different format. | ||
|
||
Only 2 formats are currently supported; PEM and ASN.1 DER. This tool will convert | ||
a certificate or CSR in one format to the other. | ||
If either PEM or ASN.1 DER is provided as a positional argument, this command | ||
will convert a certificate or CSR in one format to the other. | ||
|
||
If PFX / PKCS12 file is provided as a positional argument, and the format is | ||
specified as "pem"/"der", this command extracts a certificate and private key | ||
from the input. | ||
|
||
If either PEM or ASN.1 DER is provided in "--crt" | "--key" | "--ca", and the | ||
format is specified as "p12", this command creates a PFX / PKCS12 file from the input . | ||
|
||
## POSITIONAL ARGUMENTS | ||
|
||
<crt-file> | ||
: Path to a certificate or CSR file. | ||
: Path to a certificate, CSR, or .p12 file. | ||
<crt-file> | ||
|
||
## EXIT CODES | ||
|
||
|
@@ -51,12 +64,72 @@ Convert PEM format to DER and write to disk: | |
''' | ||
$ step certificate format foo.pem --out foo.der | ||
''' | ||
|
||
Convert a .p12 file to a certificate and private key: | ||
|
||
''' | ||
$ step certificate format foo.p12 --crt foo.crt --key foo.key --format pem | ||
''' | ||
|
||
Convert a .p12 file to a certificate, private key and intermediate certificates: | ||
|
||
''' | ||
$ step certificate format foo.p12 --crt foo.crt --key foo.key --ca intermediate.crt --format pem | ||
''' | ||
Comment on lines
+68
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like if the flag |
||
|
||
Convert a certificate and private key to a .p12 file: | ||
|
||
''' | ||
$ step certificate format foo.crt --crt foo.p12 --key foo.key --format p12 | ||
''' | ||
|
||
Convert a certificate, a private key, and intermediate certificates(s) to a .p12 file: | ||
|
||
''' | ||
$ step certificate format foo.crt --crt foo.p12 --key foo.key \ | ||
--ca intermediate-1.crt --ca intermediate-2 --format p12 | ||
''' | ||
maraino marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`, | ||
Flags: []cli.Flag{ | ||
cli.StringFlag{ | ||
Name: "out", | ||
Usage: `Path to write the reformatted result.`, | ||
Name: "format", | ||
Usage: `The desired output <format> for the input. The default behavior is to | ||
convert between DER and PEM format. Acceptable inputs are 'pem', 'der', and 'p12'.`, | ||
maraino marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
}, | ||
cli.StringFlag{ | ||
Name: "crt", | ||
Usage: `The path to a certificate <file>. If --format is 'p12' then this flag | ||
must be a PEM or DER encoded certificate. If the positional argument is a P12 | ||
encoded file then this flag contains the name for the PEM or DER encoded leaf | ||
certificate extracted from the p12 file.`, | ||
}, | ||
cli.StringFlag{ | ||
Name: "key", | ||
Usage: `The path to a key <file>. If --format is 'p12' then this flag | ||
must be a PEM or DER encoded private key. If the positional argument is a P12 | ||
encoded file then this flag contains the name for the PEM or DER encoded private | ||
key extracted from the p12 file.`, | ||
}, | ||
cli.StringSliceFlag{ | ||
Name: "ca", | ||
Usage: `The path to a root or intermediate certificate <file>. If --format is 'p12' | ||
then this flag can be used to submit one or more CA files encoded as PEM or DER. | ||
Additional CA certificates can be added by using the --ca flag multiple times. | ||
If the positional argument is a p12 encoded file then this flag contains the | ||
name for the PEM or DER encoded certificate chain extracted from the p12 file.`, | ||
maraino marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
cli.StringFlag{ | ||
Name: "out", | ||
Usage: `The <file> to write the reformatted result. Only use this flag | ||
for conversions between PEM and DER. Conversions to P12 should use --crt, --key, | ||
and --ca.`, | ||
}, | ||
cli.StringFlag{ | ||
Name: "password-file", | ||
Usage: `The path to the <file> containing the password to encrypt/decrypt the .p12 file.`, | ||
}, | ||
flags.NoPassword, | ||
flags.Insecure, | ||
flags.Force, | ||
}, | ||
} | ||
|
@@ -67,15 +140,68 @@ func formatAction(ctx *cli.Context) error { | |
return err | ||
} | ||
|
||
var ( | ||
out = ctx.String("out") | ||
ob []byte | ||
) | ||
sourceFile := ctx.Args().First() | ||
format := ctx.String("format") | ||
crt := ctx.String("crt") | ||
key := ctx.String("key") | ||
ca := ctx.StringSlice("ca") | ||
out := ctx.String("out") | ||
passwordFile := ctx.String("password-file") | ||
noPassword := ctx.Bool("no-password") | ||
insecure := ctx.Bool("insecure") | ||
|
||
var crtFile string | ||
if ctx.NArg() == 1 { | ||
crtFile = ctx.Args().First() | ||
} else { | ||
if out != "" { | ||
if crt != "" { | ||
return errs.IncompatibleFlagWithFlag(ctx, "out", "crt") | ||
} | ||
if key != "" { | ||
return errs.IncompatibleFlagWithFlag(ctx, "out", "key") | ||
} | ||
if len(ca) != 0 { | ||
return errs.IncompatibleFlagWithFlag(ctx, "out", "ca") | ||
} | ||
if format != "" { | ||
return errs.IncompatibleFlagWithFlag(ctx, "out", "format") | ||
} | ||
} | ||
|
||
if passwordFile != "" && noPassword { | ||
return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file") | ||
} | ||
|
||
switch { | ||
case format == "pem" || format == "der": | ||
if len(ca) > 1 { | ||
return errors.Errorf("--ca option specified for multiple times when the target format is pem/der") | ||
} | ||
caFile := "" | ||
if len(ca) == 1 { | ||
caFile = ca[0] | ||
} | ||
if err := fromP12(sourceFile, crt, key, caFile, passwordFile, noPassword, format); err != nil { | ||
return err | ||
} | ||
case format == "p12": | ||
if noPassword && !insecure { | ||
return errs.RequiredInsecureFlag(ctx, "no-password") | ||
} | ||
if err := ToP12(crt, sourceFile, key, ca, passwordFile, noPassword, insecure); err != nil { | ||
return err | ||
} | ||
case format == "": | ||
if err := interconvertPemAndDer(sourceFile, out); err != nil { | ||
return err | ||
} | ||
maraino marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
default: | ||
return errs.InvalidFlagValue(ctx, "format", format, "") | ||
} | ||
return nil | ||
} | ||
|
||
func interconvertPemAndDer(crtFile, out string) error { | ||
var ob []byte | ||
|
||
if crtFile == "" { | ||
crtFile = "-" | ||
} | ||
|
||
|
@@ -116,7 +242,7 @@ func formatAction(ctx *cli.Context) error { | |
} | ||
} | ||
if err := utils.WriteFile(out, ob, mode); err != nil { | ||
return err | ||
return errs.FileError(err, out) | ||
} | ||
ui.Printf("Your certificate has been saved in %s\n", out) | ||
} | ||
|
@@ -144,10 +270,140 @@ func decodeCertificatePem(b []byte) ([]byte, error) { | |
return nil, errors.Wrap(err, "error parsing certificate request") | ||
} | ||
return csr.Raw, nil | ||
case "RSA PRIVATE KEY": | ||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error parsing RSA private key") | ||
} | ||
keyBytes := x509.MarshalPKCS1PrivateKey(key) | ||
return keyBytes, nil | ||
case "EC PRIVATE KEY": | ||
key, err := x509.ParseECPrivateKey(block.Bytes) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error parsing EC private key") | ||
} | ||
keyBytes, err := x509.MarshalECPrivateKey(key) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error converting EC private key to DER format") | ||
} | ||
return keyBytes, nil | ||
case "PRIVATE KEY": | ||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error parsing private key") | ||
} | ||
keyBytes, err := x509.MarshalPKCS8PrivateKey(key) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error converting private key to DER format") | ||
} | ||
return keyBytes, nil | ||
maraino marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
default: | ||
continue | ||
} | ||
} | ||
|
||
return nil, errors.Errorf("error decoding certificate: invalid PEM block") | ||
} | ||
|
||
func fromP12(p12File, crtFile, keyFile, caFile, passwordFile string, noPassword bool, format string) error { | ||
var err error | ||
var password string | ||
if passwordFile != "" { | ||
password, err = utils.ReadStringPasswordFromFile(passwordFile) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if password == "" && !noPassword { | ||
pass, err := ui.PromptPassword("Please enter a password to decrypt the .p12 file") | ||
if err != nil { | ||
return errs.Wrap(err, "error reading password") | ||
} | ||
password = string(pass) | ||
} | ||
|
||
p12Data, err := utils.ReadFile(p12File) | ||
if err != nil { | ||
return errs.Wrap(err, "error reading file %s", p12File) | ||
} | ||
|
||
key, crt, ca, err := pkcs12.DecodeChain(p12Data, password) | ||
if err != nil { | ||
return errs.Wrap(err, "failed to decode PKCS12 data") | ||
} | ||
|
||
if err := write(crtFile, format, crt); err != nil { | ||
return err | ||
} | ||
|
||
if err := writeCerts(caFile, format, ca); err != nil { | ||
return err | ||
} | ||
|
||
if err := write(keyFile, format, key); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func writeCerts(filename, format string, certs []*x509.Certificate) error { | ||
if len(certs) > 1 && format == "der" { | ||
return errors.Errorf("der format does not support a certificate bundle") | ||
} | ||
var data []byte | ||
for _, cert := range certs { | ||
b, err := toByte(cert, format) | ||
if err != nil { | ||
return err | ||
} | ||
data = append(data, b...) | ||
} | ||
if err := maybeWrite(filename, data); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func write(filename, format string, in interface{}) error { | ||
b, err := toByte(in, format) | ||
if err != nil { | ||
return err | ||
} | ||
if err := maybeWrite(filename, b); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func maybeWrite(filename string, out []byte) error { | ||
if filename == "" { | ||
os.Stdout.Write(out) | ||
} else { | ||
if err := utils.WriteFile(filename, out, 0600); err != nil { | ||
return errs.FileError(err, filename) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func toByte(in interface{}, format string) ([]byte, error) { | ||
pemblk, err := pemutil.Serialize(in) | ||
if err != nil { | ||
return nil, err | ||
} | ||
pemByte := pem.EncodeToMemory(pemblk) | ||
switch format { | ||
case "der": | ||
derByte, err := decodeCertificatePem(pemByte) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return derByte, nil | ||
case "pem", "": | ||
return pemByte, nil | ||
default: | ||
return nil, errors.Errorf("unsupported format: %s", format) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.