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

Allow to use certificates as keys and add command to extract public keys #70

Merged
merged 3 commits into from
Jan 25, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion command/certificate/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ Convert PEM format certificate to DER and write to disk.
'''
$ step certificate format foo.pem --out foo.der
'''

Extract the public key from a PEM encoded certificate:
'''
$ step certificate key foo.crt
'''
`,

Subcommands: cli.Commands{
Expand All @@ -73,9 +78,9 @@ $ step certificate format foo.pem --out foo.der
inspectCommand(),
fingerprintCommand(),
lintCommand(),
//renewCommand(),
signCommand(),
verifyCommand(),
keyCommand(),
},
}

Expand Down
91 changes: 91 additions & 0 deletions command/certificate/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package certificate

import (
"encoding/pem"
"fmt"

"github.com/smallstep/cli/flags"
"github.com/smallstep/cli/ui"

"github.com/smallstep/cli/command"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/errs"
"github.com/smallstep/cli/utils"
"github.com/urfave/cli"
)

func keyCommand() cli.Command {
return cli.Command{
Name: "key",
Action: command.ActionFunc(keyAction),
Usage: "print public key embedded in a certificate",
UsageText: "**step certificate key** <crt-file> [**--out**=<file>]",
Description: `**step certificate key** prints the public key embedded in a certificate or
a certificate signing request. If <crt-file> is a certificate bundle, only the
first block will be taken into account.

The command will print a public or a decrypted private key if <crt-file>
contains only a key.

## POSITIONAL ARGUMENTS

<crt-file>
: Path to a certificate or certificate signing request (CSR).

## EXAMPLES

Get the public key of a certificate:
'''
$ step certificate key certificate.crt
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEio9DLyuglMxakS3w00DUKdGbeXXB
2Mfg6tVofeXYan9RbvftZufiypIAVqGZqO7CR9EbkoyHb/7GcKQa5HZ9rA==
-----END PUBLIC KEY-----
'''

