Add unit tests for azurekms.

This commit is contained in:
Mariano Cano 2021-10-05 20:35:52 -07:00
parent 97d08a1b61
commit 6389100325
7 changed files with 920 additions and 8 deletions

2
go.mod
View file

@ -16,7 +16,7 @@ require (
github.com/go-chi/chi v4.0.2+incompatible github.com/go-chi/chi v4.0.2+incompatible
github.com/go-kit/kit v0.10.0 // indirect github.com/go-kit/kit v0.10.0 // indirect
github.com/go-piv/piv-go v1.7.0 github.com/go-piv/piv-go v1.7.0
github.com/golang/mock v1.5.0 github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/googleapis/gax-go/v2 v2.0.5 github.com/googleapis/gax-go/v2 v2.0.5
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect

3
go.sum
View file

@ -236,8 +236,9 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

View file

@ -0,0 +1,80 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/smallstep/certificates/kms/azurekms (interfaces: KeyVaultClient)
// Package mock is a generated GoMock package.
package mock
import (
context "context"
keyvault "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// KeyVaultClient is a mock of KeyVaultClient interface
type KeyVaultClient struct {
ctrl *gomock.Controller
recorder *KeyVaultClientMockRecorder
}
// KeyVaultClientMockRecorder is the mock recorder for KeyVaultClient
type KeyVaultClientMockRecorder struct {
mock *KeyVaultClient
}
// NewKeyVaultClient creates a new mock instance
func NewKeyVaultClient(ctrl *gomock.Controller) *KeyVaultClient {
mock := &KeyVaultClient{ctrl: ctrl}
mock.recorder = &KeyVaultClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *KeyVaultClient) EXPECT() *KeyVaultClientMockRecorder {
return m.recorder
}
// CreateKey mocks base method
func (m *KeyVaultClient) CreateKey(arg0 context.Context, arg1, arg2 string, arg3 keyvault.KeyCreateParameters) (keyvault.KeyBundle, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateKey", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(keyvault.KeyBundle)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateKey indicates an expected call of CreateKey
func (mr *KeyVaultClientMockRecorder) CreateKey(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateKey", reflect.TypeOf((*KeyVaultClient)(nil).CreateKey), arg0, arg1, arg2, arg3)
}
// GetKey mocks base method
func (m *KeyVaultClient) GetKey(arg0 context.Context, arg1, arg2, arg3 string) (keyvault.KeyBundle, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetKey", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(keyvault.KeyBundle)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetKey indicates an expected call of GetKey
func (mr *KeyVaultClientMockRecorder) GetKey(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKey", reflect.TypeOf((*KeyVaultClient)(nil).GetKey), arg0, arg1, arg2, arg3)
}
// Sign mocks base method
func (m *KeyVaultClient) Sign(arg0 context.Context, arg1, arg2, arg3 string, arg4 keyvault.KeySignParameters) (keyvault.KeyOperationResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Sign", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(keyvault.KeyOperationResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Sign indicates an expected call of Sign
func (mr *KeyVaultClientMockRecorder) Sign(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sign", reflect.TypeOf((*KeyVaultClient)(nil).Sign), arg0, arg1, arg2, arg3, arg4)
}

View file

@ -114,8 +114,7 @@ type KeyVault struct {
baseClient KeyVaultClient baseClient KeyVaultClient
} }
// New initializes a new KMS implemented using Azure Key Vault. var createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) {
func New(ctx context.Context, opts apiv1.Options) (*KeyVault, error) {
// Attempt to authorize with the following methods: // Attempt to authorize with the following methods:
// 1. Environment variables. // 1. Environment variables.
// - Client credentials // - Client credentials
@ -133,9 +132,17 @@ func New(ctx context.Context, opts apiv1.Options) (*KeyVault, error) {
baseClient := keyvault.New() baseClient := keyvault.New()
baseClient.Authorizer = authorizer baseClient.Authorizer = authorizer
return &baseClient, nil
}
// New initializes a new KMS implemented using Azure Key Vault.
func New(ctx context.Context, opts apiv1.Options) (*KeyVault, error) {
baseClient, err := createClient(ctx, opts)
if err != nil {
return nil, err
}
return &KeyVault{ return &KeyVault{
baseClient: &baseClient, baseClient: baseClient,
}, nil }, nil
} }
@ -164,6 +171,10 @@ func (k *KeyVault) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKe
// CreateKey creates a asymmetric key in Azure Key Vault. // CreateKey creates a asymmetric key in Azure Key Vault.
func (k *KeyVault) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { func (k *KeyVault) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
if req.Name == "" {
return nil, errors.New("createKeyRequest 'name' cannot be empty")
}
vault, name, _, err := parseKeyName(req.Name) vault, name, _, err := parseKeyName(req.Name)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -0,0 +1,482 @@
//go:generate mockgen -package mock -mock_names=KeyVaultClient=KeyVaultClient -destination internal/mock/key_vault_client.go github.com/smallstep/certificates/kms/azurekms KeyVaultClient
package azurekms
import (
"context"
"crypto"
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/Azure/go-autorest/autorest/date"
"github.com/golang/mock/gomock"
"github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/azurekms/internal/mock"
"go.step.sm/crypto/keyutil"
"gopkg.in/square/go-jose.v2"
)
var errTest = fmt.Errorf("test error")
func mockNow(t *testing.T) time.Time {
old := now
t0 := time.Unix(1234567890, 123).UTC()
now = func() time.Time {
return t0
}
t.Cleanup(func() {
now = old
})
return t0
}
func mockClient(t *testing.T) *mock.KeyVaultClient {
t.Helper()
ctrl := gomock.NewController(t)
t.Cleanup(func() {
ctrl.Finish()
})
return mock.NewKeyVaultClient(ctrl)
}
func mockCreateClient(t *testing.T, ctrl *gomock.Controller) {
t.Helper()
old := createClient
createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) {
return mock.NewKeyVaultClient(ctrl), nil
}
t.Cleanup(func() {
createClient = old
})
}
func createJWK(t *testing.T, pub crypto.PublicKey) *keyvault.JSONWebKey {
t.Helper()
b, err := json.Marshal(&jose.JSONWebKey{
Key: pub,
})
if err != nil {
t.Fatal(err)
}
key := new(keyvault.JSONWebKey)
if err := json.Unmarshal(b, key); err != nil {
t.Fatal(err)
}
return key
}
func TestNew(t *testing.T) {
ctrl := gomock.NewController(t)
mockCreateClient(t, ctrl)
type args struct {
ctx context.Context
opts apiv1.Options
}
tests := []struct {
name string
args args
want *KeyVault
wantErr bool
}{
{"ok", args{context.Background(), apiv1.Options{}}, &KeyVault{
baseClient: mock.NewKeyVaultClient(ctrl),
}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.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 !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
func TestKeyVault_GetPublicKey(t *testing.T) {
key, err := keyutil.GenerateDefaultSigner()
if err != nil {
t.Fatal(err)
}
pub := key.Public()
jwk := createJWK(t, pub)
client := mockClient(t)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "").Return(keyvault.KeyBundle{
Key: jwk,
}, nil)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{
Key: jwk,
}, nil)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", "my-version").Return(keyvault.KeyBundle{}, errTest)
type fields struct {
baseClient KeyVaultClient
}
type args struct {
req *apiv1.GetPublicKeyRequest
}
tests := []struct {
name string
fields fields
args args
want crypto.PublicKey
wantErr bool
}{
{"ok", fields{client}, args{&apiv1.GetPublicKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
}}, pub, false},
{"ok with version", fields{client}, args{&apiv1.GetPublicKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key?version=my-version",
}}, pub, false},
{"fail GetKey", fields{client}, args{&apiv1.GetPublicKeyRequest{
Name: "azurekms:vault=my-vault;id=not-found?version=my-version",
}}, nil, true},
{"fail vault", fields{client}, args{&apiv1.GetPublicKeyRequest{
Name: "azurekms:vault=;id=not-found?version=my-version",
}}, nil, true},
{"fail id", fields{client}, args{&apiv1.GetPublicKeyRequest{
Name: "azurekms:vault=;id=?version=my-version",
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KeyVault{
baseClient: tt.fields.baseClient,
}
got, err := k.GetPublicKey(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("KeyVault.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("KeyVault.GetPublicKey() = %v, want %v", got, tt.want)
}
})
}
}
func TestKeyVault_CreateKey(t *testing.T) {
ecKey, err := keyutil.GenerateDefaultSigner()
if err != nil {
t.Fatal(err)
}
rsaKey, err := keyutil.GenerateSigner("RSA", "", 2048)
if err != nil {
t.Fatal(err)
}
ecPub := ecKey.Public()
rsaPub := rsaKey.Public()
ecJWK := createJWK(t, ecPub)
rsaJWK := createJWK(t, rsaPub)
t0 := date.UnixTime(mockNow(t))
client := mockClient(t)
expects := []struct {
Name string
Kty keyvault.JSONWebKeyType
KeySize *int32
Curve keyvault.JSONWebKeyCurveName
Key *keyvault.JSONWebKey
}{
{"P-256", keyvault.EC, nil, keyvault.P256, ecJWK},
{"P-256 HSM", keyvault.ECHSM, nil, keyvault.P256, ecJWK},
{"P-256 Default", keyvault.EC, nil, keyvault.P256, ecJWK},
{"P-384", keyvault.EC, nil, keyvault.P384, ecJWK},
{"P-521", keyvault.EC, nil, keyvault.P521, ecJWK},
{"RSA 0", keyvault.RSA, &value3072, "", rsaJWK},
{"RSA 0 HSM", keyvault.RSAHSM, &value3072, "", rsaJWK},
{"RSA 2048", keyvault.RSA, &value2048, "", rsaJWK},
{"RSA 3072", keyvault.RSA, &value3072, "", rsaJWK},
{"RSA 4096", keyvault.RSA, &value4096, "", rsaJWK},
}
for _, e := range expects {
client.EXPECT().CreateKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", keyvault.KeyCreateParameters{
Kty: e.Kty,
KeySize: e.KeySize,
Curve: e.Curve,
KeyOps: &[]keyvault.JSONWebKeyOperation{
keyvault.Sign, keyvault.Verify,
},
KeyAttributes: &keyvault.KeyAttributes{
Enabled: &valueTrue,
Created: &t0,
NotBefore: &t0,
},
}).Return(keyvault.KeyBundle{
Key: e.Key,
}, nil)
}
client.EXPECT().CreateKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", gomock.Any()).Return(keyvault.KeyBundle{}, errTest)
client.EXPECT().CreateKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", gomock.Any()).Return(keyvault.KeyBundle{
Key: nil,
}, nil)
type fields struct {
baseClient KeyVaultClient
}
type args struct {
req *apiv1.CreateKeyRequest
}
tests := []struct {
name string
fields fields
args args
want *apiv1.CreateKeyResponse
wantErr bool
}{
{"ok P-256", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
ProtectionLevel: apiv1.Software,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: ecPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok P-256 HSM", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
ProtectionLevel: apiv1.HSM,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: ecPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok P-256 Default", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: ecPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok P-384", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
SignatureAlgorithm: apiv1.ECDSAWithSHA384,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: ecPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok P-521", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
SignatureAlgorithm: apiv1.ECDSAWithSHA512,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: ecPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok RSA 0", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
Bits: 0,
SignatureAlgorithm: apiv1.SHA256WithRSA,
ProtectionLevel: apiv1.Software,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: rsaPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok RSA 0 HSM", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
Bits: 0,
SignatureAlgorithm: apiv1.SHA256WithRSAPSS,
ProtectionLevel: apiv1.HSM,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: rsaPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok RSA 2048", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
Bits: 2048,
SignatureAlgorithm: apiv1.SHA384WithRSA,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: rsaPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok RSA 3072", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
Bits: 3072,
SignatureAlgorithm: apiv1.SHA512WithRSA,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: rsaPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"ok RSA 4096", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=my-key",
Bits: 4096,
SignatureAlgorithm: apiv1.SHA512WithRSAPSS,
}}, &apiv1.CreateKeyResponse{
Name: "azurekms:id=my-key;vault=my-vault",
PublicKey: rsaPub,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "azurekms:id=my-key;vault=my-vault",
},
}, false},
{"fail createKey", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=not-found",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
}}, nil, true},
{"fail convertKey", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=not-found",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
}}, nil, true},
{"fail name", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "",
}}, nil, true},
{"fail vault", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=;id=not-found?version=my-version",
}}, nil, true},
{"fail id", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=?version=my-version",
}}, nil, true},
{"fail SignatureAlgorithm", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=not-found",
SignatureAlgorithm: apiv1.PureEd25519,
}}, nil, true},
{"fail bit size", fields{client}, args{&apiv1.CreateKeyRequest{
Name: "azurekms:vault=my-vault;id=not-found",
SignatureAlgorithm: apiv1.SHA384WithRSAPSS,
Bits: 1024,
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KeyVault{
baseClient: tt.fields.baseClient,
}
got, err := k.CreateKey(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("KeyVault.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("KeyVault.CreateKey() = %v, want %v", got, tt.want)
}
})
}
}
func TestKeyVault_CreateSigner(t *testing.T) {
key, err := keyutil.GenerateDefaultSigner()
if err != nil {
t.Fatal(err)
}
pub := key.Public()
jwk := createJWK(t, pub)
client := mockClient(t)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "").Return(keyvault.KeyBundle{
Key: jwk,
}, nil)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{
Key: jwk,
}, nil)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", "my-version").Return(keyvault.KeyBundle{}, errTest)
type fields struct {
baseClient KeyVaultClient
}
type args struct {
req *apiv1.CreateSignerRequest
}
tests := []struct {
name string
fields fields
args args
want crypto.Signer
wantErr bool
}{
{"ok", fields{client}, args{&apiv1.CreateSignerRequest{
SigningKey: "azurekms:vault=my-vault;id=my-key",
}}, &Signer{
client: client,
vaultBaseURL: "https://my-vault.vault.azure.net/",
name: "my-key",
version: "",
publicKey: pub,
}, false},
{"ok with version", fields{client}, args{&apiv1.CreateSignerRequest{
SigningKey: "azurekms:vault=my-vault;id=my-key;version=my-version",
}}, &Signer{
client: client,
vaultBaseURL: "https://my-vault.vault.azure.net/",
name: "my-key",
version: "my-version",
publicKey: pub,
}, false},
{"fail GetKey", fields{client}, args{&apiv1.CreateSignerRequest{
SigningKey: "azurekms:vault=my-vault;id=not-found;version=my-version",
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KeyVault{
baseClient: tt.fields.baseClient,
}
got, err := k.CreateSigner(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("KeyVault.CreateSigner() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("KeyVault.CreateSigner() = %v, want %v", got, tt.want)
}
})
}
}
func TestKeyVault_Close(t *testing.T) {
client := mockClient(t)
type fields struct {
baseClient KeyVaultClient
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{"ok", fields{client}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KeyVault{
baseClient: tt.fields.baseClient,
}
if err := k.Close(); (err != nil) != tt.wantErr {
t.Errorf("KeyVault.Close() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -24,7 +24,7 @@ type Signer struct {
} }
// NewSigner creates a new signer using a key in the AWS KMS. // NewSigner creates a new signer using a key in the AWS KMS.
func NewSigner(client KeyVaultClient, signingKey string) (*Signer, error) { func NewSigner(client KeyVaultClient, signingKey string) (crypto.Signer, error) {
vault, name, version, err := parseKeyName(signingKey) vault, name, version, err := parseKeyName(signingKey)
if err != nil { if err != nil {
return nil, err return nil, err
@ -114,8 +114,17 @@ func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]
func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (keyvault.JSONWebKeySignatureAlgorithm, error) { func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (keyvault.JSONWebKeySignatureAlgorithm, error) {
switch key.(type) { switch key.(type) {
case *rsa.PublicKey: case *rsa.PublicKey:
_, isPSS := opts.(*rsa.PSSOptions) hashFunc := opts.HashFunc()
switch h := opts.HashFunc(); h { pss, isPSS := opts.(*rsa.PSSOptions)
// Random salt lengths are not supported
if isPSS &&
pss.SaltLength != rsa.PSSSaltLengthAuto &&
pss.SaltLength != rsa.PSSSaltLengthEqualsHash &&
pss.SaltLength != hashFunc.Size() {
return "", errors.Errorf("unsupported RSA-PSS salt length %d", pss.SaltLength)
}
switch h := hashFunc; h {
case crypto.SHA256: case crypto.SHA256:
if isPSS { if isPSS {
return keyvault.PS256, nil return keyvault.PS256, nil

329
kms/azurekms/signer_test.go Normal file
View file

@ -0,0 +1,329 @@
package azurekms
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"io"
"reflect"
"testing"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/golang/mock/gomock"
"go.step.sm/crypto/keyutil"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)
func TestNewSigner(t *testing.T) {
key, err := keyutil.GenerateDefaultSigner()
if err != nil {
t.Fatal(err)
}
pub := key.Public()
jwk := createJWK(t, pub)
client := mockClient(t)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "").Return(keyvault.KeyBundle{
Key: jwk,
}, nil)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{
Key: jwk,
}, nil)
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", "my-version").Return(keyvault.KeyBundle{}, errTest)
type args struct {
client KeyVaultClient
signingKey string
}
tests := []struct {
name string
args args
want crypto.Signer
wantErr bool
}{
{"ok", args{client, "azurekms:vault=my-vault;id=my-key"}, &Signer{
client: client,
vaultBaseURL: "https://my-vault.vault.azure.net/",
name: "my-key",
version: "",
publicKey: pub,
}, false},
{"ok with version", args{client, "azurekms:id=my-key;vault=my-vault?version=my-version"}, &Signer{
client: client,
vaultBaseURL: "https://my-vault.vault.azure.net/",
name: "my-key",
version: "my-version",
publicKey: pub,
}, false},
{"fail GetKey", args{client, "azurekms:id=not-found;vault=my-vault?version=my-version"}, nil, true},
{"fail vault", args{client, "azurekms:id=not-found;vault="}, nil, true},
{"fail id", args{client, "azurekms:id=;vault=my-vault?version=my-version"}, nil, true},
{"fail scheme", args{client, "kms:id=not-found;vault=my-vault?version=my-version"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewSigner(tt.args.client, 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) {
key, err := keyutil.GenerateDefaultSigner()
if err != nil {
t.Fatal(err)
}
pub := key.Public()
type fields struct {
publicKey crypto.PublicKey
}
tests := []struct {
name string
fields fields
want crypto.PublicKey
}{
{"ok", fields{pub}, pub},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Signer{
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) {
sign := func(kty, crv string, bits int, opts crypto.SignerOpts) (crypto.PublicKey, []byte, string, []byte) {
key, err := keyutil.GenerateSigner(kty, crv, bits)
if err != nil {
t.Fatal(err)
}
h := opts.HashFunc().New()
h.Write([]byte("random-data"))
sum := h.Sum(nil)
var sig, resultSig []byte
if priv, ok := key.(*ecdsa.PrivateKey); ok {
r, s, err := ecdsa.Sign(rand.Reader, priv, sum)
if err != nil {
t.Fatal(err)
}
curveBits := priv.Params().BitSize
keyBytes := curveBits / 8
if curveBits%8 > 0 {
keyBytes++
}
rBytes := r.Bytes()
rBytesPadded := make([]byte, keyBytes)
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
sBytes := s.Bytes()
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
resultSig = append(rBytesPadded, sBytesPadded...)
var b cryptobyte.Builder
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1BigInt(r)
b.AddASN1BigInt(s)
})
sig, err = b.Bytes()
if err != nil {
t.Fatal(err)
}
} else {
sig, err = key.Sign(rand.Reader, sum, opts)
if err != nil {
t.Fatal(err)
}
resultSig = sig
}
return key.Public(), h.Sum(nil), base64.RawURLEncoding.EncodeToString(resultSig), sig
}
p256, p256Digest, p256ResultSig, p256Sig := sign("EC", "P-256", 0, crypto.SHA256)
p384, p384Digest, p386ResultSig, p384Sig := sign("EC", "P-384", 0, crypto.SHA384)
p521, p521Digest, p521ResultSig, p521Sig := sign("EC", "P-521", 0, crypto.SHA512)
rsaSHA256, rsaSHA256Digest, rsaSHA256ResultSig, rsaSHA256Sig := sign("RSA", "", 2048, crypto.SHA256)
rsaSHA384, rsaSHA384Digest, rsaSHA384ResultSig, rsaSHA384Sig := sign("RSA", "", 2048, crypto.SHA384)
rsaSHA512, rsaSHA512Digest, rsaSHA512ResultSig, rsaSHA512Sig := sign("RSA", "", 2048, crypto.SHA512)
rsaPSSSHA256, rsaPSSSHA256Digest, rsaPSSSHA256ResultSig, rsaPSSSHA256Sig := sign("RSA", "", 2048, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthAuto,
Hash: crypto.SHA256,
})
rsaPSSSHA384, rsaPSSSHA384Digest, rsaPSSSHA384ResultSig, rsaPSSSHA384Sig := sign("RSA", "", 2048, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthAuto,
Hash: crypto.SHA512,
})
rsaPSSSHA512, rsaPSSSHA512Digest, rsaPSSSHA512ResultSig, rsaPSSSHA512Sig := sign("RSA", "", 2048, &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthAuto,
Hash: crypto.SHA512,
})
ed25519Key, err := keyutil.GenerateSigner("OKP", "Ed25519", 0)
if err != nil {
t.Fatal(err)
}
client := mockClient(t)
expects := []struct {
name string
keyVersion string
alg keyvault.JSONWebKeySignatureAlgorithm
digest []byte
result keyvault.KeyOperationResult
err error
}{
{"P-256", "", keyvault.ES256, p256Digest, keyvault.KeyOperationResult{
Result: &p256ResultSig,
}, nil},
{"P-384", "my-version", keyvault.ES384, p384Digest, keyvault.KeyOperationResult{
Result: &p386ResultSig,
}, nil},
{"P-521", "my-version", keyvault.ES512, p521Digest, keyvault.KeyOperationResult{
Result: &p521ResultSig,
}, nil},
{"RSA SHA256", "", keyvault.RS256, rsaSHA256Digest, keyvault.KeyOperationResult{
Result: &rsaSHA256ResultSig,
}, nil},
{"RSA SHA384", "", keyvault.RS384, rsaSHA384Digest, keyvault.KeyOperationResult{
Result: &rsaSHA384ResultSig,
}, nil},
{"RSA SHA512", "", keyvault.RS512, rsaSHA512Digest, keyvault.KeyOperationResult{
Result: &rsaSHA512ResultSig,
}, nil},
{"RSA-PSS SHA256", "", keyvault.PS256, rsaPSSSHA256Digest, keyvault.KeyOperationResult{
Result: &rsaPSSSHA256ResultSig,
}, nil},
{"RSA-PSS SHA384", "", keyvault.PS384, rsaPSSSHA384Digest, keyvault.KeyOperationResult{
Result: &rsaPSSSHA384ResultSig,
}, nil},
{"RSA-PSS SHA512", "", keyvault.PS512, rsaPSSSHA512Digest, keyvault.KeyOperationResult{
Result: &rsaPSSSHA512ResultSig,
}, nil},
// Errors
{"fail Sign", "", keyvault.RS256, rsaSHA256Digest, keyvault.KeyOperationResult{}, errTest},
{"fail sign length", "", keyvault.ES256, p256Digest, keyvault.KeyOperationResult{
Result: &rsaSHA256ResultSig,
}, nil},
}
for _, e := range expects {
value := base64.RawURLEncoding.EncodeToString(e.digest)
client.EXPECT().Sign(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", e.keyVersion, keyvault.KeySignParameters{
Algorithm: e.alg,
Value: &value,
}).Return(e.result, e.err)
}
type fields struct {
client KeyVaultClient
vaultBaseURL string
name string
version 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 P-256", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{
rand.Reader, p256Digest[:], crypto.SHA256,
}, p256Sig, false},
{"ok P-384", fields{client, "https://my-vault.vault.azure.net/", "my-key", "my-version", p384}, args{
rand.Reader, p384Digest[:], crypto.SHA384,
}, p384Sig, false},
{"ok P-521", fields{client, "https://my-vault.vault.azure.net/", "my-key", "my-version", p521}, args{
rand.Reader, p521Digest[:], crypto.SHA512,
}, p521Sig, false},
{"ok RSA SHA256", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA256}, args{
rand.Reader, rsaSHA256Digest[:], crypto.SHA256,
}, rsaSHA256Sig, false},
{"ok RSA SHA384", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA384}, args{
rand.Reader, rsaSHA384Digest[:], crypto.SHA384,
}, rsaSHA384Sig, false},
{"ok RSA SHA512", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA512}, args{
rand.Reader, rsaSHA512Digest[:], crypto.SHA512,
}, rsaSHA512Sig, false},
{"ok RSA-PSS SHA256", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA256}, args{
rand.Reader, rsaPSSSHA256Digest[:], &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthAuto,
Hash: crypto.SHA256,
},
}, rsaPSSSHA256Sig, false},
{"ok RSA-PSS SHA384", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA384}, args{
rand.Reader, rsaPSSSHA384Digest[:], &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: crypto.SHA384,
},
}, rsaPSSSHA384Sig, false},
{"ok RSA-PSS SHA512", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA512}, args{
rand.Reader, rsaPSSSHA512Digest[:], &rsa.PSSOptions{
SaltLength: 64,
Hash: crypto.SHA512,
},
}, rsaPSSSHA512Sig, false},
{"fail Sign", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA256}, args{
rand.Reader, rsaSHA256Digest[:], crypto.SHA256,
}, nil, true},
{"fail sign length", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{
rand.Reader, p256Digest[:], crypto.SHA256,
}, nil, true},
{"fail RSA-PSS salt length", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA256}, args{
rand.Reader, rsaPSSSHA256Digest[:], &rsa.PSSOptions{
SaltLength: 64,
Hash: crypto.SHA256,
},
}, nil, true},
{"fail RSA Hash", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA256}, args{
rand.Reader, rsaSHA256Digest[:], crypto.SHA1,
}, nil, true},
{"fail ECDSA Hash", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{
rand.Reader, p256Digest[:], crypto.MD5,
}, nil, true},
{"fail Ed25519", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", ed25519Key}, args{
rand.Reader, []byte("message"), crypto.Hash(0),
}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Signer{
client: tt.fields.client,
vaultBaseURL: tt.fields.vaultBaseURL,
name: tt.fields.name,
version: tt.fields.version,
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)
}
})
}
}