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. - -Check out - -* 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 +# Clef + +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. + +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/). + +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. + +### 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, +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). +- 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,46 @@ 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 } + 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 { + 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) - address := ctx.Args().First() - password := getPassPhrase("Enter a passphrase to store with this address.", true) + pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) + 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 { @@ -322,10 +372,10 @@ 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.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'); + 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); + 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) + // 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()); + 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) + // Would we exceed weekly limit ? + return sum.plus(value).lt(limit) +} +function ApproveTx(r) { + if (isLimitOk(r.transaction)) { + return "Approve" } - function ApproveTx(r){ - if (isLimitOk(r.transaction)){ - return "Approve" - } - return "Nope" - } + 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)); +/** +* 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(){ - return "Approve" -} +```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 ``` -Get the `sha256` hash. If you have openssl, you can do `openssl sha256 rules.js`... +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 -#sha256sum rules.js -6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 rules.js +echo '{"id": 1, "jsonrpc": "2.0", "method": "account_list"}' | nc -U ~/.clef/clef.ipc ``` -...now `attest` the file... -```text -#./signer attest 6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 -INFO [02-21|12:14:38] Ruleset attestation updated sha256=6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 +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]: +> ``` -...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: +Depending on whether we approve or deny the request, the original NetCat process will get: + ```text -#cp $GOPATH/src/github.com/ethereum/go-ethereum/cmd/clef/4byte.json $PWD +{"jsonrpc":"2.0","id":1,"result":["0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3","0x086278a6c067775f71d6b2bb1856db6e28c30418"]} + +or + +{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Request denied"}} ``` -At this point, we can start the signer with the rule-file: +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" +} +``` + +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. + ```text -#./signer --rules rules.js --rpc +$ sha256sum rules.js +645b58e4f945e24d0221714ff29f6aa8e860382ced43490529db1695f5fcc71c 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=