Przeglądaj źródła

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: formatting
Martin Holst Swende 7 lat temu
rodzic
commit
ec3db0f56c

+ 16 - 0
accounts/url.go

@@ -74,6 +74,22 @@ func (u URL) MarshalJSON() ([]byte, error) {
 	return json.Marshal(u.String())
 }
 
+// UnmarshalJSON parses url.
+func (u *URL) UnmarshalJSON(input []byte) error {
+	var textUrl string
+	err := json.Unmarshal(input, &textUrl)
+	if err != nil {
+		return err
+	}
+	url, err := parseURL(textUrl)
+	if err != nil {
+		return err
+	}
+	u.Scheme = url.Scheme
+	u.Path = url.Path
+	return nil
+}
+
 // Cmp compares x and y and returns:
 //
 //   -1 if x <  y

+ 1 - 1
accounts/usbwallet/hub.go

@@ -127,7 +127,7 @@ func (hub *Hub) refreshWallets() {
 		// breaking the Ledger protocol if that is waiting for user confirmation. This
 		// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
 		// need to prevent concurrent comms ourselves. The more elegant solution would
-		// be to ditch enumeration in favor of hutplug events, but that don't work yet
+		// be to ditch enumeration in favor of hotplug events, but that don't work yet
 		// on Windows so if we need to hack it anyway, this is more elegant for now.
 		hub.commsLock.Lock()
 		if hub.commsPend > 0 { // A confirmation is pending, don't refresh

+ 1 - 1
accounts/usbwallet/wallet.go

@@ -99,7 +99,7 @@ type wallet struct {
 	//
 	// As such, a hardware wallet needs two locks to function correctly. A state
 	// lock can be used to protect the wallet's software-side internal state, which
-	// must not be held exlusively during hardware communication. A communication
+	// must not be held exclusively during hardware communication. A communication
 	// lock can be used to achieve exclusive access to the device itself, this one
 	// however should allow "skipping" waiting for operations that might want to
 	// use the device, but can live without too (e.g. account self-derivation).

Plik diff jest za duży
+ 0 - 0
cmd/clef/4byte.json


+ 864 - 0
cmd/clef/README.md

@@ -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.
+
+

+ 25 - 0
cmd/clef/extapi_changelog.md

@@ -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.

+ 86 - 0
cmd/clef/intapi_changelog.md

@@ -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.

+ 640 - 0
cmd/clef/main.go

@@ -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/
+
+
+**/

+ 179 - 0
cmd/clef/pythonsigner.py

@@ -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:])

+ 236 - 0
cmd/clef/rules.md

@@ -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"
+    }
+
+```

BIN
cmd/clef/sign_flow.png


+ 198 - 0
cmd/clef/tutorial.md

@@ -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"
+```

+ 62 - 0
common/types.go

