Skip to content

Commit 53cceb0

Browse files
authored
Merge pull request #44 from smallstep/kmsfs
Kmsfs
2 parents b126dcf + 75693f7 commit 53cceb0

File tree

15 files changed

+1133
-145
lines changed

15 files changed

+1133
-145
lines changed

kms/apiv1/options.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package apiv1
33
import (
44
"crypto"
55
"crypto/x509"
6+
"fmt"
67
"strings"
78

8-
"github.com/pkg/errors"
9+
"go.step.sm/crypto/kms/uri"
910
)
1011

1112
// KeyManager is the interface implemented by all the KMS.
@@ -86,7 +87,7 @@ const (
8687
// Options are the KMS options. They represent the kms object in the ca.json.
8788
type Options struct {
8889
// The type of the KMS to use.
89-
Type string `json:"type"`
90+
Type Type `json:"type"`
9091

9192
// Path to the credentials file used in CloudKMS and AmazonKMS.
9293
CredentialsFile string `json:"credentialsFile,omitempty"`
@@ -124,14 +125,30 @@ func (o *Options) Validate() error {
124125
return nil
125126
}
126127

127-
switch Type(strings.ToLower(o.Type)) {
128+
typ := strings.ToLower(string(o.Type))
129+
switch Type(typ) {
128130
case DefaultKMS, SoftKMS: // Go crypto based kms.
129131
case CloudKMS, AmazonKMS, AzureKMS: // Cloud based kms.
130132
case YubiKey, PKCS11: // Hardware based kms.
131133
case SSHAgentKMS: // Others
132134
default:
133-
return errors.Errorf("unsupported kms type %s", o.Type)
135+
return fmt.Errorf("unsupported kms type %s", o.Type)
134136
}
135137

136138
return nil
137139
}
140+
141+
// GetType returns the type in the type property or the one present in the URI.
142+
func (o *Options) GetType() (Type, error) {
143+
if o.Type != "" {
144+
return o.Type, nil
145+
}
146+
if o.URI != "" {
147+
u, err := uri.Parse(o.URI)
148+
if err != nil {
149+
return DefaultKMS, err
150+
}
151+
return Type(strings.ToLower(u.Scheme)), nil
152+
}
153+
return SoftKMS, nil
154+
}

kms/apiv1/options_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,47 @@ func TestOptions_Validate(t *testing.T) {
2727
}
2828
}
2929

30+
func TestOptions_GetType(t *testing.T) {
31+
type fields struct {
32+
Type Type
33+
URI string
34+
}
35+
tests := []struct {
36+
name string
37+
fields fields
38+
want Type
39+
wantErr bool
40+
}{
41+
{"ok", fields{PKCS11, ""}, PKCS11, false},
42+
{"ok default", fields{"", ""}, SoftKMS, false},
43+
{"ok by uri", fields{"", "PKCS11:foo=bar"}, PKCS11, false},
44+
{"ok by uri", fields{"", "softkms:foo=bar"}, SoftKMS, false},
45+
{"ok by uri", fields{"", "cloudkms:foo=bar"}, CloudKMS, false},
46+
{"ok by uri", fields{"", "awskms:foo=bar"}, AmazonKMS, false},
47+
{"ok by uri", fields{"", "pkcs11:foo=bar"}, PKCS11, false},
48+
{"ok by uri", fields{"", "yubikey:foo=bar"}, YubiKey, false},
49+
{"ok by uri", fields{"", "sshagentkms:foo=bar"}, SSHAgentKMS, false},
50+
{"ok by uri", fields{"", "azurekms:foo=bar"}, AzureKMS, false},
51+
{"fail uri", fields{"", "foo=bar"}, DefaultKMS, true},
52+
}
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
o := &Options{
56+
Type: tt.fields.Type,
57+
URI: tt.fields.URI,
58+
}
59+
got, err := o.GetType()
60+
if (err != nil) != tt.wantErr {
61+
t.Errorf("Options.GetType() error = %v, wantErr %v", err, tt.wantErr)
62+
return
63+
}
64+
if got != tt.want {
65+
t.Errorf("Options.GetType() = %v, want %v", got, tt.want)
66+
}
67+
})
68+
}
69+
}
70+
3071
func TestErrNotImplemented_Error(t *testing.T) {
3172
type fields struct {
3273
msg string

kms/kms.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package kms
22

33
import (
44
"context"
5-
"strings"
65

76
"github.com/pkg/errors"
87
"go.step.sm/crypto/kms/apiv1"
@@ -30,14 +29,13 @@ func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) {
3029
return nil, err
3130
}
3231

33-
t := apiv1.Type(strings.ToLower(opts.Type))
34-
if t == apiv1.DefaultKMS {
35-
t = apiv1.SoftKMS
32+
typ, err := opts.GetType()
33+
if err != nil {
34+
return nil, err
3635
}
37-
38-
fn, ok := apiv1.LoadKeyManagerNewFunc(t)
36+
fn, ok := apiv1.LoadKeyManagerNewFunc(typ)
3937
if !ok {
40-
return nil, errors.Errorf("unsupported kms type '%s'", t)
38+
return nil, errors.Errorf("unsupported kms type '%s'", typ)
4139
}
4240
return fn(ctx, opts)
4341
}

kms/kms_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package kms
33
import (
44
"context"
55
"os"
6+
"path/filepath"
67
"reflect"
78
"testing"
89

@@ -15,6 +16,14 @@ import (
1516
func TestNew(t *testing.T) {
1617
ctx := context.Background()
1718

19+
failCloudKMS := true
20+
if home, err := os.UserHomeDir(); err == nil {
21+
file := filepath.Join(home, ".config", "gcloud", "application_default_credentials.json")
22+
if _, err := os.Stat(file); err == nil {
23+
failCloudKMS = false
24+
}
25+
}
26+
1827
type args struct {
1928
ctx context.Context
2029
opts apiv1.Options
@@ -26,11 +35,12 @@ func TestNew(t *testing.T) {
2635
want KeyManager
2736
wantErr bool
2837
}{
29-
{"softkms", false, args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false},
3038
{"default", false, args{ctx, apiv1.Options{}}, &softkms.SoftKMS{}, false},
39+
{"softkms", false, args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false},
40+
{"uri", false, args{ctx, apiv1.Options{URI: "softkms:foo=bar"}}, &softkms.SoftKMS{}, false},
3141
{"awskms", false, args{ctx, apiv1.Options{Type: "awskms"}}, &awskms.KMS{}, false},
32-
{"cloudkms", true, args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, true}, // fails because not credentials
33-
{"pkcs11", false, args{ctx, apiv1.Options{Type: "pkcs11"}}, nil, true}, // not yet supported
42+
{"cloudkms", true, args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, failCloudKMS},
43+
{"fail not enabled", false, args{ctx, apiv1.Options{Type: "pkcs11"}}, nil, true}, // not enabled
3444
{"fail validation", false, args{ctx, apiv1.Options{Type: "foobar"}}, nil, true},
3545
}
3646
for _, tt := range tests {

kms/kmsfs.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package kms
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io/fs"
7+
8+
"go.step.sm/crypto/kms/apiv1"
9+
)
10+
11+
type kmsfs struct {
12+
apiv1.KeyManager
13+
}
14+
15+
func newFS(ctx context.Context, kmsuri string) (*kmsfs, error) {
16+
if kmsuri == "" {
17+
return &kmsfs{}, nil
18+
}
19+
20+
km, err := loadKMS(ctx, kmsuri)
21+
if err != nil {
22+
return nil, err
23+
}
24+
return &kmsfs{KeyManager: km}, nil
25+
}
26+
27+
func (f *kmsfs) getKMS(kmsuri string) (apiv1.KeyManager, error) {
28+
if f.KeyManager == nil {
29+
return loadKMS(context.TODO(), kmsuri)
30+
}
31+
return f.KeyManager, nil
32+
}
33+
34+
func loadKMS(ctx context.Context, kmsuri string) (apiv1.KeyManager, error) {
35+
return New(ctx, apiv1.Options{
36+
URI: kmsuri,
37+
})
38+
}
39+
40+
func openError(name string, err error) *fs.PathError {
41+
return &fs.PathError{
42+
Path: name,
43+
Op: "open",
44+
Err: err,
45+
}
46+
}
47+
48+
// certFS implements an io/fs to load certificates from a KMS.
49+
type certFS struct {
50+
*kmsfs
51+
}
52+
53+
// CertFS creates a new io/fs with the given KMS URI.
54+
func CertFS(ctx context.Context, kmsuri string) (fs.FS, error) {
55+
km, err := newFS(ctx, kmsuri)
56+
if err != nil {
57+
return nil, err
58+
}
59+
_, ok := km.KeyManager.(apiv1.CertificateManager)
60+
if !ok {
61+
return nil, fmt.Errorf("%s does not implement a CertificateManager", kmsuri)
62+
}
63+
return &certFS{kmsfs: km}, nil
64+
}
65+
66+
// Open returns a file representing a certificate in an KMS.
67+
func (f *certFS) Open(name string) (fs.File, error) {
68+
km, err := f.getKMS(name)
69+
if err != nil {
70+
return nil, openError(name, err)
71+
}
72+
cert, err := km.(apiv1.CertificateManager).LoadCertificate(&apiv1.LoadCertificateRequest{
73+
Name: name,
74+
})
75+
if err != nil {
76+
return nil, openError(name, err)
77+
}
78+
return &object{
79+
Path: name,
80+
Object: cert,
81+
}, nil
82+
}
83+
84+
// keyFS implements an io/fs to load public keys from a KMS.
85+
type keyFS struct {
86+
*kmsfs
87+
}
88+
89+
// KeyFS creates a new KeyFS with the given KMS URI.
90+
func KeyFS(ctx context.Context, kmsuri string) (fs.FS, error) {
91+
km, err := newFS(ctx, kmsuri)
92+
if err != nil {
93+
return nil, err
94+
}
95+
return &keyFS{kmsfs: km}, nil
96+
}
97+
98+
// Open returns a file representing a public key in a KMS.
99+
func (f *keyFS) Open(name string) (fs.File, error) {
100+
km, err := f.getKMS(name)
101+
if err != nil {
102+
return nil, openError(name, err)
103+
}
104+
// Attempt with a public key
105+
pub, err := km.GetPublicKey(&apiv1.GetPublicKeyRequest{
106+
Name: name,
107+
})
108+
if err != nil {
109+
return nil, openError(name, err)
110+
}
111+
return &object{
112+
Path: name,
113+
Object: pub,
114+
}, nil
115+
}

0 commit comments

Comments
 (0)