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