@@ -23,8 +23,10 @@ import (
 	"math/rand"
 	"reflect"
 
+	"encoding/json"
 	"github.com/ethereum/go-ethereum/common/hexutil"
 	"github.com/ethereum/go-ethereum/crypto/sha3"
+	"strings"
 )
 
 const (
@@ -238,3 +240,63 @@ func (a *UnprefixedAddress) UnmarshalText(input []byte) error {
 func (a UnprefixedAddress) MarshalText() ([]byte, error) {
 	return []byte(hex.EncodeToString(a[:])), nil
 }
+
+// MixedcaseAddress retains the original string, which may or may not be
+// correctly checksummed
+type MixedcaseAddress struct {
+	addr     Address
+	original string
+}
+
+// NewMixedcaseAddress constructor (mainly for testing)
+func NewMixedcaseAddress(addr Address) MixedcaseAddress {
+	return MixedcaseAddress{addr: addr, original: addr.Hex()}
+}
+
+// NewMixedcaseAddressFromString is mainly meant for unit-testing
+func NewMixedcaseAddressFromString(hexaddr string) (*MixedcaseAddress, error) {
+	if !IsHexAddress(hexaddr) {
+		return nil, fmt.Errorf("Invalid address")
+	}
+	a := FromHex(hexaddr)
+	return &MixedcaseAddress{addr: BytesToAddress(a), original: hexaddr}, nil
+}
+
+// UnmarshalJSON parses MixedcaseAddress
+func (ma *MixedcaseAddress) UnmarshalJSON(input []byte) error {
+	if err := hexutil.UnmarshalFixedJSON(addressT, input, ma.addr[:]); err != nil {
+		return err
+	}
+	return json.Unmarshal(input, &ma.original)
+}
+
+// MarshalJSON marshals the original value
+func (ma *MixedcaseAddress) MarshalJSON() ([]byte, error) {
+	if strings.HasPrefix(ma.original, "0x") || strings.HasPrefix(ma.original, "0X") {
+		return json.Marshal(fmt.Sprintf("0x%s", ma.original[2:]))
+	}
+	return json.Marshal(fmt.Sprintf("0x%s", ma.original))
+}
+
+// Address returns the address
+func (ma *MixedcaseAddress) Address() Address {
+	return ma.addr
+}
+
+// String implements fmt.Stringer
+func (ma *MixedcaseAddress) String() string {
+	if ma.ValidChecksum() {
+		return fmt.Sprintf("%s [chksum ok]", ma.original)
+	}
+	return fmt.Sprintf("%s [chksum INVALID]", ma.original)
+}
+
+// ValidChecksum returns true if the address has valid checksum
+func (ma *MixedcaseAddress) ValidChecksum() bool {
+	return ma.original == ma.addr.Hex()
+}
+
+// Original returns the mixed-case input string
+func (ma *MixedcaseAddress) Original() string {
+	return ma.original
+}

+ 44 - 0
common/types_test.go

@@ -18,6 +18,7 @@ package common
 
 import (
 	"encoding/json"
+
 	"math/big"
 	"strings"
 	"testing"
@@ -149,3 +150,46 @@ func BenchmarkAddressHex(b *testing.B) {
 		testAddr.Hex()
 	}
 }
+
+func TestMixedcaseAccount_Address(t *testing.T) {
+
+	// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
+	// Note: 0X{checksum_addr} is not valid according to spec above
+
+	var res []struct {
+		A     MixedcaseAddress
+		Valid bool
+	}
+	if err := json.Unmarshal([]byte(`[
+		{"A" : "0xae967917c465db8578ca9024c205720b1a3651A9", "Valid": false},
+		{"A" : "0xAe967917c465db8578ca9024c205720b1a3651A9", "Valid": true},
+		{"A" : "0XAe967917c465db8578ca9024c205720b1a3651A9", "Valid": false},
+		{"A" : "0x1111111111111111111112222222222223333323", "Valid": true}
+		]`), &res); err != nil {
+		t.Fatal(err)
+	}
+
+	for _, r := range res {
+		if got := r.A.ValidChecksum(); got != r.Valid {
+			t.Errorf("Expected checksum %v, got checksum %v, input %v", r.Valid, got, r.A.String())
+		}
+	}
+
+	//These should throw exceptions:
+	var r2 []MixedcaseAddress
+	for _, r := range []string{
+		`["0x11111111111111111111122222222222233333"]`,     // Too short
+		`["0x111111111111111111111222222222222333332"]`,    // Too short
+		`["0x11111111111111111111122222222222233333234"]`,  // Too long
+		`["0x111111111111111111111222222222222333332344"]`, // Too long
+		`["1111111111111111111112222222222223333323"]`,     // Missing 0x
+		`["x1111111111111111111112222222222223333323"]`,    // Missing 0
+		`["0xG111111111111111111112222222222223333323"]`,   //Non-hex
+	} {
+		if err := json.Unmarshal([]byte(r), &r2); err == nil {
+			t.Errorf("Expected failure, input %v", r)
+		}
+
+	}
+
+}

+ 15 - 80
node/node.go

@@ -306,47 +306,23 @@ func (n *Node) startIPC(apis []rpc.API) error {
 	// Short circuit if the IPC endpoint isn't being exposed
 	if n.ipcEndpoint == "" {
 		return nil
+
 	}
-	// Register all the APIs exposed by the services
-	handler := rpc.NewServer()
-	for _, api := range apis {
-		if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
-			return err
-		}
-		n.log.Debug("IPC registered", "service", api.Service, "namespace", api.Namespace)
-	}
-	// All APIs registered, start the IPC listener
-	var (
-		listener net.Listener
-		err      error
-	)
-	if listener, err = rpc.CreateIPCListener(n.ipcEndpoint); err != nil {
+	isClosed := func() bool {
+		n.lock.RLock()
+		defer n.lock.RUnlock()
+		return n.ipcListener == nil
+	}
+
+	listener, handler, err := rpc.StartIPCEndpoint(isClosed, n.ipcEndpoint, apis)
+	if err != nil {
 		return err
 	}
-	go func() {
-		n.log.Info("IPC endpoint opened", "url", n.ipcEndpoint)
-
-		for {
-			conn, err := listener.Accept()
-			if err != nil {
-				// Terminate if the listener was closed
-				n.lock.RLock()
-				closed := n.ipcListener == nil
-				n.lock.RUnlock()
-				if closed {
-					return
-				}
-				// Not closed, just some error; report and continue
-				n.log.Error("IPC accept failed", "err", err)
-				continue
-			}
-			go handler.ServeCodec(rpc.NewJSONCodec(conn), rpc.OptionMethodInvocation|rpc.OptionSubscriptions)
-		}
-	}()
+
 	// All listeners booted successfully
 	n.ipcListener = listener
 	n.ipcHandler = handler
-
+	n.log.Info("IPC endpoint opened", "url", n.ipcEndpoint)
 	return nil
 }
 
@@ -370,30 +346,10 @@ func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors
 	if endpoint == "" {
 		return nil
 	}
-	// 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 := rpc.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 err
-			}
-			n.log.Debug("HTTP 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 {
+	listener, handler, err := rpc.StartHTTPEndpoint(endpoint, apis, modules, cors, vhosts)
+	if err != nil {
 		return err
 	}
-	go rpc.NewHTTPServer(cors, vhosts, handler).Serve(listener)
 	n.log.Info("HTTP endpoint opened", "url", fmt.Sprintf("http://%s", endpoint), "cors", strings.Join(cors, ","), "vhosts", strings.Join(vhosts, ","))
 	// All listeners booted successfully
 	n.httpEndpoint = endpoint
@@ -423,32 +379,11 @@ func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, wsOrig
 	if endpoint == "" {
 		return nil
 	}
-	// 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 := rpc.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 err
-			}
-			n.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 {
+	listener, handler, err := rpc.StartWSEndpoint(endpoint, apis, modules, wsOrigins, exposeAll)
+	if err != nil {
 		return err
 	}
-	go rpc.NewWSServer(wsOrigins, handler).Serve(listener)
 	n.log.Info("WebSocket endpoint opened", "url", fmt.Sprintf("ws://%s", listener.Addr()))
-
 	// All listeners booted successfully
 	n.wsEndpoint = endpoint
 	n.wsListener = listener

+ 47 - 6
rpc/client.go

@@ -33,6 +33,7 @@ import (
 	"time"
 
 	"github.com/ethereum/go-ethereum/log"
+	"os"
 )
 
 var (
@@ -171,6 +172,8 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) {
 		return DialHTTP(rawurl)
 	case "ws", "wss":
 		return DialWebsocket(ctx, rawurl, "")
+	case "stdio":
+		return DialStdIO(ctx)
 	case "":
 		return DialIPC(ctx, rawurl)
 	default:
@@ -178,13 +181,51 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) {
 	}
 }
 
+type StdIOConn struct{}
+
+func (io StdIOConn) Read(b []byte) (n int, err error) {
+	return os.Stdin.Read(b)
+}
+
+func (io StdIOConn) Write(b []byte) (n int, err error) {
+	return os.Stdout.Write(b)
+}
+
+func (io StdIOConn) Close() error {
+	return nil
+}
+
+func (io StdIOConn) LocalAddr() net.Addr {
+	return &net.UnixAddr{Name: "stdio", Net: "stdio"}
+}
+
+func (io StdIOConn) RemoteAddr() net.Addr {
+	return &net.UnixAddr{Name: "stdio", Net: "stdio"}
+}
+
+func (io StdIOConn) SetDeadline(t time.Time) error {
+	return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
+}
+
+func (io StdIOConn) SetReadDeadline(t time.Time) error {
+	return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
+}
+
+func (io StdIOConn) SetWriteDeadline(t time.Time) error {
+	return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
+}
+func DialStdIO(ctx context.Context) (*Client, error) {
+	return newClient(ctx, func(_ context.Context) (net.Conn, error) {
+		return StdIOConn{}, nil
+	})
+}
+
 func newClient(initctx context.Context, connectFunc func(context.Context) (net.Conn, error)) (*Client, error) {
 	conn, err := connectFunc(initctx)
 	if err != nil {
 		return nil, err
 	}
 	_, isHTTP := conn.(*httpConn)
-
 	c := &Client{
 		writeConn:   conn,
 		isHTTP:      isHTTP,
@@ -524,13 +565,13 @@ func (c *Client) dispatch(conn net.Conn) {
 			}
 
 		case err := <-c.readErr:
-			log.Debug(fmt.Sprintf("<-readErr: %v", err))
+			log.Debug("<-readErr", "err", err)
 			c.closeRequestOps(err)
 			conn.Close()
 			reading = false
 
 		case newconn := <-c.reconnected:
-			log.Debug(fmt.Sprintf("<-reconnected: (reading=%t) %v", reading, conn.RemoteAddr()))
+			log.Debug("<-reconnected", "reading", reading, "remote", conn.RemoteAddr())
 			if reading {
 				// Wait for the previous read loop to exit. This is a rare case.
 				conn.Close()
@@ -587,7 +628,7 @@ func (c *Client) closeRequestOps(err error) {
 
 func (c *Client) handleNotification(msg *jsonrpcMessage) {
 	if !strings.HasSuffix(msg.Method, notificationMethodSuffix) {
-		log.Debug(fmt.Sprint("dropping non-subscription message: ", msg))
+		log.Debug("dropping non-subscription message", "msg", msg)
 		return
 	}
 	var subResult struct {
@@ -595,7 +636,7 @@ func (c *Client) handleNotification(msg *jsonrpcMessage) {
 		Result json.RawMessage `json:"result"`
 	}
 	if err := json.Unmarshal(msg.Params, &subResult); err != nil {
-		log.Debug(fmt.Sprint("dropping invalid subscription message: ", msg))
+		log.Debug("dropping invalid subscription message", "msg", msg)
 		return
 	}
 	if c.subs[subResult.ID] != nil {
@@ -606,7 +647,7 @@ func (c *Client) handleNotification(msg *jsonrpcMessage) {
 func (c *Client) handleResponse(msg *jsonrpcMessage) {
 	op := c.respWait[string(msg.ID)]
 	if op == nil {
-		log.Debug(fmt.Sprintf("unsolicited response %v", msg))
+		log.Debug("unsolicited response", "msg", msg)
 		return
 	}
 	delete(c.respWait, string(msg.ID))

+ 120 - 0
rpc/endpoints.go

@@ -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
+}

+ 6 - 1
rpc/http.go

@@ -169,12 +169,17 @@ func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// All checks passed, create a codec that reads direct from the request body
 	// untilEOF and writes the response to w and order the server to process a
 	// single request.
+	ctx := context.Background()
+	ctx = context.WithValue(ctx, "remote", r.RemoteAddr)
+	ctx = context.WithValue(ctx, "scheme", r.Proto)
+	ctx = context.WithValue(ctx, "local", r.Host)
+
 	body := io.LimitReader(r.Body, maxRequestContentLength)
 	codec := NewJSONCodec(&httpReadWriteNopCloser{body, w})
 	defer codec.Close()
 
 	w.Header().Set("content-type", contentType)
-	srv.ServeSingleRequest(codec, OptionMethodInvocation)
+	srv.ServeSingleRequest(codec, OptionMethodInvocation, ctx)
 }
 
 // validateRequest returns a non-zero response code and error message if the

+ 6 - 5
rpc/server.go

@@ -125,7 +125,7 @@ func (s *Server) RegisterName(name string, rcvr interface{}) error {
 // If singleShot is true it will process a single request, otherwise it will handle
 // requests until the codec returns an error when reading a request (in most cases
 // an EOF). It executes requests in parallel when singleShot is false.
-func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecOption) error {
+func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecOption, ctx context.Context) error {
 	var pend sync.WaitGroup
 
 	defer func() {
@@ -140,7 +140,8 @@ func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecO
 		s.codecsMu.Unlock()
 	}()
 
-	ctx, cancel := context.WithCancel(context.Background())
+	//	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
 	// if the codec supports notification include a notifier that callbacks can use
@@ -215,14 +216,14 @@ func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecO
 // stopped. In either case the codec is closed.
 func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) {
 	defer codec.Close()
-	s.serveRequest(codec, false, options)
+	s.serveRequest(codec, false, options, context.Background())
 }
 
 // ServeSingleRequest reads and processes a single RPC request from the given codec. It will not
 // close the codec unless a non-recoverable error has occurred. Note, this method will return after
 // a single request has been processed!
-func (s *Server) ServeSingleRequest(codec ServerCodec, options CodecOption) {
-	s.serveRequest(codec, true, options)
+func (s *Server) ServeSingleRequest(codec ServerCodec, options CodecOption, ctx context.Context) {
+	s.serveRequest(codec, true, options, ctx)
 }
 
 // Stop will stop reading new requests, wait for stopPendingRequestTimeout to allow pending requests to finish,

+ 256 - 0
signer/core/abihelper.go

@@ -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)
+}

+ 247 - 0
signer/core/abihelper_test.go

@@ -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")
+	}
+}

+ 500 - 0
signer/core/api.go

@@ -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
+}

+ 386 - 0
signer/core/api_test.go

@@ -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)
+
+
+	}
+*/

+ 110 - 0
signer/core/auditlog.go

@@ -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
+}

+ 247 - 0
signer/core/cliui.go

@@ -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)
+	}
+}

+ 113 - 0
signer/core/stdioui.go

@@ -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)
+	}
+}

+ 95 - 0
signer/core/types.go

@@ -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)
+}

+ 163 - 0
signer/core/validation.go

@@ -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)
+}

+ 139 - 0
signer/core/validation_test.go

@@ -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()
+			}
+		}
+	}
+}

Plik diff jest za duży
+ 2 - 0
signer/rules/deps/bignumber.js


Plik diff jest za duży
+ 70 - 0
signer/rules/deps/bindata.go


+ 21 - 0
signer/rules/deps/deps.go

@@ -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

+ 248 - 0
signer/rules/rules.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)
+	}
+}

+ 631 - 0
signer/rules/rules_test.go

@@ -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")
+	}
+}

+ 164 - 0
signer/storage/aes_gcm_storage.go

@@ -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
+}

+ 115 - 0
signer/storage/aes_gcm_storage_test.go

@@ -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)
+	}
+}

+ 62 - 0
signer/storage/storage.go

@@ -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
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików