mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-18 01:43:47 +00:00
signer: introduce external signer command
This commit is contained in:
parent
1100e8ba63
commit
6a49fd22c5
8 changed files with 845 additions and 2 deletions
|
|
@ -127,7 +127,7 @@ func (hub *Hub) refreshWallets() {
|
||||||
// breaking the Ledger protocol if that is waiting for user confirmation. This
|
// breaking the Ledger protocol if that is waiting for user confirmation. This
|
||||||
// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
|
// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
|
||||||
// need to prevent concurrent comms ourselves. The more elegant solution would
|
// need to prevent concurrent comms ourselves. The more elegant solution would
|
||||||
// be to ditch enumeration in favor of hutplug events, but that don't work yet
|
// be to ditch enumeration in favor of hotplug events, but that don't work yet
|
||||||
// on Windows so if we need to hack it anyway, this is more elegant for now.
|
// on Windows so if we need to hack it anyway, this is more elegant for now.
|
||||||
hub.commsLock.Lock()
|
hub.commsLock.Lock()
|
||||||
if hub.commsPend > 0 { // A confirmation is pending, don't refresh
|
if hub.commsPend > 0 { // A confirmation is pending, don't refresh
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ type wallet struct {
|
||||||
//
|
//
|
||||||
// As such, a hardware wallet needs two locks to function correctly. A state
|
// As such, a hardware wallet needs two locks to function correctly. A state
|
||||||
// lock can be used to protect the wallet's software-side internal state, which
|
// lock can be used to protect the wallet's software-side internal state, which
|
||||||
// must not be held exlusively during hardware communication. A communication
|
// must not be held exclusively during hardware communication. A communication
|
||||||
// lock can be used to achieve exclusive access to the device itself, this one
|
// lock can be used to achieve exclusive access to the device itself, this one
|
||||||
// however should allow "skipping" waiting for operations that might want to
|
// however should allow "skipping" waiting for operations that might want to
|
||||||
// use the device, but can live without too (e.g. account self-derivation).
|
// use the device, but can live without too (e.g. account self-derivation).
|
||||||
|
|
|
||||||
342
cmd/signer/README.md
Normal file
342
cmd/signer/README.md
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
**Signer API**
|
||||||
|
----
|
||||||
|
The signer utility can be used to sign transactions and data and is meant as a replacement for geth's account management.
|
||||||
|
This allows DApp's not to depend on geth's account management. When a DApp wants to sign data it can send the data to
|
||||||
|
the signer, the signer will than provide the user with context and asks the user for permission to sign the data. If
|
||||||
|
the users grants the signing request the signer will send the signature back to the DApp.
|
||||||
|
|
||||||
|
This setup allows a DApp to connect to a remote Ethereum node and send transactions that are locally signed. This can
|
||||||
|
help in situations when a DApp is connected to a remote node because a local Ethereum node is not available, not
|
||||||
|
synchronised with the chain or a particular Ethereum node that has no build in, or limited account management.
|
||||||
|
|
||||||
|
In its current form the signer is very limited and designed to work with Mist. It hasn't got a connection to an
|
||||||
|
Ethereum node. This restriction imposed many limitations such as the lack of ability to keep track of nonces, balances
|
||||||
|
or fetching additional information that can help the user to make a decision to sign a transaction or data. Currently
|
||||||
|
the signer only supports password protected accounts. Support for hardware tokens such as Trezor and Legder is planned.
|
||||||
|
|
||||||
|
## Command line flags
|
||||||
|
The signer accepts the following command line options:
|
||||||
|
- keystore, the directory where the password protected keystore stores keyfiles. The default directory is within geth's datadir. It is OS dependand, use `signer -h` to see where the location is on your system.
|
||||||
|
- chainid, the chain identifier. Default value is the Ethereum mainnet. See of a list of chain identifiers that are used https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
signer -keystore /my/keystore -chainid 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Communicating
|
||||||
|
The signer listens on stdin for incoming requests and sends responses on stdout. Messages are expected to follow the
|
||||||
|
[jsonrpc 2.0 standard](http://www.jsonrpc.org/specification).
|
||||||
|
|
||||||
|
Some of these call can require user interaction. Clients must be aware that responses may be deplayed significanlty or
|
||||||
|
may never be received if a users decideds to ignore the confirmation request.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Encoding
|
||||||
|
- number: positive integers that are hex encoded
|
||||||
|
- data: hex encoded data
|
||||||
|
- string: ASCII string
|
||||||
|
|
||||||
|
All hex encoded values must be prefixed with `0x`.
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### account_new
|
||||||
|
|
||||||
|
#### Create new password protected account
|
||||||
|
The signer will generate a new private key, encrypts it according to [web3 keystore spec](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) and stores it in the keystore directory.
|
||||||
|
The client is responsible for creating a backup of the keystore. If the keystore is lost there is no method of retrieving
|
||||||
|
lost accounts.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
- passphrase [string]: passphrase that is used to protect the private key that is stored within the keystore
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
- address [string]: account address that is derived from the generated key
|
||||||
|
- url [string]: location of the keyfile
|
||||||
|
|
||||||
|
#### Sample call
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "account_new",
|
||||||
|
"params": [
|
||||||
|
"my password"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"address": "0xbea9183f8f4f03d427f6bcea17388bdff1cab133",
|
||||||
|
"url": "keystore:///my/keystore/UTC--2017-08-24T08-40-15.419655028Z--bea9183f8f4f03d427f6bcea17388bdff1cab133"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### account_list
|
||||||
|
|
||||||
|
#### List available accounts
|
||||||
|
List all accounts that this signer currently manages
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
none
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
- array with account records:
|
||||||
|
- account.address [string]: account address that is derived from the generated key
|
||||||
|
- account.type [string]: type of the
|
||||||
|
- account.url [string]: location of the account
|
||||||
|
|
||||||
|
#### Sample call
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "account_list"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"address": "0xafb2f771f58513609765698f65d3f2f0224a956f",
|
||||||
|
"type": "account",
|
||||||
|
"url": "keystore:///tmp/keystore/UTC--2017-08-24T07-26-47.162109726Z--afb2f771f58513609765698f65d3f2f0224a956f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "0xbea9183f8f4f03d427f6bcea17388bdff1cab133",
|
||||||
|
"type": "account",
|
||||||
|
"url": "keystore:///tmp/keystore/UTC--2017-08-24T08-40-15.419655028Z--bea9183f8f4f03d427f6bcea17388bdff1cab133"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### account_signTransaction
|
||||||
|
|
||||||
|
#### Sign transactions
|
||||||
|
Signs a transactions and respons with the signed transaction in RLP encoded form.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
- from [address]: account to send the transaction from
|
||||||
|
- passphrase [string]: passphrase to unlock the from account
|
||||||
|
- Transaction object:
|
||||||
|
- transaction.to [address]: receiver account
|
||||||
|
- gas [number]: maximum amount of gas to burn
|
||||||
|
- gasPrice [number]: gas price
|
||||||
|
- value [number:optional]: amount of Wei to send with the transaction
|
||||||
|
- data [data:optional]: input data
|
||||||
|
- transaction.nonce [number]: account nonce
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
- signed transaction in RLP encoded form [data]
|
||||||
|
|
||||||
|
#### Sample call
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "account_signTransaction",
|
||||||
|
"params": [
|
||||||
|
"0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db",
|
||||||
|
"my password",
|
||||||
|
{
|
||||||
|
"gas": "0x55555",
|
||||||
|
"gasPrice": "0x1234",
|
||||||
|
"input": "0xabcd",
|
||||||
|
"nonce": "0x0",
|
||||||
|
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0",
|
||||||
|
"value": "0x1234"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": "0xf86480821234830555559407a565b7ed7d7a678680a4c162885bedbb695fe0821234802ea028f9ebeff90732eae45692a11c4ca2ef7f631a0a25bf8763d093e770c4ec464aa01fae77b24617913e718b989be78bc1aabb2fed3f2d4e3b93bd36759f1b5b4904"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### account_sign
|
||||||
|
|
||||||
|
#### Sign data
|
||||||
|
Signs a chunk of data and returns the calculated signature.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
- account [address]: account to sign with
|
||||||
|
- passphrase [string]: passphrase to unlock the account
|
||||||
|
- data [data]: data to sign
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
- calculated signature [data]
|
||||||
|
|
||||||
|
#### Sample call
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "account_sign",
|
||||||
|
"params": [
|
||||||
|
"0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db",
|
||||||
|
"my password",
|
||||||
|
"0xaabbccdd"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": "0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### account_ecRecover
|
||||||
|
|
||||||
|
#### Recover address
|
||||||
|
Derive the address from the account that was used to sign data from the data and signature.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
- data [data]: data that was signed
|
||||||
|
- signature [data]: the signature to verify
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
- derived account [address]
|
||||||
|
|
||||||
|
#### Sample call
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "account_ecRecover",
|
||||||
|
"params": [
|
||||||
|
"0xaabbccdd",
|
||||||
|
"0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db"
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### account_import
|
||||||
|
|
||||||
|
#### Import account
|
||||||
|
Import a private key into the keystore. The imported key is expected to be encrypted according to the web3 keystore
|
||||||
|
format.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
- account [object]: key in [web3 keystore format](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) (retrieved with account_export)
|
||||||
|
- passphrase [string]: password to decrypt the given account
|
||||||
|
- newPassphrase [string]: password to encrypt the imported key in the keystore with
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
- imported key [object]:
|
||||||
|
- key.address [address]: address of the imported key
|
||||||
|
- key.type [string]: type of the account
|
||||||
|
- key.url [string]: key URL
|
||||||
|
|
||||||
|
#### Sample call
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "account_import",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"address": "c7412fc59930fd90099c917a50e5f11d0934b2f5",
|
||||||
|
"crypto": {
|
||||||
|
"cipher": "aes-128-ctr",
|
||||||
|
"cipherparams": {
|
||||||
|
"iv": "401c39a7c7af0388491c3d3ecb39f532"
|
||||||
|
},
|
||||||
|
"ciphertext": "eb045260b18dd35cd0e6d99ead52f8fa1e63a6b0af2d52a8de198e59ad783204",
|
||||||
|
"kdf": "scrypt",
|
||||||
|
"kdfparams": {
|
||||||
|
"dklen": 32,
|
||||||
|
"n": 262144,
|
||||||
|
"p": 1,
|
||||||
|
"r": 8,
|
||||||
|
"salt": "9a657e3618527c9b5580ded60c12092e5038922667b7b76b906496f021bb841a"
|
||||||
|
},
|
||||||
|
"mac": "880dc10bc06e9cec78eb9830aeb1e7a4a26b4c2c19615c94acb632992b952806"
|
||||||
|
},
|
||||||
|
"id": "09bccb61-b8d3-4e93-bf4f-205a8194f0b9",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"my password",
|
||||||
|
"my password"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"address": "0xc7412fc59930fd90099c917a50e5f11d0934b2f5",
|
||||||
|
"type": "account",
|
||||||
|
"url": "keystore:///tmp/keystore/UTC--2017-08-24T11-00-42.032024108Z--c7412fc59930fd90099c917a50e5f11d0934b2f5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### account_export
|
||||||
|
|
||||||
|
#### Export account from keystore
|
||||||
|
Export a private key from the keystore. The exported private key is encrypted with the original passphrase. When the
|
||||||
|
key is imported later this passphrase is required.
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
- account [address]: export private key that is associated with this account
|
||||||
|
|
||||||
|
#### Result
|
||||||
|
- exported key, see [web3 keystore format](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) for
|
||||||
|
more information
|
||||||
|
|
||||||
|
#### Sample call
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "account_export",
|
||||||
|
"params": [
|
||||||
|
"0xc7412fc59930fd90099c917a50e5f11d0934b2f5"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"address": "c7412fc59930fd90099c917a50e5f11d0934b2f5",
|
||||||
|
"crypto": {
|
||||||
|
"cipher": "aes-128-ctr",
|
||||||
|
"cipherparams": {
|
||||||
|
"iv": "401c39a7c7af0388491c3d3ecb39f532"
|
||||||
|
},
|
||||||
|
"ciphertext": "eb045260b18dd35cd0e6d99ead52f8fa1e63a6b0af2d52a8de198e59ad783204",
|
||||||
|
"kdf": "scrypt",
|
||||||
|
"kdfparams": {
|
||||||
|
"dklen": 32,
|
||||||
|
"n": 262144,
|
||||||
|
"p": 1,
|
||||||
|
"r": 8,
|
||||||
|
"salt": "9a657e3618527c9b5580ded60c12092e5038922667b7b76b906496f021bb841a"
|
||||||
|
},
|
||||||
|
"mac": "880dc10bc06e9cec78eb9830aeb1e7a4a26b4c2c19615c94acb632992b952806"
|
||||||
|
},
|
||||||
|
"id": "09bccb61-b8d3-4e93-bf4f-205a8194f0b9",
|
||||||
|
"version": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
229
cmd/signer/api.go
Normal file
229
cmd/signer/api.go
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
// Copyright 2017 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/usbwallet"
|
||||||
|
"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/log"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SignerAPI struct {
|
||||||
|
chainID *big.Int
|
||||||
|
am *accounts.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSignerAPI creates a new API that can be used for account management.
|
||||||
|
// ksLocation specifies the directory where to store the password protected private
|
||||||
|
// key that is generated when a new account is created.
|
||||||
|
// noUSB disables USB support that is required to support hardware devices such as
|
||||||
|
// ledger and trezor.
|
||||||
|
func NewSignerAPI(chainID int64, ksLocation string, noUSB bool) *SignerAPI {
|
||||||
|
var backends []accounts.Backend
|
||||||
|
|
||||||
|
// support password based accounts
|
||||||
|
if len(ksLocation) > 0 {
|
||||||
|
backends = append(backends, keystore.NewKeyStore(ksLocation, keystore.StandardScryptN, keystore.StandardScryptP))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !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)
|
||||||
|
log.Debug("Ledger support enabled")
|
||||||
|
}
|
||||||
|
// 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.Debug("Trezor support enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns the set of wallet this signer manages. Each wallet can contain
|
||||||
|
// multiple accounts.
|
||||||
|
func (api *SignerAPI) List(ctx context.Context) []Account {
|
||||||
|
var accounts []Account
|
||||||
|
for _, wallet := range api.am.Wallets() {
|
||||||
|
for _, acc := range wallet.Accounts() {
|
||||||
|
acc := Account{Typ: "account", URL: wallet.URL(), Address: acc.Address}
|
||||||
|
accounts = append(accounts, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new password protected account. The private key is protected with
|
||||||
|
// the given password. Users are responsible to backup the private key that is stored
|
||||||
|
// in the keystore location thas was specified when this API was created.
|
||||||
|
func (api *SignerAPI) New(ctx context.Context, passphrase string) (accounts.Account, error) {
|
||||||
|
be := api.am.Backends(keystore.KeyStoreType)
|
||||||
|
if len(be) == 0 {
|
||||||
|
return accounts.Account{}, errors.New("password based accounts not supported")
|
||||||
|
}
|
||||||
|
acc, err := be[0].(*keystore.KeyStore).NewAccount(passphrase)
|
||||||
|
return acc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignTransaction signs the given transaction and returns it in an RLP encoded form
|
||||||
|
// that can be posted to `eth_sendRawTransaction`.
|
||||||
|
func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address, passwd string, args TransactionArg) (hexutil.Bytes, error) {
|
||||||
|
acc := accounts.Account{Address: from}
|
||||||
|
|
||||||
|
wallet, err := api.am.Find(acc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx *types.Transaction
|
||||||
|
if args.To == nil {
|
||||||
|
tx = types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
|
||||||
|
} else {
|
||||||
|
tx = types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedTx, err := wallet.SignTxWithPassphrase(acc, passwd, tx, api.chainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rlp.EncodeToBytes(signedTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign calculates an Ethereum ECDSA signature for:
|
||||||
|
// keccack256("\x19Ethereum Signed Message:\n" + len(message) + message))
|
||||||
|
//
|
||||||
|
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
||||||
|
// where the V value will be 27 or 28 for legacy reasons.
|
||||||
|
//
|
||||||
|
// The key used to calculate the signature is decrypted with the given password.
|
||||||
|
//
|
||||||
|
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
|
||||||
|
func (api *SignerAPI) Sign(ctx context.Context, addr common.Address, passwd string, data hexutil.Bytes) (hexutil.Bytes, error) {
|
||||||
|
// Look up the wallet containing the requested signer
|
||||||
|
account := accounts.Account{Address: addr}
|
||||||
|
|
||||||
|
wallet, err := api.am.Find(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Assemble sign the data with the wallet
|
||||||
|
signature, err := wallet.SignHashWithPassphrase(account, passwd, signHash(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||||
|
return signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EcRecover returns the address for the account that was used to create the signature.
|
||||||
|
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
|
||||||
|
// the address of:
|
||||||
|
// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
|
||||||
|
// addr = ecrecover(hash, signature)
|
||||||
|
//
|
||||||
|
// Note, the signature must conform to the secp256k1 curve R, S and V values, where
|
||||||
|
// the V value must be be 27 or 28 for legacy reasons.
|
||||||
|
//
|
||||||
|
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecover
|
||||||
|
func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) {
|
||||||
|
if len(sig) != 65 {
|
||||||
|
return common.Address{}, fmt.Errorf("signature must be 65 bytes long")
|
||||||
|
}
|
||||||
|
if sig[64] != 27 && sig[64] != 28 {
|
||||||
|
return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
|
||||||
|
}
|
||||||
|
sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
|
||||||
|
|
||||||
|
rpk, err := crypto.Ecrecover(signHash(data), sig)
|
||||||
|
if err != nil {
|
||||||
|
return common.Address{}, err
|
||||||
|
}
|
||||||
|
pubKey := crypto.ToECDSAPub(rpk)
|
||||||
|
recoveredAddr := crypto.PubkeyToAddress(*pubKey)
|
||||||
|
return recoveredAddr, 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export returns encrypted private key associated with the given address in web3 keystore format.
|
||||||
|
func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
|
||||||
|
// Look up the wallet containing the requested signer
|
||||||
|
account := accounts.Account{Address: addr}
|
||||||
|
|
||||||
|
wallet, err := api.am.Find(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
url := wallet.URL()
|
||||||
|
if url.Scheme != keystore.KeyStoreScheme {
|
||||||
|
return nil, fmt.Errorf("account is not a password protected account")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.ReadFile(url.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
|
||||||
|
// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
|
||||||
|
// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
|
||||||
|
func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage, passphrase, newPassphrase string) (Account, error) {
|
||||||
|
be := api.am.Backends(keystore.KeyStoreType)
|
||||||
|
if len(be) == 0 {
|
||||||
|
return Account{}, errors.New("password based accounts not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, passphrase, newPassphrase)
|
||||||
|
if err != nil {
|
||||||
|
return Account{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Account{Typ: "account", URL: acc.URL, Address: acc.Address}, nil
|
||||||
|
}
|
||||||
141
cmd/signer/demo.js
Normal file
141
cmd/signer/demo.js
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
/* Demonstrates the following signer methods
|
||||||
|
|
||||||
|
- account_new: generate new password protected account
|
||||||
|
1. password: string
|
||||||
|
|
||||||
|
returns account object with address and URL
|
||||||
|
|
||||||
|
- account_list: listing of accounts
|
||||||
|
no args
|
||||||
|
|
||||||
|
returns array with accounts
|
||||||
|
|
||||||
|
- account_signTransaction: sign transaction and get tx in RLP encoded form back
|
||||||
|
1. from: address
|
||||||
|
2. passwd: string
|
||||||
|
3. transaction: object
|
||||||
|
|
||||||
|
returns signed transaction in RLP form (can be used with eth_sendRawTransaction)
|
||||||
|
|
||||||
|
- account_sign: calculate signature
|
||||||
|
1. from: address
|
||||||
|
2. passwd: string
|
||||||
|
3. data: hex string
|
||||||
|
|
||||||
|
returns signature
|
||||||
|
|
||||||
|
- account_ecRecover: derive address from signature
|
||||||
|
1. data: hex string
|
||||||
|
2. signature: hex string
|
||||||
|
|
||||||
|
returns address
|
||||||
|
*/
|
||||||
|
|
||||||
|
var spawn = require('child_process').spawn;
|
||||||
|
|
||||||
|
// by default the signer used the keystore for the mainnet, in this case it is pointed to a non-standard location.
|
||||||
|
// also it accepts the chainid, by default it uses the chainid for the mainnet.
|
||||||
|
const signer = spawn('./signer', ['-keystore', '/tmp/keystore', '-chainid', 5]);
|
||||||
|
const passwd = 'my password';
|
||||||
|
var createdAccountAddress = '0x';
|
||||||
|
var signData = '0xaabbccdd';
|
||||||
|
var signSignature = '0x';
|
||||||
|
var keystoreKeyData = '0x';
|
||||||
|
|
||||||
|
var currentRequest = -1;
|
||||||
|
function nextRequest() {
|
||||||
|
currentRequest++;
|
||||||
|
var req = null;
|
||||||
|
|
||||||
|
if (currentRequest < 7) {
|
||||||
|
req = {
|
||||||
|
id: currentRequest,
|
||||||
|
jsonrpc: "2.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (currentRequest) {
|
||||||
|
case 0:
|
||||||
|
req.method = 'account_new';
|
||||||
|
req.params = [passwd];
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
req.method = 'account_list';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
req.method = 'account_signTransaction';
|
||||||
|
req.params = [createdAccountAddress, passwd, {
|
||||||
|
nonce: "0x0",
|
||||||
|
gasPrice: "0x1234",
|
||||||
|
gas: "0x55555",
|
||||||
|
value: "0x1234",
|
||||||
|
input: "0xabcd",
|
||||||
|
to: "0x07a565b7ed7d7a678680a4c162885bedbb695fe0"
|
||||||
|
}];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
req.method = 'account_sign';
|
||||||
|
req.params = [createdAccountAddress, passwd, signData];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
req.method = 'account_ecRecover';
|
||||||
|
req.params = [signData, signSignature];
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
req.method = 'account_export';
|
||||||
|
req.params = [createdAccountAddress]
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
req.method = 'account_import';
|
||||||
|
req.params = [keystoreKeyData, passwd, passwd];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
signer.stdout.on('data', (data) => {
|
||||||
|
console.log(`${data}`);
|
||||||
|
|
||||||
|
response = JSON.parse(`${data}`);
|
||||||
|
|
||||||
|
switch (response.id) {
|
||||||
|
case 0:
|
||||||
|
createdAccountAddress = response.result.address;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
signSignature = response.result;
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
if (createdAccountAddress !== response.result) {
|
||||||
|
console.error("expected address", createdAccountAddress, "got", response.result);
|
||||||
|
} else {
|
||||||
|
//console.log("Address recovered correct");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
keystoreKeyData = response.result;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = nextRequest();
|
||||||
|
if (req !== null) {
|
||||||
|
req = JSON.stringify(req);
|
||||||
|
console.log(req);
|
||||||
|
signer.stdin.write(req);
|
||||||
|
} else {
|
||||||
|
signer.kill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
signer.stderr.on('data', (data) => {
|
||||||
|
console.log(`stderr: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
signer.on('close', (code) => {
|
||||||
|
//console.log(`signer process exited with code ${code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// kickstart request cycle
|
||||||
|
req = JSON.stringify(nextRequest());
|
||||||
|
console.log(req);
|
||||||
|
signer.stdin.write(req);
|
||||||
68
cmd/signer/main.go
Normal file
68
cmd/signer/main.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2017 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/>.
|
||||||
|
|
||||||
|
// signer is a utility that can be used so sign transactions and
|
||||||
|
// arbitrary data.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
|
"github.com/ethereum/go-ethereum/node"
|
||||||
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ksLocation = flag.String("keystore", filepath.Join(node.DefaultDataDir(), "keystore"), "Directory for the keystore")
|
||||||
|
chainID = flag.Int64("chainid", params.MainnetChainConfig.ChainId.Int64(), "chain identifier")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var (
|
||||||
|
server = rpc.NewServer()
|
||||||
|
api = NewSignerAPI(*chainID, *ksLocation, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// register signer API with server
|
||||||
|
if err := server.RegisterName("account", api); err != nil {
|
||||||
|
utils.Fatalf("Could not register signer API: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start server with in-/output connected to stdin/stdout
|
||||||
|
in, out := bufio.NewReader(os.Stdin), os.Stdout
|
||||||
|
codec := rpc.NewJSONCodec(&rwc{in, out})
|
||||||
|
server.ServeCodec(codec, rpc.OptionMethodInvocation|rpc.OptionSubscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rwc struct {
|
||||||
|
io.Reader
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rwc) Close() error {
|
||||||
|
if err := os.Stdin.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Stdout.Close()
|
||||||
|
}
|
||||||
24
cmd/signer/test.js
Normal file
24
cmd/signer/test.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
const net = require('net');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let conn = net.connect(path.join('\\\\?\\pipe', 'ethereum-signer'))
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
id: 1234,
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
//method: 'account_list',
|
||||||
|
method: 'account_signTransaction',
|
||||||
|
params: ['0xaabbccddaabbccddaabbccddaabbccddaabbccdd', {
|
||||||
|
to: '0x0011223344556677889900112233445566778899',
|
||||||
|
value: '0x123450000',
|
||||||
|
data: '0xabcdef',
|
||||||
|
gas: '0x12345',
|
||||||
|
gasPrice: '0x67890'
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
conn.on('data', (data) => {
|
||||||
|
console.log(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.write(JSON.stringify(req));
|
||||||
39
cmd/signer/types.go
Normal file
39
cmd/signer/types.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2017 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Typ string `json:"type"`
|
||||||
|
URL accounts.URL `json:"url"`
|
||||||
|
Address common.Address `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionArg represents a transaction for the signer.
|
||||||
|
type TransactionArg struct {
|
||||||
|
To *common.Address `json:"to"`
|
||||||
|
Gas *hexutil.Big `json:"gas"`
|
||||||
|
GasPrice *hexutil.Big `json:"gasPrice"`
|
||||||
|
Value *hexutil.Big `json:"value"`
|
||||||
|
Data hexutil.Bytes `json:"data"`
|
||||||
|
Nonce *hexutil.Uint64 `json:"nonce"`
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue