constellation/keyservice/kms/azure/azure.go
Otto Bittner 90b88e1cf9 kms: rename kms to keyservice
In the light of extending our eKMS support it will be helpful
to have a tighter use of the word "KMS".
KMS should refer to the actual component that manages keys.
The keyservice, also called KMS in the constellation code,
does not manage keys itself. It talks to a KMS backend,
which in turn does the actual key management.
2023-01-16 11:56:34 +01:00

161 lines
5.4 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package azure
import (
"context"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets"
"github.com/edgelesssys/constellation/v2/keyservice/internal/config"
"github.com/edgelesssys/constellation/v2/keyservice/internal/storage"
"github.com/edgelesssys/constellation/v2/keyservice/kms"
"github.com/edgelesssys/constellation/v2/keyservice/kms/util"
)
const (
vaultPrefix = "https://"
// DefaultCloud is the suffix for the default Vault URL.
DefaultCloud VaultSuffix = ".vault.azure.net/"
// ChinaCloud is the suffix for Vaults in Azure China Cloud.
ChinaCloud VaultSuffix = ".vault.azure.cn/"
// USGovCloud is the suffix for Vaults in Azure US Government Cloud.
USGovCloud VaultSuffix = ".vault.usgovcloudapi.net/"
// GermanCloud is the suffix for Vaults in Azure German Cloud.
GermanCloud VaultSuffix = ".vault.microsoftazure.de/"
)
// VaultSuffix is the suffix added to a Vault name to create a valid Vault URL.
type VaultSuffix string
type kmsClientAPI interface {
SetSecret(ctx context.Context, secretName string, parameters azsecrets.SetSecretParameters, options *azsecrets.SetSecretOptions) (azsecrets.SetSecretResponse, error)
GetSecret(ctx context.Context, secretName string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error)
}
// KMSClient implements the CloudKMS interface for Azure Key Vault.
type KMSClient struct {
client kmsClientAPI
storage kms.Storage
}
// Opts are optional settings for AKV clients.
type Opts struct {
Credentials *azidentity.DefaultAzureCredentialOptions
Keys *azkeys.ClientOptions
Secrets *azsecrets.ClientOptions
}
// New initializes a KMS client for Azure Key Vault.
func New(ctx context.Context, vaultName string, vaultType VaultSuffix, store kms.Storage, opts *Opts) (*KMSClient, error) {
if opts == nil {
opts = &Opts{}
}
cred, err := azidentity.NewDefaultAzureCredential(opts.Credentials)
if err != nil {
return nil, fmt.Errorf("loading credentials: %w", err)
}
client, err := azsecrets.NewClient(vaultPrefix+vaultName+string(vaultType), cred, opts.Secrets)
if err != nil {
return nil, fmt.Errorf("creating azure secrets client: %w", err)
}
// `azsecrets.NewClient()` does not error if the vault is non existent
// Test here if we can reach the vault, and error otherwise
pager := client.NewListSecretsPager(nil)
if _, err := pager.NextPage(ctx); err != nil {
return nil, fmt.Errorf("AKV not reachable: %w", err)
}
if store == nil {
store = storage.NewMemMapStorage()
}
return &KMSClient{client: client, storage: store}, nil
}
// CreateKEK saves a new Key Encryption Key using Azure Key Vault.
//
// Keys are saved as software protected secrets.
// If no key material is provided, a new random 32 Byte key is generated and imported to the Vault.
func (c *KMSClient) CreateKEK(ctx context.Context, keyID string, key []byte) error {
if len(key) == 0 {
var err error
key, err = util.GetRandomKey(config.SymmetricKeyLength)
if err != nil {
return fmt.Errorf("key generation: %w", err)
}
}
// Saving symmetric keys in Azure Key Vault requires encoding them to base64
secretValue := azsecrets.SetSecretParameters{
Value: to.Ptr(base64.StdEncoding.EncodeToString(key)),
ContentType: to.Ptr("KeyEncryptionKey"),
Tags: toAzureTags(config.KmsTags),
}
_, err := c.client.SetSecret(ctx, keyID, secretValue, &azsecrets.SetSecretOptions{})
if err != nil {
return fmt.Errorf("importing KEK to Azure Key Vault: %w", err)
}
return nil
}
// GetDEK decrypts a DEK from storage.
func (c *KMSClient) GetDEK(ctx context.Context, kekID, keyID string, dekSize int) ([]byte, error) {
kek, err := c.getKEK(ctx, kekID)
if err != nil {
return nil, fmt.Errorf("loading KEK from key vault: %w", err)
}
encryptedDEK, err := c.storage.Get(ctx, keyID)
if err != nil {
if !errors.Is(err, storage.ErrDEKUnset) {
return nil, fmt.Errorf("loading encrypted DEK from storage: %w", err)
}
// If the DEK does not exist we generate a new random DEK and save it to storage
newDEK, err := util.GetRandomKey(dekSize)
if err != nil {
return nil, fmt.Errorf("key generation: %w", err)
}
return newDEK, c.putDEK(ctx, keyID, kek, newDEK)
}
// Azure Key Vault does not support crypto operations with secrets, so we do the unwrapping ourselves
return util.UnwrapAES(encryptedDEK, kek)
}
// putDEK encrypts a DEK and saves it to storage.
func (c *KMSClient) putDEK(ctx context.Context, keyID string, kek, plainDEK []byte) error {
// Azure Key Vault does not support crypto operations with secrets, so we do the wrapping ourselves
encryptedDEK, err := util.WrapAES(plainDEK, kek)
if err != nil {
return fmt.Errorf("encrypting DEK: %w", err)
}
return c.storage.Put(ctx, keyID, encryptedDEK)
}
// getKEK loads a Key Encryption Key from Azure Key Vault.
func (c *KMSClient) getKEK(ctx context.Context, kekID string) ([]byte, error) {
res, err := c.client.GetSecret(ctx, kekID, "", nil)
if err != nil {
if strings.Contains(err.Error(), "SecretNotFound") {
return nil, kms.ErrKEKUnknown
}
return nil, err
}
// Keys are saved in base64, decode them
return base64.StdEncoding.DecodeString(*res.Value)
}