diff --git a/go.mod b/go.mod index 5bed4c9f7..e958f3793 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/google/go-cmp v0.5.7 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/goware/prefixer v0.0.0-20160118172347-395022866408 - github.com/hashicorp/vault/api v1.5.0 + github.com/hashicorp/vault/api v1.7.2 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef github.com/lib/pq v1.10.5 github.com/mitchellh/go-homedir v1.1.0 @@ -90,14 +90,14 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.0 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-version v1.4.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/vault/sdk v0.4.1 // indirect + github.com/hashicorp/vault/sdk v0.5.1 // indirect github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -105,7 +105,7 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/go.sum b/go.sum index cac254020..173e9d4c2 100644 --- a/go.sum +++ b/go.sum @@ -88,7 +88,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -140,7 +139,6 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -174,7 +172,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -196,30 +193,23 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= -github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -333,42 +323,33 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n 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.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= 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-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= github.com/hashicorp/go-retryablehttp v0.7.0/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/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ= github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3 h1:geBw3SBrxQq+buvbf4K+Qltv1gjaXJxy8asD4CjGYow= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.3/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/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-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= 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/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -377,10 +358,10 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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.5.0 h1:Bp6yc2bn7CWkOrVIzFT/Qurzx528bdavF3nz590eu28= -github.com/hashicorp/vault/api v1.5.0/go.mod h1:LkMdrZnWNrFaQyYYazWVn7KshilfDidgVBq6YiTq/bM= -github.com/hashicorp/vault/sdk v0.4.1 h1:3SaHOJY687jY1fnB61PtL0cOkKItphrbLmux7T92HBo= -github.com/hashicorp/vault/sdk v0.4.1/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= +github.com/hashicorp/vault/api v1.7.2 h1:kawHE7s/4xwrdKbkmwQi0wYaIeUhk5ueek7ljuezCVQ= +github.com/hashicorp/vault/api v1.7.2/go.mod h1:xbfA+1AvxFseDzxxdWaL0uO99n1+tndus4GCrtouy0M= +github.com/hashicorp/vault/sdk v0.5.1 h1:zly/TmNgOXCGgWIRA8GojyXzG817POtVh3uzIwzZx+8= +github.com/hashicorp/vault/sdk v0.5.1/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I= github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= @@ -389,7 +370,6 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -407,13 +387,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= @@ -421,7 +399,6 @@ github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -433,13 +410,11 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= @@ -447,10 +422,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= @@ -478,7 +451,6 @@ github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnh github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= @@ -584,7 +556,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -787,7 +758,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -990,7 +960,6 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= @@ -1019,7 +988,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/hcvault/keysource.go b/hcvault/keysource.go index 5078df619..a1e1736e1 100644 --- a/hcvault/keysource.go +++ b/hcvault/keysource.go @@ -3,6 +3,7 @@ package hcvault import ( "bytes" "encoding/base64" + "errors" "fmt" "io" "net/url" @@ -14,34 +15,66 @@ import ( "time" "github.com/hashicorp/vault/api" - homedir "github.com/mitchellh/go-homedir" + "github.com/mitchellh/go-homedir" "github.com/sirupsen/logrus" + "go.mozilla.org/sops/v3/logging" ) -var log *logrus.Logger - func init() { log = logging.NewLogger("VAULT_TRANSIT") } -// MasterKey is a Vault Transit backend path used to encrypt and decrypt sops' data key. -type MasterKey struct { - EncryptedKey string - KeyName string - EnginePath string - VaultAddress string - CreationDate time.Time +var ( + // log is the global logger for any Vault Transit MasterKey. + log *logrus.Logger + // vaultTTL is the duration after which a MasterKey requires rotation. + vaultTTL = time.Hour * 24 * 30 * 6 + // defaultTokenFile is the name of the file in the user's home directory + // where a Vault token is expected to be stored. + defaultTokenFile = ".vault-token" +) + +// Token used for authenticating towards a Vault server. +type Token string + +// ApplyToMasterKey configures the token on the provided key. +func (t Token) ApplyToMasterKey(key *MasterKey) { + key.token = string(t) } -// NewMasterKeysFromURIs gets lots of keys from lots of URIs +// MasterKey is a Vault Transit backend path used to Encrypt and Decrypt +// SOPS' data key. +type MasterKey struct { + // VaultAddress is the address of the Vault server. + VaultAddress string + // EnginePath is the path to the Vault Transit Secret engine relative + // to the VaultAddress. + EnginePath string + // KeyName is the name of the key in the Vault Transit engine. + KeyName string + // EncryptedKey contains the SOPS data key encrypted with the Vault Transit + // key. + EncryptedKey string + // CreationDate of the MasterKey, used to determine if the EncryptedKey + // needs rotation. + CreationDate time.Time + + // token is the token used for authenticating against the VaultAddress + // server. It can be injected by a (local) keyservice.KeyServiceServer + // Token.ApplyToMasterKey. If empty, the default client configuration + // is used, before falling back to the token stored in defaultTokenFile. + token string +} + +// NewMasterKeysFromURIs creates a list of MasterKeys from a list of Vault +// URIs. func NewMasterKeysFromURIs(uris string) ([]*MasterKey, error) { var keys []*MasterKey if uris == "" { return keys, nil } - uriList := strings.Split(uris, ",") - for _, uri := range uriList { + for _, uri := range strings.Split(uris, ",") { if uri == "" { continue } @@ -54,9 +87,9 @@ func NewMasterKeysFromURIs(uris string) ([]*MasterKey, error) { return keys, nil } -// NewMasterKeyFromURI obtains the vaultAddress the transit backend path and the key name from the full URI of the key +// NewMasterKeyFromURI obtains the Vault address, Transit backend path and the +// key name from the full URI of the key. func NewMasterKeyFromURI(uri string) (*MasterKey, error) { - log.Debugln("Called NewMasterKeyFromURI with uri: ", uri) var key *MasterKey if uri == "" { return key, nil @@ -66,9 +99,10 @@ func NewMasterKeyFromURI(uri string) (*MasterKey, error) { return nil, err } if u.Scheme == "" { - return nil, fmt.Errorf("missing scheme in vault URL (should be like this: https://vault.example.com:8200/v1/transit/keys/keyName), got: %v", uri) + return nil, fmt.Errorf("missing scheme in Vault URL (should be like this: +"+ + "https://vault.example.com:8200/v1/transit/keys/keyName), got: %v", uri) } - enginePath, keyName, err := getBackendAndKeyFromPath(u.RequestURI()) + enginePath, keyName, err := engineAndKeyFromPath(u.RequestURI()) if err != nil { return nil, err } @@ -77,110 +111,47 @@ func NewMasterKeyFromURI(uri string) (*MasterKey, error) { } -func getBackendAndKeyFromPath(fullPath string) (enginePath, keyName string, err error) { - // Running vault behind a reverse proxy with longer urls seems not to be supported - // by the vault client api so we have a separate Error for that here. - if re := regexp.MustCompile(`/[^/]+/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) { - return "", "", fmt.Errorf("running Vault with a prefixed url is not supported! (Format has to be like https://vault.example.com:8200/v1/transit/keys/keyName)") - } else if re := regexp.MustCompile(`/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) == false { - return "", "", fmt.Errorf("vault path does not seem to be formatted correctly: (eg. https://vault.example.com:8200/v1/transit/keys/keyName)") - } - fullPath = strings.TrimPrefix(fullPath, "/") - fullPath = strings.TrimSuffix(fullPath, "/") - - dirs := strings.Split(fullPath, "/") - - keyName = dirs[len(dirs)-1] - enginePath = path.Join(dirs[1 : len(dirs)-2]...) - err = nil - return -} - -// NewMasterKey creates a new MasterKey from a vault address, transit backend path and a key name and setting the creation date to the current date -func NewMasterKey(addess, enginePath, keyName string) *MasterKey { - mk := &MasterKey{ - VaultAddress: addess, +// NewMasterKey creates a new MasterKey from a Vault address, Transit backend +// path and a key name. +func NewMasterKey(address, enginePath, keyName string) *MasterKey { + key := &MasterKey{ + VaultAddress: address, EnginePath: enginePath, KeyName: keyName, CreationDate: time.Now().UTC(), } - log.Debugln("Created Vault Master Key: ", mk) - return mk + return key } -// EncryptedDataKey returns the encrypted data key this master key holds -func (key *MasterKey) EncryptedDataKey() []byte { - return []byte(key.EncryptedKey) -} - -// SetEncryptedDataKey sets the encrypted data key for this master key -func (key *MasterKey) SetEncryptedDataKey(enc []byte) { - key.EncryptedKey = string(enc) -} - -func vaultClient(address string) (*api.Client, error) { - cfg := api.DefaultConfig() - cfg.Address = address - cli, err := api.NewClient(cfg) - if err != nil { - return nil, fmt.Errorf("Cannot create Vault Client: %w", err) - } - if cli.Token() != "" { - return cli, nil - } - homePath, err := homedir.Dir() - if err != nil { - panic(fmt.Sprintf("error getting user's home directory: %v", err)) - } - tokenPath := filepath.Join(homePath, ".vault-token") - f, err := os.Open(tokenPath) - if os.IsNotExist(err) { - return cli, nil - } - if err != nil { - return nil, err - } - defer f.Close() - - buf := bytes.NewBuffer(nil) - if _, err := io.Copy(buf, f); err != nil { - return nil, err - } - cli.SetToken(strings.TrimSpace(buf.String())) - return cli, nil -} - -// Encrypt takes a sops data key, encrypts it with Vault Transit and stores the result in the EncryptedKey field +// Encrypt takes a SOPS data key, encrypts it with Vault Transit, and stores +// the result in the EncryptedKey field. func (key *MasterKey) Encrypt(dataKey []byte) error { - fullPath := path.Join(key.EnginePath, "encrypt", key.KeyName) - cli, err := vaultClient(key.VaultAddress) + fullPath := key.encryptPath() + + client, err := vaultClient(key.VaultAddress, key.token) if err != nil { + log.WithError(err).WithField("Path", fullPath).Error("Encryption failed") return err } - encoded := base64.StdEncoding.EncodeToString(dataKey) - payload := make(map[string]interface{}) - payload["plaintext"] = encoded - raw, err := cli.Logical().Write(fullPath, payload) + + secret, err := client.Logical().Write(fullPath, encryptPayload(dataKey)) if err != nil { - log.WithField("Path", fullPath).Info("Encryption failed") - return err + log.WithError(err).WithField("Path", fullPath).Error("Encryption failed") + return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err) } - if raw == nil || raw.Data == nil { - return fmt.Errorf("The transit backend %s is empty", fullPath) - } - encrypted, ok := raw.Data["ciphertext"] - if !ok { - return fmt.Errorf("there's not encrypted data") - } - encryptedKey, ok := encrypted.(string) - if !ok { - return fmt.Errorf("the ciphertext cannot be casted to string") + encryptedKey, err := encryptedKeyFromSecret(secret) + if err != nil { + log.WithError(err).WithField("Path", fullPath).Error("Encryption failed") + return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err) } + key.EncryptedKey = encryptedKey + log.WithField("Path", fullPath).Info("Encryption successful") return nil } -// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet +// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been +// encrypted yet. func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { if key.EncryptedKey == "" { return key.Encrypt(dataKey) @@ -188,81 +159,53 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { return nil } +// EncryptedDataKey returns the encrypted data key this master key holds. +func (key *MasterKey) EncryptedDataKey() []byte { + return []byte(key.EncryptedKey) +} + +// SetEncryptedDataKey sets the encrypted data key for this master key. +func (key *MasterKey) SetEncryptedDataKey(enc []byte) { + key.EncryptedKey = string(enc) +} + // Decrypt decrypts the EncryptedKey field with Vault Transit and returns the result. func (key *MasterKey) Decrypt() ([]byte, error) { - fullPath := path.Join(key.EnginePath, "decrypt", key.KeyName) - cli, err := vaultClient(key.VaultAddress) + fullPath := key.decryptPath() + + client, err := vaultClient(key.VaultAddress, key.token) if err != nil { + log.WithError(err).WithField("Path", fullPath).Error("Decryption failed") return nil, err } - payload := make(map[string]interface{}) - payload["ciphertext"] = key.EncryptedKey - raw, err := cli.Logical().Write(fullPath, payload) + + secret, err := client.Logical().Write(fullPath, decryptPayload(key.EncryptedKey)) if err != nil { - log.WithField("Path", fullPath).Info("Encryption failed") - return nil, err + log.WithError(err).WithField("Path", fullPath).Error("Decryption failed") + return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err) } - if raw == nil || raw.Data == nil { - return nil, fmt.Errorf("The transit backend %s is empty", fullPath) - } - decrypted, ok := raw.Data["plaintext"] - if ok != true { - return nil, fmt.Errorf("there's no decrypted data") - } - dataKey, ok := decrypted.(string) - if ok != true { - return nil, fmt.Errorf("the plaintest cannot be casted to string") - } - result, err := base64.StdEncoding.DecodeString(dataKey) + dataKey, err := dataKeyFromSecret(secret) if err != nil { - return nil, fmt.Errorf("Couldn't decode base64 plaintext") + log.WithError(err).WithField("Path", fullPath).Error("Decryption failed") + return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err) } - return result, nil + + log.WithField("Path", fullPath).Info("Decryption successful") + return dataKey, nil } // NeedsRotation returns whether the data key needs to be rotated or not. -// This is simply copied from GCPKMS -// TODO: handle key rotation on vault side func (key *MasterKey) NeedsRotation() bool { - //TODO: manage rewrapping https://www.vaultproject.io/api/secret/transit/index.html#rewrap-data - return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6) + // TODO: manage rewrapping https://www.vaultproject.io/api/secret/transit/index.html#rewrap-data + return time.Since(key.CreationDate) > (vaultTTL) } -// ToString converts the key to a string representation +// ToString converts the key to a string representation. func (key *MasterKey) ToString() string { return fmt.Sprintf("%s/v1/%s/keys/%s", key.VaultAddress, key.EnginePath, key.KeyName) } -func (key *MasterKey) createVaultTransitAndKey() error { - cli, err := vaultClient(key.VaultAddress) - if err != nil { - return err - } - if err != nil { - return fmt.Errorf("Cannot create Vault Client: %w", err) - } - err = cli.Sys().Mount(key.EnginePath, &api.MountInput{ - Type: "transit", - Description: "backend transit used by SOPS", - }) - if err != nil { - return err - } - path := path.Join(key.EnginePath, "keys", key.KeyName) - payload := make(map[string]interface{}) - payload["type"] = "rsa-4096" - _, err = cli.Logical().Write(path, payload) - if err != nil { - return err - } - _, err = cli.Logical().Read(path) - if err != nil { - return err - } - return nil -} - -// ToMap converts the MasterKey to a map for serialization purposes +// ToMap converts the MasterKey to a map for serialization purposes. func (key MasterKey) ToMap() map[string]interface{} { out := make(map[string]interface{}) out["vault_address"] = key.VaultAddress @@ -272,3 +215,145 @@ func (key MasterKey) ToMap() map[string]interface{} { out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) return out } + +// encryptPath returns the path for Encrypt requests. +func (key *MasterKey) encryptPath() string { + return path.Join(key.EnginePath, "encrypt", key.KeyName) +} + +// decryptPath returns the path for Decrypt requests. +func (key *MasterKey) decryptPath() string { + return path.Join(key.EnginePath, "decrypt", key.KeyName) +} + +// encryptPayload returns the payload for an encrypt request of the dataKey. +func encryptPayload(dataKey []byte) map[string]interface{} { + encoded := base64.StdEncoding.EncodeToString(dataKey) + return map[string]interface{}{ + "plaintext": encoded, + } +} + +// encryptedKeyFromSecret attempts to extract the encrypted key from the data +// of the provided secret. +func encryptedKeyFromSecret(secret *api.Secret) (string, error) { + if secret == nil || secret.Data == nil { + return "", fmt.Errorf("transit backend is empty") + } + encrypted, ok := secret.Data["ciphertext"] + if !ok { + return "", fmt.Errorf("no encrypted data") + } + encryptedKey, ok := encrypted.(string) + if !ok { + return "", fmt.Errorf("encrypted ciphertext cannot be cast to string") + } + return encryptedKey, nil +} + +// decryptPayload returns the payload for a decrypt request of the +// encryptedKey. +func decryptPayload(encryptedKey string) map[string]interface{} { + return map[string]interface{}{ + "ciphertext": encryptedKey, + } +} + +// dataKeyFromSecret attempts to extract the data key from the data of the +// provided secret. +func dataKeyFromSecret(secret *api.Secret) ([]byte, error) { + if secret == nil || secret.Data == nil { + return nil, fmt.Errorf("transit backend is empty") + } + decrypted, ok := secret.Data["plaintext"] + if !ok { + return nil, fmt.Errorf("no decrypted data") + } + plaintext, ok := decrypted.(string) + if !ok { + return nil, fmt.Errorf("decrypted plaintext data cannot be cast to string") + } + dataKey, err := base64.StdEncoding.DecodeString(plaintext) + if err != nil { + return nil, fmt.Errorf("cannot decode base64 plaintext into data key bytes") + } + return dataKey, nil +} + +// vaultClient returns a new Vault client, configured with the given address +// and token. +func vaultClient(address, token string) (*api.Client, error) { + cfg := api.DefaultConfig() + cfg.Address = address + + client, err := api.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("cannot create Vault client: %w", err) + } + + if token != "" { + client.SetToken(token) + } + // Provided token takes precedence over the user's token. + if client.Token() == "" { + if token, err = userVaultToken(); err != nil { + return nil, fmt.Errorf("cannot get Vault token: %w", err) + } + if token != "" { + client.SetToken(token) + } + } + + return client, nil +} + +// userVaultsToken returns the token from `$HOME/.vault-token` if the file +// exists. It returns an error if the file exists but cannot be read from. +// If the file does not exist, it returns an empty string. +func userVaultToken() (string, error) { + homePath, err := homedir.Dir() + if err != nil { + return "", fmt.Errorf("error getting user's home directory: %w", err) + } + tokenPath := filepath.Join(homePath, defaultTokenFile) + + f, err := os.Open(tokenPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return "", nil + } + return "", err + } + defer f.Close() + + buf := bytes.NewBuffer(nil) + if _, err := io.Copy(buf, f); err != nil { + return "", err + } + return strings.TrimSpace(buf.String()), nil +} + +// engineAndKeyFromPath returns the engine path and key name from the full +// path, or an error. +func engineAndKeyFromPath(fullPath string) (enginePath, keyName string, err error) { + // Running vault behind a reverse proxy with longer URLs seems not to be + // supported by the Vault client API. Check for this here. + // TODO(hidde): this may no longer be necessary with newer Vault versions, + // but needs to be confirmed. + if re := regexp.MustCompile(`/[^/]+/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) { + err = fmt.Errorf("running Vault with a prefixed URL is not supported! (Format has to be like " + + "https://vault.example.com:8200/v1/transit/keys/keyName)") + return + } else if re := regexp.MustCompile(`/v[\d]+/[^/]+/[^/]+/[^/]+`); !re.Match([]byte(fullPath)) { + err = fmt.Errorf("vault path does not seem to be formatted correctly: (eg. " + + "https://vault.example.com:8200/v1/transit/keys/keyName)") + return + } + + fullPath = strings.Trim(fullPath, "/") + dirs := strings.Split(fullPath, "/") + + keyName = dirs[len(dirs)-1] + enginePath = path.Join(dirs[1 : len(dirs)-2]...) + return +} diff --git a/hcvault/keysource_test.go b/hcvault/keysource_test.go index 48a31dc83..6f488c418 100644 --- a/hcvault/keysource_test.go +++ b/hcvault/keysource_test.go @@ -4,166 +4,511 @@ import ( "fmt" logger "log" "os" + "path" + "path/filepath" + "strings" "testing" "time" "github.com/hashicorp/vault/api" + "github.com/mitchellh/go-homedir" "github.com/ory/dockertest" "github.com/stretchr/testify/assert" ) +var ( + // testVaultVersion is the version (image tag) of the Vault server image + // used to test against. + testVaultVersion = "1.10.0" + // testVaultToken is the token of the Vault server. + testVaultToken = "secret" + // testEnginePath is the path to mount the Vault Transit on. + testEnginePath = "sops" + // testVaultAddress is the HTTP/S address of the Vault server, it is set + // by TestMain after booting it. + testVaultAddress string +) + +// TestMain initializes a Vault server using Docker, writes the HTTP address to +// testVaultAddress, waits for it to become ready to serve requests, and enables +// Vault Transit on the testEnginePath. It then runs all the tests, which can +// make use of the various `test*` variables. func TestMain(m *testing.M) { - // uses a sensible default on windows (tcp/http) and linux/osx (socket) + // Uses a sensible default on Windows (TCP/HTTP) and Linux/MacOS (socket) pool, err := dockertest.NewPool("") if err != nil { - logger.Fatalf("Could not connect to docker: %s", err) + logger.Fatalf("could not connect to docker: %s", err) } - // pulls an image, creates a container based on it and runs it - resource, err := pool.Run("vault", "1.2.2", []string{"VAULT_DEV_ROOT_TOKEN_ID=secret"}) + // Pull the image, create a container based on it, and run it + resource, err := pool.Run("vault", testVaultVersion, []string{"VAULT_DEV_ROOT_TOKEN_ID=" + testVaultToken}) if err != nil { - logger.Fatalf("Could not start resource: %s", err) + logger.Fatalf("could not start resource: %s", err) } - vaultAddr := fmt.Sprintf("http://%s", resource.GetHostPort("8200/tcp")) - os.Setenv("VAULT_ADDR", vaultAddr) - os.Setenv("VAULT_TOKEN", "secret") - // exponential backoff-retry, because the application in the container might not be ready to accept connections yet + purgeResource := func() { + if err := pool.Purge(resource); err != nil { + logger.Printf("could not purge resource: %s", err) + } + } + + testVaultAddress = fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("8200/tcp")) + // Wait until Vault is ready to serve requests if err := pool.Retry(func() error { - cli, err := api.NewClient(api.DefaultConfig()) + cfg := api.DefaultConfig() + cfg.Address = testVaultAddress + cli, err := api.NewClient(cfg) if err != nil { - return fmt.Errorf("Cannot create Vault Client: %w", err) + return fmt.Errorf("cannot create Vault client: %w", err) } status, err := cli.Sys().InitStatus() if err != nil { return err } if status != true { - return fmt.Errorf("Vault not ready yet") + return fmt.Errorf("waiting on Vault server to become ready") } return nil }); err != nil { - logger.Fatalf("Could not connect to docker: %s", err) + purgeResource() + logger.Fatalf("could not connect to docker: %s", err) } - key := NewMasterKey(vaultAddr, "sops", "main") - err = key.createVaultTransitAndKey() - if err != nil { - logger.Fatal(err) + if err = enableVaultTransit(testVaultAddress, testVaultToken, testEnginePath); err != nil { + purgeResource() + logger.Fatalf("could not enable Vault transit: %s", err) } - code := 0 + + // Run the tests, but only if we succeeded in setting up the Vault server + var code int if err == nil { code = m.Run() } - // You can't defer this because os.Exit doesn't care for defer + // This can't be deferred, as os.Exit simpy does not care if err := pool.Purge(resource); err != nil { - logger.Fatalf("Could not purge resource: %s", err) + logger.Fatalf("could not purge resource: %s", err) } os.Exit(code) } -func TestKeyToMap(t *testing.T) { - key := MasterKey{ - CreationDate: time.Date(2016, time.October, 31, 10, 0, 0, 0, time.UTC), - VaultAddress: "http://127.0.0.1:8200", - EnginePath: "foo", - KeyName: "bar", - EncryptedKey: "this is encrypted", - } - assert.Equal(t, map[string]interface{}{ - "vault_address": "http://127.0.0.1:8200", - "engine_path": "foo", - "key_name": "bar", - "enc": "this is encrypted", - "created_at": "2016-10-31T10:00:00Z", - }, key.ToMap()) -} +func TestNewMasterKeysFromURIs(t *testing.T) { + t.Run("multiple URIs", func(t *testing.T) { + uris := []string{ + "https://vault.example.com:8200/v1/transit/keys/keyName", + "", // Empty should be skipped + "https://vault.me.com/v1/super42/bestmarket/keys/slig", + } + keys, err := NewMasterKeysFromURIs(strings.Join(uris, ",")) + assert.NoError(t, err) + assert.Len(t, keys, 2) + }) -func TestEncryptionDecryption(t *testing.T) { - dataKey := []byte("super very Secret Key!!!") - key := MasterKey{ - VaultAddress: os.Getenv("VAULT_ADDR"), - EnginePath: "sops", - KeyName: "main", - } - err := key.Encrypt(dataKey) - if err != nil { - fmt.Println(err) - t.Fail() - return - } - decrypted, err := key.Decrypt() - if err != nil { - fmt.Println(err) - t.Fail() - return - } - assert.Equal(t, dataKey, decrypted) + t.Run("with invalid URI", func(t *testing.T) { + uris := []string{ + "https://vault.example.com:8200/v1/transit/keys/keyName", + "vault.me/keys/dev/mykey", + } + keys, err := NewMasterKeysFromURIs(strings.Join(uris, ",")) + assert.Error(t, err) + assert.Nil(t, keys) + }) } func TestNewMasterKeyFromURI(t *testing.T) { - uri1 := "https://vault.example.com:8200/v1/transit/keys/keyName" - uri2 := "https://vault.me.com/v1/super42/bestmarket/keys/slig" - uri3 := "http://127.0.0.1:12121/v1/transit/keys/dev" - - mk1 := &MasterKey{ - VaultAddress: "https://vault.example.com:8200", - EnginePath: "transit", - KeyName: "keyName", + tests := []struct { + url string + want *MasterKey + wantErr bool + }{ + { + url: "https://vault.example.com:8200/v1/transit/keys/keyName", + want: &MasterKey{ + VaultAddress: "https://vault.example.com:8200", + EnginePath: "transit", + KeyName: "keyName", + }, + }, + { + url: "https://vault.me.com/v1/super42/bestmarket/keys/slig", + want: &MasterKey{ + VaultAddress: "https://vault.me.com", + EnginePath: "super42/bestmarket", + KeyName: "slig", + }, + }, + { + url: "http://127.0.0.1:12121/v1/transit/keys/dev", + want: &MasterKey{ + VaultAddress: "http://127.0.0.1:12121", + EnginePath: "transit", + KeyName: "dev", + }, + }, + { + url: "vault.me/keys/dev/mykey", + want: nil, + wantErr: true, + }, + { + url: "http://127.0.0.1:12121/v1/keys/dev", + want: nil, + wantErr: true, + }, + { + url: "tcp://127.0.0.1:12121/v1/keys/dev", + want: nil, + wantErr: true, + }, } - mk2 := &MasterKey{ - VaultAddress: "https://vault.me.com", - EnginePath: "super42/bestmarket", - KeyName: "slig", + for _, tt := range tests { + t.Run(tt.url, func(t *testing.T) { + got, err := NewMasterKeyFromURI(tt.url) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + if tt.want != nil && got != nil { + tt.want.CreationDate = got.CreationDate + } + assert.Equal(t, tt.want, got) + }) } - mk3 := &MasterKey{ - VaultAddress: "http://127.0.0.1:12121", - EnginePath: "transit", - KeyName: "dev", - } - genMk1, err := NewMasterKeyFromURI(uri1) - if err != nil { - log.Errorln(err) - t.Fail() - } - - genMk2, err := NewMasterKeyFromURI(uri2) - if err != nil { - log.Errorln(err) - t.Fail() - } - - genMk3, err := NewMasterKeyFromURI(uri3) - if err != nil { - log.Errorln(err) - t.Fail() - } - - if assert.NotNil(t, genMk1) { - mk1.CreationDate = genMk1.CreationDate - assert.Equal(t, mk1, genMk1) - } - if assert.NotNil(t, genMk2) { - mk2.CreationDate = genMk2.CreationDate - assert.Equal(t, mk2, genMk2) - } - if assert.NotNil(t, genMk3) { - mk3.CreationDate = genMk3.CreationDate - assert.Equal(t, mk3, genMk3) - } - - badURIs := []string{ - "vault.me/keys/dev/mykey", - "http://127.0.0.1:12121/v1/keys/dev", - "tcp://127.0.0.1:12121/v1/keys/dev", - } - for _, uri := range badURIs { - if _, err = NewMasterKeyFromURI(uri); err == nil { - log.Errorf("Should be a invalid uri: %s", uri) - t.Fail() - } - } - +} + +func TestMasterKey_Encrypt(t *testing.T) { + key := NewMasterKey(testVaultAddress, testEnginePath, "encrypt") + (Token(testVaultToken)).ApplyToMasterKey(key) + assert.NoError(t, createVaultKey(key)) + + dataKey := []byte("the majority of your brain is fat") + assert.NoError(t, key.Encrypt(dataKey)) + assert.NotEmpty(t, key.EncryptedKey) + + client, err := vaultClient(key.VaultAddress, key.token) + assert.NoError(t, err) + + payload := decryptPayload(key.EncryptedKey) + secret, err := client.Logical().Write(key.decryptPath(), payload) + assert.NoError(t, err) + + decryptedData, err := dataKeyFromSecret(secret) + assert.NoError(t, err) + assert.Equal(t, dataKey, decryptedData) + + key.EnginePath = "invalid" + assert.Error(t, key.Encrypt(dataKey)) + + key.EnginePath = testEnginePath + key.token = "" + assert.Error(t, key.Encrypt(dataKey)) +} + +func TestMasterKey_EncryptIfNeeded(t *testing.T) { + key := NewMasterKey(testVaultAddress, testEnginePath, "encrypt-if-needed") + (Token(testVaultToken)).ApplyToMasterKey(key) + assert.NoError(t, createVaultKey(key)) + + assert.NoError(t, key.EncryptIfNeeded([]byte("stingy string"))) + + encryptedKey := key.EncryptedKey + assert.NotEmpty(t, encryptedKey) + + assert.NoError(t, key.EncryptIfNeeded([]byte("stringy sting"))) + assert.Equal(t, encryptedKey, key.EncryptedKey) +} + +func TestMasterKey_EncryptedDataKey(t *testing.T) { + key := &MasterKey{EncryptedKey: "some key"} + assert.EqualValues(t, key.EncryptedKey, key.EncryptedDataKey()) +} + +func TestMasterKey_Decrypt(t *testing.T) { + key := NewMasterKey(testVaultAddress, testEnginePath, "decrypt") + (Token(testVaultToken)).ApplyToMasterKey(key) + assert.NoError(t, createVaultKey(key)) + + client, err := vaultClient(key.VaultAddress, key.token) + assert.NoError(t, err) + + dataKey := []byte("the heart of a shrimp is located in its head") + secret, err := client.Logical().Write(key.encryptPath(), encryptPayload(dataKey)) + assert.NoError(t, err) + + encryptedKey, err := encryptedKeyFromSecret(secret) + assert.NoError(t, err) + + key.EncryptedKey = encryptedKey + got, err := key.Decrypt() + assert.NoError(t, err) + assert.Equal(t, dataKey, got) + + key.EnginePath = "invalid" + assert.Error(t, key.Encrypt(dataKey)) + + key.EnginePath = testEnginePath + key.token = "" + assert.Error(t, key.Encrypt(dataKey)) +} + +func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { + token := Token(testVaultToken) + + encryptKey := NewMasterKey(testVaultAddress, testEnginePath, "roundtrip") + token.ApplyToMasterKey(encryptKey) + assert.NoError(t, createVaultKey(encryptKey)) + + dataKey := []byte("some people have an extra bone in their knee") + assert.NoError(t, encryptKey.Encrypt(dataKey)) + assert.NotEmpty(t, encryptKey.EncryptedKey) + + decryptKey := NewMasterKey(testVaultAddress, testEnginePath, "roundtrip") + token.ApplyToMasterKey(decryptKey) + decryptKey.EncryptedKey = encryptKey.EncryptedKey + + decryptedData, err := decryptKey.Decrypt() + assert.NoError(t, err) + assert.Equal(t, dataKey, decryptedData) +} + +func TestMasterKey_NeedsRotation(t *testing.T) { + key := NewMasterKey("", "", "") + assert.False(t, key.NeedsRotation()) + + key.CreationDate = key.CreationDate.Add(-(vaultTTL + time.Second)) + assert.True(t, key.NeedsRotation()) +} + +func TestMasterKey_ToString(t *testing.T) { + key := NewMasterKey("https://example.com", "engine", "key-name") + assert.Equal(t, "https://example.com/v1/engine/keys/key-name", key.ToString()) +} + +func TestMasterKey_ToMap(t *testing.T) { + key := &MasterKey{ + KeyName: "test-key", + EnginePath: "engine", + VaultAddress: testVaultAddress, + EncryptedKey: "some-encrypted-key", + } + assert.Equal(t, map[string]interface{}{ + "vault_address": key.VaultAddress, + "key_name": key.KeyName, + "engine_path": key.EnginePath, + "enc": key.EncryptedKey, + "created_at": "0001-01-01T00:00:00Z", + }, key.ToMap()) +} + +func Test_encryptedKeyFromSecret(t *testing.T) { + tests := []struct { + name string + secret *api.Secret + want string + wantErr bool + }{ + {name: "nil secret", secret: nil, wantErr: true}, + {name: "secret with nil data", secret: &api.Secret{Data: nil}, wantErr: true}, + {name: "secret without ciphertext data", secret: &api.Secret{Data: map[string]interface{}{"other": true}}, wantErr: true}, + {name: "ciphertext non string", secret: &api.Secret{Data: map[string]interface{}{"ciphertext": 123}}, wantErr: true}, + {name: "ciphertext data", secret: &api.Secret{Data: map[string]interface{}{"ciphertext": "secret string"}}, want: "secret string"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := encryptedKeyFromSecret(tt.secret) + if tt.wantErr { + assert.Error(t, err) + assert.Empty(t, got) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_dataKeyFromSecret(t *testing.T) { + tests := []struct { + name string + secret *api.Secret + want []byte + wantErr bool + }{ + {name: "nil secret", secret: nil, wantErr: true}, + {name: "secret with nil data", secret: &api.Secret{Data: nil}, wantErr: true}, + {name: "secret without plaintext data", secret: &api.Secret{Data: map[string]interface{}{"other": true}}, wantErr: true}, + {name: "plaintext non string", secret: &api.Secret{Data: map[string]interface{}{"plaintext": 123}}, wantErr: true}, + {name: "plaintext non base64", secret: &api.Secret{Data: map[string]interface{}{"plaintext": "notbase64"}}, wantErr: true}, + {name: "plaintext base64 data", secret: &api.Secret{Data: map[string]interface{}{"plaintext": "Zm9v"}}, want: []byte("foo")}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := dataKeyFromSecret(tt.secret) + if tt.wantErr { + assert.Error(t, err) + assert.Empty(t, got) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_vaultClient(t *testing.T) { + t.Run("client", func(t *testing.T) { + t.Setenv("VAULT_TOKEN", "") + + got, err := vaultClient(testVaultAddress, "") + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Empty(t, got.Token()) + }) + + t.Run("client with VAULT_TOKEN", func(t *testing.T) { + token := "test-token" + t.Setenv("VAULT_TOKEN", token) + + got, err := vaultClient(testVaultAddress, "") + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Equal(t, token, got.Token()) + }) + + t.Run("client with token", func(t *testing.T) { + ignored := "test-token" + t.Setenv("VAULT_TOKEN", ignored) + + got, err := vaultClient(testVaultAddress, testVaultToken) + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Equal(t, testVaultToken, got.Token()) + }) + + t.Run("client with token from file", func(t *testing.T) { + tmpDir := t.TempDir() + + token := "test-token" + assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, defaultTokenFile), []byte(token), 0600)) + + // Reset before and after to make sure the override is taken into + // account, and restored after the test. + homedir.Reset() + t.Cleanup(func() { homedir.Reset() }) + t.Setenv("VAULT_TOKEN", "") + t.Setenv("HOME", tmpDir) + + got, err := vaultClient(testVaultAddress, "") + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Equal(t, token, got.Token()) + }) +} + +func Test_userVaultToken(t *testing.T) { + t.Run("reads token from file", func(t *testing.T) { + tmpDir := t.TempDir() + + token := "test-token" + assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, defaultTokenFile), []byte(token), 0600)) + + // Reset before and after to make sure the override is taken into + // account, and restored after the test. + homedir.Reset() + t.Cleanup(func() { homedir.Reset() }) + t.Setenv("HOME", tmpDir) + + got, err := userVaultToken() + assert.NoError(t, err) + assert.Equal(t, token, got) + }) + + t.Run("ignores missing file", func(t *testing.T) { + tmpDir := t.TempDir() + + // Reset before and after to make sure the override is taken into + // account, and restored after the test. + homedir.Reset() + t.Cleanup(func() { homedir.Reset() }) + t.Setenv("HOME", tmpDir) + + got, err := userVaultToken() + assert.NoError(t, err) + assert.Empty(t, got) + }) + + t.Run("trims spaces", func(t *testing.T) { + tmpDir := t.TempDir() + + token := " test-token " + assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, defaultTokenFile), []byte(token), 0600)) + + // Reset before and after to make sure the override is taken into + // account, and restored after the test. + homedir.Reset() + t.Cleanup(func() { homedir.Reset() }) + t.Setenv("HOME", tmpDir) + + got, err := userVaultToken() + assert.NoError(t, err) + assert.Equal(t, "test-token", got) + }) +} + +func Test_engineAndKeyFromPath(t *testing.T) { + t.Run("engine and key", func(t *testing.T) { + enginePath, key, err := engineAndKeyFromPath("/v1/transit/keys/keyName") + assert.NoError(t, err) + assert.Equal(t, "transit", enginePath) + assert.Equal(t, "keyName", key) + }) + + t.Run("long (nested) path error", func(t *testing.T) { + _, _, err := engineAndKeyFromPath("/nested/v1/transit/keys/bar") + assert.Error(t, err) + assert.ErrorContains(t, err, "running Vault with a prefixed URL is not supported") + }) + + t.Run("invalid format error", func(t *testing.T) { + _, _, err := engineAndKeyFromPath("/secret/foo/bar") + assert.Error(t, err) + assert.ErrorContains(t, err, "vault path does not seem to be formatted correctly") + }) +} + +// enableVaultTransit enables the Vault Transit backend on the given enginePath. +func enableVaultTransit(address, token, enginePath string) error { + client, err := vaultClient(address, token) + if err != nil { + return fmt.Errorf("cannot create Vault client: %w", err) + } + + if err = client.Sys().Mount(enginePath, &api.MountInput{ + Type: "transit", + Description: "backend transit used by SOPS", + }); err != nil { + return fmt.Errorf("failed to mount transit on engine path '%s': %w", enginePath, err) + } + return nil +} + +// createVaultKey creates a new RSA-4096 Vault key using the data from the +// provided MasterKey. +func createVaultKey(key *MasterKey) error { + client, err := vaultClient(key.VaultAddress, key.token) + if err != nil { + return fmt.Errorf("cannot create Vault client: %w", err) + } + + p := path.Join(key.EnginePath, "keys", key.KeyName) + payload := make(map[string]interface{}) + payload["type"] = "rsa-4096" + if _, err = client.Logical().Write(p, payload); err != nil { + return err + } + + _, err = client.Logical().Read(p) + return err }