Crypto component for Azure Key Vault (#2692)

Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
Alessandro (Ale) Segala 2023-03-24 19:05:37 -07:00 committed by GitHub
parent f2d46b4489
commit f6d9ac7e99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 853 additions and 0 deletions

View File

@ -161,6 +161,15 @@ const components = {
'configuration.redis': {
certification: true,
},
'crypto.azure.keyvault': {
conformance: true,
requiredSecrets: [
'AzureKeyVaultName',
'AzureKeyVaultTenantId',
'AzureKeyVaultServicePrincipalClientId',
'AzureKeyVaultServicePrincipalClientSecret',
],
},
'crypto.localstorage': {
conformance: true,
},

View File

@ -0,0 +1,70 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package keyvault
import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
internals "github.com/dapr/kit/crypto"
)
var (
validEncryptionAlgs map[string]struct{}
validSignatureAlgs map[string]struct{}
)
// GetJWKEncryptionAlgorithm returns a JSONWebKeyEncryptionAlgorithm constant is the algorithm is a supported one.
func GetJWKEncryptionAlgorithm(algorithm string) *azkeys.JSONWebKeyEncryptionAlgorithm {
// Special case for AES-CBC, since we treat A[NNN]CBC as having PKCS#7 padding, and A[NNN]CBC-NOPAD as not using padding
switch algorithm {
case internals.Algorithm_A128CBC, internals.Algorithm_A192CBC, internals.Algorithm_A256CBC:
// Append "PAD", e.g. "A128CBCPAD"
algorithm += "PAD"
case internals.Algorithm_A128CBC_NOPAD, internals.Algorithm_A192CBC_NOPAD, internals.Algorithm_A256CBC_NOPAD:
// Remove the "-NOPAD" suffix, e.g. "A128CBC"
algorithm = algorithm[:len(algorithm)-6]
}
if _, ok := validEncryptionAlgs[algorithm]; ok {
return to.Ptr(azkeys.JSONWebKeyEncryptionAlgorithm(algorithm))
} else {
return nil
}
}
// GetJWKSignatureAlgorithm returns a JSONWebKeySignatureAlgorithm constant is the algorithm is a supported one.
func GetJWKSignatureAlgorithm(algorithm string) *azkeys.JSONWebKeySignatureAlgorithm {
if _, ok := validSignatureAlgs[algorithm]; ok {
return to.Ptr(azkeys.JSONWebKeySignatureAlgorithm(algorithm))
} else {
return nil
}
}
type algorithms interface {
azkeys.JSONWebKeyEncryptionAlgorithm | azkeys.JSONWebKeySignatureAlgorithm
}
// IsAlgorithmAsymmetric returns true if the algorithm identifier is asymmetric.
func IsAlgorithmAsymmetric[T algorithms](algorithm T) bool {
algStr := string(algorithm)
switch algStr[0:2] {
case "RS", "ES", "PS":
// RSNULL is a reserved keyword
return algStr != "RSNULL"
default:
return false
}
}

View File

@ -0,0 +1,437 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package keyvault
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
contribCrypto "github.com/dapr/components-contrib/crypto"
contribMetadata "github.com/dapr/components-contrib/metadata"
internals "github.com/dapr/kit/crypto"
"github.com/dapr/kit/logger"
)
var (
errKeyNotFound = errors.New("key not found in the vault")
// Used to initialize validEncryptionAlgs and validSignatureAlgs lazily when the first component of this kind is initialized
algsParsed sync.Once
)
type keyvaultCrypto struct {
keyCache *contribCrypto.PubKeyCache
md keyvaultMetadata
vaultClient *azkeys.Client
logger logger.Logger
}
// NewAzureKeyvaultCrypto returns a new Azure Key Vault crypto provider.
func NewAzureKeyvaultCrypto(logger logger.Logger) contribCrypto.SubtleCrypto {
return &keyvaultCrypto{
logger: logger,
}
}
// Init creates a Azure Key Vault client.
func (k *keyvaultCrypto) Init(_ context.Context, metadata contribCrypto.Metadata) error {
// Convert from data from the Azure SDK, which returns a slice, into a map
// We perform the initialization here, lazily, when the first component of this kind is initialized
// (These functions do not make network calls)
algsParsed.Do(func() {
listEncryption := azkeys.PossibleJSONWebKeyEncryptionAlgorithmValues()
validEncryptionAlgs = make(map[string]struct{}, len(listEncryption))
for _, v := range listEncryption {
validEncryptionAlgs[string(v)] = struct{}{}
}
listSignature := azkeys.PossibleJSONWebKeySignatureAlgorithmValues()
validSignatureAlgs = make(map[string]struct{}, len(listSignature))
for _, v := range listSignature {
validSignatureAlgs[string(v)] = struct{}{}
}
})
// Init the metadata
err := k.md.InitWithMetadata(metadata)
if err != nil {
return fmt.Errorf("failed to load metadata: %w", err)
}
// Create a cache for keys
k.keyCache = contribCrypto.NewPubKeyCache(k.getKeyCacheFn)
// Init the Azure SDK client
k.vaultClient, err = azkeys.NewClient(k.getVaultURI(), k.md.cred, &azkeys.ClientOptions{
ClientOptions: azcore.ClientOptions{
Telemetry: policy.TelemetryOptions{
ApplicationID: "dapr-" + logger.DaprVersion,
},
},
})
if err != nil {
return err
}
return nil
}
// Features returns the features available in this crypto provider.
func (k *keyvaultCrypto) Features() []contribCrypto.Feature {
return []contribCrypto.Feature{} // No Feature supported.
}
// GetKey returns the public part of a key stored in the vault.
// This method returns an error if the key is symmetric.
// The key argument can be in the format "name" or "name/version".
func (k *keyvaultCrypto) GetKey(parentCtx context.Context, key string) (pubKey jwk.Key, err error) {
kid := newKeyID(key)
// If the key is cacheable, get it from the cache
if kid.Cacheable() {
return k.keyCache.GetKey(parentCtx, key)
}
return k.getKeyFromVault(parentCtx, kid)
}
func (k *keyvaultCrypto) getKeyFromVault(parentCtx context.Context, kid keyID) (pubKey jwk.Key, err error) {
ctx, cancel := context.WithTimeout(parentCtx, k.md.RequestTimeout)
res, err := k.vaultClient.GetKey(ctx, kid.Name, kid.Version, nil)
cancel()
if err != nil {
return nil, fmt.Errorf("failed to get key from Key Vault: %w", err)
}
return KeyBundleToKey(&res.KeyBundle)
}
// Handler for the getKeyCacheFn method
func (k *keyvaultCrypto) getKeyCacheFn(key string) func(resolve func(jwk.Key), reject func(error)) {
kid := newKeyID(key)
parentCtx := context.Background()
return func(resolve func(jwk.Key), reject func(error)) {
pk, err := k.getKeyFromVault(parentCtx, kid)
if err != nil {
reject(err)
return
}
resolve(pk)
}
}
// Encrypt a small message and returns the ciphertext.
// The key argument can be in the format "name" or "name/version".
func (k *keyvaultCrypto) Encrypt(parentCtx context.Context, plaintext []byte, algorithmStr string, key string, nonce []byte, associatedData []byte) (ciphertext []byte, tag []byte, err error) {
kid := newKeyID(key)
algorithm := GetJWKEncryptionAlgorithm(algorithmStr)
if algorithm == nil {
return nil, nil, fmt.Errorf("invalid algorithm: %s", algorithmStr)
}
// Encrypting with symmetric or non-cacheable keys must happen in the vault
if !kid.Cacheable() || !IsAlgorithmAsymmetric(*algorithm) {
return k.encryptInVault(parentCtx, plaintext, algorithm, kid, nonce, associatedData)
}
// Using a cacheable, asymmetric key, we can encrypt the data directly here
pk, err := k.keyCache.GetKey(parentCtx, key)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve public key: %w", err)
}
// If the key has expired, we cannot use that to encrypt data
if dpk, ok := pk.(*contribCrypto.Key); ok && !dpk.IsValid() {
return nil, nil, errors.New("the key is outside of its time validity bounds")
}
ciphertext, err = internals.EncryptPublicKey(plaintext, algorithmStr, pk, associatedData)
if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt data: %w", err)
}
return ciphertext, nil, nil
}
func (k *keyvaultCrypto) encryptInVault(parentCtx context.Context, plaintext []byte, algorithm *azkeys.JSONWebKeyEncryptionAlgorithm, kid keyID, nonce []byte, associatedData []byte) (ciphertext []byte, tag []byte, err error) {
ctx, cancel := context.WithTimeout(parentCtx, k.md.RequestTimeout)
res, err := k.vaultClient.Encrypt(ctx, kid.Name, kid.Version, azkeys.KeyOperationsParameters{
Algorithm: algorithm,
Value: plaintext,
IV: nonce,
AAD: associatedData,
}, nil)
cancel()
if err != nil {
return nil, nil, fmt.Errorf("error from Key Vault: %w", err)
}
if res.Result == nil {
return nil, nil, errors.New("response from Key Vault does not contain a valid ciphertext")
}
return res.Result, res.AuthenticationTag, nil
}
// Decrypt a small message and returns the plaintext.
// The key argument can be in the format "name" or "name/version".
func (k *keyvaultCrypto) Decrypt(parentCtx context.Context, ciphertext []byte, algorithmStr string, key string, nonce []byte, tag []byte, associatedData []byte) (plaintext []byte, err error) {
kid := newKeyID(key)
algorithm := GetJWKEncryptionAlgorithm(algorithmStr)
if algorithm == nil {
return nil, fmt.Errorf("invalid algorithm: %s", algorithmStr)
}
ctx, cancel := context.WithTimeout(parentCtx, k.md.RequestTimeout)
res, err := k.vaultClient.Decrypt(ctx, kid.Name, kid.Version, azkeys.KeyOperationsParameters{
Algorithm: algorithm,
Value: ciphertext,
IV: nonce,
Tag: tag,
AAD: associatedData,
}, nil)
cancel()
if err != nil {
return nil, fmt.Errorf("error from Key Vault: %w", err)
}
if res.Result == nil {
return nil, errors.New("response from Key Vault does not contain a valid plaintext")
}
return res.Result, nil
}
// WrapKey wraps a symmetric key.
// The key argument can be in the format "name" or "name/version".
func (k *keyvaultCrypto) WrapKey(parentCtx context.Context, plaintextKey jwk.Key, algorithmStr string, key string, nonce []byte, associatedData []byte) (wrappedKey []byte, tag []byte, err error) {
// Azure Key Vault does not support wrapping asymmetric keys
if plaintextKey.KeyType() != jwa.OctetSeq {
return nil, nil, errors.New("cannot wrap asymmetric keys")
}
plaintext, err := internals.SerializeKey(plaintextKey)
if err != nil {
return nil, nil, fmt.Errorf("cannot serialize key: %w", err)
}
kid := newKeyID(key)
algorithm := GetJWKEncryptionAlgorithm(algorithmStr)
if algorithm == nil {
return nil, nil, fmt.Errorf("invalid algorithm: %s", algorithmStr)
}
// Encrypting with symmetric or non-cacheable keys must happen in the vault
if !kid.Cacheable() || !IsAlgorithmAsymmetric(*algorithm) {
return k.wrapKeyInVault(parentCtx, plaintext, algorithm, kid, nonce, associatedData)
}
// Using a cacheable, asymmetric key, we can encrypt the data directly here
pk, err := k.keyCache.GetKey(parentCtx, key)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve public key: %w", err)
}
// If the key has expired, we cannot use that to encrypt data
if dpk, ok := pk.(*contribCrypto.Key); ok && !dpk.IsValid() {
return nil, nil, errors.New("the key is outside of its time validity bounds")
}
wrappedKey, err = internals.EncryptPublicKey(plaintext, algorithmStr, pk, associatedData)
if err != nil {
return nil, nil, fmt.Errorf("failed to wrap key: %w", err)
}
return wrappedKey, nil, nil
}
func (k *keyvaultCrypto) wrapKeyInVault(parentCtx context.Context, plaintextKey []byte, algorithm *azkeys.JSONWebKeyEncryptionAlgorithm, kid keyID, nonce []byte, associatedData []byte) (wrappedKey []byte, tag []byte, err error) {
ctx, cancel := context.WithTimeout(parentCtx, k.md.RequestTimeout)
res, err := k.vaultClient.WrapKey(ctx, kid.Name, kid.Version, azkeys.KeyOperationsParameters{
Algorithm: algorithm,
Value: plaintextKey,
IV: nonce,
AAD: associatedData,
}, nil)
cancel()
if err != nil {
return nil, nil, fmt.Errorf("error from Key Vault: %w", err)
}
if res.Result == nil {
return nil, nil, errors.New("response from Key Vault does not contain a valid wrapped key")
}
return res.Result, res.AuthenticationTag, nil
}
// UnwrapKey unwraps a key.
// The key argument can be in the format "name" or "name/version".
func (k *keyvaultCrypto) UnwrapKey(parentCtx context.Context, wrappedKey []byte, algorithmStr string, key string, nonce []byte, tag []byte, associatedData []byte) (plaintextKey jwk.Key, err error) {
kid := newKeyID(key)
algorithm := GetJWKEncryptionAlgorithm(algorithmStr)
if algorithm == nil {
return nil, fmt.Errorf("invalid algorithm: %s", algorithmStr)
}
ctx, cancel := context.WithTimeout(parentCtx, k.md.RequestTimeout)
res, err := k.vaultClient.UnwrapKey(ctx, kid.Name, kid.Version, azkeys.KeyOperationsParameters{
Algorithm: algorithm,
Value: wrappedKey,
IV: nonce,
Tag: tag,
AAD: associatedData,
}, nil)
cancel()
if err != nil {
return nil, fmt.Errorf("error from Key Vault: %w", err)
}
if res.Result == nil {
return nil, errors.New("response from Key Vault does not contain a valid unwrapped key")
}
// Key Vault allows wrapping/unwrapping only symmetric keys, so no need to try and decode an ASN.1 DER-encoded sequence
plaintextKey, err = jwk.FromRaw(res.Result)
if err != nil {
return nil, fmt.Errorf("failed to create JWK from raw key: %w", err)
}
return plaintextKey, nil
}
// Sign a digest.
// The key argument can be in the format "name" or "name/version".
func (k *keyvaultCrypto) Sign(parentCtx context.Context, digest []byte, algorithmStr string, key string) (signature []byte, err error) {
kid := newKeyID(key)
algorithm := GetJWKSignatureAlgorithm(algorithmStr)
if algorithm == nil {
return nil, fmt.Errorf("invalid algorithm: %s", algorithmStr)
}
ctx, cancel := context.WithTimeout(parentCtx, k.md.RequestTimeout)
res, err := k.vaultClient.Sign(ctx, kid.Name, kid.Version, azkeys.SignParameters{
Algorithm: algorithm,
Value: digest,
}, nil)
cancel()
if err != nil {
return nil, fmt.Errorf("error from Key Vault: %w", err)
}
if res.Result == nil {
return nil, errors.New("response from Key Vault does not contain a valid signature")
}
return res.Result, nil
}
// Verify a signature.
// The key argument can be in the format "name" or "name/version".
func (k *keyvaultCrypto) Verify(parentCtx context.Context, digest []byte, signature []byte, algorithmStr string, key string) (valid bool, err error) {
kid := newKeyID(key)
algorithm := GetJWKSignatureAlgorithm(algorithmStr)
if algorithm == nil {
return false, fmt.Errorf("invalid algorithm: %s", algorithmStr)
}
// Verifying with non-cacheable keys must happen in the vault
if !kid.Cacheable() {
return k.verifyInVault(parentCtx, digest, signature, algorithm, kid)
}
// Using a cacheable, asymmetric key, we can verify the data directly here
pk, err := k.keyCache.GetKey(parentCtx, key)
if err != nil {
return false, fmt.Errorf("failed to retrieve public key: %w", err)
}
valid, err = internals.VerifyPublicKey(digest, signature, algorithmStr, pk)
if err != nil {
return false, fmt.Errorf("failed to verify signature: %w", err)
}
return valid, nil
}
func (k *keyvaultCrypto) verifyInVault(parentCtx context.Context, digest []byte, signature []byte, algorithm *azkeys.JSONWebKeySignatureAlgorithm, kid keyID) (valid bool, err error) {
ctx, cancel := context.WithTimeout(parentCtx, k.md.RequestTimeout)
res, err := k.vaultClient.Verify(ctx, kid.Name, kid.Version, azkeys.VerifyParameters{
Algorithm: algorithm,
Digest: digest,
Signature: signature,
}, nil)
cancel()
if err != nil {
return false, fmt.Errorf("error from Key Vault: %w", err)
}
if res.Value == nil {
return false, errors.New("response from Key Vault does not contain a valid response")
}
return *res.Value, nil
}
// getVaultURI returns Azure Key Vault URI.
func (k *keyvaultCrypto) getVaultURI() string {
return fmt.Sprintf("https://%s.%s", k.md.VaultName, k.md.vaultDNSSuffix)
}
func (keyvaultCrypto) GetComponentMetadata() map[string]string {
metadataStruct := keyvaultMetadata{}
metadataInfo := map[string]string{}
contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo)
return metadataInfo
}
type keyID struct {
Version string
Name string
}
func newKeyID(val string) keyID {
obj := keyID{}
idx := strings.IndexRune(val, '/')
// Can't be on position 0, because the key name must be at least 1 character
if idx > 0 {
obj.Version = val[idx+1:]
obj.Name = val[:idx]
} else {
obj.Name = val
}
return obj
}
// Cacheable returns true if the key can be cached locally.
func (id keyID) Cacheable() bool {
switch strings.ToLower(id.Version) {
case "", "latest":
return false
default:
return true
}
}

