Add Store to VaultWallet & create new accts through the geth CLI

* Implement Store for VaultWallet and use in CLI account creation

* Define Hashicorp Vault CLI flags

* Update account new usage with Vault CLI flags

* Export Vault Client and Secret config types
This commit is contained in:
chris-j-h 2019-08-11 14:34:40 +01:00
parent 002fc84f2c
commit e68ef0a561
10 changed files with 471 additions and 82 deletions

View File

@ -7,11 +7,11 @@ import (
)
type HashicorpWalletConfig struct {
Client hashicorpClientConfig
Secrets []hashicorpSecretConfig
Client HashicorpClientConfig
Secrets []HashicorpSecretConfig
}
type hashicorpClientConfig struct {
type HashicorpClientConfig struct {
Url string `toml:",omitempty"`
Approle string `toml:",omitempty"`
CaCert string `toml:",omitempty"`
@ -21,7 +21,7 @@ type hashicorpClientConfig struct {
VaultPollingIntervalMillis int `toml:",omitempty"`
}
type hashicorpSecretConfig struct {
type HashicorpSecretConfig struct {
AddressSecret string `toml:",omitempty"`
PrivateKeySecret string `toml:",omitempty"`
AddressSecretVersion int `toml:",omitempty"`

View File

@ -4,10 +4,10 @@ import "testing"
func getMinimumValidConfig() HashicorpWalletConfig {
return HashicorpWalletConfig{
Client: hashicorpClientConfig{
Client: HashicorpClientConfig{
Url: "someurl",
},
Secrets: []hashicorpSecretConfig{
Secrets: []HashicorpSecretConfig{
{
AddressSecret: "addr",
PrivateKeySecret: "key",

View File

@ -12,10 +12,10 @@ func TestNewHashicorpBackend_CreatesWalletsWithUrlsFromConfig(t *testing.T) {
makeConfs := func (url string, urls... string) []HashicorpWalletConfig {
var confs []HashicorpWalletConfig
confs = append(confs, HashicorpWalletConfig{Client: hashicorpClientConfig{Url: url}})
confs = append(confs, HashicorpWalletConfig{Client: HashicorpClientConfig{Url: url}})
for _, u := range urls {
confs = append(confs, HashicorpWalletConfig{Client: hashicorpClientConfig{Url: u}})
confs = append(confs, HashicorpWalletConfig{Client: HashicorpClientConfig{Url: u}})
}
return confs
@ -137,7 +137,7 @@ func TestVaultBackend_Subscribe_SubscriberReceivesEventsAddedToFeed(t *testing.T
}
func TestVaultBackend_Subscribe_SubscriberReceivesEventsAddedToFeedByHashicorpWallet(t *testing.T) {
conf := HashicorpWalletConfig{Client: hashicorpClientConfig{Url: "http://url:1"}}
conf := HashicorpWalletConfig{Client: HashicorpClientConfig{Url: "http://url:1"}}
b := NewHashicorpBackend([]HashicorpWalletConfig{conf})
if len(b.wallets) != 1 {

View File

@ -2,6 +2,9 @@ package vault
import (
"crypto/ecdsa"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/ethereum/go-ethereum"
@ -17,6 +20,7 @@ import (
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
)
@ -36,6 +40,7 @@ type vaultService interface {
getKey(acct accounts.Account) (key *ecdsa.PrivateKey, zeroFn func(), err error)
timedUnlock(acct accounts.Account, timeout time.Duration) error
lock(acct accounts.Account) error
writeSecret(name, value, secretEngine string) (path string, version int64, err error)
}
func newHashicorpWallet(config HashicorpWalletConfig, updateFeed *event.Feed) (VaultWallet, error) {
@ -171,10 +176,39 @@ func (w VaultWallet) Lock(account accounts.Account) error {
return w.vault.lock(account)
}
// Store writes the provided private key to the vault. The hex string values of the key and address are stored in the locations specified by config.
// TODO write tests
func (w *VaultWallet) Store(key *ecdsa.PrivateKey, config HashicorpSecretConfig) (common.Address, []string, error) {
address := crypto.PubkeyToAddress(key.PublicKey)
// TODO check if this trim behaviour is in filesystem account creation
addrHex := strings.TrimPrefix(address.Hex(), "0x")
addrPath, addrVersion, err := w.vault.writeSecret(config.AddressSecret, addrHex, config.SecretEngine)
if err != nil {
return common.Address{}, nil, fmt.Errorf("unable to store address: %v", err.Error())
}
addrSecretUrl := fmt.Sprintf("%v/v1/%v?version=%v", w.url, addrPath, addrVersion)
keyBytes := crypto.FromECDSA(key)
keyHex := hex.EncodeToString(keyBytes)
keyPath, keyVersion, err := w.vault.writeSecret(config.PrivateKeySecret, keyHex, config.SecretEngine)
if err != nil {
return common.Address{}, nil, fmt.Errorf("unable to store key: %v", err.Error())
}
keySecretUrl := fmt.Sprintf("%v/v1/%v?version=%v", w.url, keyPath, keyVersion)
return address, []string{addrSecretUrl, keySecretUrl}, nil
}
type hashicorpService struct {
client *api.Client
config hashicorpClientConfig
secrets []hashicorpSecretConfig
config HashicorpClientConfig
secrets []HashicorpSecretConfig
mutex sync.RWMutex
accts []accounts.Account
keyHandlers map[common.Address]map[accounts.URL]*hashicorpKeyHandler
@ -192,9 +226,9 @@ func newHashicorpService(config HashicorpWalletConfig) *hashicorpService {
}
type hashicorpKeyHandler struct {
secret hashicorpSecretConfig
mutex sync.RWMutex
key *ecdsa.PrivateKey
secret HashicorpSecretConfig
mutex sync.RWMutex
key *ecdsa.PrivateKey
cancel chan struct{}
}
@ -242,13 +276,13 @@ func (h *hashicorpService) status() (string, error) {
}
const (
roleIDEnv = "VAULT_ROLE_ID"
secretIDEnv = "VAULT_SECRET_ID"
RoleIDEnv = "VAULT_ROLE_ID"
SecretIDEnv = "VAULT_SECRET_ID"
)
var (
noHashicorpEnvSetErr = fmt.Errorf("environment variables must be set when creating the Hashicorp client. Set %v and %v if the Vault is configured to use Approle authentication. Else set %v", roleIDEnv, secretIDEnv, api.EnvVaultToken)
invalidApproleAuthErr = fmt.Errorf("both %v and %v must be set if using Approle authentication", roleIDEnv, secretIDEnv)
noHashicorpEnvSetErr = fmt.Errorf("environment variables must be set when creating the Hashicorp client. Set %v and %v if the Vault is configured to use Approle authentication. Else set %v", RoleIDEnv, SecretIDEnv, api.EnvVaultToken)
invalidApproleAuthErr = fmt.Errorf("both %v and %v must be set if using Approle authentication", RoleIDEnv, SecretIDEnv)
)
func (h *hashicorpService) open() error {
@ -275,8 +309,8 @@ func (h *hashicorpService) open() error {
return fmt.Errorf("error creating Hashicorp client: %v", err)
}
roleID := os.Getenv(roleIDEnv)
secretID := os.Getenv(secretIDEnv)
roleID := os.Getenv(RoleIDEnv)
secretID := os.Getenv(SecretIDEnv)
if roleID == "" && secretID == "" && os.Getenv(api.EnvVaultToken) == "" {
return noHashicorpEnvSetErr
@ -383,7 +417,7 @@ func (h *hashicorpService) accountRetrievalLoop(ticker *time.Ticker) {
}
}
func (h *hashicorpService) getAddressFromVault(s hashicorpSecretConfig) (common.Address, error) {
func (h *hashicorpService) getAddressFromVault(s HashicorpSecretConfig) (common.Address, error) {
hexAddr, err := h.getSecretFromVault(s.AddressSecret, s.AddressSecretVersion, s.SecretEngine)
if err != nil {
@ -438,7 +472,7 @@ func (h *hashicorpService) privateKeyRetrievalLoop(ticker *time.Ticker) {
}
}
func (h *hashicorpService) getKeyFromVault(s hashicorpSecretConfig) (*ecdsa.PrivateKey, error) {
func (h *hashicorpService) getKeyFromVault(s HashicorpSecretConfig) (*ecdsa.PrivateKey, error) {
hexKey, err := h.getSecretFromVault(s.PrivateKeySecret, s.PrivateKeySecretVersion, s.SecretEngine)
if err != nil {
@ -671,6 +705,38 @@ func (h *hashicorpService) lock(acct accounts.Account) error {
return nil
}
// Even if error is returned, data might have been written to Vault. path and version may contain useful information even in the case of an error. version = -1 indicates no version was retrieved from the Vault (Vault version numbers are >= 0)
func (h *hashicorpService) writeSecret(name, value, secretEngine string) (string, int64, error) {
path := fmt.Sprintf("%s/data/%s", secretEngine, name)
data := make(map[string]interface{})
data["data"] = map[string]interface{}{
"secret": value,
}
resp, err := h.client.Logical().Write(path, data)
if err != nil {
return "", -1, fmt.Errorf("unable to write secret to vault: %v", err)
}
v, ok := resp.Data["version"]
if !ok {
v = json.Number("-1")
}
vJson, ok := v.(json.Number)
vInt, err := vJson.Int64()
if err != nil {
return path, -1, fmt.Errorf("unable to convert version in Vault response to int64: version number is %v", vJson.String())
}
return path, vInt, nil
}
func (h *hashicorpKeyHandler) timedLock(duration time.Duration) {
h.mutex.Lock()
defer h.mutex.Unlock()
@ -690,3 +756,60 @@ func (h *hashicorpKeyHandler) timedLock(duration time.Duration) {
//do nothing
}
}
// zeroKey zeroes a private key in memory
// TODO use where appropriate
func zeroKey(k *ecdsa.PrivateKey) {
b := k.D.Bits()
for i := range b {
b[i] = 0
}
}
// CreateAccount generates a secp256k1 key and corresponding Geth address and stored both in the Vault defined in the provided config.
// The key and address are stored in hex string format.
//
// The generated key and address will be saved to only the first HashicorpSecretConfig provided. Any other secret configs are ignored.
func CreateAccount(config HashicorpWalletConfig) (common.Address, []string, error) {
w, err := newHashicorpWallet(config, &event.Feed{})
if err != nil {
return common.Address{}, nil, err
}
err = w.Open("")
if err != nil {
return common.Address{}, nil, err
}
if status, err := w.Status(); err != nil {
return common.Address{}, nil, err
} else if status != open {
return common.Address{}, nil, fmt.Errorf("error creating Vault client, %v", status)
}
key, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
if err != nil {
return common.Address{}, nil, err
}
defer zeroKey(key)
// This gets tricky as an error while storing the key would occur after the addr has already been stored. The user should be made aware of this as data has been stored in the vault, so even if an error is returned address and secretInfo may still be populated. We also need to close the wallet so do not return straight away in the case of an error.
var errMsgs []string
address, secretInfo, err := w.Store(key, config.Secrets[0])
if err != nil {
errMsgs = append(errMsgs, err.Error())
}
if err := w.Close(); err != nil {
errMsgs = append(errMsgs, fmt.Sprintf("unable to close Hashicorp Vault wallet: %v", err))
}
if len(errMsgs) > 0 {
return address, secretInfo, fmt.Errorf(strings.Join(errMsgs, "\n"))
}
return address, secretInfo, nil
}

View File

@ -3,8 +3,10 @@ package vault
import (
"bytes"
"crypto/ecdsa"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/accounts"
@ -23,6 +25,7 @@ import (
"os"
"reflect"
"sort"
"strings"
"sync"
"testing"
"time"
@ -181,7 +184,7 @@ func TestVaultWallet_Open_Hashicorp_CreatesClientUsingConfig(t *testing.T) {
}))
defer vaultServer.Close()
config := hashicorpClientConfig{
config := HashicorpClientConfig{
Url: vaultServer.URL,
}
@ -277,7 +280,7 @@ func TestVaultWallet_Open_Hashicorp_CreatesTLSClientUsingConfig(t *testing.T) {
defer vaultServer.Close()
// create wallet with config and open
config := hashicorpClientConfig{
config := HashicorpClientConfig{
Url: vaultServer.URL,
CaCert: "testdata/caRoot.pem",
ClientCert: "testdata/quorum-client-chain.pem",
@ -331,10 +334,10 @@ func TestVaultWallet_Open_Hashicorp_ClientAuthenticatesUsingEnvVars(t *testing.T
switch env {
case api.EnvVaultToken:
setAndHandleErrors(t, api.EnvVaultToken, myToken)
case roleIDEnv:
setAndHandleErrors(t, roleIDEnv, myRoleId)
case secretIDEnv:
setAndHandleErrors(t, secretIDEnv, mySecretId)
case RoleIDEnv:
setAndHandleErrors(t, RoleIDEnv, myRoleId)
case SecretIDEnv:
setAndHandleErrors(t, SecretIDEnv, mySecretId)
}
}
@ -370,8 +373,8 @@ func TestVaultWallet_Open_Hashicorp_ClientAuthenticatesUsingEnvVars(t *testing.T
wantToken string
}{
"token auth": {envVars: []string{api.EnvVaultToken}, wantToken: myToken},
"default approle auth": {envVars: []string{roleIDEnv, secretIDEnv}, wantToken: myApproleToken},
"custom approle auth": {envVars: []string{roleIDEnv, secretIDEnv}, approle: "nondefault", wantToken: myApproleToken},
"default approle auth": {envVars: []string{RoleIDEnv, SecretIDEnv}, wantToken: myApproleToken},
"custom approle auth": {envVars: []string{RoleIDEnv, SecretIDEnv}, approle: "nondefault", wantToken: myApproleToken},
}
for name, tt := range tests {
@ -386,7 +389,7 @@ func TestVaultWallet_Open_Hashicorp_ClientAuthenticatesUsingEnvVars(t *testing.T
vaultServer, cleanup := makeMockApproleVaultServer(t, tt.approle)
defer cleanup()
config := hashicorpClientConfig{
config := HashicorpClientConfig{
Url: vaultServer.URL,
Approle: tt.approle,
}
@ -434,10 +437,10 @@ func TestVaultWallet_Open_Hashicorp_ErrAuthenticatingClient(t *testing.T) {
switch env {
case api.EnvVaultToken:
setAndHandleErrors(t, api.EnvVaultToken, myToken)
case roleIDEnv:
setAndHandleErrors(t, roleIDEnv, myRoleId)
case secretIDEnv:
setAndHandleErrors(t, secretIDEnv, mySecretId)
case RoleIDEnv:
setAndHandleErrors(t, RoleIDEnv, myRoleId)
case SecretIDEnv:
setAndHandleErrors(t, SecretIDEnv, mySecretId)
}
}
@ -446,10 +449,10 @@ func TestVaultWallet_Open_Hashicorp_ErrAuthenticatingClient(t *testing.T) {
want error
}{
"no auth provided": {envVars: []string{}, want: noHashicorpEnvSetErr},
"only role id": {envVars: []string{roleIDEnv}, want: invalidApproleAuthErr},
"only secret id": {envVars: []string{secretIDEnv}, want: invalidApproleAuthErr},
"role id and token": {envVars: []string{api.EnvVaultToken, roleIDEnv}, want: invalidApproleAuthErr},
"secret id and token": {envVars: []string{api.EnvVaultToken, secretIDEnv}, want: invalidApproleAuthErr},
"only role id": {envVars: []string{RoleIDEnv}, want: invalidApproleAuthErr},
"only secret id": {envVars: []string{SecretIDEnv}, want: invalidApproleAuthErr},
"role id and token": {envVars: []string{api.EnvVaultToken, RoleIDEnv}, want: invalidApproleAuthErr},
"secret id and token": {envVars: []string{api.EnvVaultToken, SecretIDEnv}, want: invalidApproleAuthErr},
}
for name, tt := range tests {
@ -461,7 +464,7 @@ func TestVaultWallet_Open_Hashicorp_ErrAuthenticatingClient(t *testing.T) {
defer os.Unsetenv(e)
}
config := hashicorpClientConfig{
config := HashicorpClientConfig{
Url: "http://url:1",
}
@ -481,7 +484,7 @@ func TestVaultWallet_Open_Hashicorp_SendsEventToBackendSubscribers(t *testing.T)
}
walletConfig := HashicorpWalletConfig{
Client: hashicorpClientConfig{
Client: HashicorpClientConfig{
Url: "http://url:1",
},
}
@ -572,8 +575,8 @@ func TestVaultWallet_Open_Hashicorp_AccountsRetrieved(t *testing.T) {
return b
}
makeSecret := func(name string) hashicorpSecretConfig {
return hashicorpSecretConfig{AddressSecret: name, AddressSecretVersion: 1, SecretEngine: "kv"}
makeSecret := func(name string) HashicorpSecretConfig {
return HashicorpSecretConfig{AddressSecret: name, AddressSecretVersion: 1, SecretEngine: "kv"}
}
const (
@ -614,27 +617,27 @@ func TestVaultWallet_Open_Hashicorp_AccountsRetrieved(t *testing.T) {
defer vaultServer.Close()
tests := map[string]struct{
secrets []hashicorpSecretConfig
secrets []HashicorpSecretConfig
wantAccts []accounts.Account
}{
"account retrieved": {
secrets: []hashicorpSecretConfig{makeSecret(secret1)},
secrets: []HashicorpSecretConfig{makeSecret(secret1)},
wantAccts: []accounts.Account{
{Address: common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")},
},
},
"account not retrieved when vault secret has multiple values": {
secrets: []hashicorpSecretConfig{makeSecret(multiValSecret)},
secrets: []HashicorpSecretConfig{makeSecret(multiValSecret)},
wantAccts: []accounts.Account{},
},
"unretrievable accounts are ignored": {
secrets: []hashicorpSecretConfig{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: []hashicorpSecretConfig{makeSecret(secret1), makeSecret(secret2)},
secrets: []HashicorpSecretConfig{makeSecret(secret1), makeSecret(secret2)},
wantAccts: []accounts.Account{
{Address: common.HexToAddress("ed9d02e382b34818e88b88a309c7fe71e65f419d")},
{Address: common.HexToAddress("ca843569e3427144cead5e4d5999a3d0ccf92b8e")},
@ -645,7 +648,7 @@ func TestVaultWallet_Open_Hashicorp_AccountsRetrieved(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
wltConfig := HashicorpWalletConfig{
Client: hashicorpClientConfig{
Client: HashicorpClientConfig{
Url: vaultServer.URL,
VaultPollingIntervalMillis: 1,
},
@ -703,11 +706,11 @@ func TestVaultWallet_Open_Hashicorp_AccountsRetrievedWhenVaultAvailable(t *testi
// use an incorrect vault url to simulate an inaccessible vault
wltConfig := HashicorpWalletConfig{
Client: hashicorpClientConfig{
Client: HashicorpClientConfig{
Url: "http://incorrecturl:1",
VaultPollingIntervalMillis: 1,
},
Secrets: []hashicorpSecretConfig{
Secrets: []HashicorpSecretConfig{
{AddressSecret: "sec1", AddressSecretVersion: 1, SecretEngine: "kv"},
},
}
@ -808,8 +811,8 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedIndefinitelyWhenEnabled(
return b
}
makeSecret := func(addrName, keyName string) hashicorpSecretConfig {
return hashicorpSecretConfig{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"}
}
makeKey := func(hex string) *ecdsa.PrivateKey {
@ -878,27 +881,27 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedIndefinitelyWhenEnabled(
defer vaultServer.Close()
tests := map[string]struct{
secrets []hashicorpSecretConfig
secrets []HashicorpSecretConfig
wantKeys []*ecdsa.PrivateKey
}{
"key retrieved": {
secrets: []hashicorpSecretConfig{makeSecret(addr1, key1)},
secrets: []HashicorpSecretConfig{makeSecret(addr1, key1)},
wantKeys: []*ecdsa.PrivateKey{
makeKey("e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1"),
},
},
"key not retrieved when vault secret has multiple values": {
secrets: []hashicorpSecretConfig{makeSecret(addr1, multiValSecret)},
secrets: []HashicorpSecretConfig{makeSecret(addr1, multiValSecret)},
wantKeys: []*ecdsa.PrivateKey{},
},
"unretrievable keys are ignored": {
secrets: []hashicorpSecretConfig{makeSecret(addr1, multiValSecret), makeSecret(addr2, key2)},
secrets: []HashicorpSecretConfig{makeSecret(addr1, multiValSecret), makeSecret(addr2, key2)},
wantKeys: []*ecdsa.PrivateKey{
makeKey("4762e04d10832808a0aebdaa79c12de54afbe006bfffd228b3abcc494fe986f9"),
},
},
"keys retrieved regardless of vault secrets keyvalue key": {
secrets: []hashicorpSecretConfig{makeSecret(addr1, key1), makeSecret(addr2, key2)},
secrets: []HashicorpSecretConfig{makeSecret(addr1, key1), makeSecret(addr2, key2)},
wantKeys: []*ecdsa.PrivateKey{
makeKey("e6181caaffff94a09d7e332fc8da9884d99902c7874eb74354bdcadf411929f1"), makeKey("4762e04d10832808a0aebdaa79c12de54afbe006bfffd228b3abcc494fe986f9"),
},
@ -908,7 +911,7 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedIndefinitelyWhenEnabled(
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
wltConfig := HashicorpWalletConfig{
Client: hashicorpClientConfig{
Client: HashicorpClientConfig{
Url: vaultServer.URL,
VaultPollingIntervalMillis: 1,
StorePrivateKeys: true,
@ -1025,12 +1028,12 @@ func TestVaultWallet_Open_Hashicorp_PrivateKeysRetrievedWhenEnabledAndVaultAvail
t.Run(name, func(t *testing.T) {
// use an incorrect vault url to simulate an inaccessible vault
wltConfig := HashicorpWalletConfig{
Client: hashicorpClientConfig{
Client: HashicorpClientConfig{
Url: "http://incorrecturl:1",
VaultPollingIntervalMillis: 1,
StorePrivateKeys: tt.storePrivateKeys,
},
Secrets: []hashicorpSecretConfig{
Secrets: []HashicorpSecretConfig{
{PrivateKeySecret: "sec1", PrivateKeySecretVersion: 1, SecretEngine: "kv"},
},
}
@ -1098,8 +1101,8 @@ func TestVaultWallet_Open_Hashicorp_RetrievalLoopsStopWhenAllSecretsRetrieved(t
return b
}
makeSecret := func(addrName, keyName string) hashicorpSecretConfig {
return hashicorpSecretConfig{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 (
@ -1136,12 +1139,12 @@ func TestVaultWallet_Open_Hashicorp_RetrievalLoopsStopWhenAllSecretsRetrieved(t
defer vaultServer.Close()
wltConfig := HashicorpWalletConfig{
Client: hashicorpClientConfig{
Client: HashicorpClientConfig{
Url: vaultServer.URL,
VaultPollingIntervalMillis: 1,
StorePrivateKeys: true,
},
Secrets: []hashicorpSecretConfig{makeSecret(addrName, keyName)},
Secrets: []HashicorpSecretConfig{makeSecret(addrName, keyName)},
}
w, err := newHashicorpWallet(wltConfig, &event.Feed{})
@ -1169,8 +1172,8 @@ func TestVaultWallet_Close_Hashicorp_ReturnsStateToBeforeOpen(t *testing.T) {
}
config := HashicorpWalletConfig{
Client: hashicorpClientConfig{Url: "http://url:1"},
Secrets: []hashicorpSecretConfig{{AddressSecret: "addr1"}},
Client: HashicorpClientConfig{Url: "http://url:1"},
Secrets: []HashicorpSecretConfig{{AddressSecret: "addr1"}},
}
w, err := newHashicorpWallet(config, &event.Feed{})
@ -1474,7 +1477,7 @@ func TestVaultWallet_SignHash_Hashicorp_SignsWithKeyFromVaultAndDoesNotStoreInMe
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -1812,7 +1815,7 @@ func TestVaultWallet_SignTx_Hashicorp_SignsWithKeyFromVaultAndDoesNotStoreInMemo
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -1895,7 +1898,7 @@ func TestVaultWallet_TimedUnlock_Hashicorp_StoresKeyInMemoryThenZeroesAfterSpeci
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -1971,7 +1974,7 @@ func TestVaultWallet_TimedUnlock_Hashicorp_IfAlreadyUnlockedThenOverridesExistin
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2053,7 +2056,7 @@ func TestVaultWallet_TimedUnlock_Hashicorp_IfAlreadyUnlockedThenOverridesExistin
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2139,7 +2142,7 @@ func TestVaultWallet_TimedUnlock_Hashicorp_SigningAfterUnlockTimedOutGetsKeyFrom
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2217,7 +2220,7 @@ func TestVaultWallet_TimedUnlock_Hashicorp_DurationZeroUnlocksIndefinitely(t *te
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2290,7 +2293,7 @@ func TestVaultWallet_TimedUnlock_Hashicorp_TryingToTimedUnlockAnIndefinitelyUnlo
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2366,7 +2369,7 @@ func TestVaultWallet_Lock_Hashicorp_LockIndefinitelyUnlockedKey(t *testing.T) {
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2448,7 +2451,7 @@ func TestVaultWallet_Lock_Hashicorp_LockTimedUnlockedKey(t *testing.T) {
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2533,7 +2536,7 @@ func TestVaultWallet_Lock_Hashicorp_LockAlreadyLockedKeyDoesNothing(t *testing.T
client, cleanup := makeMockHashicorpClient(t, makeMockHashicorpResponse(t, hexKey))
defer cleanup()
secret := hashicorpSecretConfig{
secret := HashicorpSecretConfig{
PrivateKeySecret: "mykey",
PrivateKeySecretVersion: 1,
SecretEngine: "kv",
@ -2557,3 +2560,156 @@ func TestVaultWallet_Lock_Hashicorp_LockAlreadyLockedKeyDoesNothing(t *testing.T
t.Fatalf("error locking: %v", err)
}
}
func TestVaultWallet_Store_Hashicorp_KeyAndAddressWrittenToVault(t *testing.T) {
mux := http.NewServeMux()
const (
secretEngine = "kv"
addr1 = "addr1"
key1 = "key1"
)
makeVaultResponse := func(version int) []byte {
resp := api.Secret{
Data: map[string]interface{}{
"version": version,
},
}
b, err := json.Marshal(resp)
if err != nil {
t.Fatal(err)
}
return b
}
var (
writtenAddr, writtenKey string
)
const (
addrVersion = 2
keyVersion = 5
)
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, addr1), func(w http.ResponseWriter, r *http.Request) {
body := makeVaultResponse(addrVersion)
w.Write(body)
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
var data map[string]interface{}
if err := json.Unmarshal(reqBody, &data); err != nil {
t.Fatal(err)
}
d := data["data"]
dd := d.(map[string]interface{})
writtenAddr = dd["secret"].(string)
})
mux.HandleFunc(fmt.Sprintf("/v1/%s/data/%s", secretEngine, key1), func(w http.ResponseWriter, r *http.Request) {
body := makeVaultResponse(keyVersion)
w.Write(body)
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
var data map[string]interface{}
if err := json.Unmarshal(reqBody, &data); err != nil {
t.Fatal(err)
}
d := data["data"]
dd := d.(map[string]interface{})
writtenKey = dd["secret"].(string)
//hasWrittenKey = true
})
vaultServer := httptest.NewServer(mux)
defer vaultServer.Close()
//create default client and update URL to use mock vault server
config := api.DefaultConfig()
config.Address = vaultServer.URL
client, err := api.NewClient(config)
if err != nil {
t.Fatalf("err creating client: %v", err)
}
parseURL := func(u string) accounts.URL {
parts := strings.Split(u, "://")
if len(parts) != 2 || parts[0] == "" {
t.Fatal("protocol scheme missing")
}
return accounts.URL{Scheme: parts[0], Path: parts[1]}
}
w := VaultWallet{
url: parseURL(vaultServer.URL),
vault: &hashicorpService{
client: client,
},
}
location := HashicorpSecretConfig{
AddressSecret: addr1,
PrivateKeySecret: key1,
SecretEngine: secretEngine,
}
toStore, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
wantAddr := crypto.PubkeyToAddress(toStore.PublicKey)
addr, urls, err := w.Store(toStore, location)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(wantAddr, addr) {
t.Fatalf("incorrect address returned\nwant: %v\ngot : %v", wantAddr, addr)
}
if len(urls) != 2 {
t.Fatalf("urls should have been returned for 2 new secrets, got: %v\nurls = %+v", len(urls), urls)
}
wantAddrUrl := fmt.Sprintf("%v/v1/%s/data/%s?version=%v", vaultServer.URL, secretEngine, addr1, addrVersion)
if urls[0] != wantAddrUrl {
t.Fatalf("incorrect url for created address: want: %v, got: %v", wantAddrUrl, urls[0])
}
wantKeyUrl := fmt.Sprintf("%v/v1/%s/data/%s?version=%v", vaultServer.URL, secretEngine, key1, keyVersion)
if urls[1] != wantKeyUrl {
t.Fatalf("incorrect url for key: want: %v, got: %v", wantKeyUrl, urls[1])
}
wantWrittenAddr := strings.TrimPrefix(wantAddr.Hex(), "0x")
if !cmp.Equal(wantWrittenAddr, writtenAddr) {
t.Fatalf("incorrect address hex written to Vault\nwant: %v\ngot : %v", wantWrittenAddr, writtenAddr)
}
wantWrittenKey := hex.EncodeToString(crypto.FromECDSA(toStore))
if !cmp.Equal(wantWrittenKey, writtenKey) {
t.Fatalf("incorrect key hex written to Vault\nwant: %v\ngot : %v", wantWrittenKey, writtenKey)
}
}

View File

@ -18,6 +18,7 @@ package main
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/vault"
"io/ioutil"
"github.com/ethereum/go-ethereum/accounts"
@ -105,12 +106,13 @@ Print a short summary of all accounts`,
Name: "new",
Usage: "Create a new account",
Action: utils.MigrateFlags(accountCreate),
Flags: []cli.Flag{
Flags: append(
HashicorpVaultFlags,
utils.DataDirFlag,
utils.KeyStoreDirFlag,
utils.PasswordFileFlag,
utils.LightKDFFlag,
},
),
Description: `
geth account new
@ -299,6 +301,31 @@ func accountCreate(ctx *cli.Context) error {
}
}
utils.SetNodeConfig(ctx, &cfg.Node)
// start quorum
// doing a local (not global) bool lookup here as Vault CLI options are only defined on accountcmd
if ctx.Bool(utils.HashicorpFlag.Name) {
vaultConfig := cfg.Node.HashicorpVault
if err := vaultConfig.ValidateSkipVersion(); err != nil {
return err
}
address, secretUrls, err := vault.CreateAccount(vaultConfig)
for _, url := range secretUrls {
fmt.Printf("Written to Vault: %v\n", url)
}
if err != nil {
utils.Fatalf("Error creating account: %v", err)
}
fmt.Printf("Address: {%x}\n", address)
return nil
}
// end quorum
scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()
if err != nil {

View File

@ -282,6 +282,24 @@ var AppHelpFlagGroups = []flagGroup{
},
}
var HashicorpVaultFlags = []cli.Flag {
utils.HashicorpFlag,
utils.HashicorpUrlFlag,
utils.HashicorpApproleFlag,
utils.HashicorpClientCertFlag,
utils.HashicorpClientKeyFlag,
utils.HashicorpCaCertFlag,
utils.HashicorpEngineFlag,
utils.HashicorpNamePrefixFlag,
}
var VaultHelpFlagGroups = []flagGroup {
{
Name: "HASHICORP VAULT",
Flags: HashicorpVaultFlags,
},
}
// byCategory sorts an array of flagGroup by Name in the order
// defined in AppHelpFlagGroups.
type byCategory []flagGroup
@ -305,7 +323,7 @@ func (a byCategory) Less(i, j int) bool {
}
func flagCategory(flag cli.Flag) string {
for _, category := range AppHelpFlagGroups {
for _, category := range append(AppHelpFlagGroups, VaultHelpFlagGroups...) {
for _, flg := range category.Flags {
if flg.GetName() == flag.GetName() {
return category.Name

View File

@ -20,6 +20,7 @@ package utils
import (
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/accounts/vault"
"io/ioutil"
"math/big"
"os"
@ -127,6 +128,41 @@ var (
Name: "nousb",
Usage: "Disables monitoring for and managing USB hardware wallets",
}
// start quorum
HashicorpFlag = cli.BoolFlag{
Name: "hashicorp",
Usage: "Store the newly created account in a Hashicorp Vault",
}
HashicorpUrlFlag = cli.StringFlag{
Name: "hashicorp.url",
Usage: "Address of the Vault server expressed as a URL and port, for example: https://127.0.0.1:8200/",
}
HashicorpApproleFlag = cli.StringFlag{
Name: "hashicorp.approle",
Usage: fmt.Sprintf("Vault path for an enabled Approle auth method (requires %v and %v env vars to be set)", vault.RoleIDEnv, vault.SecretIDEnv),
Value: "approle",
}
HashicorpClientCertFlag = cli.StringFlag{
Name: "hashicorp.clientcert",
Usage: "Path to a PEM-encoded client certificate. Required when communicating with the Vault server using TLS",
}
HashicorpClientKeyFlag = cli.StringFlag{
Name: "hashicorp.clientkey",
Usage: "Path to an unencrypted, PEM-encoded private key which corresponds to the matching client certificate",
}
HashicorpCaCertFlag = cli.StringFlag{
Name: "hashicorp.cacert",
Usage: "Path to a PEM-encoded CA certificate file. Used to verify the Vault server's SSL certificate",
}
HashicorpEngineFlag = cli.StringFlag{
Name: "hashicorp.engine",
Usage: "Vault path for an enabled KV v2 secret engine",
}
HashicorpNamePrefixFlag = cli.StringFlag{
Name: "hashicorp.nameprefix",
Usage: "The new address and key will be created with name <prefix>Addr and <prefix>Key respectively. Secrets with the same name in the Vault will be versioned and overwritten.",
}
// end quorum
NetworkIdFlag = cli.Uint64Flag{
Name: "networkid",
Usage: "Network identifier (integer, 1=Frontier, 2=Morden (disused), 3=Ropsten, 4=Rinkeby, 5=Ottoman)",
@ -1044,6 +1080,33 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
if ctx.GlobalIsSet(NoUSBFlag.Name) {
cfg.NoUSB = ctx.GlobalBool(NoUSBFlag.Name)
}
//start quorum
if ctx.Bool(HashicorpFlag.Name) {
setHashicorpVault(ctx, cfg)
}
//end quorum
}
// setHashicorpVault uses CLI options to set the config for a single Vault with a single key (Quorum)
func setHashicorpVault(ctx *cli.Context, cfg *node.Config) {
c := vault.HashicorpClientConfig{
Url: ctx.String(HashicorpUrlFlag.Name),
Approle: ctx.String(HashicorpApproleFlag.Name),
ClientCert: ctx.String(HashicorpClientCertFlag.Name),
ClientKey: ctx.String(HashicorpClientKeyFlag.Name),
CaCert: ctx.String(HashicorpCaCertFlag.Name),
}
s := vault.HashicorpSecretConfig{
AddressSecret: fmt.Sprintf("%vAddr", ctx.String(HashicorpNamePrefixFlag.Name)),
PrivateKeySecret: fmt.Sprintf("%vKey", ctx.String(HashicorpNamePrefixFlag.Name)),
SecretEngine: ctx.String(HashicorpEngineFlag.Name),
}
cfg.HashicorpVault = vault.HashicorpWalletConfig{
Client: c,
Secrets: []vault.HashicorpSecretConfig{s},
}
}
func setGPO(ctx *cli.Context, cfg *gasprice.Config) {

View File

@ -309,6 +309,8 @@ func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error)
return common.Address{}, err
}
// TODO NewVaultAccount: Create a new account whilst the node is running, store in vault, add to node, allow locking and unlocking
// fetchKeystore retrives the encrypted keystore from the account manager.
func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
return am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)

View File

@ -445,7 +445,7 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
}
}
// TODO to allow connection to more than one vault make conf.HashicorpVault a slice
// TODO allow connection to more than one vault
if !reflect.DeepEqual(conf.HashicorpVault, vault.HashicorpWalletConfig{}) {
if err := conf.HashicorpVault.Validate(); err != nil {
return nil, "", err