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
This commit is contained in:
Martin Holst Swende 2019-02-05 11:23:57 +01:00 committed by GitHub
parent 520024dfd6
commit 43e8efe895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 460 additions and 117 deletions

@ -18,12 +18,14 @@
package accounts package accounts
import ( import (
"fmt"
"math/big" "math/big"
ethereum "github.com/ethereum/go-ethereum" ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"golang.org/x/crypto/sha3"
) )
// Account represents an Ethereum account located at a specific location defined // Account represents an Ethereum account located at a specific location defined
@ -87,8 +89,7 @@ type Wallet interface {
// chain state reader. // chain state reader.
SelfDerive(base DerivationPath, chain ethereum.ChainStateReader) 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, // 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. // or optionally with the aid of any location metadata from the embedded URL field.
// //
@ -98,7 +99,20 @@ type Wallet interface {
// about which fields or actions are needed. The user may retry by providing // 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 needed details via SignHashWithPassphrase, or by other means (e.g. unlock
// the account in a keystore). // the account in a keystore).
SignHash(account Account, hash []byte) ([]byte, error) 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.
//
// 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).
SignText(account Account, text []byte) ([]byte, error)
// SignTx requests the wallet to sign the given transaction. // SignTx requests the wallet to sign the given transaction.
// //
@ -113,12 +127,12 @@ type Wallet interface {
// the account in a keystore). // the account in a keystore).
SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) 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. // given passphrase as extra authentication information.
// //
// It looks up the account specified either solely via its address contained within, // 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. // 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 // SignTxWithPassphrase requests the wallet to sign the given transaction, with the
// given passphrase as extra authentication information. // given passphrase as extra authentication information.
@ -148,6 +162,19 @@ type Backend interface {
Subscribe(sink chan<- WalletEvent) event.Subscription 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 // WalletEventType represents the different event types that can be fired by
// the wallet subscription subsystem. // the wallet subscription subsystem.
type WalletEventType int type WalletEventType int

32
accounts/accounts_test.go Normal file

@ -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)
}
}

220
accounts/external/backend.go vendored Normal file

@ -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
}

