mirror of https://github.com/poanetwork/quorum.git
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:
parent
58673bbb60
commit
25a5fd5ce8
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue