From b0f768a3fb095b426a654dce11067d947ab9da77 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 19 May 2020 17:32:52 -0700 Subject: [PATCH] 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) + } + }) + } +}