Implement SignHash and make updates to retrieval loops

* Introduce a keyGetter (final name TBD) to get signing keys either from memory or vault
* Update retrieval loops to use keyGetter data structure
This commit is contained in:
chris-j-h 2019-07-18 16:43:40 +01:00
parent 58673bbb60
commit 25a5fd5ce8
3 changed files with 583 additions and 84 deletions

View File

@ -2,7 +2,7 @@ package vault
type hashicorpWalletConfig struct {
Client hashicorpClientConfig
Secrets []hashicorpSecretData
Secrets []hashicorpSecretConfig
}
type hashicorpClientConfig struct {
@ -15,7 +15,7 @@ type hashicorpClientConfig struct {
VaultPollingIntervalSecs int `toml:",omitempty"`
}
type hashicorpSecretData struct {
type hashicorpSecretConfig struct {
AddressSecret string `toml:",omitempty"`
PrivateKeySecret string `toml:",omitempty"`
AddressSecretVersion int `toml:",omitempty"`

View File

@ -6,6 +6,7 @@ import (
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
@ -14,7 +15,9 @@ import (
"github.com/hashicorp/vault/api"
"math/big"
"os"
"sort"
"strconv"
"sync"
"time"
)
@ -30,6 +33,7 @@ type vaultService interface {
open() error
close() error
accounts() []accounts.Account
getKey(acct accounts.Account) (key *ecdsa.PrivateKey, zeroFn func(), err error)
}
func newHashicorpWallet(config hashicorpWalletConfig, updateFeed *event.Feed) (vaultWallet, error) {
@ -42,7 +46,13 @@ func newHashicorpWallet(config hashicorpWalletConfig, updateFeed *event.Feed) (v
return vaultWallet{}, err
}
return vaultWallet{url: url, vault: &hashicorpService{config: config.Client, secrets: config.Secrets}, updateFeed: updateFeed}, nil
w := vaultWallet{
url: url,
vault: newHashicorpService(config),
updateFeed: updateFeed,
}
return w, nil
}
func (w vaultWallet) URL() accounts.URL {
@ -94,7 +104,19 @@ func (w vaultWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Ac
func (w vaultWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {}
func (w vaultWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) {
panic("implement me")
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
}
key, zero, err := w.vault.getKey(account)
if err != nil {
return nil, err
}
defer zero()
return crypto.Sign(hash, key)
}
func (w vaultWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int, isQuorum bool) (*types.Transaction, error) {
@ -112,9 +134,27 @@ func (w vaultWallet) SignTxWithPassphrase(account accounts.Account, passphrase s
type hashicorpService struct {
client *api.Client
config hashicorpClientConfig
secrets []hashicorpSecretData
secrets []hashicorpSecretConfig
mutex sync.RWMutex
accts []accounts.Account
keys []*ecdsa.PrivateKey
keyGetters map[common.Address]map[accounts.URL]hashicorpKeyGetter
}
func newHashicorpService(config hashicorpWalletConfig) *hashicorpService {
s := &hashicorpService{
config: config.Client,
secrets: config.Secrets,
keyGetters: make(map[common.Address]map[accounts.URL]hashicorpKeyGetter),
}
return s
}
// TODO change name as it doesn't actually do any getting
type hashicorpKeyGetter struct {
secret hashicorpSecretConfig
key *ecdsa.PrivateKey
}
const (
@ -245,6 +285,7 @@ func (h *hashicorpService) open() error {
return nil
}
// TODO move account and key retrieval into function
func (h *hashicorpService) accountRetrievalLoop(ticker *time.Ticker) {
for range ticker.C {
if len(h.accts) == len(h.secrets) {
@ -308,72 +349,169 @@ func (h *hashicorpService) accountRetrievalLoop(ticker *time.Ticker) {
URL: parsedUrl,
}
// update state
h.mutex.Lock()
if _, ok := h.keyGetters[acct.Address]; !ok {
h.keyGetters[acct.Address] = make(map[accounts.URL]hashicorpKeyGetter)
}
keyGettersByUrl := h.keyGetters[acct.Address]
if _, ok := keyGettersByUrl[acct.URL]; ok {
log.Warn("Hashicorp Vault key getter already exists. Not updated.", "url", url)
h.mutex.Unlock()
continue
}
keyGettersByUrl[acct.URL] = hashicorpKeyGetter{secret: s}
h.accts = append(h.accts, acct)
h.mutex.Unlock()
}
}
}
func countRetrievedKeys(keyGetters map[common.Address]map[accounts.URL]hashicorpKeyGetter) int {
var n int
for _, kgByUrl := range keyGetters {
for _, kg := range kgByUrl {
if kg.key != nil {
n++
}
}
}
return n
}
func (h *hashicorpService) privateKeyRetrievalLoop(ticker *time.Ticker) {
for range ticker.C {
if len(h.keys) == len(h.secrets) {
h.mutex.RLock()
keyGetters := h.keyGetters
h.mutex.RUnlock()
if countRetrievedKeys(keyGetters) == len(h.secrets) {
ticker.Stop()
return
}
for _, s := range h.secrets {
path := fmt.Sprintf("%s/data/%s", s.SecretEngine, s.PrivateKeySecret)
for addr, byUrl := range keyGetters {
url := fmt.Sprintf("%v/v1/%v?version=%v", h.client.Address(), path, s.PrivateKeySecretVersion)
for u, g := range byUrl {
path := fmt.Sprintf("%s/data/%s", g.secret.SecretEngine, g.secret.PrivateKeySecret)
versionData := make(map[string][]string)
versionData["version"] = []string{strconv.Itoa(s.PrivateKeySecretVersion)}
url := fmt.Sprintf("%v/v1/%v?version=%v", h.client.Address(), path, g.secret.PrivateKeySecretVersion)
// get key from vault
resp, err := h.client.Logical().ReadWithData(path, versionData)
versionData := make(map[string][]string)
versionData["version"] = []string{strconv.Itoa(g.secret.PrivateKeySecretVersion)}
if err != nil {
log.Warn("unable to get secret from Hashicorp Vault", "url", url, "err", err)
continue
// get key from vault
resp, err := h.client.Logical().ReadWithData(path, versionData)
if err != nil {
log.Warn("unable to get secret from Hashicorp Vault", "url", url, "err", err)
continue
}
respData, ok := resp.Data["data"].(map[string]interface{})
if !ok {
log.Warn("Hashicorp Vault response does not contain data", "url", url)
continue
}
if len(respData) != 1 {
log.Warn("only one key/value pair is allowed in each Hashicorp Vault secret", "url", url)
continue
}
// get secret regardless of key in map
var k interface{}
for _, d := range respData {
k = d
}
hex, ok := k.(string)
if !ok {
log.Warn("Hashicorp Vault response data is not in string format", "url", url)
continue
}
// create *ecdsa.PrivateKey
key, err := crypto.HexToECDSA(hex)
if err != nil {
log.Warn("unable to parse data from Hashicorp Vault to *ecdsa.PrivateKey", "url", url, "err", err)
continue
}
h.mutex.Lock()
existing := h.keyGetters[addr][u]
updated := hashicorpKeyGetter{secret: existing.secret, key: key}
h.keyGetters[addr][u] = updated
h.mutex.Unlock()
}
respData, ok := resp.Data["data"].(map[string]interface{})
if !ok {
log.Warn("Hashicorp Vault response does not contain data", "url", url)
continue
}
if len(respData) != 1 {
log.Warn("only one key/value pair is allowed in each Hashicorp Vault secret", "url", url)
continue
}
// get secret regardless of key in map
var k interface{}
for _, d := range respData {
k = d
}
hex, ok := k.(string)
if !ok {
log.Warn("Hashicorp Vault response data is not in string format", "url", url)
continue
}
// create *ecdsa.PrivateKey
key, err := crypto.HexToECDSA(hex)
if err != nil {
log.Warn("unable to parse data from Hashicorp Vault to *ecdsa.PrivateKey", "url", url, "err", err)
continue
}
h.keys = append(h.keys, key)
}
}
}
func (h *hashicorpService) getKeyFromVault(s hashicorpSecretConfig) (*ecdsa.PrivateKey, error) {
path := fmt.Sprintf("%s/data/%s", s.SecretEngine, s.PrivateKeySecret)
url := fmt.Sprintf("%v/v1/%v?version=%v", h.client.Address(), path, s.PrivateKeySecretVersion)
versionData := make(map[string][]string)
versionData["version"] = []string{strconv.Itoa(s.PrivateKeySecretVersion)}
// get key from vault
resp, err := h.client.Logical().ReadWithData(path, versionData)
if err != nil {
// TODO make an error type to be returned
log.Warn("unable to get secret from Hashicorp Vault", "url", url, "err", err)
return nil, nil
}
respData, ok := resp.Data["data"].(map[string]interface{})
if !ok {
log.Warn("Hashicorp Vault response does not contain data", "url", url)
return nil, nil
}
if len(respData) != 1 {
log.Warn("only one key/value pair is allowed in each Hashicorp Vault secret", "url", url)
return nil, nil
}
// get secret regardless of key in map
var k interface{}
for _, d := range respData {
k = d
}
hex, ok := k.(string)
if !ok {
log.Warn("Hashicorp Vault response data is not in string format", "url", url)
return nil, nil
}
// create *ecdsa.PrivateKey
key, err := crypto.HexToECDSA(hex)
if err != nil {
log.Warn("unable to parse data from Hashicorp Vault to *ecdsa.PrivateKey", "url", url, "err", err)
return nil, nil
}
return key, nil
}
func usingApproleAuth(roleID, secretID string) bool {
return roleID != "" && secretID != ""
}
@ -390,3 +528,76 @@ func (h *hashicorpService) accounts() []accounts.Account {
return cpy
}
type accountsByURL []accounts.Account
func (s accountsByURL) Len() int { return len(s) }
func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 }
func (s accountsByURL) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (h *hashicorpService) getKey(acct accounts.Account) (*ecdsa.PrivateKey, func(), error) {
keyGettersByUrl, ok := h.keyGetters[acct.Address]
if !ok {
return nil, nil, accounts.ErrUnknownAccount
}
if (acct.URL == accounts.URL{}) && len(keyGettersByUrl) > 1 {
ambiguousAccounts := []accounts.Account{}
for url, _ := range keyGettersByUrl {
ambiguousAccounts = append(ambiguousAccounts, accounts.Account{Address: acct.Address, URL: url})
}
sort.Sort(accountsByURL(ambiguousAccounts))
err := &keystore.AmbiguousAddrError{
Addr: acct.Address,
Matches: ambiguousAccounts,
}
return nil, nil, err
}
// return the only key for this address
if (acct.URL == accounts.URL{}) && len(keyGettersByUrl) == 1 {
var keyGetter hashicorpKeyGetter
for _, g := range keyGettersByUrl {
keyGetter = g
}
return h.useKeyGetter(keyGetter)
}
keyGetter, ok := keyGettersByUrl[acct.URL]
if !ok {
return nil, nil, accounts.ErrUnknownAccount
}
return h.useKeyGetter(keyGetter)
}
func (h *hashicorpService) useKeyGetter(getter hashicorpKeyGetter) (*ecdsa.PrivateKey, func(), error) {
if key := getter.key; key != nil {
return key, func(){}, nil
}
key, err := h.getKeyFromVault(getter.secret)
if err != nil {
return nil, nil, err
}
// zeroFn zeroes the retrieved private key
zeroFn := func () {
b := key.D.Bits()
for i := range b {
b[i] = 0
}
key = nil
}
return key, zeroFn, nil
}

View File

@ -1,12 +1,14 @@
package vault
import (
"bytes"
"crypto/ecdsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/event"
@ -19,6 +21,7 @@ import (
"os"
"reflect"
"sort"
"sync"
"testing"
"time"
)
@ -568,8 +571,8 @@ func TestVaultWallet_Open_Hashicorp_AccountsRetrieved(t *testing.T) {
return b
}
makeSecret := func(name string) hashicorpSecretData {
return hashicorpSecretData{AddressSecret: name, AddressSecretVersion: 1, SecretEngine: "kv"}
makeSecret := func(name string) hashicorpSecretConfig {
return hashicorpSecretConfig{AddressSecret: name, AddressSecretVersion: 1, SecretEngine: "kv"}
}
const (
@ -610,27 +613,27 @@ func TestVaultWallet_Open_Hashicorp_AccountsRetrieved(t *testing.T) {
defer vaultServer.Close()
tests := map[string]struct{
secrets []hashicorpSecretData
secrets []hashicorpSecretConfig
wantAccts []accounts.Account
}{
"account retrieved": {
secrets: []hashicorpSecretData{makeSecret(secret1)},
secrets: []hashicorpSecretConfig{makeSecret(secret1)},
wantAccts: []accounts.Account{
{Address: common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")},
},
},
"account not retrieved when vault secret has multiple values": {
secrets: []hashicorpSecretData{makeSecret(multiValSecret)},
secrets: []hashicorpSecretConfig{makeSecret(multiValSecret)},
wantAccts: []accounts.Account{},
},
"unretrievable accounts are ignored": {
secrets: []hashicorpSecretData{makeSecret(multiValSecret), makeSecret(secret1)},
secrets: []hashicorpSecretConfig{makeSecret(multiValSecret), makeSecret(secret1)},
wantAccts: []accounts.Account{
{Address: common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")},
},
},
"accounts retrieved regardless of vault secrets keyvalue key": {
secrets: []hashicorpSecretData{makeSecret(secret1), makeSecret(secret2)},
secrets: []hashicorpSecretConfig{makeSecret(secret1), makeSecret(secret2)},
wantAccts: []accounts.Account{
{Address: common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")},
{Address: common.HexToAddress("ca843569e3427144cead5e4d5999a3d0ccf92b8e")},
@ -704,7 +707,7 @@ func TestVaultWallet_Open_Hashicorp_AccountsRetrievedWhenVaultAvailable(t *testi
Url: "http://incorrecturl:1",
VaultPollingIntervalSecs: 1,
},
Secrets: []hashicorpSecretData{
Secrets: []hashicorpSecretConfig{
{AddressSecret: "sec1", AddressSecretVersion: 1, SecretEngine: "kv"},
},
}
@ -782,6 +785,8 @@ func keysEqual(a, b []*ecdsa.PrivateKey) bool {
return true
}
// TODO use milliseconds instead of seconds as duration unit for retrieval loops to make these tests quicker to run by setting the retrieval period to 1ms
// TODO This is a long running test (~8secs) so perhaps should be excluded from test suite by default?
func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabled(t *testing.T) {
if err := os.Setenv(api.EnvVaultToken, "mytoken"); err != nil {
@ -804,8 +809,8 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabled(t *testing.T
return b
}
makeSecret := func(name string) hashicorpSecretData {
return hashicorpSecretData{PrivateKeySecret: name, PrivateKeySecretVersion: 1, SecretEngine: "kv"}
makeSecret := func(addrName, keyName string) hashicorpSecretConfig {
return hashicorpSecretConfig{AddressSecret: addrName, AddressSecretVersion: 1, PrivateKeySecret: keyName, PrivateKeySecretVersion: 1, SecretEngine: "kv"}
}
makeKey := func(hex string) *ecdsa.PrivateKey {
@ -820,14 +825,24 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabled(t *testing.T
const (
secretEngine = "kv"
secret1 = "sec1"
secret2 = "sec2"
key1 = "key1"
key2 = "key2"
addr1 = "addr1"
addr2 = "addr2"
multiValSecret = "multiValSec"
)
mux := http.NewServeMux()
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, secret1), func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, addr1), func(w http.ResponseWriter, r *http.Request) {
body := makeVaultResponse(map[string]string{
"addr": "ed9d02e382b34818e88b88a309c7fe71e65f419d",
})
w.Write(body)
})
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, key1), func(w http.ResponseWriter, r *http.Request) {
body := makeVaultResponse(map[string]string{
"key": "e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1",
})
@ -835,7 +850,15 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabled(t *testing.T
w.Write(body)
})
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, secret2), func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, addr2), func(w http.ResponseWriter, r *http.Request) {
body := makeVaultResponse(map[string]string{
"addr": "ca843569e3427144cead5e4d5999a3d0ccf92b8e",
})
w.Write(body)
})
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, key2), func(w http.ResponseWriter, r *http.Request) {
body := makeVaultResponse(map[string]string{
"otherKey": "4762e04d10832808a0aebdaa79c12de54afbe006bfffd228b3abcc494fe986f9",
})
@ -856,27 +879,27 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabled(t *testing.T
defer vaultServer.Close()
tests := map[string]struct{
secrets []hashicorpSecretData
secrets []hashicorpSecretConfig
wantKeys []*ecdsa.PrivateKey
}{
"key retrieved": {
secrets: []hashicorpSecretData{makeSecret(secret1)},
secrets: []hashicorpSecretConfig{makeSecret(addr1, key1)},
wantKeys: []*ecdsa.PrivateKey{
makeKey("e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1"),
},
},
"key not retrieved when vault secret has multiple values": {
secrets: []hashicorpSecretData{makeSecret(multiValSecret)},
secrets: []hashicorpSecretConfig{makeSecret(addr1, multiValSecret)},
wantKeys: []*ecdsa.PrivateKey{},
},
"unretrievable keys are ignored": {
secrets: []hashicorpSecretData{makeSecret(multiValSecret), makeSecret(secret1)},
secrets: []hashicorpSecretConfig{makeSecret(addr1, multiValSecret), makeSecret(addr2, key2)},
wantKeys: []*ecdsa.PrivateKey{
makeKey("e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1"),
makeKey("4762e04d10832808a0aebdaa79c12de54afbe006bfffd228b3abcc494fe986f9"),
},
},
"keys retrieved regardless of vault secrets keyvalue key": {
secrets: []hashicorpSecretData{makeSecret(secret1), makeSecret(secret2)},
secrets: []hashicorpSecretConfig{makeSecret(addr1, key1), makeSecret(addr2, key2)},
wantKeys: []*ecdsa.PrivateKey{
makeKey("e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1"), makeKey("4762e04d10832808a0aebdaa79c12de54afbe006bfffd228b3abcc494fe986f9"),
},
@ -906,9 +929,11 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabled(t *testing.T
// need to block to let accountRetrievalLoop do its thing
// a long sleep is used here to give the vault client time to make its request to the vault and wait for the response before the go scheduler returns focus to this test
time.Sleep(2 * time.Second)
time.Sleep(4 * time.Second)
gotKeys := w.vault.(*hashicorpService).keys
keyGetters := w.vault.(*hashicorpService).keyGetters
gotKeys := getRetrievedKeys(keyGetters)
if !keysEqual(tt.wantKeys, gotKeys) {
t.Fatalf("keys in vaultService do not equal wanted keys\nwant: %v\ngot : %v", tt.wantKeys, gotKeys)
@ -917,6 +942,20 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabled(t *testing.T
}
}
func getRetrievedKeys(keyGetters map[common.Address]map[accounts.URL]hashicorpKeyGetter) []*ecdsa.PrivateKey {
gotKeys := []*ecdsa.PrivateKey{}
for _, g := range keyGetters {
for _, gg := range g {
if gg.key != nil {
gotKeys = append(gotKeys, gg.key)
}
}
}
return gotKeys
}
// TODO This is a long running test (~20secs) so perhaps should be excluded from test suite by default?
func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabledAndVaultAvailable(t *testing.T) {
if err := os.Setenv(api.EnvVaultToken, "mytoken"); err != nil {
@ -972,7 +1011,7 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabledAndVaultAvail
VaultPollingIntervalSecs: 1,
StorePrivateKeys: tt.storePrivateKeys,
},
Secrets: []hashicorpSecretData{
Secrets: []hashicorpSecretConfig{
{PrivateKeySecret: "sec1", PrivateKeySecretVersion: 1, SecretEngine: "kv"},
},
}
@ -993,8 +1032,10 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabledAndVaultAvail
v := w.vault.(*hashicorpService)
if len(v.keys) != 0 {
t.Fatalf("vaultService should have no keys as vault server is inaccessible: got: %v", v.keys)
gotKeys := getRetrievedKeys(v.keyGetters)
if len(gotKeys) != 0 {
t.Fatalf("vaultService should have no keys as vault server is inaccessible: got: %v", gotKeys)
}
// update vault client to use correct url to simulate vault becoming accessible
@ -1006,8 +1047,10 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabledAndVaultAvail
// a long sleep is used here to give the vault client time to make its request to the vault and wait for the response before the go scheduler returns focus to this test
time.Sleep(5 * time.Second)
if !keysEqual(tt.wantKeys, v.keys) {
t.Fatalf("keys in vaultService do not equal wanted keys\nwant: %v\ngot : %v", tt.wantKeys, v.keys)
gotKeys = getRetrievedKeys(v.keyGetters)
if !keysEqual(tt.wantKeys, gotKeys) {
t.Fatalf("keys in vaultService do not equal wanted keys\nwant: %v\ngot : %v", tt.wantKeys, gotKeys)
}
})
}
@ -1034,8 +1077,8 @@ func TestVaultWallet_Open_Hashicorp_RetrievalLoopsStopWhenAllSecretsRetrieved(t
return b
}
makeSecret := func(addrName, keyName string) hashicorpSecretData {
return hashicorpSecretData{AddressSecret: addrName, AddressSecretVersion: 1, PrivateKeySecret: keyName, PrivateKeySecretVersion: 1, SecretEngine: "kv"}
makeSecret := func(addrName, keyName string) hashicorpSecretConfig {
return hashicorpSecretConfig{AddressSecret: addrName, AddressSecretVersion: 1, PrivateKeySecret: keyName, PrivateKeySecretVersion: 1, SecretEngine: "kv"}
}
const (
@ -1077,7 +1120,7 @@ func TestVaultWallet_Open_Hashicorp_RetrievalLoopsStopWhenAllSecretsRetrieved(t
VaultPollingIntervalSecs: 1,
StorePrivateKeys: true,
},
Secrets: []hashicorpSecretData{makeSecret(addrName, keyName)},
Secrets: []hashicorpSecretConfig{makeSecret(addrName, keyName)},
}
w, err := newHashicorpWallet(wltConfig, &event.Feed{})
@ -1107,7 +1150,7 @@ func TestVaultWallet_Close_Hashicorp_ReturnsStateToBeforeOpen(t *testing.T) {
config := hashicorpWalletConfig{
Client: hashicorpClientConfig{Url: "http://url:1"},
Secrets: []hashicorpSecretData{{AddressSecret: "addr1"}},
Secrets: []hashicorpSecretConfig{{AddressSecret: "addr1"}},
}
w, err := newHashicorpWallet(config, &event.Feed{})
@ -1122,7 +1165,10 @@ func TestVaultWallet_Close_Hashicorp_ReturnsStateToBeforeOpen(t *testing.T) {
t.Fatal(err)
}
cmpOpts := []cmp.Option{cmp.AllowUnexported(vaultWallet{}, hashicorpService{}), cmpopts.IgnoreUnexported(event.Feed{})}
cmpOpts := []cmp.Option{
cmp.AllowUnexported(vaultWallet{}, hashicorpService{}),
cmpopts.IgnoreUnexported(event.Feed{}, sync.RWMutex{}),
}
if diff := cmp.Diff(unopened, w, cmpOpts...); diff != "" {
t.Fatalf("cmp does not consider the two wallets equal\n%v", diff)
@ -1210,3 +1256,245 @@ func TestVaultWallet_Contains(t *testing.T) {
})
}
}
func TestVaultWallet_SignHash_Hashicorp_ErrorIfAccountNotKnown(t *testing.T) {
w := vaultWallet{
vault: &hashicorpService{
accts: []accounts.Account{},
},
}
acct := accounts.Account{Address: common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")}
toSign := crypto.Keccak256([]byte("to_sign"))
if _, err := w.SignHash(acct, toSign); err != accounts.ErrUnknownAccount {
t.Fatalf("incorrect error returned:\nwant: %v\ngot : %v", accounts.ErrUnknownAccount, err)
}
}
func TestVaultWallet_SignHash_Hashicorp_SignsWithInMemoryKeyIfAvailableAndDoesNotZeroKey(t *testing.T) {
addr := common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")
url := accounts.URL{Scheme: "http", Path: "url:1"}
acct := accounts.Account{
Address: addr,
URL: url,
}
key, err := crypto.HexToECDSA("e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1")
if err != nil {
t.Fatal(err)
}
w := vaultWallet{
vault: &hashicorpService{
accts: []accounts.Account{acct},
keyGetters: map[common.Address]map[accounts.URL]hashicorpKeyGetter{
addr: {
url: hashicorpKeyGetter{key: key},
},
},
},
}
toSign := crypto.Keccak256([]byte("to_sign"))
got, err := w.SignHash(acct, toSign)
if err != nil {
t.Fatalf("error signing hash: %v", err)
}
want, err := crypto.Sign(toSign, key)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(want, got) {
t.Fatalf("incorrect signHash result:\nwant: %v\ngot : %v", want, got)
}
vaultServiceKey := w.vault.(*hashicorpService).keyGetters[acct.Address][acct.URL].key
if vaultServiceKey == nil || vaultServiceKey.D.Int64() == 0 {
t.Fatal("unlocked key was zeroed after use")
}
}
func TestVaultWallet_SignHash_Hashicorp_ErrorIfAmbiguousAccount(t *testing.T) {
addr := common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")
url1 := accounts.URL{Scheme: "http", Path: "url:1"}
url2 := accounts.URL{Scheme: "http", Path: "url:2"}
acct1 := accounts.Account{Address: addr, URL: url1}
acct2 := accounts.Account{Address: addr, URL: url2}
// Two accounts have the same address but different URLs
w := vaultWallet{
vault: &hashicorpService{
accts: []accounts.Account{acct1, acct2},
keyGetters: map[common.Address]map[accounts.URL]hashicorpKeyGetter{
addr: {
url1: hashicorpKeyGetter{},
url2: hashicorpKeyGetter{},
},
},
},
}
toSign := crypto.Keccak256([]byte("to_sign"))
// The provided account does not specify the exact account to use as no URL is provided
acct := accounts.Account{
Address: addr,
}
_, err := w.SignHash(acct, toSign)
e := err.(*keystore.AmbiguousAddrError)
want := []accounts.Account{acct1, acct2}
if diff := cmp.Diff(want, e.Matches); diff != "" {
t.Fatalf("ambiguous accounts mismatch (-want +got):\n%s", diff)
}
}
func TestVaultWallet_SignHash_Hashicorp_AmbiguousAccountAllowedIfOnlyOneAccountWithGivenAddress(t *testing.T) {
addr := common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")
url := accounts.URL{Scheme: "http", Path: "url:1"}
acct1 := accounts.Account{Address: addr, URL: url}
key, err := crypto.HexToECDSA("e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1")
if err != nil {
t.Fatal(err)
}
w := vaultWallet{
vault: &hashicorpService{
accts: []accounts.Account{acct1},
keyGetters: map[common.Address]map[accounts.URL]hashicorpKeyGetter{
addr: {
url: hashicorpKeyGetter{key: key},
},
},
},
}
toSign := crypto.Keccak256([]byte("to_sign"))
// The provided account does not specify the exact account to use as no URL is provided
acct := accounts.Account{
Address: addr,
}
got, err := w.SignHash(acct, toSign)
if err != nil {
t.Fatalf("error signing hash: %v", err)
}
want, err := crypto.Sign(toSign, key)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(want, got) {
t.Fatalf("incorrect signHash result:\nwant: %v\ngot : %v", want, got)
}
vaultServiceKeyGetters := w.vault.(*hashicorpService).keyGetters[acct.Address]
var vaultServiceKey *ecdsa.PrivateKey
for _, g := range vaultServiceKeyGetters {
vaultServiceKey = g.key
if vaultServiceKey == nil || vaultServiceKey.D.Int64() == 0 {
t.Fatal("unlocked key was zeroed after use")
}
}
}
func TestVaultWallet_SignHash_Hashicorp_SignsWithKeyFromVaultAndDoesNotStoreInMemory(t *testing.T) {
makeMockHashicorpResponse := func(t *testing.T, hexKey string) []byte {
var vaultResponse api.Secret
vaultResponse.Data = map[string]interface{}{
"data": map[string]interface{}{
"key": hexKey,
},
}
b, err := json.Marshal(vaultResponse)
if err != nil {
t.Fatalf("err marshalling mock response: %v", err)
}
return b
}
acct := accounts.Account{
Address: common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d"),
URL: accounts.URL{Scheme: "http", Path: "url:1"},
}
hexKey := "e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1"
key, err := crypto.HexToECDSA(hexKey)
if err != nil {
t.Fatal(err)
}
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
}
w := vaultWallet{
vault: &hashicorpService{
client: client,
accts: []accounts.Account{acct},
keyGetters: map[common.Address]map[accounts.URL]hashicorpKeyGetter{
acct.Address: {
acct.URL: {
secret: secret,
},
},
},
},
}
toSign := crypto.Keccak256([]byte("to_sign"))
got, err := w.SignHash(acct, toSign)
if err != nil {
t.Fatalf("error signing hash: %v", err)
}
want, err := crypto.Sign(toSign, key)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(want, got) {
t.Fatalf("incorrect signHash result:\nwant: %v\ngot : %v", want, got)
}
vaultServiceKey := w.vault.(*hashicorpService).keyGetters[acct.Address][acct.URL].key
if vaultServiceKey != nil {
t.Fatal("unlocked key should not be stored after use")
}
}