mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
90b88e1cf9
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.
161 lines
5.4 KiB
Go
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)
|
|
}
|