View File

@ -0,0 +1,142 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package keyvault
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"errors"
"fmt"
"math/big"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
"github.com/lestrrat-go/jwx/v2/jwk"
contribCrypto "github.com/dapr/components-contrib/crypto"
)
// KeyBundleToKey converts an azkeys.KeyBundle object to a contribCrypto.Key object, containing only the public part of the asymmetric key.
func KeyBundleToKey(bundle *azkeys.KeyBundle) (*contribCrypto.Key, error) {
if bundle == nil ||
bundle.Key == nil || bundle.Key.KID == nil ||
bundle.Attributes == nil || bundle.Attributes.Enabled == nil || *bundle.Attributes.Enabled == false {
return nil, errKeyNotFound
}
// Get the key ID
kid := bundle.Key.KID.Name()
if ver := bundle.Key.KID.Version(); ver != "" {
kid += "/" + ver
}
// Extract the public key and create a jwk.Key from that
pk, err := JSONWebKey{*bundle.Key}.Public()
if err != nil {
return nil, fmt.Errorf("failed to extract public key as crypto.PublicKey: %w", err)
}
jwkObj, err := jwk.FromRaw(pk)
if err != nil {
return nil, fmt.Errorf("failed to create jwk.Key: %w", err)
}
// Convert to daprcrypto.Key
return contribCrypto.NewKey(jwkObj, kid, bundle.Attributes.Expires, bundle.Attributes.NotBefore), nil
}
// JSONWebKey extends azkeys.JSONWebKey to add methods to export the key.
type JSONWebKey struct {
azkeys.JSONWebKey
}
// Public returns the public key included the object, as a crypto.PublicKey object.
// This method returns an error if it's invoked on a JSONWebKey object representing a symmetric key.
func (key JSONWebKey) Public() (crypto.PublicKey, error) {
if key.Kty == nil {
return nil, errors.New("property Kty is nil")
}
switch {
case IsRSAKey(*key.Kty):
return key.publicRSA()
case IsECKey(*key.Kty):
return key.publicEC()
}
return nil, errors.New("unsupported key type")
}
func (key JSONWebKey) publicRSA() (*rsa.PublicKey, error) {
res := &rsa.PublicKey{}
// N = modulus
if len(key.N) == 0 {
return nil, errors.New("property N is empty")
}
res.N = &big.Int{}
res.N.SetBytes(key.N)
// e = public exponent
if len(key.E) == 0 {
return nil, errors.New("property e is empty")
}
res.E = int(big.NewInt(0).SetBytes(key.E).Uint64())
return res, nil
}
func (key JSONWebKey) publicEC() (*ecdsa.PublicKey, error) {
res := &ecdsa.PublicKey{}
if key.Crv == nil {
return nil, errors.New("property Crv is nil")
}
switch *key.Crv {
case azkeys.JSONWebKeyCurveNameP256:
res.Curve = elliptic.P256()
case azkeys.JSONWebKeyCurveNameP384:
res.Curve = elliptic.P384()
case azkeys.JSONWebKeyCurveNameP521:
res.Curve = elliptic.P521()
case azkeys.JSONWebKeyCurveNameP256K:
return nil, errors.New("curves of type P-256K are not supported by this method")
}
// X coordinate
if len(key.X) == 0 {
return nil, errors.New("property X is empty")
}
res.X = &big.Int{}
res.X.SetBytes(key.X)
// Y coordinate
if len(key.Y) == 0 {
return nil, errors.New("property Y is empty")
}
res.Y = &big.Int{}
res.Y.SetBytes(key.Y)
return res, nil
}
// IsRSAKey returns true if the key is an RSA key (RSA or RSA-HSM).
func IsRSAKey(kt azkeys.JSONWebKeyType) bool {
return kt == azkeys.JSONWebKeyTypeRSA || kt == azkeys.JSONWebKeyTypeRSAHSM
}
// IsECKey returns true if the key is an EC key (EC or EC-HSM).
func IsECKey(kt azkeys.JSONWebKeyType) bool {
return kt == azkeys.JSONWebKeyTypeEC || kt == azkeys.JSONWebKeyTypeECHSM
}

