Skip to content

Commit

Permalink
feat: add ability to sign with vault transit secret engine
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhailswift committed May 30, 2024
1 parent 319d50a commit 025676f
Show file tree
Hide file tree
Showing 7 changed files with 488 additions and 1 deletion.
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down
3 changes: 3 additions & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
2 changes: 1 addition & 1 deletion registry/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
234 changes: 234 additions & 0 deletions signer/kms/hashivault/client.go
Original file line number Diff line number Diff line change
@@ -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://<key>")
referenceRegex = regexp.MustCompile(`^hashivault://(?P<path>\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)

Check failure on line 74 in signer/kms/hashivault/client.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `vaultClient.SetToken` is not checked (errcheck)
}

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
}
Loading

0 comments on commit 025676f

Please sign in to comment.