From a0943b8932f2fcd28dc103689f904a3c75ea07a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Tue, 2 Jul 2019 14:01:47 +0300 Subject: [PATCH] cmd/clef, signer: refresh tutorial, fix noticed issues (#19774) * cmd/clef, signer: refresh tutorial, fix noticed issues * cmd/clef, signer: support removing stored keys (delpw + rules) * cmd/clef: polishes + Geth integration in the tutorial --- cmd/clef/README.md | 160 +++++----- cmd/clef/datatypes.md | 6 +- cmd/clef/extapi_changelog.md | 31 +- cmd/clef/intapi_changelog.md | 77 ++--- cmd/clef/main.go | 168 ++++++----- cmd/clef/pythonsigner.py | 6 +- cmd/clef/rules.md | 182 ++++++------ cmd/clef/tutorial.md | 392 +++++++++++++++++-------- signer/core/api.go | 31 +- signer/core/cliui.go | 8 +- signer/core/signed_data.go | 21 +- signer/rules/rules.go | 18 +- signer/rules/rules_test.go | 51 ++-- signer/storage/aes_gcm_storage.go | 30 +- signer/storage/aes_gcm_storage_test.go | 8 +- signer/storage/storage.go | 46 ++- 16 files changed, 709 insertions(+), 526 deletions(-) diff --git a/cmd/clef/README.md b/cmd/clef/README.md index 956957f3d1..eeaee0908c 100644 --- a/cmd/clef/README.md +++ b/cmd/clef/README.md @@ -1,41 +1,38 @@ -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. +# Clef -Check out +Clef can be used to sign transactions and data and is meant as a(n eventual) replacement for Geth's account management. This allows DApps to not depend on Geth's account management. When a DApp wants to sign data (or a transaction), it can send the content to Clef, which will then provide the user with context and asks for permission to sign the content. If the users grants the signing request, Clef will send the signature back to the DApp. -* the [tutorial](tutorial.md) for some concrete examples on how the signer works. -* the [setup docs](docs/setup.md) for some information on how to configure it to work on QubesOS or USBArmory. -* the [data types](datatypes.md) for detailed information on the json types used in the communication between - clef and an external UI +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 an untrusted remote Ethereum node, because a local one is not available, not synchronised with the chain, or is a node that has no built-in (or limited) account management. + +Clef can run as a daemon on the same machine, off a usb-stick like [USB armory](https://inversepath.com/usbarmory), or even a separate VM in a [QubesOS](https://www.qubes-os.org/) type setup. + +Check out the + +* [CLI tutorial](tutorial.md) for some concrete examples on how Clef works. +* [Setup docs](docs/setup.md) for infos on how to configure Clef on QubesOS or USB Armory. +* [Data types](datatypes.md) for details on the communication messages between Clef and an external UI. ## 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 setpw Store a credential for a keystore file + delpw Remove a credential for a keystore file gendoc Generate documentation about json-rpc format 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") - --chainid value Chain id to use for signing (1=mainnet, 3=ropsten, 4=rinkeby, 5=Goerli) (default: 1) + --chainid value Chain id to use for signing (1=mainnet, 3=Ropsten, 4=Rinkeby, 5=Goerli) (default: 1) --lightkdf Reduce key-derivation RAM & CPU usage at some expense of KDF strength --nousb Disables monitoring for and managing USB hardware wallets + --pcscdpath value Path to the smartcard daemon (pcscd) socket file (default: "/run/pcscd/pcscd.comm") --rpcaddr value HTTP-RPC server listening interface (default: "localhost") --rpcvhosts value Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard. (default: "localhost") --ipcdisable Disable the IPC-RPC server @@ -43,60 +40,48 @@ GLOBAL OPTIONS: --rpc Enable the HTTP-RPC server --rpcport value HTTP-RPC server listening port (default: 8550) --signersecret value A file containing the (encrypted) master seed to encrypt Clef data, 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") + --rules value Path to the rule file to auto-authorize requests with --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 Clef is started by an external process. --stdio-ui-test Mechanism to test interface between Clef and UI. Requires 'stdio-ui'. --advanced If enabled, issues warnings instead of rejections for suspicious requests. Default off --help, -h show help --version, -v print the version - ``` - Example: -``` -signer -keystore /my/keystore -chainid 4 -``` +``` +$ clef -keystore /my/keystore -chainid 4 +``` ## Security model -The security model of the signer is as follows: +The security model of Clef 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. +* One critical component (the Clef binary / daemon) is responsible for handling cryptographic operations: signing, private keys, encryption/decryption of keystore files. +* Clef 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. +* Clef 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: +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`. +In this case, `geth` would be started with `--signer 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: +* [ ] Clef 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 Clef 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 Clef 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" (side channel attack). +* [x] It should be possible to configure Clef to auto-allow listing (certain) accounts, instead of asking every time. +* [x] Done Upon startup, Clef 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) + * [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 within e.g. 60 seconds, the request is rejected. * [ ] `account_signRawTransaction` @@ -109,21 +94,16 @@ invoking methods with the following info: * 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. - + - 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 --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 + * [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. + * Thus Geth/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. @@ -132,37 +112,31 @@ put together is a bit of a hack into the http server. This could probably be gre ### 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). +Clef listens to HTTP requests on `rpcaddr`:`rpcport` (or to IPC on `ipcpath`), with the same JSON-RPC standard as Geth. The messages are expected to be [JSON-RPC 2.0 standard](https://www.jsonrpc.org/specification). -Some of these call can require user interaction. Clients must be aware that responses -may be delayed significantly or may never be received if a users decides to ignore the confirmation request. +Some of these call can require user interaction. Clients must be aware that responses may be delayed significantly 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. +The External API is **untrusted**: it does not accept credentials over this API, nor does it expect that requests have any authority. -### UI API +### Internal 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. +Clef 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 `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 +* The UI app starts `clef` 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. +* `clef` opens the external HTTP API. +* When the `signer` receives requests, it sends a JSON-RPC request via `stdout`. +* The UI app prompts the user accordingly, and responds to `clef`. +* `clef` 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. +See the [external API changelog](extapi_changelog.md) for information about changes to this API. ### Encoding - number: positive integers that are hex encoded @@ -187,7 +161,7 @@ None #### Result - address [string]: account address that is derived from the generated key - url [string]: location of the keyfile - + #### Sample call ```json { @@ -221,9 +195,9 @@ 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.type [string]: type of the - account.url [string]: location of the account - + #### Sample call ```json { @@ -272,7 +246,7 @@ Response #### Result - signed transaction in RLP encoded form [data] - + #### Sample call ```json { @@ -372,7 +346,7 @@ Bash example: #### Result - calculated signature [data] - + #### Sample call ```json { @@ -407,7 +381,7 @@ Response #### Result - calculated signature [data] - + #### Sample call ```json { @@ -505,7 +479,7 @@ Derive the address from the account that was used to sign data with content type #### Result - derived account [address] - + #### Sample call ```json { @@ -534,16 +508,16 @@ Response #### 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) + - 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 { @@ -594,14 +568,14 @@ Response #### 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 { @@ -643,8 +617,6 @@ Response } ``` - - ## UI API These methods needs to be implemented by a UI listener. @@ -655,7 +627,7 @@ See `pythonsigner`, which can be invoked via `python3 pythonsigner.py test` to p 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. +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 @@ -909,7 +881,7 @@ TLDR; Use this method to keep track of signed transactions, instead of using the ### OnSignerStartup / `ui_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, +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: @@ -953,9 +925,9 @@ A UI should conform to the following rules. along with the UI. -### UI Implementations +### UI Implementations -There are a couple of implementation for a UI. We'll try to keep this list up to date. +There are a couple of implementation for a UI. We'll try to keep this list up to date. | Name | Repo | UI type| No external resources| Blocky support| Verifies permissions | Hash information | No secondary storage | Statically linked| Can modify parameters| | ---- | ---- | -------| ---- | ---- | ---- |---- | ---- | ---- | ---- | diff --git a/cmd/clef/datatypes.md b/cmd/clef/datatypes.md index a26c1c9375..5ebf9adc97 100644 --- a/cmd/clef/datatypes.md +++ b/cmd/clef/datatypes.md @@ -11,7 +11,7 @@ Example: "content_type": "text/plain", "address": "0xDEADbEeF000000000000000000000000DeaDbeEf", "raw_data": "GUV0aGVyZXVtIFNpZ25lZCBNZXNzYWdlOgoxMWhlbGxvIHdvcmxk", - "message": [ + "messages": [ { "name": "message", "value": "\u0019Ethereum Signed Message:\n11hello world", @@ -133,7 +133,7 @@ This occurs _after_ successful completion of the entire signing procedure, but r A ruleset that implements a rate limitation needs to know what transactions are sent out to the external interface. By hooking into this methods, the ruleset can maintain track of that count. -**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. +**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. The `OnApproved` method cannot be responded to, it's purely informative @@ -179,7 +179,7 @@ Example: ``` ### ListRequest -Sent when a request has been made to list addresses. The UI is provided with the full `account`s, including local directory names. Note: this information is not passed back to the external caller, who only sees the `address`es. +Sent when a request has been made to list addresses. The UI is provided with the full `account`s, including local directory names. Note: this information is not passed back to the external caller, who only sees the `address`es. Example: ```json diff --git a/cmd/clef/extapi_changelog.md b/cmd/clef/extapi_changelog.md index dc7e65c01b..dbc302631b 100644 --- a/cmd/clef/extapi_changelog.md +++ b/cmd/clef/extapi_changelog.md @@ -1,4 +1,15 @@ -### Changelog for external API +## Changelog for external API + +The API uses [semantic versioning](https://semver.org/). + +TL;DR: 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. + ### 6.0.0 @@ -14,15 +25,15 @@ The addition of `contentType` makes it possible to use the method for different * signing clique headers, * signing plain personal messages, * The external method `account_signTypedData` implements [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) and makes it possible to sign typed data. - + #### 4.0.0 -* The external `account_Ecrecover`-method was removed. +* The external `account_Ecrecover`-method was removed. * The external `account_Import`-method was removed. #### 3.0.0 -* The external `account_List`-method was changed to not expose `url`, which contained info about the local filesystem. It now returns only a list of addresses. +* The external `account_List`-method was changed to not expose `url`, which contained info about the local filesystem. It now returns only a list of addresses. #### 2.0.0 @@ -33,15 +44,3 @@ 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. diff --git a/cmd/clef/intapi_changelog.md b/cmd/clef/intapi_changelog.md index b7d4937cdd..38424f06b9 100644 --- a/cmd/clef/intapi_changelog.md +++ b/cmd/clef/intapi_changelog.md @@ -1,24 +1,39 @@ -### Changelog for internal API (ui-api) +## Changelog for internal API (ui-api) -### 6.0.0 +The API uses [semantic versioning](https://semver.org/). -Removed `password` from responses to operations which require them. This is for two reasons, +TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the: -- Consistency between how rulesets operate and how manual processing works. A rule can `Approve` but require the actual password to be stored in the clef storage. -With this change, the same stored password can be used even if rulesets are not enabled, but storage is. -- It also removes the usability-shortcut that a UI might otherwise want to implement; remembering passwords. Since we now will not require the -password on every `Approve`, there's no need for the UI to cache it locally. - - In a future update, we'll likely add `clef_storePassword` to the internal API, so the user can store it via his UI (currently only CLI works). +* 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. + +### 7.0.0 + +- The `message` field was renamed to `messages` in all data signing request methods to better reflect that it's a list, not a value. +- The `storage.Put` and `storage.Get` methods in the rule execution engine were lower-cased to `storage.put` and `storage.get` to be consistent with JavaScript call conventions. + +### 6.0.0 + +Removed `password` from responses to operations which require them. This is for two reasons, + +- Consistency between how rulesets operate and how manual processing works. A rule can `Approve` but require the actual password to be stored in the clef storage. +With this change, the same stored password can be used even if rulesets are not enabled, but storage is. +- It also removes the usability-shortcut that a UI might otherwise want to implement; remembering passwords. Since we now will not require the +password on every `Approve`, there's no need for the UI to cache it locally. + - In a future update, we'll likely add `clef_storePassword` to the internal API, so the user can store it via his UI (currently only CLI works). Affected datatypes: - `SignTxResponse` - `SignDataResponse` - `NewAccountResponse` -If `clef` requires a password, the `OnInputRequired` will be used to collect it. +If `clef` requires a password, the `OnInputRequired` will be used to collect it. -### 5.0.0 +### 5.0.0 Changed the namespace format to adhere to the legacy ethereum format: `name_methodName`. Changes: @@ -38,7 +53,7 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth ### 4.0.0 * Bidirectional communication implemented, so the UI can query `clef` via the stdin/stdout RPC channel. Methods implemented are: - - `clef_listWallets` + - `clef_listWallets` - `clef_listAccounts` - `clef_listWallets` - `clef_deriveAccount` @@ -48,10 +63,10 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth - `clef_setChainId` - `clef_export` - `clef_import` - -* The type `Account` was modified (the json-field `type` was removed), to consist of -```golang +* The type `Account` was modified (the json-field `type` was removed), to consist of + +```go type Account struct { Address common.Address `json:"address"` // Ethereum account address derived from the key URL URL `json:"url"` // Optional resource locator within a backend @@ -64,7 +79,7 @@ type Account struct { * Make `ShowError`, `OnApprovedTx`, `OnSignerStartup` be json-rpc [notifications](https://www.jsonrpc.org/specification#notification): > A Notification is a Request object without an "id" member. A Request object that is a Notification signifies the Client's lack of interest in the corresponding Response object, and as such no Response object needs to be returned to the client. The Server MUST NOT reply to a Notification, including those that are within a batch request. -> +> > Notifications are not confirmable by definition, since they do not have a Response object to be returned. As such, the Client would not be aware of any errors (like e.g. "Invalid params","Internal error" ### 3.1.0 @@ -79,15 +94,17 @@ type Account struct { * Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords. The following structures are used: -```golang - UserInputRequest struct { - Prompt string `json:"prompt"` - Title string `json:"title"` - IsPassword bool `json:"isPassword"` - } - UserInputResponse struct { - Text string `json:"text"` - } + +```go +UserInputRequest struct { + Prompt string `json:"prompt"` + Title string `json:"title"` + IsPassword bool `json:"isPassword"` +} +UserInputResponse struct { + Text string `json:"text"` +} +``` ### 2.0.0 @@ -161,15 +178,3 @@ Example call: #### 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. diff --git a/cmd/clef/main.go b/cmd/clef/main.go index 0ea6f36f10..f4d94f0274 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -93,7 +93,7 @@ var ( chainIdFlag = cli.Int64Flag{ Name: "chainid", Value: params.MainnetChainConfig.ChainID.Int64(), - Usage: "Chain id to use for signing (1=mainnet, 3=ropsten, 4=rinkeby, 5=Goerli)", + Usage: "Chain id to use for signing (1=mainnet, 3=Ropsten, 4=Rinkeby, 5=Goerli)", } rpcPortFlag = cli.IntFlag{ Name: "rpcport", @@ -116,8 +116,7 @@ var ( } ruleFlag = cli.StringFlag{ Name: "rules", - Usage: "Enable rule-engine", - Value: "", + Usage: "Path to the rule file to auto-authorize requests with", } stdiouiFlag = cli.BoolFlag{ Name: "stdio-ui", @@ -160,7 +159,6 @@ 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.`, } - setCredentialCommand = cli.Command{ Action: utils.MigrateFlags(setCredential), Name: "setpw", @@ -172,8 +170,20 @@ Clef that the file is 'safe' to execute.`, signerSecretFlag, }, Description: ` -The setpw command stores a password for a given address (keyfile). If you enter a blank passphrase, it will -remove any stored credential for that address (keyfile) +The setpw command stores a password for a given address (keyfile). +`} + delCredentialCommand = cli.Command{ + Action: utils.MigrateFlags(removeCredential), + Name: "delpw", + Usage: "Remove a credential for a keystore file", + ArgsUsage: "
", + Flags: []cli.Flag{ + logLevelFlag, + configdirFlag, + signerSecretFlag, + }, + Description: ` +The delpw command removes a password for a given address (keyfile). `} gendocCommand = cli.Command{ Action: GenDoc, @@ -210,9 +220,9 @@ func init() { advancedMode, } app.Action = signer - app.Commands = []cli.Command{initCommand, attestCommand, setCredentialCommand, gendocCommand} - + app.Commands = []cli.Command{initCommand, attestCommand, setCredentialCommand, delCredentialCommand, gendocCommand} } + func main() { if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) @@ -221,11 +231,20 @@ func main() { } func initializeSecrets(c *cli.Context) error { + // Get past the legal message if err := initialize(c); err != nil { return err } + // Ensure the master key does not yet exist, we're not willing to overwrite configDir := c.GlobalString(configdirFlag.Name) - + if err := os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { + return err + } + location := filepath.Join(configDir, "masterseed.json") + if _, err := os.Stat(location); err == nil { + return fmt.Errorf("master key %v already exists, will not overwrite", location) + } + // Key file does not exist yet, generate a new one and encrypt it masterSeed := make([]byte, 256) num, err := io.ReadFull(rand.Reader, masterSeed) if err != nil { @@ -234,18 +253,18 @@ func initializeSecrets(c *cli.Context) error { if num != len(masterSeed) { return fmt.Errorf("failed to read enough random") } - n, p := keystore.StandardScryptN, keystore.StandardScryptP if c.GlobalBool(utils.LightKDFFlag.Name) { n, p = keystore.LightScryptN, keystore.LightScryptP } - text := "The master seed of clef is locked with a password. Please give a password. Do not forget this password." + text := "The master seed of clef will be locked with a password.\nPlease specify a password. Do not forget this password!" var password string for { password = getPassPhrase(text, true) if err := core.ValidatePasswordFormat(password); err != nil { fmt.Printf("invalid password: %v\n", err) } else { + fmt.Println() break } } @@ -253,28 +272,27 @@ func initializeSecrets(c *cli.Context) error { if err != nil { return fmt.Errorf("failed to encrypt master seed: %v", err) } - - err = os.Mkdir(configDir, 0700) - if err != nil && !os.IsExist(err) { + // Double check the master key path to ensure nothing wrote there in between + if err = os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { return err } - location := filepath.Join(configDir, "masterseed.json") if _, err := os.Stat(location); err == nil { - return fmt.Errorf("file %v already exists, will not overwrite", location) + return fmt.Errorf("master key %v already exists, will not overwrite", location) } - err = ioutil.WriteFile(location, cipherSeed, 0400) - if err != nil { + // Write the file and print the usual warning message + if err = ioutil.WriteFile(location, cipherSeed, 0400); 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 : +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 +* Storage for JavaScript auto-signing rules +* Hash of JavaScript 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! +You should treat 'masterseed.json' with utmost secrecy and make a backup of it! +* The password is necessary but not enough, you need to back up the master seed too! +* The master seed does not contain your accounts, those need to be backed up separately! `) return nil @@ -305,14 +323,18 @@ func attestFile(ctx *cli.Context) error { func setCredential(ctx *cli.Context) error { if len(ctx.Args()) < 1 { - utils.Fatalf("This command requires an address to be passed as an argument.") + utils.Fatalf("This command requires an address to be passed as an argument") } if err := initialize(ctx); err != nil { return err } - - address := ctx.Args().First() - password := getPassPhrase("Enter a passphrase to store with this address.", true) + addr := ctx.Args().First() + if !common.IsHexAddress(addr) { + utils.Fatalf("Invalid address specified: %s", addr) + } + address := common.HexToAddress(addr) + password := getPassPhrase("Please enter a passphrase to store for this address:", true) + fmt.Println() stretchedKey, err := readMasterKey(ctx, nil) if err != nil { @@ -322,10 +344,38 @@ func setCredential(ctx *cli.Context) error { 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) - pwStorage.Put(address, password) - log.Info("Credential store updated", "key", address) + pwStorage.Put(address.Hex(), password) + + log.Info("Credential store updated", "set", address) + return nil +} + +func removeCredential(ctx *cli.Context) error { + if len(ctx.Args()) < 1 { + utils.Fatalf("This command requires an address to be passed as an argument") + } + if err := initialize(ctx); err != nil { + return err + } + addr := ctx.Args().First() + if !common.IsHexAddress(addr) { + utils.Fatalf("Invalid address specified: %s", addr) + } + address := common.HexToAddress(addr) + + stretchedKey, err := readMasterKey(ctx, nil) + if err != nil { + utils.Fatalf(err.Error()) + } + configDir := ctx.GlobalString(configdirFlag.Name) + vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) + pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) + + pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) + pwStorage.Del(address.Hex()) + + log.Info("Credential store updated", "unset", address) return nil } @@ -340,13 +390,17 @@ func initialize(c *cli.Context) error { if !confirm(legalWarning) { return fmt.Errorf("aborted by user") } + fmt.Println() } - 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 we have some unrecognized command, bail out + if args := c.Args(); len(args) > 0 { + return fmt.Errorf("invalid command: %q", args[0]) + } if err := initialize(c); err != nil { return err } @@ -376,7 +430,7 @@ func signer(c *cli.Context) error { configDir := c.GlobalString(configdirFlag.Name) if stretchedKey, err := readMasterKey(c, ui); err != nil { - log.Info("No master seed provided, rules disabled", "error", err) + log.Warn("Failed to open master, rules disabled", "err", err) } else { vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) @@ -390,17 +444,17 @@ func signer(c *cli.Context) error { jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) - //Do we have a rule-file? + // Do we have a rule-file? if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" { - ruleJS, err := ioutil.ReadFile(c.GlobalString(ruleFile)) + ruleJS, err := ioutil.ReadFile(ruleFile) if err != nil { - log.Info("Could not load rulefile, rules not enabled", "file", "rulefile") + log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err) } else { shasum := sha256.Sum256(ruleJS) foundShaSum := hex.EncodeToString(shasum[:]) - storedShasum := configStorage.Get("ruleset_sha256") + storedShasum, _ := configStorage.Get("ruleset_sha256") if storedShasum != foundShaSum { - log.Info("Could not validate ruleset hash, rules not enabled", "got", foundShaSum, "expected", storedShasum) + log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum) } else { // Initialize rules ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage) @@ -452,7 +506,6 @@ func signer(c *cli.Context) error { Version: "1.0"}, } if c.GlobalBool(utils.RPCEnabledFlag.Name) { - vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) @@ -469,7 +522,6 @@ func signer(c *cli.Context) error { listener.Close() log.Info("HTTP endpoint closed", "url", httpEndpoint) }() - } if !c.GlobalBool(utils.IPCDisabledFlag.Name) { if c.IsSet(utils.IPCPathFlag.Name) { @@ -496,8 +548,8 @@ func signer(c *cli.Context) error { } ui.OnSignerStartup(core.StartupInfo{ Info: map[string]interface{}{ - "extapi_version": core.ExternalAPIVersion, "intapi_version": core.InternalAPIVersion, + "extapi_version": core.ExternalAPIVersion, "extapi_http": extapiURL, "extapi_ipc": ipcapiURL, }, @@ -592,7 +644,6 @@ func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) { if len(masterSeed) < 256 { return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) } - // Create vault location vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) err = os.Mkdir(vaultLocation, 0700) @@ -620,13 +671,12 @@ func checkFile(filename string) error { // confirm displays a text and asks for user confirmation func confirm(text string) bool { fmt.Printf(text) - fmt.Printf("\nEnter 'ok' to proceed:\n>") + 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 } @@ -642,7 +692,7 @@ func testExternalUI(api *core.SignerAPI) { a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") addErr := func(errStr string) { - log.Info("Test error", "error", errStr) + log.Info("Test error", "err", errStr) errs = append(errs, errStr) } @@ -864,14 +914,14 @@ func GenDoc(ctx *cli.Context) { "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + "the user with the contents of the `message`" sighash, msg := accounts.TextAndHash([]byte("hello world")) - message := []*core.NameValueType{{"message", msg, accounts.MimetypeTextPlain}} + messages := []*core.NameValueType{{"message", msg, accounts.MimetypeTextPlain}} add("SignDataRequest", desc, &core.SignDataRequest{ Address: common.NewMixedcaseAddress(a), Meta: meta, ContentType: accounts.MimetypeTextPlain, Rawdata: []byte(msg), - Message: message, + Messages: messages, Hash: sighash}) } { // Sign plain text response @@ -982,29 +1032,3 @@ These data types are defined in the channel between clef and the UI`) fmt.Println(elem) } } - -/** -//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/ - - -**/ diff --git a/cmd/clef/pythonsigner.py b/cmd/clef/pythonsigner.py index 46fa23bd8c..315aabd73f 100644 --- a/cmd/clef/pythonsigner.py +++ b/cmd/clef/pythonsigner.py @@ -42,7 +42,6 @@ class PipeTransport(ServerTransport): self.output.write("\n") class StdIOHandler(): - def __init__(self): pass @@ -76,7 +75,7 @@ class StdIOHandler(): :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: + :return: """ transaction = req.get('transaction') _from = req.get('from') @@ -158,8 +157,7 @@ class StdIOHandler(): return def main(args): - - cmd = ["./clef", "--stdio-ui"] + cmd = ["clef", "--stdio-ui"] if len(args) > 0 and args[0] == "test": cmd.extend(["--stdio-ui-test"]) print("cmd: {}".format(" ".join(cmd))) diff --git a/cmd/clef/rules.md b/cmd/clef/rules.md index 327ba765c5..153e69ff72 100644 --- a/cmd/clef/rules.md +++ b/cmd/clef/rules.md @@ -19,32 +19,30 @@ The section below deals with both of them 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) +```js +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") +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 + 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 +// 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 @@ -140,97 +138,97 @@ This is now implemented (with ephemeral non-encrypted storage for now, so not ye ## 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) +```js +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; +// Time window: 1 week +var window = 1000* 3600*24*7; - // Limit : 1 ether - var limit = new BigNumber("1e18"); +// 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; +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) + var txs = []; + var stored = storage.get('txs'); + if (stored != "") { + txs = JSON.parse(stored) } - function ApproveTx(r){ - if (isLimitOk(r.transaction)){ - return "Approve" - } - return "Nope" - } + // 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); - /** - * 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)); - } + // 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 +```js +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" - } - -``` \ No newline at end of file +```js +function ApproveListing() { + return "Approve" +} +``` diff --git a/cmd/clef/tutorial.md b/cmd/clef/tutorial.md index dfb31ba4ef..a0a6b052af 100644 --- a/cmd/clef/tutorial.md +++ b/cmd/clef/tutorial.md @@ -1,200 +1,278 @@ -## Initializing the signer +## Initializing Clef -First, initialize the master seed. +First thing's first, Clef needs to store some data itself. Since that data might be sensitive (passwords, signing rules, accounts), Clef's entire storage is encrypted. To support encrypting data, the first step is to initialize Clef with a random master seed, itself too encrypted with your chosen password: ```text -#./signer init +$ clef 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. +Clef is an account management tool. It may, like any software, contain bugs. -TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE! +Please take care to +- backup your keystore files, +- verify that the keystore(s) can be opened with your password. +Clef 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. Enter 'ok' to proceed: ->ok -A master seed has been generated into /home/martin/.signer/secrets.dat +> ok -This is required to be able to store credentials, such as : +The master seed of clef will be locked with a password. +Please specify a password. Do not forget this password! +Passphrase: +Repeat passphrase: + +A master seed has been generated into /home/martin/.clef/masterseed.json + +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 +* Storage for JavaScript auto-signing rules +* Hash of JavaScript 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! +You should treat 'masterseed.json' with utmost secrecy and make a backup of it! +* The password is necessary but not enough, you need to back up the master seed too! +* The master seed 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) +*For readability purposes, we'll remove the WARNING printout, user confirmation and the unlocking of the master seed in the rest of this document.* -## Creating rules +## Remote interactions -Now, you can create a rule-file. Note that it is not mandatory to use predefined rules, but it's really handy. +Clef is capable of managing both key-file based accounts as well as hardware wallets. To evaluate clef, we're going to point it to our Rinkeby testnet keystore and specify the Rinkeby chain ID for signing (Clef doesn't have a backing chain, so it doesn't know what network it runs on). -```javascript -function ApproveListing(){ +```text +$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 + +INFO [07-01|11:00:46.385] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false +DEBUG[07-01|11:00:46.389] FS scan times list=3.521941ms set=9.017µs diff=4.112µs +DEBUG[07-01|11:00:46.391] Ledger support enabled +DEBUG[07-01|11:00:46.391] Trezor support enabled via HID +DEBUG[07-01|11:00:46.391] Trezor support enabled via WebUSB +INFO [07-01|11:00:46.391] Audit logs configured file=audit.log +DEBUG[07-01|11:00:46.392] IPC registered namespace=account +INFO [07-01|11:00:46.392] IPC endpoint opened url=$HOME/.clef/clef.ipc +------- Signer info ------- +* intapi_version : 7.0.0 +* extapi_version : 6.0.0 +* extapi_http : n/a +* extapi_ipc : $HOME/.clef/clef.ipc +``` + +By default, Clef starts up in CLI (Command Line Interface) mode. Arbitrary remote processes may *request* account interactions (e.g. sign a transaction), which the user will need to individually *confirm*. + +To test this out, we can *request* Clef to list all account via its *External API endpoint*: + +```text +echo '{"id": 1, "jsonrpc": "2.0", "method": "account_list"}' | nc -U ~/.clef/clef.ipc +``` + +This will prompt the user within the Clef CLI to confirm or deny the request: + +```text +-------- List Account request-------------- +A request has been made to list all accounts. +You can select which accounts the caller can see + [x] 0xD9C9Cd5f6779558b6e0eD4e6Acf6b1947E7fA1F3 + URL: keystore://$HOME/.ethereum/rinkeby/keystore/UTC--2017-04-14T15-15-00.327614556Z--d9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 + [x] 0x086278A6C067775F71d6B2BB1856Db6E28c30418 + URL: keystore://$HOME/.ethereum/rinkeby/keystore/UTC--2018-02-06T22-53-11.211657239Z--086278a6c067775f71d6b2bb1856db6e28c30418 +------------------------------------------- +Request context: + NA -> NA -> NA + +Additional HTTP header data, provided by the external caller: + User-Agent: + Origin: +Approve? [y/N]: +> +``` + +Depending on whether we approve or deny the request, the original NetCat process will get: + +```text +{"jsonrpc":"2.0","id":1,"result":["0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3","0x086278a6c067775f71d6b2bb1856db6e28c30418"]} + +or + +{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Request denied"}} +``` + +Apart from listing accounts, you can also *request* creating a new account; signing transactions and data; and recovering signatures. You can find the available methods in the Clef [External API Spec](https://github.com/ethereum/go-ethereum/tree/master/cmd/clef#external-api-1) and the [External API Changelog](https://github.com/ethereum/go-ethereum/blob/master/cmd/clef/extapi_changelog.md). + +*Note, the number of things you can do from the External API is deliberately small, since we want to limit the power of remote calls by as much as possible! Clef has an [Internal API](https://github.com/ethereum/go-ethereum/tree/master/cmd/clef#ui-api-1) too for the UI (User Interface) which is much richer and can support custom interfaces on top. But that's out of scope here.* + +## Automatic rules + +For most users, manually confirming every transaction is the way to go. However, there are cases when it makes sense to set up some rules which permit Clef to sign a transaction without prompting the user. One such example would be running a signer on Rinkeby or other PoA networks. + +For starters, we can create a rule file that automatically permits anyone to list our available accounts without user confirmation. The rule file is a tiny JavaScript snippet that you can program however you want: + +```js +function ApproveListing() { return "Approve" } ``` -Get the `sha256` hash. If you have openssl, you can do `openssl sha256 rules.js`... -```text -#sha256sum rules.js -6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 rules.js -``` -...now `attest` the file... -```text -#./signer attest 6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 +Of course, Clef isn't going to just accept and run arbitrary scripts you give it, that would be dangerous if someone changes your rule file! Instead, you need to explicitly *attest* the rule file, which entails injecting its hash into Clef's secure store. -INFO [02-21|12:14:38] Ruleset attestation updated sha256=6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 +```text +$ sha256sum rules.js +645b58e4f945e24d0221714ff29f6aa8e860382ced43490529db1695f5fcc71c rules.js + +$ clef attest 645b58e4f945e24d0221714ff29f6aa8e860382ced43490529db1695f5fcc71c +Decrypt master seed of clef +Passphrase: +INFO [07-01|13:25:03.290] Ruleset attestation updated sha256=645b58e4f945e24d0221714ff29f6aa8e860382ced43490529db1695f5fcc71c ``` -...and (this is required only for non-production versions) load a mock-up `4byte.json` by copying the file from the source to your current working directory: -```text -#cp $GOPATH/src/github.com/ethereum/go-ethereum/cmd/clef/4byte.json $PWD -``` +At this point, we can start Clef with the rule file: -At this point, we can start the signer with the rule-file: ```text -#./signer --rules rules.js --rpc +$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 --rules rules.js -INFO [09-25|20:28:11.866] Using CLI as UI-channel -INFO [09-25|20:28:11.876] Loaded 4byte db signatures=5509 file=./4byte.json -INFO [09-25|20:28:11.877] Rule engine configured file=./rules.js -DEBUG[09-25|20:28:11.877] FS scan times list=100.781µs set=13.253µs diff=5.761µs -DEBUG[09-25|20:28:11.884] Ledger support enabled -DEBUG[09-25|20:28:11.888] Trezor support enabled -INFO [09-25|20:28:11.888] Audit logs configured file=audit.log -DEBUG[09-25|20:28:11.888] HTTP registered namespace=account -INFO [09-25|20:28:11.890] HTTP endpoint opened url=http://localhost:8550 -DEBUG[09-25|20:28:11.890] IPC registered namespace=account -INFO [09-25|20:28:11.890] IPC endpoint opened url= +INFO [07-01|13:39:49.726] Rule engine configured file=rules.js +INFO [07-01|13:39:49.726] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false +DEBUG[07-01|13:39:49.726] FS scan times list=35.15µs set=4.251µs diff=2.766µs +DEBUG[07-01|13:39:49.727] Ledger support enabled +DEBUG[07-01|13:39:49.727] Trezor support enabled via HID +DEBUG[07-01|13:39:49.727] Trezor support enabled via WebUSB +INFO [07-01|13:39:49.728] Audit logs configured file=audit.log +DEBUG[07-01|13:39:49.728] IPC registered namespace=account +INFO [07-01|13:39:49.728] IPC endpoint opened url=$HOME/.clef/clef.ipc ------- Signer info ------- -* extapi_version : 2.0.0 -* intapi_version : 2.0.0 -* extapi_http : http://localhost:8550 -* extapi_ipc : +* intapi_version : 7.0.0 +* extapi_version : 6.0.0 +* extapi_http : n/a +* extapi_ipc : $HOME/.clef/clef.ipc ``` -Any list-requests will now be auto-approved by our rule-file. +Any account listing *request* will now be auto-approved by the rule file: + +```text +$ echo '{"id": 1, "jsonrpc": "2.0", "method": "account_list"}' | nc -U ~/.clef/clef.ipc +{"jsonrpc":"2.0","id":1,"result":["0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3","0x086278a6c067775f71d6b2bb1856db6e28c30418"]} +``` ## 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 +$ ls -laR ~/.clef/ -/home/martin/.signer/43f73718397aa54d1b22: +$HOME/.clef/: +total 24 +drwxr-x--x 3 user user 4096 Jul 1 13:45 . +drwxr-xr-x 102 user user 12288 Jul 1 13:39 .. +drwx------ 2 user user 4096 Jul 1 13:25 02f90c0603f4f2f60188 +-r-------- 1 user user 868 Jun 28 13:55 masterseed.json + +$HOME/.clef/02f90c0603f4f2f60188: 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="}} +drwx------ 2 user user 4096 Jul 1 13:25 . +drwxr-x--x 3 user user 4096 Jul 1 13:45 .. +-rw------- 1 user user 159 Jul 1 13:25 config.json +$ cat ~/.clef/02f90c0603f4f2f60188/config.json +{"ruleset_sha256":{"iv":"SWWEtnl+R+I+wfG7","c":"I3fjmwmamxVcfGax7D0MdUOL29/rBWcs73WBILmYK0o1CrX7wSMc3y37KsmtlZUAjp0oItYq01Ow8VGUOzilG91tDHInB5YHNtm/YkufEbo="}} ``` -In `~/.signer`, the `secrets.dat` file was created, containing the `master_seed`. -The `master_seed` was then used to derive a few other things: +In `$HOME/.clef`, the `masterseed.json` file was created, containing the master seed. This 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`. +- **Vault location**: in this case `02f90c0603f4f2f60188`. + - If you use a different master seed, a different vault location will be used that does not conflict with each other (e.g. `clef --signersecret /path/to/file`). This allows you to run multiple instances of Clef, each with its own rules (e.g. mainnet + testnet). +- **`config.json`**: the encrypted key/value storage for configuration data, currently only containing the key `ruleset_sha256`, the attested hash of the automatic rules to use. +## Advanced rules -## Adding credentials - -In order to make more useful rules like signing transactions, the signer needs access to the passwords needed to unlock keystores. +In order to make more useful rules - like signing transactions - the signer needs access to the passwords needed to unlock keys from the keystore. You can inject an unlock password via `clef setpw`. ```text -#./signer addpw "0x694267f14675d7e1b9494fd8d72fefe1755710fa" "test_password" +$ clef setpw 0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 -INFO [02-21|13:43:21] Credential store updated key=0x694267f14675d7e1b9494fd8d72fefe1755710fa +Please enter a passphrase to store for this address: +Passphrase: +Repeat passphrase: + +Decrypt master seed of clef +Passphrase: +INFO [07-01|14:05:56.031] Credential store updated key=0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 ``` -## More advanced rules -Now let's update the rules to make use of credentials: +Now let's update the rules to make use of the new credentials: -```javascript -function ApproveListing(){ +```js +function ApproveListing() { return "Approve" } -function ApproveSignData(r){ - if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") - { - if(r.message.indexOf("bazonk") >= 0){ + +function ApproveSignData(req) { + if (req.address.toLowerCase() == "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3") { + if (req.messages[0].value.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` - * auto-rejected if it does not. -* Any other signing-requests will be passed along for manual approve/reject. -_Note: make sure that `0x694...` is an account you have access to. You can create it either via the clef or the traditional account cli tool. If the latter was chosen, make sure both clef and geth use the same keystore by specifing `--keystore path/to/your/keystore` when running clef._ +- Any requests to sign data with the account `0xd9c9...` will be: + - Auto-approved if the message contains `bazonk`, + - Auto-rejected if the message does not contain `bazonk`, +- Any other requests will be passed along for manual confirmation. + +*Note, to make this example work, please use you own accounts. You can create a new account either via Clef or the traditional account CLI tools. If the latter was chosen, make sure both Clef and Geth use the same keystore by specifing `--keystore path/to/your/keystore` when running Clef.* + +Attest the new rule file so that Clef will accept loading it: -Attest the new file... ```text -#sha256sum rules.js -2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f rules.js +$ sha256sum rules.js +f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178 rules.js -#./signer attest 2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f - -INFO [02-21|14:36:30] Ruleset attestation updated sha256=2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f +$ clef attest f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178 +Decrypt master seed of clef +Passphrase: +INFO [07-01|14:11:28.509] Ruleset attestation updated sha256=f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178 ``` -And start the signer: +Restart Clef with the new rules in place: ``` -#./signer --rules rules.js --rpc +$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 --rules rules.js -INFO [09-25|21:02:16.450] Using CLI as UI-channel -INFO [09-25|21:02:16.466] Loaded 4byte db signatures=5509 file=./4byte.json -INFO [09-25|21:02:16.467] Rule engine configured file=./rules.js -DEBUG[09-25|21:02:16.468] FS scan times list=1.45262ms set=21.926µs diff=6.944µs -DEBUG[09-25|21:02:16.473] Ledger support enabled -DEBUG[09-25|21:02:16.475] Trezor support enabled -INFO [09-25|21:02:16.476] Audit logs configured file=audit.log -DEBUG[09-25|21:02:16.476] HTTP registered namespace=account -INFO [09-25|21:02:16.478] HTTP endpoint opened url=http://localhost:8550 -DEBUG[09-25|21:02:16.478] IPC registered namespace=account -INFO [09-25|21:02:16.478] IPC endpoint opened url= +INFO [07-01|14:12:41.636] Rule engine configured file=rules.js +INFO [07-01|14:12:41.636] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false +DEBUG[07-01|14:12:41.636] FS scan times list=46.722µs set=4.47µs diff=2.157µs +DEBUG[07-01|14:12:41.637] Ledger support enabled +DEBUG[07-01|14:12:41.637] Trezor support enabled via HID +DEBUG[07-01|14:12:41.638] Trezor support enabled via WebUSB +INFO [07-01|14:12:41.638] Audit logs configured file=audit.log +DEBUG[07-01|14:12:41.638] IPC registered namespace=account +INFO [07-01|14:12:41.638] IPC endpoint opened url=$HOME/.clef/clef.ipc ------- Signer info ------- -* extapi_version : 2.0.0 -* intapi_version : 2.0.0 -* extapi_http : http://localhost:8550 -* extapi_ipc : +* intapi_version : 7.0.0 +* extapi_version : 6.0.0 +* extapi_http : n/a +* extapi_ipc : $HOME/.clef/clef.ipc ``` -And then test signing, once with `bazonk` and once without: +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"}} +$ echo '{"id": 1, "jsonrpc":"2.0", "method":"account_signData", "params":["data/plain", "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", "0x202062617a6f6e6b2062617a2067617a0a"]}' | nc -U ~/.clef/clef.ipc +{"jsonrpc":"2.0","id":1,"result":"0x4f93e3457027f6be99b06b3392d0ebc60615ba448bb7544687ef1248dea4f5317f789002df783979c417d969836b6fda3710f5bffb296b4d51c8aaae6e2ac4831c"} +$ echo '{"id": 1, "jsonrpc":"2.0", "method":"account_signData", "params":["data/plain", "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", "0x2020626f6e6b2062617a2067617a0a"]}' | nc -U ~/.clef/clef.ipc +{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Request denied"}} ``` -Meanwhile, in the signer output: +Meanwhile, in the Clef output log you can see: ```text INFO [02-21|14:42:41] Op approved INFO [02-21|14:42:56] Op rejected @@ -203,9 +281,73 @@ 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 -n 4 audit.log -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" -``` \ No newline at end of file +$ tail -n 4 audit.log +t=2019-07-01T15:52:14+0300 lvl=info msg=SignData api=signer type=request metadata="{\"remote\":\"NA\",\"local\":\"NA\",\"scheme\":\"NA\",\"User-Agent\":\"\",\"Origin\":\"\"}" addr="0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 [chksum INVALID]" data=0x202062617a6f6e6b2062617a2067617a0a content-type=data/plain +t=2019-07-01T15:52:14+0300 lvl=info msg=SignData api=signer type=response data=4f93e3457027f6be99b06b3392d0ebc60615ba448bb7544687ef1248dea4f5317f789002df783979c417d969836b6fda3710f5bffb296b4d51c8aaae6e2ac4831c error=nil +t=2019-07-01T15:52:23+0300 lvl=info msg=SignData api=signer type=request metadata="{\"remote\":\"NA\",\"local\":\"NA\",\"scheme\":\"NA\",\"User-Agent\":\"\",\"Origin\":\"\"}" addr="0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 [chksum INVALID]" data=0x2020626f6e6b2062617a2067617a0a content-type=data/plain +t=2019-07-01T15:52:23+0300 lvl=info msg=SignData api=signer type=response data= error="Request denied" +``` + +For more details on writing automatic rules, please see the [rules spec](https://github.com/ethereum/go-ethereum/blob/master/cmd/clef/rules.md). + +## Geth integration + +Of course, as awesome as Clef is, it's not feasible to interact with it via JSON RPC by hand. Long term, we're hoping to convince the general Ethereum community to support Clef as a general signer (it's only 3-5 methods), thus allowing your favorite DApp, Metamask, MyCrypto, etc to request signatures directly. + +Until then however, we're trying to pave the way via Geth. Geth v1.9.0 has built in support via `--signer ` for using a local or remote Clef instance as an account backend! + +We can try this by running Clef with our previous rules on Rinkeby (for now it's a good idea to allow auto-listing accounts, since Geth likes to retrieve them once in a while). + +```text +$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 --rules rules.js +``` + +In a different window we can start Geth, list our accounts, even list our wallets to see where the accounts originate from: + +```text +$ geth --rinkeby --signer=~/.clef/clef.ipc console + +> eth.accounts +["0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", "0x086278a6c067775f71d6b2bb1856db6e28c30418"] + +> personal.listWallets +[{ + accounts: [{ + address: "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", + url: "extapi://$HOME/.clef/clef.ipc" + }, { + address: "0x086278a6c067775f71d6b2bb1856db6e28c30418", + url: "extapi://$HOME/.clef/clef.ipc" + }], + status: "ok [version=6.0.0]", + url: "extapi://$HOME/.clef/clef.ipc" +}] + +> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[0]}) +``` + +Lastly, when we requested a transaction to be sent, Clef prompted us in the original window to approve it: + +```text +--------- Transaction request------------- +to: 0xD9C9Cd5f6779558b6e0eD4e6Acf6b1947E7fA1F3 +from: 0xD9C9Cd5f6779558b6e0eD4e6Acf6b1947E7fA1F3 [chksum ok] +value: 0 wei +gas: 0x5208 (21000) +gasprice: 1000000000 wei +nonce: 0x2366 (9062) + +Request context: + NA -> NA -> NA + +Additional HTTP header data, provided by the external caller: + User-Agent: + Origin: +------------------------------------------- +Approve? [y/N]: +> y +``` + +:boom: + +*Note, if you enable the external signer backend in Geth, all other account management is disabled. This is because long term we want to remove account management from Geth.* diff --git a/signer/core/api.go b/signer/core/api.go index 251ee55dcd..d68919c6f4 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -24,7 +24,6 @@ import ( "math/big" "os" "reflect" - "strings" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -44,7 +43,7 @@ const ( // ExternalAPIVersion -- see extapi_changelog.md ExternalAPIVersion = "6.0.0" // InternalAPIVersion -- see intapi_changelog.md - InternalAPIVersion = "6.0.0" + InternalAPIVersion = "7.0.0" ) // ExternalAPI defines the external API through which signing requests are made. @@ -234,7 +233,7 @@ type ( ContentType string `json:"content_type"` Address common.MixedcaseAddress `json:"address"` Rawdata []byte `json:"raw_data"` - Message []*NameValueType `json:"message"` + Messages []*NameValueType `json:"messages"` Hash hexutil.Bytes `json:"hash"` Meta Metadata `json:"meta"` } @@ -477,22 +476,24 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool { return modified } -func (api *SignerAPI) lookupPassword(address common.Address) string { - return api.credentials.Get(strings.ToLower(address.String())) +func (api *SignerAPI) lookupPassword(address common.Address) (string, error) { + return api.credentials.Get(address.Hex()) } + func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) { - if pw := api.lookupPassword(address); pw != "" { + // Look up the password and return if available + if pw, err := api.lookupPassword(address); err == nil { return pw, nil - } else { - pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true}) - if err != nil { - log.Warn("error obtaining password", "error", err) - // We'll not forward the error here, in case the error contains info about the response from the UI, - // which could leak the password if it was malformed json or something - return "", errors.New("internal error") - } - return pwResp.Text, nil } + // Password unavailable, request it from the user + pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true}) + if err != nil { + log.Warn("error obtaining password", "error", err) + // We'll not forward the error here, in case the error contains info about the response from the UI, + // which could leak the password if it was malformed json or something + return "", errors.New("internal error") + } + return pwResp.Text, nil } // SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form diff --git a/signer/core/cliui.go b/signer/core/cliui.go index cf7101441b..46a13f1e4c 100644 --- a/signer/core/cliui.go +++ b/signer/core/cliui.go @@ -169,13 +169,12 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp fmt.Printf("-------- Sign data request--------------\n") fmt.Printf("Account: %s\n", request.Address.String()) - fmt.Printf("message:\n") - for _, nvt := range request.Message { + fmt.Printf("messages:\n") + for _, nvt := range request.Messages { fmt.Printf("%v\n", nvt.Pprint(1)) } - //fmt.Printf("message: \n%v\n", request.Message) fmt.Printf("raw data: \n%q\n", request.Rawdata) - fmt.Printf("message hash: %v\n", request.Hash) + fmt.Printf("data hash: %v\n", request.Hash) fmt.Printf("-------------------------------------------\n") showMetadata(request.Meta) if !ui.confirm() { @@ -187,7 +186,6 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp // 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() diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go index 27eca91831..91e6cc7dac 100644 --- a/signer/core/signed_data.go +++ b/signer/core/signed_data.go @@ -123,11 +123,10 @@ type TypedDataDomain struct { var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`) // sign receives a request and produces a signature - +// // 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, if legacyV==true. func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { - // We make the request prior to looking up if we actually have the account, to prevent // account-enumeration via the API res, err := api.UI.ApproveSignData(req) @@ -169,7 +168,6 @@ func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr com if err != nil { return nil, err } - signature, err := api.sign(addr, req, transformV) if err != nil { api.UI.ShowError(err.Error()) @@ -202,7 +200,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType return nil, useEthereumV, err } sighash, msg := SignTextValidator(validatorData) - message := []*NameValueType{ + messages := []*NameValueType{ { Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)", Typ: "description", @@ -224,7 +222,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType Value: fmt.Sprintf("0x%x", msg), }, } - req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash} + req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} case ApplicationClique.Mime: // Clique is the Ethereum PoA standard stringData, ok := data.(string) @@ -251,7 +249,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType if err != nil { return nil, useEthereumV, err } - message := []*NameValueType{ + messages := []*NameValueType{ { Name: "Clique header", Typ: "clique", @@ -260,7 +258,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType } // Clique uses V on the form 0 or 1 useEthereumV = false - req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Message: message, Hash: sighash} + req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash} default: // also case TextPlain.Mime: // Calculates an Ethereum ECDSA signature for: // hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}") @@ -272,21 +270,20 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType return nil, useEthereumV, err } else { sighash, msg := accounts.TextAndHash(textData) - message := []*NameValueType{ + messages := []*NameValueType{ { Name: "message", Typ: accounts.MimetypeTextPlain, Value: msg, }, } - req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash} + req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} } } } req.Address = addr req.Meta = MetadataFromContext(ctx) return req, useEthereumV, nil - } // SignTextWithValidator signs the given message which can be further recovered @@ -327,11 +324,11 @@ func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAd } rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) sighash := crypto.Keccak256(rawData) - message, err := typedData.Format() + messages, err := typedData.Format() if err != nil { return nil, err } - req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Message: message, Hash: sighash} + req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Messages: messages, Hash: sighash} signature, err := api.sign(addr, req, true) if err != nil { api.UI.ShowError(err.Error()) diff --git a/signer/rules/rules.go b/signer/rules/rules.go index 5ebffa3af6..f731dac40a 100644 --- a/signer/rules/rules.go +++ b/signer/rules/rules.go @@ -74,12 +74,28 @@ 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) + vm.Set("storage", struct{}{}) + storageObj, _ := vm.Get("storage") + storageObj.Object().Set("put", func(call otto.FunctionCall) otto.Value { + key, val := call.Argument(0).String(), call.Argument(1).String() + if val == "" { + r.storage.Del(key) + } else { + r.storage.Put(key, val) + } + return otto.NullValue() + }) + storageObj.Object().Set("get", func(call otto.FunctionCall) otto.Value { + goval, _ := r.storage.Get(call.Argument(0).String()) + jsval, _ := otto.ToValue(goval) + return jsval + }) // Load bootstrap libraries script, err := vm.Compile("bignumber.js", BigNumber_JS) if err != nil { diff --git a/signer/rules/rules_test.go b/signer/rules/rules_test.go index 19d9049c72..e1a23236cd 100644 --- a/signer/rules/rules_test.go +++ b/signer/rules/rules_test.go @@ -301,23 +301,23 @@ func TestStorage(t *testing.T) { js := ` function testStorage(){ - storage.Put("mykey", "myvalue") - a = storage.Get("mykey") + 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", ["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", {"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") + 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 '' + 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) @@ -337,7 +337,6 @@ func TestStorage(t *testing.T) { if err != nil { t.Errorf("Unexpected error %v", err) } - retval, err := v.ToString() if err != nil { @@ -369,7 +368,7 @@ const ExampleTxWindow = ` var windowstart = new Date().getTime() - window; var txs = []; - var stored = storage.Get('txs'); + var stored = storage.get('txs'); if(stored != ""){ txs = JSON.parse(stored) @@ -414,19 +413,18 @@ const ExampleTxWindow = ` var value = big(resp.tx.value) var txs = [] // Load stored transactions - var stored = storage.Get('txs'); + 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)); + storage.put("txs", JSON.stringify(txs)); } ` func dummyTx(value hexutil.Big) *core.SignTxRequest { - to, _ := mixAddr("000000000000000000000000000000000000dead") from, _ := mixAddr("000000000000000000000000000000000000dead") n := hexutil.Uint64(3) @@ -448,28 +446,27 @@ func dummyTx(value hexutil.Big) *core.SignTxRequest { Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, } } -func dummyTxWithV(value uint64) *core.SignTxRequest { +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) { +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) @@ -496,7 +493,6 @@ func TestLimitWindow(t *testing.T) { 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 @@ -508,6 +504,7 @@ func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInput d.t.Fatalf("Did not expect next-handler to be called") return core.UserInputResponse{}, nil } + func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) { } @@ -589,7 +586,7 @@ func TestSignData(t *testing.T) { function ApproveSignData(r){ if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") { - if(r.message[0].value.indexOf("bazonk") >= 0){ + if(r.messages[0].value.indexOf("bazonk") >= 0){ return "Approve" } return "Reject" @@ -615,11 +612,11 @@ function ApproveSignData(r){ }, } resp, err := r.ApproveSignData(&core.SignDataRequest{ - Address: *addr, - Message: nvt, - Hash: hash, - Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, - Rawdata: []byte(rawdata), + Address: *addr, + Messages: nvt, + Hash: hash, + Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, + Rawdata: []byte(rawdata), }) if err != nil { t.Fatalf("Unexpected error %v", err) diff --git a/signer/storage/aes_gcm_storage.go b/signer/storage/aes_gcm_storage.go index 9008318679..e6a8f145cc 100644 --- a/signer/storage/aes_gcm_storage.go +++ b/signer/storage/aes_gcm_storage.go @@ -53,7 +53,7 @@ func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage { } } -// Put stores a value by key. 0-length keys results in no-op +// Put stores a value by key. 0-length keys results in noop. func (s *AESEncryptedStorage) Put(key, value string) { if len(key) == 0 { return @@ -75,27 +75,41 @@ func (s *AESEncryptedStorage) 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 -func (s *AESEncryptedStorage) Get(key string) string { +// Get returns the previously stored value, or an error if it does not exist or +// key is of 0-length. +func (s *AESEncryptedStorage) Get(key string) (string, error) { if len(key) == 0 { - return "" + return "", ErrZeroKey } data, err := s.readEncryptedStorage() if err != nil { log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename) - return "" + return "", err } encrypted, exist := data[key] if !exist { log.Warn("Key does not exist", "key", key) - return "" + return "", ErrNotFound } entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key)) if err != nil { log.Warn("Failed to decrypt key", "key", key) - return "" + return "", err + } + return string(entry), nil +} + +// Del removes a key-value pair. If the key doesn't exist, the method is a noop. +func (s *AESEncryptedStorage) Del(key string) { + data, err := s.readEncryptedStorage() + if err != nil { + log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename) + return + } + delete(data, key) + if err = s.writeEncryptedStorage(data); err != nil { + log.Warn("Failed to write entry", "err", err) } - return string(entry) } // readEncryptedStorage reads the file with encrypted creds diff --git a/signer/storage/aes_gcm_storage_test.go b/signer/storage/aes_gcm_storage_test.go index a421a8449d..0aaaf58d25 100644 --- a/signer/storage/aes_gcm_storage_test.go +++ b/signer/storage/aes_gcm_storage_test.go @@ -110,8 +110,8 @@ func TestEnd2End(t *testing.T) { } s1.Put("bazonk", "foobar") - if v := s2.Get("bazonk"); v != "foobar" { - t.Errorf("Expected bazonk->foobar, got '%v'", v) + if v, err := s2.Get("bazonk"); v != "foobar" || err != nil { + t.Errorf("Expected bazonk->foobar (nil error), got '%v' (%v error)", v, err) } } @@ -154,11 +154,11 @@ func TestSwappedKeys(t *testing.T) { } } swap() - if v := s1.Get("k1"); v != "" { + if v, _ := s1.Get("k1"); v != "" { t.Errorf("swapped value should return empty") } swap() - if v := s1.Get("k1"); v != "v1" { + if v, _ := s1.Get("k1"); v != "v1" { t.Errorf("double-swapped value should work fine") } } diff --git a/signer/storage/storage.go b/signer/storage/storage.go index 50c55e455f..c1f593d960 100644 --- a/signer/storage/storage.go +++ b/signer/storage/storage.go @@ -17,11 +17,26 @@ package storage +import "errors" + +var ( + // ErrZeroKey is returned if an attempt was made to inset a 0-length key. + ErrZeroKey = errors.New("0-length key") + + // ErrNotFound is returned if an unknown key is attempted to be retrieved. + ErrNotFound = errors.New("not found") +) + type Storage interface { - // Put stores a value by key. 0-length keys results in no-op + // Put stores a value by key. 0-length keys results in noop. 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 + + // Get returns the previously stored value, or an error if the key is 0-length + // or unknown. + Get(key string) (string, error) + + // Del removes a key-value pair. If the key doesn't exist, the method is a noop. + Del(key string) } // EphemeralStorage is an in-memory storage that does @@ -31,23 +46,29 @@ type EphemeralStorage struct { namespace string } +// Put stores a value by key. 0-length keys results in noop. 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 { +// Get returns the previously stored value, or an error if the key is 0-length +// or unknown. +func (s *EphemeralStorage) Get(key string) (string, error) { if len(key) == 0 { - return "" + return "", ErrZeroKey } - //fmt.Printf("storage: get %v\n", key) - if v, exist := s.data[key]; exist { - return v + if v, ok := s.data[key]; ok { + return v, nil } - return "" + return "", ErrNotFound +} + +// Del removes a key-value pair. If the key doesn't exist, the method is a noop. +func (s *EphemeralStorage) Del(key string) { + delete(s.data, key) } func NewEphemeralStorage() Storage { @@ -61,6 +82,7 @@ func NewEphemeralStorage() Storage { type NoStorage struct{} func (s *NoStorage) Put(key, value string) {} -func (s *NoStorage) Get(key string) string { - return "" +func (s *NoStorage) Del(key string) {} +func (s *NoStorage) Get(key string) (string, error) { + return "", errors.New("I forgot") }