parent
896fd5efae
commit
392a18465f
@ -0,0 +1,242 @@
|
|||||||
|
package azurekms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||||
|
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||||
|
"github.com/Azure/go-autorest/autorest/date"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/certificates/kms/apiv1"
|
||||||
|
"github.com/smallstep/certificates/kms/uri"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
apiv1.Register(apiv1.CloudKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) {
|
||||||
|
return New(ctx, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scheme is the scheme used for Azure Key Vault uris.
|
||||||
|
const Scheme = "azurekms"
|
||||||
|
|
||||||
|
var (
|
||||||
|
valueTrue = true
|
||||||
|
value2048 int32 = 2048
|
||||||
|
value3072 int32 = 3072
|
||||||
|
value4096 int32 = 4096
|
||||||
|
)
|
||||||
|
|
||||||
|
var now = func() time.Time {
|
||||||
|
return time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyType struct {
|
||||||
|
Kty keyvault.JSONWebKeyType
|
||||||
|
Curve keyvault.JSONWebKeyCurveName
|
||||||
|
KeySize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k keyType) KeyType(pl apiv1.ProtectionLevel) keyvault.JSONWebKeyType {
|
||||||
|
switch k.Kty {
|
||||||
|
case keyvault.EC:
|
||||||
|
if pl == apiv1.HSM {
|
||||||
|
return keyvault.ECHSM
|
||||||
|
}
|
||||||
|
return k.Kty
|
||||||
|
case keyvault.RSA:
|
||||||
|
if pl == apiv1.HSM {
|
||||||
|
return keyvault.RSAHSM
|
||||||
|
}
|
||||||
|
return k.Kty
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]keyType{
|
||||||
|
apiv1.UnspecifiedSignAlgorithm: {
|
||||||
|
Kty: keyvault.EC,
|
||||||
|
Curve: keyvault.P256,
|
||||||
|
},
|
||||||
|
apiv1.SHA256WithRSA: {
|
||||||
|
Kty: keyvault.RSA,
|
||||||
|
},
|
||||||
|
apiv1.SHA384WithRSA: {
|
||||||
|
Kty: keyvault.RSA,
|
||||||
|
},
|
||||||
|
apiv1.SHA512WithRSA: {
|
||||||
|
Kty: keyvault.RSA,
|
||||||
|
},
|
||||||
|
apiv1.SHA256WithRSAPSS: {
|
||||||
|
Kty: keyvault.RSA,
|
||||||
|
},
|
||||||
|
apiv1.SHA384WithRSAPSS: {
|
||||||
|
Kty: keyvault.RSA,
|
||||||
|
},
|
||||||
|
apiv1.SHA512WithRSAPSS: {
|
||||||
|
Kty: keyvault.RSA,
|
||||||
|
},
|
||||||
|
apiv1.ECDSAWithSHA256: {
|
||||||
|
Kty: keyvault.EC,
|
||||||
|
Curve: keyvault.P256,
|
||||||
|
},
|
||||||
|
apiv1.ECDSAWithSHA384: {
|
||||||
|
Kty: keyvault.EC,
|
||||||
|
Curve: keyvault.P384,
|
||||||
|
},
|
||||||
|
apiv1.ECDSAWithSHA512: {
|
||||||
|
Kty: keyvault.EC,
|
||||||
|
Curve: keyvault.P521,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// vaultResource is that the client will use as audience.
|
||||||
|
const vaultResource = "https://vault.azure.net"
|
||||||
|
|
||||||
|
// KeyVaultClient is the interface implemented by keyvault.BaseClient. It it
|
||||||
|
// will be used for testing purposes.
|
||||||
|
type KeyVaultClient interface {
|
||||||
|
GetKey(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string) (keyvault.KeyBundle, error)
|
||||||
|
CreateKey(ctx context.Context, vaultBaseURL string, keyName string, parameters keyvault.KeyCreateParameters) (keyvault.KeyBundle, error)
|
||||||
|
Sign(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string, parameters keyvault.KeySignParameters) (keyvault.KeyOperationResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVault implements a KMS using Azure Key Vault.
|
||||||
|
//
|
||||||
|
// TODO(mariano): The implementation is using /services/keyvault/v7.1/keyvault
|
||||||
|
// package, at some point Azure might create a keyvault client with all the
|
||||||
|
// functionality in /sdk/keyvault, we should migrate to that once available.
|
||||||
|
type KeyVault struct {
|
||||||
|
baseClient KeyVaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// New initializes a new KMS implemented using Azure Key Vault.
|
||||||
|
func New(ctx context.Context, opts apiv1.Options) (*KeyVault, error) {
|
||||||
|
// Attempt to authorize with the following methods:
|
||||||
|
// 1. Environment variables.
|
||||||
|
// - Client credentials
|
||||||
|
// - Client certificate
|
||||||
|
// - Username and password
|
||||||
|
// - MSI
|
||||||
|
// 2. Using Azure CLI 2.0 on local development.
|
||||||
|
authorizer, err := auth.NewAuthorizerFromEnvironmentWithResource(vaultResource)
|
||||||
|
if err != nil {
|
||||||
|
authorizer, err = auth.NewAuthorizerFromCLIWithResource(vaultResource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error getting authorizer for key vault")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseClient := keyvault.New()
|
||||||
|
baseClient.Authorizer = authorizer
|
||||||
|
|
||||||
|
return &KeyVault{
|
||||||
|
baseClient: &baseClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPublicKey loads a public key from Azure Key Vault by its resource name.
|
||||||
|
func (k *KeyVault) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
|
||||||
|
switch {
|
||||||
|
case req.Name == "":
|
||||||
|
return nil, errors.New("getPublicKeyRequest 'name' cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
vault, name, version, err := parseKeyName(req.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := defaultContext()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := k.baseClient.GetKey(ctx, vaultBaseURL(vault), name, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "keyVault GetKey failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertKey(resp.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateKey creates a asymmetric key in Azure Key Vault.
|
||||||
|
func (k *KeyVault) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
|
||||||
|
vault, name, _, err := parseKeyName(req.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kt, ok := signatureAlgorithmMapping[req.SignatureAlgorithm]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("keyVault does not support signature algorithm '%s'", req.SignatureAlgorithm)
|
||||||
|
}
|
||||||
|
var keySize *int32
|
||||||
|
if kt.Kty == keyvault.RSA || kt.Kty == keyvault.RSAHSM {
|
||||||
|
switch req.Bits {
|
||||||
|
case 2048:
|
||||||
|
keySize = &value2048
|
||||||
|
case 0, 3072:
|
||||||
|
keySize = &value3072
|
||||||
|
case 4096:
|
||||||
|
keySize = &value4096
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("keyVault does not support key size %d", req.Bits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
created := date.UnixTime(now())
|
||||||
|
|
||||||
|
ctx, cancel := defaultContext()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := k.baseClient.CreateKey(ctx, vaultBaseURL(vault), name, keyvault.KeyCreateParameters{
|
||||||
|
Kty: kt.KeyType(req.ProtectionLevel),
|
||||||
|
KeySize: keySize,
|
||||||
|
Curve: kt.Curve,
|
||||||
|
KeyOps: &[]keyvault.JSONWebKeyOperation{
|
||||||
|
keyvault.Sign, keyvault.Verify,
|
||||||
|
},
|
||||||
|
KeyAttributes: &keyvault.KeyAttributes{
|
||||||
|
Enabled: &valueTrue,
|
||||||
|
Created: &created,
|
||||||
|
NotBefore: &created,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "keyVault CreateKey failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyURI := uri.New("azurekms", url.Values{
|
||||||
|
"vault": []string{vault},
|
||||||
|
"id": []string{name},
|
||||||
|
}).String()
|
||||||
|
|
||||||
|
publicKey, err := convertKey(resp.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiv1.CreateKeyResponse{
|
||||||
|
Name: keyURI,
|
||||||
|
PublicKey: publicKey,
|
||||||
|
CreateSignerRequest: apiv1.CreateSignerRequest{
|
||||||
|
SigningKey: keyURI,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSigner returns a crypto.Signer from a previously created asymmetric key.
|
||||||
|
func (k *KeyVault) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
|
||||||
|
if req.SigningKey == "" {
|
||||||
|
return nil, errors.New("createSignerRequest 'signingKey' cannot be empty")
|
||||||
|
}
|
||||||
|
return NewSigner(k.baseClient, req.SigningKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the client connection to the Azure Key Vault. This is a noop.
|
||||||
|
func (k *KeyVault) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
package azurekms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
"golang.org/x/crypto/cryptobyte/asn1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signer implements a crypto.Signer using the AWS KMS.
|
||||||
|
type Signer struct {
|
||||||
|
client KeyVaultClient
|
||||||
|
vaultBaseURL string
|
||||||
|
name string
|
||||||
|
version string
|
||||||
|
publicKey crypto.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSigner creates a new signer using a key in the AWS KMS.
|
||||||
|
func NewSigner(client KeyVaultClient, signingKey string) (*Signer, error) {
|
||||||
|
vault, name, version, err := parseKeyName(signingKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the key exists.
|
||||||
|
signer := &Signer{
|
||||||
|
client: client,
|
||||||
|
vaultBaseURL: vaultBaseURL(vault),
|
||||||
|
name: name,
|
||||||
|
version: version,
|
||||||
|
}
|
||||||
|
if err := signer.preloadKey(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signer) preloadKey() error {
|
||||||
|
ctx, cancel := defaultContext()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := s.client.GetKey(ctx, s.vaultBaseURL, s.name, s.version)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "keyVault GetKey failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.publicKey, err = convertKey(resp.Key)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public returns the public key of this signer or an error.
|
||||||
|
func (s *Signer) Public() crypto.PublicKey {
|
||||||
|
return s.publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs digest with the private key stored in the AWS KMS.
|
||||||
|
func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||||
|
alg, err := getSigningAlgorithm(s.Public(), opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := defaultContext()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
b64 := base64.RawURLEncoding.EncodeToString(digest)
|
||||||
|
|
||||||
|
resp, err := s.client.Sign(ctx, s.vaultBaseURL, s.name, s.version, keyvault.KeySignParameters{
|
||||||
|
Algorithm: alg,
|
||||||
|
Value: &b64,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "keyVault Sign failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := base64.RawURLEncoding.DecodeString(*resp.Result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error decoding keyVault Sign result")
|
||||||
|
}
|
||||||
|
|
||||||
|
var octetSize int
|
||||||
|
switch alg {
|
||||||
|
case keyvault.ES256:
|
||||||
|
octetSize = 32 // 256-bit, concat(R,S) = 64 bytes
|
||||||
|
case keyvault.ES384:
|
||||||
|
octetSize = 48 // 384-bit, concat(R,S) = 96 bytes
|
||||||
|
case keyvault.ES512:
|
||||||
|
octetSize = 66 // 528-bit, concat(R,S) = 132 bytes
|
||||||
|
default:
|
||||||
|
return sig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to ans1
|
||||||
|
if len(sig) != octetSize*2 {
|
||||||
|
return nil, errors.Errorf("keyVault Sign failed: unexpected signature length")
|
||||||
|
}
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddASN1BigInt(new(big.Int).SetBytes(sig[:octetSize])) // R
|
||||||
|
b.AddASN1BigInt(new(big.Int).SetBytes(sig[octetSize:])) // S
|
||||||
|
})
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (keyvault.JSONWebKeySignatureAlgorithm, error) {
|
||||||
|
switch key.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
_, isPSS := opts.(*rsa.PSSOptions)
|
||||||
|
switch h := opts.HashFunc(); h {
|
||||||
|
case crypto.SHA256:
|
||||||
|
if isPSS {
|
||||||
|
return keyvault.PS256, nil
|
||||||
|
}
|
||||||
|
return keyvault.RS256, nil
|
||||||
|
case crypto.SHA384:
|
||||||
|
if isPSS {
|
||||||
|
return keyvault.PS384, nil
|
||||||
|
}
|
||||||
|
return keyvault.RS384, nil
|
||||||
|
case crypto.SHA512:
|
||||||
|
if isPSS {
|
||||||
|
return keyvault.PS512, nil
|
||||||
|
}
|
||||||
|
return keyvault.RS512, nil
|
||||||
|
default:
|
||||||
|
return "", errors.Errorf("unsupported hash function %v", h)
|
||||||
|
}
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
switch h := opts.HashFunc(); h {
|
||||||
|
case crypto.SHA256:
|
||||||
|
return keyvault.ES256, nil
|
||||||
|
case crypto.SHA384:
|
||||||
|
return keyvault.ES384, nil
|
||||||
|
case crypto.SHA512:
|
||||||
|
return keyvault.ES512, nil
|
||||||
|
default:
|
||||||
|
return "", errors.Errorf("unsupported hash function %v", h)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", errors.Errorf("unsupported key type %T", key)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package azurekms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/certificates/kms/uri"
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultContext returns the default context used in requests to azure.
|
||||||
|
func defaultContext() (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseKeyName returns the key vault, name and version for urls like
|
||||||
|
// azurekms:vault=key-vault;id=key-name?version=key-version. If version is not
|
||||||
|
// passed the latest version will be used.
|
||||||
|
func parseKeyName(rawURI string) (vault, name, version string, err error) {
|
||||||
|
var u *uri.URI
|
||||||
|
|
||||||
|
u, err = uri.ParseWithScheme("azurekms", rawURI)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if vault = u.Get("vault"); vault == "" {
|
||||||
|
err = errors.Errorf("key uri %s is not valid: vault is missing", rawURI)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if name = u.Get("id"); name == "" {
|
||||||
|
err = errors.Errorf("key uri %s is not valid: id is missing", rawURI)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
version = u.Get("version")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func vaultBaseURL(vault string) string {
|
||||||
|
return "https://" + vault + ".vault.azure.net/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertKey(key *keyvault.JSONWebKey) (crypto.PublicKey, error) {
|
||||||
|
b, err := json.Marshal(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error marshalling key")
|
||||||
|
}
|
||||||
|
var jwk jose.JSONWebKey
|
||||||
|
if err := jwk.UnmarshalJSON(b); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error unmarshalling key")
|
||||||
|
}
|
||||||
|
return jwk.Key, nil
|
||||||
|
}
|
Loading…
Reference in New Issue