diff --git a/go.mod b/go.mod index bb0763e88..5a1dcb880 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,11 @@ go 1.16 require ( cloud.google.com/go v0.87.0 + github.com/Azure/azure-sdk-for-go v55.0.0+incompatible + github.com/Azure/go-autorest/autorest v0.11.19 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.0 + github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/ReneKroon/ttlcache/v2 v2.7.0 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/aws/aws-sdk-go v1.39.6 diff --git a/go.sum b/go.sum index 56e398fdc..0670168e7 100644 --- a/go.sum +++ b/go.sum @@ -41,7 +41,36 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v55.0.0+incompatible h1:L4/vUGbg1Xkw5L20LZD+hJI5I+ibWSytqQ68lTCfLwY= +github.com/Azure/azure-sdk-for-go v55.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +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/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.19 h1:7/IqD2fEYVha1EPeaiytVKhzmPV223pfkRIQUGOK2IE= +github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +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/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +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/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -126,6 +155,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +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/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -152,6 +184,8 @@ github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI= +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/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -708,6 +742,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/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-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/pkg/signature/kms/azure/README.md b/pkg/signature/kms/azure/README.md new file mode 100644 index 000000000..dbe5bb62e --- /dev/null +++ b/pkg/signature/kms/azure/README.md @@ -0,0 +1,14 @@ +# Azure KMS + +In order to use Azure KMS with sigstore project you should setup the azure first, the key creation +will be handled in sigstore, however the vault and any needed permission will not and those things need to be configured. + +### What I need? + +- Create a Resource Group +- In this Resource Group create the Azure KMS +- Configure any custom permission + +After that you can use the created vault to generate the key, sign and verify. + +For more information check the official Azure Docs: https://azure.microsoft.com/en-us/services/key-vault/ diff --git a/pkg/signature/kms/azure/azure_test.go b/pkg/signature/kms/azure/azure_test.go new file mode 100644 index 000000000..5a2618bcc --- /dev/null +++ b/pkg/signature/kms/azure/azure_test.go @@ -0,0 +1,66 @@ +// +// Copyright 2021 The Sigstore 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 azure + +import "testing" + +func TestParseReference(t *testing.T) { + tests := []struct { + in string + wantVaultURL string + wantVaultName string + wantKeyName string + wantErr bool + }{ + { + in: "azurekms://honk-vault.vault.azure.net/honk-key", + wantVaultURL: "https://honk-vault.vault.azure.net/", + wantVaultName: "honk-vault", + wantKeyName: "honk-key", + wantErr: false, + }, + { + in: "foo://bar", + wantErr: true, + }, + { + in: "", + wantErr: true, + }, + { + in: "azurekms://wrong-test/test/1/3", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + gotVaultURL, gotVaultName, gotKeyName, err := parseReference(tt.in) + if (err != nil) != tt.wantErr { + t.Errorf("parseReference() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotVaultURL != tt.wantVaultURL { + t.Errorf("parseReference() gotVaultURL = %v, want %v", gotVaultURL, tt.wantVaultURL) + } + if gotVaultName != tt.wantVaultName { + t.Errorf("parseReference() gotVaultName = %v, want %v", gotVaultName, tt.wantVaultName) + } + if gotKeyName != tt.wantKeyName { + t.Errorf("parseReference() gotKeyName = %v, want %v", gotKeyName, tt.wantKeyName) + } + }) + } +} diff --git a/pkg/signature/kms/azure/client.go b/pkg/signature/kms/azure/client.go new file mode 100644 index 000000000..e8d1d5853 --- /dev/null +++ b/pkg/signature/kms/azure/client.go @@ -0,0 +1,262 @@ +// +// Copyright 2021 The Sigstore 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 azure + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "regexp" + "strings" + "time" + + "github.com/ReneKroon/ttlcache/v2" + "github.com/pkg/errors" + jose "gopkg.in/square/go-jose.v2" + + kvauth "github.com/Azure/azure-sdk-for-go/services/keyvault/auth" + "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/Azure/go-autorest/autorest/to" +) + +type azureVaultClient struct { + client *keyvault.BaseClient + keyCache *ttlcache.Cache + vaultURL string + vaultName string + keyName string +} + +var ( + errAzureReference = errors.New("kms specification should be in the format azurekms://[VAULT_NAME][VAULT_URL]/[KEY_NAME]") + + referenceRegex = regexp.MustCompile(`^azurekms://([^/]+)/([^/]+)?$`) +) + +const ( + ReferenceScheme = "azurekms://" + CacheKey = "azure_vault_signer" +) + +func ValidReference(ref string) error { + if !referenceRegex.MatchString(ref) { + return errAzureReference + } + return nil +} + +func parseReference(resourceID string) (vaultURL, vaultName, keyName string, err error) { + v := referenceRegex.FindStringSubmatch(resourceID) + if len(v) != 3 { + err = errors.Errorf("invalid azurekms format %q", resourceID) + return + } + + vaultURL = fmt.Sprintf("https://%s/", v[1]) + vaultName, keyName = strings.Split(v[1], ".")[0], v[2] + return +} + +func newAzureKMS(ctx context.Context, keyResourceID string) (*azureVaultClient, error) { + vaultURL, vaultName, keyName, err := parseReference(keyResourceID) + if err != nil { + return nil, err + } + + azureTenantID := os.Getenv("AZURE_TENANT_ID") + if azureTenantID == "" { + return nil, errors.New("AZURE_TENANT_ID is not set") + } + + azureClientID := os.Getenv("AZURE_CLIENT_ID") + if azureClientID == "" { + return nil, errors.New("AZURE_CLIENT_ID is not set") + } + + azureClientSecret := os.Getenv("AZURE_CLIENT_SECRET") + if azureClientSecret == "" { + return nil, errors.New("AZURE_CLIENT_SECRET is not set") + } + + client, err := getKeysClient() + if err != nil { + return nil, errors.Wrap(err, "new azure kms client") + } + + azClient := &azureVaultClient{ + client: &client, + vaultURL: vaultURL, + vaultName: vaultName, + keyName: keyName, + keyCache: ttlcache.NewCache(), + } + + azClient.keyCache.SetLoaderFunction(azClient.keyCacheLoaderFunction) + azClient.keyCache.SkipTTLExtensionOnHit(true) + + return azClient, nil +} + +func getKeysClient() (keyvault.BaseClient, error) { + keyClient := keyvault.New() + + authorizer, err := kvauth.NewAuthorizerFromEnvironment() + if err != nil { + return keyvault.BaseClient{}, err + } + + keyClient.Authorizer = authorizer + err = keyClient.AddToUserAgent("sigstore") + if err != nil { + return keyvault.BaseClient{}, err + } + + return keyClient, nil +} + +func (a *azureVaultClient) keyCacheLoaderFunction(key string) (data interface{}, ttl time.Duration, err error) { + ttl = time.Second * 300 + var pubKey crypto.PublicKey + + pubKey, err = a.fetchPublicKey(context.Background()) + if err != nil { + data = nil + return + } + + data = pubKey + return data, ttl, err +} + +func (a *azureVaultClient) fetchPublicKey(ctx context.Context) (crypto.PublicKey, error) { + key, err := a.getKey(ctx) + if err != nil { + return nil, errors.Wrap(err, "public key") + } + + jwkJSON, err := json.Marshal(*key.Key) + if err != nil { + return nil, errors.Wrap(err, "encoding the jsonWebKey") + } + + jwk := jose.JSONWebKey{} + err = jwk.UnmarshalJSON(jwkJSON) + if err != nil { + return nil, errors.Wrap(err, "decoding the jsonWebKey") + } + + pub, ok := jwk.Key.(*ecdsa.PublicKey) + if !ok { + if err != nil { + return nil, fmt.Errorf("public key was not ECDSA: %#v", pub) + } + } + + return pub, nil +} + +func (a *azureVaultClient) getKey(ctx context.Context) (keyvault.KeyBundle, error) { + key, err := a.client.GetKey(ctx, a.vaultURL, a.keyName, "") + if err != nil { + return keyvault.KeyBundle{}, errors.Wrap(err, "public key") + } + + return key, err +} + +func (a *azureVaultClient) public() (crypto.PublicKey, error) { + return a.keyCache.Get(CacheKey) +} + +func (a *azureVaultClient) createKey(ctx context.Context) (crypto.PublicKey, error) { + _, err := a.getKey(ctx) + if err == nil { + return a.public() + } + + _, err = a.client.CreateKey( + ctx, + a.vaultURL, + a.keyName, + keyvault.KeyCreateParameters{ + KeyAttributes: &keyvault.KeyAttributes{ + Enabled: to.BoolPtr(true), + }, + KeySize: to.Int32Ptr(2048), + KeyOps: &[]keyvault.JSONWebKeyOperation{ + keyvault.Sign, + keyvault.Verify, + }, + Kty: keyvault.EC, + Tags: map[string]*string{ + "use": to.StringPtr("sigstore"), + }, + }) + if err != nil { + return nil, err + } + + return a.public() +} + +func (a *azureVaultClient) sign(ctx context.Context, rawPayload []byte) ([]byte, error) { + hash := sha256.Sum256(rawPayload) + signed := hash[:] + + params := keyvault.KeySignParameters{ + Algorithm: keyvault.ES256, + Value: to.StringPtr(base64.RawURLEncoding.EncodeToString(signed)), + } + + result, err := a.client.Sign(ctx, a.vaultURL, a.keyName, "", params) + if err != nil { + return nil, errors.Wrap(err, "signing the payload") + } + + decResult, err := base64.RawURLEncoding.DecodeString(*result.Result) + if err != nil { + return nil, errors.Wrap(err, "decoding the result") + } + + return decResult, nil +} + +func (a *azureVaultClient) verify(ctx context.Context, signature, payload []byte) error { + hash := sha256.Sum256(payload) + signed := hash[:] + + params := keyvault.KeyVerifyParameters{ + Algorithm: keyvault.ES256, + Digest: to.StringPtr(base64.RawURLEncoding.EncodeToString(signed)), + Signature: to.StringPtr(base64.RawURLEncoding.EncodeToString(signature)), + } + + result, err := a.client.Verify(ctx, a.vaultURL, a.keyName, "", params) + if err != nil { + return errors.Wrap(err, "verify") + } + + if !*result.Value { + return errors.New("Failed vault verification") + } + + return nil +} diff --git a/pkg/signature/kms/azure/signer.go b/pkg/signature/kms/azure/signer.go new file mode 100644 index 000000000..6c4c42a26 --- /dev/null +++ b/pkg/signature/kms/azure/signer.go @@ -0,0 +1,199 @@ +// +// Copyright 2021 The Sigstore 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 azure + +import ( + "bytes" + "context" + "crypto" + "io" + + "github.com/pkg/errors" + + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" +) + +var azureSupportedHashFuncs = []crypto.Hash{ + crypto.SHA256, +} + +//nolint:golint +const ( + Algorithm_ES256 = "ES256" +) + +var azureSupportedAlgorithms []string = []string{ + Algorithm_ES256, +} + +type SignerVerifier struct { + defaultCtx context.Context + hashFunc crypto.Hash + client *azureVaultClient +} + +// LoadSignerVerifier generates signatures using the specified key object in GCP KMS and hash algorithm. +// +// It also can verify signatures locally using the public key. hashFunc must not be crypto.Hash(0). +func LoadSignerVerifier(defaultCtx context.Context, referenceStr string, hashFunc crypto.Hash) (*SignerVerifier, error) { + a := &SignerVerifier{ + defaultCtx: defaultCtx, + } + + var err error + a.client, err = newAzureKMS(defaultCtx, referenceStr) + if err != nil { + return nil, err + } + + switch hashFunc { + case 0, crypto.SHA224, crypto.SHA256, crypto.SHA384, crypto.SHA512: + a.hashFunc = hashFunc + default: + return nil, errors.New("hash function not supported by Hashivault") + } + + return a, nil +} + +// THIS WILL BE REMOVED ONCE ALL SIGSTORE PROJECTS NO LONGER USE IT +func (a *SignerVerifier) Sign(ctx context.Context, payload []byte) ([]byte, []byte, error) { + sig, err := a.SignMessage(bytes.NewReader(payload), options.WithContext(ctx)) + return sig, nil, err +} + +// SignMessage signs the provided message using GCP KMS. If the message is provided, +// this method will compute the digest according to the hash function specified +// when the Signer was created. +// +// SignMessage recognizes the following Options listed in order of preference: +// +// - WithContext() +// +// - WithDigest() +// +// - WithCryptoSignerOpts() +// +// All other options are ignored if specified. +func (a *SignerVerifier) SignMessage(message io.Reader, opts ...signature.SignOption) ([]byte, error) { + ctx := context.Background() + var digest []byte + var signerOpts crypto.SignerOpts = a.hashFunc + + for _, opt := range opts { + opt.ApplyDigest(&digest) + opt.ApplyCryptoSignerOpts(&signerOpts) + } + + digest, _, err := signature.ComputeDigestForSigning(message, signerOpts.HashFunc(), azureSupportedHashFuncs, opts...) + if err != nil { + return nil, err + } + + return a.client.sign(ctx, digest) +} + +// VerifySignature verifies the signature for the given message. Unless provided +// in an option, the digest of the message will be computed using the hash function specified +// when the SignerVerifier was created. +// +// This function returns nil if the verification succeeded, and an error message otherwise. +// +// This function recognizes the following Options listed in order of preference: +// +// - WithDigest() +// +// All other options are ignored if specified. +func (a *SignerVerifier) VerifySignature(sig, message io.Reader, opts ...signature.VerifyOption) error { + ctx := context.Background() + var digest []byte + var signerOpts crypto.SignerOpts = a.hashFunc + for _, opt := range opts { + opt.ApplyDigest(&digest) + } + + digest, _, err := signature.ComputeDigestForVerifying(message, signerOpts.HashFunc(), azureSupportedHashFuncs, opts...) + if err != nil { + return err + } + + sigBytes, err := io.ReadAll(sig) + if err != nil { + return errors.Wrap(err, "reading signature") + } + + return a.client.verify(ctx, sigBytes, digest) +} + +// PublicKey returns the public key that can be used to verify signatures created by +// this signer. All options provided in arguments to this method are ignored. +func (a *SignerVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) { + return a.client.public() +} + +// CreateKey attempts to create a new key in Vault with the specified algorithm. +func (a *SignerVerifier) CreateKey(ctx context.Context, algorithm string) (crypto.PublicKey, error) { + return a.client.createKey(ctx) +} + +type cryptoSignerWrapper struct { + ctx context.Context + hashFunc crypto.Hash + sv *SignerVerifier + errFunc func(error) +} + +func (c cryptoSignerWrapper) Public() crypto.PublicKey { + pk, err := c.sv.PublicKey(options.WithContext(c.ctx)) + if err != nil && c.errFunc != nil { + c.errFunc(err) + } + return pk +} + +func (c cryptoSignerWrapper) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + hashFunc := c.hashFunc + if opts != nil { + hashFunc = opts.HashFunc() + } + gcpOptions := []signature.SignOption{ + options.WithContext(c.ctx), + options.WithDigest(digest), + options.WithCryptoSignerOpts(hashFunc), + } + + return c.sv.SignMessage(nil, gcpOptions...) +} + +func (a *SignerVerifier) CryptoSigner(ctx context.Context, errFunc func(error)) (crypto.Signer, crypto.SignerOpts, error) { + csw := &cryptoSignerWrapper{ + ctx: ctx, + sv: a, + hashFunc: a.hashFunc, + errFunc: errFunc, + } + + return csw, a.hashFunc, nil +} + +func (*SignerVerifier) SupportedAlgorithms() []string { + return azureSupportedAlgorithms +} + +func (*SignerVerifier) DefaultAlgorithm() string { + return Algorithm_ES256 +} diff --git a/pkg/signature/kms/kms.go b/pkg/signature/kms/kms.go index adf0dd193..b46c9ea01 100644 --- a/pkg/signature/kms/kms.go +++ b/pkg/signature/kms/kms.go @@ -23,6 +23,7 @@ import ( "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms/aws" + "github.com/sigstore/sigstore/pkg/signature/kms/azure" "github.com/sigstore/sigstore/pkg/signature/kms/gcp" "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) @@ -31,6 +32,9 @@ func init() { ProvidersMux().AddProvider(aws.ReferenceScheme, func(ctx context.Context, keyResourceID string, hashFunc crypto.Hash) (SignerVerifier, error) { return aws.LoadSignerVerifier(keyResourceID) }) + ProvidersMux().AddProvider(azure.ReferenceScheme, func(ctx context.Context, keyResourceID string, hashFunc crypto.Hash) (SignerVerifier, error) { + return azure.LoadSignerVerifier(ctx, keyResourceID, hashFunc) + }) ProvidersMux().AddProvider(gcp.ReferenceScheme, func(ctx context.Context, keyResourceID string, _ crypto.Hash) (SignerVerifier, error) { return gcp.LoadSignerVerifier(ctx, keyResourceID) }) diff --git a/test/e2e/docker-compose.yml b/test/e2e/docker-compose.yml index 3bc7b7b6d..db6e253a1 100644 --- a/test/e2e/docker-compose.yml +++ b/test/e2e/docker-compose.yml @@ -18,7 +18,7 @@ services: vault: image: vault:latest environment: - VAULT_DEV_ROOT_TOKEN_ID: testtoken + VAULT_DEV_ROOT_TOKEN_ID: ${VAULT_TOKEN} ports: - 8200:8200 privileged: true diff --git a/test/e2e/e2e-test.sh b/test/e2e/e2e-test.sh index fa12a765d..062543e58 100755 --- a/test/e2e/e2e-test.sh +++ b/test/e2e/e2e-test.sh @@ -16,7 +16,18 @@ set -ex +cleanup() { + echo "cleanup" + docker-compose down +} + +trap cleanup ERR + +export VAULT_TOKEN=testtoken +export VAULT_ADDR=http://localhost:8200/ + echo "starting services" +docker-compose config docker-compose up -d count=0 @@ -50,5 +61,4 @@ export AWS_TLS_INSECURE_SKIP_VERIFY=1 go test -tags e2e -count=1 ./... -echo "cleanup" -docker-compose down +cleanup diff --git a/test/e2e/kms/vault_test.go b/test/e2e/kms/vault_test.go index 51289278f..be6f08d00 100644 --- a/test/e2e/kms/vault_test.go +++ b/test/e2e/kms/vault_test.go @@ -81,7 +81,7 @@ func (suite *VaultSuite) TearDownSuite() { func (suite *VaultSuite) TestProviders() { providers := kms.ProvidersMux().Providers() - assert.Len(suite.T(), providers, 3) + assert.Len(suite.T(), providers, 4) } func (suite *VaultSuite) TestProvider() {