2022-09-05 03:06:08 -04:00
|
|
|
/*
|
|
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
2022-03-22 11:03:15 -04:00
|
|
|
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"
|
2022-10-18 06:08:59 -04:00
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
|
2022-03-22 11:03:15 -04:00
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets"
|
2023-01-11 04:08:57 -05:00
|
|
|
"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"
|
2022-03-22 11:03:15 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-03-24 13:00:17 -04:00
|
|
|
type kmsClientAPI interface {
|
2022-07-27 16:02:33 -04:00
|
|
|
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)
|
2022-03-24 13:00:17 -04:00
|
|
|
}
|
|
|
|
|
2022-03-22 11:03:15 -04:00
|
|
|
// KMSClient implements the CloudKMS interface for Azure Key Vault.
|
|
|
|
type KMSClient struct {
|
2022-03-24 13:00:17 -04:00
|
|
|
client kmsClientAPI
|
2022-03-22 11:03:15 -04:00
|
|
|
storage kms.Storage
|
|
|
|
}
|
|
|
|
|
|
|
|
// Opts are optional settings for AKV clients.
|
|
|
|
type Opts struct {
|
2022-10-18 06:08:59 -04:00
|
|
|
Credentials *azidentity.DefaultAzureCredentialOptions
|
|
|
|
Keys *azkeys.ClientOptions
|
|
|
|
Secrets *azsecrets.ClientOptions
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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{}
|
|
|
|
}
|
2022-10-18 06:08:59 -04:00
|
|
|
cred, err := azidentity.NewDefaultAzureCredential(opts.Credentials)
|
2022-03-22 11:03:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("loading credentials: %w", err)
|
|
|
|
}
|
2022-11-09 06:09:22 -05:00
|
|
|
client, err := azsecrets.NewClient(vaultPrefix+vaultName+string(vaultType), cred, opts.Secrets)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("creating azure secrets client: %w", err)
|
|
|
|
}
|
2022-03-22 11:03:15 -04:00
|
|
|
|
|
|
|
// `azsecrets.NewClient()` does not error if the vault is non existent
|
|
|
|
// Test here if we can reach the vault, and error otherwise
|
2022-07-27 16:02:33 -04:00
|
|
|
pager := client.NewListSecretsPager(nil)
|
|
|
|
if _, err := pager.NextPage(ctx); err != nil {
|
|
|
|
return nil, fmt.Errorf("AKV not reachable: %w", err)
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if store == nil {
|
|
|
|
store = storage.NewMemMapStorage()
|
|
|
|
}
|
2022-10-18 06:08:59 -04:00
|
|
|
return &KMSClient{client: client, storage: store}, nil
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2022-06-09 10:04:30 -04:00
|
|
|
return fmt.Errorf("key generation: %w", err)
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Saving symmetric keys in Azure Key Vault requires encoding them to base64
|
2022-07-27 16:02:33 -04:00
|
|
|
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{})
|
2022-03-22 11:03:15 -04:00
|
|
|
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 {
|
2022-06-09 10:04:30 -04:00
|
|
|
return nil, fmt.Errorf("key generation: %w", err)
|
2022-03-22 11:03:15 -04:00
|
|
|
}
|
|
|
|
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) {
|
2022-07-27 16:02:33 -04:00
|
|
|
res, err := c.client.GetSecret(ctx, kekID, "", nil)
|
2022-03-22 11:03:15 -04:00
|
|
|
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)
|
|
|
|
}
|