Get the public key of a CSR and save it to a file:
'''
$ step certificate key certificate.csr --out key.pem
'''`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "out,output-file",
Usage: "The destination <file> of the public key.",
},
flags.Force,
},
}
}

func keyAction(ctx *cli.Context) error {
if err := errs.NumberOfArguments(ctx, 1); err != nil {
return err
}

filename := ctx.Args().Get(0)
b, err := utils.ReadFile(filename)
if err != nil {
return err
}

// Look only at the first block
key, err := pemutil.ParseKey(b, pemutil.WithFirstBlock())
if err != nil {
return err
}
block, err := pemutil.Serialize(key)
if err != nil {
return err
}

if outputFile := ctx.String("output-file"); len(outputFile) > 0 {
if err := utils.WriteFile(outputFile, pem.EncodeToMemory(block), 0600); err != nil {
return err
}
ui.Printf("The public key has been saved in %s.\n", outputFile)
return nil
}

fmt.Print(string(pem.EncodeToMemory(block)))
return nil
}
18 changes: 18 additions & 0 deletions crypto/keys/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"math/big"

"github.com/pkg/errors"
Expand Down Expand Up @@ -58,6 +59,23 @@ func GenerateKey(kty, crv string, size int) (interface{}, error) {
}
}

// ExtractKey returns the given public or private key or extracts the public key
// if a x509.Certificate or x509.CertificateRequest is given.
func ExtractKey(in interface{}) (interface{}, error) {
switch k := in.(type) {
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
return in, nil
case []byte:
return in, nil
case *x509.Certificate:
return k.PublicKey, nil
case *x509.CertificateRequest:
maraino marked this conversation as resolved.
Show resolved Hide resolved
return k.PublicKey, nil
default:
return nil, errors.Errorf("cannot extract the key from type '%T'", k)
}
}

func generateECKey(crv string) (interface{}, error) {
var c elliptic.Curve
switch crv {
Expand Down
83 changes: 83 additions & 0 deletions crypto/keys/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,37 @@ package keys
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"reflect"
"testing"

"github.com/smallstep/assert"
"golang.org/x/crypto/ed25519"
)

const (
testCRT = `-----BEGIN CERTIFICATE-----
MIICLjCCAdSgAwIBAgIQBvswFbAODY9xtJ/myiuEHzAKBggqhkjOPQQDAjAkMSIw
IAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTE4MTEzMDE5NTkw
OVoXDTE4MTIwMTE5NTkwOVowHjEcMBoGA1UEAxMTaGVsbG8uc21hbGxzdGVwLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIqPQy8roJTMWpEt8NNA1CnRm3l1
wdjH4OrVaH3l2Gp/UW737Wbn4sqSAFahmajuwkfRG5KMh2/+xnCkGuR2fayjge0w
geowDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAdBgNVHQ4EFgQU5bqyXvZaEmtZ3OpZapq7pBIkVvgwHwYDVR0jBBgwFoAUu97P
aFQPfuyKOeew7Hg45WFIAVMwHgYDVR0RBBcwFYITaGVsbG8uc21hbGxzdGVwLmNv
bTBZBgwrBgEEAYKkZMYoQAEESTBHAgEBBBVtYXJpYW5vQHNtYWxsc3RlcC5jb20E
K2pPMzdkdERia3UtUW5hYnM1VlIwWXc2WUZGdjl3ZUExOGRwM2h0dmRFanMwCgYI
KoZIzj0EAwIDSAAwRQIhALKeC2q0HWyHoZobZFK9HQynLbPOOtAK437RaetlX5ty
AiBXQzvaLlDprQu+THj18aDYLnHA//5mdD3HPJV6KmgdDg==
-----END CERTIFICATE-----`
testCSR = `-----BEGIN CERTIFICATE REQUEST-----
MIHYMIGAAgEAMB4xHDAaBgNVBAMTE2hlbGxvLnNtYWxsc3RlcC5jb20wWTATBgcq
hkjOPQIBBggqhkjOPQMBBwNCAASKj0MvK6CUzFqRLfDTQNQp0Zt5dcHYx+Dq1Wh9
5dhqf1Fu9+1m5+LKkgBWoZmo7sJH0RuSjIdv/sZwpBrkdn2soAAwCgYIKoZIzj0E
AwIDRwAwRAIgZgz9gdx9inOp6bSX4EkYiUCyLV9xGvabovu5C9UkRr8CIBGBbkp0
l4tesAKoXelsLygJjPuUGRLK+OtdjPBIN1Zo
-----END CERTIFICATE REQUEST-----`
)

func TestGenerateKey_unrecognizedkt(t *testing.T) {
Expand Down Expand Up @@ -50,3 +78,58 @@ func TestGenerateKey_unrecognizedkt(t *testing.T) {
assert.True(t, ok)
}
}

func TestExtractKey(t *testing.T) {
k, err := GenerateKey("RSA", "", 2048)
assert.FatalError(t, err)
rsaKey := k.(*rsa.PrivateKey)
k, err = GenerateKey("EC", "P-256", 0)
assert.FatalError(t, err)
ecKey := k.(*ecdsa.PrivateKey)
k, err = GenerateKey("OKP", "Ed25519", 0)
assert.FatalError(t, err)
edKey := k.(ed25519.PrivateKey)
k, err = GenerateKey("oct", "", 64)
assert.FatalError(t, err)
octKey := k.([]byte)

b, _ := pem.Decode([]byte(testCRT))
cert, err := x509.ParseCertificate(b.Bytes)
assert.FatalError(t, err)
b, _ = pem.Decode([]byte(testCSR))
csr, err := x509.ParseCertificateRequest(b.Bytes)
assert.FatalError(t, err)

type args struct {
in interface{}
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{"RSA private key", args{rsaKey}, rsaKey, false},
{"RSA public key", args{rsaKey.Public()}, rsaKey.Public(), false},
{"EC private key", args{ecKey}, ecKey, false},
{"EC public key", args{ecKey.Public()}, ecKey.Public(), false},
{"OKP private key", args{edKey}, edKey, false},
{"OKP public key", args{edKey.Public()}, edKey.Public(), false},
{"oct key", args{octKey}, octKey, false},
{"certificate", args{cert}, cert.PublicKey, false},
{"csr", args{csr}, csr.PublicKey, false},
{"fail", args{"fooo"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ExtractKey(tt.args.in)
if (err != nil) != tt.wantErr {
t.Errorf("ExtractKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ExtractKey() = %v, want %v", got, tt.want)
}
})
}
}
23 changes: 22 additions & 1 deletion crypto/pemutil/pem.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os"

"github.com/pkg/errors"
"github.com/smallstep/cli/crypto/keys"
"github.com/smallstep/cli/errs"
"github.com/smallstep/cli/pkg/x509"
"github.com/smallstep/cli/ui"
Expand All @@ -31,6 +32,7 @@ type context struct {
password []byte
pkcs8 bool
stepCrypto bool
firstBlock bool
}

// newContext initializes the context with a filename.
Expand Down Expand Up @@ -127,6 +129,15 @@ func WithStepCrypto() Options {
}
}

// WithFirstBlock will avoid failing if a PEM contains more than one block or
// certificate and it will only look at the first.
func WithFirstBlock() Options {
return func(ctx *context) error {
ctx.firstBlock = true
return nil
}
}

// ReadCertificate returns a *x509.Certificate from the given filename. It
// supports certificates formats PEM and DER.
func ReadCertificate(filename string) (*realx509.Certificate, error) {
Expand Down Expand Up @@ -193,7 +204,7 @@ func Parse(b []byte, opts ...Options) (interface{}, error) {
switch {
case block == nil:
return nil, errors.Errorf("error decoding %s: is not a valid PEM encoded key", ctx.filename)
case len(rest) > 0:
case len(rest) > 0 && !ctx.firstBlock:
return nil, errors.Errorf("error decoding %s: contains more than one key", ctx.filename)
}

Expand Down Expand Up @@ -249,6 +260,16 @@ func Parse(b []byte, opts ...Options) (interface{}, error) {
}
}

// ParseKey returns the key or the public key of a certificate or certificate
// signing request in the given PEM-encoded bytes.
func ParseKey(b []byte, opts ...Options) (interface{}, error) {
k, err := Parse(b, opts...)
if err != nil {
return nil, err
}
return keys.ExtractKey(k)
}

// Read returns the key or certificate encoded in the given PEM file.
// If the file is encrypted it will ask for a password and it will try
// to decrypt it.
Expand Down
70 changes: 70 additions & 0 deletions crypto/pemutil/pem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ const (
rsaPrivateKey
)

const (
testCRT = `-----BEGIN CERTIFICATE-----
MIICLjCCAdSgAwIBAgIQBvswFbAODY9xtJ/myiuEHzAKBggqhkjOPQQDAjAkMSIw
IAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTE4MTEzMDE5NTkw
OVoXDTE4MTIwMTE5NTkwOVowHjEcMBoGA1UEAxMTaGVsbG8uc21hbGxzdGVwLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIqPQy8roJTMWpEt8NNA1CnRm3l1
wdjH4OrVaH3l2Gp/UW737Wbn4sqSAFahmajuwkfRG5KMh2/+xnCkGuR2fayjge0w
geowDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAdBgNVHQ4EFgQU5bqyXvZaEmtZ3OpZapq7pBIkVvgwHwYDVR0jBBgwFoAUu97P
aFQPfuyKOeew7Hg45WFIAVMwHgYDVR0RBBcwFYITaGVsbG8uc21hbGxzdGVwLmNv
bTBZBgwrBgEEAYKkZMYoQAEESTBHAgEBBBVtYXJpYW5vQHNtYWxsc3RlcC5jb20E
K2pPMzdkdERia3UtUW5hYnM1VlIwWXc2WUZGdjl3ZUExOGRwM2h0dmRFanMwCgYI
KoZIzj0EAwIDSAAwRQIhALKeC2q0HWyHoZobZFK9HQynLbPOOtAK437RaetlX5ty
AiBXQzvaLlDprQu+THj18aDYLnHA//5mdD3HPJV6KmgdDg==
-----END CERTIFICATE-----`
testCSR = `-----BEGIN CERTIFICATE REQUEST-----
MIHYMIGAAgEAMB4xHDAaBgNVBAMTE2hlbGxvLnNtYWxsc3RlcC5jb20wWTATBgcq
hkjOPQIBBggqhkjOPQMBBwNCAASKj0MvK6CUzFqRLfDTQNQp0Zt5dcHYx+Dq1Wh9
5dhqf1Fu9+1m5+LKkgBWoZmo7sJH0RuSjIdv/sZwpBrkdn2soAAwCgYIKoZIzj0E
AwIDRwAwRAIgZgz9gdx9inOp6bSX4EkYiUCyLV9xGvabovu5C9UkRr8CIBGBbkp0
l4tesAKoXelsLygJjPuUGRLK+OtdjPBIN1Zo
-----END CERTIFICATE REQUEST-----`
)

type testdata struct {
typ keyType
encrypted bool
Expand Down Expand Up @@ -554,3 +578,49 @@ func TestParseDER(t *testing.T) {
})
}
}

func TestParseKey(t *testing.T) {
var key interface{}
for fn, td := range files {
t.Run(fn, func(t *testing.T) {
data, err := ioutil.ReadFile(fn)
if td.encrypted {
key, err = ParseKey(data, WithPassword([]byte("mypassword")))
} else {
key, err = ParseKey(data)
}
assert.NotNil(t, key)
assert.NoError(t, err)

switch td.typ {
case ecdsaPublicKey:
assert.Type(t, &ecdsa.PublicKey{}, key)
case ecdsaPrivateKey:
assert.Type(t, &ecdsa.PrivateKey{}, key)
case ed25519PublicKey:
assert.Type(t, ed25519.PublicKey{}, key)
case ed25519PrivateKey:
assert.Type(t, ed25519.PrivateKey{}, key)
case rsaPublicKey:
assert.Type(t, &rsa.PublicKey{}, key)
case rsaPrivateKey:
assert.Type(t, &rsa.PrivateKey{}, key)
default:
t.Errorf("type %T not supported", key)
}
})
}
}
func TestParseKey_x509(t *testing.T) {
b, _ := pem.Decode([]byte(testCRT))
cert, err := x509.ParseCertificate(b.Bytes)
assert.FatalError(t, err)
key, err := ParseKey([]byte(testCRT))
assert.Equals(t, cert.PublicKey, key)

b, _ = pem.Decode([]byte(testCSR))
csr, err := x509.ParseCertificateRequest(b.Bytes)
assert.FatalError(t, err)
key, err = ParseKey([]byte(testCSR))
assert.Equals(t, csr.PublicKey, key)
}
2 changes: 1 addition & 1 deletion jose/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func ParseKey(filename string, opts ...Option) (*JSONWebKey, error) {
return nil, errors.Errorf("error reading %s: unsupported format", filename)
}
case pemKeyType:
jwk.Key, err = pemutil.Parse(b, pemutil.WithFilename(filename), pemutil.WithPassword(ctx.password))
jwk.Key, err = pemutil.ParseKey(b, pemutil.WithFilename(filename), pemutil.WithPassword(ctx.password))
if err != nil {
return nil, err
}
Expand Down