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,7 @@ 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. // @@ -98,7 +99,20 @@ 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) + 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. // @@ -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 . + +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 . + +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) - } - // 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) + log.Info("Error configuring external signer", "error", err) } } + 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")