From 80d3b880ecbc9bced2c999804b1de03fd7a3b8c6 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 Signed-off-by: Mikhail Swift --- go.mod | 14 ++ go.sum | 48 +++++++ imports.go | 3 + signer/kms/hashivault/auth.go | 147 +++++++++++++++++++ signer/kms/hashivault/client.go | 239 +++++++++++++++++++++++++++++++ signer/kms/hashivault/options.go | 234 ++++++++++++++++++++++++++++++ signer/kms/hashivault/signer.go | 119 +++++++++++++++ 7 files changed, 804 insertions(+) create mode 100644 signer/kms/hashivault/auth.go 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 df5b64a5..68714a7a 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( github.com/gabriel-vasile/mimetype v1.4.5 github.com/go-git/go-git/v5 v5.11.0 github.com/go-jose/go-jose/v3 v3.0.3 + github.com/hashicorp/vault/api v1.14.0 + github.com/hashicorp/vault/api/auth/kubernetes v0.7.0 github.com/in-toto/archivista v0.5.3 github.com/in-toto/attestation v1.0.2 github.com/invopop/jsonschema v0.12.0 @@ -60,6 +62,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/coreos/go-oidc/v3 v3.11.0 // indirect @@ -81,10 +84,20 @@ 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.20.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.6 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.0 // 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-20240620165639-de9c06129bec // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/omnibor/omnibor-go v0.0.0-20230521145532-a77de61a16cd // indirect @@ -96,6 +109,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.53.0 // indirect github.com/prometheus/procfs v0.14.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 7ab9bea6..022b8e98 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,7 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.50.38 h1:h8wxaLin7sFGK4sKassc1VpNcDbgAAEQJ5PHjqLAvXQ= @@ -72,6 +73,7 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -79,6 +81,8 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -122,6 +126,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/fkautz/omnitrail-go v0.0.0-20230808061951-37d34c23539d h1:p4DOjnN5IAuUhtksK+RuwR2q3VclzeI1+zh+AfNFFjw= @@ -212,6 +219,33 @@ 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.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +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 v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM= +github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +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/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +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/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.14.0 h1:Ah3CFLixD5jmjusOgm8grfN9M0d+Y8fVR2SW0K6pJLU= +github.com/hashicorp/vault/api v1.14.0/go.mod h1:pV9YLxBGSz+cItFDd8Ii4G17waWOQ32zVjMWHe/cOqk= +github.com/hashicorp/vault/api/auth/kubernetes v0.7.0 h1:pHCbeeyD6E5KmMMCc9vwwZZ5OVlM6yFayxFHWodiOUU= +github.com/hashicorp/vault/api/auth/kubernetes v0.7.0/go.mod h1:Eey0x0X2g+b2LYWgBrQFyf5W0fp+Y1HGrEckP8Q0wns= github.com/in-toto/archivista v0.5.3 h1:zvrUfbjubVriF+h7xB34styIxSVPeKe/zskBQ1slFMg= github.com/in-toto/archivista v0.5.3/go.mod h1:044cyfixO6J/XWAZ+WMwTbCZcLnN0deHsRUK48GF4vk= github.com/in-toto/attestation v1.0.2 h1:ICqV41bfaDC3ixVUzAtFxFu+Dy56EPcjiIrJQe+4LVM= @@ -248,12 +282,21 @@ github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2T github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= 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.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +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.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 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= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 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-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -280,6 +323,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -293,6 +337,9 @@ 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/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +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= @@ -449,6 +496,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/imports.go b/imports.go index 4db6e32c..6163a223 100644 --- a/imports.go +++ b/imports.go @@ -40,6 +40,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/signer/kms/hashivault/auth.go b/signer/kms/hashivault/auth.go new file mode 100644 index 00000000..3eab3c51 --- /dev/null +++ b/signer/kms/hashivault/auth.go @@ -0,0 +1,147 @@ +// Copyright 2024 The Witness Contributors +// +// 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 hashivault + +import ( + "context" + "fmt" + "os" + "strings" + + vault "github.com/hashicorp/vault/api" + k8sAuth "github.com/hashicorp/vault/api/auth/kubernetes" + "github.com/in-toto/go-witness/log" +) + +type contextDoneErr struct{} + +func (contextDoneErr) Error() string { + return "context done" +} + +type needLoginErr struct { + watcherErr error +} + +func (e needLoginErr) Error() string { + return fmt.Sprintf("need login: %v", e.watcherErr) +} + +// login will authenticate with vault with the configured auth method +func (c *client) login(ctx context.Context) (*vault.Secret, error) { + if c.client == nil { + return nil, fmt.Errorf("vault client cannot be nil for login") + } + + switch strings.ToLower(c.authMethod) { + case "token": + token := "" + if len(c.tokenPath) > 0 { + tokenBytes, err := os.ReadFile(c.tokenPath) + if err != nil { + return nil, fmt.Errorf("could not read vault token file: %w", err) + } + + token = string(tokenBytes) + } + + if len(token) > 0 { + c.client.SetToken(token) + } + + // token based auth can't be refreshed, so no secret to return + return nil, nil + + case "kubernetes": + authMethod, err := k8sAuth.NewKubernetesAuth( + c.role, + k8sAuth.WithServiceAccountTokenPath(c.kubernetesSaTokenPath), + k8sAuth.WithMountPath(c.kubernetesAuthMountPath), + ) + + if err != nil { + return nil, fmt.Errorf("could not create kubernetes auth method: %w", err) + } + + authInfo, err := c.client.Auth().Login(ctx, authMethod) + if err != nil { + return nil, fmt.Errorf("could not login with kubernetes auth method: %w", err) + } + + return authInfo, nil + + default: + return nil, fmt.Errorf("unknown auth method: %v", c.authMethod) + } +} + +// periodicallyRenewAuth will start a watcher that will attempt to periodically renew vault's auth token. +// if the auth token's refresh lease is up, it will attempt to re-login entirely. +func (c *client) periodicallyRenewAuth(ctx context.Context, authInfo *vault.Secret) { + if authInfo == nil { + log.Debugf("can't refresh a nil vault secret") + return + } + + currentAuthInfo := authInfo + for { + err := c.renewAuth(ctx, currentAuthInfo) + // if the context signed on it's Done channel, we're cleaning up, bail out + if _, ok := err.(contextDoneErr); ok { + return + } else if _, ok := err.(needLoginErr); ok { + authInfo, err := c.login(ctx) + if err != nil { + log.Errorf("could not re-login to vault: %v", err) + return + } + + currentAuthInfo = authInfo + } else if err != nil { + log.Errorf("could not renew auth token: %v", err) + return + } + } +} + +// renewAuth is the meat of periodicallyRenewAuth. it creates the vault lifetime watcher and refreshes +// the current auth token as long as it has lease duration available. +func (c *client) renewAuth(ctx context.Context, authInfo *vault.Secret) error { + watcher, err := c.client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{ + Secret: authInfo, + }) + + if err != nil { + return fmt.Errorf("could not create vault token watcher: %w", err) + } + + go watcher.Start() + defer watcher.Stop() + + for { + select { + case <-ctx.Done(): + return contextDoneErr{} + + // if the watcher signals on it's done channel it means it either failed to refresh the current + // token's lease, or the token's remaining lease duration is too short and we need to re-login. + case err := <-watcher.DoneCh(): + return needLoginErr{watcherErr: err} + + case info := <-watcher.RenewCh(): + log.Debugf("renewed vault auth token, remaining lease duration: %d", info.Secret.LeaseDuration) + } + } +} diff --git a/signer/kms/hashivault/client.go b/signer/kms/hashivault/client.go new file mode 100644 index 00000000..7211c60d --- /dev/null +++ b/signer/kms/hashivault/client.go @@ -0,0 +1,239 @@ +// Copyright 2024 The Witness Contributors +// +// 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 hashivault + +import ( + "context" + "crypto" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "regexp" + "strconv" + + vault "github.com/hashicorp/vault/api" + "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 + + authMethod string + tokenPath string + kubernetesAuthMountPath string + kubernetesSaTokenPath string + role string +} + +func newClient(ctx context.Context, opts *clientOptions) (*client, error) { + vaultConf := vault.DefaultConfig() + if len(opts.addr) > 0 { + vaultConf.Address = opts.addr + } + + vaultClient, err := vault.NewClient(vaultConf) + if err != nil { + return nil, fmt.Errorf("could not create vault client: %w", err) + } + + c := &client{ + client: vaultClient, + keyPath: opts.keyPath, + transitSecretsEnginePath: opts.transitSecretEnginePath, + keyVersion: opts.keyVersion, + authMethod: opts.authMethod, + tokenPath: opts.tokenPath, + kubernetesAuthMountPath: opts.kubernetesMountPath, + kubernetesSaTokenPath: opts.kubernetesSaTokenPath, + role: opts.role, + } + + authInfo, err := c.login(ctx) + if err != nil { + return nil, fmt.Errorf("could not authenticate with vault: %w", err) + } + + if authInfo != nil { + go c.periodicallyRenewAuth(ctx, authInfo) + } + + return c, err +} + +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()) + } + + path := fmt.Sprintf("/%v/sign/%v/%v", c.transitSecretsEnginePath, c.keyPath, hashStr) + fmt.Println(path) + resp, err := c.client.Logical().WriteWithContext( + ctx, + path, + map[string]interface{}{ + "input": base64.StdEncoding.Strict().EncodeToString(digest), + "prehashed": true, + "key_version": c.keyVersion, + "signature_algorithm": "pkcs1v15", + }) + + 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.Logical().WriteWithContext( + ctx, + fmt.Sprintf("/%v/verify/%v/%v", c.transitSecretsEnginePath, c.keyPath, hashStr), + map[string]interface{}{ + "signature_algorithm": "pkcs1v15", + "input": base64.StdEncoding.Strict().EncodeToString(digest), + "signature": string(sig), + "prehashed": true, + }, + ) + + 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.Logical().ReadWithContext( + ctx, + fmt.Sprintf("/%v/keys/%v", c.transitSecretsEnginePath, c.keyPath), + ) + + 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 +} diff --git a/signer/kms/hashivault/options.go b/signer/kms/hashivault/options.go new file mode 100644 index 00000000..8c8259ca --- /dev/null +++ b/signer/kms/hashivault/options.go @@ -0,0 +1,234 @@ +// Copyright 2024 The Witness Contributors +// +// 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 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 + defaultAuthMethod = "token" + defaultKubernetesSATokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" + defaultKubernetesAuthMountPath = "kubernetes" +) + +type Option func(*clientOptions) + +type clientOptions struct { + addr string + transitSecretEnginePath string + keyVersion int32 + keyPath string + authMethod string + tokenPath string + kubernetesSaTokenPath string + role string + kubernetesMountPath 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 for token auth. 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 + }, + ), + registry.StringConfigOption( + "auth-method", + "Method to use to authenticate with Vault. Currently supported methods are token and kubernetes", + defaultAuthMethod, + func(sp signer.SignerProvider, authMethod 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") + } + + WithAuthMethod(authMethod)(co) + return ksp, nil + + }, + ), + registry.StringConfigOption( + "kubernetes-service-account-token-path", + "Path to the file containing the token for the kubernetes service account when using the kubernetes auth method", + defaultKubernetesSATokenPath, + func(sp signer.SignerProvider, saTokenPath 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") + } + + WithKubernetesServiceAccountTokenPath(saTokenPath)(co) + return ksp, nil + + }, + ), + registry.StringConfigOption( + "role", + "Role name to use when authenticating with the kubernetes auth method", + "", + func(sp signer.SignerProvider, role 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") + } + + WithRole(role)(co) + return ksp, nil + + }, + ), + registry.StringConfigOption( + "kubernetes-auth-mount-path", + "Path where the kubernetes auth endpoint is mounted on the vault server", + defaultKubernetesAuthMountPath, + func(sp signer.SignerProvider, kubernetesAuthMountPath 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") + } + + WithKubernetesAuthMountPath(kubernetesAuthMountPath)(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.tokenPath = tokenFile + } +} + +func WithTransitSecretEnginePath(transitSecretEnginePath string) Option { + return func(hco *clientOptions) { + hco.transitSecretEnginePath = transitSecretEnginePath + } +} + +func WithAuthMethod(authMethod string) Option { + return func(hco *clientOptions) { + hco.authMethod = authMethod + } +} + +func WithKubernetesServiceAccountTokenPath(saTokenPath string) Option { + return func(hco *clientOptions) { + hco.kubernetesSaTokenPath = saTokenPath + } +} + +func WithRole(role string) Option { + return func(hco *clientOptions) { + hco.role = role + } +} + +func WithKubernetesAuthMountPath(kubernetesAuthMountPath string) Option { + return func(hco *clientOptions) { + hco.kubernetesMountPath = kubernetesAuthMountPath + } +} diff --git a/signer/kms/hashivault/signer.go b/signer/kms/hashivault/signer.go new file mode 100644 index 00000000..e2582941 --- /dev/null +++ b/signer/kms/hashivault/signer.go @@ -0,0 +1,119 @@ +// Copyright 2024 The Witness Contributors +// +// 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 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(ctx, 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 +}