Crypto component for Azure Key Vault (#2692)
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
parent
f2d46b4489
commit
f6d9ac7e99
|
|
@ -161,6 +161,15 @@ const components = {
|
|||
'configuration.redis': {
|
||||
certification: true,
|
||||
},
|
||||
'crypto.azure.keyvault': {
|
||||
conformance: true,
|
||||
requiredSecrets: [
|
||||
'AzureKeyVaultName',
|
||||
'AzureKeyVaultTenantId',
|
||||
'AzureKeyVaultServicePrincipalClientId',
|
||||
'AzureKeyVaultServicePrincipalClientSecret',
|
||||
],
|
||||
},
|
||||
'crypto.localstorage': {
|
||||
conformance: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
Loading…
Reference in New Issue