From 6a49fd22c5320577af75507b9460665f22879bd8 Mon Sep 17 00:00:00 2001 From: Bas van Kervel Date: Wed, 12 Jul 2017 15:28:08 +0200 Subject: [PATCH] signer: introduce external signer command --- accounts/usbwallet/hub.go | 2 +- accounts/usbwallet/wallet.go | 2 +- cmd/signer/README.md | 342 +++++++++++++++++++++++++++++++++++ cmd/signer/api.go | 229 +++++++++++++++++++++++ cmd/signer/demo.js | 141 +++++++++++++++ cmd/signer/main.go | 68 +++++++ cmd/signer/test.js | 24 +++ cmd/signer/types.go | 39 ++++ 8 files changed, 845 insertions(+), 2 deletions(-) create mode 100644 cmd/signer/README.md create mode 100644 cmd/signer/api.go create mode 100644 cmd/signer/demo.js create mode 100644 cmd/signer/main.go create mode 100644 cmd/signer/test.js create mode 100644 cmd/signer/types.go diff --git a/accounts/usbwallet/hub.go b/accounts/usbwallet/hub.go index 61fc98ccc8..640320bc91 100644 --- a/accounts/usbwallet/hub.go +++ b/accounts/usbwallet/hub.go @@ -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 diff --git a/accounts/usbwallet/wallet.go b/accounts/usbwallet/wallet.go index 8b3b5a5224..6cef6e0fb0 100644 --- a/accounts/usbwallet/wallet.go +++ b/accounts/usbwallet/wallet.go @@ -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). diff --git a/cmd/signer/README.md b/cmd/signer/README.md new file mode 100644 index 0000000000..bd43f8acd1 --- /dev/null +++ b/cmd/signer/README.md @@ -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 + } +} +``` + + + diff --git a/cmd/signer/api.go b/cmd/signer/api.go new file mode 100644 index 0000000000..66c09f1d05 --- /dev/null +++ b/cmd/signer/api.go @@ -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 . + +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 +} \ No newline at end of file diff --git a/cmd/signer/demo.js b/cmd/signer/demo.js new file mode 100644 index 0000000000..921a35bbbe --- /dev/null +++ b/cmd/signer/demo.js @@ -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); diff --git a/cmd/signer/main.go b/cmd/signer/main.go new file mode 100644 index 0000000000..803e9e8ef5 --- /dev/null +++ b/cmd/signer/main.go @@ -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 . + +// 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() +} diff --git a/cmd/signer/test.js b/cmd/signer/test.js new file mode 100644 index 0000000000..4f50f1834b --- /dev/null +++ b/cmd/signer/test.js @@ -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)); diff --git a/cmd/signer/types.go b/cmd/signer/types.go new file mode 100644 index 0000000000..926a6241d5 --- /dev/null +++ b/cmd/signer/types.go @@ -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 . + +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"` +}