parent
896fd5efae
commit
392a18465f
5 changed files with 489 additions and 1 deletions
7
go.mod
7
go.mod
|
@ -1,9 +1,14 @@
|
|||
module github.com/smallstep/certificates
|
||||
|
||||
go 1.14
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.83.0
|
||||
github.com/Azure/azure-sdk-for-go v58.0.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.8
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2
|
||||
github.com/ThalesIgnite/crypto11 v1.2.4
|
||||
github.com/aws/aws-sdk-go v1.30.29
|
||||
|
|
33
go.sum
33
go.sum
|
@ -40,6 +40,31 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
|||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/Azure/azure-sdk-for-go v58.0.0+incompatible h1:Cw16jiP4dI+CK761aq44ol4RV5dUiIIXky1+EKpoiVM=
|
||||
github.com/Azure/azure-sdk-for-go v58.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.17 h1:2zCdHwNgRH+St1J+ZMf66xI8aLr/5KMy+wWLH97zwYM=
|
||||
github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.11 h1:L4/pmq7poLdsy41Bj1FayKvBhayuWRYkx9HU5i4Ybl0=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 h1:TzPg6B6fTZ0G1zBf3T54aI7p3cAT6u//TOXGPmFMOXg=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
|
||||
github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE=
|
||||
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
|
@ -146,6 +171,9 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
|||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
|
@ -163,6 +191,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
|
|||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
|
@ -375,6 +405,7 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW
|
|||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
|
@ -559,6 +590,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA=
|
||||
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
242
kms/azurekms/key_vault.go
Normal file
242
kms/azurekms/key_vault.go
Normal file
|
@ -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
|
||||
}
|
151
kms/azurekms/signer.go
Normal file
151
kms/azurekms/signer.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
57
kms/azurekms/utils.go
Normal file
57
kms/azurekms/utils.go
Normal file
|
@ -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 a new issue