From 57639813e0058d007464d4b23092127ce6414f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Mon, 13 Feb 2023 19:30:01 -0800 Subject: [PATCH] Extracted version from other code --- LICENSE | 21 +++ go.mod | 31 ++++ go.sum | 90 ++++++++++ vault.go | 478 ++++++++++++++++++++++++++++++++++++++++++++++++++ vault_test.go | 36 ++++ 5 files changed, 656 insertions(+) create mode 100644 LICENSE create mode 100644 go.mod create mode 100644 go.sum create mode 100644 vault.go create mode 100644 vault_test.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b78eaad --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2012 NTP Pool Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5a49aed --- /dev/null +++ b/go.mod @@ -0,0 +1,31 @@ +module go.ntppool.org/vault-token-manager + +go 1.19 + +require github.com/hashicorp/vault/api v1.9.0 + +require ( + github.com/cenkalti/backoff/v3 v3.2.2 // indirect + github.com/fatih/color v1.14.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.4.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.2 // 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/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/stretchr/testify v1.7.2 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/time v0.3.0 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3a061a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,90 @@ +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +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/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= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +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 v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= +github.com/hashicorp/go-hclog v1.4.0/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.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +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.9.0 h1:ab7dI6W8DuCY7yCU8blo0UCYl2oHre/dloCmzMWg9w8= +github.com/hashicorp/vault/api v1.9.0/go.mod h1:lloELQP4EyhjnCQhF8agKvWIVTmxbpEJj70b98959sM= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vault.go b/vault.go new file mode 100644 index 0000000..4d365de --- /dev/null +++ b/vault.go @@ -0,0 +1,478 @@ +package vaulttokenmanager + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "os" + "path" + "strconv" + "sync" + "time" + + vaultapi "github.com/hashicorp/vault/api" +) + +const tokenRefreshInterval = 10 * time.Hour + +type notFoundError struct{} + +func (m *notFoundError) Error() string { + return "token not found" +} + +type token struct { + Secret string `json:"token"` + Created int64 `json:"token-ts"` + version int `json:"-"` +} + +type TokenManager struct { + key string + basePath string + + latest *token + versions map[int]*token + + vault *vaultapi.Client + lock sync.RWMutex +} + +func New(key, depEnv string) (*TokenManager, error) { + + if len(depEnv) == 0 { + return nil, fmt.Errorf("invalid deployment mode parameter %q", depEnv) + } + + var basePath = fmt.Sprintf("kv/data/ntppool/%s/", depEnv) + + cl, err := vaultClient() + if err != nil { + return nil, err + } + + tm := &TokenManager{ + key: key, + basePath: basePath, + vault: cl, + versions: map[int]*token{}, + } + + err = tm.populate() + if err != nil { + return nil, err + } + + // todo: pass context so it can be shutdown + go tm.rotateTokensBackground() + + return tm, nil +} + +func getSignatureVersion(sig []byte) (int, error) { + idx := bytes.IndexByte(sig, '-') + if idx < 1 { + return 0, fmt.Errorf("invalid signature") + } + + versionb := sig[0:idx] + version, err := strconv.Atoi(string(versionb)) + if err != nil || version == 0 { + return 0, fmt.Errorf("unknown signature version %d: %s", version, err) + } + return version, nil +} + +func (tm *TokenManager) Validate(sig []byte, data ...[]byte) (bool, error) { + version, err := getSignatureVersion(sig) + if err != nil || version == 0 { + return false, err + } + + token, err := tm.getTokenVersion(context.Background(), version) + if err != nil { + return false, err + } + + expected, err := tm.signWith(token, data...) + if err != nil { + return false, err + } + if len(expected) > 0 && bytes.Equal(sig, expected) { + return true, nil + } + + log.Printf("exp: %s", expected) + log.Printf("got: %s", sig) + + return false, fmt.Errorf("could not validate signature") +} + +func (tm *TokenManager) Sign(data ...[]byte) ([]byte, error) { + + token, err := tm.getToken(context.Background()) + if err != nil { + return nil, err + } + // log.Printf("got version: %d, token: %s", token.version, token.Secret) + + return tm.signWith(token, data...) +} + +func (tm *TokenManager) signWith(token *token, data ...[]byte) ([]byte, error) { + + hm := hmac.New(sha256.New, []byte(token.Secret)) + + b := bytes.Join(data, []byte("|")) + + p, err := hm.Write([]byte(b)) + if err != nil || p != len(b) { + return nil, fmt.Errorf("hmac error: %s", err) + } + + sha := hm.Sum(nil) + + r := strconv.AppendInt([]byte{}, int64(token.version), 10) + + r = append(r, []byte("-")...) + + shaenc := make([]byte, base64.RawURLEncoding.EncodedLen(len(sha))) + base64.RawURLEncoding.Encode(shaenc, sha) + + r = append(r, shaenc...) + + return r, nil +} + +func (tm *TokenManager) rotateTokensBackground() { + ctx := context.Background() // for when the app has context properly + + l := log.New(os.Stderr, "rotateTokensBackground: ", 0) + + ticker := time.NewTicker(tokenRefreshInterval / 5) + defer ticker.Stop() + + for { + + latest, err := tm.getToken(ctx) + if err != nil { + l.Printf("could not get token: %s", err) + } + + latestTime := time.Unix(latest.Created, 0) + + l.Printf("checking token age, latest is %s old (interval: %s)", time.Since(latestTime), tokenRefreshInterval.String()) + + if age := time.Since(latestTime); age > tokenRefreshInterval { + l.Printf("token age (%s) is more than %s, rotate it", age, tokenRefreshInterval) + tm.createNewToken(ctx, latest.version) + tm.lock.Lock() + tm.latest = nil + tm.lock.Unlock() + tm.getToken(ctx) + l.Printf("finished renewing token") + } + + select { + case <-ctx.Done(): + l.Printf("context done") + return + case <-ticker.C: + } + + } +} + +func (tm *TokenManager) createNewToken(ctx context.Context, cas int) error { + data := map[string]interface{}{ + "data": makeToken(), + "metadata": map[string]interface{}{ + "cas_required": true, + }, + "options": map[string]interface{}{ + "cas": cas, + }, + } + + _, err := tm.vault.Logical().WriteWithContext(ctx, tm.path(), data) + if err != nil { + return err + } + + return nil +} + +func (tm *TokenManager) path() string { + return path.Join(tm.basePath, tm.key) +} + +func (tm *TokenManager) populate() error { + ctx := context.Background() + + t, err := tm.getToken(ctx) + if err != nil { + if _, ok := err.(*notFoundError); !ok { + return err + } + } + + if t == nil { + err := tm.createNewToken(ctx, 0) + if err != nil { + return fmt.Errorf("could not save token: %s", err) + } + + t, err = tm.getToken(ctx) + if err != nil { + return err + } + if t == nil { + return fmt.Errorf("could not find token data") + } + } + + if t != nil { + tm.latest = t + tm.versions[t.version] = t + } + + return nil +} + +func (tm *TokenManager) getTokenVersionCache(ctx context.Context, version int) (*token, error) { + tm.lock.RLock() + defer tm.lock.RUnlock() + + if t, ok := tm.versions[version]; ok { + return t, nil + } + + return nil, nil +} + +func (tm *TokenManager) getTokenVersion(ctx context.Context, version int) (*token, error) { + + token, err := tm.getTokenVersionCache(ctx, version) + if err != nil { + return nil, err + } + if token != nil { + return token, nil + } + + latest, err := tm.getToken(ctx) + if err != nil { + return nil, err + } + + if latest.version < version { + return nil, fmt.Errorf("invalid signature version") + } + + tm.lock.Lock() + defer tm.lock.Unlock() + + // in case it was set while we were waiting for a lock + if token, ok := tm.versions[version]; ok { + return token, nil + } + + log.Printf("requesting token %q/%d", tm.key, version) + rv, err := tm.getKVVersion(ctx, tm.key, version) + if err != nil { + return nil, err + } + + token, err = parseTokenVaultSecret(rv.Data) + if err != nil { + return nil, err + } + + tm.versions[version] = token + + return token, err + +} + +func (tm *TokenManager) getToken(ctx context.Context) (*token, error) { + tm.lock.RLock() + + if token := tm.latest; token != nil { + tm.lock.RUnlock() + return token, nil + } + + log.Printf("getToken didn't have latest token, getting from vault") + + tm.lock.RUnlock() + tm.lock.Lock() + defer tm.lock.Unlock() + + // in case it was set while we were waiting for a lock + if tm.latest != nil { + return tm.latest, nil + } + + log.Printf("getToken calling getKV") + + rv, err := tm.getKV(ctx, tm.key) + if err != nil { + return nil, err + } + if rv == nil { + return nil, ¬FoundError{} + } + + t, err := parseTokenVaultSecret(rv.Data) + if err != nil { + return nil, err + } + + tm.latest = t + tm.versions[t.version] = t + + log.Printf("getToken returning success") + + return t, nil +} + +func parseTokenVaultSecret(data map[string]interface{}) (*token, error) { + t := &token{} + + var err error + + if dataif, ok := data["data"]; ok { + data := dataif.(map[string]interface{}) + + if tokData, ok := data["token"]; ok { + if tokStr, ok := tokData.(string); ok { + t.Secret = tokStr + } + } + + if tokData, ok := data["token-ts"]; ok { + if tokInt, ok := tokData.(json.Number); ok { + t.Created, err = tokInt.Int64() + if t.Created == 0 || err != nil { + log.Printf("could not parse Created from token secret (%T: %+v): %s", tokData, tokData, err) + } + } + } + } + + if metaif, ok := data["metadata"]; ok { + meta := metaif.(map[string]interface{}) + if version, ok := meta["version"]; ok { + if v, ok := version.(json.Number); ok { + v64, err := v.Int64() + if err != nil { + return nil, err + } + t.version = int(v64) + } + } + } + + if t.version == 0 || len(t.Secret) == 0 { + return nil, fmt.Errorf("expected token data not found") + } + + return t, nil +} + +func makeToken() *token { + randomBytes := make([]byte, 16) + _, err := rand.Read(randomBytes) + if err != nil { + return nil + } + return &token{ + Secret: base64.URLEncoding.EncodeToString(randomBytes), + Created: time.Now().Unix(), + } +} + +var hasOutputVaultEnvMessage bool + +func vaultClient() (*vaultapi.Client, error) { + + c := vaultapi.DefaultConfig() + + if c.Address == "https://127.0.0.1:8200" { + c.Address = "https://vault.ntppool.org" + } + + cl, err := vaultapi.NewClient(c) + if err != nil { + return nil, err + } + + // VAULT_TOKEN is read automatically from the environment if set + // so we just try the file here + token, err := os.ReadFile("/vault/secrets/token") + if err == nil { + cl.SetToken(string(token)) + } else { + if !hasOutputVaultEnvMessage { + hasOutputVaultEnvMessage = true + log.Printf("could not read /vault/secrets/token (%s), using VAULT_TOKEN", err) + } + } + + return cl, nil +} + +func (tm *TokenManager) getKV(ctx context.Context, k string) (*vaultapi.Secret, error) { + + cl, err := vaultClient() + if err != nil { + return nil, nil + } + + rv, err := cl.Logical().ReadWithContext(ctx, tm.path()) + if err != nil { + return nil, err + } + + return rv, nil +} + +func (tm *TokenManager) getKVVersion(ctx context.Context, k string, version int) (*vaultapi.Secret, error) { + + cl, err := vaultClient() + if err != nil { + return nil, nil + } + + data := map[string][]string{ + "version": {strconv.Itoa(version)}, + } + + rv, err := cl.Logical().ReadWithDataWithContext(ctx, tm.path(), data) + if err != nil { + return nil, err + } + + return rv, nil +} + +func (tm *TokenManager) SetKV(ctx context.Context, k string, data *vaultapi.Secret) error { + p := tm.path() + cl, err := vaultClient() + if err != nil { + return nil + } + + _, err = cl.Logical().WriteWithContext(ctx, p, data.Data) + if err != nil { + return err + } + + return nil +} diff --git a/vault_test.go b/vault_test.go new file mode 100644 index 0000000..6febcdc --- /dev/null +++ b/vault_test.go @@ -0,0 +1,36 @@ +package vaulttokenmanager + +import ( + "testing" +) + +func TestParseSignature(t *testing.T) { + v, err := getSignatureVersion([]byte("7-gjqkh34gq3i4gqf")) + if err != nil { + t.Log(err) + t.Fail() + } + if v != 7 { + t.Logf("expected 7, got %d", v) + t.Fail() + } +} + +// func TestSignature(t *testing.T) { +// tm, err := New("monitor-token") +// if err != nil { +// t.Log(err) +// t.Fail() +// } + +// batchID := []byte("0000000ABCEJKLFWEF") +// ipb := []byte{108, 61, 56, 35} + +// sig, err := tm.Sign(1, batchID, ipb) +// if err != nil { +// t.Log(err) +// t.Fail() +// } + +// t.Log(sig) +// }