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
|
||||
// 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
|
||||
// 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.
|
||||
hub.commsLock.Lock()
|
||||
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
|
||||
// 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
|
||||
// however should allow "skipping" waiting for operations that might want to
|
||||
// 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