Skip to content
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

Add certificate extract command for conversion between P12, PEM, and DER #589

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
273 changes: 251 additions & 22 deletions command/certificate/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,43 @@
"encoding/pem"
"os"

"go.step.sm/crypto/pemutil"

maraino marked this conversation as resolved.
Show resolved Hide resolved
"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

Expand All @@ -51,12 +64,72 @@
'''
$ 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like if the flag --format is not passed it will write PEM files, as it should be. We should show this in one of these two examples, explaining that is the default behavior.


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 formats are 'pem', 'der', and 'p12'.`,
},
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,
},
}
Expand All @@ -67,15 +140,97 @@
return err
}

sourceFile := ctx.Args().First()
format := ctx.String("format")
crtFile := ctx.String("crt")
keyFile := ctx.String("key")
caFiles := ctx.StringSlice("ca")
out := ctx.String("out")
passwordFile := ctx.String("password-file")
noPassword := ctx.Bool("no-password")
insecure := ctx.Bool("insecure")

Check warning on line 151 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L143-L151

Added lines #L143 - L151 were not covered by tests

if out != "" {
if crtFile != "" {
return errs.IncompatibleFlagWithFlag(ctx, "out", "crt")

Check warning on line 155 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L153-L155

Added lines #L153 - L155 were not covered by tests
}
if keyFile != "" {
return errs.IncompatibleFlagWithFlag(ctx, "out", "key")

Check warning on line 158 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L157-L158

Added lines #L157 - L158 were not covered by tests
}
if format != "" {
return errs.IncompatibleFlagWithFlag(ctx, "out", "format")

Check warning on line 161 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L160-L161

Added lines #L160 - L161 were not covered by tests
}
}

if passwordFile != "" && noPassword {
return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file")

Check warning on line 166 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L165-L166

Added lines #L165 - L166 were not covered by tests
}

var (
out = ctx.String("out")
ob []byte
err error
pass = ""

Check warning on line 171 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L170-L171

Added lines #L170 - L171 were not covered by tests
)
if passwordFile != "" {
pass, err = utils.ReadStringPasswordFromFile(passwordFile)
if err != nil {
return errs.FileError(err, passwordFile)

Check warning on line 176 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L173-L176

Added lines #L173 - L176 were not covered by tests
}
}

var crtFile string
if ctx.NArg() == 1 {
crtFile = ctx.Args().First()
} else {
if sourceFile != "" {
srcBytes, err := os.ReadFile(sourceFile)
if err != nil {
return errs.FileError(err, sourceFile)

Check warning on line 183 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L180-L183

Added lines #L180 - L183 were not covered by tests
}

// First check if P12 input.
if keyFrom, crtFrom, caFrom, err := pkcs12.DecodeChain(srcBytes, pass); err == nil {
if format == "p12" {
return errors.Errorf("invalid flag --format with value 'p12'; cannot from P12 format to P12 format")

Check warning on line 189 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L187-L189

Added lines #L187 - L189 were not covered by tests
}
if len(caFrom) > 1 {
return errors.Errorf("flag --ca cannot be used multiple times when converting from P12 format")

Check warning on line 192 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L191-L192

Added lines #L191 - L192 were not covered by tests
}
caFile := ""
if len(caFiles) == 1 {
caFile = caFiles[0]

Check warning on line 196 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L194-L196

Added lines #L194 - L196 were not covered by tests
}
if err := write(crtFile, format, crtFrom); err != nil {
return err

Check warning on line 199 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L198-L199

Added lines #L198 - L199 were not covered by tests
}

if err := writeCerts(caFile, format, caFrom); err != nil {
return err

Check warning on line 203 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L202-L203

Added lines #L202 - L203 were not covered by tests
}

if err := write(keyFile, format, keyFrom); err != nil {
return err

Check warning on line 207 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L206-L207

Added lines #L206 - L207 were not covered by tests
}
}

// Now we know input is not P12 format. Check if we're converting to P12.
if format == "p12" {
if noPassword && !insecure {
return errs.RequiredInsecureFlag(ctx, "no-password")

Check warning on line 214 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L212-L214

Added lines #L212 - L214 were not covered by tests
}
return ToP12(out, crtFile, keyFile, caFiles, passwordFile, noPassword, insecure)

Check warning on line 216 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L216

Added line #L216 was not covered by tests
}

// Otherwise interconvert between PEM and DER.
return interconvertPemAndDer(sourceFile, out)

Check warning on line 220 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L220

Added line #L220 was not covered by tests
}

// If format is PEM or DER (not P12) then an input certificate file is required.
if format != "p12" {
return errors.Errorf("flag --format with value '%s' requires a certificate file as positional argument", format)

Check warning on line 225 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L224-L225

Added lines #L224 - L225 were not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commands like these will stop working:

$ cat cert.pem | step certificate format
... der data ...
$ cat cert.der | step certificate format
... pem data ...

}
return ToP12(out, crtFile, keyFile, caFiles, passwordFile, noPassword, insecure)

Check warning on line 227 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L227

Added line #L227 was not covered by tests
}

func interconvertPemAndDer(crtFile, out string) error {
var ob []byte

Check warning on line 231 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L230-L231

Added lines #L230 - L231 were not covered by tests

if crtFile == "" {

Check warning on line 233 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L233

Added line #L233 was not covered by tests
crtFile = "-"
}

Expand Down Expand Up @@ -116,7 +271,7 @@
}
}
if err := utils.WriteFile(out, ob, mode); err != nil {
return err
return errs.FileError(err, out)

Check warning on line 274 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L274

Added line #L274 was not covered by tests
}
ui.Printf("Your certificate has been saved in %s\n", out)
}
Expand All @@ -133,21 +288,95 @@
}
switch block.Type {
case "CERTIFICATE":
crt, err := x509.ParseCertificate(block.Bytes)
if err != nil {
if _, err := x509.ParseCertificate(block.Bytes); err != nil {

Check warning on line 291 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L291

Added line #L291 was not covered by tests
return nil, errors.Wrap(err, "error parsing certificate")
}
return crt.Raw, nil
return block.Bytes, nil

Check warning on line 294 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L294

Added line #L294 was not covered by tests
case "CERTIFICATE REQUEST":
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
if _, err := x509.ParseCertificateRequest(block.Bytes); err != nil {

Check warning on line 296 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L296

Added line #L296 was not covered by tests
return nil, errors.Wrap(err, "error parsing certificate request")
}
return csr.Raw, nil
return block.Bytes, nil
case "RSA PRIVATE KEY":
if _, err := x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
return nil, errors.Wrap(err, "error parsing RSA private key")

Check warning on line 302 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L299-L302

Added lines #L299 - L302 were not covered by tests
}
return block.Bytes, nil
case "EC PRIVATE KEY":
if _, err := x509.ParseECPrivateKey(block.Bytes); err != nil {
return nil, errors.Wrap(err, "error parsing EC private key")

Check warning on line 307 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L304-L307

Added lines #L304 - L307 were not covered by tests
}
return block.Bytes, nil
case "PRIVATE KEY":
if _, err := x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
return nil, errors.Wrap(err, "error parsing private key")

Check warning on line 312 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L309-L312

Added lines #L309 - L312 were not covered by tests
}

return block.Bytes, nil

Check warning on line 315 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L315

Added line #L315 was not covered by tests
default:
continue
}
}

return nil, errors.Errorf("error decoding certificate: invalid PEM block")
}

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")

Check warning on line 326 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L324-L326

Added lines #L324 - L326 were not covered by tests
}
var data []byte
for _, cert := range certs {
b, err := toByte(cert, format)
if err != nil {
return err

Check warning on line 332 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L328-L332

Added lines #L328 - L332 were not covered by tests
}
data = append(data, b...)

Check warning on line 334 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L334

Added line #L334 was not covered by tests
}
if err := maybeWrite(filename, data); err != nil {
return err

Check warning on line 337 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L336-L337

Added lines #L336 - L337 were not covered by tests
}
return nil

Check warning on line 339 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L339

Added line #L339 was not covered by tests
}

func write(filename, format string, in interface{}) error {
b, err := toByte(in, format)
if err != nil {
return err

Check warning on line 345 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L342-L345

Added lines #L342 - L345 were not covered by tests
}
if err := maybeWrite(filename, b); err != nil {
return err

Check warning on line 348 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L347-L348

Added lines #L347 - L348 were not covered by tests
}
return nil

Check warning on line 350 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L350

Added line #L350 was not covered by tests
}

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)

Check warning on line 358 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L353-L358

Added lines #L353 - L358 were not covered by tests
}
}
return nil

Check warning on line 361 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L361

Added line #L361 was not covered by tests
}

func toByte(in interface{}, format string) ([]byte, error) {
pemblk, err := pemutil.Serialize(in)
if err != nil {
return nil, err

Check warning on line 367 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L364-L367

Added lines #L364 - L367 were not covered by tests
}
pemByte := pem.EncodeToMemory(pemblk)
switch format {
case "der":
derByte, err := decodeCertificatePem(pemByte)
if err != nil {
return nil, err

Check warning on line 374 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L369-L374

Added lines #L369 - L374 were not covered by tests
}
return derByte, nil
case "pem", "":
return pemByte, nil
default:
return nil, errors.Errorf("unsupported format: %s", format)

Check warning on line 380 in command/certificate/format.go

View check run for this annotation

Codecov / codecov/patch

command/certificate/format.go#L376-L380

Added lines #L376 - L380 were not covered by tests
}
}
Loading