aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Holst Swende <martin@swende.se>2019-02-05 18:23:57 +0800
committerGitHub <noreply@github.com>2019-02-05 18:23:57 +0800
commit43e8efe8955b8bb1fab7bfced33a6302fb69e48e (patch)
tree3db96494c913b31be950f5580bf49a82141b9255
parent520024dfd689d264807b7fe1fc28deba51d6ab20 (diff)
downloadgo-tangerine-43e8efe8955b8bb1fab7bfced33a6302fb69e48e.tar
go-tangerine-43e8efe8955b8bb1fab7bfced33a6302fb69e48e.tar.gz
go-tangerine-43e8efe8955b8bb1fab7bfced33a6302fb69e48e.tar.bz2
go-tangerine-43e8efe8955b8bb1fab7bfced33a6302fb69e48e.tar.lz
go-tangerine-43e8efe8955b8bb1fab7bfced33a6302fb69e48e.tar.xz
go-tangerine-43e8efe8955b8bb1fab7bfced33a6302fb69e48e.tar.zst
go-tangerine-43e8efe8955b8bb1fab7bfced33a6302fb69e48e.zip
accounts, eth, clique, signer: support for external signer API (#18079)
* accounts, eth, clique: implement external backend + move sighash calc to backend * signer: implement account_Version on external API * accounts/external: enable ipc, add copyright * accounts, internal, signer: formatting * node: go fmt * flags: disallow --dev in combo with --externalsigner * accounts: remove clique-specific signing method, replace with more generic * accounts, consensus: formatting + fix error in tests * signer/core: remove (test-) import cycle * clique: remove unused import * accounts: remove CliqueHash and avoid dependency on package crypto * consensus/clique: unduplicate header encoding
-rw-r--r--accounts/accounts.go35
-rw-r--r--accounts/accounts_test.go32
-rw-r--r--accounts/external/backend.go220
-rw-r--r--accounts/keystore/wallet.go18
-rw-r--r--accounts/usbwallet/wallet.go18
-rw-r--r--cmd/clef/main.go10
-rw-r--r--cmd/geth/main.go16
-rw-r--r--cmd/geth/usage.go1
-rw-r--r--cmd/utils/flags.go30
-rw-r--r--consensus/clique/clique.go88
-rw-r--r--consensus/clique/snapshot_test.go2
-rw-r--r--eth/backend.go2
-rw-r--r--internal/ethapi/api.go20
-rw-r--r--node/config.go44
-rw-r--r--signer/core/api.go19
-rw-r--r--signer/core/api_test.go3
-rw-r--r--signer/core/auditlog.go15
17 files changed, 458 insertions, 115 deletions
diff --git a/accounts/accounts.go b/accounts/accounts.go
index cb1eae281..11232b19a 100644
--- a/accounts/accounts.go
+++ b/accounts/accounts.go
@@ -18,12 +18,14 @@
package accounts
import (
+ "fmt"
"math/big"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
+ "golang.org/x/crypto/sha3"
)
// Account represents an Ethereum account located at a specific location defined
@@ -87,8 +89,20 @@ type Wallet interface {
// chain state reader.
SelfDerive(base DerivationPath, chain ethereum.ChainStateReader)
- // SignHash requests the wallet to sign the given hash.
+ // SignData requests the wallet to sign the hash of the given data
+ // It looks up the account specified either solely via its address contained within,
+ // or optionally with the aid of any location metadata from the embedded URL field.
//
+ // If the wallet requires additional authentication to sign the request (e.g.
+ // a password to decrypt the account, or a PIN code o verify the transaction),
+ // an AuthNeededError instance will be returned, containing infos for the user
+ // about which fields or actions are needed. The user may retry by providing
+ // the needed details via SignHashWithPassphrase, or by other means (e.g. unlock
+ // the account in a keystore).
+ SignData(account Account, mimeType string, data []byte) ([]byte, error)
+
+ // Signtext requests the wallet to sign the hash of a given piece of data, prefixed
+ // by the Ethereum prefix scheme
// It looks up the account specified either solely via its address contained within,
// or optionally with the aid of any location metadata from the embedded URL field.
//
@@ -98,7 +112,7 @@ type Wallet interface {
// about which fields or actions are needed. The user may retry by providing
// the needed details via SignHashWithPassphrase, or by other means (e.g. unlock
// the account in a keystore).
- SignHash(account Account, hash []byte) ([]byte, error)
+ SignText(account Account, text []byte) ([]byte, error)
// SignTx requests the wallet to sign the given transaction.
//
@@ -113,12 +127,12 @@ type Wallet interface {
// the account in a keystore).
SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
- // SignHashWithPassphrase requests the wallet to sign the given hash with the
+ // SignTextWithPassphrase requests the wallet to sign the given text with the
// given passphrase as extra authentication information.
//
// It looks up the account specified either solely via its address contained within,
// or optionally with the aid of any location metadata from the embedded URL field.
- SignHashWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error)
+ SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error)
// SignTxWithPassphrase requests the wallet to sign the given transaction, with the
// given passphrase as extra authentication information.
@@ -148,6 +162,19 @@ type Backend interface {
Subscribe(sink chan<- WalletEvent) event.Subscription
}
+// TextHash is a helper function that calculates a hash for the given message that can be
+// safely used to calculate a signature from.
+//
+// The hash is calulcated as
+// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
+//
+// This gives context to the signed message and prevents signing of transactions.
+func TextHash(data []byte) []byte {
+ hash := sha3.NewLegacyKeccak256()
+ fmt.Fprintf(hash, "\x19Ethereum Signed Message:\n%d%s", len(data), data)
+ return hash.Sum(nil)
+}
+
// WalletEventType represents the different event types that can be fired by
// the wallet subscription subsystem.
type WalletEventType int
diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go
new file mode 100644
index 000000000..a49e3954e
--- /dev/null
+++ b/accounts/accounts_test.go
@@ -0,0 +1,32 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common/hexutil"
+)
+
+func TestTextHash(t *testing.T) {
+ hash := TextHash([]byte("Hello Joe"))
+ want := hexutil.MustDecode("0xa080337ae51c4e064c189e113edd0ba391df9206e2f49db658bb32cf2911730b")
+ if !bytes.Equal(hash, want) {
+ t.Fatalf("wrong hash: %x", hash)
+ }
+}
diff --git a/accounts/external/backend.go b/accounts/external/backend.go
new file mode 100644
index 000000000..35b9c276d
--- /dev/null
+++ b/accounts/external/backend.go
@@ -0,0 +1,220 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package external
+
+import (
+ "fmt"
+ "math/big"
+ "sync"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/event"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/ethereum/go-ethereum/signer/core"
+)
+
+type ExternalBackend struct {
+ signers []accounts.Wallet
+}
+
+func (eb *ExternalBackend) Wallets() []accounts.Wallet {
+ return eb.signers
+}
+
+func NewExternalBackend(endpoint string) (*ExternalBackend, error) {
+ signer, err := NewExternalSigner(endpoint)
+ if err != nil {
+ return nil, err
+ }
+ return &ExternalBackend{
+ signers: []accounts.Wallet{signer},
+ }, nil
+}
+
+func (eb *ExternalBackend) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
+ return event.NewSubscription(func(quit <-chan struct{}) error {
+ <-quit
+ return nil
+ })
+}
+
+// ExternalSigner provides an API to interact with an external signer (clef)
+// It proxies request to the external signer while forwarding relevant
+// request headers
+type ExternalSigner struct {
+ client *rpc.Client
+ endpoint string
+ status string
+ cacheMu sync.RWMutex
+ cache []accounts.Account
+}
+
+func NewExternalSigner(endpoint string) (*ExternalSigner, error) {
+ client, err := rpc.Dial(endpoint)
+ if err != nil {
+ return nil, err
+ }
+ extsigner := &ExternalSigner{
+ client: client,
+ endpoint: endpoint,
+ }
+ // Check if reachable
+ version, err := extsigner.pingVersion()
+ if err != nil {
+ return nil, err
+ }
+ extsigner.status = fmt.Sprintf("ok [version=%v]", version)
+ return extsigner, nil
+}
+
+func (api *ExternalSigner) URL() accounts.URL {
+ return accounts.URL{
+ Scheme: "extapi",
+ Path: api.endpoint,
+ }
+}
+
+func (api *ExternalSigner) Status() (string, error) {
+ return api.status, nil
+}
+
+func (api *ExternalSigner) Open(passphrase string) error {
+ return fmt.Errorf("operation not supported on external signers")
+}
+
+func (api *ExternalSigner) Close() error {
+ return fmt.Errorf("operation not supported on external signers")
+}
+
+func (api *ExternalSigner) Accounts() []accounts.Account {
+ var accnts []accounts.Account
+ res, err := api.listAccounts()
+ if err != nil {
+ log.Error("account listing failed", "error", err)
+ return accnts
+ }
+ for _, addr := range res {
+ accnts = append(accnts, accounts.Account{
+ URL: accounts.URL{
+ Scheme: "extapi",
+ Path: api.endpoint,
+ },
+ Address: addr,
+ })
+ }
+ api.cacheMu.Lock()
+ api.cache = accnts
+ api.cacheMu.Unlock()
+ return accnts
+}
+
+func (api *ExternalSigner) Contains(account accounts.Account) bool {
+ api.cacheMu.RLock()
+ defer api.cacheMu.RUnlock()
+ for _, a := range api.cache {
+ if a.Address == account.Address && (account.URL == (accounts.URL{}) || account.URL == api.URL()) {
+ return true
+ }
+ }
+ return false
+}
+
+func (api *ExternalSigner) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
+ return accounts.Account{}, fmt.Errorf("operation not supported on external signers")
+}
+
+func (api *ExternalSigner) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {
+ log.Error("operation SelfDerive not supported on external signers")
+}
+
+func (api *ExternalSigner) signHash(account accounts.Account, hash []byte) ([]byte, error) {
+ return []byte{}, fmt.Errorf("operation not supported on external signers")
+}
+
+// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed
+func (api *ExternalSigner) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
+ // TODO! Replace this with a call to clef SignData with correct mime-type for Clique, once we
+ // have that in place
+ return api.signHash(account, crypto.Keccak256(data))
+}
+
+func (api *ExternalSigner) SignText(account accounts.Account, text []byte) ([]byte, error) {
+ return api.signHash(account, accounts.TextHash(text))
+}
+
+func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+ res := ethapi.SignTransactionResult{}
+ to := common.NewMixedcaseAddress(*tx.To())
+ data := hexutil.Bytes(tx.Data())
+ args := &core.SendTxArgs{
+ Data: &data,
+ Nonce: hexutil.Uint64(tx.Nonce()),
+ Value: hexutil.Big(*tx.Value()),
+ Gas: hexutil.Uint64(tx.Gas()),
+ GasPrice: hexutil.Big(*tx.GasPrice()),
+ To: &to,
+ From: common.NewMixedcaseAddress(account.Address),
+ }
+
+ if err := api.client.Call(&res, "account_signTransaction", args); err != nil {
+ return nil, err
+ }
+ return res.Tx, nil
+}
+
+func (api *ExternalSigner) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) {
+ return []byte{}, fmt.Errorf("operation not supported on external signers")
+}
+
+func (api *ExternalSigner) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+ return nil, fmt.Errorf("operation not supported on external signers")
+}
+
+func (api *ExternalSigner) listAccounts() ([]common.Address, error) {
+ var res []common.Address
+ if err := api.client.Call(&res, "account_list"); err != nil {
+ return nil, err
+ }
+ return res, nil
+}
+
+func (api *ExternalSigner) signCliqueBlock(a common.Address, rlpBlock hexutil.Bytes) (hexutil.Bytes, error) {
+ var sig hexutil.Bytes
+ if err := api.client.Call(&sig, "account_signData", "application/clique", a, rlpBlock); err != nil {
+ return nil, err
+ }
+ if sig[64] != 27 && sig[64] != 28 {
+ return nil, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
+ }
+ sig[64] -= 27 // Transform V from 27/28 to 0/1 for Clique use
+ return sig, nil
+}
+
+func (api *ExternalSigner) pingVersion() (string, error) {
+ var v string
+ if err := api.client.Call(&v, "account_version"); err != nil {
+ return "", err
+ }
+ return v, nil
+}
diff --git a/accounts/keystore/wallet.go b/accounts/keystore/wallet.go
index 2f774cc94..0490f39ff 100644
--- a/accounts/keystore/wallet.go
+++ b/accounts/keystore/wallet.go
@@ -22,6 +22,7 @@ import (
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
)
// keystoreWallet implements the accounts.Wallet interface for the original
@@ -78,11 +79,11 @@ func (w *keystoreWallet) Derive(path accounts.DerivationPath, pin bool) (account
// there is no notion of hierarchical account derivation for plain keystore accounts.
func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {}
-// SignHash implements accounts.Wallet, attempting to sign the given hash with
+// signHash attempts to sign the given hash with
// the given account. If the wallet does not wrap this particular account, an
// error is returned to avoid account leakage (even though in theory we may be
// able to sign via our shared keystore backend).
-func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) {
+func (w *keystoreWallet) signHash(account accounts.Account, hash []byte) ([]byte, error) {
// Make sure the requested account is contained within
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
@@ -91,6 +92,15 @@ func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte
return w.keystore.SignHash(account, hash)
}
+// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed
+func (w *keystoreWallet) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
+ return w.signHash(account, crypto.Keccak256(data))
+}
+
+func (w *keystoreWallet) SignText(account accounts.Account, text []byte) ([]byte, error) {
+ return w.signHash(account, accounts.TextHash(text))
+}
+
// SignTx implements accounts.Wallet, attempting to sign the given transaction
// with the given account. If the wallet does not wrap this particular account,
// an error is returned to avoid account leakage (even though in theory we may
@@ -106,13 +116,13 @@ func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction,
// SignHashWithPassphrase implements accounts.Wallet, attempting to sign the
// given hash with the given account using passphrase as extra authentication.
-func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) {
+func (w *keystoreWallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) {
// Make sure the requested account is contained within
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
}
// Account seems valid, request the keystore to sign
- return w.keystore.SignHashWithPassphrase(account, passphrase, hash)
+ return w.keystore.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text))
}
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
diff --git a/accounts/usbwallet/wallet.go b/accounts/usbwallet/wallet.go
index 6cef6e0fb..a99dcd0f5 100644
--- a/accounts/usbwallet/wallet.go
+++ b/accounts/usbwallet/wallet.go
@@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/karalabe/hid"
)
@@ -495,12 +496,21 @@ func (w *wallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainSt
w.deriveChain = chain
}
-// SignHash implements accounts.Wallet, however signing arbitrary data is not
+// signHash implements accounts.Wallet, however signing arbitrary data is not
// supported for hardware wallets, so this method will always return an error.
-func (w *wallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) {
+func (w *wallet) signHash(account accounts.Account, hash []byte) ([]byte, error) {
return nil, accounts.ErrNotSupported
}
+// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed
+func (w *wallet) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
+ return w.signHash(account, crypto.Keccak256(data))
+}
+
+func (w *wallet) SignText(account accounts.Account, text []byte) ([]byte, error) {
+ return w.signHash(account, accounts.TextHash(text))
+}
+
// SignTx implements accounts.Wallet. It sends the transaction over to the Ledger
// wallet to request a confirmation from the user. It returns either the signed
// transaction or a failure if the user denied the transaction.
@@ -550,8 +560,8 @@ func (w *wallet) SignTx(account accounts.Account, tx *types.Transaction, chainID
// SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary
// data is not supported for Ledger wallets, so this method will always return
// an error.
-func (w *wallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) {
- return w.SignHash(account, hash)
+func (w *wallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) {
+ return w.SignText(account, accounts.TextHash(text))
}
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
diff --git a/cmd/clef/main.go b/cmd/clef/main.go
index 519d63b3c..e2b85288d 100644
--- a/cmd/clef/main.go
+++ b/cmd/clef/main.go
@@ -49,12 +49,6 @@ import (
"gopkg.in/urfave/cli.v1"
)
-// ExternalAPIVersion -- see extapi_changelog.md
-const ExternalAPIVersion = "4.0.0"
-
-// InternalAPIVersion -- see intapi_changelog.md
-const InternalAPIVersion = "3.0.0"
-
const legalWarning = `
WARNING!
@@ -479,8 +473,8 @@ func signer(c *cli.Context) error {
}
ui.OnSignerStartup(core.StartupInfo{
Info: map[string]interface{}{
- "extapi_version": ExternalAPIVersion,
- "intapi_version": InternalAPIVersion,
+ "extapi_version": core.ExternalAPIVersion,
+ "intapi_version": core.InternalAPIVersion,
"extapi_http": extapiURL,
"extapi_ipc": ipcapiURL,
},
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index 0a6616312..dca02c82e 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -62,6 +62,7 @@ var (
utils.BootnodesV5Flag,
utils.DataDirFlag,
utils.KeyStoreDirFlag,
+ utils.ExternalSignerFlag,
utils.NoUSBFlag,
utils.DashboardEnabledFlag,
utils.DashboardAddrFlag,
@@ -293,13 +294,14 @@ func startNode(ctx *cli.Context, stack *node.Node) {
utils.StartNode(stack)
// Unlock any account specifically requested
- ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
-
- passwords := utils.MakePasswordList(ctx)
- unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
- for i, account := range unlocks {
- if trimmed := strings.TrimSpace(account); trimmed != "" {
- unlockAccount(ctx, ks, trimmed, i, passwords)
+ if keystores := stack.AccountManager().Backends(keystore.KeyStoreType); len(keystores) > 0 {
+ ks := keystores[0].(*keystore.KeyStore)
+ passwords := utils.MakePasswordList(ctx)
+ unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
+ for i, account := range unlocks {
+ if trimmed := strings.TrimSpace(account); trimmed != "" {
+ unlockAccount(ctx, ks, trimmed, i, passwords)
+ }
}
}
// Register wallet event handlers to open and auto-derive wallets
diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go
index 1579134f9..0b5c6fd90 100644
--- a/cmd/geth/usage.go
+++ b/cmd/geth/usage.go
@@ -145,6 +145,7 @@ var AppHelpFlagGroups = []flagGroup{
Flags: []cli.Flag{
utils.UnlockedAccountFlag,
utils.PasswordFileFlag,
+ utils.ExternalSignerFlag,
},
},
{
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index a3602182a..bf20abe81 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -427,7 +427,11 @@ var (
Usage: "Password file to use for non-interactive password input",
Value: "",
}
-
+ ExternalSignerFlag = cli.StringFlag{
+ Name: "signer",
+ Usage: "External signer (url or path to ipc file)",
+ Value: "",
+ }
VMEnableDebugFlag = cli.BoolFlag{
Name: "vmdebug",
Usage: "Record information useful for VM and contract debugging",
@@ -990,11 +994,15 @@ func setEtherbase(ctx *cli.Context, ks *keystore.KeyStore, cfg *eth.Config) {
}
// Convert the etherbase into an address and configure it
if etherbase != "" {
- account, err := MakeAddress(ks, etherbase)
- if err != nil {
- Fatalf("Invalid miner etherbase: %v", err)
+ if ks != nil {
+ account, err := MakeAddress(ks, etherbase)
+ if err != nil {
+ Fatalf("Invalid miner etherbase: %v", err)
+ }
+ cfg.Etherbase = account.Address
+ } else {
+ Fatalf("No etherbase configured")
}
- cfg.Etherbase = account.Address
}
}
@@ -1093,6 +1101,10 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
setNodeUserIdent(ctx, cfg)
setDataDir(ctx, cfg)
+ if ctx.GlobalIsSet(ExternalSignerFlag.Name) {
+ cfg.ExternalSigner = ctx.GlobalString(ExternalSignerFlag.Name)
+ }
+
if ctx.GlobalIsSet(KeyStoreDirFlag.Name) {
cfg.KeyStoreDir = ctx.GlobalString(KeyStoreDirFlag.Name)
}
@@ -1274,8 +1286,12 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) {
// Avoid conflicting network flags
checkExclusive(ctx, DeveloperFlag, TestnetFlag, RinkebyFlag, GoerliFlag)
checkExclusive(ctx, LightServFlag, SyncModeFlag, "light")
-
- ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
+ // Can't use both ephemeral unlocked and external signer
+ checkExclusive(ctx, DeveloperFlag, ExternalSignerFlag)
+ var ks *keystore.KeyStore
+ if keystores := stack.AccountManager().Backends(keystore.KeyStoreType); len(keystores) > 0 {
+ ks = keystores[0].(*keystore.KeyStore)
+ }
setEtherbase(ctx, ks, cfg)
setGPO(ctx, &cfg.GPO)
setTxPool(ctx, &cfg.TxPool)
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go
index c79c30cae..c0f78ce65 100644
--- a/consensus/clique/clique.go
+++ b/consensus/clique/clique.go
@@ -20,6 +20,7 @@ package clique
import (
"bytes"
"errors"
+ "io"
"math/big"
"math/rand"
"sync"
@@ -136,40 +137,9 @@ var (
errRecentlySigned = errors.New("recently signed")
)
-// SignerFn is a signer callback function to request a hash to be signed by a
+// SignerFn is a signer callback function to request a header to be signed by a
// backing account.
-type SignerFn func(accounts.Account, []byte) ([]byte, error)
-
-// sigHash returns the hash which is used as input for the proof-of-authority
-// signing. It is the hash of the entire header apart from the 65 byte signature
-// contained at the end of the extra data.
-//
-// Note, the method requires the extra data to be at least 65 bytes, otherwise it
-// panics. This is done to avoid accidentally using both forms (signature present
-// or not), which could be abused to produce different hashes for the same header.
-func sigHash(header *types.Header) (hash common.Hash) {
- hasher := sha3.NewLegacyKeccak256()
-
- rlp.Encode(hasher, []interface{}{
- header.ParentHash,
- header.UncleHash,
- header.Coinbase,
- header.Root,
- header.TxHash,
- header.ReceiptHash,
- header.Bloom,
- header.Difficulty,
- header.Number,
- header.GasLimit,
- header.GasUsed,
- header.Time,
- header.Extra[:len(header.Extra)-65], // Yes, this will panic if extra is too short
- header.MixDigest,
- header.Nonce,
- })
- hasher.Sum(hash[:0])
- return hash
-}
+type SignerFn func(accounts.Account, string, []byte) ([]byte, error)
// ecrecover extracts the Ethereum account address from a signed header.
func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, error) {
@@ -185,7 +155,7 @@ func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, er
signature := header.Extra[len(header.Extra)-extraSeal:]
// Recover the public key and the Ethereum address
- pubkey, err := crypto.Ecrecover(sigHash(header).Bytes(), signature)
+ pubkey, err := crypto.Ecrecover(SealHash(header).Bytes(), signature)
if err != nil {
return common.Address{}, err
}
@@ -646,7 +616,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c
log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))
}
// Sign all the things!
- sighash, err := signFn(accounts.Account{Address: signer}, sigHash(header).Bytes())
+ sighash, err := signFn(accounts.Account{Address: signer}, "application/x-clique-header", CliqueRLP(header))
if err != nil {
return err
}
@@ -663,7 +633,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c
select {
case results <- block.WithSeal(header):
default:
- log.Warn("Sealing result is not read by miner", "sealhash", c.SealHash(header))
+ log.Warn("Sealing result is not read by miner", "sealhash", SealHash(header))
}
}()
@@ -693,7 +663,7 @@ func CalcDifficulty(snap *Snapshot, signer common.Address) *big.Int {
// SealHash returns the hash of a block prior to it being sealed.
func (c *Clique) SealHash(header *types.Header) common.Hash {
- return sigHash(header)
+ return SealHash(header)
}
// Close implements consensus.Engine. It's a noop for clique as there are no background threads.
@@ -711,3 +681,47 @@ func (c *Clique) APIs(chain consensus.ChainReader) []rpc.API {
Public: false,
}}
}
+
+// SealHash returns the hash of a block prior to it being sealed.
+func SealHash(header *types.Header) (hash common.Hash) {
+ hasher := sha3.NewLegacyKeccak256()
+ encodeSigHeader(hasher, header)
+ hasher.Sum(hash[:0])
+ return hash
+}
+
+// CliqueRLP returns the rlp bytes which needs to be signed for the proof-of-authority
+// sealing. The RLP to sign consists of the entire header apart from the 65 byte signature
+// contained at the end of the extra data.
+//
+// Note, the method requires the extra data to be at least 65 bytes, otherwise it
+// panics. This is done to avoid accidentally using both forms (signature present
+// or not), which could be abused to produce different hashes for the same header.
+func CliqueRLP(header *types.Header) []byte {
+ b := new(bytes.Buffer)
+ encodeSigHeader(b, header)
+ return b.Bytes()
+}
+
+func encodeSigHeader(w io.Writer, header *types.Header) {
+ err := rlp.Encode(w, []interface{}{
+ header.ParentHash,
+ header.UncleHash,
+ header.Coinbase,
+ header.Root,
+ header.TxHash,
+ header.ReceiptHash,
+ header.Bloom,
+ header.Difficulty,
+ header.Number,
+ header.GasLimit,
+ header.GasUsed,
+ header.Time,
+ header.Extra[:len(header.Extra)-65], // Yes, this will panic if extra is too short
+ header.MixDigest,
+ header.Nonce,
+ })
+ if err != nil {
+ panic("can't encode: " + err.Error())
+ }
+}
diff --git a/consensus/clique/snapshot_test.go b/consensus/clique/snapshot_test.go
index 41dae1426..b920312a8 100644
--- a/consensus/clique/snapshot_test.go
+++ b/consensus/clique/snapshot_test.go
@@ -80,7 +80,7 @@ func (ap *testerAccountPool) sign(header *types.Header, signer string) {
ap.accounts[signer], _ = crypto.GenerateKey()
}
// Sign the header and embed the signature in extra data
- sig, _ := crypto.Sign(sigHash(header).Bytes(), ap.accounts[signer])
+ sig, _ := crypto.Sign(SealHash(header).Bytes(), ap.accounts[signer])
copy(header.Extra[len(header.Extra)-extraSeal:], sig)
}
diff --git a/eth/backend.go b/eth/backend.go
index 2a9d56c5c..3ec6749b6 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -434,7 +434,7 @@ func (s *Ethereum) StartMining(threads int) error {
log.Error("Etherbase account unavailable locally", "err", err)
return fmt.Errorf("signer missing: %v", err)
}
- clique.Authorize(eb, wallet.SignHash)
+ clique.Authorize(eb, wallet.SignData)
}
// If mining is started, we can disable the transaction rejection mechanism
// introduced to speed sync times.
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 26dc1e8a0..0aeec8ad1 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -217,7 +217,7 @@ func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI {
}
}
-// ListAccounts will return a list of addresses for accounts this node manages.
+// listAccounts will return a list of addresses for accounts this node manages.
func (s *PrivateAccountAPI) ListAccounts() []common.Address {
addresses := make([]common.Address, 0) // return [] instead of nil if empty
for _, wallet := range s.am.Wallets() {
@@ -409,18 +409,6 @@ func (s *PrivateAccountAPI) SignTransaction(ctx context.Context, args SendTxArgs
return &SignTransactionResult{data, signed}, nil
}
-// signHash is a helper function that calculates a hash for the given message that can be
-// safely used to calculate a signature from.
-//
-// The hash is calulcated as
-// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
-//
-// This gives context to the signed message and prevents signing of transactions.
-func signHash(data []byte) []byte {
- msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
- return crypto.Keccak256([]byte(msg))
-}
-
// Sign calculates an Ethereum ECDSA signature for:
// keccack256("\x19Ethereum Signed Message:\n" + len(message) + message))
//
@@ -439,7 +427,7 @@ func (s *PrivateAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr c
return nil, err
}
// Assemble sign the data with the wallet
- signature, err := wallet.SignHashWithPassphrase(account, passwd, signHash(data))
+ signature, err := wallet.SignTextWithPassphrase(account, passwd, data)
if err != nil {
log.Warn("Failed data sign attempt", "address", addr, "err", err)
return nil, err
@@ -467,7 +455,7 @@ func (s *PrivateAccountAPI) EcRecover(ctx context.Context, data, sig hexutil.Byt
}
sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
- rpk, err := crypto.SigToPub(signHash(data), sig)
+ rpk, err := crypto.SigToPub(accounts.TextHash(data), sig)
if err != nil {
return common.Address{}, err
}
@@ -1357,7 +1345,7 @@ func (s *PublicTransactionPoolAPI) Sign(addr common.Address, data hexutil.Bytes)
return nil, err
}
// Sign the requested hash with the wallet
- signature, err := wallet.SignHash(account, signHash(data))
+ signature, err := wallet.SignText(account, data)
if err == nil {
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
}
diff --git a/node/config.go b/node/config.go
index 99f325840..ff0725aab 100644
--- a/node/config.go
+++ b/node/config.go
@@ -27,6 +27,7 @@ import (
"sync"
"github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/external"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/common"
@@ -80,6 +81,9 @@ type Config struct {
// is created by New and destroyed when the node is stopped.
KeyStoreDir string `toml:",omitempty"`
+ // ExternalSigner specifies an external URI for a clef-type signer
+ ExternalSigner string `toml:"omitempty"`
+
// UseLightweightKDF lowers the memory and CPU requirements of the key store
// scrypt KDF at the expense of security.
UseLightweightKDF bool `toml:",omitempty"`
@@ -462,23 +466,37 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
return nil, "", err
}
// Assemble the account manager and supported backends
- backends := []accounts.Backend{
- keystore.NewKeyStore(keydir, scryptN, scryptP),
- }
- if !conf.NoUSB {
- // Start a USB hub for Ledger hardware wallets
- if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
- log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
+ backends := []accounts.Backend{}
+ if len(conf.ExternalSigner) > 0 {
+ log.Info("Using external signer", "url", conf.ExternalSigner)
+ if extapi, err := external.NewExternalBackend(conf.ExternalSigner); err == nil {
+ backends = append(backends, extapi)
} else {
- backends = append(backends, ledgerhub)
+ log.Info("Error configuring external signer", "error", err)
}
- // Start a USB hub for Trezor hardware wallets
- if trezorhub, err := usbwallet.NewTrezorHub(); err != nil {
- log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err))
- } else {
- backends = append(backends, trezorhub)
+ }
+ if len(backends) == 0 {
+ // For now, we're using EITHER external signer OR local signers.
+ // If/when we implement some form of lockfile for USB and keystore wallets,
+ // we can have both, but it's very confusing for the user to see the same
+ // accounts in both externally and locally, plus very racey.
+ backends = append(backends, keystore.NewKeyStore(keydir, scryptN, scryptP))
+ if !conf.NoUSB {
+ // Start a USB hub for Ledger hardware wallets
+ if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
+ } else {
+ backends = append(backends, ledgerhub)
+ }
+ // Start a USB hub for Trezor hardware wallets
+ if trezorhub, err := usbwallet.NewTrezorHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err))
+ } else {
+ backends = append(backends, trezorhub)
+ }
}
}
+
return accounts.NewManager(backends...), ephemeral, nil
}
diff --git a/signer/core/api.go b/signer/core/api.go
index e9a335785..e112df9c7 100644
--- a/signer/core/api.go
+++ b/signer/core/api.go
@@ -36,8 +36,14 @@ import (
"github.com/ethereum/go-ethereum/rlp"
)
-// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
-const numberOfAccountsToDerive = 10
+const (
+ // numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
+ numberOfAccountsToDerive = 10
+ // ExternalAPIVersion -- see extapi_changelog.md
+ ExternalAPIVersion = "4.0.0"
+ // InternalAPIVersion -- see intapi_changelog.md
+ InternalAPIVersion = "3.0.0"
+)
// ExternalAPI defines the external API through which signing requests are made.
type ExternalAPI interface {
@@ -55,6 +61,7 @@ type ExternalAPI interface {
// Should be moved to Internal API, in next phase when we have
// bi-directional communication
//Import(ctx context.Context, keyJSON json.RawMessage) (Account, error)
+ Version(ctx context.Context) (string, error)
}
// SignerUI specifies what method a UI needs to implement to be able to be used as a UI for the signer
@@ -539,7 +546,7 @@ func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, da
return nil, err
}
// Assemble sign the data with the wallet
- signature, err := wallet.SignHashWithPassphrase(account, res.Password, sighash)
+ signature, err := wallet.SignTextWithPassphrase(account, res.Password, data)
if err != nil {
api.UI.ShowError(err.Error())
return nil, err
@@ -610,3 +617,9 @@ func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage) (Acco
}
return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil
}
+
+// Returns the external api version. This method does not require user acceptance. Available methods are
+// available via enumeration anyway, and this info does not contain user-specific data
+func (api *SignerAPI) Version(ctx context.Context) (string, error) {
+ return ExternalAPIVersion, nil
+}
diff --git a/signer/core/api_test.go b/signer/core/api_test.go
index a8aa23896..114470cf9 100644
--- a/signer/core/api_test.go
+++ b/signer/core/api_test.go
@@ -29,7 +29,6 @@ import (
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
- "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
@@ -135,7 +134,7 @@ func setup(t *testing.T) (*SignerAPI, chan string) {
db, err := NewAbiDBFromFile("../../cmd/clef/4byte.json")
if err != nil {
- utils.Fatalf(err.Error())
+ t.Fatal(err.Error())
}
var (
ui = &HeadlessUI{controller}
diff --git a/signer/core/auditlog.go b/signer/core/auditlog.go
index 1f9c90918..0cb6c9c47 100644
--- a/signer/core/auditlog.go
+++ b/signer/core/auditlog.go
@@ -80,14 +80,13 @@ func (l *AuditLogger) Export(ctx context.Context, addr common.Address) (json.Raw
return j, e
}
-//func (l *AuditLogger) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) {
-// // Don't actually log the json contents
-// l.log.Info("Import", "type", "request", "metadata", MetadataFromContext(ctx).String(),
-// "keyJSON size", len(keyJSON))
-// a, e := l.api.Import(ctx, keyJSON)
-// l.log.Info("Import", "type", "response", "addr", a.String(), "error", e)
-// return a, e
-//}
+func (l *AuditLogger) Version(ctx context.Context) (string, error) {
+ l.log.Info("Version", "type", "request", "metadata", MetadataFromContext(ctx).String())
+ data, err := l.api.Version(ctx)
+ l.log.Info("Version", "type", "response", "data", data, "error", err)
+ return data, err
+
+}
func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) {
l := log.New("api", "signer")