From b0f768a3fb095b426a654dce11067d947ab9da77 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 19 May 2020 17:32:52 -0700 Subject: [PATCH 01/13] Add implementation of URIs for KMS. Implementation is based on the PKCS #11 URI Scheme RFC https://tools.ietf.org/html/rfc7512 --- kms/uri/uri.go | 86 +++++++++++++++++++ kms/uri/uri_test.go | 198 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 kms/uri/uri.go create mode 100644 kms/uri/uri_test.go diff --git a/kms/uri/uri.go b/kms/uri/uri.go new file mode 100644 index 00000000..02bec42c --- /dev/null +++ b/kms/uri/uri.go @@ -0,0 +1,86 @@ +package uri + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// URI implements a parser for a URI format based on the the PKCS #11 URI Scheme +// defined in https://tools.ietf.org/html/rfc7512 +// +// These URIs will be used to define the key names in a KMS. +type URI struct { + *url.URL + Values url.Values +} + +// New creates a new URI from a scheme and key-value pairs. +func New(scheme string, values url.Values) *URI { + return &URI{ + URL: &url.URL{ + Scheme: scheme, + Opaque: strings.ReplaceAll(values.Encode(), "&", ";"), + }, + Values: values, + } +} + +// NewFile creates an uri for a file. +func NewFile(path string) *URI { + return &URI{ + URL: &url.URL{ + Scheme: "file", + Path: path, + }, + } +} + +// HasScheme returns true if the given uri has the given scheme, false otherwise. +func HasScheme(scheme, rawuri string) bool { + u, err := url.Parse(rawuri) + if err != nil { + return false + } + return strings.EqualFold(u.Scheme, scheme) +} + +// Parse returns the URI for the given string or an error. +func Parse(rawuri string) (*URI, error) { + u, err := url.Parse(rawuri) + if err != nil { + return nil, errors.Wrapf(err, "error parsing %s", rawuri) + } + if u.Scheme == "" { + return nil, errors.Errorf("error parsing %s: scheme is missing", rawuri) + } + v, err := url.ParseQuery(u.Opaque) + if err != nil { + return nil, errors.Wrapf(err, "error parsing %s", rawuri) + } + + return &URI{ + URL: u, + Values: v, + }, nil +} + +// ParseWithScheme returns the URI for the given string only if it has the given +// scheme. +func ParseWithScheme(scheme, rawuri string) (*URI, error) { + u, err := Parse(rawuri) + if err != nil { + return nil, err + } + if !strings.EqualFold(u.Scheme, scheme) { + return nil, errors.Errorf("error parsing %s: scheme not expected", rawuri) + } + return u, nil +} + +// Get returns the first value in the uri with the give n key, it will return +// empty string if that field is not present. +func (u *URI) Get(key string) string { + return u.Values.Get(key) +} diff --git a/kms/uri/uri_test.go b/kms/uri/uri_test.go new file mode 100644 index 00000000..68746f90 --- /dev/null +++ b/kms/uri/uri_test.go @@ -0,0 +1,198 @@ +package uri + +import ( + "net/url" + "reflect" + "testing" +) + +func TestNew(t *testing.T) { + type args struct { + scheme string + values url.Values + } + tests := []struct { + name string + args args + want *URI + }{ + {"ok", args{"yubikey", url.Values{"slot-id": []string{"9a"}}}, &URI{ + URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"}, + Values: url.Values{"slot-id": []string{"9a"}}, + }}, + {"ok multiple", args{"yubikey", url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}}}, &URI{ + URL: &url.URL{Scheme: "yubikey", Opaque: "foo=bar;slot-id=9a"}, + Values: url.Values{ + "slot-id": []string{"9a"}, + "foo": []string{"bar"}, + }, + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := New(tt.args.scheme, tt.args.values); !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewFile(t *testing.T) { + type args struct { + path string + } + tests := []struct { + name string + args args + want *URI + }{ + {"ok", args{"/tmp/ca.crt"}, &URI{ + URL: &url.URL{Scheme: "file", Path: "/tmp/ca.crt"}, + Values: url.Values(nil), + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewFile(tt.args.path); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHasScheme(t *testing.T) { + type args struct { + scheme string + rawuri string + } + tests := []struct { + name string + args args + want bool + }{ + {"ok", args{"yubikey", "yubikey:slot-id=9a"}, true}, + {"ok empty", args{"yubikey", "yubikey:"}, true}, + {"ok letter case", args{"awsKMS", "AWSkms:key-id=abcdefg?foo=bar"}, true}, + {"fail", args{"yubikey", "awskms:key-id=abcdefg"}, false}, + {"fail parse", args{"yubikey", "yubi%key:slot-id=9a"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := HasScheme(tt.args.scheme, tt.args.rawuri); got != tt.want { + t.Errorf("HasScheme() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParse(t *testing.T) { + type args struct { + rawuri string + } + tests := []struct { + name string + args args + want *URI + wantErr bool + }{ + {"ok", args{"yubikey:slot-id=9a"}, &URI{ + URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"}, + Values: url.Values{"slot-id": []string{"9a"}}, + }, false}, + {"ok file", args{"file:///tmp/ca.cert"}, &URI{ + URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"}, + Values: url.Values{}, + }, false}, + {"ok file simple", args{"file:/tmp/ca.cert"}, &URI{ + URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"}, + Values: url.Values{}, + }, false}, + {"ok file host", args{"file://tmp/ca.cert"}, &URI{ + URL: &url.URL{Scheme: "file", Host: "tmp", Path: "/ca.cert"}, + Values: url.Values{}, + }, false}, + {"fail parse", args{"yubi%key:slot-id=9a"}, nil, true}, + {"fail scheme", args{"yubikey"}, nil, true}, + {"fail parse opaque", args{"yubikey:slot-id=%ZZ"}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.args.rawuri) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() = %#v, want %v", got.URL, tt.want) + } + }) + } +} + +func TestParseWithScheme(t *testing.T) { + type args struct { + scheme string + rawuri string + } + tests := []struct { + name string + args args + want *URI + wantErr bool + }{ + {"ok", args{"yubikey", "yubikey:slot-id=9a"}, &URI{ + URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"}, + Values: url.Values{"slot-id": []string{"9a"}}, + }, false}, + {"ok file", args{"file", "file:///tmp/ca.cert"}, &URI{ + URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"}, + Values: url.Values{}, + }, false}, + {"fail parse", args{"yubikey", "yubikey"}, nil, true}, + {"fail scheme", args{"yubikey", "awskms:slot-id=9a"}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseWithScheme(tt.args.scheme, tt.args.rawuri) + if (err != nil) != tt.wantErr { + t.Errorf("ParseWithScheme() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseWithScheme() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestURI_Get(t *testing.T) { + mustParse := func(s string) *URI { + u, err := Parse(s) + if err != nil { + t.Fatal(err) + } + return u + } + type args struct { + key string + } + tests := []struct { + name string + uri *URI + args args + want string + }{ + {"ok", mustParse("yubikey:slot-id=9a"), args{"slot-id"}, "9a"}, + {"ok first", mustParse("yubikey:slot-id=9a;slot-id=9b"), args{"slot-id"}, "9a"}, + {"ok multiple", mustParse("yubikey:slot-id=9a;foo=bar"), args{"foo"}, "bar"}, + {"fail missing", mustParse("yubikey:slot-id=9a"), args{"foo"}, ""}, + {"fail in query", mustParse("yubikey:slot-id=9a?foo=bar"), args{"foo"}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.uri.Get(tt.args.key); got != tt.want { + t.Errorf("URI.Get() = %v, want %v", got, tt.want) + } + }) + } +} From c32abb76cda54400e1381450cdc2c22e9348fe48 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 19 May 2020 17:35:36 -0700 Subject: [PATCH 02/13] Add initial implementation to support AWS KMS. --- go.mod | 5 +- go.sum | 14 +++ kms/apiv1/options.go | 12 ++- kms/awskms/awskms.go | 237 +++++++++++++++++++++++++++++++++++++++++++ kms/awskms/signer.go | 121 ++++++++++++++++++++++ kms/kms.go | 1 + 6 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 kms/awskms/awskms.go create mode 100644 kms/awskms/signer.go diff --git a/go.mod b/go.mod index c81244cf..49f6c511 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,14 @@ go 1.13 require ( cloud.google.com/go v0.51.0 github.com/Masterminds/sprig/v3 v3.0.0 + github.com/aws/aws-sdk-go v1.30.29 github.com/go-chi/chi v4.0.2+incompatible github.com/go-piv/piv-go v1.5.0 github.com/googleapis/gax-go/v2 v2.0.5 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/newrelic/go-agent v2.15.0+incompatible - github.com/pkg/errors v0.8.1 + github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15 @@ -19,7 +20,7 @@ require ( github.com/smallstep/nosql v0.3.0 github.com/urfave/cli v1.22.2 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 - golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/net v0.0.0-20200202094626-16171245cfb2 google.golang.org/api v0.15.0 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb google.golang.org/grpc v1.26.0 diff --git a/go.sum b/go.sum index 7a9caaef..1bd57d61 100644 --- a/go.sum +++ b/go.sum @@ -43,7 +43,10 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.19.18 h1:Hb3+b9HCqrOrbAtFstUWg7H5TQ+/EcklJtE8VShVs8o= github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0= +github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -128,6 +131,8 @@ github.com/go-piv/piv-go v1.5.0 h1:UtHPfrJsZKY+Z3UIjmJLh6DY+KtmNOl/9b/zt4N81pM= github.com/go-piv/piv-go v1.5.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= @@ -268,7 +273,10 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -386,6 +394,8 @@ github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= @@ -500,6 +510,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= @@ -618,6 +630,8 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index c136e3f4..05421bd2 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -57,7 +57,7 @@ type Options struct { // The type of the KMS to use. Type string `json:"type"` - // Path to the credentials file used in CloudKMS. + // Path to the credentials file used in CloudKMS and AmazonKMS. CredentialsFile string `json:"credentialsFile"` // Path to the module used with PKCS11 KMS. @@ -65,6 +65,12 @@ type Options struct { // Pin used to access the PKCS11 module. Pin string `json:"pin"` + + // Region to use in AmazonKMS. + Region string `json:"region"` + + // Profile to use in AmazonKMS. + Profile string `json:"profile"` } // Validate checks the fields in Options. @@ -74,10 +80,8 @@ func (o *Options) Validate() error { } switch Type(strings.ToLower(o.Type)) { - case DefaultKMS, SoftKMS, CloudKMS: + case DefaultKMS, SoftKMS, CloudKMS, AmazonKMS: case YubiKey: - case AmazonKMS: - return ErrNotImplemented{"support for AmazonKMS is not yet implemented"} case PKCS11: return ErrNotImplemented{"support for PKCS11 is not yet implemented"} default: diff --git a/kms/awskms/awskms.go b/kms/awskms/awskms.go new file mode 100644 index 00000000..aea157b2 --- /dev/null +++ b/kms/awskms/awskms.go @@ -0,0 +1,237 @@ +package awskms + +import ( + "context" + "crypto" + "net/url" + "strings" + "time" + + "github.com/smallstep/certificates/kms/uri" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/pemutil" +) + +// KMS implements a KMS using AWS Key Management Service. +type KMS struct { + session *session.Session + service *kms.KMS +} + +// customerMasterKeySpecMapping is a mapping between the step signature algorithm, +// and bits for RSA keys, with awskms CustomerMasterKeySpec. +var customerMasterKeySpecMapping = map[apiv1.SignatureAlgorithm]interface{}{ + apiv1.UnspecifiedSignAlgorithm: kms.CustomerMasterKeySpecEccNistP256, + apiv1.SHA256WithRSA: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa3072, + 2048: kms.CustomerMasterKeySpecRsa2048, + 3072: kms.CustomerMasterKeySpecRsa3072, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.SHA512WithRSA: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa4096, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.SHA256WithRSAPSS: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa3072, + 2048: kms.CustomerMasterKeySpecRsa2048, + 3072: kms.CustomerMasterKeySpecRsa3072, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.SHA512WithRSAPSS: map[int]string{ + 0: kms.CustomerMasterKeySpecRsa4096, + 4096: kms.CustomerMasterKeySpecRsa4096, + }, + apiv1.ECDSAWithSHA256: kms.CustomerMasterKeySpecEccNistP256, + apiv1.ECDSAWithSHA384: kms.CustomerMasterKeySpecEccNistP384, + apiv1.ECDSAWithSHA512: kms.CustomerMasterKeySpecEccNistP521, +} + +// New creates a new AWSKMS. By default, sessions will be created using the +// credentials in `~/.aws/credentials`, but this can be overridden using the +// CredentialsFile option, the Region and Profile can also be configured as +// options. +// +// AWS sessions can also be configured with environment variables, see docs at +// https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for all the options. +func New(ctx context.Context, opts apiv1.Options) (*KMS, error) { + o := session.Options{} + if opts.Region != "" { + o.Config.Region = &opts.Region + } + if opts.Profile != "" { + o.Profile = opts.Profile + } + if opts.CredentialsFile != "" { + o.SharedConfigFiles = []string{opts.CredentialsFile} + } + + sess, err := session.NewSessionWithOptions(o) + if err != nil { + return nil, errors.Wrap(err, "error creating AWS session") + } + + return &KMS{ + session: sess, + service: kms.New(sess), + }, nil +} + +func init() { + apiv1.Register(apiv1.AmazonKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + +// GetPublicKey returns a public key from KMS. +func (k *KMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + if req.Name == "" { + return nil, errors.New("getPublicKey 'name' cannot be empty") + } + keyID, err := parseKeyID(req.Name) + if err != nil { + return nil, err + } + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := k.service.GetPublicKeyWithContext(ctx, &kms.GetPublicKeyInput{ + KeyId: &keyID, + }) + if err != nil { + return nil, errors.Wrap(err, "awskms GetPublicKeyWithContext failed") + } + + return pemutil.ParseDER(resp.PublicKey) +} + +// CreateKey generates a new key in KMS and returns the public key version +// of it. +func (k *KMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + if req.Name == "" { + return nil, errors.New("createKeyRequest 'name' cannot be empty") + } + + keySpec, err := getCustomerMasterKeySpecMapping(req.SignatureAlgorithm, req.Bits) + if err != nil { + return nil, err + } + + tag := new(kms.Tag) + tag.SetTagKey("name") + tag.SetTagValue(req.Name) + + input := &kms.CreateKeyInput{ + Description: &req.Name, + CustomerMasterKeySpec: &keySpec, + Tags: []*kms.Tag{tag}, + } + input.SetKeyUsage(kms.KeyUsageTypeSignVerify) + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := k.service.CreateKeyWithContext(ctx, input) + if err != nil { + return nil, errors.Wrap(err, "awskms CreateKeyWithContext failed") + } + if err := k.createKeyAlias(*resp.KeyMetadata.KeyId, req.Name); err != nil { + return nil, err + } + + // Create uri for key + name := uri.New("awskms", url.Values{ + "key-id": []string{*resp.KeyMetadata.KeyId}, + }).String() + + publicKey, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{ + Name: name, + }) + if err != nil { + return nil, err + } + + // Names uses Amazon Resource Name + // https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms + return &apiv1.CreateKeyResponse{ + Name: name, + PublicKey: publicKey, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: name, + }, + }, nil +} + +func (k *KMS) createKeyAlias(keyID, alias string) error { + alias = "alias/" + alias + "-" + keyID[:8] + + ctx, cancel := defaultContext() + defer cancel() + + _, err := k.service.CreateAliasWithContext(ctx, &kms.CreateAliasInput{ + AliasName: &alias, + TargetKeyId: &keyID, + }) + if err != nil { + return errors.Wrap(err, "awskms CreateAliasWithContext failed") + } + return nil +} + +// CreateSigner creates a new crypto.Signer with a previously configured key. +func (k *KMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + if req.SigningKey == "" { + return nil, errors.New("createSigner 'signingKey' cannot be empty") + } + return NewSigner(k.service, req.SigningKey) +} + +// Close closes the connection of the KMS client. +func (k *KMS) Close() error { + return nil +} + +func defaultContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 15*time.Second) +} + +// parseKeyID extracts the key-id from an uri. +func parseKeyID(name string) (string, error) { + name = strings.ToLower(name) + if strings.HasPrefix(name, "awskms:") || strings.HasPrefix(name, "aws:") { + u, err := uri.Parse(name) + if err != nil { + return "", err + } + if k := u.Get("key-id"); k != "" { + return k, nil + } + return "", errors.Errorf("failed to get key-id from %s", name) + } + return name, nil +} + +func getCustomerMasterKeySpecMapping(alg apiv1.SignatureAlgorithm, bits int) (string, error) { + v, ok := customerMasterKeySpecMapping[alg] + if !ok { + return "", errors.Errorf("awskms does not support signature algorithm '%s'", alg) + } + + switch v := v.(type) { + case string: + return v, nil + case map[int]string: + s, ok := v[bits] + if !ok { + return "", errors.Errorf("awskms does not support signature algorithm '%s' with '%d' bits", alg, bits) + } + return s, nil + default: + return "", errors.Errorf("unexpected error: this should not happen") + } +} diff --git a/kms/awskms/signer.go b/kms/awskms/signer.go new file mode 100644 index 00000000..194eeae4 --- /dev/null +++ b/kms/awskms/signer.go @@ -0,0 +1,121 @@ +package awskms + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "io" + + "github.com/aws/aws-sdk-go/service/kms" + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/pemutil" +) + +type Signer struct { + service *kms.KMS + keyID string + publicKey crypto.PublicKey +} + +// NewSigner creates a new signer using a key in the AWS KMS. +func NewSigner(svc *kms.KMS, signingKey string) (*Signer, error) { + keyID, err := parseKeyID(signingKey) + if err != nil { + return nil, err + } + + // Make sure that the key exists. + signer := &Signer{ + service: svc, + keyID: keyID, + } + if err := signer.preloadKey(keyID); err != nil { + return nil, err + } + + return signer, nil +} + +func (s *Signer) preloadKey(keyID string) error { + ctx, cancel := defaultContext() + defer cancel() + + resp, err := s.service.GetPublicKeyWithContext(ctx, &kms.GetPublicKeyInput{ + KeyId: &keyID, + }) + if err != nil { + return errors.Wrap(err, "awskms GetPublicKeyWithContext failed") + } + + s.publicKey, err = pemutil.ParseDER(resp.PublicKey) + return err +} + +// Public returns the public key of this signer or an error. +func (s *Signer) Public() crypto.PublicKey { + return s.publicKey +} + +// Sign signs digest with the private key stored in the AWS KMS. +func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + alg, err := getSigningAlgorithm(s.Public(), opts) + if err != nil { + return nil, err + } + + req := &kms.SignInput{ + KeyId: &s.keyID, + SigningAlgorithm: &alg, + Message: digest, + } + req.SetMessageType("DIGEST") + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := s.service.SignWithContext(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "awsKMS SignWithContext failed") + } + + return resp.Signature, nil +} + +func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (string, error) { + switch key.(type) { + case *rsa.PublicKey: + _, isPSS := opts.(*rsa.PSSOptions) + switch h := opts.HashFunc(); h { + case crypto.SHA256: + if isPSS { + return "RSASSA_PSS_SHA_256", nil + } + return "RSASSA_PKCS1_V1_5_SHA_256", nil + case crypto.SHA384: + if isPSS { + return "RSASSA_PSS_SHA_384", nil + } + return "RSASSA_PKCS1_V1_5_SHA_384", nil + case crypto.SHA512: + if isPSS { + return "RSASSA_PSS_SHA_512", nil + } + return "RSASSA_PKCS1_V1_5_SHA_512", nil + default: + return "", errors.Errorf("unsupported hash function %v", h) + } + case *ecdsa.PublicKey: + switch h := opts.HashFunc(); h { + case crypto.SHA256: + return "ECDSA_SHA_256", nil + case crypto.SHA384: + return "ECDSA_SHA_384", nil + case crypto.SHA512: + return "ECDSA_SHA_512", nil + default: + return "", errors.Errorf("unsupported hash function %v", h) + } + default: + return "", errors.Errorf("unsupported key type %T", key) + } +} diff --git a/kms/kms.go b/kms/kms.go index 310214f0..1a37135b 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -8,6 +8,7 @@ import ( "github.com/smallstep/certificates/kms/apiv1" // Enabled kms interfaces. + _ "github.com/smallstep/certificates/kms/awskms" _ "github.com/smallstep/certificates/kms/cloudkms" _ "github.com/smallstep/certificates/kms/softkms" From 5b680b2349e7fc5e672fe5e801af9f926c8af157 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 19 May 2020 17:35:58 -0700 Subject: [PATCH 03/13] Add initialization script for an AWS KMS. --- Makefile | 8 +- cmd/step-awskms-init/main.go | 236 +++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 cmd/step-awskms-init/main.go diff --git a/Makefile b/Makefile index 1cc94894..b4632eef 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ PKG?=github.com/smallstep/certificates/cmd/step-ca BINNAME?=step-ca CLOUDKMS_BINNAME?=step-cloudkms-init CLOUDKMS_PKG?=github.com/smallstep/certificates/cmd/step-cloudkms-init +AWSKMS_BINNAME?=step-awskms-init +AWSKMS_PKG?=github.com/smallstep/certificates/cmd/step-awskms-init YUBIKEY_BINNAME?=step-yubikey-init YUBIKEY_PKG?=github.com/smallstep/certificates/cmd/step-yubikey-init @@ -66,7 +68,7 @@ GOFLAGS := CGO_ENABLED=0 download: $Q go mod download -build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) +build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) @echo "Build Complete!" $(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) @@ -77,6 +79,10 @@ $(PREFIX)bin/$(CLOUDKMS_BINNAME): download $(call rwildcard,*.go) $Q mkdir -p $(@D) $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(LDFLAGS) $(CLOUDKMS_PKG) +$(PREFIX)bin/$(AWSKMS_BINNAME): download $(call rwildcard,*.go) + $Q mkdir -p $(@D) + $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(AWSKMS_BINNAME) $(LDFLAGS) $(AWSKMS_PKG) + $(PREFIX)bin/$(YUBIKEY_BINNAME): download $(call rwildcard,*.go) $Q mkdir -p $(@D) $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(YUBIKEY_BINNAME) $(LDFLAGS) $(YUBIKEY_PKG) diff --git a/cmd/step-awskms-init/main.go b/cmd/step-awskms-init/main.go new file mode 100644 index 00000000..b704729a --- /dev/null +++ b/cmd/step-awskms-init/main.go @@ -0,0 +1,236 @@ +package main + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "math/big" + "os" + "time" + + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/awskms" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/ui" + "github.com/smallstep/cli/utils" + "golang.org/x/crypto/ssh" +) + +func main() { + var credentialsFile, region string + var ssh bool + flag.StringVar(&credentialsFile, "credentials-file", "", "Path to the `file` containing the AWS KMS credentials.") + flag.StringVar(®ion, "region", "", "AWS KMS region name.") + flag.BoolVar(&ssh, "ssh", false, "Create SSH keys.") + flag.Usage = usage + flag.Parse() + + c, err := awskms.New(context.Background(), apiv1.Options{ + Type: string(apiv1.AmazonKMS), + Region: region, + CredentialsFile: credentialsFile, + }) + if err != nil { + fatal(err) + } + + if err := createPKI(c); err != nil { + fatal(err) + } + + if ssh { + ui.Println() + if err := createSSH(c); err != nil { + fatal(err) + } + } +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func usage() { + fmt.Fprintln(os.Stderr, "Usage: step-awskms-init --project ") + fmt.Fprintln(os.Stderr, ` +The step-awskms-init command initializes a public key infrastructure (PKI) +to be used by step-ca. + +This tool is experimental and in the future it will be integrated in step cli. + +OPTIONS`) + fmt.Fprintln(os.Stderr) + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, ` +COPYRIGHT + + (c) 2018-2020 Smallstep Labs, Inc.`) + os.Exit(1) +} + +func createPKI(c *awskms.KMS) error { + ui.Println("Creating PKI ...") + + // Root Certificate + resp, err := c.CreateKey(&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + + signer, err := c.CreateSigner(&resp.CreateSignerRequest) + if err != nil { + return err + } + + now := time.Now() + root := &x509.Certificate{ + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 1, + MaxPathLenZero: false, + Issuer: pkix.Name{CommonName: "Smallstep Root"}, + Subject: pkix.Name{CommonName: "Smallstep Root"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + AuthorityKeyId: mustSubjectKeyID(resp.PublicKey), + } + + b, err := x509.CreateCertificate(rand.Reader, root, root, resp.PublicKey, signer) + if err != nil { + return err + } + + if err = utils.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: b, + }), 0600); err != nil { + return err + } + + ui.PrintSelected("Root Key", resp.Name) + ui.PrintSelected("Root Certificate", "root_ca.crt") + + root, err = pemutil.ReadCertificate("root_ca.crt") + if err != nil { + return err + } + + // Intermediate Certificate + resp, err = c.CreateKey(&apiv1.CreateKeyRequest{ + Name: "intermediate", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + + intermediate := &x509.Certificate{ + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 0, + MaxPathLenZero: true, + Issuer: root.Subject, + Subject: pkix.Name{CommonName: "Smallstep Intermediate"}, + SerialNumber: mustSerialNumber(), + SubjectKeyId: mustSubjectKeyID(resp.PublicKey), + } + + b, err = x509.CreateCertificate(rand.Reader, intermediate, root, resp.PublicKey, signer) + if err != nil { + return err + } + + if err = utils.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: b, + }), 0600); err != nil { + return err + } + + ui.PrintSelected("Intermediate Key", resp.Name) + ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt") + + return nil +} + +func createSSH(c *awskms.KMS) error { + ui.Println("Creating SSH Keys ...") + + // User Key + resp, err := c.CreateKey(&apiv1.CreateKeyRequest{ + Name: "ssh-user-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + + key, err := ssh.NewPublicKey(resp.PublicKey) + if err != nil { + return err + } + + if err = utils.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + return err + } + + ui.PrintSelected("SSH User Public Key", "ssh_user_ca_key.pub") + ui.PrintSelected("SSH User Private Key", resp.Name) + + // Host Key + resp, err = c.CreateKey(&apiv1.CreateKeyRequest{ + Name: "ssh-host-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }) + if err != nil { + return err + } + + key, err = ssh.NewPublicKey(resp.PublicKey) + if err != nil { + return err + } + + if err = utils.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + return err + } + + ui.PrintSelected("SSH Host Public Key", "ssh_host_ca_key.pub") + ui.PrintSelected("SSH Host Private Key", resp.Name) + + return nil +} + +func mustSerialNumber() *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + sn, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + panic(err) + } + return sn +} + +func mustSubjectKeyID(key crypto.PublicKey) []byte { + b, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + panic(err) + } + hash := sha1.Sum(b) + return hash[:] +} From 82fb96588e151967775c1740a3e882da9a955dfd Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 19 May 2020 17:45:15 -0700 Subject: [PATCH 04/13] Fix unit tests. --- kms/apiv1/options_test.go | 2 +- kms/kms_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kms/apiv1/options_test.go b/kms/apiv1/options_test.go index 645b63b1..47ba3ec0 100644 --- a/kms/apiv1/options_test.go +++ b/kms/apiv1/options_test.go @@ -13,7 +13,7 @@ func TestOptions_Validate(t *testing.T) { {"nil", nil, false}, {"softkms", &Options{Type: "softkms"}, false}, {"cloudkms", &Options{Type: "cloudkms"}, false}, - {"awskms", &Options{Type: "awskms"}, true}, + {"awskms", &Options{Type: "awskms"}, false}, {"pkcs11", &Options{Type: "pkcs11"}, true}, {"unsupported", &Options{Type: "unsupported"}, true}, } diff --git a/kms/kms_test.go b/kms/kms_test.go index f377072f..f3d2f61f 100644 --- a/kms/kms_test.go +++ b/kms/kms_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/awskms" "github.com/smallstep/certificates/kms/cloudkms" "github.com/smallstep/certificates/kms/softkms" ) @@ -27,8 +28,8 @@ func TestNew(t *testing.T) { }{ {"softkms", false, args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false}, {"default", false, args{ctx, apiv1.Options{}}, &softkms.SoftKMS{}, false}, + {"awskms", false, args{ctx, apiv1.Options{Type: "awskms"}}, &awskms.KMS{}, false}, {"cloudkms", true, args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, true}, // fails because not credentials - {"awskms", false, args{ctx, apiv1.Options{Type: "awskms"}}, nil, true}, // not yet supported {"pkcs11", false, args{ctx, apiv1.Options{Type: "pkcs11"}}, nil, true}, // not yet supported {"fail validation", false, args{ctx, apiv1.Options{Type: "foobar"}}, nil, true}, } From deac15327f5605a1a963e50818760a95cee9d882 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 May 2020 12:30:32 -0700 Subject: [PATCH 05/13] Add docs for AWS KMS. --- docs/kms.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/docs/kms.md b/docs/kms.md index 76412081..53d63bed 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -3,8 +3,10 @@ This document describes how to use a key management service or KMS to store the private keys and sign certificates. -Support for multiple KMS are planned, but currently the only supported one is -Google's Cloud KMS. +Support for multiple KMS are planned, but currently the only Google's Cloud KMS, +and Amazon's AWS KMS are supported. A still experimental version for YubiKeys is +also available if you compile +[step-certificates](https://github.com/smallstep/certificates) yourself. ## Google's Cloud KMS @@ -66,6 +68,79 @@ Creating SSH Keys ... See `step-cloudkms-init --help` for more options. +## AWS KMS + +[AWS KMS](https://docs.aws.amazon.com/kms/index.html) is the Amazon's managed +encryption and key management service. It creates and store the cryptographic +keys, and use their infrastructure for signing operations. Amazon KMS operations +are always backed by hardware security modules (HSMs). + +To configure AWS KMS in your CA you need add the `"kms"` property to you +`ca.json`, and replace the property`"key"` with the AWS KMS key name of your +intermediate key: + +```json +{ + ... + "key": "awskms:key-id=f879f239-feb6-4596-9ed2-b1606277c7fe", + ... + "kms": { + "type": "awskms", + "region": "us-east-1" + } +} +``` + +By default it uses the credentials in `~/.aws/credentials`, but this can be +overridden using the `credentialsFile` option, `region` and `profile` can also +be configured as options. These can also be configured using environment +variables as described by their [session +docs](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/). + +To configure SSH certificate signing we do something similar, and replace the +ssh keys with the ones in the KMS: + +```json +{ + ... + "ssh": { + "hostKey": "awskms:key-id=d48e502a-09bc-4bf7-9af8-ae1bccedc931", + "userKey": "awskms:key-id=cf28e942-1e10-4a08-b84c-5359af1b5f12" + }, +} +``` + +The keys can also be just the Amazon's Key ID or the ARN, but using the format +based on the [RFC7512](https://tools.ietf.org/html/rfc7512) will allow more +flexibility for future releases of `step`. + +Currently [step](https://github.com/smallstep/cli) does not provide an automatic +way to initialize the public key infrastructure (PKI) using AWS KMS, but an +experimental tool named `step-awskms-init` is available for this use case. At +some point this tool will be integrated into `step` and it will be deleted. + +To use `step-awskms-init` make sure to have to have your [environment +configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) +running `aws configure` and then just run: + +```sh +$ bin/step-awskms-init --ssh --region us-east-1 +Creating PKI ... +✔ Root Key: awskms:key-id=f53fb767-4029-40ff-b650-0dd35fb661df +✔ Root Certificate: root_ca.crt +✔ Intermediate Key: awskms:key-id=f879f239-feb6-4596-9ed2-b1606277c7fe +✔ Intermediate Certificate: intermediate_ca.crt + +Creating SSH Keys ... +✔ SSH User Public Key: ssh_user_ca_key.pub +✔ SSH User Private Key: awskms:key-id=cf28e942-1e10-4a08-b84c-5359af1b5f12 +✔ SSH Host Public Key: ssh_host_ca_key.pub +✔ SSH Host Private Key: awskms:key-id=cf28e942-1e10-4a08-b84c-5359af1b5f12 +``` + +The `--region` parameter is only required if your aws configuration does not +define a region. See `step-awskms-init --help` for more options. + ## YubiKey And incomplete and experimental support for [YubiKeys](https://www.yubico.com) From d4cb9f4ac739503948adb3fa7c314e6072d92bbe Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 May 2020 12:43:14 -0700 Subject: [PATCH 06/13] Define an interface for kms operations. This interface will be used for unit testing. --- kms/awskms/awskms.go | 16 +++++++++++++--- kms/awskms/signer.go | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/kms/awskms/awskms.go b/kms/awskms/awskms.go index aea157b2..d287043d 100644 --- a/kms/awskms/awskms.go +++ b/kms/awskms/awskms.go @@ -7,19 +7,29 @@ import ( "strings" "time" - "github.com/smallstep/certificates/kms/uri" - + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/kms" "github.com/pkg/errors" "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/uri" "github.com/smallstep/cli/crypto/pemutil" ) // KMS implements a KMS using AWS Key Management Service. type KMS struct { session *session.Session - service *kms.KMS + service KeyManagementClient +} + +// KeyManagementClient defines the methods on KeyManagementClient that this +// package will use. This interface will be used for unit testing. +type KeyManagementClient interface { + GetPublicKeyWithContext(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) + CreateKeyWithContext(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) + CreateAliasWithContext(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) + SignWithContext(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) } // customerMasterKeySpecMapping is a mapping between the step signature algorithm, diff --git a/kms/awskms/signer.go b/kms/awskms/signer.go index 194eeae4..aa1eb26c 100644 --- a/kms/awskms/signer.go +++ b/kms/awskms/signer.go @@ -12,13 +12,13 @@ import ( ) type Signer struct { - service *kms.KMS + service KeyManagementClient keyID string publicKey crypto.PublicKey } // NewSigner creates a new signer using a key in the AWS KMS. -func NewSigner(svc *kms.KMS, signingKey string) (*Signer, error) { +func NewSigner(svc KeyManagementClient, signingKey string) (*Signer, error) { keyID, err := parseKeyID(signingKey) if err != nil { return nil, err From aaf71ce66a7f52c87ca4c4e1cd530a0e66168eb2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 May 2020 17:04:01 -0700 Subject: [PATCH 07/13] Add unit tests for awskms. --- kms/awskms/awskms_test.go | 358 ++++++++++++++++++++++++++++++++++++++ kms/awskms/mock_test.go | 72 ++++++++ kms/awskms/signer.go | 19 +- kms/awskms/signer_test.go | 191 ++++++++++++++++++++ 4 files changed, 631 insertions(+), 9 deletions(-) create mode 100644 kms/awskms/awskms_test.go create mode 100644 kms/awskms/mock_test.go create mode 100644 kms/awskms/signer_test.go diff --git a/kms/awskms/awskms_test.go b/kms/awskms/awskms_test.go new file mode 100644 index 00000000..f19e1c49 --- /dev/null +++ b/kms/awskms/awskms_test.go @@ -0,0 +1,358 @@ +package awskms + +import ( + "context" + "crypto" + "fmt" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/cli/crypto/pemutil" +) + +func TestNew(t *testing.T) { + ctx := context.Background() + + sess, err := session.NewSessionWithOptions(session.Options{}) + if err != nil { + t.Fatal(err) + } + expected := &KMS{ + session: sess, + service: kms.New(sess), + } + + // This will force an error in the session creation. + // It does not fail with missing credentials. + forceError := func(t *testing.T) { + key := "AWS_CA_BUNDLE" + value := os.Getenv(key) + os.Setenv(key, filepath.Join(os.TempDir(), "missing-ca.crt")) + t.Cleanup(func() { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + }) + } + + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + want *KMS + wantErr bool + }{ + {"ok", args{ctx, apiv1.Options{}}, expected, false}, + {"ok with options", args{ctx, apiv1.Options{ + Region: "us-east-1", + Profile: "smallstep", + CredentialsFile: "~/aws/credentials", + }}, expected, false}, + {"fail", args{ctx, apiv1.Options{}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Force an error in the session loading + if tt.wantErr { + forceError(t) + } + + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %#v, want %#v", got, tt.want) + } + } else { + if got.session == nil || got.service == nil { + t.Errorf("New() = %#v, want %#v", got, tt.want) + } + } + }) + } +} + +func TestKMS_GetPublicKey(t *testing.T) { + okClient := getOKClient() + key, err := pemutil.ParseKey([]byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + type fields struct { + session *session.Session + service KeyManagementClient + } + type args struct { + req *apiv1.GetPublicKeyRequest + } + tests := []struct { + name string + fields fields + args args + want crypto.PublicKey + wantErr bool + }{ + {"ok", fields{nil, okClient}, args{&apiv1.GetPublicKeyRequest{ + Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + }}, key, false}, + {"fail empty", fields{nil, okClient}, args{&apiv1.GetPublicKeyRequest{}}, nil, true}, + {"fail name", fields{nil, okClient}, args{&apiv1.GetPublicKeyRequest{ + Name: "awskms:key-id=", + }}, nil, true}, + {"fail getPublicKey", fields{nil, &MockClient{ + getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) { + return nil, fmt.Errorf("an error") + }, + }}, args{&apiv1.GetPublicKeyRequest{ + Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + }}, nil, true}, + {"fail not der", fields{nil, &MockClient{ + getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) { + return &kms.GetPublicKeyOutput{ + KeyId: input.KeyId, + PublicKey: []byte(publicKey), + }, nil + }, + }}, args{&apiv1.GetPublicKeyRequest{ + Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KMS{ + session: tt.fields.session, + service: tt.fields.service, + } + got, err := k.GetPublicKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("KMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("KMS.GetPublicKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKMS_CreateKey(t *testing.T) { + okClient := getOKClient() + key, err := pemutil.ParseKey([]byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + type fields struct { + session *session.Session + service KeyManagementClient + } + type args struct { + req *apiv1.CreateKeyRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.CreateKeyResponse + wantErr bool + }{ + {"ok", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }}, &apiv1.CreateKeyResponse{ + Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + PublicKey: key, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + }, + }, false}, + {"ok rsa", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.SHA256WithRSA, + Bits: 2048, + }}, &apiv1.CreateKeyResponse{ + Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + PublicKey: key, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + }, + }, false}, + {"fail empty", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{}}, nil, true}, + {"fail unsupported alg", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.PureEd25519, + }}, nil, true}, + {"fail unsupported bits", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.SHA256WithRSA, + Bits: 1234, + }}, nil, true}, + {"fail createKey", fields{nil, &MockClient{ + createKeyWithContext: func(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) { + return nil, fmt.Errorf("an error") + }, + createAliasWithContext: okClient.createAliasWithContext, + getPublicKeyWithContext: okClient.getPublicKeyWithContext, + }}, args{&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }}, nil, true}, + {"fail createAlias", fields{nil, &MockClient{ + createKeyWithContext: okClient.createKeyWithContext, + createAliasWithContext: func(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) { + return nil, fmt.Errorf("an error") + }, + getPublicKeyWithContext: okClient.getPublicKeyWithContext, + }}, args{&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }}, nil, true}, + {"fail getPublicKey", fields{nil, &MockClient{ + createKeyWithContext: okClient.createKeyWithContext, + createAliasWithContext: okClient.createAliasWithContext, + getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) { + return nil, fmt.Errorf("an error") + }, + }}, args{&apiv1.CreateKeyRequest{ + Name: "root", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KMS{ + session: tt.fields.session, + service: tt.fields.service, + } + got, err := k.CreateKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("KMS.CreateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("KMS.CreateKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKMS_CreateSigner(t *testing.T) { + client := getOKClient() + key, err := pemutil.ParseKey([]byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + type fields struct { + session *session.Session + service KeyManagementClient + } + type args struct { + req *apiv1.CreateSignerRequest + } + tests := []struct { + name string + fields fields + args args + want crypto.Signer + wantErr bool + }{ + {"ok", fields{nil, client}, args{&apiv1.CreateSignerRequest{ + SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + }}, &Signer{ + service: client, + keyID: "be468355-ca7a-40d9-a28b-8ae1c4c7f936", + publicKey: key, + }, false}, + {"fail empty", fields{nil, client}, args{&apiv1.CreateSignerRequest{}}, nil, true}, + {"fail preload", fields{nil, client}, args{&apiv1.CreateSignerRequest{}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KMS{ + session: tt.fields.session, + service: tt.fields.service, + } + got, err := k.CreateSigner(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("KMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("KMS.CreateSigner() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKMS_Close(t *testing.T) { + type fields struct { + session *session.Session + service KeyManagementClient + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{nil, getOKClient()}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KMS{ + session: tt.fields.session, + service: tt.fields.service, + } + if err := k.Close(); (err != nil) != tt.wantErr { + t.Errorf("KMS.Close() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_parseKeyID(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {"ok uri", args{"awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", false}, + {"ok key id", args{"be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", false}, + {"ok arn", args{"arn:aws:kms:us-east-1:123456789:key/be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, "arn:aws:kms:us-east-1:123456789:key/be468355-ca7a-40d9-a28b-8ae1c4c7f936", false}, + {"fail parse", args{"awskms:key-id=%ZZ"}, "", true}, + {"fail empty key", args{"awskms:key-id="}, "", true}, + {"fail missing", args{"awskms:foo=bar"}, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseKeyID(tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("parseKeyID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parseKeyID() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/awskms/mock_test.go b/kms/awskms/mock_test.go new file mode 100644 index 00000000..ba35d87a --- /dev/null +++ b/kms/awskms/mock_test.go @@ -0,0 +1,72 @@ +package awskms + +import ( + "encoding/pem" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/kms" +) + +type MockClient struct { + getPublicKeyWithContext func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) + createKeyWithContext func(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) + createAliasWithContext func(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) + signWithContext func(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) +} + +func (m *MockClient) GetPublicKeyWithContext(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) { + return m.getPublicKeyWithContext(ctx, input, opts...) +} + +func (m *MockClient) CreateKeyWithContext(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) { + return m.createKeyWithContext(ctx, input, opts...) +} + +func (m *MockClient) CreateAliasWithContext(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) { + return m.createAliasWithContext(ctx, input, opts...) +} + +func (m *MockClient) SignWithContext(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) { + return m.signWithContext(ctx, input, opts...) +} + +const ( + publicKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8XWlIWkOThxNjGbZLYUgRHmsvCrW +KF+HLktPfPTIK3lGd1k4849WQs59XIN+LXZQ6b2eRBEBKAHEyQus8UU7gw== +-----END PUBLIC KEY-----` + keyId = "be468355-ca7a-40d9-a28b-8ae1c4c7f936" +) + +var signature = []byte{ + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, +} + +func getOKClient() *MockClient { + return &MockClient{ + getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) { + block, _ := pem.Decode([]byte(publicKey)) + return &kms.GetPublicKeyOutput{ + KeyId: input.KeyId, + PublicKey: block.Bytes, + }, nil + }, + createKeyWithContext: func(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) { + md := new(kms.KeyMetadata) + md.SetKeyId(keyId) + return &kms.CreateKeyOutput{ + KeyMetadata: md, + }, nil + }, + createAliasWithContext: func(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) { + return &kms.CreateAliasOutput{}, nil + }, + signWithContext: func(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) { + return &kms.SignOutput{ + Signature: signature, + }, nil + }, + } +} diff --git a/kms/awskms/signer.go b/kms/awskms/signer.go index aa1eb26c..3d9767d0 100644 --- a/kms/awskms/signer.go +++ b/kms/awskms/signer.go @@ -11,6 +11,7 @@ import ( "github.com/smallstep/cli/crypto/pemutil" ) +// Signer implements a crypto.Signer using the AWS KMS. type Signer struct { service KeyManagementClient keyID string @@ -88,30 +89,30 @@ func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (string, switch h := opts.HashFunc(); h { case crypto.SHA256: if isPSS { - return "RSASSA_PSS_SHA_256", nil + return kms.SigningAlgorithmSpecRsassaPssSha256, nil } - return "RSASSA_PKCS1_V1_5_SHA_256", nil + return kms.SigningAlgorithmSpecRsassaPkcs1V15Sha256, nil case crypto.SHA384: if isPSS { - return "RSASSA_PSS_SHA_384", nil + return kms.SigningAlgorithmSpecRsassaPssSha384, nil } - return "RSASSA_PKCS1_V1_5_SHA_384", nil + return kms.SigningAlgorithmSpecRsassaPkcs1V15Sha384, nil case crypto.SHA512: if isPSS { - return "RSASSA_PSS_SHA_512", nil + return kms.SigningAlgorithmSpecRsassaPssSha512, nil } - return "RSASSA_PKCS1_V1_5_SHA_512", nil + return kms.SigningAlgorithmSpecRsassaPkcs1V15Sha512, nil default: return "", errors.Errorf("unsupported hash function %v", h) } case *ecdsa.PublicKey: switch h := opts.HashFunc(); h { case crypto.SHA256: - return "ECDSA_SHA_256", nil + return kms.SigningAlgorithmSpecEcdsaSha256, nil case crypto.SHA384: - return "ECDSA_SHA_384", nil + return kms.SigningAlgorithmSpecEcdsaSha384, nil case crypto.SHA512: - return "ECDSA_SHA_512", nil + return kms.SigningAlgorithmSpecEcdsaSha512, nil default: return "", errors.Errorf("unsupported hash function %v", h) } diff --git a/kms/awskms/signer_test.go b/kms/awskms/signer_test.go new file mode 100644 index 00000000..51915174 --- /dev/null +++ b/kms/awskms/signer_test.go @@ -0,0 +1,191 @@ +package awskms + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "fmt" + "io" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/smallstep/cli/crypto/pemutil" +) + +func TestNewSigner(t *testing.T) { + okClient := getOKClient() + key, err := pemutil.ParseKey([]byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + type args struct { + svc KeyManagementClient + signingKey string + } + tests := []struct { + name string + args args + want *Signer + wantErr bool + }{ + {"ok", args{okClient, "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, &Signer{ + service: okClient, + keyID: "be468355-ca7a-40d9-a28b-8ae1c4c7f936", + publicKey: key, + }, false}, + {"fail parse", args{okClient, "awskms:key-id="}, nil, true}, + {"fail preload", args{&MockClient{ + getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) { + return nil, fmt.Errorf("an error") + }, + }, "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, nil, true}, + {"fail preload not der", args{&MockClient{ + getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) { + return &kms.GetPublicKeyOutput{ + KeyId: input.KeyId, + PublicKey: []byte(publicKey), + }, nil + }, + }, "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewSigner(tt.args.svc, tt.args.signingKey) + if (err != nil) != tt.wantErr { + t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewSigner() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigner_Public(t *testing.T) { + okClient := getOKClient() + key, err := pemutil.ParseKey([]byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + type fields struct { + service KeyManagementClient + keyID string + publicKey crypto.PublicKey + } + tests := []struct { + name string + fields fields + want crypto.PublicKey + }{ + {"ok", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, key}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signer{ + service: tt.fields.service, + keyID: tt.fields.keyID, + publicKey: tt.fields.publicKey, + } + if got := s.Public(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Signer.Public() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigner_Sign(t *testing.T) { + okClient := getOKClient() + key, err := pemutil.ParseKey([]byte(publicKey)) + if err != nil { + t.Fatal(err) + } + + type fields struct { + service KeyManagementClient + keyID string + publicKey crypto.PublicKey + } + type args struct { + rand io.Reader + digest []byte + opts crypto.SignerOpts + } + tests := []struct { + name string + fields fields + args args + want []byte + wantErr bool + }{ + {"ok", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, args{rand.Reader, []byte("digest"), crypto.SHA256}, signature, false}, + {"fail alg", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, args{rand.Reader, []byte("digest"), crypto.MD5}, nil, true}, + {"fail key", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", []byte("key")}, args{rand.Reader, []byte("digest"), crypto.SHA256}, nil, true}, + {"fail sign", fields{&MockClient{ + signWithContext: func(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) { + return nil, fmt.Errorf("an error") + }, + }, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, args{rand.Reader, []byte("digest"), crypto.SHA256}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signer{ + service: tt.fields.service, + keyID: tt.fields.keyID, + publicKey: tt.fields.publicKey, + } + got, err := s.Sign(tt.args.rand, tt.args.digest, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("Signer.Sign() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Signer.Sign() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getSigningAlgorithm(t *testing.T) { + type args struct { + key crypto.PublicKey + opts crypto.SignerOpts + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {"rsa+sha256", args{&rsa.PublicKey{}, crypto.SHA256}, "RSASSA_PKCS1_V1_5_SHA_256", false}, + {"rsa+sha384", args{&rsa.PublicKey{}, crypto.SHA384}, "RSASSA_PKCS1_V1_5_SHA_384", false}, + {"rsa+sha512", args{&rsa.PublicKey{}, crypto.SHA512}, "RSASSA_PKCS1_V1_5_SHA_512", false}, + {"pssrsa+sha256", args{&rsa.PublicKey{}, &rsa.PSSOptions{Hash: crypto.SHA256.HashFunc()}}, "RSASSA_PSS_SHA_256", false}, + {"pssrsa+sha384", args{&rsa.PublicKey{}, &rsa.PSSOptions{Hash: crypto.SHA384.HashFunc()}}, "RSASSA_PSS_SHA_384", false}, + {"pssrsa+sha512", args{&rsa.PublicKey{}, &rsa.PSSOptions{Hash: crypto.SHA512.HashFunc()}}, "RSASSA_PSS_SHA_512", false}, + {"P256", args{&ecdsa.PublicKey{}, crypto.SHA256}, "ECDSA_SHA_256", false}, + {"P384", args{&ecdsa.PublicKey{}, crypto.SHA384}, "ECDSA_SHA_384", false}, + {"P521", args{&ecdsa.PublicKey{}, crypto.SHA512}, "ECDSA_SHA_512", false}, + {"fail type", args{[]byte("key"), crypto.SHA256}, "", true}, + {"fail rsa alg", args{&rsa.PublicKey{}, crypto.MD5}, "", true}, + {"fail ecdsa alg", args{&ecdsa.PublicKey{}, crypto.MD5}, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getSigningAlgorithm(tt.args.key, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("getSigningAlgorithm() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getSigningAlgorithm() = %v, want %v", got, tt.want) + } + }) + } +} From f006cca87a53de89c13a80722a5b0e616b65f83b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 May 2020 17:45:57 -0700 Subject: [PATCH 08/13] Use Go 1.14. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 665470ec..ec6dce01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: -- 1.13.x +- 1.14.x addons: apt: packages: From 7104588fcb1499afdcbbde0c5bc87460a2d8856b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 20 May 2020 17:58:37 -0700 Subject: [PATCH 09/13] Fix linter error. --- kms/awskms/mock_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kms/awskms/mock_test.go b/kms/awskms/mock_test.go index ba35d87a..5a7d5bd4 100644 --- a/kms/awskms/mock_test.go +++ b/kms/awskms/mock_test.go @@ -36,7 +36,7 @@ const ( MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8XWlIWkOThxNjGbZLYUgRHmsvCrW KF+HLktPfPTIK3lGd1k4849WQs59XIN+LXZQ6b2eRBEBKAHEyQus8UU7gw== -----END PUBLIC KEY-----` - keyId = "be468355-ca7a-40d9-a28b-8ae1c4c7f936" + keyID = "be468355-ca7a-40d9-a28b-8ae1c4c7f936" ) var signature = []byte{ @@ -55,7 +55,7 @@ func getOKClient() *MockClient { }, createKeyWithContext: func(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) { md := new(kms.KeyMetadata) - md.SetKeyId(keyId) + md.SetKeyId(keyID) return &kms.CreateKeyOutput{ KeyMetadata: md, }, nil From dfe8e11e44c524f56da6d9f18adc89122f4520fa Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 May 2020 10:55:26 -0700 Subject: [PATCH 10/13] Remove anchor from link. --- kms/awskms/awskms.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kms/awskms/awskms.go b/kms/awskms/awskms.go index d287043d..df75d0e1 100644 --- a/kms/awskms/awskms.go +++ b/kms/awskms/awskms.go @@ -167,7 +167,7 @@ func (k *KMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, } // Names uses Amazon Resource Name - // https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms + // https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html return &apiv1.CreateKeyResponse{ Name: name, PublicKey: publicKey, From 6c9cd7050ca7e7609fb59984cda8e0559e95211f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 May 2020 11:13:07 -0700 Subject: [PATCH 11/13] Add test with query strings. --- kms/uri/uri_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kms/uri/uri_test.go b/kms/uri/uri_test.go index 68746f90..a2b69b65 100644 --- a/kms/uri/uri_test.go +++ b/kms/uri/uri_test.go @@ -99,6 +99,10 @@ func TestParse(t *testing.T) { URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"}, Values: url.Values{"slot-id": []string{"9a"}}, }, false}, + {"ok query", args{"yubikey:slot-id=9a;foo=bar?pin=123456&foo=bar"}, &URI{ + URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a;foo=bar", RawQuery: "pin=123456&foo=bar"}, + Values: url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}}, + }, false}, {"ok file", args{"file:///tmp/ca.cert"}, &URI{ URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"}, Values: url.Values{}, From 7a985b1470d1cd0d7d7d6656ab53a7ea2cef8430 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 May 2020 14:26:05 -0700 Subject: [PATCH 12/13] Fix usage, remove unsupported flag. --- cmd/step-awskms-init/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/step-awskms-init/main.go b/cmd/step-awskms-init/main.go index b704729a..69e3e8e3 100644 --- a/cmd/step-awskms-init/main.go +++ b/cmd/step-awskms-init/main.go @@ -58,7 +58,7 @@ func fatal(err error) { } func usage() { - fmt.Fprintln(os.Stderr, "Usage: step-awskms-init --project ") + fmt.Fprintln(os.Stderr, "Usage: step-awskms-init") fmt.Fprintln(os.Stderr, ` The step-awskms-init command initializes a public key infrastructure (PKI) to be used by step-ca. From 26c89cf77918902d88679f543d669a918c6db665 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 26 May 2020 14:34:47 -0700 Subject: [PATCH 13/13] Rename method. --- cmd/step-awskms-init/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/step-awskms-init/main.go b/cmd/step-awskms-init/main.go index 69e3e8e3..2241cdd6 100644 --- a/cmd/step-awskms-init/main.go +++ b/cmd/step-awskms-init/main.go @@ -40,7 +40,7 @@ func main() { fatal(err) } - if err := createPKI(c); err != nil { + if err := createX509(c); err != nil { fatal(err) } @@ -75,8 +75,8 @@ COPYRIGHT os.Exit(1) } -func createPKI(c *awskms.KMS) error { - ui.Println("Creating PKI ...") +func createX509(c *awskms.KMS) error { + ui.Println("Creating X.509 PKI ...") // Root Certificate resp, err := c.CreateKey(&apiv1.CreateKeyRequest{