constellation/keyservice/kms/azure/azure.go

161 lines
5.4 KiB
Go
Raw Normal View History

/*
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)
}