View File

@ -0,0 +1,84 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package keyvault
import (
"errors"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
contribCrypto "github.com/dapr/components-contrib/crypto"
azauth "github.com/dapr/components-contrib/internal/authentication/azure"
"github.com/dapr/components-contrib/metadata"
)
const defaultRequestTimeout = 30 * time.Second
type keyvaultMetadata struct {
// Name of the Azure Key Vault resource (required).
VaultName string `json:"vaultName" mapstructure:"vaultName"`
// Timeout for network requests, as a Go duration string (e.g. "30s")
// Defaults to "30s".
RequestTimeout time.Duration `json:"requestTimeout" mapstructure:"requestTimeout"`
// Internal properties
vaultDNSSuffix string
cred azcore.TokenCredential
}
func (m *keyvaultMetadata) InitWithMetadata(meta contribCrypto.Metadata) error {
m.reset()
// Decode the metadata
err := metadata.DecodeMetadata(meta.Properties, &m)
if err != nil {
return err
}
// Vault name
if m.VaultName == "" {
return errors.New("metadata property 'vaultName' is required")
}
// Set default requestTimeout if empty
if m.RequestTimeout < time.Second {
m.RequestTimeout = defaultRequestTimeout
}
// Get the DNS suffix
settings, err := azauth.NewEnvironmentSettings(meta.Properties)
if err != nil {
return err
}
m.vaultDNSSuffix = settings.EndpointSuffix(azauth.ServiceAzureKeyVault)
// Get the credentials object
m.cred, err = settings.GetTokenCredential()
if err != nil {
return err
}
return nil
}
// Reset the object
func (m *keyvaultMetadata) reset() {
m.VaultName = ""
m.RequestTimeout = defaultRequestTimeout
m.vaultDNSSuffix = ""
m.cred = nil
}