@ -22,6 +22,7 @@ import (
ethereum "github.com/ethereum/go-ethereum" ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
) )
// keystoreWallet implements the accounts.Wallet interface for the original // 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. // there is no notion of hierarchical account derivation for plain keystore accounts.
func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {} 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 // 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 // error is returned to avoid account leakage (even though in theory we may be
// able to sign via our shared keystore backend). // 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 // Make sure the requested account is contained within
if !w.Contains(account) { if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
@ -91,6 +92,15 @@ func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte
return w.keystore.SignHash(account, hash) 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 // SignTx implements accounts.Wallet, attempting to sign the given transaction
// with the given account. If the wallet does not wrap this particular account, // 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 // 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 // SignHashWithPassphrase implements accounts.Wallet, attempting to sign the
// given hash with the given account using passphrase as extra authentication. // 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 // Make sure the requested account is contained within
if !w.Contains(account) { if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
// Account seems valid, request the keystore to sign // 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 // SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given

@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/karalabe/hid" "github.com/karalabe/hid"
) )
@ -495,12 +496,21 @@ func (w *wallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainSt
w.deriveChain = chain 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. // 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 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 // 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 // wallet to request a confirmation from the user. It returns either the signed
// transaction or a failure if the user denied the transaction. // 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 // SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary
// data is not supported for Ledger wallets, so this method will always return // data is not supported for Ledger wallets, so this method will always return
// an error. // an error.
func (w *wallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { func (w *wallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) {
return w.SignHash(account, hash) return w.SignText(account, accounts.TextHash(text))
} }
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given // SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given

@ -49,12 +49,6 @@ import (
"gopkg.in/urfave/cli.v1" "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 = ` const legalWarning = `
WARNING! WARNING!
@ -479,8 +473,8 @@ func signer(c *cli.Context) error {
} }
ui.OnSignerStartup(core.StartupInfo{ ui.OnSignerStartup(core.StartupInfo{
Info: map[string]interface{}{ Info: map[string]interface{}{
"extapi_version": ExternalAPIVersion, "extapi_version": core.ExternalAPIVersion,
"intapi_version": InternalAPIVersion, "intapi_version": core.InternalAPIVersion,
"extapi_http": extapiURL, "extapi_http": extapiURL,
"extapi_ipc": ipcapiURL, "extapi_ipc": ipcapiURL,
}, },

@ -62,6 +62,7 @@ var (
utils.BootnodesV5Flag, utils.BootnodesV5Flag,
utils.DataDirFlag, utils.DataDirFlag,
utils.KeyStoreDirFlag, utils.KeyStoreDirFlag,
utils.ExternalSignerFlag,
utils.NoUSBFlag, utils.NoUSBFlag,
utils.DashboardEnabledFlag, utils.DashboardEnabledFlag,
utils.DashboardAddrFlag, utils.DashboardAddrFlag,
@ -293,13 +294,14 @@ func startNode(ctx *cli.Context, stack *node.Node) {
utils.StartNode(stack) utils.StartNode(stack)
// Unlock any account specifically requested // Unlock any account specifically requested
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) if keystores := stack.AccountManager().Backends(keystore.KeyStoreType); len(keystores) > 0 {
ks := keystores[0].(*keystore.KeyStore)
passwords := utils.MakePasswordList(ctx) passwords := utils.MakePasswordList(ctx)
unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",") unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
for i, account := range unlocks { for i, account := range unlocks {
if trimmed := strings.TrimSpace(account); trimmed != "" { if trimmed := strings.TrimSpace(account); trimmed != "" {
unlockAccount(ctx, ks, trimmed, i, passwords) unlockAccount(ctx, ks, trimmed, i, passwords)
}
} }
} }
// Register wallet event handlers to open and auto-derive wallets // Register wallet event handlers to open and auto-derive wallets

@ -145,6 +145,7 @@ var AppHelpFlagGroups = []flagGroup{
Flags: []cli.Flag{ Flags: []cli.Flag{
utils.UnlockedAccountFlag, utils.UnlockedAccountFlag,
utils.PasswordFileFlag, utils.PasswordFileFlag,
utils.ExternalSignerFlag,
}, },
}, },
{ {

@ -427,7 +427,11 @@ var (
Usage: "Password file to use for non-interactive password input", Usage: "Password file to use for non-interactive password input",
Value: "", Value: "",
} }
ExternalSignerFlag = cli.StringFlag{
Name: "signer",
Usage: "External signer (url or path to ipc file)",
Value: "",
}
VMEnableDebugFlag = cli.BoolFlag{ VMEnableDebugFlag = cli.BoolFlag{
Name: "vmdebug", Name: "vmdebug",
Usage: "Record information useful for VM and contract debugging", 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 // Convert the etherbase into an address and configure it
if etherbase != "" { if etherbase != "" {
account, err := MakeAddress(ks, etherbase) if ks != nil {
if err != nil { account, err := MakeAddress(ks, etherbase)
Fatalf("Invalid miner etherbase: %v", err) 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) setNodeUserIdent(ctx, cfg)
setDataDir(ctx, cfg) setDataDir(ctx, cfg)
if ctx.GlobalIsSet(ExternalSignerFlag.Name) {
cfg.ExternalSigner = ctx.GlobalString(ExternalSignerFlag.Name)
}
if ctx.GlobalIsSet(KeyStoreDirFlag.Name) { if ctx.GlobalIsSet(KeyStoreDirFlag.Name) {
cfg.KeyStoreDir = ctx.GlobalString(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 // Avoid conflicting network flags
checkExclusive(ctx, DeveloperFlag, TestnetFlag, RinkebyFlag, GoerliFlag) checkExclusive(ctx, DeveloperFlag, TestnetFlag, RinkebyFlag, GoerliFlag)
checkExclusive(ctx, LightServFlag, SyncModeFlag, "light") checkExclusive(ctx, LightServFlag, SyncModeFlag, "light")
// Can't use both ephemeral unlocked and external signer
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) 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) setEtherbase(ctx, ks, cfg)
setGPO(ctx, &cfg.GPO) setGPO(ctx, &cfg.GPO)
setTxPool(ctx, &cfg.TxPool) setTxPool(ctx, &cfg.TxPool)

@ -20,6 +20,7 @@ package clique
import ( import (
"bytes" "bytes"
"errors" "errors"
"io"
"math/big" "math/big"
"math/rand" "math/rand"
"sync" "sync"
@ -136,40 +137,9 @@ var (
errRecentlySigned = errors.New("recently signed") 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. // backing account.
type SignerFn func(accounts.Account, []byte) ([]byte, error) type SignerFn func(accounts.Account, string, []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
}
// ecrecover extracts the Ethereum account address from a signed header. // ecrecover extracts the Ethereum account address from a signed header.
func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, error) { 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:] signature := header.Extra[len(header.Extra)-extraSeal:]
// Recover the public key and the Ethereum address // 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 { if err != nil {
return common.Address{}, err 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)) log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))
} }
// Sign all the things! // 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 { if err != nil {
return err return err
} }
@ -663,7 +633,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c
select { select {
case results <- block.WithSeal(header): case results <- block.WithSeal(header):
default: 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. // SealHash returns the hash of a block prior to it being sealed.
func (c *Clique) SealHash(header *types.Header) common.Hash { 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. // 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, 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())
}
}

@ -80,7 +80,7 @@ func (ap *testerAccountPool) sign(header *types.Header, signer string) {
ap.accounts[signer], _ = crypto.GenerateKey() ap.accounts[signer], _ = crypto.GenerateKey()
} }
// Sign the header and embed the signature in extra data // 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) copy(header.Extra[len(header.Extra)-extraSeal:], sig)
} }

@ -434,7 +434,7 @@ func (s *Ethereum) StartMining(threads int) error {
log.Error("Etherbase account unavailable locally", "err", err) log.Error("Etherbase account unavailable locally", "err", err)
return fmt.Errorf("signer missing: %v", 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 // If mining is started, we can disable the transaction rejection mechanism
// introduced to speed sync times. // introduced to speed sync times.

@ -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 { func (s *PrivateAccountAPI) ListAccounts() []common.Address {
addresses := make([]common.Address, 0) // return [] instead of nil if empty addresses := make([]common.Address, 0) // return [] instead of nil if empty
for _, wallet := range s.am.Wallets() { for _, wallet := range s.am.Wallets() {
@ -409,18 +409,6 @@ func (s *PrivateAccountAPI) SignTransaction(ctx context.Context, args SendTxArgs
return &SignTransactionResult{data, signed}, nil 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: // Sign calculates an Ethereum ECDSA signature for:
// keccack256("\x19Ethereum Signed Message:\n" + len(message) + message)) // 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 return nil, err
} }
// Assemble sign the data with the wallet // 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 { if err != nil {
log.Warn("Failed data sign attempt", "address", addr, "err", err) log.Warn("Failed data sign attempt", "address", addr, "err", err)
return nil, 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 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 { if err != nil {
return common.Address{}, err return common.Address{}, err
} }
@ -1357,7 +1345,7 @@ func (s *PublicTransactionPoolAPI) Sign(addr common.Address, data hexutil.Bytes)
return nil, err return nil, err
} }
// Sign the requested hash with the wallet // Sign the requested hash with the wallet
signature, err := wallet.SignHash(account, signHash(data)) signature, err := wallet.SignText(account, data)
if err == nil { if err == nil {
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
} }

@ -27,6 +27,7 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/accounts" "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/keystore"
"github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -80,6 +81,9 @@ type Config struct {
// is created by New and destroyed when the node is stopped. // is created by New and destroyed when the node is stopped.
KeyStoreDir string `toml:",omitempty"` 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 // UseLightweightKDF lowers the memory and CPU requirements of the key store
// scrypt KDF at the expense of security. // scrypt KDF at the expense of security.
UseLightweightKDF bool `toml:",omitempty"` UseLightweightKDF bool `toml:",omitempty"`
@ -462,23 +466,37 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
return nil, "", err return nil, "", err
} }
// Assemble the account manager and supported backends // Assemble the account manager and supported backends
backends := []accounts.Backend{ backends := []accounts.Backend{}
keystore.NewKeyStore(keydir, scryptN, scryptP), if len(conf.ExternalSigner) > 0 {
} log.Info("Using external signer", "url", conf.ExternalSigner)
if !conf.NoUSB { if extapi, err := external.NewExternalBackend(conf.ExternalSigner); err == nil {
// Start a USB hub for Ledger hardware wallets backends = append(backends, extapi)
if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
} else { } 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 return accounts.NewManager(backends...), ephemeral, nil
} }

@ -36,8 +36,14 @@ import (
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive const (
const numberOfAccountsToDerive = 10 // 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. // ExternalAPI defines the external API through which signing requests are made.
type ExternalAPI interface { type ExternalAPI interface {
@ -55,6 +61,7 @@ type ExternalAPI interface {
// Should be moved to Internal API, in next phase when we have // Should be moved to Internal API, in next phase when we have
// bi-directional communication // bi-directional communication
//Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) //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 // 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 return nil, err
} }
// Assemble sign the data with the wallet // 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 { if err != nil {
api.UI.ShowError(err.Error()) api.UI.ShowError(err.Error())
return nil, err 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 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
}

@ -29,7 +29,6 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/accounts/keystore" "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"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "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") db, err := NewAbiDBFromFile("../../cmd/clef/4byte.json")
if err != nil { if err != nil {
utils.Fatalf(err.Error()) t.Fatal(err.Error())
} }
var ( var (
ui = &HeadlessUI{controller} ui = &HeadlessUI{controller}

@ -80,14 +80,13 @@ func (l *AuditLogger) Export(ctx context.Context, addr common.Address) (json.Raw
return j, e return j, e
} }
//func (l *AuditLogger) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) { func (l *AuditLogger) Version(ctx context.Context) (string, error) {
// // Don't actually log the json contents l.log.Info("Version", "type", "request", "metadata", MetadataFromContext(ctx).String())
// l.log.Info("Import", "type", "request", "metadata", MetadataFromContext(ctx).String(), data, err := l.api.Version(ctx)
// "keyJSON size", len(keyJSON)) l.log.Info("Version", "type", "response", "data", data, "error", err)
// a, e := l.api.Import(ctx, keyJSON) return data, err
// l.log.Info("Import", "type", "response", "addr", a.String(), "error", e)
// return a, e }
//}
func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) { func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) {
l := log.New("api", "signer") l := log.New("api", "signer")