From 025676fc13f807df23b946d30b89ed06dcbe931c Mon Sep 17 00:00:00 2001 From: Mikhail Swift Date: Tue, 28 May 2024 18:32:53 -0400 Subject: [PATCH] feat: add ability to sign with vault transit secret engine --- go.mod | 9 ++ go.sum | 28 ++++ imports.go | 3 + registry/option.go | 2 +- signer/kms/hashivault/client.go | 234 +++++++++++++++++++++++++++++++ signer/kms/hashivault/options.go | 108 ++++++++++++++ signer/kms/hashivault/signer.go | 105 ++++++++++++++ 7 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 signer/kms/hashivault/client.go create mode 100644 signer/kms/hashivault/options.go create mode 100644 signer/kms/hashivault/signer.go diff --git a/go.mod b/go.mod index f534c03b..d01d10ee 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d github.com/go-git/go-git/v5 v5.11.0 github.com/go-jose/go-jose/v3 v3.0.3 + github.com/hashicorp/vault-client-go v0.4.3 github.com/in-toto/archivista v0.4.0 github.com/in-toto/attestation v1.0.2 github.com/invopop/jsonschema v0.12.0 @@ -59,6 +60,7 @@ require ( github.com/coreos/go-oidc/v3 v3.10.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect @@ -75,10 +77,16 @@ require ( github.com/googleapis/gax-go/v2 v2.12.3 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v0.16.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/letsencrypt/boulder v0.0.0-20240226214708-a97e074b5a3e // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -88,6 +96,7 @@ require ( github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/common v0.51.1 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect github.com/segmentio/ksuid v1.0.4 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/go.sum b/go.sum index 0ece32bf..7ede2738 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,9 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -202,6 +205,20 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc= +github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY= github.com/in-toto/archivista v0.4.0 h1:5g79iqmyXblnnwuD+768lrEbeoE0V5H7URYJFnr0p4I= github.com/in-toto/archivista v0.4.0/go.mod h1:HgqAu7az0Ql0Jf844Paf0Ji5PdUMKxO5JIBh4hOjMs8= github.com/in-toto/attestation v1.0.2 h1:ICqV41bfaDC3ixVUzAtFxFu+Dy56EPcjiIrJQe+4LVM= @@ -238,6 +255,12 @@ github.com/letsencrypt/boulder v0.0.0-20240226214708-a97e074b5a3e h1:0YcEneR01Ff github.com/letsencrypt/boulder v0.0.0-20240226214708-a97e074b5a3e/go.mod h1:qY5wBgmaPwKkhGd2gNWZcoJBe9c76gsHm4OTc/N12+g= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= @@ -277,6 +300,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= @@ -426,7 +451,9 @@ golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -436,6 +463,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/imports.go b/imports.go index ef7a4e71..b931be8b 100644 --- a/imports.go +++ b/imports.go @@ -37,6 +37,9 @@ import ( // signer providers _ "github.com/in-toto/go-witness/signer/file" _ "github.com/in-toto/go-witness/signer/fulcio" + _ "github.com/in-toto/go-witness/signer/kms/aws" + _ "github.com/in-toto/go-witness/signer/kms/gcp" + _ "github.com/in-toto/go-witness/signer/kms/hashivault" _ "github.com/in-toto/go-witness/signer/spiffe" _ "github.com/in-toto/go-witness/signer/vault" ) diff --git a/registry/option.go b/registry/option.go index 28591008..5de039d2 100644 --- a/registry/option.go +++ b/registry/option.go @@ -26,7 +26,7 @@ type Configurer interface { } type Option interface { - int | string | []string | bool | time.Duration + int | string | []string | bool | time.Duration | uint64 } type ConfigOption[T any, TOption Option] struct { diff --git a/signer/kms/hashivault/client.go b/signer/kms/hashivault/client.go new file mode 100644 index 00000000..8fc20ad4 --- /dev/null +++ b/signer/kms/hashivault/client.go @@ -0,0 +1,234 @@ +package hashivault + +import ( + "context" + "crypto" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "regexp" + "strconv" + + vault "github.com/hashicorp/vault-client-go" + "github.com/hashicorp/vault-client-go/schema" + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/signer/kms" +) + +func init() { + kms.AddProvider(ReferenceScheme, &clientOptions{}, func(ctx context.Context, ksp *kms.KMSSignerProvider) (cryptoutil.Signer, error) { + return LoadSignerVerifier(ctx, ksp) + }) +} + +const ( + ReferenceScheme = "hashivault://" + providerName = "kms-hashivault" +) + +var ( + errReference = errors.New("kms specification should be in the format hashivault://") + referenceRegex = regexp.MustCompile(`^hashivault://(?P\w(([\w-.]+)?\w)?)$`) +) + +func ValidReference(ref string) error { + if !referenceRegex.MatchString(ref) { + return errReference + } + + return nil +} + +type client struct { + client *vault.Client + keyPath string + transitSecretsEnginePath string + keyVersion int32 +} + +func newClient(opts *clientOptions) (*client, error) { + vaultOpts := []vault.ClientOption{vault.WithEnvironment()} + if len(opts.addr) > 0 { + vaultOpts = append(vaultOpts, vault.WithAddress(opts.addr)) + } + + vaultClient, err := vault.New(vaultOpts...) + if err != nil { + return nil, fmt.Errorf("could not create vault client: %w", err) + } + + token := "" + if len(opts.tokenFile) > 0 { + tokenBytes, err := os.ReadFile(opts.tokenFile) + if err != nil { + return nil, fmt.Errorf("could not read vault token file: %w", err) + } + + token = string(tokenBytes) + } + + if len(token) > 0 { + vaultClient.SetToken(token) + } + + return &client{ + client: vaultClient, + keyPath: opts.keyPath, + transitSecretsEnginePath: opts.transitSecretEnginePath, + keyVersion: opts.keyVersion, + }, nil +} + +func (c *client) sign(ctx context.Context, digest []byte, hashFunc crypto.Hash) ([]byte, error) { + hashStr, ok := supportedHashesToString[hashFunc] + if !ok { + return nil, fmt.Errorf("unsupported hash algorithm: %v", hashFunc.String()) + } + + resp, err := c.client.Secrets.TransitSignWithAlgorithm( + ctx, + c.keyPath, + hashStr, + schema.TransitSignWithAlgorithmRequest{ + SignatureAlgorithm: "pkcs1v15", + HashAlgorithm: hashStr, + KeyVersion: c.keyVersion, + Prehashed: true, + Input: base64.StdEncoding.Strict().EncodeToString(digest), + }, + c.requestOptions()..., + ) + + if err != nil { + return nil, fmt.Errorf("could not sign: %w", err) + } + + signature, ok := resp.Data["signature"] + if !ok { + return nil, fmt.Errorf("no signature in response: %w", err) + } + + sigStr, ok := signature.(string) + if !ok { + return nil, fmt.Errorf("invalid signature in response") + } + + return []byte(sigStr), nil +} + +func (c *client) verify(ctx context.Context, r io.Reader, sig []byte, hashFunc crypto.Hash) error { + hashStr, ok := supportedHashesToString[hashFunc] + if !ok { + return fmt.Errorf("unsupported hash algorithm: %v", hashFunc.String()) + } + + digest, err := cryptoutil.Digest(r, hashFunc) + if err != nil { + return fmt.Errorf("could not calculate digest: %w", err) + } + + resp, err := c.client.Secrets.TransitVerifyWithAlgorithm( + ctx, + c.keyPath, + hashStr, + schema.TransitVerifyWithAlgorithmRequest{ + SignatureAlgorithm: "pkcs1v15", + HashAlgorithm: hashStr, + Prehashed: true, + Signature: string(sig), + Input: base64.StdEncoding.Strict().EncodeToString(digest), + }, + c.requestOptions()..., + ) + + if err != nil { + return fmt.Errorf("could not verify: %w", err) + } + + valid, ok := resp.Data["valid"] + if !ok { + return fmt.Errorf("invalid response") + } + + validBool, ok := valid.(bool) + if !ok { + return fmt.Errorf("expected valid to be bool but is %T", valid) + } + + if !validBool { + return fmt.Errorf("failed verification") + } + + return nil +} + +func (c *client) getPublicKeyBytes(ctx context.Context) ([]byte, error) { + resp, err := c.client.Secrets.TransitReadKey( + ctx, + c.keyPath, + c.requestOptions()..., + ) + + if err != nil { + return nil, fmt.Errorf("could not read key: %w", err) + } + + keyVersion := strconv.FormatInt(int64(c.keyVersion), 10) + if keyVersion == "0" { + latestVersion, ok := resp.Data["lastest_version"] + if !ok { + return nil, fmt.Errorf("latest key version not in response") + } + + latestVersionNum, ok := latestVersion.(json.Number) + if !ok { + return nil, fmt.Errorf("latest version not a number") + } + + keyVersion = latestVersionNum.String() + } + + keys, ok := resp.Data["keys"] + if !ok { + return nil, fmt.Errorf("no keys in response") + } + + keysMap, ok := keys.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected keys value in response") + } + + keyInfo, ok := keysMap[keyVersion] + if !ok { + return nil, fmt.Errorf("could not find key with version %v", keyVersion) + } + + keyMap, ok := keyInfo.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected key data format in response") + } + + publicKey, ok := keyMap["public_key"] + if !ok { + return nil, fmt.Errorf("public key not in key data") + } + + publicKeyStr, ok := publicKey.(string) + if !ok { + return nil, fmt.Errorf("unexpected public key data in response") + } + + return []byte(publicKeyStr), nil +} + +func (c *client) requestOptions() []vault.RequestOption { + opts := []vault.RequestOption{} + if len(c.transitSecretsEnginePath) > 0 { + opts = append(opts, vault.WithMountPath(c.transitSecretsEnginePath)) + } + + return opts +} diff --git a/signer/kms/hashivault/options.go b/signer/kms/hashivault/options.go new file mode 100644 index 00000000..bbdd23cb --- /dev/null +++ b/signer/kms/hashivault/options.go @@ -0,0 +1,108 @@ +package hashivault + +import ( + "fmt" + + "github.com/in-toto/go-witness/registry" + "github.com/in-toto/go-witness/signer" + "github.com/in-toto/go-witness/signer/kms" +) + +const ( + defaultTransitSecretEnginePath = "transit" + defaultKeyVersion uint64 = 0 +) + +type Option func(*clientOptions) + +type clientOptions struct { + addr string + tokenFile string + transitSecretEnginePath string + keyVersion int32 + keyPath string +} + +func (*clientOptions) ProviderName() string { + return providerName +} + +func (hv *clientOptions) Init() []registry.Configurer { + return []registry.Configurer{ + registry.StringConfigOption( + "addr", + "Address of the vault instance to connect to. Defaults to the environment variable VAULT_ADDR if unset", + "", + func(sp signer.SignerProvider, addr string) (signer.SignerProvider, error) { + ksp, ok := sp.(*kms.KMSSignerProvider) + if !ok { + return sp, fmt.Errorf("provided signer provider is not a kms signer provider") + } + + co, ok := ksp.Options[providerName].(*clientOptions) + if !ok { + return sp, fmt.Errorf("failed to get hashivault client options from kms signer provider") + } + + WithAddr(addr)(co) + return ksp, nil + }, + ), + registry.StringConfigOption( + "token-file", + "File to read the Vault token from. Token will be read from the environment variable VAULT_TOKEN if unset", + "", + func(sp signer.SignerProvider, tokenFile string) (signer.SignerProvider, error) { + ksp, ok := sp.(*kms.KMSSignerProvider) + if !ok { + return sp, fmt.Errorf("provided signer provider is not a kms signer provider") + } + + co, ok := ksp.Options[providerName].(*clientOptions) + if !ok { + return sp, fmt.Errorf("failed to get hashivault client options from kms signer provider") + } + + WithTokenFile(tokenFile)(co) + return ksp, nil + }, + ), + registry.StringConfigOption( + "transit-secret-engine-path", + "Path to the Vault Transit secret engine to use", + defaultTransitSecretEnginePath, + func(sp signer.SignerProvider, transitSecretEnginePath string) (signer.SignerProvider, error) { + ksp, ok := sp.(*kms.KMSSignerProvider) + if !ok { + return sp, fmt.Errorf("provided signer provider is not a kms signer provider") + } + + co, ok := ksp.Options[providerName].(*clientOptions) + if !ok { + return sp, fmt.Errorf("failed to get hashivault client options from kms signer provider") + } + + WithTransitSecretEnginePath(transitSecretEnginePath)(co) + return ksp, nil + }, + ), + } +} + +func WithAddr(addr string) Option { + return func(hco *clientOptions) { + hco.addr = addr + } +} + +func WithTokenFile(tokenFile string) Option { + return func(hco *clientOptions) { + hco.tokenFile = tokenFile + } +} + +func WithTransitSecretEnginePath(transitSecretEnginePath string) Option { + return func(hco *clientOptions) { + hco.transitSecretEnginePath = transitSecretEnginePath + } +} diff --git a/signer/kms/hashivault/signer.go b/signer/kms/hashivault/signer.go new file mode 100644 index 00000000..ed486a5a --- /dev/null +++ b/signer/kms/hashivault/signer.go @@ -0,0 +1,105 @@ +package hashivault + +import ( + "context" + "crypto" + "fmt" + "io" + "strconv" + + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/signer/kms" +) + +var ( + supportedHashesToString = map[crypto.Hash]string{ + crypto.SHA224: "sha2-224", + crypto.SHA256: "sha2-256", + crypto.SHA384: "sha2-384", + crypto.SHA512: "sha2-512", + } +) + +type SignerVerifier struct { + reference string + hashFunc crypto.Hash + client *client +} + +func LoadSignerVerifier(ctx context.Context, ksp *kms.KMSSignerProvider) (*SignerVerifier, error) { + potentialOpts := ksp.Options[providerName] + clientOpts, ok := potentialOpts.(*clientOptions) + if !ok { + return nil, fmt.Errorf("unexpected client options type: %T", potentialOpts) + } + + keyPath, err := parseReference(ksp.Reference) + if err != nil { + return nil, fmt.Errorf("could not parse vault ref: %w", err) + } + clientOpts.keyPath = keyPath + + _, ok = supportedHashesToString[ksp.HashFunc] + if !ok { + return nil, fmt.Errorf("vault does not support provided hash function %v", ksp.HashFunc.String()) + } + + if len(ksp.KeyVersion) > 0 { + keyVer, err := strconv.ParseInt(ksp.KeyVersion, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid vault key version %v: %w", ksp.KeyVersion, err) + } + + clientOpts.keyVersion = int32(keyVer) + } + + client, err := newClient(clientOpts) + if err != nil { + return nil, fmt.Errorf("could not create vault client: %w", err) + } + sv := &SignerVerifier{ + reference: ksp.Reference, + client: client, + hashFunc: ksp.HashFunc, + } + + return sv, nil +} + +func (sv *SignerVerifier) KeyID() (string, error) { + return sv.reference, nil +} + +func (sv *SignerVerifier) Sign(r io.Reader) ([]byte, error) { + ctx := context.TODO() + digest, err := cryptoutil.Digest(r, sv.hashFunc) + if err != nil { + return nil, fmt.Errorf("could not calculate digest: %w", err) + } + + return sv.client.sign(ctx, digest, sv.hashFunc) +} + +func (sv *SignerVerifier) Verifier() (cryptoutil.Verifier, error) { + return sv, nil +} + +func (sv *SignerVerifier) Bytes() ([]byte, error) { + return sv.client.getPublicKeyBytes(context.TODO()) +} + +func (sv *SignerVerifier) Verify(r io.Reader, sig []byte) error { + return sv.client.verify(context.TODO(), r, sig, sv.hashFunc) +} + +func parseReference(resourceID string) (string, error) { + keyPath := "" + i := referenceRegex.SubexpIndex("path") + v := referenceRegex.FindStringSubmatch(resourceID) + if len(v) < i+1 { + return keyPath, fmt.Errorf("invalid vault format %q", resourceID) + } + + keyPath = v[i] + return keyPath, nil +}