71
crypto/pubkey_cache.go Normal file
View File

@ -0,0 +1,71 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package crypto
import (
"context"
"sync"
"github.com/chebyrash/promise"
"github.com/lestrrat-go/jwx/v2/jwk"
)
// GetKeyFn is the type of the getKeyFn function used by the PubKeyCache.
type GetKeyFn = func(key string) func(resolve func(jwk.Key), reject func(error))
// PubKeyCache implements GetKey with a local cache.
type PubKeyCache struct {
getKeyFn GetKeyFn
pubKeys map[string]*promise.Promise[jwk.Key]
pubKeysLock *sync.Mutex
}
// NewPubKeyCache returns a new PubKeyCache object
func NewPubKeyCache(getKeyFn GetKeyFn) *PubKeyCache {
return &PubKeyCache{
getKeyFn: getKeyFn,
pubKeys: map[string]*promise.Promise[jwk.Key]{},
pubKeysLock: &sync.Mutex{},
}
}
// GetKey returns a public key from the cache, or uses getKeyFn to request it
func (kc *PubKeyCache) GetKey(parentCtx context.Context, key string) (pubKey jwk.Key, err error) {
timeoutPromise := promise.New(func(_ func(jwk.Key), reject func(error)) {
<-parentCtx.Done()
reject(parentCtx.Err())
})
// Check if the key is in the cache already
kc.pubKeysLock.Lock()
p, ok := kc.pubKeys[key]
if ok {
kc.pubKeysLock.Unlock()
return promise.Race(p, timeoutPromise).Await()
}
// Create a new promise, which resolves with a background context
p = promise.New(kc.getKeyFn(key))
p = promise.Catch(p, func(err error) error {
kc.pubKeysLock.Lock()
delete(kc.pubKeys, key)
kc.pubKeysLock.Unlock()
return err
})
kc.pubKeys[key] = p
kc.pubKeysLock.Unlock()
return promise.Race(p, timeoutPromise).Await()
}

