From 08f33f02943a1ab227770af105dd206baf5d10f5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 23 Jan 2019 18:19:05 -0800 Subject: [PATCH 1/3] Allow to extract keys from certificates and CSRs Fixes #69 --- crypto/keys/key.go | 18 +++++++++ crypto/keys/key_test.go | 83 ++++++++++++++++++++++++++++++++++++++ crypto/pemutil/pem.go | 11 +++++ crypto/pemutil/pem_test.go | 70 ++++++++++++++++++++++++++++++++ jose/parse.go | 2 +- 5 files changed, 183 insertions(+), 1 deletion(-) diff --git a/crypto/keys/key.go b/crypto/keys/key.go index d28790c09..9d2eba97c 100644 --- a/crypto/keys/key.go +++ b/crypto/keys/key.go @@ -5,6 +5,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/x509" "math/big" "github.com/pkg/errors" @@ -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: + 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 { diff --git a/crypto/keys/key_test.go b/crypto/keys/key_test.go index bd3a7c20b..e15f6095d 100644 --- a/crypto/keys/key_test.go +++ b/crypto/keys/key_test.go @@ -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) { @@ -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) + } + }) + } +} diff --git a/crypto/pemutil/pem.go b/crypto/pemutil/pem.go index ac0b38ead..409834214 100644 --- a/crypto/pemutil/pem.go +++ b/crypto/pemutil/pem.go @@ -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" @@ -249,6 +250,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. diff --git a/crypto/pemutil/pem_test.go b/crypto/pemutil/pem_test.go index 0b6ed20eb..cdf033152 100644 --- a/crypto/pemutil/pem_test.go +++ b/crypto/pemutil/pem_test.go @@ -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 @@ -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) +} diff --git a/jose/parse.go b/jose/parse.go index b65a3c6e5..93c9ea404 100644 --- a/jose/parse.go +++ b/jose/parse.go @@ -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 } From e9052f68190b684fa4e6c5b8c1810a05c7d58307 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 23 Jan 2019 19:09:44 -0800 Subject: [PATCH 2/3] Add a command to get the public key of a certificate or CSR Fixes #68 --- command/certificate/certificate.go | 7 ++- command/certificate/key.go | 91 ++++++++++++++++++++++++++++++ crypto/pemutil/pem.go | 12 +++- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 command/certificate/key.go diff --git a/command/certificate/certificate.go b/command/certificate/certificate.go index 5626eb61a..d894eaef8 100644 --- a/command/certificate/certificate.go +++ b/command/certificate/certificate.go @@ -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{ @@ -73,9 +78,9 @@ $ step certificate format foo.pem --out foo.der inspectCommand(), fingerprintCommand(), lintCommand(), - //renewCommand(), signCommand(), verifyCommand(), + keyCommand(), }, } diff --git a/command/certificate/key.go b/command/certificate/key.go new file mode 100644 index 000000000..9a2c77871 --- /dev/null +++ b/command/certificate/key.go @@ -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** [**--out**=]", + Description: `**step certificate key** prints the public key embedded in a certificate or +a certificate signing request. If 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 +contains only a key. + +## POSITIONAL ARGUMENTS + + +: 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 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 +} diff --git a/crypto/pemutil/pem.go b/crypto/pemutil/pem.go index 409834214..aa6bba672 100644 --- a/crypto/pemutil/pem.go +++ b/crypto/pemutil/pem.go @@ -32,6 +32,7 @@ type context struct { password []byte pkcs8 bool stepCrypto bool + firstBlock bool } // newContext initializes the context with a filename. @@ -128,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) { @@ -194,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) } From 20e174d4987f1e5a3ae18918719e5a2ffaf186bd Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 25 Jan 2019 10:28:51 -0800 Subject: [PATCH 3/3] Add cases for stepx509 --- crypto/keys/key.go | 5 +++++ crypto/keys/key_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/crypto/keys/key.go b/crypto/keys/key.go index 9d2eba97c..12f93a334 100644 --- a/crypto/keys/key.go +++ b/crypto/keys/key.go @@ -9,6 +9,7 @@ import ( "math/big" "github.com/pkg/errors" + stepx509 "github.com/smallstep/cli/pkg/x509" "golang.org/x/crypto/ed25519" ) @@ -71,6 +72,10 @@ func ExtractKey(in interface{}) (interface{}, error) { return k.PublicKey, nil case *x509.CertificateRequest: return k.PublicKey, nil + case *stepx509.Certificate: + return k.PublicKey, nil + case *stepx509.CertificateRequest: + return k.PublicKey, nil default: return nil, errors.Errorf("cannot extract the key from type '%T'", k) } diff --git a/crypto/keys/key_test.go b/crypto/keys/key_test.go index e15f6095d..4fe565266 100644 --- a/crypto/keys/key_test.go +++ b/crypto/keys/key_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/smallstep/assert" + stepx509 "github.com/smallstep/cli/pkg/x509" "golang.org/x/crypto/ed25519" ) @@ -96,9 +97,14 @@ func TestExtractKey(t *testing.T) { b, _ := pem.Decode([]byte(testCRT)) cert, err := x509.ParseCertificate(b.Bytes) assert.FatalError(t, err) + stepCert, err := stepx509.ParseCertificate(b.Bytes) + assert.FatalError(t, err) + b, _ = pem.Decode([]byte(testCSR)) csr, err := x509.ParseCertificateRequest(b.Bytes) assert.FatalError(t, err) + stepCsr, err := stepx509.ParseCertificateRequest(b.Bytes) + assert.FatalError(t, err) type args struct { in interface{} @@ -118,6 +124,8 @@ func TestExtractKey(t *testing.T) { {"oct key", args{octKey}, octKey, false}, {"certificate", args{cert}, cert.PublicKey, false}, {"csr", args{csr}, csr.PublicKey, false}, + {"step certificate", args{stepCert}, stepCert.PublicKey, false}, + {"step csr", args{stepCsr}, stepCsr.PublicKey, false}, {"fail", args{"fooo"}, nil, true}, } for _, tt := range tests {