Allow to configure azurekms using the URI

With an URI, azurekms can be configured with client credentials,
and it can define a default vault and protection level.
This commit is contained in:
Mariano Cano 2021-10-12 18:24:58 -07:00
parent e3220232a0
commit edd475b81b
7 changed files with 203 additions and 35 deletions

1
go.mod
View file

@ -5,6 +5,7 @@ go 1.15
require ( require (
cloud.google.com/go v0.83.0 cloud.google.com/go v0.83.0
github.com/Azure/azure-sdk-for-go v58.0.0+incompatible github.com/Azure/azure-sdk-for-go v58.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.17
github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 github.com/Azure/go-autorest/autorest/azure/auth v0.5.8
github.com/Azure/go-autorest/autorest/date v0.3.0 github.com/Azure/go-autorest/autorest/date v0.3.0
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect

View file

@ -7,10 +7,12 @@ import (
"time" "time"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth" "github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/date" "github.com/Azure/go-autorest/autorest/date"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/uri"
) )
func init() { func init() {
@ -126,9 +128,60 @@ type KeyVaultClient interface {
// functionality in /sdk/keyvault, we should migrate to that once available. // functionality in /sdk/keyvault, we should migrate to that once available.
type KeyVault struct { type KeyVault struct {
baseClient KeyVaultClient baseClient KeyVaultClient
defaults DefaultOptions
}
// DefaultOptions are custom options that can be passed as defaults using the
// URI in apiv1.Options.
type DefaultOptions struct {
Vault string
ProtectionLevel apiv1.ProtectionLevel
} }
var createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { var createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) {
baseClient := keyvault.New()
// With an URI, try to log in only using client credentials in the URI.
// Client credentials requires:
// - client-id
// - client-secret
// - tenant-id
// And optionally the aad-endpoint to support custom clouds:
// - aad-endpoint (defaults to https://login.microsoftonline.com/)
if opts.URI != "" {
u, err := uri.ParseWithScheme(Scheme, opts.URI)
if err != nil {
return nil, err
}
// Required options
clientID := u.Get("client-id")
clientSecret := u.Get("client-secret")
tenantID := u.Get("tenant-id")
// optional
aadEndpoint := u.Get("aad-endpoint")
if clientID != "" && clientSecret != "" && tenantID != "" {
s := auth.EnvironmentSettings{
Values: map[string]string{
auth.ClientID: clientID,
auth.ClientSecret: clientSecret,
auth.TenantID: tenantID,
auth.Resource: vaultResource,
},
Environment: azure.PublicCloud,
}
if aadEndpoint != "" {
s.Environment.ActiveDirectoryEndpoint = aadEndpoint
}
baseClient.Authorizer, err = s.GetAuthorizer()
if err != nil {
return nil, err
}
return baseClient, nil
}
}
// Attempt to authorize with the following methods: // Attempt to authorize with the following methods:
// 1. Environment variables. // 1. Environment variables.
// - Client credentials // - Client credentials
@ -143,8 +196,6 @@ var createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient
return nil, errors.Wrap(err, "error getting authorizer for key vault") return nil, errors.Wrap(err, "error getting authorizer for key vault")
} }
} }
baseClient := keyvault.New()
baseClient.Authorizer = authorizer baseClient.Authorizer = authorizer
return &baseClient, nil return &baseClient, nil
} }
@ -155,8 +206,24 @@ func New(ctx context.Context, opts apiv1.Options) (*KeyVault, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// step and step-ca do not need and URI, but having a default vault and
// protection level is useful if this package is used as an api
var defaults DefaultOptions
if opts.URI != "" {
u, err := uri.ParseWithScheme(Scheme, opts.URI)
if err != nil {
return nil, err
}
defaults.Vault = u.Get("vault")
if u.GetBool("hsm") {
defaults.ProtectionLevel = apiv1.HSM
}
}
return &KeyVault{ return &KeyVault{
baseClient: baseClient, baseClient: baseClient,
defaults: defaults,
}, nil }, nil
} }
@ -166,7 +233,7 @@ func (k *KeyVault) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKe
return nil, errors.New("getPublicKeyRequest 'name' cannot be empty") return nil, errors.New("getPublicKeyRequest 'name' cannot be empty")
} }
vault, name, version, _, err := parseKeyName(req.Name) vault, name, version, _, err := parseKeyName(req.Name, k.defaults)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -188,7 +255,7 @@ func (k *KeyVault) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo
return nil, errors.New("createKeyRequest 'name' cannot be empty") return nil, errors.New("createKeyRequest 'name' cannot be empty")
} }
vault, name, _, hsm, err := parseKeyName(req.Name) vault, name, _, hsm, err := parseKeyName(req.Name, k.defaults)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -260,7 +327,7 @@ func (k *KeyVault) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer,
if req.SigningKey == "" { if req.SigningKey == "" {
return nil, errors.New("createSignerRequest 'signingKey' cannot be empty") return nil, errors.New("createSignerRequest 'signingKey' cannot be empty")
} }
return NewSigner(k.baseClient, req.SigningKey) return NewSigner(k.baseClient, req.SigningKey, k.defaults)
} }
// Close closes the client connection to the Azure Key Vault. This is a noop. // Close closes the client connection to the Azure Key Vault. This is a noop.
@ -270,6 +337,6 @@ func (k *KeyVault) Close() error {
// ValidateName validates that the given string is a valid URI. // ValidateName validates that the given string is a valid URI.
func (k *KeyVault) ValidateName(s string) error { func (k *KeyVault) ValidateName(s string) error {
_, _, _, _, err := parseKeyName(s) _, _, _, _, err := parseKeyName(s, k.defaults)
return err return err
} }

View file

@ -89,11 +89,44 @@ func TestNew(t *testing.T) {
}, args{context.Background(), apiv1.Options{}}, &KeyVault{ }, args{context.Background(), apiv1.Options{}}, &KeyVault{
baseClient: client, baseClient: client,
}, false}, }, false},
{"ok with vault", func() {
createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) {
return client, nil
}
}, args{context.Background(), apiv1.Options{
URI: "azurekms:vault=my-vault",
}}, &KeyVault{
baseClient: client,
defaults: DefaultOptions{
Vault: "my-vault",
ProtectionLevel: apiv1.UnspecifiedProtectionLevel,
},
}, false},
{"ok with vault + hsm", func() {
createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) {
return client, nil
}
}, args{context.Background(), apiv1.Options{
URI: "azurekms:vault=my-vault;hsm=true",
}}, &KeyVault{
baseClient: client,
defaults: DefaultOptions{
Vault: "my-vault",
ProtectionLevel: apiv1.HSM,
},
}, false},
{"fail", func() { {"fail", func() {
createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) {
return nil, errTest return nil, errTest
} }
}, args{context.Background(), apiv1.Options{}}, nil, true}, }, args{context.Background(), apiv1.Options{}}, nil, true},
{"fail uri", func() {
createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) {
return client, nil
}
}, args{context.Background(), apiv1.Options{
URI: "kms:vault=my-vault;hsm=true",
}}, nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -110,6 +143,45 @@ func TestNew(t *testing.T) {
} }
} }
func TestKeyVault_createClient(t *testing.T) {
type args struct {
ctx context.Context
opts apiv1.Options
}
tests := []struct {
name string
args args
skip bool
wantErr bool
}{
{"ok", args{context.Background(), apiv1.Options{}}, true, false},
{"ok with uri", args{context.Background(), apiv1.Options{
URI: "azurekms:client-id=id;client-secret=secret;tenant-id=id",
}}, false, false},
{"ok with uri+aad", args{context.Background(), apiv1.Options{
URI: "azurekms:client-id=id;client-secret=secret;tenant-id=id;aad-enpoint=https%3A%2F%2Flogin.microsoftonline.us%2F",
}}, false, false},
{"ok with uri no config", args{context.Background(), apiv1.Options{
URI: "azurekms:",
}}, true, false},
{"fail uri", args{context.Background(), apiv1.Options{
URI: "kms:client-id=id;client-secret=secret;tenant-id=id",
}}, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skip {
t.SkipNow()
}
_, err := createClient(tt.args.ctx, tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestKeyVault_GetPublicKey(t *testing.T) { func TestKeyVault_GetPublicKey(t *testing.T) {
key, err := keyutil.GenerateDefaultSigner() key, err := keyutil.GenerateDefaultSigner()
if err != nil { if err != nil {

View file

@ -24,8 +24,8 @@ 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) (crypto.Signer, error) { func NewSigner(client KeyVaultClient, signingKey string, defaults DefaultOptions) (crypto.Signer, error) {
vault, name, version, _, err := parseKeyName(signingKey) vault, name, version, _, err := parseKeyName(signingKey, defaults)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -12,6 +12,7 @@ import (
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/smallstep/certificates/kms/apiv1"
"go.step.sm/crypto/keyutil" "go.step.sm/crypto/keyutil"
"golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1" "golang.org/x/crypto/cryptobyte/asn1"
@ -32,11 +33,16 @@ func TestNewSigner(t *testing.T) {
client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{ client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{
Key: jwk, Key: jwk,
}, nil) }, 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) client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", "my-version").Return(keyvault.KeyBundle{}, errTest)
var noOptions DefaultOptions
type args struct { type args struct {
client KeyVaultClient client KeyVaultClient
signingKey string signingKey string
defaults DefaultOptions
} }
tests := []struct { tests := []struct {
name string name string
@ -44,28 +50,35 @@ func TestNewSigner(t *testing.T) {
want crypto.Signer want crypto.Signer
wantErr bool wantErr bool
}{ }{
{"ok", args{client, "azurekms:vault=my-vault;name=my-key"}, &Signer{ {"ok", args{client, "azurekms:vault=my-vault;name=my-key", noOptions}, &Signer{
client: client, client: client,
vaultBaseURL: "https://my-vault.vault.azure.net/", vaultBaseURL: "https://my-vault.vault.azure.net/",
name: "my-key", name: "my-key",
version: "", version: "",
publicKey: pub, publicKey: pub,
}, false}, }, false},
{"ok with version", args{client, "azurekms:name=my-key;vault=my-vault?version=my-version"}, &Signer{ {"ok with version", args{client, "azurekms:name=my-key;vault=my-vault?version=my-version", noOptions}, &Signer{
client: client, client: client,
vaultBaseURL: "https://my-vault.vault.azure.net/", vaultBaseURL: "https://my-vault.vault.azure.net/",
name: "my-key", name: "my-key",
version: "my-version", version: "my-version",
publicKey: pub, publicKey: pub,
}, false}, }, false},
{"fail GetKey", args{client, "azurekms:name=not-found;vault=my-vault?version=my-version"}, nil, true}, {"ok with options", args{client, "azurekms:name=my-key?version=my-version", DefaultOptions{Vault: "my-vault", ProtectionLevel: apiv1.HSM}}, &Signer{
{"fail vault", args{client, "azurekms:name=not-found;vault="}, nil, true}, client: client,
{"fail id", args{client, "azurekms:name=;vault=my-vault?version=my-version"}, nil, true}, vaultBaseURL: "https://my-vault.vault.azure.net/",
{"fail scheme", args{client, "kms:name=not-found;vault=my-vault?version=my-version"}, nil, true}, name: "my-key",
version: "my-version",
publicKey: pub,
}, false},
{"fail GetKey", args{client, "azurekms:name=not-found;vault=my-vault?version=my-version", noOptions}, nil, true},
{"fail vault", args{client, "azurekms:name=not-found;vault=", noOptions}, nil, true},
{"fail id", args{client, "azurekms:name=;vault=my-vault?version=my-version", noOptions}, nil, true},
{"fail scheme", args{client, "kms:name=not-found;vault=my-vault?version=my-version", noOptions}, nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := NewSigner(tt.args.client, tt.args.signingKey) got, err := NewSigner(tt.args.client, tt.args.signingKey, tt.args.defaults)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr)
return return

View file

@ -9,6 +9,7 @@ import (
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/uri" "github.com/smallstep/certificates/kms/uri"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
) )
@ -50,10 +51,10 @@ func getKeyName(vault, name string, bundle keyvault.KeyBundle) string {
// //
// HSM can also be passed to define the protection level if this is not given in // HSM can also be passed to define the protection level if this is not given in
// CreateQuery. // CreateQuery.
func parseKeyName(rawURI string) (vault, name, version string, hsm bool, err error) { func parseKeyName(rawURI string, defaults DefaultOptions) (vault, name, version string, hsm bool, err error) {
var u *uri.URI var u *uri.URI
u, err = uri.ParseWithScheme("azurekms", rawURI) u, err = uri.ParseWithScheme(Scheme, rawURI)
if err != nil { if err != nil {
return return
} }
@ -62,12 +63,21 @@ func parseKeyName(rawURI string) (vault, name, version string, hsm bool, err err
return return
} }
if vault = u.Get("vault"); vault == "" { if vault = u.Get("vault"); vault == "" {
err = errors.Errorf("key uri %s is not valid: vault is missing", rawURI) if defaults.Vault == "" {
name = "" name = ""
return err = errors.Errorf("key uri %s is not valid: vault is missing", rawURI)
return
}
vault = defaults.Vault
} }
if u.Get("hsm") == "" {
hsm = (defaults.ProtectionLevel == apiv1.HSM)
} else {
hsm = u.GetBool("hsm")
}
version = u.Get("version") version = u.Get("version")
hsm = u.GetBool("hsm")
return return
} }

View file

@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
"github.com/smallstep/certificates/kms/apiv1"
) )
func Test_getKeyName(t *testing.T) { func Test_getKeyName(t *testing.T) {
@ -42,8 +43,10 @@ func Test_getKeyName(t *testing.T) {
} }
func Test_parseKeyName(t *testing.T) { func Test_parseKeyName(t *testing.T) {
var noOptions DefaultOptions
type args struct { type args struct {
rawURI string rawURI string
defaults DefaultOptions
} }
tests := []struct { tests := []struct {
name string name string
@ -54,22 +57,24 @@ func Test_parseKeyName(t *testing.T) {
wantHsm bool wantHsm bool
wantErr bool wantErr bool
}{ }{
{"ok", args{"azurekms:name=my-key;vault=my-vault?version=my-version"}, "my-vault", "my-key", "my-version", false, false}, {"ok", args{"azurekms:name=my-key;vault=my-vault?version=my-version", noOptions}, "my-vault", "my-key", "my-version", false, false},
{"ok opaque version", args{"azurekms:name=my-key;vault=my-vault;version=my-version"}, "my-vault", "my-key", "my-version", false, false}, {"ok opaque version", args{"azurekms:name=my-key;vault=my-vault;version=my-version", noOptions}, "my-vault", "my-key", "my-version", false, false},
{"ok no version", args{"azurekms:name=my-key;vault=my-vault"}, "my-vault", "my-key", "", false, false}, {"ok no version", args{"azurekms:name=my-key;vault=my-vault", noOptions}, "my-vault", "my-key", "", false, false},
{"ok hsm", args{"azurekms:name=my-key;vault=my-vault?hsm=true"}, "my-vault", "my-key", "", true, false}, {"ok hsm", args{"azurekms:name=my-key;vault=my-vault?hsm=true", noOptions}, "my-vault", "my-key", "", true, false},
{"ok hsm false", args{"azurekms:name=my-key;vault=my-vault?hsm=false"}, "my-vault", "my-key", "", false, false}, {"ok hsm false", args{"azurekms:name=my-key;vault=my-vault?hsm=false", noOptions}, "my-vault", "my-key", "", false, false},
{"fail scheme", args{"azure:name=my-key;vault=my-vault"}, "", "", "", false, true}, {"ok default vault", args{"azurekms:name=my-key?version=my-version", DefaultOptions{Vault: "my-vault"}}, "my-vault", "my-key", "my-version", false, false},
{"fail parse uri", args{"azurekms:name=%ZZ;vault=my-vault"}, "", "", "", false, true}, {"ok default hsm", args{"azurekms:name=my-key;vault=my-vault?version=my-version", DefaultOptions{Vault: "other-vault", ProtectionLevel: apiv1.HSM}}, "my-vault", "my-key", "my-version", true, false},
{"fail no name", args{"azurekms:vault=my-vault"}, "", "", "", false, true}, {"fail scheme", args{"azure:name=my-key;vault=my-vault", noOptions}, "", "", "", false, true},
{"fail empty name", args{"azurekms:name=;vault=my-vault"}, "", "", "", false, true}, {"fail parse uri", args{"azurekms:name=%ZZ;vault=my-vault", noOptions}, "", "", "", false, true},
{"fail no vault", args{"azurekms:name=my-key"}, "", "", "", false, true}, {"fail no name", args{"azurekms:vault=my-vault", noOptions}, "", "", "", false, true},
{"fail empty vault", args{"azurekms:name=my-key;vault="}, "", "", "", false, true}, {"fail empty name", args{"azurekms:name=;vault=my-vault", noOptions}, "", "", "", false, true},
{"fail empty", args{""}, "", "", "", false, true}, {"fail no vault", args{"azurekms:name=my-key", noOptions}, "", "", "", false, true},
{"fail empty vault", args{"azurekms:name=my-key;vault=", noOptions}, "", "", "", false, true},
{"fail empty", args{"", noOptions}, "", "", "", false, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
gotVault, gotName, gotVersion, gotHsm, err := parseKeyName(tt.args.rawURI) gotVault, gotName, gotVersion, gotHsm, err := parseKeyName(tt.args.rawURI, tt.args.defaults)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("parseKeyName() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("parseKeyName() error = %v, wantErr %v", err, tt.wantErr)
return return