2
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v0.5.0
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.3
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.11.0
github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v0.5.0
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.2.1
@ -42,6 +43,7 @@ require (
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746
github.com/camunda/zeebe/clients/go/v8 v8.1.8
github.com/cenkalti/backoff/v4 v4.2.0
github.com/chebyrash/promise v0.0.0-20220530143319-1123826567d6
github.com/cinience/go_rocketmq v0.0.2
github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.13.0
github.com/cloudevents/sdk-go/v2 v2.13.0

4
go.sum
View File

@ -431,6 +431,8 @@ github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1/go.mod h1:l3wvZkG9oW0
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 h1:TOFrNxfjslms5nLLIMjW7N0+zSALX4KiGsptmpb16AA=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0/go.mod h1:EAyXOW1F6BTJPiK2pDvmnvxOHPxoTYWoqBeIlql+QhI=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.11.0 h1:82w8tzLcOwDP/Q35j/wEBPt0n0kVC3cjtPdD62G8UAk=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.11.0/go.mod h1:S78i9yTr4o/nXlH76bKjGUye9Z2wSxO5Tz7GoDr4vfI=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 h1:Lg6BW0VPmCwcMlvOviL3ruHFO+H9tZNqscK0AeuFjGM=
@ -663,6 +665,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chebyrash/promise v0.0.0-20220530143319-1123826567d6 h1:AtcTeZIfucJjiqhIeMoOAR292ti2QOyo2aqN3SoWopo=
github.com/chebyrash/promise v0.0.0-20220530143319-1123826567d6/go.mod h1:4DRxP3p0R7/5msq1uKcI1THYmfWgFXxQqr0DutaIAEk=
github.com/chenzhuoyu/iasm v0.0.0-20220818063314-28c361dae733/go.mod h1:wOQ0nsbeOLa2awv8bUYFW/EHXbjQMlZ10fAlXDB2sz8=
github.com/chenzhuoyu/iasm v0.0.0-20230222070914-0b1b64b0e762 h1:4+00EOUb1t9uxAbgY8VvgfKJKDpim3co4MqsAbelIbs=
github.com/chenzhuoyu/iasm v0.0.0-20230222070914-0b1b64b0e762/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=

View File

@ -0,0 +1,15 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azurekeyvault
spec:
type: crypto.azure.keyvault
metadata:
- name: vaultName
value: ${{AzureKeyVaultName}}
- name: azureTenantId
value: ${{AzureKeyVaultTenantId}}
- name: azureClientId
value: ${{AzureKeyVaultServicePrincipalClientId}}
- name: azureClientSecret
value: ${{AzureKeyVaultServicePrincipalClientSecret}}

View File

@ -53,3 +53,19 @@ components:
- algorithms: ["A256CBC", "A256GCM", "A256KW", "C20P", "XC20P", "C20PKW", "XC20PKW", "A128CBC-HS256"]
type: symmetric
name: symmetric-256
- component: azure.keyvault
# Althoguh Azure Key Vault supports symmetric keys, those are only available in "Managed HSMs", which are too impractical for our tests
allOperations: false
operations: []
config:
keys:
- algorithms: ["ES256"]
type: private
name: ec256key
- algorithms: ["ES512"]
type: private
# "521" is not a typo
name: ec521key
- algorithms: ["PS256" , "PS384" , "PS512" , "RS256" , "RS384" , "RS512" , "RSA1_5" , "RSA-OAEP" , "RSA-OAEP-256"]
type: private
name: rsakey

View File

@ -59,6 +59,7 @@ import (
b_rabbitmq "github.com/dapr/components-contrib/bindings/rabbitmq"
b_redis "github.com/dapr/components-contrib/bindings/redis"
c_redis "github.com/dapr/components-contrib/configuration/redis"
cr_azurekeyvault "github.com/dapr/components-contrib/crypto/azure/keyvault"
cr_jwks "github.com/dapr/components-contrib/crypto/jwks"
cr_localstorage "github.com/dapr/components-contrib/crypto/localstorage"
p_snssqs "github.com/dapr/components-contrib/pubsub/aws/snssqs"
@ -538,6 +539,8 @@ func loadSecretStore(tc TestComponent) secretstores.SecretStore {
func loadCryptoProvider(tc TestComponent) contribCrypto.SubtleCrypto {
var component contribCrypto.SubtleCrypto
switch tc.Component {
case "azure.keyvault":
component = cr_azurekeyvault.NewAzureKeyvaultCrypto(testLogger)
case "localstorage":
component = cr_localstorage.NewLocalStorageCrypto(testLogger)
case "jwks":