mirror of https://github.com/ethereum/go-ethereum
cmd/clef, signer: initial poc of the standalone signer (#16154)
* signer: introduce external signer command * cmd/signer, rpc: Implement new signer. Add info about remote user to Context * signer: refactored request/response, made use of urfave.cli * cmd/signer: Use common flags * cmd/signer: methods to validate calldata against abi * cmd/signer: work on abi parser * signer: add mutex around UI * cmd/signer: add json 4byte directory, remove passwords from api * cmd/signer: minor changes * cmd/signer: Use ErrRequestDenied, enable lightkdf * cmd/signer: implement tests * cmd/signer: made possible for UI to modify tx parameters * cmd/signer: refactors, removed channels in ui comms, added UI-api via stdin/out * cmd/signer: Made lowercase json-definitions, added UI-signer test functionality * cmd/signer: update documentation * cmd/signer: fix bugs, improve abi detection, abi argument display * cmd/signer: minor change in json format * cmd/signer: rework json communication * cmd/signer: implement mixcase addresses in API, fix json id bug * cmd/signer: rename fromaccount, update pythonpoc with new json encoding format * cmd/signer: make use of new abi interface * signer: documentation * signer/main: remove redundant option * signer: implement audit logging * signer: create package 'signer', minor changes * common: add 0x-prefix to mixcaseaddress in json marshalling + validation * signer, rules, storage: implement rules + ephemeral storage for signer rules * signer: implement OnApprovedTx, change signing response (API BREAKAGE) * signer: refactoring + documentation * signer/rules: implement dispatching to next handler * signer: docs * signer/rules: hide json-conversion from users, ensure context is cleaned * signer: docs * signer: implement validation rules, change signature of call_info * signer: fix log flaw with string pointer * signer: implement custom 4byte databsae that saves submitted signatures * signer/storage: implement aes-gcm-backed credential storage * accounts: implement json unmarshalling of url * signer: fix listresponse, fix gas->uint64 * node: make http/ipc start methods public * signer: add ipc capability+review concerns * accounts: correct docstring * signer: address review concerns * rpc: go fmt -s * signer: review concerns+ baptize Clef * signer,node: move Start-functions to separate file * signer: formattingpull/16498/merge
parent
de2a7bb764
commit
ec3db0f56c
File diff suppressed because one or more lines are too long
@ -0,0 +1,864 @@ |
||||
Clef |
||||
---- |
||||
Clef can be used to sign transactions and data and is meant as a replacement for geth's account management. |
||||
This allows DApps 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 then 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 built-in (or limited) account management. |
||||
|
||||
Clef can run as a daemon on the same machine, or off a usb-stick like [usb armory](https://inversepath.com/usbarmory), |
||||
or a separate VM in a [QubesOS](https://www.qubes-os.org/) type os setup. |
||||
|
||||
|
||||
## Command line flags |
||||
Clef accepts the following command line options: |
||||
``` |
||||
COMMANDS: |
||||
init Initialize the signer, generate secret storage |
||||
attest Attest that a js-file is to be used |
||||
addpw Store a credential for a keystore file |
||||
help Shows a list of commands or help for one command |
||||
|
||||
GLOBAL OPTIONS: |
||||
--loglevel value log level to emit to the screen (default: 4) |
||||
--keystore value Directory for the keystore (default: "$HOME/.ethereum/keystore") |
||||
--configdir value Directory for clef configuration (default: "$HOME/.clef") |
||||
--networkid value Network identifier (integer, 1=Frontier, 2=Morden (disused), 3=Ropsten, 4=Rinkeby) (default: 1) |
||||
--lightkdf Reduce key-derivation RAM & CPU usage at some expense of KDF strength |
||||
--nousb Disables monitoring for and managing USB hardware wallets |
||||
--rpcaddr value HTTP-RPC server listening interface (default: "localhost") |
||||
--rpcport value HTTP-RPC server listening port (default: 8550) |
||||
--signersecret value A file containing the password used to encrypt signer credentials, e.g. keystore credentials and ruleset hash |
||||
--4bytedb value File containing 4byte-identifiers (default: "./4byte.json") |
||||
--4bytedb-custom value File used for writing new 4byte-identifiers submitted via API (default: "./4byte-custom.json") |
||||
--auditlog value File used to emit audit logs. Set to "" to disable (default: "audit.log") |
||||
--rules value Enable rule-engine (default: "rules.json") |
||||
--stdio-ui Use STDIN/STDOUT as a channel for an external UI. This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user interface, and can be used when the signer is started by an external process. |
||||
--stdio-ui-test Mechanism to test interface between signer and UI. Requires 'stdio-ui'. |
||||
--help, -h show help |
||||
--version, -v print the version |
||||
|
||||
``` |
||||
|
||||
|
||||
Example: |
||||
``` |
||||
signer -keystore /my/keystore -chainid 4 |
||||
``` |
||||
|
||||
Check out the [tutorial](tutorial.md) for some concrete examples on how the signer works. |
||||
|
||||
## Security model |
||||
|
||||
The security model of the signer is as follows: |
||||
|
||||
* One critical component (the signer binary / daemon) is responsible for handling cryptographic operations: signing, private keys, encryption/decryption of keystore files. |
||||
* The signer binary has a well-defined 'external' API. |
||||
* The 'external' API is considered UNTRUSTED. |
||||
* The signer binary also communicates with whatever process that invoked the binary, via stdin/stdout. |
||||
* This channel is considered 'trusted'. Over this channel, approvals and passwords are communicated. |
||||
|
||||
The general flow for signing a transaction using e.g. geth is as follows: |
||||
![image](sign_flow.png) |
||||
|
||||
In this case, `geth` would be started with `--externalsigner=http://localhost:8550` and would relay requests to `eth.sendTransaction`. |
||||
|
||||
## TODOs |
||||
|
||||
Some snags and todos |
||||
|
||||
* [ ] The signer should take a startup param "--no-change", for UIs that do not contain the capability |
||||
to perform changes to things, only approve/deny. Such a UI should be able to start the signer in |
||||
a more secure mode by telling it that it only wants approve/deny capabilities. |
||||
|
||||
* [x] It would be nice if the signer could collect new 4byte-id:s/method selectors, and have a |
||||
secondary database for those (`4byte_custom.json`). Users could then (optionally) submit their collections for |
||||
inclusion upstream. |
||||
|
||||
* It should be possible to configure the signer to check if an account is indeed known to it, before |
||||
passing on to the UI. The reason it currently does not, is that it would make it possible to enumerate |
||||
accounts if it immediately returned "unknown account". |
||||
* [x] It should be possible to configure the signer to auto-allow listing (certain) accounts, instead of asking every time. |
||||
* [x] Done Upon startup, the signer should spit out some info to the caller (particularly important when executed in `stdio-ui`-mode), |
||||
invoking methods with the following info: |
||||
* [x] Version info about the signer |
||||
* [x] Address of API (http/ipc) |
||||
* [ ] List of known accounts |
||||
* [ ] Have a default timeout on signing operations, so that if the user has not answered withing e.g. 60 seconds, the request is rejected. |
||||
* [ ] `account_signRawTransaction` |
||||
* [ ] `account_bulkSignTransactions([] transactions)` should |
||||
* only exist if enabled via config/flag |
||||
* only allow non-data-sending transactions |
||||
* all txs must use the same `from`-account |
||||
* let the user confirm, showing |
||||
* the total amount |
||||
* the number of unique recipients |
||||
|
||||
* Geth todos |
||||
- The signer should pass the `Origin` header as call-info to the UI. As of right now, the way that info about the request is |
||||
put together is a bit of a hack into the http server. This could probably be greatly improved |
||||
- Relay: Geth should be started in `geth --external_signer localhost:8550`. |
||||
- Currently, the Geth APIs use `common.Address` in the arguments to transaction submission (e.g `to` field). This |
||||
type is 20 `bytes`, and is incapable of carrying checksum information. The signer uses `common.MixedcaseAddress`, which |
||||
retains the original input. |
||||
- The Geth api should switch to use the same type, and relay `to`-account verbatim to the external api. |
||||
|
||||
* [x] Storage |
||||
* [x] An encrypted key-value storage should be implemented |
||||
* See [rules.md](rules.md) for more info about this. |
||||
|
||||
* Another potential thing to introduce is pairing. |
||||
* To prevent spurious requests which users just accept, implement a way to "pair" the caller with the signer (external API). |
||||
* Thus geth/mist/cpp would cryptographically handshake and afterwards the caller would be allowed to make signing requests. |
||||
* This feature would make the addition of rules less dangerous. |
||||
|
||||
* Wallets / accounts. Add API methods for wallets. |
||||
|
||||
## Communication |
||||
|
||||
### External API |
||||
|
||||
The signer listens to HTTP requests on `rpcaddr`:`rpcport`, with the same JSONRPC standard as Geth. The messages are |
||||
expected to be JSON [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 delayed significanlty or may never be received if a users decides to ignore the confirmation request. |
||||
|
||||
The External API is **untrusted** : it does not accept credentials over this api, nor does it expect |
||||
that requests have any authority. |
||||
|
||||
### UI API |
||||
|
||||
The signer has one native console-based UI, for operation without any standalone tools. |
||||
However, there is also an API to communicate with an external UI. To enable that UI, |
||||
the signer needs to be executed with the `--stdio-ui` option, which allocates the |
||||
`stdin`/`stdout` for the UI-api. |
||||
|
||||
An example (insecure) proof-of-concept of has been implemented in `pythonsigner.py`. |
||||
|
||||
The model is as follows: |
||||
|
||||
* The user starts the UI app (`pythonsigner.py`). |
||||
* The UI app starts the `signer` with `--stdio-ui`, and listens to the |
||||
process output for confirmation-requests. |
||||
* The `signer` opens the external http api. |
||||
* When the `signer` receives requests, it sends a `jsonrpc` request via `stdout`. |
||||
* The UI app prompts the user accordingly, and responds to the `signer` |
||||
* The `signer` signs (or not), and responds to the original request. |
||||
|
||||
## External API |
||||
|
||||
See the [external api changelog](extapi_changelog.md) for information about changes to this 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 |
||||
|
||||
None |
||||
|
||||
#### Result |
||||
- address [string]: account address that is derived from the generated key |
||||
- url [string]: location of the keyfile |
||||
|
||||
#### Sample call |
||||
```json |
||||
{ |
||||
"id": 0, |
||||
"jsonrpc": "2.0", |
||||
"method": "account_new", |
||||
"params": [] |
||||
} |
||||
|
||||
{ |
||||
"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 |
||||
```json |
||||
{ |
||||
"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 responds with the signed transaction in RLP encoded form. |
||||
|
||||
#### Arguments |
||||
2. transaction object: |
||||
- `from` [address]: account to send the transaction from |
||||
- `to` [address]: receiver account. If omitted or `0x`, will cause contract creation. |
||||
- `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 |
||||
- `nonce` [number]: account nonce |
||||
3. method signature [string:optional] |
||||
- The method signature, if present, is to aid decoding the calldata. Should consist of `methodname(paramtype,...)`, e.g. `transfer(uint256,address)`. The signer may use this data to parse the supplied calldata, and show the user. The data, however, is considered totally untrusted, and reliability is not expected. |
||||
|
||||
|
||||
#### Result |
||||
- signed transaction in RLP encoded form [data] |
||||
|
||||
#### Sample call |
||||
```json |
||||
{ |
||||
"id": 2, |
||||
"jsonrpc": "2.0", |
||||
"method": "account_signTransaction", |
||||
"params": [ |
||||
{ |
||||
"from": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db", |
||||
"gas": "0x55555", |
||||
"gasPrice": "0x1234", |
||||
"input": "0xabcd", |
||||
"nonce": "0x0", |
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", |
||||
"value": "0x1234" |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
Response |
||||
|
||||
```json |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 67, |
||||
"error": { |
||||
"code": -32000, |
||||
"message": "Request denied" |
||||
} |
||||
} |
||||
``` |
||||
#### Sample call with ABI-data |
||||
|
||||
|
||||
```json |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"method": "account_signTransaction", |
||||
"params": [ |
||||
{ |
||||
"from": "0x694267f14675d7e1b9494fd8d72fefe1755710fa", |
||||
"gas": "0x333", |
||||
"gasPrice": "0x1", |
||||
"nonce": "0x0", |
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", |
||||
"value": "0x0", |
||||
"data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012" |
||||
}, |
||||
"safeSend(address)" |
||||
], |
||||
"id": 67 |
||||
} |
||||
``` |
||||
Response |
||||
|
||||
```json |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 67, |
||||
"result": { |
||||
"raw": "0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", |
||||
"tx": { |
||||
"nonce": "0x0", |
||||
"gasPrice": "0x1", |
||||
"gas": "0x333", |
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", |
||||
"value": "0x0", |
||||
"input": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012", |
||||
"v": "0x26", |
||||
"r": "0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e", |
||||
"s": "0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", |
||||
"hash": "0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Bash example: |
||||
```bash |
||||
#curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/ |
||||
|
||||
{"jsonrpc":"2.0","id":67,"result":{"raw":"0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","tx":{"nonce":"0x0","gasPrice":"0x1","gas":"0x333","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0","value":"0x0","input":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012","v":"0x26","r":"0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e","s":"0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","hash":"0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e"}}} |
||||
``` |
||||
|
||||
|
||||
### account_sign |
||||
|
||||
#### Sign data |
||||
Signs a chunk of data and returns the calculated signature. |
||||
|
||||
#### Arguments |
||||
- account [address]: account to sign with |
||||
- data [data]: data to sign |
||||
|
||||
#### Result |
||||
- calculated signature [data] |
||||
|
||||
#### Sample call |
||||
```json |
||||
{ |
||||
"id": 3, |
||||
"jsonrpc": "2.0", |
||||
"method": "account_sign", |
||||
"params": [ |
||||
"0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db", |
||||
"0xaabbccdd" |
||||
] |
||||
} |
||||
``` |
||||
Response |
||||
|
||||
```json |
||||
{ |
||||
"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 |
||||
```json |
||||
{ |
||||
"id": 4, |
||||
"jsonrpc": "2.0", |
||||
"method": "account_ecRecover", |
||||
"params": [ |
||||
"0xaabbccdd", |
||||
"0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c" |
||||
] |
||||
} |
||||
``` |
||||
Response |
||||
|
||||
```json |
||||
{ |
||||
"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) |
||||
|
||||
#### 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 |
||||
```json |
||||
{ |
||||
"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 |
||||
}, |
||||
] |
||||
} |
||||
``` |
||||
Response |
||||
|
||||
```json |
||||
{ |
||||
"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 |
||||
```json |
||||
{ |
||||
"id": 5, |
||||
"jsonrpc": "2.0", |
||||
"method": "account_export", |
||||
"params": [ |
||||
"0xc7412fc59930fd90099c917a50e5f11d0934b2f5" |
||||
] |
||||
} |
||||
``` |
||||
Response |
||||
|
||||
```json |
||||
{ |
||||
"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 |
||||
} |
||||
} |
||||
``` |
||||
|
||||
|
||||
|
||||
## UI API |
||||
|
||||
These methods needs to be implemented by a UI listener. |
||||
|
||||
By starting the signer with the switch `--stdio-ui-test`, the signer will invoke all known methods, and expect the UI to respond with |
||||
denials. This can be used during development to ensure that the API is (at least somewhat) correctly implemented. |
||||
See `pythonsigner`, which can be invoked via `python3 pythonsigner.py test` to perform the 'denial-handshake-test'. |
||||
|
||||
All methods in this API uses object-based parameters, so that there can be no mixups of parameters: each piece of data is accessed by key. |
||||
|
||||
See the [ui api changelog](intapi_changelog.md) for information about changes to this API. |
||||
|
||||
OBS! A slight deviation from `json` standard is in place: every request and response should be confined to a single line. |
||||
Whereas the `json` specification allows for linebreaks, linebreaks __should not__ be used in this communication channel, to make |
||||
things simpler for both parties. |
||||
|
||||
### ApproveTx |
||||
|
||||
Invoked when there's a transaction for approval. |
||||
|
||||
|
||||
#### Sample call |
||||
|
||||
Here's a method invocation: |
||||
```bash |
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/ |
||||
``` |
||||
|
||||
```json |
||||
|
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 1, |
||||
"method": "ApproveTx", |
||||
"params": [ |
||||
{ |
||||
"transaction": { |
||||
"from": "0x0x694267f14675d7e1b9494fd8d72fefe1755710fa", |
||||
"to": "0x0x07a565b7ed7d7a678680a4c162885bedbb695fe0", |
||||
"gas": "0x333", |
||||
"gasPrice": "0x1", |
||||
"value": "0x0", |
||||
"nonce": "0x0", |
||||
"data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012", |
||||
"input": null |
||||
}, |
||||
"call_info": [ |
||||
{ |
||||
"type": "WARNING", |
||||
"message": "Invalid checksum on to-address" |
||||
}, |
||||
{ |
||||
"type": "Info", |
||||
"message": "safeSend(address: 0x0000000000000000000000000000000000000012)" |
||||
} |
||||
], |
||||
"meta": { |
||||
"remote": "127.0.0.1:48486", |
||||
"local": "localhost:8550", |
||||
"scheme": "HTTP/1.1" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
``` |
||||
|
||||
The same method invocation, but with invalid data: |
||||
```bash |
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000002000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/ |
||||
``` |
||||
|
||||
```json |
||||
|
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 1, |
||||
"method": "ApproveTx", |
||||
"params": [ |
||||
{ |
||||
"transaction": { |
||||
"from": "0x0x694267f14675d7e1b9494fd8d72fefe1755710fa", |
||||
"to": "0x0x07a565b7ed7d7a678680a4c162885bedbb695fe0", |
||||
"gas": "0x333", |
||||
"gasPrice": "0x1", |
||||
"value": "0x0", |
||||
"nonce": "0x0", |
||||
"data": "0x4401a6e40000000000000002000000000000000000000000000000000000000000000012", |
||||
"input": null |
||||
}, |
||||
"call_info": [ |
||||
{ |
||||
"type": "WARNING", |
||||
"message": "Invalid checksum on to-address" |
||||
}, |
||||
{ |
||||
"type": "WARNING", |
||||
"message": "Transaction data did not match ABI-interface: WARNING: Supplied data is stuffed with extra data. \nWant 0000000000000002000000000000000000000000000000000000000000000012\nHave 0000000000000000000000000000000000000000000000000000000000000012\nfor method safeSend(address)" |
||||
} |
||||
], |
||||
"meta": { |
||||
"remote": "127.0.0.1:48492", |
||||
"local": "localhost:8550", |
||||
"scheme": "HTTP/1.1" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
|
||||
``` |
||||
|
||||
One which has missing `to`, but with no `data`: |
||||
|
||||
|
||||
```json |
||||
|
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 3, |
||||
"method": "ApproveTx", |
||||
"params": [ |
||||
{ |
||||
"transaction": { |
||||
"from": "", |
||||
"to": null, |
||||
"gas": "0x0", |
||||
"gasPrice": "0x0", |
||||
"value": "0x0", |
||||
"nonce": "0x0", |
||||
"data": null, |
||||
"input": null |
||||
}, |
||||
"call_info": [ |
||||
{ |
||||
"type": "CRITICAL", |
||||
"message": "Tx will create contract with empty code!" |
||||
} |
||||
], |
||||
"meta": { |
||||
"remote": "signer binary", |
||||
"local": "main", |
||||
"scheme": "in-proc" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
### ApproveExport |
||||
|
||||
Invoked when a request to export an account has been made. |
||||
|
||||
#### Sample call |
||||
|
||||
```json |
||||
|
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 7, |
||||
"method": "ApproveExport", |
||||
"params": [ |
||||
{ |
||||
"address": "0x0000000000000000000000000000000000000000", |
||||
"meta": { |
||||
"remote": "signer binary", |
||||
"local": "main", |
||||
"scheme": "in-proc" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
``` |
||||
|
||||
### ApproveListing |
||||
|
||||
Invoked when a request for account listing has been made. |
||||
|
||||
#### Sample call |
||||
|
||||
```json |
||||
|
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 5, |
||||
"method": "ApproveListing", |
||||
"params": [ |
||||
{ |
||||
"accounts": [ |
||||
{ |
||||
"type": "Account", |
||||
"url": "keystore:///home/bazonk/.ethereum/keystore/UTC--2017-11-20T14-44-54.089682944Z--123409812340981234098123409812deadbeef42", |
||||
"address": "0x123409812340981234098123409812deadbeef42" |
||||
}, |
||||
{ |
||||
"type": "Account", |
||||
"url": "keystore:///home/bazonk/.ethereum/keystore/UTC--2017-11-23T21-59-03.199240693Z--cafebabedeadbeef34098123409812deadbeef42", |
||||
"address": "0xcafebabedeadbeef34098123409812deadbeef42" |
||||
} |
||||
], |
||||
"meta": { |
||||
"remote": "signer binary", |
||||
"local": "main", |
||||
"scheme": "in-proc" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
``` |
||||
|
||||
|
||||
### ApproveSignData |
||||
|
||||
#### Sample call |
||||
|
||||
```json |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 4, |
||||
"method": "ApproveSignData", |
||||
"params": [ |
||||
{ |
||||
"address": "0x123409812340981234098123409812deadbeef42", |
||||
"raw_data": "0x01020304", |
||||
"message": "\u0019Ethereum Signed Message:\n4\u0001\u0002\u0003\u0004", |
||||
"hash": "0x7e3a4e7a9d1744bc5c675c25e1234ca8ed9162bd17f78b9085e48047c15ac310", |
||||
"meta": { |
||||
"remote": "signer binary", |
||||
"local": "main", |
||||
"scheme": "in-proc" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
``` |
||||
|
||||
### ShowInfo |
||||
|
||||
The UI should show the info to the user. Does not expect response. |
||||
|
||||
#### Sample call |
||||
|
||||
```json |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 9, |
||||
"method": "ShowInfo", |
||||
"params": [ |
||||
{ |
||||
"text": "Tests completed" |
||||
} |
||||
] |
||||
} |
||||
|
||||
``` |
||||
|
||||
### ShowError |
||||
|
||||
The UI should show the info to the user. Does not expect response. |
||||
|
||||
```json |
||||
|
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 2, |
||||
"method": "ShowError", |
||||
"params": [ |
||||
{ |
||||
"text": "Testing 'ShowError'" |
||||
} |
||||
] |
||||
} |
||||
|
||||
``` |
||||
|
||||
### OnApproved |
||||
|
||||
`OnApprovedTx` is called when a transaction has been approved and signed. The call contains the return value that will be sent to the external caller. The return value from this method is ignored - the reason for having this callback is to allow the ruleset to keep track of approved transactions. |
||||
|
||||
When implementing rate-limited rules, this callback should be used. |
||||
|
||||
TLDR; Use this method to keep track of signed transactions, instead of using the data in `ApproveTx`. |
||||
|
||||
### OnSignerStartup |
||||
|
||||
This method provide the UI with information about what API version the signer uses (both internal and external) aswell as build-info and external api, |
||||
in k/v-form. |
||||
|
||||
Example call: |
||||
```json |
||||
|
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 1, |
||||
"method": "OnSignerStartup", |
||||
"params": [ |
||||
{ |
||||
"info": { |
||||
"extapi_http": "http://localhost:8550", |
||||
"extapi_ipc": null, |
||||
"extapi_version": "2.0.0", |
||||
"intapi_version": "1.2.0" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
``` |
||||
|
||||
|
||||
### Rules for UI apis |
||||
|
||||
A UI should conform to the following rules. |
||||
|
||||
* A UI MUST NOT load any external resources that were not embedded/part of the UI package. |
||||
* For example, not load icons, stylesheets from the internet |
||||
* Not load files from the filesystem, unless they reside in the same local directory (e.g. config files) |
||||
* A Graphical UI MUST show the blocky-identicon for ethereum addresses. |
||||
* A UI MUST warn display approproate warning if the destination-account is formatted with invalid checksum. |
||||
* A UI MUST NOT open any ports or services |
||||
* The signer opens the public port |
||||
* A UI SHOULD verify the permissions on the signer binary, and refuse to execute or warn if permissions allow non-user write. |
||||
* A UI SHOULD inform the user about the `SHA256` or `MD5` hash of the binary being executed |
||||
* A UI SHOULD NOT maintain a secondary storage of data, e.g. list of accounts |
||||
* The signer provides accounts |
||||
* A UI SHOULD, to the best extent possible, use static linking / bundling, so that requried libraries are bundled |
||||
along with the UI. |
||||
|
||||
|
@ -0,0 +1,25 @@ |
||||
### Changelog for external API |
||||
|
||||
|
||||
|
||||
#### 2.0.0 |
||||
|
||||
* Commit `73abaf04b1372fa4c43201fb1b8019fe6b0a6f8d`, move `from` into `transaction` object in `signTransaction`. This |
||||
makes the `accounts_signTransaction` identical to the old `eth_signTransaction`. |
||||
|
||||
|
||||
#### 1.0.0 |
||||
|
||||
Initial release. |
||||
|
||||
### Versioning |
||||
|
||||
The API uses [semantic versioning](https://semver.org/). |
||||
|
||||
TLDR; Given a version number MAJOR.MINOR.PATCH, increment the: |
||||
|
||||
* MAJOR version when you make incompatible API changes, |
||||
* MINOR version when you add functionality in a backwards-compatible manner, and |
||||
* PATCH version when you make backwards-compatible bug fixes. |
||||
|
||||
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. |
@ -0,0 +1,86 @@ |
||||
### Changelog for internal API (ui-api) |
||||
|
||||
### 2.0.0 |
||||
|
||||
* Modify how `call_info` on a transaction is conveyed. New format: |
||||
|
||||
``` |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 2, |
||||
"method": "ApproveTx", |
||||
"params": [ |
||||
{ |
||||
"transaction": { |
||||
"from": "0x82A2A876D39022B3019932D30Cd9c97ad5616813", |
||||
"to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", |
||||
"gas": "0x333", |
||||
"gasPrice": "0x123", |
||||
"value": "0x10", |
||||
"nonce": "0x0", |
||||
"data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012", |
||||
"input": null |
||||
}, |
||||
"call_info": [ |
||||
{ |
||||
"type": "WARNING", |
||||
"message": "Invalid checksum on to-address" |
||||
}, |
||||
{ |
||||
"type": "WARNING", |
||||
"message": "Tx contains data, but provided ABI signature could not be matched: Did not match: test (0 matches)" |
||||
} |
||||
], |
||||
"meta": { |
||||
"remote": "127.0.0.1:54286", |
||||
"local": "localhost:8550", |
||||
"scheme": "HTTP/1.1" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
#### 1.2.0 |
||||
|
||||
* Add `OnStartup` method, to provide the UI with information about what API version |
||||
the signer uses (both internal and external) aswell as build-info and external api. |
||||
|
||||
Example call: |
||||
```json |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"id": 1, |
||||
"method": "OnSignerStartup", |
||||
"params": [ |
||||
{ |
||||
"info": { |
||||
"extapi_http": "http://localhost:8550", |
||||
"extapi_ipc": null, |
||||
"extapi_version": "2.0.0", |
||||
"intapi_version": "1.2.0" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
#### 1.1.0 |
||||
|
||||
* Add `OnApproved` method |
||||
|
||||
#### 1.0.0 |
||||
|
||||
Initial release. |
||||
|
||||
### Versioning |
||||
|
||||
The API uses [semantic versioning](https://semver.org/). |
||||
|
||||
TLDR; Given a version number MAJOR.MINOR.PATCH, increment the: |
||||
|
||||
* MAJOR version when you make incompatible API changes, |
||||
* MINOR version when you add functionality in a backwards-compatible manner, and |
||||
* PATCH version when you make backwards-compatible bug fixes. |
||||
|
||||
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. |
@ -0,0 +1,640 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// signer is a utility that can be used so sign transactions and
|
||||
// arbitrary data.
|
||||
package main |
||||
|
||||
import ( |
||||
"bufio" |
||||
"context" |
||||
"crypto/rand" |
||||
"crypto/sha256" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"os" |
||||
"os/user" |
||||
"path/filepath" |
||||
"runtime" |
||||
"strings" |
||||
|
||||
"encoding/hex" |
||||
"github.com/ethereum/go-ethereum/cmd/utils" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/rpc" |
||||
"github.com/ethereum/go-ethereum/signer/core" |
||||
"github.com/ethereum/go-ethereum/signer/rules" |
||||
"github.com/ethereum/go-ethereum/signer/storage" |
||||
"gopkg.in/urfave/cli.v1" |
||||
"os/signal" |
||||
) |
||||
|
||||
// ExternalApiVersion -- see extapi_changelog.md
|
||||
const ExternalApiVersion = "2.0.0" |
||||
|
||||
// InternalApiVersion -- see intapi_changelog.md
|
||||
const InternalApiVersion = "2.0.0" |
||||
|
||||
const legalWarning = ` |
||||
WARNING!
|
||||
|
||||
Clef is alpha software, and not yet publically released. This software has _not_ been audited, and there |
||||
are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software |
||||
unless you agree to take full responsibility for doing so, and know what you are doing.
|
||||
|
||||
TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE!
|
||||
|
||||
` |
||||
|
||||
var ( |
||||
logLevelFlag = cli.IntFlag{ |
||||
Name: "loglevel", |
||||
Value: 4, |
||||
Usage: "log level to emit to the screen", |
||||
} |
||||
keystoreFlag = cli.StringFlag{ |
||||
Name: "keystore", |
||||
Value: filepath.Join(node.DefaultDataDir(), "keystore"), |
||||
Usage: "Directory for the keystore", |
||||
} |
||||
configdirFlag = cli.StringFlag{ |
||||
Name: "configdir", |
||||
Value: DefaultConfigDir(), |
||||
Usage: "Directory for Clef configuration", |
||||
} |
||||
rpcPortFlag = cli.IntFlag{ |
||||
Name: "rpcport", |
||||
Usage: "HTTP-RPC server listening port", |
||||
Value: node.DefaultHTTPPort + 5, |
||||
} |
||||
signerSecretFlag = cli.StringFlag{ |
||||
Name: "signersecret", |
||||
Usage: "A file containing the password used to encrypt Clef credentials, e.g. keystore credentials and ruleset hash", |
||||
} |
||||
dBFlag = cli.StringFlag{ |
||||
Name: "4bytedb", |
||||
Usage: "File containing 4byte-identifiers", |
||||
Value: "./4byte.json", |
||||
} |
||||
customDBFlag = cli.StringFlag{ |
||||
Name: "4bytedb-custom", |
||||
Usage: "File used for writing new 4byte-identifiers submitted via API", |
||||
Value: "./4byte-custom.json", |
||||
} |
||||
auditLogFlag = cli.StringFlag{ |
||||
Name: "auditlog", |
||||
Usage: "File used to emit audit logs. Set to \"\" to disable", |
||||
Value: "audit.log", |
||||
} |
||||
ruleFlag = cli.StringFlag{ |
||||
Name: "rules", |
||||
Usage: "Enable rule-engine", |
||||
Value: "rules.json", |
||||
} |
||||
stdiouiFlag = cli.BoolFlag{ |
||||
Name: "stdio-ui", |
||||
Usage: "Use STDIN/STDOUT as a channel for an external UI. " + |
||||
"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " + |
||||
"interface, and can be used when Clef is started by an external process.", |
||||
} |
||||
testFlag = cli.BoolFlag{ |
||||
Name: "stdio-ui-test", |
||||
Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.", |
||||
} |
||||
app = cli.NewApp() |
||||
initCommand = cli.Command{ |
||||
Action: utils.MigrateFlags(initializeSecrets), |
||||
Name: "init", |
||||
Usage: "Initialize the signer, generate secret storage", |
||||
ArgsUsage: "", |
||||
Flags: []cli.Flag{ |
||||
logLevelFlag, |
||||
configdirFlag, |
||||
}, |
||||
Description: ` |
||||
The init command generates a master seed which Clef can use to store credentials and data needed for
|
||||
the rule-engine to work.`, |
||||
} |
||||
attestCommand = cli.Command{ |
||||
Action: utils.MigrateFlags(attestFile), |
||||
Name: "attest", |
||||
Usage: "Attest that a js-file is to be used", |
||||
ArgsUsage: "<sha256sum>", |
||||
Flags: []cli.Flag{ |
||||
logLevelFlag, |
||||
configdirFlag, |
||||
signerSecretFlag, |
||||
}, |
||||
Description: ` |
||||
The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of
|
||||
incoming requests.
|
||||
|
||||
Whenever you make an edit to the rule file, you need to use attestation to tell
|
||||
Clef that the file is 'safe' to execute.`, |
||||
} |
||||
|
||||
addCredentialCommand = cli.Command{ |
||||
Action: utils.MigrateFlags(addCredential), |
||||
Name: "addpw", |
||||
Usage: "Store a credential for a keystore file", |
||||
ArgsUsage: "<address> <password>", |
||||
Flags: []cli.Flag{ |
||||
logLevelFlag, |
||||
configdirFlag, |
||||
signerSecretFlag, |
||||
}, |
||||
Description: ` |
||||
The addpw command stores a password for a given address (keyfile). If you invoke it with only one parameter, it will
|
||||
remove any stored credential for that address (keyfile) |
||||
`, |
||||
} |
||||
) |
||||
|
||||
func init() { |
||||
app.Name = "Clef" |
||||
app.Usage = "Manage Ethereum account operations" |
||||
app.Flags = []cli.Flag{ |
||||
logLevelFlag, |
||||
keystoreFlag, |
||||
configdirFlag, |
||||
utils.NetworkIdFlag, |
||||
utils.LightKDFFlag, |
||||
utils.NoUSBFlag, |
||||
utils.RPCListenAddrFlag, |
||||
utils.RPCVirtualHostsFlag, |
||||
utils.IPCDisabledFlag, |
||||
utils.IPCPathFlag, |
||||
utils.RPCEnabledFlag, |
||||
rpcPortFlag, |
||||
signerSecretFlag, |
||||
dBFlag, |
||||
customDBFlag, |
||||
auditLogFlag, |
||||
ruleFlag, |
||||
stdiouiFlag, |
||||
testFlag, |
||||
} |
||||
app.Action = signer |
||||
app.Commands = []cli.Command{initCommand, attestCommand, addCredentialCommand} |
||||
|
||||
} |
||||
func main() { |
||||
if err := app.Run(os.Args); err != nil { |
||||
fmt.Fprintln(os.Stderr, err) |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
|
||||
func initializeSecrets(c *cli.Context) error { |
||||
if err := initialize(c); err != nil { |
||||
return err |
||||
} |
||||
configDir := c.String(configdirFlag.Name) |
||||
|
||||
masterSeed := make([]byte, 256) |
||||
n, err := io.ReadFull(rand.Reader, masterSeed) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if n != len(masterSeed) { |
||||
return fmt.Errorf("failed to read enough random") |
||||
} |
||||
err = os.Mkdir(configDir, 0700) |
||||
if err != nil && !os.IsExist(err) { |
||||
return err |
||||
} |
||||
location := filepath.Join(configDir, "secrets.dat") |
||||
if _, err := os.Stat(location); err == nil { |
||||
return fmt.Errorf("file %v already exists, will not overwrite", location) |
||||
} |
||||
err = ioutil.WriteFile(location, masterSeed, 0700) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
fmt.Printf("A master seed has been generated into %s\n", location) |
||||
fmt.Printf(` |
||||
This is required to be able to store credentials, such as :
|
||||
* Passwords for keystores (used by rule engine) |
||||
* Storage for javascript rules |
||||
* Hash of rule-file |
||||
|
||||
You should treat that file with utmost secrecy, and make a backup of it.
|
||||
NOTE: This file does not contain your accounts. Those need to be backed up separately! |
||||
|
||||
`) |
||||
return nil |
||||
} |
||||
func attestFile(ctx *cli.Context) error { |
||||
if len(ctx.Args()) < 1 { |
||||
utils.Fatalf("This command requires an argument.") |
||||
} |
||||
if err := initialize(ctx); err != nil { |
||||
return err |
||||
} |
||||
|
||||
stretchedKey, err := readMasterKey(ctx) |
||||
if err != nil { |
||||
utils.Fatalf(err.Error()) |
||||
} |
||||
configDir := ctx.String(configdirFlag.Name) |
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) |
||||
confKey := crypto.Keccak256([]byte("config"), stretchedKey) |
||||
|
||||
// Initialize the encrypted storages
|
||||
configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey) |
||||
val := ctx.Args().First() |
||||
configStorage.Put("ruleset_sha256", val) |
||||
log.Info("Ruleset attestation updated", "sha256", val) |
||||
return nil |
||||
} |
||||
|
||||
func addCredential(ctx *cli.Context) error { |
||||
if len(ctx.Args()) < 1 { |
||||
utils.Fatalf("This command requires at leaste one argument.") |
||||
} |
||||
if err := initialize(ctx); err != nil { |
||||
return err |
||||
} |
||||
|
||||
stretchedKey, err := readMasterKey(ctx) |
||||
if err != nil { |
||||
utils.Fatalf(err.Error()) |
||||
} |
||||
configDir := ctx.String(configdirFlag.Name) |
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) |
||||
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) |
||||
|
||||
// Initialize the encrypted storages
|
||||
pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) |
||||
key := ctx.Args().First() |
||||
value := "" |
||||
if len(ctx.Args()) > 1 { |
||||
value = ctx.Args().Get(1) |
||||
} |
||||
pwStorage.Put(key, value) |
||||
log.Info("Credential store updated", "key", key) |
||||
return nil |
||||
} |
||||
|
||||
func initialize(c *cli.Context) error { |
||||
// Set up the logger to print everything
|
||||
logOutput := os.Stdout |
||||
if c.Bool(stdiouiFlag.Name) { |
||||
logOutput = os.Stderr |
||||
// If using the stdioui, we can't do the 'confirm'-flow
|
||||
fmt.Fprintf(logOutput, legalWarning) |
||||
} else { |
||||
if !confirm(legalWarning) { |
||||
return fmt.Errorf("aborted by user") |
||||
} |
||||
} |
||||
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true)))) |
||||
return nil |
||||
} |
||||
|
||||
func signer(c *cli.Context) error { |
||||
if err := initialize(c); err != nil { |
||||
return err |
||||
} |
||||
var ( |
||||
ui core.SignerUI |
||||
) |
||||
if c.Bool(stdiouiFlag.Name) { |
||||
log.Info("Using stdin/stdout as UI-channel") |
||||
ui = core.NewStdIOUI() |
||||
} else { |
||||
log.Info("Using CLI as UI-channel") |
||||
ui = core.NewCommandlineUI() |
||||
} |
||||
db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name)) |
||||
if err != nil { |
||||
utils.Fatalf(err.Error()) |
||||
} |
||||
log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb")) |
||||
|
||||
var ( |
||||
api core.ExternalAPI |
||||
) |
||||
|
||||
configDir := c.String(configdirFlag.Name) |
||||
if stretchedKey, err := readMasterKey(c); err != nil { |
||||
log.Info("No master seed provided, rules disabled") |
||||
} else { |
||||
|
||||
if err != nil { |
||||
utils.Fatalf(err.Error()) |
||||
} |
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) |
||||
|
||||
// Generate domain specific keys
|
||||
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) |
||||
jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) |
||||
confkey := crypto.Keccak256([]byte("config"), stretchedKey) |
||||
|
||||
// Initialize the encrypted storages
|
||||
pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) |
||||
jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) |
||||
configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) |
||||
|
||||
//Do we have a rule-file?
|
||||
ruleJS, err := ioutil.ReadFile(c.String(ruleFlag.Name)) |
||||
if err != nil { |
||||
log.Info("Could not load rulefile, rules not enabled", "file", "rulefile") |
||||
} else { |
||||
hasher := sha256.New() |
||||
hasher.Write(ruleJS) |
||||
shasum := hasher.Sum(nil) |
||||
storedShasum := configStorage.Get("ruleset_sha256") |
||||
if storedShasum != hex.EncodeToString(shasum) { |
||||
log.Info("Could not validate ruleset hash, rules not enabled", "got", hex.EncodeToString(shasum), "expected", storedShasum) |
||||
} else { |
||||
// Initialize rules
|
||||
ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage, pwStorage) |
||||
if err != nil { |
||||
utils.Fatalf(err.Error()) |
||||
} |
||||
ruleEngine.Init(string(ruleJS)) |
||||
ui = ruleEngine |
||||
log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
apiImpl := core.NewSignerAPI( |
||||
c.Int64(utils.NetworkIdFlag.Name), |
||||
c.String(keystoreFlag.Name), |
||||
c.Bool(utils.NoUSBFlag.Name), |
||||
ui, db, |
||||
c.Bool(utils.LightKDFFlag.Name)) |
||||
|
||||
api = apiImpl |
||||
|
||||
// Audit logging
|
||||
if logfile := c.String(auditLogFlag.Name); logfile != "" { |
||||
api, err = core.NewAuditLogger(logfile, api) |
||||
if err != nil { |
||||
utils.Fatalf(err.Error()) |
||||
} |
||||
log.Info("Audit logs configured", "file", logfile) |
||||
} |
||||
// register signer API with server
|
||||
var ( |
||||
extapiUrl = "n/a" |
||||
ipcApiUrl = "n/a" |
||||
) |
||||
rpcApi := []rpc.API{ |
||||
{ |
||||
Namespace: "account", |
||||
Public: true, |
||||
Service: api, |
||||
Version: "1.0"}, |
||||
} |
||||
if c.Bool(utils.RPCEnabledFlag.Name) { |
||||
|
||||
vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) |
||||
cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) |
||||
|
||||
// start http server
|
||||
httpEndpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) |
||||
listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcApi, []string{"account"}, cors, vhosts) |
||||
if err != nil { |
||||
utils.Fatalf("Could not start RPC api: %v", err) |
||||
} |
||||
extapiUrl = fmt.Sprintf("http://%s", httpEndpoint) |
||||
log.Info("HTTP endpoint opened", "url", extapiUrl) |
||||
|
||||
defer func() { |
||||
listener.Close() |
||||
log.Info("HTTP endpoint closed", "url", httpEndpoint) |
||||
}() |
||||
|
||||
} |
||||
if !c.Bool(utils.IPCDisabledFlag.Name) { |
||||
if c.IsSet(utils.IPCPathFlag.Name) { |
||||
ipcApiUrl = c.String(utils.IPCPathFlag.Name) |
||||
} else { |
||||
ipcApiUrl = filepath.Join(configDir, "clef.ipc") |
||||
} |
||||
|
||||
listener, _, err := rpc.StartIPCEndpoint(func() bool { return true }, ipcApiUrl, rpcApi) |
||||
if err != nil { |
||||
utils.Fatalf("Could not start IPC api: %v", err) |
||||
} |
||||
log.Info("IPC endpoint opened", "url", ipcApiUrl) |
||||
defer func() { |
||||
listener.Close() |
||||
log.Info("IPC endpoint closed", "url", ipcApiUrl) |
||||
}() |
||||
|
||||
} |
||||
|
||||
if c.Bool(testFlag.Name) { |
||||
log.Info("Performing UI test") |
||||
go testExternalUI(apiImpl) |
||||
} |
||||
ui.OnSignerStartup(core.StartupInfo{ |
||||
Info: map[string]interface{}{ |
||||
"extapi_version": ExternalApiVersion, |
||||
"intapi_version": InternalApiVersion, |
||||
"extapi_http": extapiUrl, |
||||
"extapi_ipc": ipcApiUrl, |
||||
}, |
||||
}) |
||||
|
||||
abortChan := make(chan os.Signal) |
||||
signal.Notify(abortChan, os.Interrupt) |
||||
|
||||
sig := <-abortChan |
||||
log.Info("Exiting...", "signal", sig) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// splitAndTrim splits input separated by a comma
|
||||
// and trims excessive white space from the substrings.
|
||||
func splitAndTrim(input string) []string { |
||||
result := strings.Split(input, ",") |
||||
for i, r := range result { |
||||
result[i] = strings.TrimSpace(r) |
||||
} |
||||
return result |
||||
} |
||||
|
||||
// DefaultConfigDir is the default config directory to use for the vaults and other
|
||||
// persistence requirements.
|
||||
func DefaultConfigDir() string { |
||||
// Try to place the data folder in the user's home dir
|
||||
home := homeDir() |
||||
if home != "" { |
||||
if runtime.GOOS == "darwin" { |
||||
return filepath.Join(home, "Library", "Signer") |
||||
} else if runtime.GOOS == "windows" { |
||||
return filepath.Join(home, "AppData", "Roaming", "Signer") |
||||
} else { |
||||
return filepath.Join(home, ".clef") |
||||
} |
||||
} |
||||
// As we cannot guess a stable location, return empty and handle later
|
||||
return "" |
||||
} |
||||
|
||||
func homeDir() string { |
||||
if home := os.Getenv("HOME"); home != "" { |
||||
return home |
||||
} |
||||
if usr, err := user.Current(); err == nil { |
||||
return usr.HomeDir |
||||
} |
||||
return "" |
||||
} |
||||
func readMasterKey(ctx *cli.Context) ([]byte, error) { |
||||
var ( |
||||
file string |
||||
configDir = ctx.String(configdirFlag.Name) |
||||
) |
||||
if ctx.IsSet(signerSecretFlag.Name) { |
||||
file = ctx.String(signerSecretFlag.Name) |
||||
} else { |
||||
file = filepath.Join(configDir, "secrets.dat") |
||||
} |
||||
if err := checkFile(file); err != nil { |
||||
return nil, err |
||||
} |
||||
masterKey, err := ioutil.ReadFile(file) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if len(masterKey) < 256 { |
||||
return nil, fmt.Errorf("master key of insufficient length, expected >255 bytes, got %d", len(masterKey)) |
||||
} |
||||
// Create vault location
|
||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterKey)[:10])) |
||||
err = os.Mkdir(vaultLocation, 0700) |
||||
if err != nil && !os.IsExist(err) { |
||||
return nil, err |
||||
} |
||||
//!TODO, use KDF to stretch the master key
|
||||
// stretched_key := stretch_key(master_key)
|
||||
|
||||
return masterKey, nil |
||||
} |
||||
|
||||
// checkFile is a convenience function to check if a file
|
||||
// * exists
|
||||
// * is mode 0600
|
||||
func checkFile(filename string) error { |
||||
info, err := os.Stat(filename) |
||||
if err != nil { |
||||
return fmt.Errorf("failed stat on %s: %v", filename, err) |
||||
} |
||||
// Check the unix permission bits
|
||||
if info.Mode().Perm()&077 != 0 { |
||||
return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// confirm displays a text and asks for user confirmation
|
||||
func confirm(text string) bool { |
||||
fmt.Printf(text) |
||||
fmt.Printf("\nEnter 'ok' to proceed:\n>") |
||||
|
||||
text, err := bufio.NewReader(os.Stdin).ReadString('\n') |
||||
if err != nil { |
||||
log.Crit("Failed to read user input", "err", err) |
||||
} |
||||
|
||||
if text := strings.TrimSpace(text); text == "ok" { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func testExternalUI(api *core.SignerAPI) { |
||||
|
||||
ctx := context.WithValue(context.Background(), "remote", "clef binary") |
||||
ctx = context.WithValue(ctx, "scheme", "in-proc") |
||||
ctx = context.WithValue(ctx, "local", "main") |
||||
|
||||
errs := make([]string, 0) |
||||
|
||||
api.UI.ShowInfo("Testing 'ShowInfo'") |
||||
api.UI.ShowError("Testing 'ShowError'") |
||||
|
||||
checkErr := func(method string, err error) { |
||||
if err != nil && err != core.ErrRequestDenied { |
||||
errs = append(errs, fmt.Sprintf("%v: %v", method, err.Error())) |
||||
} |
||||
} |
||||
var err error |
||||
|
||||
_, err = api.SignTransaction(ctx, core.SendTxArgs{From: common.MixedcaseAddress{}}, nil) |
||||
checkErr("SignTransaction", err) |
||||
_, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304")) |
||||
checkErr("Sign", err) |
||||
_, err = api.List(ctx) |
||||
checkErr("List", err) |
||||
_, err = api.New(ctx) |
||||
checkErr("New", err) |
||||
_, err = api.Export(ctx, common.Address{}) |
||||
checkErr("Export", err) |
||||
_, err = api.Import(ctx, json.RawMessage{}) |
||||
checkErr("Import", err) |
||||
|
||||
api.UI.ShowInfo("Tests completed") |
||||
|
||||
if len(errs) > 0 { |
||||
log.Error("Got errors") |
||||
for _, e := range errs { |
||||
log.Error(e) |
||||
} |
||||
} else { |
||||
log.Info("No errors") |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
//Create Account
|
||||
|
||||
curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550 |
||||
|
||||
// List accounts
|
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/
|
||||
|
||||
// Make Transaction
|
||||
// safeSend(0x12)
|
||||
// 4401a6e40000000000000000000000000000000000000000000000000000000000000012
|
||||
|
||||
// supplied abi
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"test"],"id":67}' http://localhost:8550/
|
||||
|
||||
// Not supplied
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"}],"id":67}' http://localhost:8550/
|
||||
|
||||
// Sign data
|
||||
|
||||
curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_sign","params":["0x694267f14675d7e1b9494fd8d72fefe1755710fa","bazonk gaz baz"],"id":67}' http://localhost:8550/
|
||||
|
||||
|
||||
**/ |
@ -0,0 +1,179 @@ |
||||
import os,sys, subprocess |
||||
from tinyrpc.transports import ServerTransport |
||||
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol |
||||
from tinyrpc.dispatch import public,RPCDispatcher |
||||
from tinyrpc.server import RPCServer |
||||
|
||||
""" This is a POC example of how to write a custom UI for Clef. The UI starts the |
||||
clef process with the '--stdio-ui' option, and communicates with clef using standard input / output. |
||||
|
||||
The standard input/output is a relatively secure way to communicate, as it does not require opening any ports |
||||
or IPC files. Needless to say, it does not protect against memory inspection mechanisms where an attacker |
||||
can access process memory.""" |
||||
|
||||
try: |
||||
import urllib.parse as urlparse |
||||
except ImportError: |
||||
import urllib as urlparse |
||||
|
||||
class StdIOTransport(ServerTransport): |
||||
""" Uses std input/output for RPC """ |
||||
def receive_message(self): |
||||
return None, urlparse.unquote(sys.stdin.readline()) |
||||
|
||||
def send_reply(self, context, reply): |
||||
print(reply) |
||||
|
||||
class PipeTransport(ServerTransport): |
||||
""" Uses std a pipe for RPC """ |
||||
|
||||
def __init__(self,input, output): |
||||
self.input = input |
||||
self.output = output |
||||
|
||||
def receive_message(self): |
||||
data = self.input.readline() |
||||
print(">> {}".format( data)) |
||||
return None, urlparse.unquote(data) |
||||
|
||||
def send_reply(self, context, reply): |
||||
print("<< {}".format( reply)) |
||||
self.output.write(reply) |
||||
self.output.write("\n") |
||||
|
||||
class StdIOHandler(): |
||||
|
||||
def __init__(self): |
||||
pass |
||||
|
||||
@public |
||||
def ApproveTx(self,req): |
||||
""" |
||||
Example request: |
||||
{ |
||||
"jsonrpc": "2.0", |
||||
"method": "ApproveTx", |
||||
"params": [{ |
||||
"transaction": { |
||||
"to": "0xae967917c465db8578ca9024c205720b1a3651A9", |
||||
"gas": "0x333", |
||||
"gasPrice": "0x123", |
||||
"value": "0x10", |
||||
"data": "0xd7a5865800000000000000000000000000000000000000000000000000000000000000ff", |
||||
"nonce": "0x0" |
||||
}, |
||||
"from": "0xAe967917c465db8578ca9024c205720b1a3651A9", |
||||
"call_info": "Warning! Could not validate ABI-data against calldata\nSupplied ABI spec does not contain method signature in data: 0xd7a58658", |
||||
"meta": { |
||||
"remote": "127.0.0.1:34572", |
||||
"local": "localhost:8550", |
||||
"scheme": "HTTP/1.1" |
||||
} |
||||
}], |
||||
"id": 1 |
||||
} |
||||
|
||||
:param transaction: transaction info |
||||
:param call_info: info abou the call, e.g. if ABI info could not be |
||||
:param meta: metadata about the request, e.g. where the call comes from |
||||
:return: |
||||
""" |
||||
transaction = req.get('transaction') |
||||
_from = req.get('from') |
||||
call_info = req.get('call_info') |
||||
meta = req.get('meta') |
||||
|
||||
return { |
||||
"approved" : False, |
||||
#"transaction" : transaction, |
||||
# "from" : _from, |
||||
# "password" : None, |
||||
} |
||||
|
||||
@public |
||||
def ApproveSignData(self, req): |
||||
""" Example request |
||||
|
||||
""" |
||||
return {"approved": False, "password" : None} |
||||
|
||||
@public |
||||
def ApproveExport(self, req): |
||||
""" Example request |
||||
|
||||
""" |
||||
return {"approved" : False} |
||||
|
||||
@public |
||||
def ApproveImport(self, req): |
||||
""" Example request |
||||
|
||||
""" |
||||
return { "approved" : False, "old_password": "", "new_password": ""} |
||||
|
||||
@public |
||||
def ApproveListing(self, req): |
||||
""" Example request |
||||
|
||||
""" |
||||
return {'accounts': []} |
||||
|
||||
@public |
||||
def ApproveNewAccount(self, req): |
||||
""" |
||||
Example request |
||||
|
||||
:return: |
||||
""" |
||||
return {"approved": False, |
||||
#"password": "" |
||||
} |
||||
|
||||
@public |
||||
def ShowError(self,message = {}): |
||||
""" |
||||
Example request: |
||||
|
||||
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowError'"},"id":1} |
||||
|
||||
:param message: to show |
||||
:return: nothing |
||||
""" |
||||
if 'text' in message.keys(): |
||||
sys.stderr.write("Error: {}\n".format( message['text'])) |
||||
return |
||||
|
||||
@public |
||||
def ShowInfo(self,message = {}): |
||||
""" |
||||
Example request |
||||
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowInfo'"},"id":0} |
||||
|
||||
:param message: to display |
||||
:return:nothing |
||||
""" |
||||
|
||||
if 'text' in message.keys(): |
||||
sys.stdout.write("Error: {}\n".format( message['text'])) |
||||
return |
||||
|
||||
def main(args): |
||||
|
||||
cmd = ["./clef", "--stdio-ui"] |
||||
if len(args) > 0 and args[0] == "test": |
||||
cmd.extend(["--stdio-ui-test"]) |
||||
print("cmd: {}".format(" ".join(cmd))) |
||||
dispatcher = RPCDispatcher() |
||||
dispatcher.register_instance(StdIOHandler(), '') |
||||
# line buffered |
||||
p = subprocess.Popen(cmd, bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) |
||||
|
||||
rpc_server = RPCServer( |
||||
PipeTransport(p.stdout, p.stdin), |
||||
JSONRPCProtocol(), |
||||
dispatcher |
||||
) |
||||
rpc_server.serve_forever() |
||||
|
||||
if __name__ == '__main__': |
||||
main(sys.argv[1:]) |
@ -0,0 +1,236 @@ |
||||
# Rules |
||||
|
||||
The `signer` binary contains a ruleset engine, implemented with [OttoVM](https://github.com/robertkrimen/otto) |
||||
|
||||
It enables usecases like the following: |
||||
|
||||
* I want to auto-approve transactions with contract `CasinoDapp`, with up to `0.05 ether` in value to maximum `1 ether` per 24h period |
||||
* I want to auto-approve transaction to contract `EthAlarmClock` with `data`=`0xdeadbeef`, if `value=0`, `gas < 44k` and `gasPrice < 40Gwei` |
||||
|
||||
The two main features that are required for this to work well are; |
||||
|
||||
1. Rule Implementation: how to create, manage and interpret rules in a flexible but secure manner |
||||
2. Credential managements and credentials; how to provide auto-unlock without exposing keys unnecessarily. |
||||
|
||||
The section below deals with both of them |
||||
|
||||
## Rule Implementation |
||||
|
||||
A ruleset file is implemented as a `js` file. Under the hood, the ruleset-engine is a `SignerUI`, implementing the same methods as the `json-rpc` methods |
||||
defined in the UI protocol. Example: |
||||
|
||||
```javascript |
||||
|
||||
function asBig(str){ |
||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)} |
||||
return new BigNumber(str) |
||||
} |
||||
|
||||
// Approve transactions to a certain contract if value is below a certain limit |
||||
function ApproveTx(req){ |
||||
|
||||
var limit = big.Newint("0xb1a2bc2ec50000") |
||||
var value = asBig(req.transaction.value); |
||||
|
||||
if(req.transaction.to.toLowerCase()=="0xae967917c465db8578ca9024c205720b1a3651a9") |
||||
&& value.lt(limit) ){ |
||||
return "Approve" |
||||
} |
||||
// If we return "Reject", it will be rejected. |
||||
// By not returning anything, it will be passed to the next UI, for manual processing |
||||
} |
||||
|
||||
//Approve listings if request made from IPC |
||||
function ApproveListing(req){ |
||||
if (req.metadata.scheme == "ipc"){ return "Approve"} |
||||
} |
||||
|
||||
``` |
||||
|
||||
Whenever the external API is called (and the ruleset is enabled), the `signer` calls the UI, which is an instance of a ruleset-engine. The ruleset-engine |
||||
invokes the corresponding method. In doing so, there are three possible outcomes: |
||||
|
||||
1. JS returns "Approve" |
||||
* Auto-approve request |
||||
2. JS returns "Reject" |
||||
* Auto-reject request |
||||
3. Error occurs, or something else is returned |
||||
* Pass on to `next` ui: the regular UI channel. |
||||
|
||||
A more advanced example can be found below, "Example 1: ruleset for a rate-limited window", using `storage` to `Put` and `Get` `string`s by key. |
||||
|
||||
* At the time of writing, storage only exists as an ephemeral unencrypted implementation, to be used during testing. |
||||
|
||||
### Things to note |
||||
|
||||
The Otto vm has a few [caveats](https://github.com/robertkrimen/otto): |
||||
|
||||
* "use strict" will parse, but does nothing. |
||||
* The regular expression engine (re2/regexp) is not fully compatible with the ECMA5 specification. |
||||
* Otto targets ES5. ES6 features (eg: Typed Arrays) are not supported. |
||||
|
||||
Additionally, a few more have been added |
||||
|
||||
* The rule execution cannot load external javascript files. |
||||
* The only preloaded libary is [`bignumber.js`](https://github.com/MikeMcl/bignumber.js) version `2.0.3`. This one is fairly old, and is not aligned with the documentation at the github repository. |
||||
* Each invocation is made in a fresh virtual machine. This means that you cannot store data in global variables between invocations. This is a deliberate choice -- if you want to store data, use the disk-backed `storage`, since rules should not rely on ephemeral data. |
||||
* Javascript API parameters are _always_ an object. This is also a design choice, to ensure that parameters are accessed by _key_ and not by order. This is to prevent mistakes due to missing parameters or parameter changes. |
||||
* The JS engine has access to `storage` and `console`. |
||||
|
||||
#### Security considerations |
||||
|
||||
##### Security of ruleset |
||||
|
||||
Some security precautions can be made, such as: |
||||
|
||||
* Never load `ruleset.js` unless the file is `readonly` (`r-??-??-?`). If the user wishes to modify the ruleset, he must make it writeable and then set back to readonly. |
||||
* This is to prevent attacks where files are dropped on the users disk. |
||||
* Since we're going to have to have some form of secure storage (not defined in this section), we could also store the `sha3` of the `ruleset.js` file in there. |
||||
* If the user wishes to modify the ruleset, he'd then have to perform e.g. `signer --attest /path/to/ruleset --credential <creds>` |
||||
|
||||
##### Security of implementation |
||||
|
||||
The drawbacks of this very flexible solution is that the `signer` needs to contain a javascript engine. This is pretty simple to implement, since it's already |
||||
implemented for `geth`. There are no known security vulnerabilities in, nor have we had any security-problems with it so far. |
||||
|
||||
The javascript engine would be an added attack surface; but if the validation of `rulesets` is made good (with hash-based attestation), the actual javascript cannot be considered |
||||
an attack surface -- if an attacker can control the ruleset, a much simpler attack would be to implement an "always-approve" rule instead of exploiting the js vm. The only benefit |
||||
to be gained from attacking the actual `signer` process from the `js` side would be if it could somehow extract cryptographic keys from memory. |
||||
|
||||
##### Security in usability |
||||
|
||||
Javascript is flexible, but also easy to get wrong, especially when users assume that `js` can handle large integers natively. Typical errors |
||||
include trying to multiply `gasCost` with `gas` without using `bigint`:s. |
||||
|
||||
It's unclear whether any other DSL could be more secure; since there's always the possibility of erroneously implementing a rule. |
||||
|
||||
|
||||
## Credential management |
||||
|
||||
The ability to auto-approve transaction means that the signer needs to have necessary credentials to decrypt keyfiles. These passwords are hereafter called `ksp` (keystore pass). |
||||
|
||||
### Example implementation |
||||
|
||||
Upon startup of the signer, the signer is given a switch: `--seed <path/to/masterseed>` |
||||
The `seed` contains a blob of bytes, which is the master seed for the `signer`. |
||||
|
||||
The `signer` uses the `seed` to: |
||||
|
||||
* Generate the `path` where the settings are stored. |
||||
* `./settings/1df094eb-c2b1-4689-90dd-790046d38025/vault.dat` |
||||
* `./settings/1df094eb-c2b1-4689-90dd-790046d38025/rules.js` |
||||
* Generate the encryption password for `vault.dat`. |
||||
|
||||
The `vault.dat` would be an encrypted container storing the following information: |
||||
|
||||
* `ksp` entries |
||||
* `sha256` hash of `rules.js` |
||||
* Information about pair:ed callers (not yet specified) |
||||
|
||||
### Security considerations |
||||
|
||||
This would leave it up to the user to ensure that the `path/to/masterseed` is handled in a secure way. It's difficult to get around this, although one could |
||||
imagine leveraging OS-level keychains where supported. The setup is however in general similar to how ssh-keys are stored in `.ssh/`. |
||||
|
||||
|
||||
# Implementation status |
||||
|
||||
This is now implemented (with ephemeral non-encrypted storage for now, so not yet enabled). |
||||
|
||||
## Example 1: ruleset for a rate-limited window |
||||
|
||||
|
||||
```javascript |
||||
|
||||
function big(str){ |
||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)} |
||||
return new BigNumber(str) |
||||
} |
||||
|
||||
// Time window: 1 week |
||||
var window = 1000* 3600*24*7; |
||||
|
||||
// Limit : 1 ether |
||||
var limit = new BigNumber("1e18"); |
||||
|
||||
function isLimitOk(transaction){ |
||||
var value = big(transaction.value) |
||||
// Start of our window function |
||||
var windowstart = new Date().getTime() - window; |
||||
|
||||
var txs = []; |
||||
var stored = storage.Get('txs'); |
||||
|
||||
if(stored != ""){ |
||||
txs = JSON.parse(stored) |
||||
} |
||||
// First, remove all that have passed out of the time-window |
||||
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart}); |
||||
console.log(txs, newtxs.length); |
||||
|
||||
// Secondly, aggregate the current sum |
||||
sum = new BigNumber(0) |
||||
|
||||
sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum); |
||||
console.log("ApproveTx > Sum so far", sum); |
||||
console.log("ApproveTx > Requested", value.toNumber()); |
||||
|
||||
// Would we exceed weekly limit ? |
||||
return sum.plus(value).lt(limit) |
||||
|
||||
} |
||||
function ApproveTx(r){ |
||||
if (isLimitOk(r.transaction)){ |
||||
return "Approve" |
||||
} |
||||
return "Nope" |
||||
} |
||||
|
||||
/** |
||||
* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter |
||||
* 'response_str' contains the return value that will be sent to the external caller. |
||||
* The return value from this method is ignore - the reason for having this callback is to allow the |
||||
* ruleset to keep track of approved transactions. |
||||
* |
||||
* When implementing rate-limited rules, this callback should be used. |
||||
* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user |
||||
* then accepts the transaction, this method will be called. |
||||
* |
||||
* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx. |
||||
*/ |
||||
function OnApprovedTx(resp){ |
||||
var value = big(resp.tx.value) |
||||
var txs = [] |
||||
// Load stored transactions |
||||
var stored = storage.Get('txs'); |
||||
if(stored != ""){ |
||||
txs = JSON.parse(stored) |
||||
} |
||||
// Add this to the storage |
||||
txs.push({tstamp: new Date().getTime(), value: value}); |
||||
storage.Put("txs", JSON.stringify(txs)); |
||||
} |
||||
|
||||
``` |
||||
|
||||
## Example 2: allow destination |
||||
|
||||
```javascript |
||||
|
||||
function ApproveTx(r){ |
||||
if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"} |
||||
if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"} |
||||
// Otherwise goes to manual processing |
||||
} |
||||
|
||||
``` |
||||
|
||||
## Example 3: Allow listing |
||||
|
||||
```javascript |
||||
|
||||
function ApproveListing(){ |
||||
return "Approve" |
||||
} |
||||
|
||||
``` |
After Width: | Height: | Size: 36 KiB |
@ -0,0 +1,198 @@ |
||||
## Initializing the signer |
||||
|
||||
First, initialize the master seed. |
||||
|
||||
```text |
||||
#./signer init |
||||
|
||||
WARNING! |
||||
|
||||
The signer is alpha software, and not yet publically released. This software has _not_ been audited, and there |
||||
are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software |
||||
unless you agree to take full responsibility for doing so, and know what you are doing. |
||||
|
||||
TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE! |
||||
|
||||
|
||||
Enter 'ok' to proceed: |
||||
>ok |
||||
A master seed has been generated into /home/martin/.signer/secrets.dat |
||||
|
||||
This is required to be able to store credentials, such as : |
||||
* Passwords for keystores (used by rule engine) |
||||
* Storage for javascript rules |
||||
* Hash of rule-file |
||||
|
||||
You should treat that file with utmost secrecy, and make a backup of it. |
||||
NOTE: This file does not contain your accounts. Those need to be backed up separately! |
||||
``` |
||||
|
||||
(for readability purposes, we'll remove the WARNING printout in the rest of this document) |
||||
|
||||
## Creating rules |
||||
|
||||
Now, you can create a rule-file. |
||||
|
||||
```javascript |
||||
function ApproveListing(){ |
||||
return "Approve" |
||||
} |
||||
``` |
||||
Get the `sha256` hash.... |
||||
```text |
||||
#sha256sum rules.js |
||||
6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 rules.js |
||||
``` |
||||
...And then `attest` the file: |
||||
```text |
||||
#./signer attest 6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 |
||||
|
||||
INFO [02-21|12:14:38] Ruleset attestation updated sha256=6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 |
||||
``` |
||||
At this point, we then start the signer with the rule-file: |
||||
|
||||
```text |
||||
#./signer --rules rules.json |
||||
|
||||
INFO [02-21|12:15:18] Using CLI as UI-channel |
||||
INFO [02-21|12:15:18] Loaded 4byte db signatures=5509 file=./4byte.json |
||||
INFO [02-21|12:15:18] Could not load rulefile, rules not enabled file=rulefile |
||||
DEBUG[02-21|12:15:18] FS scan times list=35.335µs set=5.536µs diff=5.073µs |
||||
DEBUG[02-21|12:15:18] Ledger support enabled |
||||
DEBUG[02-21|12:15:18] Trezor support enabled |
||||
INFO [02-21|12:15:18] Audit logs configured file=audit.log |
||||
INFO [02-21|12:15:18] HTTP endpoint opened url=http://localhost:8550 |
||||
------- Signer info ------- |
||||
* extapi_http : http://localhost:8550 |
||||
* extapi_ipc : <nil> |
||||
* extapi_version : 2.0.0 |
||||
* intapi_version : 1.2.0 |
||||
|
||||
``` |
||||
|
||||
Any list-requests will now be auto-approved by our rule-file. |
||||
|
||||
## Under the hood |
||||
|
||||
While doing the operations above, these files have been created: |
||||
|
||||
```text |
||||
#ls -laR ~/.signer/ |
||||
/home/martin/.signer/: |
||||
total 16 |
||||
drwx------ 3 martin martin 4096 feb 21 12:14 . |
||||
drwxr-xr-x 71 martin martin 4096 feb 21 12:12 .. |
||||
drwx------ 2 martin martin 4096 feb 21 12:14 43f73718397aa54d1b22 |
||||
-rwx------ 1 martin martin 256 feb 21 12:12 secrets.dat |
||||
|
||||
/home/martin/.signer/43f73718397aa54d1b22: |
||||
total 12 |
||||
drwx------ 2 martin martin 4096 feb 21 12:14 . |
||||
drwx------ 3 martin martin 4096 feb 21 12:14 .. |
||||
-rw------- 1 martin martin 159 feb 21 12:14 config.json |
||||
|
||||
#cat /home/martin/.signer/43f73718397aa54d1b22/config.json |
||||
{"ruleset_sha256":{"iv":"6v4W4tfJxj3zZFbl","c":"6dt5RTDiTq93yh1qDEjpsat/tsKG7cb+vr3sza26IPL2fvsQ6ZoqFx++CPUa8yy6fD9Bbq41L01ehkKHTG3pOAeqTW6zc/+t0wv3AB6xPmU="}} |
||||
|
||||
``` |
||||
|
||||
In `~/.signer`, the `secrets.dat` file was created, containing the `master_seed`. |
||||
The `master_seed` was then used to derive a few other things: |
||||
|
||||
- `vault_location` : in this case `43f73718397aa54d1b22` . |
||||
- Thus, if you use a different `master_seed`, another `vault_location` will be used that does not conflict with each other. |
||||
- Example: `signer --signersecret /path/to/afile ...` |
||||
- `config.json` which is the encrypted key/value storage for configuration data, containing the key `ruleset_sha256`. |
||||
|
||||
|
||||
## Adding credentials |
||||
|
||||
In order to make more useful rules; sign transactions, the signer needs access to the passwords needed to unlock keystores. |
||||
|
||||
```text |
||||
#./signer addpw 0x694267f14675d7e1b9494fd8d72fefe1755710fa test |
||||
|
||||
INFO [02-21|13:43:21] Credential store updated key=0x694267f14675d7e1b9494fd8d72fefe1755710fa |
||||
``` |
||||
## More advanced rules |
||||
|
||||
Now let's update the rules to make use of credentials |
||||
|
||||
```javascript |
||||
function ApproveListing(){ |
||||
return "Approve" |
||||
} |
||||
function ApproveSignData(r){ |
||||
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") |
||||
{ |
||||
if(r.message.indexOf("bazonk") >= 0){ |
||||
return "Approve" |
||||
} |
||||
return "Reject" |
||||
} |
||||
// Otherwise goes to manual processing |
||||
} |
||||
|
||||
``` |
||||
In this example, |
||||
* any requests to sign data with the account `0x694...` will be |
||||
* auto-approved if the message contains with `bazonk`, |
||||
* and auto-rejected if it does not. |
||||
* Any other signing-requests will be passed along for manual approve/reject. |
||||
|
||||
..attest the new file |
||||
```text |
||||
#sha256sum rules.js |
||||
2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f rules.js |
||||
|
||||
#./signer attest 2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f |
||||
|
||||
INFO [02-21|14:36:30] Ruleset attestation updated sha256=2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f |
||||
``` |
||||
|
||||
And start the signer: |
||||
|
||||
``` |
||||
#./signer --rules rules.js |
||||
|
||||
INFO [02-21|14:41:56] Using CLI as UI-channel |
||||
INFO [02-21|14:41:56] Loaded 4byte db signatures=5509 file=./4byte.json |
||||
INFO [02-21|14:41:56] Rule engine configured file=rules.js |
||||
DEBUG[02-21|14:41:56] FS scan times list=34.607µs set=4.509µs diff=4.87µs |
||||
DEBUG[02-21|14:41:56] Ledger support enabled |
||||
DEBUG[02-21|14:41:56] Trezor support enabled |
||||
INFO [02-21|14:41:56] Audit logs configured file=audit.log |
||||
INFO [02-21|14:41:56] HTTP endpoint opened url=http://localhost:8550 |
||||
------- Signer info ------- |
||||
* extapi_version : 2.0.0 |
||||
* intapi_version : 1.2.0 |
||||
* extapi_http : http://localhost:8550 |
||||
* extapi_ipc : <nil> |
||||
INFO [02-21|14:41:56] error occurred during execution error="ReferenceError: 'OnSignerStartup' is not defined" |
||||
``` |
||||
And then test signing, once with `bazonk` and once without: |
||||
|
||||
``` |
||||
#curl -H "Content-Type: application/json" -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"account_sign\",\"params\":[\"0x694267f14675d7e1b9494fd8d72fefe1755710fa\",\"0x$(xxd -pu <<< ' bazonk baz gaz')\"],\"id\":67}" http://localhost:8550/ |
||||
{"jsonrpc":"2.0","id":67,"result":"0x93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c"} |
||||
|
||||
#curl -H "Content-Type: application/json" -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"account_sign\",\"params\":[\"0x694267f14675d7e1b9494fd8d72fefe1755710fa\",\"0x$(xxd -pu <<< ' bonk baz gaz')\"],\"id\":67}" http://localhost:8550/ |
||||
{"jsonrpc":"2.0","id":67,"error":{"code":-32000,"message":"Request denied"}} |
||||
|
||||
``` |
||||
|
||||
Meanwhile, in the signer output: |
||||
```text |
||||
INFO [02-21|14:42:41] Op approved |
||||
INFO [02-21|14:42:56] Op rejected |
||||
``` |
||||
|
||||
The signer also stores all traffic over the external API in a log file. The last 4 lines shows the two requests and their responses: |
||||
|
||||
```text |
||||
#tail audit.log -n 4 |
||||
t=2018-02-21T14:42:41+0100 lvl=info msg=Sign api=signer type=request metadata="{\"remote\":\"127.0.0.1:49706\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\"}" addr="0x694267f14675d7e1b9494fd8d72fefe1755710fa [chksum INVALID]" data=202062617a6f6e6b2062617a2067617a0a |
||||
t=2018-02-21T14:42:42+0100 lvl=info msg=Sign api=signer type=response data=93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c error=nil |
||||
t=2018-02-21T14:42:56+0100 lvl=info msg=Sign api=signer type=request metadata="{\"remote\":\"127.0.0.1:49708\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\"}" addr="0x694267f14675d7e1b9494fd8d72fefe1755710fa [chksum INVALID]" data=2020626f6e6b2062617a2067617a0a |
||||
t=2018-02-21T14:42:56+0100 lvl=info msg=Sign api=signer type=response data= error="Request denied" |
||||
``` |
@ -0,0 +1,120 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rpc |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"net" |
||||
) |
||||
|
||||
// StartHTTPEndpoint starts the HTTP RPC endpoint, configured with cors/vhosts/modules
|
||||
func StartHTTPEndpoint(endpoint string, apis []API, modules []string, cors []string, vhosts []string) (net.Listener, *Server, error) { |
||||
// Generate the whitelist based on the allowed modules
|
||||
whitelist := make(map[string]bool) |
||||
for _, module := range modules { |
||||
whitelist[module] = true |
||||
} |
||||
// Register all the APIs exposed by the services
|
||||
handler := NewServer() |
||||
for _, api := range apis { |
||||
if whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) { |
||||
if err := handler.RegisterName(api.Namespace, api.Service); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
log.Debug("HTTP registered", "namespace", api.Namespace) |
||||
} |
||||
} |
||||
// All APIs registered, start the HTTP listener
|
||||
var ( |
||||
listener net.Listener |
||||
err error |
||||
) |
||||
if listener, err = net.Listen("tcp", endpoint); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
go NewHTTPServer(cors, vhosts, handler).Serve(listener) |
||||
return listener, handler, err |
||||
} |
||||
|
||||
// StartWSEndpoint starts a websocket endpoint
|
||||
func StartWSEndpoint(endpoint string, apis []API, modules []string, wsOrigins []string, exposeAll bool) (net.Listener, *Server, error) { |
||||
|
||||
// Generate the whitelist based on the allowed modules
|
||||
whitelist := make(map[string]bool) |
||||
for _, module := range modules { |
||||
whitelist[module] = true |
||||
} |
||||
// Register all the APIs exposed by the services
|
||||
handler := NewServer() |
||||
for _, api := range apis { |
||||
if exposeAll || whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) { |
||||
if err := handler.RegisterName(api.Namespace, api.Service); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
log.Debug("WebSocket registered", "service", api.Service, "namespace", api.Namespace) |
||||
} |
||||
} |
||||
// All APIs registered, start the HTTP listener
|
||||
var ( |
||||
listener net.Listener |
||||
err error |
||||
) |
||||
if listener, err = net.Listen("tcp", endpoint); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
go NewWSServer(wsOrigins, handler).Serve(listener) |
||||
return listener, handler, err |
||||
|
||||
} |
||||
|
||||
// StartIPCEndpoint starts an IPC endpoint
|
||||
func StartIPCEndpoint(isClosedFn func() bool, ipcEndpoint string, apis []API) (net.Listener, *Server, error) { |
||||
// Register all the APIs exposed by the services
|
||||
handler := NewServer() |
||||
for _, api := range apis { |
||||
if err := handler.RegisterName(api.Namespace, api.Service); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
log.Debug("IPC registered", "namespace", api.Namespace) |
||||
} |
||||
// All APIs registered, start the IPC listener
|
||||
var ( |
||||
listener net.Listener |
||||
err error |
||||
) |
||||
if listener, err = CreateIPCListener(ipcEndpoint); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
go func() { |
||||
for { |
||||
conn, err := listener.Accept() |
||||
if err != nil { |
||||
// Terminate if the listener was closed
|
||||
if isClosedFn() { |
||||
log.Info("IPC closed", "err", err) |
||||
} else { |
||||
// Not closed, just some error; report and continue
|
||||
log.Error("IPC accept failed", "err", err) |
||||
} |
||||
continue |
||||
} |
||||
go handler.ServeCodec(NewJSONCodec(conn), OptionMethodInvocation|OptionSubscriptions) |
||||
} |
||||
}() |
||||
|
||||
return listener, handler, nil |
||||
} |
@ -0,0 +1,256 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
|
||||
"bytes" |
||||
"os" |
||||
"regexp" |
||||
) |
||||
|
||||
type decodedArgument struct { |
||||
soltype abi.Argument |
||||
value interface{} |
||||
} |
||||
type decodedCallData struct { |
||||
signature string |
||||
name string |
||||
inputs []decodedArgument |
||||
} |
||||
|
||||
// String implements stringer interface, tries to use the underlying value-type
|
||||
func (arg decodedArgument) String() string { |
||||
var value string |
||||
switch arg.value.(type) { |
||||
case fmt.Stringer: |
||||
value = arg.value.(fmt.Stringer).String() |
||||
default: |
||||
value = fmt.Sprintf("%v", arg.value) |
||||
} |
||||
return fmt.Sprintf("%v: %v", arg.soltype.Type.String(), value) |
||||
} |
||||
|
||||
// String implements stringer interface for decodedCallData
|
||||
func (cd decodedCallData) String() string { |
||||
args := make([]string, len(cd.inputs)) |
||||
for i, arg := range cd.inputs { |
||||
args[i] = arg.String() |
||||
} |
||||
return fmt.Sprintf("%s(%s)", cd.name, strings.Join(args, ",")) |
||||
} |
||||
|
||||
// parseCallData matches the provided call data against the abi definition,
|
||||
// and returns a struct containing the actual go-typed values
|
||||
func parseCallData(calldata []byte, abidata string) (*decodedCallData, error) { |
||||
|
||||
if len(calldata) < 4 { |
||||
return nil, fmt.Errorf("Invalid ABI-data, incomplete method signature of (%d bytes)", len(calldata)) |
||||
} |
||||
|
||||
sigdata, argdata := calldata[:4], calldata[4:] |
||||
if len(argdata)%32 != 0 { |
||||
return nil, fmt.Errorf("Not ABI-encoded data; length should be a multiple of 32 (was %d)", len(argdata)) |
||||
} |
||||
|
||||
abispec, err := abi.JSON(strings.NewReader(abidata)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Failed parsing JSON ABI: %v, abidata: %v", err, abidata) |
||||
} |
||||
|
||||
method, err := abispec.MethodById(sigdata) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
v, err := method.Inputs.UnpackValues(argdata) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
decoded := decodedCallData{signature: method.Sig(), name: method.Name} |
||||
|
||||
for n, argument := range method.Inputs { |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Failed to decode argument %d (signature %v): %v", n, method.Sig(), err) |
||||
} else { |
||||
decodedArg := decodedArgument{ |
||||
soltype: argument, |
||||
value: v[n], |
||||
} |
||||
decoded.inputs = append(decoded.inputs, decodedArg) |
||||
} |
||||
} |
||||
|
||||
// We're finished decoding the data. At this point, we encode the decoded data to see if it matches with the
|
||||
// original data. If we didn't do that, it would e.g. be possible to stuff extra data into the arguments, which
|
||||
// is not detected by merely decoding the data.
|
||||
|
||||
var ( |
||||
encoded []byte |
||||
) |
||||
encoded, err = method.Inputs.PackValues(v) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if !bytes.Equal(encoded, argdata) { |
||||
was := common.Bytes2Hex(encoded) |
||||
exp := common.Bytes2Hex(argdata) |
||||
return nil, fmt.Errorf("WARNING: Supplied data is stuffed with extra data. \nWant %s\nHave %s\nfor method %v", exp, was, method.Sig()) |
||||
} |
||||
return &decoded, nil |
||||
} |
||||
|
||||
// MethodSelectorToAbi converts a method selector into an ABI struct. The returned data is a valid json string
|
||||
// which can be consumed by the standard abi package.
|
||||
func MethodSelectorToAbi(selector string) ([]byte, error) { |
||||
|
||||
re := regexp.MustCompile(`^([^\)]+)\(([a-z0-9,\[\]]*)\)`) |
||||
|
||||
type fakeArg struct { |
||||
Type string `json:"type"` |
||||
} |
||||
type fakeABI struct { |
||||
Name string `json:"name"` |
||||
Type string `json:"type"` |
||||
Inputs []fakeArg `json:"inputs"` |
||||
} |
||||
groups := re.FindStringSubmatch(selector) |
||||
if len(groups) != 3 { |
||||
return nil, fmt.Errorf("Did not match: %v (%v matches)", selector, len(groups)) |
||||
} |
||||
name := groups[1] |
||||
args := groups[2] |
||||
arguments := make([]fakeArg, 0) |
||||
if len(args) > 0 { |
||||
for _, arg := range strings.Split(args, ",") { |
||||
arguments = append(arguments, fakeArg{arg}) |
||||
} |
||||
} |
||||
abicheat := fakeABI{ |
||||
name, "function", arguments, |
||||
} |
||||
return json.Marshal([]fakeABI{abicheat}) |
||||
|
||||
} |
||||
|
||||
type AbiDb struct { |
||||
db map[string]string |
||||
customdb map[string]string |
||||
customdbPath string |
||||
} |
||||
|
||||
// NewEmptyAbiDB exists for test purposes
|
||||
func NewEmptyAbiDB() (*AbiDb, error) { |
||||
return &AbiDb{make(map[string]string), make(map[string]string), ""}, nil |
||||
} |
||||
|
||||
// NewAbiDBFromFile loads signature database from file, and
|
||||
// errors if the file is not valid json. Does no other validation of contents
|
||||
func NewAbiDBFromFile(path string) (*AbiDb, error) { |
||||
raw, err := ioutil.ReadFile(path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
db, err := NewEmptyAbiDB() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
json.Unmarshal(raw, &db.db) |
||||
return db, nil |
||||
} |
||||
|
||||
// NewAbiDBFromFiles loads both the standard signature database and a custom database. The latter will be used
|
||||
// to write new values into if they are submitted via the API
|
||||
func NewAbiDBFromFiles(standard, custom string) (*AbiDb, error) { |
||||
|
||||
db := &AbiDb{make(map[string]string), make(map[string]string), custom} |
||||
db.customdbPath = custom |
||||
|
||||
raw, err := ioutil.ReadFile(standard) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
json.Unmarshal(raw, &db.db) |
||||
// Custom file may not exist. Will be created during save, if needed
|
||||
if _, err := os.Stat(custom); err == nil { |
||||
raw, err = ioutil.ReadFile(custom) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
json.Unmarshal(raw, &db.customdb) |
||||
} |
||||
|
||||
return db, nil |
||||
} |
||||
|
||||
// LookupMethodSelector checks the given 4byte-sequence against the known ABI methods.
|
||||
// OBS: This method does not validate the match, it's assumed the caller will do so
|
||||
func (db *AbiDb) LookupMethodSelector(id []byte) (string, error) { |
||||
if len(id) < 4 { |
||||
return "", fmt.Errorf("Expected 4-byte id, got %d", len(id)) |
||||
} |
||||
sig := common.ToHex(id[:4]) |
||||
if key, exists := db.db[sig]; exists { |
||||
return key, nil |
||||
} |
||||
if key, exists := db.customdb[sig]; exists { |
||||
return key, nil |
||||
} |
||||
return "", fmt.Errorf("Signature %v not found", sig) |
||||
} |
||||
func (db *AbiDb) Size() int { |
||||
return len(db.db) |
||||
} |
||||
|
||||
// saveCustomAbi saves a signature ephemerally. If custom file is used, also saves to disk
|
||||
func (db *AbiDb) saveCustomAbi(selector, signature string) error { |
||||
db.customdb[signature] = selector |
||||
if db.customdbPath == "" { |
||||
return nil //Not an error per se, just not used
|
||||
} |
||||
d, err := json.Marshal(db.customdb) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
err = ioutil.WriteFile(db.customdbPath, d, 0600) |
||||
return err |
||||
} |
||||
|
||||
// Adds a signature to the database, if custom database saving is enabled.
|
||||
// OBS: This method does _not_ validate the correctness of the data,
|
||||
// it is assumed that the caller has already done so
|
||||
func (db *AbiDb) AddSignature(selector string, data []byte) error { |
||||
if len(data) < 4 { |
||||
return nil |
||||
} |
||||
_, err := db.LookupMethodSelector(data[:4]) |
||||
if err == nil { |
||||
return nil |
||||
} |
||||
sig := common.ToHex(data[:4]) |
||||
return db.saveCustomAbi(selector, sig) |
||||
} |
@ -0,0 +1,247 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"io/ioutil" |
||||
"math/big" |
||||
"reflect" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
func verify(t *testing.T, jsondata, calldata string, exp []interface{}) { |
||||
|
||||
abispec, err := abi.JSON(strings.NewReader(jsondata)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
cd := common.Hex2Bytes(calldata) |
||||
sigdata, argdata := cd[:4], cd[4:] |
||||
method, err := abispec.MethodById(sigdata) |
||||
|
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
data, err := method.Inputs.UnpackValues(argdata) |
||||
|
||||
if len(data) != len(exp) { |
||||
t.Fatalf("Mismatched length, expected %d, got %d", len(exp), len(data)) |
||||
} |
||||
for i, elem := range data { |
||||
if !reflect.DeepEqual(elem, exp[i]) { |
||||
t.Fatalf("Unpack error, arg %d, got %v, want %v", i, elem, exp[i]) |
||||
} |
||||
} |
||||
} |
||||
func TestNewUnpacker(t *testing.T) { |
||||
type unpackTest struct { |
||||
jsondata string |
||||
calldata string |
||||
exp []interface{} |
||||
} |
||||
testcases := []unpackTest{ |
||||
{ // https://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types
|
||||
`[{"type":"function","name":"f", "inputs":[{"type":"uint256"},{"type":"uint32[]"},{"type":"bytes10"},{"type":"bytes"}]}]`, |
||||
// 0x123, [0x456, 0x789], "1234567890", "Hello, world!"
|
||||
"8be65246" + "00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000", |
||||
[]interface{}{ |
||||
big.NewInt(0x123), |
||||
[]uint32{0x456, 0x789}, |
||||
[10]byte{49, 50, 51, 52, 53, 54, 55, 56, 57, 48}, |
||||
common.Hex2Bytes("48656c6c6f2c20776f726c6421"), |
||||
}, |
||||
}, { // https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#examples
|
||||
`[{"type":"function","name":"sam","inputs":[{"type":"bytes"},{"type":"bool"},{"type":"uint256[]"}]}]`, |
||||
// "dave", true and [1,2,3]
|
||||
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003", |
||||
[]interface{}{ |
||||
[]byte{0x64, 0x61, 0x76, 0x65}, |
||||
true, |
||||
[]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)}, |
||||
}, |
||||
}, { |
||||
`[{"type":"function","name":"send","inputs":[{"type":"uint256"}]}]`, |
||||
"a52c101e0000000000000000000000000000000000000000000000000000000000000012", |
||||
[]interface{}{big.NewInt(0x12)}, |
||||
}, { |
||||
`[{"type":"function","name":"compareAndApprove","inputs":[{"type":"address"},{"type":"uint256"},{"type":"uint256"}]}]`, |
||||
"751e107900000000000000000000000000000133700000deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
[]interface{}{ |
||||
common.HexToAddress("0x00000133700000deadbeef000000000000000000"), |
||||
new(big.Int).SetBytes([]byte{0x00}), |
||||
big.NewInt(0x1), |
||||
}, |
||||
}, |
||||
} |
||||
for _, c := range testcases { |
||||
verify(t, c.jsondata, c.calldata, c.exp) |
||||
} |
||||
|
||||
} |
||||
|
||||
/* |
||||
func TestReflect(t *testing.T) { |
||||
a := big.NewInt(0) |
||||
b := new(big.Int).SetBytes([]byte{0x00}) |
||||
if !reflect.DeepEqual(a, b) { |
||||
t.Fatalf("Nope, %v != %v", a, b) |
||||
} |
||||
} |
||||
*/ |
||||
|
||||
func TestCalldataDecoding(t *testing.T) { |
||||
|
||||
// send(uint256) : a52c101e
|
||||
// compareAndApprove(address,uint256,uint256) : 751e1079
|
||||
// issue(address[],uint256) : 42958b54
|
||||
jsondata := ` |
||||
[ |
||||
{"type":"function","name":"send","inputs":[{"name":"a","type":"uint256"}]}, |
||||
{"type":"function","name":"compareAndApprove","inputs":[{"name":"a","type":"address"},{"name":"a","type":"uint256"},{"name":"a","type":"uint256"}]}, |
||||
{"type":"function","name":"issue","inputs":[{"name":"a","type":"address[]"},{"name":"a","type":"uint256"}]}, |
||||
{"type":"function","name":"sam","inputs":[{"name":"a","type":"bytes"},{"name":"a","type":"bool"},{"name":"a","type":"uint256[]"}]} |
||||
]` |
||||
//Expected failures
|
||||
for _, hexdata := range []string{ |
||||
"a52c101e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042", |
||||
"a52c101e000000000000000000000000000000000000000000000000000000000000001200", |
||||
"a52c101e00000000000000000000000000000000000000000000000000000000000000", |
||||
"a52c101e", |
||||
"a52c10", |
||||
"", |
||||
// Too short
|
||||
"751e10790000000000000000000000000000000000000000000000000000000000000012", |
||||
"751e1079FFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", |
||||
//Not valid multiple of 32
|
||||
"deadbeef00000000000000000000000000000000000000000000000000000000000000", |
||||
//Too short 'issue'
|
||||
"42958b5400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042", |
||||
// Too short compareAndApprove
|
||||
"a52c101e00ff0000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042", |
||||
// From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
|
||||
// contains a bool with illegal values
|
||||
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003", |
||||
} { |
||||
_, err := parseCallData(common.Hex2Bytes(hexdata), jsondata) |
||||
if err == nil { |
||||
t.Errorf("Expected decoding to fail: %s", hexdata) |
||||
} |
||||
} |
||||
|
||||
//Expected success
|
||||
for _, hexdata := range []string{ |
||||
// From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
|
||||
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003", |
||||
"a52c101e0000000000000000000000000000000000000000000000000000000000000012", |
||||
"a52c101eFFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", |
||||
"751e1079000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"42958b54" + |
||||
// start of dynamic type
|
||||
"0000000000000000000000000000000000000000000000000000000000000040" + |
||||
//uint256
|
||||
"0000000000000000000000000000000000000000000000000000000000000001" + |
||||
// length of array
|
||||
"0000000000000000000000000000000000000000000000000000000000000002" + |
||||
// array values
|
||||
"000000000000000000000000000000000000000000000000000000000000dead" + |
||||
"000000000000000000000000000000000000000000000000000000000000beef", |
||||
} { |
||||
_, err := parseCallData(common.Hex2Bytes(hexdata), jsondata) |
||||
if err != nil { |
||||
t.Errorf("Unexpected failure on input %s:\n %v (%d bytes) ", hexdata, err, len(common.Hex2Bytes(hexdata))) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestSelectorUnmarshalling(t *testing.T) { |
||||
var ( |
||||
db *AbiDb |
||||
err error |
||||
abistring []byte |
||||
abistruct abi.ABI |
||||
) |
||||
|
||||
db, err = NewAbiDBFromFile("../../cmd/clef/4byte.json") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
fmt.Printf("DB size %v\n", db.Size()) |
||||
for id, selector := range db.db { |
||||
|
||||
abistring, err = MethodSelectorToAbi(selector) |
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
abistruct, err = abi.JSON(strings.NewReader(string(abistring))) |
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
m, err := abistruct.MethodById(common.Hex2Bytes(id[2:])) |
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
if m.Sig() != selector { |
||||
t.Errorf("Expected equality: %v != %v", m.Sig(), selector) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
func TestCustomABI(t *testing.T) { |
||||
d, err := ioutil.TempDir("", "signer-4byte-test") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
filename := fmt.Sprintf("%s/4byte_custom.json", d) |
||||
abidb, err := NewAbiDBFromFiles("../../cmd/clef/4byte.json", filename) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
// Now we'll remove all existing signatures
|
||||
abidb.db = make(map[string]string) |
||||
calldata := common.Hex2Bytes("a52c101edeadbeef") |
||||
_, err = abidb.LookupMethodSelector(calldata) |
||||
if err == nil { |
||||
t.Fatalf("Should not find a match on empty db") |
||||
} |
||||
if err = abidb.AddSignature("send(uint256)", calldata); err != nil { |
||||
t.Fatalf("Failed to save file: %v", err) |
||||
} |
||||
_, err = abidb.LookupMethodSelector(calldata) |
||||
if err != nil { |
||||
t.Fatalf("Should find a match for abi signature, got: %v", err) |
||||
} |
||||
//Check that it wrote to file
|
||||
abidb2, err := NewAbiDBFromFile(filename) |
||||
if err != nil { |
||||
t.Fatalf("Failed to create new abidb: %v", err) |
||||
} |
||||
_, err = abidb2.LookupMethodSelector(calldata) |
||||
if err != nil { |
||||
t.Fatalf("Save failed: should find a match for abi signature after loading from disk") |
||||
} |
||||
} |
@ -0,0 +1,500 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"math/big" |
||||
"reflect" |
||||
|
||||
"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/crypto" |
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
// ExternalAPI defines the external API through which signing requests are made.
|
||||
type ExternalAPI interface { |
||||
// List available accounts
|
||||
List(ctx context.Context) (Accounts, error) |
||||
// New request to create a new account
|
||||
New(ctx context.Context) (accounts.Account, error) |
||||
// SignTransaction request to sign the specified transaction
|
||||
SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) |
||||
// Sign - request to sign the given data (plus prefix)
|
||||
Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) |
||||
// EcRecover - request to perform ecrecover
|
||||
EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) |
||||
// Export - request to export an account
|
||||
Export(ctx context.Context, addr common.Address) (json.RawMessage, error) |
||||
// Import - request to import an account
|
||||
Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) |
||||
} |
||||
|
||||
// SignerUI specifies what method a UI needs to implement to be able to be used as a UI for the signer
|
||||
type SignerUI interface { |
||||
// ApproveTx prompt the user for confirmation to request to sign Transaction
|
||||
ApproveTx(request *SignTxRequest) (SignTxResponse, error) |
||||
// ApproveSignData prompt the user for confirmation to request to sign data
|
||||
ApproveSignData(request *SignDataRequest) (SignDataResponse, error) |
||||
// ApproveExport prompt the user for confirmation to export encrypted Account json
|
||||
ApproveExport(request *ExportRequest) (ExportResponse, error) |
||||
// ApproveImport prompt the user for confirmation to import Account json
|
||||
ApproveImport(request *ImportRequest) (ImportResponse, error) |
||||
// ApproveListing prompt the user for confirmation to list accounts
|
||||
// the list of accounts to list can be modified by the UI
|
||||
ApproveListing(request *ListRequest) (ListResponse, error) |
||||
// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
|
||||
ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) |
||||
// ShowError displays error message to user
|
||||
ShowError(message string) |
||||
// ShowInfo displays info message to user
|
||||
ShowInfo(message string) |
||||
// OnApprovedTx notifies the UI about a transaction having been successfully signed.
|
||||
// This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient.
|
||||
OnApprovedTx(tx ethapi.SignTransactionResult) |
||||
// OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
|
||||
// information
|
||||
OnSignerStartup(info StartupInfo) |
||||
} |
||||
|
||||
// SignerAPI defines the actual implementation of ExternalAPI
|
||||
type SignerAPI struct { |
||||
chainID *big.Int |
||||
am *accounts.Manager |
||||
UI SignerUI |
||||
validator *Validator |
||||
} |
||||
|
||||
// Metadata about a request
|
||||
type Metadata struct { |
||||
Remote string `json:"remote"` |
||||
Local string `json:"local"` |
||||
Scheme string `json:"scheme"` |
||||
} |
||||
|
||||
// MetadataFromContext extracts Metadata from a given context.Context
|
||||
func MetadataFromContext(ctx context.Context) Metadata { |
||||
m := Metadata{"NA", "NA", "NA"} // batman
|
||||
|
||||
if v := ctx.Value("remote"); v != nil { |
||||
m.Remote = v.(string) |
||||
} |
||||
if v := ctx.Value("scheme"); v != nil { |
||||
m.Scheme = v.(string) |
||||
} |
||||
if v := ctx.Value("local"); v != nil { |
||||
m.Local = v.(string) |
||||
} |
||||
return m |
||||
} |
||||
|
||||
// String implements Stringer interface
|
||||
func (m Metadata) String() string { |
||||
s, err := json.Marshal(m) |
||||
if err == nil { |
||||
return string(s) |
||||
} |
||||
return err.Error() |
||||
} |
||||
|
||||
// types for the requests/response types between signer and UI
|
||||
type ( |
||||
// SignTxRequest contains info about a Transaction to sign
|
||||
SignTxRequest struct { |
||||
Transaction SendTxArgs `json:"transaction"` |
||||
Callinfo []ValidationInfo `json:"call_info"` |
||||
Meta Metadata `json:"meta"` |
||||
} |
||||
// SignTxResponse result from SignTxRequest
|
||||
SignTxResponse struct { |
||||
//The UI may make changes to the TX
|
||||
Transaction SendTxArgs `json:"transaction"` |
||||
Approved bool `json:"approved"` |
||||
Password string `json:"password"` |
||||
} |
||||
// ExportRequest info about query to export accounts
|
||||
ExportRequest struct { |
||||
Address common.Address `json:"address"` |
||||
Meta Metadata `json:"meta"` |
||||
} |
||||
// ExportResponse response to export-request
|
||||
ExportResponse struct { |
||||
Approved bool `json:"approved"` |
||||
} |
||||
// ImportRequest info about request to import an Account
|
||||
ImportRequest struct { |
||||
Meta Metadata `json:"meta"` |
||||
} |
||||
ImportResponse struct { |
||||
Approved bool `json:"approved"` |
||||
OldPassword string `json:"old_password"` |
||||
NewPassword string `json:"new_password"` |
||||
} |
||||
SignDataRequest struct { |
||||
Address common.MixedcaseAddress `json:"address"` |
||||
Rawdata hexutil.Bytes `json:"raw_data"` |
||||
Message string `json:"message"` |
||||
Hash hexutil.Bytes `json:"hash"` |
||||
Meta Metadata `json:"meta"` |
||||
} |
||||
SignDataResponse struct { |
||||
Approved bool `json:"approved"` |
||||
Password string |
||||
} |
||||
NewAccountRequest struct { |
||||
Meta Metadata `json:"meta"` |
||||
} |
||||
NewAccountResponse struct { |
||||
Approved bool `json:"approved"` |
||||
Password string `json:"password"` |
||||
} |
||||
ListRequest struct { |
||||
Accounts []Account `json:"accounts"` |
||||
Meta Metadata `json:"meta"` |
||||
} |
||||
ListResponse struct { |
||||
Accounts []Account `json:"accounts"` |
||||
} |
||||
Message struct { |
||||
Text string `json:"text"` |
||||
} |
||||
StartupInfo struct { |
||||
Info map[string]interface{} `json:"info"` |
||||
} |
||||
) |
||||
|
||||
var ErrRequestDenied = errors.New("Request denied") |
||||
|
||||
type errorWrapper struct { |
||||
msg string |
||||
err error |
||||
} |
||||
|
||||
func (ew errorWrapper) String() string { |
||||
return fmt.Sprintf("%s\n%s", ew.msg, ew.err) |
||||
} |
||||
|
||||
// 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, ui SignerUI, abidb *AbiDb, lightKDF bool) *SignerAPI { |
||||
var ( |
||||
backends []accounts.Backend |
||||
n, p = keystore.StandardScryptN, keystore.StandardScryptP |
||||
) |
||||
if lightKDF { |
||||
n, p = keystore.LightScryptN, keystore.LightScryptP |
||||
} |
||||
// support password based accounts
|
||||
if len(ksLocation) > 0 { |
||||
backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) |
||||
} |
||||
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...), ui, NewValidator(abidb)} |
||||
} |
||||
|
||||
// List returns the set of wallet this signer manages. Each wallet can contain
|
||||
// multiple accounts.
|
||||
func (api *SignerAPI) List(ctx context.Context) (Accounts, error) { |
||||
var accs []Account |
||||
for _, wallet := range api.am.Wallets() { |
||||
for _, acc := range wallet.Accounts() { |
||||
acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address} |
||||
accs = append(accs, acc) |
||||
} |
||||
} |
||||
result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if result.Accounts == nil { |
||||
return nil, ErrRequestDenied |
||||
|
||||
} |
||||
return result.Accounts, nil |
||||
} |
||||
|
||||
// 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) (accounts.Account, error) { |
||||
be := api.am.Backends(keystore.KeyStoreType) |
||||
if len(be) == 0 { |
||||
return accounts.Account{}, errors.New("password based accounts not supported") |
||||
} |
||||
resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}) |
||||
|
||||
if err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
if !resp.Approved { |
||||
return accounts.Account{}, ErrRequestDenied |
||||
} |
||||
return be[0].(*keystore.KeyStore).NewAccount(resp.Password) |
||||
} |
||||
|
||||
// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer.
|
||||
// it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow
|
||||
// UI-modifications to requests
|
||||
func logDiff(original *SignTxRequest, new *SignTxResponse) bool { |
||||
modified := false |
||||
if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { |
||||
log.Info("Sender-account changed by UI", "was", f0, "is", f1) |
||||
modified = true |
||||
} |
||||
if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { |
||||
log.Info("Recipient-account changed by UI", "was", t0, "is", t1) |
||||
modified = true |
||||
} |
||||
if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 { |
||||
modified = true |
||||
log.Info("Gas changed by UI", "was", g0, "is", g1) |
||||
} |
||||
if g0, g1 := big.Int(original.Transaction.GasPrice), big.Int(new.Transaction.GasPrice); g0.Cmp(&g1) != 0 { |
||||
modified = true |
||||
log.Info("GasPrice changed by UI", "was", g0, "is", g1) |
||||
} |
||||
if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 { |
||||
modified = true |
||||
log.Info("Value changed by UI", "was", v0, "is", v1) |
||||
} |
||||
if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { |
||||
d0s := "" |
||||
d1s := "" |
||||
if d0 != nil { |
||||
d0s = common.ToHex(*d0) |
||||
} |
||||
if d1 != nil { |
||||
d1s = common.ToHex(*d1) |
||||
} |
||||
if d1s != d0s { |
||||
modified = true |
||||
log.Info("Data changed by UI", "was", d0s, "is", d1s) |
||||
} |
||||
} |
||||
if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { |
||||
modified = true |
||||
log.Info("Nonce changed by UI", "was", n0, "is", n1) |
||||
} |
||||
return modified |
||||
} |
||||
|
||||
// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form
|
||||
func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { |
||||
var ( |
||||
err error |
||||
result SignTxResponse |
||||
) |
||||
msgs, err := api.validator.ValidateTransaction(&args, methodSelector) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
req := SignTxRequest{ |
||||
Transaction: args, |
||||
Meta: MetadataFromContext(ctx), |
||||
Callinfo: msgs.Messages, |
||||
} |
||||
// Process approval
|
||||
result, err = api.UI.ApproveTx(&req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if !result.Approved { |
||||
return nil, ErrRequestDenied |
||||
} |
||||
// Log changes made by the UI to the signing-request
|
||||
logDiff(&req, &result) |
||||
var ( |
||||
acc accounts.Account |
||||
wallet accounts.Wallet |
||||
) |
||||
acc = accounts.Account{Address: result.Transaction.From.Address()} |
||||
wallet, err = api.am.Find(acc) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Convert fields into a real transaction
|
||||
var unsignedTx = result.Transaction.toTransaction() |
||||
|
||||
// The one to sign is the one that was returned from the UI
|
||||
signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, unsignedTx, api.chainID) |
||||
if err != nil { |
||||
api.UI.ShowError(err.Error()) |
||||
return nil, err |
||||
} |
||||
|
||||
rlpdata, err := rlp.EncodeToBytes(signedTx) |
||||
response := ethapi.SignTransactionResult{Raw: rlpdata, Tx: signedTx} |
||||
|
||||
// Finally, send the signed tx to the UI
|
||||
api.UI.OnApprovedTx(response) |
||||
// ...and to the external caller
|
||||
return &response, nil |
||||
|
||||
} |
||||
|
||||
// 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.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) { |
||||
sighash, msg := SignHash(data) |
||||
// We make the request prior to looking up if we actually have the account, to prevent
|
||||
// account-enumeration via the API
|
||||
req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: MetadataFromContext(ctx)} |
||||
res, err := api.UI.ApproveSignData(req) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if !res.Approved { |
||||
return nil, ErrRequestDenied |
||||
} |
||||
// Look up the wallet containing the requested signer
|
||||
account := accounts.Account{Address: addr.Address()} |
||||
wallet, err := api.am.Find(account) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// Assemble sign the data with the wallet
|
||||
signature, err := wallet.SignHashWithPassphrase(account, res.Password, sighash) |
||||
if err != nil { |
||||
api.UI.ShowError(err.Error()) |
||||
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
|
||||
hash, _ := SignHash(data) |
||||
rpk, err := crypto.Ecrecover(hash, 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 calculated 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, string) { |
||||
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) |
||||
return crypto.Keccak256([]byte(msg)), 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) { |
||||
res, err := api.UI.ApproveExport(&ExportRequest{Address: addr, Meta: MetadataFromContext(ctx)}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if !res.Approved { |
||||
return nil, ErrRequestDenied |
||||
} |
||||
// Look up the wallet containing the requested signer
|
||||
wallet, err := api.am.Find(accounts.Account{Address: addr}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if wallet.URL().Scheme != keystore.KeyStoreScheme { |
||||
return nil, fmt.Errorf("Account is not a keystore-account") |
||||
} |
||||
return ioutil.ReadFile(wallet.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) (Account, error) { |
||||
be := api.am.Backends(keystore.KeyStoreType) |
||||
|
||||
if len(be) == 0 { |
||||
return Account{}, errors.New("password based accounts not supported") |
||||
} |
||||
res, err := api.UI.ApproveImport(&ImportRequest{Meta: MetadataFromContext(ctx)}) |
||||
|
||||
if err != nil { |
||||
return Account{}, err |
||||
} |
||||
if !res.Approved { |
||||
return Account{}, ErrRequestDenied |
||||
} |
||||
acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, res.OldPassword, res.NewPassword) |
||||
if err != nil { |
||||
api.UI.ShowError(err.Error()) |
||||
return Account{}, err |
||||
} |
||||
return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil |
||||
} |
@ -0,0 +1,386 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
package core |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"math/big" |
||||
"os" |
||||
"path/filepath" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore" |
||||
"github.com/ethereum/go-ethereum/cmd/utils" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
//Used for testing
|
||||
type HeadlessUI struct { |
||||
controller chan string |
||||
} |
||||
|
||||
func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) { |
||||
} |
||||
|
||||
func (ui *HeadlessUI) OnApprovedTx(tx ethapi.SignTransactionResult) { |
||||
fmt.Printf("OnApproved called") |
||||
} |
||||
|
||||
func (ui *HeadlessUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { |
||||
|
||||
switch <-ui.controller { |
||||
case "Y": |
||||
return SignTxResponse{request.Transaction, true, <-ui.controller}, nil |
||||
case "M": //Modify
|
||||
old := big.Int(request.Transaction.Value) |
||||
newVal := big.NewInt(0).Add(&old, big.NewInt(1)) |
||||
request.Transaction.Value = hexutil.Big(*newVal) |
||||
return SignTxResponse{request.Transaction, true, <-ui.controller}, nil |
||||
default: |
||||
return SignTxResponse{request.Transaction, false, ""}, nil |
||||
} |
||||
} |
||||
func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { |
||||
if "Y" == <-ui.controller { |
||||
return SignDataResponse{true, <-ui.controller}, nil |
||||
} |
||||
return SignDataResponse{false, ""}, nil |
||||
} |
||||
func (ui *HeadlessUI) ApproveExport(request *ExportRequest) (ExportResponse, error) { |
||||
|
||||
return ExportResponse{<-ui.controller == "Y"}, nil |
||||
|
||||
} |
||||
func (ui *HeadlessUI) ApproveImport(request *ImportRequest) (ImportResponse, error) { |
||||
|
||||
if "Y" == <-ui.controller { |
||||
return ImportResponse{true, <-ui.controller, <-ui.controller}, nil |
||||
} |
||||
return ImportResponse{false, "", ""}, nil |
||||
} |
||||
func (ui *HeadlessUI) ApproveListing(request *ListRequest) (ListResponse, error) { |
||||
|
||||
switch <-ui.controller { |
||||
case "A": |
||||
return ListResponse{request.Accounts}, nil |
||||
case "1": |
||||
l := make([]Account, 1) |
||||
l[0] = request.Accounts[1] |
||||
return ListResponse{l}, nil |
||||
default: |
||||
return ListResponse{nil}, nil |
||||
} |
||||
} |
||||
func (ui *HeadlessUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { |
||||
|
||||
if "Y" == <-ui.controller { |
||||
return NewAccountResponse{true, <-ui.controller}, nil |
||||
} |
||||
return NewAccountResponse{false, ""}, nil |
||||
} |
||||
func (ui *HeadlessUI) ShowError(message string) { |
||||
//stdout is used by communication
|
||||
fmt.Fprint(os.Stderr, message) |
||||
} |
||||
func (ui *HeadlessUI) ShowInfo(message string) { |
||||
//stdout is used by communication
|
||||
fmt.Fprint(os.Stderr, message) |
||||
} |
||||
|
||||
func tmpDirName(t *testing.T) string { |
||||
d, err := ioutil.TempDir("", "eth-keystore-test") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
d, err = filepath.EvalSymlinks(d) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return d |
||||
} |
||||
|
||||
func setup(t *testing.T) (*SignerAPI, chan string) { |
||||
|
||||
controller := make(chan string, 10) |
||||
|
||||
db, err := NewAbiDBFromFile("../../cmd/clef/4byte.json") |
||||
if err != nil { |
||||
utils.Fatalf(err.Error()) |
||||
} |
||||
var ( |
||||
ui = &HeadlessUI{controller} |
||||
api = NewSignerAPI( |
||||
1, |
||||
tmpDirName(t), |
||||
true, |
||||
ui, |
||||
db, |
||||
true) |
||||
) |
||||
return api, controller |
||||
} |
||||
func createAccount(control chan string, api *SignerAPI, t *testing.T) { |
||||
|
||||
control <- "Y" |
||||
control <- "apassword" |
||||
_, err := api.New(context.Background()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
// Some time to allow changes to propagate
|
||||
time.Sleep(250 * time.Millisecond) |
||||
} |
||||
func failCreateAccount(control chan string, api *SignerAPI, t *testing.T) { |
||||
control <- "N" |
||||
acc, err := api.New(context.Background()) |
||||
if err != ErrRequestDenied { |
||||
t.Fatal(err) |
||||
} |
||||
if acc.Address != (common.Address{}) { |
||||
t.Fatal("Empty address should be returned") |
||||
} |
||||
} |
||||
func list(control chan string, api *SignerAPI, t *testing.T) []Account { |
||||
control <- "A" |
||||
list, err := api.List(context.Background()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return list |
||||
} |
||||
|
||||
func TestNewAcc(t *testing.T) { |
||||
|
||||
api, control := setup(t) |
||||
verifyNum := func(num int) { |
||||
if list := list(control, api, t); len(list) != num { |
||||
t.Errorf("Expected %d accounts, got %d", num, len(list)) |
||||
} |
||||
} |
||||
// Testing create and create-deny
|
||||
createAccount(control, api, t) |
||||
createAccount(control, api, t) |
||||
failCreateAccount(control, api, t) |
||||
failCreateAccount(control, api, t) |
||||
createAccount(control, api, t) |
||||
failCreateAccount(control, api, t) |
||||
createAccount(control, api, t) |
||||
failCreateAccount(control, api, t) |
||||
verifyNum(4) |
||||
|
||||
// Testing listing:
|
||||
// Listing one Account
|
||||
control <- "1" |
||||
list, err := api.List(context.Background()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(list) != 1 { |
||||
t.Fatalf("List should only show one Account") |
||||
} |
||||
// Listing denied
|
||||
control <- "Nope" |
||||
list, err = api.List(context.Background()) |
||||
if len(list) != 0 { |
||||
t.Fatalf("List should be empty") |
||||
} |
||||
if err != ErrRequestDenied { |
||||
t.Fatal("Expected deny") |
||||
} |
||||
} |
||||
|
||||
func TestSignData(t *testing.T) { |
||||
|
||||
api, control := setup(t) |
||||
//Create two accounts
|
||||
createAccount(control, api, t) |
||||
createAccount(control, api, t) |
||||
control <- "1" |
||||
list, err := api.List(context.Background()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
a := common.NewMixedcaseAddress(list[0].Address) |
||||
|
||||
control <- "Y" |
||||
control <- "wrongpassword" |
||||
h, err := api.Sign(context.Background(), a, []byte("EHLO world")) |
||||
if h != nil { |
||||
t.Errorf("Expected nil-data, got %x", h) |
||||
} |
||||
if err != keystore.ErrDecrypt { |
||||
t.Errorf("Expected ErrLocked! %v", err) |
||||
} |
||||
|
||||
control <- "No way" |
||||
h, err = api.Sign(context.Background(), a, []byte("EHLO world")) |
||||
if h != nil { |
||||
t.Errorf("Expected nil-data, got %x", h) |
||||
} |
||||
if err != ErrRequestDenied { |
||||
t.Errorf("Expected ErrRequestDenied! %v", err) |
||||
} |
||||
|
||||
control <- "Y" |
||||
control <- "apassword" |
||||
h, err = api.Sign(context.Background(), a, []byte("EHLO world")) |
||||
|
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if h == nil || len(h) != 65 { |
||||
t.Errorf("Expected 65 byte signature (got %d bytes)", len(h)) |
||||
} |
||||
} |
||||
func mkTestTx(from common.MixedcaseAddress) SendTxArgs { |
||||
to := common.NewMixedcaseAddress(common.HexToAddress("0x1337")) |
||||
gas := hexutil.Uint64(21000) |
||||
gasPrice := (hexutil.Big)(*big.NewInt(2000000000)) |
||||
value := (hexutil.Big)(*big.NewInt(1e18)) |
||||
nonce := (hexutil.Uint64)(0) |
||||
data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a")) |
||||
tx := SendTxArgs{ |
||||
From: from, |
||||
To: &to, |
||||
Gas: gas, |
||||
GasPrice: gasPrice, |
||||
Value: value, |
||||
Data: &data, |
||||
Nonce: nonce} |
||||
return tx |
||||
} |
||||
|
||||
func TestSignTx(t *testing.T) { |
||||
|
||||
var ( |
||||
list Accounts |
||||
res, res2 *ethapi.SignTransactionResult |
||||
err error |
||||
) |
||||
|
||||
api, control := setup(t) |
||||
createAccount(control, api, t) |
||||
control <- "A" |
||||
list, err = api.List(context.Background()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
a := common.NewMixedcaseAddress(list[0].Address) |
||||
|
||||
methodSig := "test(uint)" |
||||
tx := mkTestTx(a) |
||||
|
||||
control <- "Y" |
||||
control <- "wrongpassword" |
||||
res, err = api.SignTransaction(context.Background(), tx, &methodSig) |
||||
if res != nil { |
||||
t.Errorf("Expected nil-response, got %v", res) |
||||
} |
||||
if err != keystore.ErrDecrypt { |
||||
t.Errorf("Expected ErrLocked! %v", err) |
||||
} |
||||
|
||||
control <- "No way" |
||||
res, err = api.SignTransaction(context.Background(), tx, &methodSig) |
||||
if res != nil { |
||||
t.Errorf("Expected nil-response, got %v", res) |
||||
} |
||||
if err != ErrRequestDenied { |
||||
t.Errorf("Expected ErrRequestDenied! %v", err) |
||||
} |
||||
|
||||
control <- "Y" |
||||
control <- "apassword" |
||||
res, err = api.SignTransaction(context.Background(), tx, &methodSig) |
||||
|
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
parsedTx := &types.Transaction{} |
||||
rlp.Decode(bytes.NewReader(res.Raw), parsedTx) |
||||
//The tx should NOT be modified by the UI
|
||||
if parsedTx.Value().Cmp(tx.Value.ToInt()) != 0 { |
||||
t.Errorf("Expected value to be unchanged, expected %v got %v", tx.Value, parsedTx.Value()) |
||||
} |
||||
control <- "Y" |
||||
control <- "apassword" |
||||
|
||||
res2, err = api.SignTransaction(context.Background(), tx, &methodSig) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !bytes.Equal(res.Raw, res2.Raw) { |
||||
t.Error("Expected tx to be unmodified by UI") |
||||
} |
||||
|
||||
//The tx is modified by the UI
|
||||
control <- "M" |
||||
control <- "apassword" |
||||
|
||||
res2, err = api.SignTransaction(context.Background(), tx, &methodSig) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
parsedTx2 := &types.Transaction{} |
||||
rlp.Decode(bytes.NewReader(res.Raw), parsedTx2) |
||||
//The tx should be modified by the UI
|
||||
if parsedTx2.Value().Cmp(tx.Value.ToInt()) != 0 { |
||||
t.Errorf("Expected value to be unchanged, got %v", parsedTx.Value()) |
||||
} |
||||
|
||||
if bytes.Equal(res.Raw, res2.Raw) { |
||||
t.Error("Expected tx to be modified by UI") |
||||
} |
||||
|
||||
} |
||||
|
||||
/* |
||||
func TestAsyncronousResponses(t *testing.T){ |
||||
|
||||
//Set up one account
|
||||
api, control := setup(t) |
||||
createAccount(control, api, t) |
||||
|
||||
// Two transactions, the second one with larger value than the first
|
||||
tx1 := mkTestTx() |
||||
newVal := big.NewInt(0).Add((*big.Int) (tx1.Value), big.NewInt(1)) |
||||
tx2 := mkTestTx() |
||||
tx2.Value = (*hexutil.Big)(newVal) |
||||
|
||||
control <- "W" //wait
|
||||
control <- "Y" //
|
||||
control <- "apassword" |
||||
control <- "Y" //
|
||||
control <- "apassword" |
||||
|
||||
var err error |
||||
|
||||
h1, err := api.SignTransaction(context.Background(), common.HexToAddress("1111"), tx1, nil) |
||||
h2, err := api.SignTransaction(context.Background(), common.HexToAddress("2222"), tx2, nil) |
||||
|
||||
|
||||
} |
||||
*/ |
@ -0,0 +1,110 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"encoding/json" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
type AuditLogger struct { |
||||
log log.Logger |
||||
api ExternalAPI |
||||
} |
||||
|
||||
func (l *AuditLogger) List(ctx context.Context) (Accounts, error) { |
||||
l.log.Info("List", "type", "request", "metadata", MetadataFromContext(ctx).String()) |
||||
res, e := l.api.List(ctx) |
||||
|
||||
l.log.Info("List", "type", "response", "data", res.String()) |
||||
|
||||
return res, e |
||||
} |
||||
|
||||
func (l *AuditLogger) New(ctx context.Context) (accounts.Account, error) { |
||||
return l.api.New(ctx) |
||||
} |
||||
|
||||
func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { |
||||
sel := "<nil>" |
||||
if methodSelector != nil { |
||||
sel = *methodSelector |
||||
} |
||||
l.log.Info("SignTransaction", "type", "request", "metadata", MetadataFromContext(ctx).String(), |
||||
"tx", args.String(), |
||||
"methodSelector", sel) |
||||
|
||||
res, e := l.api.SignTransaction(ctx, args, methodSelector) |
||||
if res != nil { |
||||
l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(res.Raw), "error", e) |
||||
} else { |
||||
l.log.Info("SignTransaction", "type", "response", "data", res, "error", e) |
||||
} |
||||
return res, e |
||||
} |
||||
|
||||
func (l *AuditLogger) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) { |
||||
l.log.Info("Sign", "type", "request", "metadata", MetadataFromContext(ctx).String(), |
||||
"addr", addr.String(), "data", common.Bytes2Hex(data)) |
||||
b, e := l.api.Sign(ctx, addr, data) |
||||
l.log.Info("Sign", "type", "response", "data", common.Bytes2Hex(b), "error", e) |
||||
return b, e |
||||
} |
||||
|
||||
func (l *AuditLogger) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) { |
||||
l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(), |
||||
"data", common.Bytes2Hex(data)) |
||||
a, e := l.api.EcRecover(ctx, data, sig) |
||||
l.log.Info("EcRecover", "type", "response", "addr", a.String(), "error", e) |
||||
return a, e |
||||
} |
||||
|
||||
func (l *AuditLogger) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { |
||||
l.log.Info("Export", "type", "request", "metadata", MetadataFromContext(ctx).String(), |
||||
"addr", addr.Hex()) |
||||
j, e := l.api.Export(ctx, addr) |
||||
// In this case, we don't actually log the json-response, which may be extra sensitive
|
||||
l.log.Info("Export", "type", "response", "json response size", len(j), "error", e) |
||||
return j, e |
||||
} |
||||
|
||||
func (l *AuditLogger) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) { |
||||
// Don't actually log the json contents
|
||||
l.log.Info("Import", "type", "request", "metadata", MetadataFromContext(ctx).String(), |
||||
"keyJSON size", len(keyJSON)) |
||||
a, e := l.api.Import(ctx, keyJSON) |
||||
l.log.Info("Import", "type", "response", "addr", a.String(), "error", e) |
||||
return a, e |
||||
} |
||||
|
||||
func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) { |
||||
l := log.New("api", "signer") |
||||
handler, err := log.FileHandler(path, log.LogfmtFormat()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
l.SetHandler(handler) |
||||
l.Info("Configured", "audit log", path) |
||||
return &AuditLogger{l, api}, nil |
||||
} |
@ -0,0 +1,247 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
package core |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"os" |
||||
"strings" |
||||
|
||||
"sync" |
||||
|
||||
"github.com/davecgh/go-spew/spew" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"golang.org/x/crypto/ssh/terminal" |
||||
) |
||||
|
||||
type CommandlineUI struct { |
||||
in *bufio.Reader |
||||
mu sync.Mutex |
||||
} |
||||
|
||||
func NewCommandlineUI() *CommandlineUI { |
||||
return &CommandlineUI{in: bufio.NewReader(os.Stdin)} |
||||
} |
||||
|
||||
// readString reads a single line from stdin, trimming if from spaces, enforcing
|
||||
// non-emptyness.
|
||||
func (ui *CommandlineUI) readString() string { |
||||
for { |
||||
fmt.Printf("> ") |
||||
text, err := ui.in.ReadString('\n') |
||||
if err != nil { |
||||
log.Crit("Failed to read user input", "err", err) |
||||
} |
||||
if text = strings.TrimSpace(text); text != "" { |
||||
return text |
||||
} |
||||
} |
||||
} |
||||
|
||||
// readPassword reads a single line from stdin, trimming it from the trailing new
|
||||
// line and returns it. The input will not be echoed.
|
||||
func (ui *CommandlineUI) readPassword() string { |
||||
fmt.Printf("Enter password to approve:\n") |
||||
fmt.Printf("> ") |
||||
|
||||
text, err := terminal.ReadPassword(int(os.Stdin.Fd())) |
||||
if err != nil { |
||||
log.Crit("Failed to read password", "err", err) |
||||
} |
||||
fmt.Println() |
||||
fmt.Println("-----------------------") |
||||
return string(text) |
||||
} |
||||
|
||||
// readPassword reads a single line from stdin, trimming it from the trailing new
|
||||
// line and returns it. The input will not be echoed.
|
||||
func (ui *CommandlineUI) readPasswordText(inputstring string) string { |
||||
fmt.Printf("Enter %s:\n", inputstring) |
||||
fmt.Printf("> ") |
||||
text, err := terminal.ReadPassword(int(os.Stdin.Fd())) |
||||
if err != nil { |
||||
log.Crit("Failed to read password", "err", err) |
||||
} |
||||
fmt.Println("-----------------------") |
||||
return string(text) |
||||
} |
||||
|
||||
// confirm returns true if user enters 'Yes', otherwise false
|
||||
func (ui *CommandlineUI) confirm() bool { |
||||
fmt.Printf("Approve? [y/N]:\n") |
||||
if ui.readString() == "y" { |
||||
return true |
||||
} |
||||
fmt.Println("-----------------------") |
||||
return false |
||||
} |
||||
|
||||
func showMetadata(metadata Metadata) { |
||||
fmt.Printf("Request context:\n\t%v -> %v -> %v\n", metadata.Remote, metadata.Scheme, metadata.Local) |
||||
} |
||||
|
||||
// ApproveTx prompt the user for confirmation to request to sign Transaction
|
||||
func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { |
||||
ui.mu.Lock() |
||||
defer ui.mu.Unlock() |
||||
weival := request.Transaction.Value.ToInt() |
||||
fmt.Printf("--------- Transaction request-------------\n") |
||||
if to := request.Transaction.To; to != nil { |
||||
fmt.Printf("to: %v\n", to.Original()) |
||||
if !to.ValidChecksum() { |
||||
fmt.Printf("\nWARNING: Invalid checksum on to-address!\n\n") |
||||
} |
||||
} else { |
||||
fmt.Printf("to: <contact creation>\n") |
||||
} |
||||
fmt.Printf("from: %v\n", request.Transaction.From.String()) |
||||
fmt.Printf("value: %v wei\n", weival) |
||||
if request.Transaction.Data != nil { |
||||
d := *request.Transaction.Data |
||||
if len(d) > 0 { |
||||
fmt.Printf("data: %v\n", common.Bytes2Hex(d)) |
||||
} |
||||
} |
||||
if request.Callinfo != nil { |
||||
fmt.Printf("\nTransaction validation:\n") |
||||
for _, m := range request.Callinfo { |
||||
fmt.Printf(" * %s : %s", m.Typ, m.Message) |
||||
} |
||||
fmt.Println() |
||||
|
||||
} |
||||
fmt.Printf("\n") |
||||
showMetadata(request.Meta) |
||||
fmt.Printf("-------------------------------------------\n") |
||||
if !ui.confirm() { |
||||
return SignTxResponse{request.Transaction, false, ""}, nil |
||||
} |
||||
return SignTxResponse{request.Transaction, true, ui.readPassword()}, nil |
||||
} |
||||
|
||||
// ApproveSignData prompt the user for confirmation to request to sign data
|
||||
func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { |
||||
ui.mu.Lock() |
||||
defer ui.mu.Unlock() |
||||
|
||||
fmt.Printf("-------- Sign data request--------------\n") |
||||
fmt.Printf("Account: %s\n", request.Address.String()) |
||||
fmt.Printf("message: \n%q\n", request.Message) |
||||
fmt.Printf("raw data: \n%v\n", request.Rawdata) |
||||
fmt.Printf("message hash: %v\n", request.Hash) |
||||
fmt.Printf("-------------------------------------------\n") |
||||
showMetadata(request.Meta) |
||||
if !ui.confirm() { |
||||
return SignDataResponse{false, ""}, nil |
||||
} |
||||
return SignDataResponse{true, ui.readPassword()}, nil |
||||
} |
||||
|
||||
// ApproveExport prompt the user for confirmation to export encrypted Account json
|
||||
func (ui *CommandlineUI) ApproveExport(request *ExportRequest) (ExportResponse, error) { |
||||
ui.mu.Lock() |
||||
defer ui.mu.Unlock() |
||||
|
||||
fmt.Printf("-------- Export Account request--------------\n") |
||||
fmt.Printf("A request has been made to export the (encrypted) keyfile\n") |
||||
fmt.Printf("Approving this operation means that the caller obtains the (encrypted) contents\n") |
||||
fmt.Printf("\n") |
||||
fmt.Printf("Account: %x\n", request.Address) |
||||
//fmt.Printf("keyfile: \n%v\n", request.file)
|
||||
fmt.Printf("-------------------------------------------\n") |
||||
showMetadata(request.Meta) |
||||
return ExportResponse{ui.confirm()}, nil |
||||
} |
||||
|
||||
// ApproveImport prompt the user for confirmation to import Account json
|
||||
func (ui *CommandlineUI) ApproveImport(request *ImportRequest) (ImportResponse, error) { |
||||
ui.mu.Lock() |
||||
defer ui.mu.Unlock() |
||||
|
||||
fmt.Printf("-------- Import Account request--------------\n") |
||||
fmt.Printf("A request has been made to import an encrypted keyfile\n") |
||||
fmt.Printf("-------------------------------------------\n") |
||||
showMetadata(request.Meta) |
||||
if !ui.confirm() { |
||||
return ImportResponse{false, "", ""}, nil |
||||
} |
||||
return ImportResponse{true, ui.readPasswordText("Old password"), ui.readPasswordText("New password")}, nil |
||||
} |
||||
|
||||
// ApproveListing prompt the user for confirmation to list accounts
|
||||
// the list of accounts to list can be modified by the UI
|
||||
func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) { |
||||
|
||||
ui.mu.Lock() |
||||
defer ui.mu.Unlock() |
||||
|
||||
fmt.Printf("-------- List Account request--------------\n") |
||||
fmt.Printf("A request has been made to list all accounts. \n") |
||||
fmt.Printf("You can select which accounts the caller can see\n") |
||||
for _, account := range request.Accounts { |
||||
fmt.Printf("\t[x] %v\n", account.Address.Hex()) |
||||
} |
||||
fmt.Printf("-------------------------------------------\n") |
||||
showMetadata(request.Meta) |
||||
if !ui.confirm() { |
||||
return ListResponse{nil}, nil |
||||
} |
||||
return ListResponse{request.Accounts}, nil |
||||
} |
||||
|
||||
// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
|
||||
func (ui *CommandlineUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { |
||||
|
||||
ui.mu.Lock() |
||||
defer ui.mu.Unlock() |
||||
|
||||
fmt.Printf("-------- New Account request--------------\n") |
||||
fmt.Printf("A request has been made to create a new. \n") |
||||
fmt.Printf("Approving this operation means that a new Account is created,\n") |
||||
fmt.Printf("and the address show to the caller\n") |
||||
showMetadata(request.Meta) |
||||
if !ui.confirm() { |
||||
return NewAccountResponse{false, ""}, nil |
||||
} |
||||
return NewAccountResponse{true, ui.readPassword()}, nil |
||||
} |
||||
|
||||
// ShowError displays error message to user
|
||||
func (ui *CommandlineUI) ShowError(message string) { |
||||
|
||||
fmt.Printf("ERROR: %v\n", message) |
||||
} |
||||
|
||||
// ShowInfo displays info message to user
|
||||
func (ui *CommandlineUI) ShowInfo(message string) { |
||||
fmt.Printf("Info: %v\n", message) |
||||
} |
||||
|
||||
func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) { |
||||
fmt.Printf("Transaction signed:\n ") |
||||
spew.Dump(tx.Tx) |
||||
} |
||||
|
||||
func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) { |
||||
|
||||
fmt.Printf("------- Signer info -------\n") |
||||
for k, v := range info.Info { |
||||
fmt.Printf("* %v : %v\n", k, v) |
||||
} |
||||
} |
@ -0,0 +1,113 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"sync" |
||||
|
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/rpc" |
||||
) |
||||
|
||||
type StdIOUI struct { |
||||
client rpc.Client |
||||
mu sync.Mutex |
||||
} |
||||
|
||||
func NewStdIOUI() *StdIOUI { |
||||
log.Info("NewStdIOUI") |
||||
client, err := rpc.DialContext(context.Background(), "stdio://") |
||||
if err != nil { |
||||
log.Crit("Could not create stdio client", "err", err) |
||||
} |
||||
return &StdIOUI{client: *client} |
||||
} |
||||
|
||||
// dispatch sends a request over the stdio
|
||||
func (ui *StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error { |
||||
err := ui.client.Call(&reply, serviceMethod, args) |
||||
if err != nil { |
||||
log.Info("Error", "exc", err.Error()) |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func (ui *StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { |
||||
var result SignTxResponse |
||||
err := ui.dispatch("ApproveTx", request, &result) |
||||
return result, err |
||||
} |
||||
|
||||
func (ui *StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { |
||||
var result SignDataResponse |
||||
err := ui.dispatch("ApproveSignData", request, &result) |
||||
return result, err |
||||
} |
||||
|
||||
func (ui *StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error) { |
||||
var result ExportResponse |
||||
err := ui.dispatch("ApproveExport", request, &result) |
||||
return result, err |
||||
} |
||||
|
||||
func (ui *StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error) { |
||||
var result ImportResponse |
||||
err := ui.dispatch("ApproveImport", request, &result) |
||||
return result, err |
||||
} |
||||
|
||||
func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) { |
||||
var result ListResponse |
||||
err := ui.dispatch("ApproveListing", request, &result) |
||||
return result, err |
||||
} |
||||
|
||||
func (ui *StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { |
||||
var result NewAccountResponse |
||||
err := ui.dispatch("ApproveNewAccount", request, &result) |
||||
return result, err |
||||
} |
||||
|
||||
func (ui *StdIOUI) ShowError(message string) { |
||||
err := ui.dispatch("ShowError", &Message{message}, nil) |
||||
if err != nil { |
||||
log.Info("Error calling 'ShowError'", "exc", err.Error(), "msg", message) |
||||
} |
||||
} |
||||
|
||||
func (ui *StdIOUI) ShowInfo(message string) { |
||||
err := ui.dispatch("ShowInfo", Message{message}, nil) |
||||
if err != nil { |
||||
log.Info("Error calling 'ShowInfo'", "exc", err.Error(), "msg", message) |
||||
} |
||||
} |
||||
func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) { |
||||
err := ui.dispatch("OnApprovedTx", tx, nil) |
||||
if err != nil { |
||||
log.Info("Error calling 'OnApprovedTx'", "exc", err.Error(), "tx", tx) |
||||
} |
||||
} |
||||
|
||||
func (ui *StdIOUI) OnSignerStartup(info StartupInfo) { |
||||
err := ui.dispatch("OnSignerStartup", info, nil) |
||||
if err != nil { |
||||
log.Info("Error calling 'OnSignerStartup'", "exc", err.Error(), "info", info) |
||||
} |
||||
} |
@ -0,0 +1,95 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"strings" |
||||
|
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
) |
||||
|
||||
type Accounts []Account |
||||
|
||||
func (as Accounts) String() string { |
||||
var output []string |
||||
for _, a := range as { |
||||
output = append(output, a.String()) |
||||
} |
||||
return strings.Join(output, "\n") |
||||
} |
||||
|
||||
type Account struct { |
||||
Typ string `json:"type"` |
||||
URL accounts.URL `json:"url"` |
||||
Address common.Address `json:"address"` |
||||
} |
||||
|
||||
func (a Account) String() string { |
||||
s, err := json.Marshal(a) |
||||
if err == nil { |
||||
return string(s) |
||||
} |
||||
return err.Error() |
||||
} |
||||
|
||||
type ValidationInfo struct { |
||||
Typ string `json:"type"` |
||||
Message string `json:"message"` |
||||
} |
||||
type ValidationMessages struct { |
||||
Messages []ValidationInfo |
||||
} |
||||
|
||||
// SendTxArgs represents the arguments to submit a transaction
|
||||
type SendTxArgs struct { |
||||
From common.MixedcaseAddress `json:"from"` |
||||
To *common.MixedcaseAddress `json:"to"` |
||||
Gas hexutil.Uint64 `json:"gas"` |
||||
GasPrice hexutil.Big `json:"gasPrice"` |
||||
Value hexutil.Big `json:"value"` |
||||
Nonce hexutil.Uint64 `json:"nonce"` |
||||
// We accept "data" and "input" for backwards-compatibility reasons.
|
||||
Data *hexutil.Bytes `json:"data"` |
||||
Input *hexutil.Bytes `json:"input"` |
||||
} |
||||
|
||||
func (t SendTxArgs) String() string { |
||||
s, err := json.Marshal(t) |
||||
if err == nil { |
||||
return string(s) |
||||
} |
||||
return err.Error() |
||||
} |
||||
|
||||
func (args *SendTxArgs) toTransaction() *types.Transaction { |
||||
var input []byte |
||||
if args.Data != nil { |
||||
input = *args.Data |
||||
} else if args.Input != nil { |
||||
input = *args.Input |
||||
} |
||||
if args.To == nil { |
||||
return types.NewContractCreation(uint64(args.Nonce), (*big.Int)(&args.Value), uint64(args.Gas), (*big.Int)(&args.GasPrice), input) |
||||
} |
||||
return types.NewTransaction(uint64(args.Nonce), args.To.Address(), (*big.Int)(&args.Value), (uint64)(args.Gas), (*big.Int)(&args.GasPrice), input) |
||||
} |
@ -0,0 +1,163 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
// The validation package contains validation checks for transactions
|
||||
// - ABI-data validation
|
||||
// - Transaction semantics validation
|
||||
// The package provides warnings for typical pitfalls
|
||||
|
||||
func (vs *ValidationMessages) crit(msg string) { |
||||
vs.Messages = append(vs.Messages, ValidationInfo{"CRITICAL", msg}) |
||||
} |
||||
func (vs *ValidationMessages) warn(msg string) { |
||||
vs.Messages = append(vs.Messages, ValidationInfo{"WARNING", msg}) |
||||
} |
||||
func (vs *ValidationMessages) info(msg string) { |
||||
vs.Messages = append(vs.Messages, ValidationInfo{"Info", msg}) |
||||
} |
||||
|
||||
type Validator struct { |
||||
db *AbiDb |
||||
} |
||||
|
||||
func NewValidator(db *AbiDb) *Validator { |
||||
return &Validator{db} |
||||
} |
||||
func testSelector(selector string, data []byte) (*decodedCallData, error) { |
||||
if selector == "" { |
||||
return nil, fmt.Errorf("selector not found") |
||||
} |
||||
abiData, err := MethodSelectorToAbi(selector) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
info, err := parseCallData(data, string(abiData)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return info, nil |
||||
|
||||
} |
||||
|
||||
// validateCallData checks if the ABI-data + methodselector (if given) can be parsed and seems to match
|
||||
func (v *Validator) validateCallData(msgs *ValidationMessages, data []byte, methodSelector *string) { |
||||
if len(data) == 0 { |
||||
return |
||||
} |
||||
if len(data) < 4 { |
||||
msgs.warn("Tx contains data which is not valid ABI") |
||||
return |
||||
} |
||||
var ( |
||||
info *decodedCallData |
||||
err error |
||||
) |
||||
// Check the provided one
|
||||
if methodSelector != nil { |
||||
info, err = testSelector(*methodSelector, data) |
||||
if err != nil { |
||||
msgs.warn(fmt.Sprintf("Tx contains data, but provided ABI signature could not be matched: %v", err)) |
||||
} else { |
||||
msgs.info(info.String()) |
||||
//Successfull match. add to db if not there already (ignore errors there)
|
||||
v.db.AddSignature(*methodSelector, data[:4]) |
||||
} |
||||
return |
||||
} |
||||
// Check the db
|
||||
selector, err := v.db.LookupMethodSelector(data[:4]) |
||||
if err != nil { |
||||
msgs.warn(fmt.Sprintf("Tx contains data, but the ABI signature could not be found: %v", err)) |
||||
return |
||||
} |
||||
info, err = testSelector(selector, data) |
||||
if err != nil { |
||||
msgs.warn(fmt.Sprintf("Tx contains data, but provided ABI signature could not be matched: %v", err)) |
||||
} else { |
||||
msgs.info(info.String()) |
||||
} |
||||
} |
||||
|
||||
// validateSemantics checks if the transactions 'makes sense', and generate warnings for a couple of typical scenarios
|
||||
func (v *Validator) validate(msgs *ValidationMessages, txargs *SendTxArgs, methodSelector *string) error { |
||||
// Prevent accidental erroneous usage of both 'input' and 'data'
|
||||
if txargs.Data != nil && txargs.Input != nil && !bytes.Equal(*txargs.Data, *txargs.Input) { |
||||
// This is a showstopper
|
||||
return errors.New(`Ambiguous request: both "data" and "input" are set and are not identical`) |
||||
} |
||||
var ( |
||||
data []byte |
||||
) |
||||
// Place data on 'data', and nil 'input'
|
||||
if txargs.Input != nil { |
||||
txargs.Data = txargs.Input |
||||
txargs.Input = nil |
||||
} |
||||
if txargs.Data != nil { |
||||
data = *txargs.Data |
||||
} |
||||
|
||||
if txargs.To == nil { |
||||
//Contract creation should contain sufficient data to deploy a contract
|
||||
// A typical error is omitting sender due to some quirk in the javascript call
|
||||
// e.g. https://github.com/ethereum/go-ethereum/issues/16106
|
||||
if len(data) == 0 { |
||||
if txargs.Value.ToInt().Cmp(big.NewInt(0)) > 0 { |
||||
// Sending ether into black hole
|
||||
return errors.New(`Tx will create contract with value but empty code!`) |
||||
} |
||||
// No value submitted at least
|
||||
msgs.crit("Tx will create contract with empty code!") |
||||
} else if len(data) < 40 { //Arbitrary limit
|
||||
msgs.warn(fmt.Sprintf("Tx will will create contract, but payload is suspiciously small (%d b)", len(data))) |
||||
} |
||||
// methodSelector should be nil for contract creation
|
||||
if methodSelector != nil { |
||||
msgs.warn("Tx will create contract, but method selector supplied; indicating intent to call a method.") |
||||
} |
||||
|
||||
} else { |
||||
if !txargs.To.ValidChecksum() { |
||||
msgs.warn("Invalid checksum on to-address") |
||||
} |
||||
// Normal transaction
|
||||
if bytes.Equal(txargs.To.Address().Bytes(), common.Address{}.Bytes()) { |
||||
// Sending to 0
|
||||
msgs.crit("Tx destination is the zero address!") |
||||
} |
||||
// Validate calldata
|
||||
v.validateCallData(msgs, data, methodSelector) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ValidateTransaction does a number of checks on the supplied transaction, and returns either a list of warnings,
|
||||
// or an error, indicating that the transaction should be immediately rejected
|
||||
func (v *Validator) ValidateTransaction(txArgs *SendTxArgs, methodSelector *string) (*ValidationMessages, error) { |
||||
msgs := &ValidationMessages{} |
||||
return msgs, v.validate(msgs, txArgs, methodSelector) |
||||
} |
@ -0,0 +1,139 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math/big" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
) |
||||
|
||||
func hexAddr(a string) common.Address { return common.BytesToAddress(common.FromHex(a)) } |
||||
func mixAddr(a string) (*common.MixedcaseAddress, error) { |
||||
return common.NewMixedcaseAddressFromString(a) |
||||
} |
||||
func toHexBig(h string) hexutil.Big { |
||||
b := big.NewInt(0).SetBytes(common.FromHex(h)) |
||||
return hexutil.Big(*b) |
||||
} |
||||
func toHexUint(h string) hexutil.Uint64 { |
||||
b := big.NewInt(0).SetBytes(common.FromHex(h)) |
||||
return hexutil.Uint64(b.Uint64()) |
||||
} |
||||
func dummyTxArgs(t txtestcase) *SendTxArgs { |
||||
to, _ := mixAddr(t.to) |
||||
from, _ := mixAddr(t.from) |
||||
n := toHexUint(t.n) |
||||
gas := toHexUint(t.g) |
||||
gasPrice := toHexBig(t.gp) |
||||
value := toHexBig(t.value) |
||||
var ( |
||||
data, input *hexutil.Bytes |
||||
) |
||||
if t.d != "" { |
||||
a := hexutil.Bytes(common.FromHex(t.d)) |
||||
data = &a |
||||
} |
||||
if t.i != "" { |
||||
a := hexutil.Bytes(common.FromHex(t.i)) |
||||
input = &a |
||||
|
||||
} |
||||
return &SendTxArgs{ |
||||
From: *from, |
||||
To: to, |
||||
Value: value, |
||||
Nonce: n, |
||||
GasPrice: gasPrice, |
||||
Gas: gas, |
||||
Data: data, |
||||
Input: input, |
||||
} |
||||
} |
||||
|
||||
type txtestcase struct { |
||||
from, to, n, g, gp, value, d, i string |
||||
expectErr bool |
||||
numMessages int |
||||
} |
||||
|
||||
func TestValidator(t *testing.T) { |
||||
var ( |
||||
// use empty db, there are other tests for the abi-specific stuff
|
||||
db, _ = NewEmptyAbiDB() |
||||
v = NewValidator(db) |
||||
) |
||||
testcases := []txtestcase{ |
||||
// Invalid to checksum
|
||||
{from: "000000000000000000000000000000000000dead", to: "000000000000000000000000000000000000dead", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1}, |
||||
// valid 0x000000000000000000000000000000000000dEaD
|
||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 0}, |
||||
// conflicting input and data
|
||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", i: "0x02", expectErr: true}, |
||||
// Data can't be parsed
|
||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x0102", numMessages: 1}, |
||||
// Data (on Input) can't be parsed
|
||||
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", i: "0x0102", numMessages: 1}, |
||||
// Send to 0
|
||||
{from: "000000000000000000000000000000000000dead", to: "0x0000000000000000000000000000000000000000", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1}, |
||||
// Create empty contract (no value)
|
||||
{from: "000000000000000000000000000000000000dead", to: "", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x00", numMessages: 1}, |
||||
// Create empty contract (with value)
|
||||
{from: "000000000000000000000000000000000000dead", to: "", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", expectErr: true}, |
||||
// Small payload for create
|
||||
{from: "000000000000000000000000000000000000dead", to: "", |
||||
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", numMessages: 1}, |
||||
} |
||||
for i, test := range testcases { |
||||
msgs, err := v.ValidateTransaction(dummyTxArgs(test), nil) |
||||
if err == nil && test.expectErr { |
||||
t.Errorf("Test %d, expected error", i) |
||||
for _, msg := range msgs.Messages { |
||||
fmt.Printf("* %s: %s\n", msg.Typ, msg.Message) |
||||
} |
||||
} |
||||
if err != nil && !test.expectErr { |
||||
t.Errorf("Test %d, unexpected error: %v", i, err) |
||||
} |
||||
if err == nil { |
||||
got := len(msgs.Messages) |
||||
if got != test.numMessages { |
||||
for _, msg := range msgs.Messages { |
||||
fmt.Printf("* %s: %s\n", msg.Typ, msg.Message) |
||||
} |
||||
t.Errorf("Test %d, expected %d messages, got %d", i, test.numMessages, got) |
||||
} else { |
||||
//Debug printout, remove later
|
||||
for _, msg := range msgs.Messages { |
||||
fmt.Printf("* [%d] %s: %s\n", i, msg.Typ, msg.Message) |
||||
} |
||||
fmt.Println() |
||||
} |
||||
} |
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,21 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package deps contains the console JavaScript dependencies Go embedded.
|
||||
package deps |
||||
|
||||
//go:generate go-bindata -nometadata -pkg deps -o bindata.go bignumber.js
|
||||
//go:generate gofmt -w -s bindata.go
|
@ -0,0 +1,248 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rules |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"os" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/signer/core" |
||||
"github.com/ethereum/go-ethereum/signer/rules/deps" |
||||
"github.com/ethereum/go-ethereum/signer/storage" |
||||
"github.com/robertkrimen/otto" |
||||
) |
||||
|
||||
var ( |
||||
BigNumber_JS = deps.MustAsset("bignumber.js") |
||||
) |
||||
|
||||
// consoleOutput is an override for the console.log and console.error methods to
|
||||
// stream the output into the configured output stream instead of stdout.
|
||||
func consoleOutput(call otto.FunctionCall) otto.Value { |
||||
output := []string{"JS:> "} |
||||
for _, argument := range call.ArgumentList { |
||||
output = append(output, fmt.Sprintf("%v", argument)) |
||||
} |
||||
fmt.Fprintln(os.Stdout, strings.Join(output, " ")) |
||||
return otto.Value{} |
||||
} |
||||
|
||||
// rulesetUi provides an implementation of SignerUI that evaluates a javascript
|
||||
// file for each defined UI-method
|
||||
type rulesetUi struct { |
||||
next core.SignerUI // The next handler, for manual processing
|
||||
storage storage.Storage |
||||
credentials storage.Storage |
||||
jsRules string // The rules to use
|
||||
} |
||||
|
||||
func NewRuleEvaluator(next core.SignerUI, jsbackend, credentialsBackend storage.Storage) (*rulesetUi, error) { |
||||
c := &rulesetUi{ |
||||
next: next, |
||||
storage: jsbackend, |
||||
credentials: credentialsBackend, |
||||
jsRules: "", |
||||
} |
||||
|
||||
return c, nil |
||||
} |
||||
|
||||
func (r *rulesetUi) Init(javascriptRules string) error { |
||||
r.jsRules = javascriptRules |
||||
return nil |
||||
} |
||||
func (r *rulesetUi) execute(jsfunc string, jsarg interface{}) (otto.Value, error) { |
||||
|
||||
// Instantiate a fresh vm engine every time
|
||||
vm := otto.New() |
||||
// Set the native callbacks
|
||||
consoleObj, _ := vm.Get("console") |
||||
consoleObj.Object().Set("log", consoleOutput) |
||||
consoleObj.Object().Set("error", consoleOutput) |
||||
vm.Set("storage", r.storage) |
||||
|
||||
// Load bootstrap libraries
|
||||
script, err := vm.Compile("bignumber.js", BigNumber_JS) |
||||
if err != nil { |
||||
log.Warn("Failed loading libraries", "err", err) |
||||
return otto.UndefinedValue(), err |
||||
} |
||||
vm.Run(script) |
||||
|
||||
// Run the actual rule implementation
|
||||
_, err = vm.Run(r.jsRules) |
||||
if err != nil { |
||||
log.Warn("Execution failed", "err", err) |
||||
return otto.UndefinedValue(), err |
||||
} |
||||
|
||||
// And the actual call
|
||||
// All calls are objects with the parameters being keys in that object.
|
||||
// To provide additional insulation between js and go, we serialize it into JSON on the Go-side,
|
||||
// and deserialize it on the JS side.
|
||||
|
||||
jsonbytes, err := json.Marshal(jsarg) |
||||
if err != nil { |
||||
log.Warn("failed marshalling data", "data", jsarg) |
||||
return otto.UndefinedValue(), err |
||||
} |
||||
// Now, we call foobar(JSON.parse(<jsondata>)).
|
||||
var call string |
||||
if len(jsonbytes) > 0 { |
||||
call = fmt.Sprintf("%v(JSON.parse(%v))", jsfunc, string(jsonbytes)) |
||||
} else { |
||||
call = fmt.Sprintf("%v()", jsfunc) |
||||
} |
||||
return vm.Run(call) |
||||
} |
||||
|
||||
func (r *rulesetUi) checkApproval(jsfunc string, jsarg []byte, err error) (bool, error) { |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
v, err := r.execute(jsfunc, string(jsarg)) |
||||
if err != nil { |
||||
log.Info("error occurred during execution", "error", err) |
||||
return false, err |
||||
} |
||||
result, err := v.ToString() |
||||
if err != nil { |
||||
log.Info("error occurred during response unmarshalling", "error", err) |
||||
return false, err |
||||
} |
||||
if result == "Approve" { |
||||
log.Info("Op approved") |
||||
return true, nil |
||||
} else if result == "Reject" { |
||||
log.Info("Op rejected") |
||||
return false, nil |
||||
} |
||||
return false, fmt.Errorf("Unknown response") |
||||
} |
||||
|
||||
func (r *rulesetUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { |
||||
jsonreq, err := json.Marshal(request) |
||||
approved, err := r.checkApproval("ApproveTx", jsonreq, err) |
||||
if err != nil { |
||||
log.Info("Rule-based approval error, going to manual", "error", err) |
||||
return r.next.ApproveTx(request) |
||||
} |
||||
|
||||
if approved { |
||||
return core.SignTxResponse{ |
||||
Transaction: request.Transaction, |
||||
Approved: true, |
||||
Password: r.lookupPassword(request.Transaction.From.Address()), |
||||
}, |
||||
nil |
||||
} |
||||
return core.SignTxResponse{Approved: false}, err |
||||
} |
||||
|
||||
func (r *rulesetUi) lookupPassword(address common.Address) string { |
||||
return r.credentials.Get(strings.ToLower(address.String())) |
||||
} |
||||
|
||||
func (r *rulesetUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { |
||||
jsonreq, err := json.Marshal(request) |
||||
approved, err := r.checkApproval("ApproveSignData", jsonreq, err) |
||||
if err != nil { |
||||
log.Info("Rule-based approval error, going to manual", "error", err) |
||||
return r.next.ApproveSignData(request) |
||||
} |
||||
if approved { |
||||
return core.SignDataResponse{Approved: true, Password: r.lookupPassword(request.Address.Address())}, nil |
||||
} |
||||
return core.SignDataResponse{Approved: false, Password: ""}, err |
||||
} |
||||
|
||||
func (r *rulesetUi) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) { |
||||
jsonreq, err := json.Marshal(request) |
||||
approved, err := r.checkApproval("ApproveExport", jsonreq, err) |
||||
if err != nil { |
||||
log.Info("Rule-based approval error, going to manual", "error", err) |
||||
return r.next.ApproveExport(request) |
||||
} |
||||
if approved { |
||||
return core.ExportResponse{Approved: true}, nil |
||||
} |
||||
return core.ExportResponse{Approved: false}, err |
||||
} |
||||
|
||||
func (r *rulesetUi) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) { |
||||
// This cannot be handled by rules, requires setting a password
|
||||
// dispatch to next
|
||||
return r.next.ApproveImport(request) |
||||
} |
||||
|
||||
func (r *rulesetUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { |
||||
jsonreq, err := json.Marshal(request) |
||||
approved, err := r.checkApproval("ApproveListing", jsonreq, err) |
||||
if err != nil { |
||||
log.Info("Rule-based approval error, going to manual", "error", err) |
||||
return r.next.ApproveListing(request) |
||||
} |
||||
if approved { |
||||
return core.ListResponse{Accounts: request.Accounts}, nil |
||||
} |
||||
return core.ListResponse{}, err |
||||
} |
||||
|
||||
func (r *rulesetUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { |
||||
// This cannot be handled by rules, requires setting a password
|
||||
// dispatch to next
|
||||
return r.next.ApproveNewAccount(request) |
||||
} |
||||
|
||||
func (r *rulesetUi) ShowError(message string) { |
||||
log.Error(message) |
||||
r.next.ShowError(message) |
||||
} |
||||
|
||||
func (r *rulesetUi) ShowInfo(message string) { |
||||
log.Info(message) |
||||
r.next.ShowInfo(message) |
||||
} |
||||
func (r *rulesetUi) OnSignerStartup(info core.StartupInfo) { |
||||
jsonInfo, err := json.Marshal(info) |
||||
if err != nil { |
||||
log.Warn("failed marshalling data", "data", info) |
||||
return |
||||
} |
||||
r.next.OnSignerStartup(info) |
||||
_, err = r.execute("OnSignerStartup", string(jsonInfo)) |
||||
if err != nil { |
||||
log.Info("error occurred during execution", "error", err) |
||||
} |
||||
} |
||||
|
||||
func (r *rulesetUi) OnApprovedTx(tx ethapi.SignTransactionResult) { |
||||
jsonTx, err := json.Marshal(tx) |
||||
if err != nil { |
||||
log.Warn("failed marshalling transaction", "tx", tx) |
||||
return |
||||
} |
||||
_, err = r.execute("OnApprovedTx", string(jsonTx)) |
||||
if err != nil { |
||||
log.Info("error occurred during execution", "error", err) |
||||
} |
||||
} |
@ -0,0 +1,631 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
package rules |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math/big" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/signer/core" |
||||
"github.com/ethereum/go-ethereum/signer/storage" |
||||
) |
||||
|
||||
const JS = ` |
||||
/** |
||||
This is an example implementation of a Javascript rule file.
|
||||
|
||||
When the signer receives a request over the external API, the corresponding method is evaluated.
|
||||
Three things can happen:
|
||||
|
||||
1. The method returns "Approve". This means the operation is permitted.
|
||||
2. The method returns "Reject". This means the operation is rejected.
|
||||
3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means |
||||
that the operation will continue to manual processing, via the regular UI method chosen by the user.
|
||||
|
||||
[*] Note: Future version of the ruleset may use more complex json-based returnvalues, making it possible to not
|
||||
only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
|
||||
accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject"). |
||||
|
||||
**/ |
||||
|
||||
function ApproveListing(request){ |
||||
console.log("In js approve listing"); |
||||
console.log(request.accounts[3].Address) |
||||
console.log(request.meta.Remote) |
||||
return "Approve" |
||||
} |
||||
|
||||
function ApproveTx(request){ |
||||
console.log("test"); |
||||
console.log("from"); |
||||
return "Reject"; |
||||
} |
||||
|
||||
function test(thing){ |
||||
console.log(thing.String()) |
||||
} |
||||
|
||||
` |
||||
|
||||
func mixAddr(a string) (*common.MixedcaseAddress, error) { |
||||
return common.NewMixedcaseAddressFromString(a) |
||||
} |
||||
|
||||
type alwaysDenyUi struct{} |
||||
|
||||
func (alwaysDenyUi) OnSignerStartup(info core.StartupInfo) { |
||||
} |
||||
|
||||
func (alwaysDenyUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { |
||||
return core.SignTxResponse{Transaction: request.Transaction, Approved: false, Password: ""}, nil |
||||
} |
||||
|
||||
func (alwaysDenyUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { |
||||
return core.SignDataResponse{Approved: false, Password: ""}, nil |
||||
} |
||||
|
||||
func (alwaysDenyUi) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) { |
||||
return core.ExportResponse{Approved: false}, nil |
||||
} |
||||
|
||||
func (alwaysDenyUi) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) { |
||||
return core.ImportResponse{Approved: false, OldPassword: "", NewPassword: ""}, nil |
||||
} |
||||
|
||||
func (alwaysDenyUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { |
||||
return core.ListResponse{Accounts: nil}, nil |
||||
} |
||||
|
||||
func (alwaysDenyUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { |
||||
return core.NewAccountResponse{Approved: false, Password: ""}, nil |
||||
} |
||||
|
||||
func (alwaysDenyUi) ShowError(message string) { |
||||
panic("implement me") |
||||
} |
||||
|
||||
func (alwaysDenyUi) ShowInfo(message string) { |
||||
panic("implement me") |
||||
} |
||||
|
||||
func (alwaysDenyUi) OnApprovedTx(tx ethapi.SignTransactionResult) { |
||||
panic("implement me") |
||||
} |
||||
|
||||
func initRuleEngine(js string) (*rulesetUi, error) { |
||||
r, err := NewRuleEvaluator(&alwaysDenyUi{}, storage.NewEphemeralStorage(), storage.NewEphemeralStorage()) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to create js engine: %v", err) |
||||
} |
||||
if err = r.Init(js); err != nil { |
||||
return nil, fmt.Errorf("failed to load bootstrap js: %v", err) |
||||
} |
||||
return r, nil |
||||
} |
||||
|
||||
func TestListRequest(t *testing.T) { |
||||
accs := make([]core.Account, 5) |
||||
|
||||
for i := range accs { |
||||
addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i) |
||||
acc := core.Account{ |
||||
Address: common.BytesToAddress(common.Hex2Bytes(addr)), |
||||
URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)}, |
||||
} |
||||
accs[i] = acc |
||||
} |
||||
|
||||
js := `function ApproveListing(){ return "Approve" }` |
||||
|
||||
r, err := initRuleEngine(js) |
||||
if err != nil { |
||||
t.Errorf("Couldn't create evaluator %v", err) |
||||
return |
||||
} |
||||
resp, err := r.ApproveListing(&core.ListRequest{ |
||||
Accounts: accs, |
||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, |
||||
}) |
||||
if len(resp.Accounts) != len(accs) { |
||||
t.Errorf("Expected check to resolve to 'Approve'") |
||||
} |
||||
} |
||||
|
||||
func TestSignTxRequest(t *testing.T) { |
||||
|
||||
js := ` |
||||
function ApproveTx(r){ |
||||
console.log("transaction.from", r.transaction.from); |
||||
console.log("transaction.to", r.transaction.to); |
||||
console.log("transaction.value", r.transaction.value); |
||||
console.log("transaction.nonce", r.transaction.nonce); |
||||
if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"} |
||||
if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"} |
||||
}` |
||||
|
||||
r, err := initRuleEngine(js) |
||||
if err != nil { |
||||
t.Errorf("Couldn't create evaluator %v", err) |
||||
return |
||||
} |
||||
to, err := mixAddr("000000000000000000000000000000000000dead") |
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
from, err := mixAddr("0000000000000000000000000000000000001337") |
||||
|
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
fmt.Printf("to %v", to.Address().String()) |
||||
resp, err := r.ApproveTx(&core.SignTxRequest{ |
||||
Transaction: core.SendTxArgs{ |
||||
From: *from, |
||||
To: to}, |
||||
Callinfo: nil, |
||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, |
||||
}) |
||||
if err != nil { |
||||
t.Errorf("Unexpected error %v", err) |
||||
} |
||||
if !resp.Approved { |
||||
t.Errorf("Expected check to resolve to 'Approve'") |
||||
} |
||||
} |
||||
|
||||
type dummyUi struct { |
||||
calls []string |
||||
} |
||||
|
||||
func (d *dummyUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { |
||||
d.calls = append(d.calls, "ApproveTx") |
||||
return core.SignTxResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dummyUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { |
||||
d.calls = append(d.calls, "ApproveSignData") |
||||
return core.SignDataResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dummyUi) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) { |
||||
d.calls = append(d.calls, "ApproveExport") |
||||
return core.ExportResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dummyUi) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) { |
||||
d.calls = append(d.calls, "ApproveImport") |
||||
return core.ImportResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dummyUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { |
||||
d.calls = append(d.calls, "ApproveListing") |
||||
return core.ListResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dummyUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { |
||||
d.calls = append(d.calls, "ApproveNewAccount") |
||||
return core.NewAccountResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dummyUi) ShowError(message string) { |
||||
d.calls = append(d.calls, "ShowError") |
||||
} |
||||
|
||||
func (d *dummyUi) ShowInfo(message string) { |
||||
d.calls = append(d.calls, "ShowInfo") |
||||
} |
||||
|
||||
func (d *dummyUi) OnApprovedTx(tx ethapi.SignTransactionResult) { |
||||
d.calls = append(d.calls, "OnApprovedTx") |
||||
} |
||||
func (d *dummyUi) OnSignerStartup(info core.StartupInfo) { |
||||
} |
||||
|
||||
//TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
|
||||
func TestForwarding(t *testing.T) { |
||||
|
||||
js := "" |
||||
ui := &dummyUi{make([]string, 0)} |
||||
jsBackend := storage.NewEphemeralStorage() |
||||
credBackend := storage.NewEphemeralStorage() |
||||
r, err := NewRuleEvaluator(ui, jsBackend, credBackend) |
||||
if err != nil { |
||||
t.Fatalf("Failed to create js engine: %v", err) |
||||
} |
||||
if err = r.Init(js); err != nil { |
||||
t.Fatalf("Failed to load bootstrap js: %v", err) |
||||
} |
||||
r.ApproveSignData(nil) |
||||
r.ApproveTx(nil) |
||||
r.ApproveImport(nil) |
||||
r.ApproveNewAccount(nil) |
||||
r.ApproveListing(nil) |
||||
r.ApproveExport(nil) |
||||
r.ShowError("test") |
||||
r.ShowInfo("test") |
||||
|
||||
//This one is not forwarded
|
||||
r.OnApprovedTx(ethapi.SignTransactionResult{}) |
||||
|
||||
expCalls := 8 |
||||
if len(ui.calls) != expCalls { |
||||
|
||||
t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ",")) |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
func TestMissingFunc(t *testing.T) { |
||||
r, err := initRuleEngine(JS) |
||||
if err != nil { |
||||
t.Errorf("Couldn't create evaluator %v", err) |
||||
return |
||||
} |
||||
|
||||
_, err = r.execute("MissingMethod", "test") |
||||
|
||||
if err == nil { |
||||
t.Error("Expected error") |
||||
} |
||||
|
||||
approved, err := r.checkApproval("MissingMethod", nil, nil) |
||||
if err == nil { |
||||
t.Errorf("Expected missing method to yield error'") |
||||
} |
||||
if approved { |
||||
t.Errorf("Expected missing method to cause non-approval") |
||||
} |
||||
fmt.Printf("Err %v", err) |
||||
|
||||
} |
||||
func TestStorage(t *testing.T) { |
||||
|
||||
js := ` |
||||
function testStorage(){ |
||||
storage.Put("mykey", "myvalue") |
||||
a = storage.Get("mykey") |
||||
|
||||
storage.Put("mykey", ["a", "list"]) // Should result in "a,list"
|
||||
a += storage.Get("mykey") |
||||
|
||||
|
||||
storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]"
|
||||
a += storage.Get("mykey") |
||||
|
||||
|
||||
storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
|
||||
a += storage.Get("mykey") |
||||
|
||||
a += storage.Get("missingkey") //Missing keys should result in empty string
|
||||
storage.Put("","missing key==noop") // Can't store with 0-length key
|
||||
a += storage.Get("") // Should result in ''
|
||||
|
||||
var b = new BigNumber(2) |
||||
var c = new BigNumber(16)//"0xf0",16)
|
||||
var d = b.plus(c) |
||||
console.log(d) |
||||
return a |
||||
} |
||||
` |
||||
r, err := initRuleEngine(js) |
||||
if err != nil { |
||||
t.Errorf("Couldn't create evaluator %v", err) |
||||
return |
||||
} |
||||
|
||||
v, err := r.execute("testStorage", nil) |
||||
|
||||
if err != nil { |
||||
t.Errorf("Unexpected error %v", err) |
||||
} |
||||
|
||||
retval, err := v.ToString() |
||||
|
||||
if err != nil { |
||||
t.Errorf("Unexpected error %v", err) |
||||
} |
||||
exp := `myvaluea,list[object Object]{"an":"object"}` |
||||
if retval != exp { |
||||
t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval) |
||||
} |
||||
fmt.Printf("Err %v", err) |
||||
|
||||
} |
||||
|
||||
const ExampleTxWindow = ` |
||||
function big(str){ |
||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)} |
||||
return new BigNumber(str) |
||||
} |
||||
|
||||
// Time window: 1 week
|
||||
var window = 1000* 3600*24*7; |
||||
|
||||
// Limit : 1 ether
|
||||
var limit = new BigNumber("1e18"); |
||||
|
||||
function isLimitOk(transaction){ |
||||
var value = big(transaction.value) |
||||
// Start of our window function
|
||||
var windowstart = new Date().getTime() - window; |
||||
|
||||
var txs = []; |
||||
var stored = storage.Get('txs'); |
||||
|
||||
if(stored != ""){ |
||||
txs = JSON.parse(stored) |
||||
} |
||||
// First, remove all that have passed out of the time-window
|
||||
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart}); |
||||
console.log(txs, newtxs.length); |
||||
|
||||
// Secondly, aggregate the current sum
|
||||
sum = new BigNumber(0) |
||||
|
||||
sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum); |
||||
console.log("ApproveTx > Sum so far", sum); |
||||
console.log("ApproveTx > Requested", value.toNumber()); |
||||
|
||||
// Would we exceed weekly limit ?
|
||||
return sum.plus(value).lt(limit) |
||||
|
||||
} |
||||
function ApproveTx(r){ |
||||
console.log(r) |
||||
console.log(typeof(r)) |
||||
if (isLimitOk(r.transaction)){ |
||||
return "Approve" |
||||
} |
||||
return "Nope" |
||||
} |
||||
|
||||
/** |
||||
* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter |
||||
* 'response_str' contains the return value that will be sent to the external caller.
|
||||
* The return value from this method is ignore - the reason for having this callback is to allow the
|
||||
* ruleset to keep track of approved transactions.
|
||||
* |
||||
* When implementing rate-limited rules, this callback should be used.
|
||||
* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user |
||||
* then accepts the transaction, this method will be called. |
||||
*
|
||||
* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx. |
||||
*/ |
||||
function OnApprovedTx(resp){ |
||||
var value = big(resp.tx.value) |
||||
var txs = [] |
||||
// Load stored transactions
|
||||
var stored = storage.Get('txs'); |
||||
if(stored != ""){ |
||||
txs = JSON.parse(stored) |
||||
} |
||||
// Add this to the storage
|
||||
txs.push({tstamp: new Date().getTime(), value: value}); |
||||
storage.Put("txs", JSON.stringify(txs)); |
||||
} |
||||
|
||||
` |
||||
|
||||
func dummyTx(value hexutil.Big) *core.SignTxRequest { |
||||
|
||||
to, _ := mixAddr("000000000000000000000000000000000000dead") |
||||
from, _ := mixAddr("000000000000000000000000000000000000dead") |
||||
n := hexutil.Uint64(3) |
||||
gas := hexutil.Uint64(21000) |
||||
gasPrice := hexutil.Big(*big.NewInt(2000000)) |
||||
|
||||
return &core.SignTxRequest{ |
||||
Transaction: core.SendTxArgs{ |
||||
From: *from, |
||||
To: to, |
||||
Value: value, |
||||
Nonce: n, |
||||
GasPrice: gasPrice, |
||||
Gas: gas, |
||||
}, |
||||
Callinfo: []core.ValidationInfo{ |
||||
{Typ: "Warning", Message: "All your base are bellong to us"}, |
||||
}, |
||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, |
||||
} |
||||
} |
||||
func dummyTxWithV(value uint64) *core.SignTxRequest { |
||||
|
||||
v := big.NewInt(0).SetUint64(value) |
||||
h := hexutil.Big(*v) |
||||
return dummyTx(h) |
||||
} |
||||
func dummySigned(value *big.Int) *types.Transaction { |
||||
to := common.HexToAddress("000000000000000000000000000000000000dead") |
||||
gas := uint64(21000) |
||||
gasPrice := big.NewInt(2000000) |
||||
data := make([]byte, 0) |
||||
return types.NewTransaction(3, to, value, gas, gasPrice, data) |
||||
|
||||
} |
||||
func TestLimitWindow(t *testing.T) { |
||||
|
||||
r, err := initRuleEngine(ExampleTxWindow) |
||||
if err != nil { |
||||
t.Errorf("Couldn't create evaluator %v", err) |
||||
return |
||||
} |
||||
|
||||
// 0.3 ether: 429D069189E0000 wei
|
||||
v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000")) |
||||
h := hexutil.Big(*v) |
||||
// The first three should succeed
|
||||
for i := 0; i < 3; i++ { |
||||
unsigned := dummyTx(h) |
||||
resp, err := r.ApproveTx(unsigned) |
||||
if err != nil { |
||||
t.Errorf("Unexpected error %v", err) |
||||
} |
||||
if !resp.Approved { |
||||
t.Errorf("Expected check to resolve to 'Approve'") |
||||
} |
||||
// Create a dummy signed transaction
|
||||
|
||||
response := ethapi.SignTransactionResult{ |
||||
Tx: dummySigned(v), |
||||
Raw: common.Hex2Bytes("deadbeef"), |
||||
} |
||||
r.OnApprovedTx(response) |
||||
} |
||||
// Fourth should fail
|
||||
resp, err := r.ApproveTx(dummyTx(h)) |
||||
if resp.Approved { |
||||
t.Errorf("Expected check to resolve to 'Reject'") |
||||
} |
||||
|
||||
} |
||||
|
||||
// dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
|
||||
type dontCallMe struct { |
||||
t *testing.T |
||||
} |
||||
|
||||
func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) { |
||||
} |
||||
|
||||
func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
return core.SignTxResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
return core.SignDataResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dontCallMe) ApproveExport(request *core.ExportRequest) (core.ExportResponse, error) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
return core.ExportResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dontCallMe) ApproveImport(request *core.ImportRequest) (core.ImportResponse, error) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
return core.ImportResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
return core.ListResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
return core.NewAccountResponse{}, core.ErrRequestDenied |
||||
} |
||||
|
||||
func (d *dontCallMe) ShowError(message string) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
} |
||||
|
||||
func (d *dontCallMe) ShowInfo(message string) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
} |
||||
|
||||
func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) { |
||||
d.t.Fatalf("Did not expect next-handler to be called") |
||||
} |
||||
|
||||
//TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
|
||||
// if it does, that would be bad since developers may rely on that to store data,
|
||||
// instead of using the disk-based data storage
|
||||
func TestContextIsCleared(t *testing.T) { |
||||
|
||||
js := ` |
||||
function ApproveTx(){ |
||||
if (typeof foobar == 'undefined') { |
||||
foobar = "Approve" |
||||
} |
||||
console.log(foobar) |
||||
if (foobar == "Approve"){ |
||||
foobar = "Reject" |
||||
}else{ |
||||
foobar = "Approve" |
||||
} |
||||
return foobar |
||||
} |
||||
` |
||||
ui := &dontCallMe{t} |
||||
r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage(), storage.NewEphemeralStorage()) |
||||
if err != nil { |
||||
t.Fatalf("Failed to create js engine: %v", err) |
||||
} |
||||
if err = r.Init(js); err != nil { |
||||
t.Fatalf("Failed to load bootstrap js: %v", err) |
||||
} |
||||
tx := dummyTxWithV(0) |
||||
r1, err := r.ApproveTx(tx) |
||||
r2, err := r.ApproveTx(tx) |
||||
if r1.Approved != r2.Approved { |
||||
t.Errorf("Expected execution context to be cleared between executions") |
||||
} |
||||
} |
||||
|
||||
func TestSignData(t *testing.T) { |
||||
|
||||
js := `function ApproveListing(){ |
||||
return "Approve" |
||||
} |
||||
function ApproveSignData(r){ |
||||
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") |
||||
{ |
||||
if(r.message.indexOf("bazonk") >= 0){ |
||||
return "Approve" |
||||
} |
||||
return "Reject" |
||||
} |
||||
// Otherwise goes to manual processing
|
||||
}` |
||||
r, err := initRuleEngine(js) |
||||
if err != nil { |
||||
t.Errorf("Couldn't create evaluator %v", err) |
||||
return |
||||
} |
||||
message := []byte("baz bazonk foo") |
||||
hash, msg := core.SignHash(message) |
||||
raw := hexutil.Bytes(message) |
||||
addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa") |
||||
|
||||
fmt.Printf("address %v %v\n", addr.String(), addr.Original()) |
||||
resp, err := r.ApproveSignData(&core.SignDataRequest{ |
||||
Address: *addr, |
||||
Message: msg, |
||||
Hash: hash, |
||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, |
||||
Rawdata: raw, |
||||
}) |
||||
if err != nil { |
||||
t.Fatalf("Unexpected error %v", err) |
||||
} |
||||
if !resp.Approved { |
||||
t.Fatalf("Expected approved") |
||||
} |
||||
} |
@ -0,0 +1,164 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
package storage |
||||
|
||||
import ( |
||||
"crypto/aes" |
||||
"crypto/cipher" |
||||
"crypto/rand" |
||||
"encoding/json" |
||||
"io" |
||||
"io/ioutil" |
||||
"os" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
type storedCredential struct { |
||||
// The iv
|
||||
Iv []byte `json:"iv"` |
||||
// The ciphertext
|
||||
CipherText []byte `json:"c"` |
||||
} |
||||
|
||||
// AESEncryptedStorage is a storage type which is backed by a json-faile. The json-file contains
|
||||
// key-value mappings, where the keys are _not_ encrypted, only the values are.
|
||||
type AESEncryptedStorage struct { |
||||
// File to read/write credentials
|
||||
filename string |
||||
// Key stored in base64
|
||||
key []byte |
||||
} |
||||
|
||||
// NewAESEncryptedStorage creates a new encrypted storage backed by the given file/key
|
||||
func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage { |
||||
return &AESEncryptedStorage{ |
||||
filename: filename, |
||||
key: key, |
||||
} |
||||
} |
||||
|
||||
// Put stores a value by key. 0-length keys results in no-op
|
||||
func (s *AESEncryptedStorage) Put(key, value string) { |
||||
if len(key) == 0 { |
||||
return |
||||
} |
||||
data, err := s.readEncryptedStorage() |
||||
if err != nil { |
||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename) |
||||
return |
||||
} |
||||
ciphertext, iv, err := encrypt(s.key, []byte(value)) |
||||
if err != nil { |
||||
log.Warn("Failed to encrypt entry", "err", err) |
||||
return |
||||
} |
||||
encrypted := storedCredential{Iv: iv, CipherText: ciphertext} |
||||
data[key] = encrypted |
||||
if err = s.writeEncryptedStorage(data); err != nil { |
||||
log.Warn("Failed to write entry", "err", err) |
||||
} |
||||
} |
||||
|
||||
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
|
||||
func (s *AESEncryptedStorage) Get(key string) string { |
||||
if len(key) == 0 { |
||||
return "" |
||||
} |
||||
data, err := s.readEncryptedStorage() |
||||
if err != nil { |
||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename) |
||||
return "" |
||||
} |
||||
encrypted, exist := data[key] |
||||
if !exist { |
||||
log.Warn("Key does not exist", "key", key) |
||||
return "" |
||||
} |
||||
entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText) |
||||
if err != nil { |
||||
log.Warn("Failed to decrypt key", "key", key) |
||||
return "" |
||||
} |
||||
return string(entry) |
||||
} |
||||
|
||||
// readEncryptedStorage reads the file with encrypted creds
|
||||
func (s *AESEncryptedStorage) readEncryptedStorage() (map[string]storedCredential, error) { |
||||
creds := make(map[string]storedCredential) |
||||
raw, err := ioutil.ReadFile(s.filename) |
||||
|
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
// Doesn't exist yet
|
||||
return creds, nil |
||||
|
||||
} else { |
||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename) |
||||
} |
||||
} |
||||
if err = json.Unmarshal(raw, &creds); err != nil { |
||||
log.Warn("Failed to unmarshal encrypted storage", "err", err, "file", s.filename) |
||||
return nil, err |
||||
} |
||||
return creds, nil |
||||
} |
||||
|
||||
// writeEncryptedStorage write the file with encrypted creds
|
||||
func (s *AESEncryptedStorage) writeEncryptedStorage(creds map[string]storedCredential) error { |
||||
raw, err := json.Marshal(creds) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if err = ioutil.WriteFile(s.filename, raw, 0600); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func encrypt(key []byte, plaintext []byte) ([]byte, []byte, error) { |
||||
block, err := aes.NewCipher(key) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
aesgcm, err := cipher.NewGCM(block) |
||||
nonce := make([]byte, aesgcm.NonceSize()) |
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) |
||||
return ciphertext, nonce, nil |
||||
} |
||||
|
||||
func decrypt(key []byte, nonce []byte, ciphertext []byte) ([]byte, error) { |
||||
block, err := aes.NewCipher(key) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
aesgcm, err := cipher.NewGCM(block) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return plaintext, nil |
||||
} |
@ -0,0 +1,115 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
package storage |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/mattn/go-colorable" |
||||
) |
||||
|
||||
func TestEncryption(t *testing.T) { |
||||
// key := []byte("AES256Key-32Characters1234567890")
|
||||
// plaintext := []byte(value)
|
||||
key := []byte("AES256Key-32Characters1234567890") |
||||
plaintext := []byte("exampleplaintext") |
||||
|
||||
c, iv, err := encrypt(key, plaintext) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
fmt.Printf("Ciphertext %x, nonce %x\n", c, iv) |
||||
|
||||
p, err := decrypt(key, iv, c) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
fmt.Printf("Plaintext %v\n", string(p)) |
||||
if !bytes.Equal(plaintext, p) { |
||||
t.Errorf("Failed: expected plaintext recovery, got %v expected %v", string(plaintext), string(p)) |
||||
} |
||||
} |
||||
|
||||
func TestFileStorage(t *testing.T) { |
||||
|
||||
a := map[string]storedCredential{ |
||||
"secret": { |
||||
Iv: common.Hex2Bytes("cdb30036279601aeee60f16b"), |
||||
CipherText: common.Hex2Bytes("f311ac49859d7260c2c464c28ffac122daf6be801d3cfd3edcbde7e00c9ff74f"), |
||||
}, |
||||
"secret2": { |
||||
Iv: common.Hex2Bytes("afb8a7579bf971db9f8ceeed"), |
||||
CipherText: common.Hex2Bytes("2df87baf86b5073ef1f03e3cc738de75b511400f5465bb0ddeacf47ae4dc267d"), |
||||
}, |
||||
} |
||||
d, err := ioutil.TempDir("", "eth-encrypted-storage-test") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
stored := &AESEncryptedStorage{ |
||||
filename: fmt.Sprintf("%v/vault.json", d), |
||||
key: []byte("AES256Key-32Characters1234567890"), |
||||
} |
||||
stored.writeEncryptedStorage(a) |
||||
read := &AESEncryptedStorage{ |
||||
filename: fmt.Sprintf("%v/vault.json", d), |
||||
key: []byte("AES256Key-32Characters1234567890"), |
||||
} |
||||
creds, err := read.readEncryptedStorage() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
for k, v := range a { |
||||
if v2, exist := creds[k]; !exist { |
||||
t.Errorf("Missing entry %v", k) |
||||
} else { |
||||
if !bytes.Equal(v.CipherText, v2.CipherText) { |
||||
t.Errorf("Wrong ciphertext, expected %x got %x", v.CipherText, v2.CipherText) |
||||
} |
||||
if !bytes.Equal(v.Iv, v2.Iv) { |
||||
t.Errorf("Wrong iv") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
func TestEnd2End(t *testing.T) { |
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(3), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) |
||||
|
||||
d, err := ioutil.TempDir("", "eth-encrypted-storage-test") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
s1 := &AESEncryptedStorage{ |
||||
filename: fmt.Sprintf("%v/vault.json", d), |
||||
key: []byte("AES256Key-32Characters1234567890"), |
||||
} |
||||
s2 := &AESEncryptedStorage{ |
||||
filename: fmt.Sprintf("%v/vault.json", d), |
||||
key: []byte("AES256Key-32Characters1234567890"), |
||||
} |
||||
|
||||
s1.Put("bazonk", "foobar") |
||||
if v := s2.Get("bazonk"); v != "foobar" { |
||||
t.Errorf("Expected bazonk->foobar, got '%v'", v) |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package storage |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
type Storage interface { |
||||
// Put stores a value by key. 0-length keys results in no-op
|
||||
Put(key, value string) |
||||
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
|
||||
Get(key string) string |
||||
} |
||||
|
||||
// EphemeralStorage is an in-memory storage that does
|
||||
// not persist values to disk. Mainly used for testing
|
||||
type EphemeralStorage struct { |
||||
data map[string]string |
||||
namespace string |
||||
} |
||||
|
||||
func (s *EphemeralStorage) Put(key, value string) { |
||||
if len(key) == 0 { |
||||
return |
||||
} |
||||
fmt.Printf("storage: put %v -> %v\n", key, value) |
||||
s.data[key] = value |
||||
} |
||||
|
||||
func (s *EphemeralStorage) Get(key string) string { |
||||
if len(key) == 0 { |
||||
return "" |
||||
} |
||||
fmt.Printf("storage: get %v\n", key) |
||||
if v, exist := s.data[key]; exist { |
||||
return v |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
func NewEphemeralStorage() Storage { |
||||
s := &EphemeralStorage{ |
||||
data: make(map[string]string), |
||||
} |
||||
return s |
||||
} |
Loading…
Reference in new issue