mirror of https://github.com/status-im/op-geth.git
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
This commit is contained in:
parent
6bf5555c4f
commit
a0943b8932
|
@ -1,41 +1,38 @@
|
||||||
Clef
|
# 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
|
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.
|
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.
|
||||||
* 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 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.
|
||||||
clef and an external UI
|
|
||||||
|
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
|
## Command line flags
|
||||||
|
|
||||||
Clef accepts the following command line options:
|
Clef accepts the following command line options:
|
||||||
|
|
||||||
```
|
```
|
||||||
COMMANDS:
|
COMMANDS:
|
||||||
init Initialize the signer, generate secret storage
|
init Initialize the signer, generate secret storage
|
||||||
attest Attest that a js-file is to be used
|
attest Attest that a js-file is to be used
|
||||||
setpw Store a credential for a keystore file
|
setpw Store a credential for a keystore file
|
||||||
|
delpw Remove a credential for a keystore file
|
||||||
gendoc Generate documentation about json-rpc format
|
gendoc Generate documentation about json-rpc format
|
||||||
help Shows a list of commands or help for one command
|
help Shows a list of commands or help for one command
|
||||||
|
|
||||||
GLOBAL OPTIONS:
|
GLOBAL OPTIONS:
|
||||||
--loglevel value log level to emit to the screen (default: 4)
|
--loglevel value log level to emit to the screen (default: 4)
|
||||||
--keystore value Directory for the keystore (default: "$HOME/.ethereum/keystore")
|
--keystore value Directory for the keystore (default: "$HOME/.ethereum/keystore")
|
||||||
--configdir value Directory for Clef configuration (default: "$HOME/.clef")
|
--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
|
--lightkdf Reduce key-derivation RAM & CPU usage at some expense of KDF strength
|
||||||
--nousb Disables monitoring for and managing USB hardware wallets
|
--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")
|
--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")
|
--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
|
--ipcdisable Disable the IPC-RPC server
|
||||||
|
@ -43,60 +40,48 @@ GLOBAL OPTIONS:
|
||||||
--rpc Enable the HTTP-RPC server
|
--rpc Enable the HTTP-RPC server
|
||||||
--rpcport value HTTP-RPC server listening port (default: 8550)
|
--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
|
--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")
|
--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")
|
--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 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'.
|
--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
|
--advanced If enabled, issues warnings instead of rejections for suspicious requests. Default off
|
||||||
--help, -h show help
|
--help, -h show help
|
||||||
--version, -v print the version
|
--version, -v print the version
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
|
||||||
signer -keystore /my/keystore -chainid 4
|
|
||||||
```
|
|
||||||
|
|
||||||
|
```
|
||||||
|
$ clef -keystore /my/keystore -chainid 4
|
||||||
|
```
|
||||||
|
|
||||||
## Security model
|
## 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.
|
* One critical component (the Clef 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.
|
* Clef has a well-defined 'external' API.
|
||||||
* The 'external' API is considered UNTRUSTED.
|
* 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.
|
* 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)
|
![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
|
## TODOs
|
||||||
|
|
||||||
Some snags and todos
|
Some snags and todos
|
||||||
|
|
||||||
* [ ] The signer should take a startup param "--no-change", for UIs that do not contain the capability
|
* [ ] 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.
|
||||||
to perform changes to things, only approve/deny. Such a UI should be able to start the signer in
|
* [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.
|
||||||
a more secure mode by telling it that it only wants approve/deny capabilities.
|
* [ ] 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] It would be nice if the signer could collect new 4byte-id:s/method selectors, and have a
|
* [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:
|
||||||
secondary database for those (`4byte_custom.json`). Users could then (optionally) submit their collections for
|
|
||||||
inclusion upstream.
|
|
||||||
|
|
||||||
* It should be possible to configure the signer to check if an account is indeed known to it, before
|
|
||||||
passing on to the UI. The reason it currently does not, is that it would make it possible to enumerate
|
|
||||||
accounts if it immediately returned "unknown account".
|
|
||||||
* [x] It should be possible to configure the signer to auto-allow listing (certain) accounts, instead of asking every time.
|
|
||||||
* [x] Done Upon startup, the signer should spit out some info to the caller (particularly important when executed in `stdio-ui`-mode),
|
|
||||||
invoking methods with the following info:
|
|
||||||
* [x] Version info about the signer
|
* [x] Version info about the signer
|
||||||
* [x] Address of API (http/ipc)
|
* [x] Address of API (HTTP/IPC)
|
||||||
* [ ] List of known accounts
|
* [ ] 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.
|
* [ ] 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`
|
* [ ] `account_signRawTransaction`
|
||||||
|
@ -109,21 +94,16 @@ invoking methods with the following info:
|
||||||
* the number of unique recipients
|
* the number of unique recipients
|
||||||
|
|
||||||
* Geth todos
|
* 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
|
- 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.
|
||||||
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`.
|
||||||
- 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.
|
||||||
- Currently, the Geth APIs use `common.Address` in the arguments to transaction submission (e.g `to` field). This
|
- The Geth API should switch to use the same type, and relay `to`-account verbatim to the external API.
|
||||||
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] 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.
|
* See [rules.md](rules.md) for more info about this.
|
||||||
|
|
||||||
* Another potential thing to introduce is pairing.
|
* 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).
|
* 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.
|
* This feature would make the addition of rules less dangerous.
|
||||||
|
|
||||||
* Wallets / accounts. Add API methods for wallets.
|
* 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
|
### External API
|
||||||
|
|
||||||
The signer listens to HTTP requests on `rpcaddr`:`rpcport`, with the same JSONRPC standard as Geth. The messages are
|
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).
|
||||||
expected to be JSON [jsonrpc 2.0 standard](http://www.jsonrpc.org/specification).
|
|
||||||
|
|
||||||
Some of these call can require user interaction. Clients must be aware that responses
|
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.
|
||||||
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
|
The External API is **untrusted**: it does not accept credentials over this API, nor does it expect that requests have any authority.
|
||||||
that requests have any authority.
|
|
||||||
|
|
||||||
### UI API
|
### Internal UI API
|
||||||
|
|
||||||
The signer has one native console-based UI, for operation without any standalone tools.
|
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.
|
||||||
However, there is also an API to communicate with an external UI. To enable that UI,
|
|
||||||
the signer needs to be executed with the `--stdio-ui` option, which allocates the
|
|
||||||
`stdin`/`stdout` for the UI-api.
|
|
||||||
|
|
||||||
An example (insecure) proof-of-concept of has been implemented in `pythonsigner.py`.
|
An example (insecure) proof-of-concept of has been implemented in `pythonsigner.py`.
|
||||||
|
|
||||||
The model is as follows:
|
The model is as follows:
|
||||||
|
|
||||||
* The user starts the UI app (`pythonsigner.py`).
|
* 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.
|
process output for confirmation-requests.
|
||||||
* The `signer` opens the external http api.
|
* `clef` opens the external HTTP API.
|
||||||
* When the `signer` receives requests, it sends a `jsonrpc` request via `stdout`.
|
* When the `signer` receives requests, it sends a JSON-RPC request via `stdout`.
|
||||||
* The UI app prompts the user accordingly, and responds to the `signer`
|
* The UI app prompts the user accordingly, and responds to `clef`.
|
||||||
* The `signer` signs (or not), and responds to the original request.
|
* `clef` signs (or not), and responds to the original request.
|
||||||
|
|
||||||
## External API
|
## 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
|
### Encoding
|
||||||
- number: positive integers that are hex encoded
|
- number: positive integers that are hex encoded
|
||||||
|
@ -187,7 +161,7 @@ None
|
||||||
#### Result
|
#### Result
|
||||||
- address [string]: account address that is derived from the generated key
|
- address [string]: account address that is derived from the generated key
|
||||||
- url [string]: location of the keyfile
|
- url [string]: location of the keyfile
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -221,9 +195,9 @@ None
|
||||||
#### Result
|
#### Result
|
||||||
- array with account records:
|
- array with account records:
|
||||||
- account.address [string]: account address that is derived from the generated key
|
- 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
|
- account.url [string]: location of the account
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -272,7 +246,7 @@ Response
|
||||||
|
|
||||||
#### Result
|
#### Result
|
||||||
- signed transaction in RLP encoded form [data]
|
- signed transaction in RLP encoded form [data]
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -372,7 +346,7 @@ Bash example:
|
||||||
|
|
||||||
#### Result
|
#### Result
|
||||||
- calculated signature [data]
|
- calculated signature [data]
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -407,7 +381,7 @@ Response
|
||||||
|
|
||||||
#### Result
|
#### Result
|
||||||
- calculated signature [data]
|
- calculated signature [data]
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -505,7 +479,7 @@ Derive the address from the account that was used to sign data with content type
|
||||||
|
|
||||||
#### Result
|
#### Result
|
||||||
- derived account [address]
|
- derived account [address]
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -534,16 +508,16 @@ Response
|
||||||
#### Import account
|
#### Import account
|
||||||
Import a private key into the keystore. The imported key is expected to be encrypted according to the web3 keystore
|
Import a private key into the keystore. The imported key is expected to be encrypted according to the web3 keystore
|
||||||
format.
|
format.
|
||||||
|
|
||||||
#### Arguments
|
#### 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
|
#### Result
|
||||||
- imported key [object]:
|
- imported key [object]:
|
||||||
- key.address [address]: address of the imported key
|
- key.address [address]: address of the imported key
|
||||||
- key.type [string]: type of the account
|
- key.type [string]: type of the account
|
||||||
- key.url [string]: key URL
|
- key.url [string]: key URL
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -594,14 +568,14 @@ Response
|
||||||
#### Export account from keystore
|
#### Export account from keystore
|
||||||
Export a private key from the keystore. The exported private key is encrypted with the original passphrase. When the
|
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.
|
key is imported later this passphrase is required.
|
||||||
|
|
||||||
#### Arguments
|
#### Arguments
|
||||||
- account [address]: export private key that is associated with this account
|
- account [address]: export private key that is associated with this account
|
||||||
|
|
||||||
#### Result
|
#### Result
|
||||||
- exported key, see [web3 keystore format](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) for
|
- exported key, see [web3 keystore format](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) for
|
||||||
more information
|
more information
|
||||||
|
|
||||||
#### Sample call
|
#### Sample call
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -643,8 +617,6 @@ Response
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## UI API
|
## UI API
|
||||||
|
|
||||||
These methods needs to be implemented by a UI listener.
|
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.
|
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.
|
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
|
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`
|
### 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.
|
in k/v-form.
|
||||||
|
|
||||||
Example call:
|
Example call:
|
||||||
|
@ -953,9 +925,9 @@ A UI should conform to the following rules.
|
||||||
along with the UI.
|
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|
|
| Name | Repo | UI type| No external resources| Blocky support| Verifies permissions | Hash information | No secondary storage | Statically linked| Can modify parameters|
|
||||||
| ---- | ---- | -------| ---- | ---- | ---- |---- | ---- | ---- | ---- |
|
| ---- | ---- | -------| ---- | ---- | ---- |---- | ---- | ---- | ---- |
|
||||||
|
|
|
@ -11,7 +11,7 @@ Example:
|
||||||
"content_type": "text/plain",
|
"content_type": "text/plain",
|
||||||
"address": "0xDEADbEeF000000000000000000000000DeaDbeEf",
|
"address": "0xDEADbEeF000000000000000000000000DeaDbeEf",
|
||||||
"raw_data": "GUV0aGVyZXVtIFNpZ25lZCBNZXNzYWdlOgoxMWhlbGxvIHdvcmxk",
|
"raw_data": "GUV0aGVyZXVtIFNpZ25lZCBNZXNzYWdlOgoxMWhlbGxvIHdvcmxk",
|
||||||
"message": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"name": "message",
|
"name": "message",
|
||||||
"value": "\u0019Ethereum Signed Message:\n11hello world",
|
"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.
|
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
|
The `OnApproved` method cannot be responded to, it's purely informative
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ Example:
|
||||||
```
|
```
|
||||||
### ListRequest
|
### 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:
|
Example:
|
||||||
```json
|
```json
|
||||||
|
|
|
@ -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
|
### 6.0.0
|
||||||
|
|
||||||
|
@ -14,15 +25,15 @@ The addition of `contentType` makes it possible to use the method for different
|
||||||
* signing clique headers,
|
* signing clique headers,
|
||||||
* signing plain personal messages,
|
* 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.
|
* 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
|
#### 4.0.0
|
||||||
|
|
||||||
* The external `account_Ecrecover`-method was removed.
|
* The external `account_Ecrecover`-method was removed.
|
||||||
* The external `account_Import`-method was removed.
|
* The external `account_Import`-method was removed.
|
||||||
|
|
||||||
#### 3.0.0
|
#### 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
|
#### 2.0.0
|
||||||
|
|
||||||
|
@ -33,15 +44,3 @@ makes the `accounts_signTransaction` identical to the old `eth_signTransaction`.
|
||||||
#### 1.0.0
|
#### 1.0.0
|
||||||
|
|
||||||
Initial release.
|
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.
|
|
||||||
|
|
|
@ -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.
|
* MAJOR version when you make incompatible API changes,
|
||||||
With this change, the same stored password can be used even if rulesets are not enabled, but storage is.
|
* MINOR version when you add functionality in a backwards-compatible manner, and
|
||||||
- It also removes the usability-shortcut that a UI might otherwise want to implement; remembering passwords. Since we now will not require the
|
* PATCH version when you make backwards-compatible bug fixes.
|
||||||
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).
|
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:
|
Affected datatypes:
|
||||||
- `SignTxResponse`
|
- `SignTxResponse`
|
||||||
- `SignDataResponse`
|
- `SignDataResponse`
|
||||||
- `NewAccountResponse`
|
- `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:
|
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
|
### 4.0.0
|
||||||
|
|
||||||
* Bidirectional communication implemented, so the UI can query `clef` via the stdin/stdout RPC channel. Methods implemented are:
|
* 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_listAccounts`
|
||||||
- `clef_listWallets`
|
- `clef_listWallets`
|
||||||
- `clef_deriveAccount`
|
- `clef_deriveAccount`
|
||||||
|
@ -48,10 +63,10 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth
|
||||||
- `clef_setChainId`
|
- `clef_setChainId`
|
||||||
- `clef_export`
|
- `clef_export`
|
||||||
- `clef_import`
|
- `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 {
|
type Account struct {
|
||||||
Address common.Address `json:"address"` // Ethereum account address derived from the key
|
Address common.Address `json:"address"` // Ethereum account address derived from the key
|
||||||
URL URL `json:"url"` // Optional resource locator within a backend
|
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):
|
* 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.
|
> 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"
|
> 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
|
### 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.
|
* Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords.
|
||||||
|
|
||||||
The following structures are used:
|
The following structures are used:
|
||||||
```golang
|
|
||||||
UserInputRequest struct {
|
```go
|
||||||
Prompt string `json:"prompt"`
|
UserInputRequest struct {
|
||||||
Title string `json:"title"`
|
Prompt string `json:"prompt"`
|
||||||
IsPassword bool `json:"isPassword"`
|
Title string `json:"title"`
|
||||||
}
|
IsPassword bool `json:"isPassword"`
|
||||||
UserInputResponse struct {
|
}
|
||||||
Text string `json:"text"`
|
UserInputResponse struct {
|
||||||
}
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 2.0.0
|
### 2.0.0
|
||||||
|
|
||||||
|
@ -161,15 +178,3 @@ Example call:
|
||||||
#### 1.0.0
|
#### 1.0.0
|
||||||
|
|
||||||
Initial release.
|
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.
|
|
||||||
|
|
168
cmd/clef/main.go
168
cmd/clef/main.go
|
@ -93,7 +93,7 @@ var (
|
||||||
chainIdFlag = cli.Int64Flag{
|
chainIdFlag = cli.Int64Flag{
|
||||||
Name: "chainid",
|
Name: "chainid",
|
||||||
Value: params.MainnetChainConfig.ChainID.Int64(),
|
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{
|
rpcPortFlag = cli.IntFlag{
|
||||||
Name: "rpcport",
|
Name: "rpcport",
|
||||||
|
@ -116,8 +116,7 @@ var (
|
||||||
}
|
}
|
||||||
ruleFlag = cli.StringFlag{
|
ruleFlag = cli.StringFlag{
|
||||||
Name: "rules",
|
Name: "rules",
|
||||||
Usage: "Enable rule-engine",
|
Usage: "Path to the rule file to auto-authorize requests with",
|
||||||
Value: "",
|
|
||||||
}
|
}
|
||||||
stdiouiFlag = cli.BoolFlag{
|
stdiouiFlag = cli.BoolFlag{
|
||||||
Name: "stdio-ui",
|
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
|
Whenever you make an edit to the rule file, you need to use attestation to tell
|
||||||
Clef that the file is 'safe' to execute.`,
|
Clef that the file is 'safe' to execute.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
setCredentialCommand = cli.Command{
|
setCredentialCommand = cli.Command{
|
||||||
Action: utils.MigrateFlags(setCredential),
|
Action: utils.MigrateFlags(setCredential),
|
||||||
Name: "setpw",
|
Name: "setpw",
|
||||||
|
@ -172,8 +170,20 @@ Clef that the file is 'safe' to execute.`,
|
||||||
signerSecretFlag,
|
signerSecretFlag,
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `
|
||||||
The setpw command stores a password for a given address (keyfile). If you enter a blank passphrase, it will
|
The setpw command stores a password for a given address (keyfile).
|
||||||
remove any stored credential for that address (keyfile)
|
`}
|
||||||
|
delCredentialCommand = cli.Command{
|
||||||
|
Action: utils.MigrateFlags(removeCredential),
|
||||||
|
Name: "delpw",
|
||||||
|
Usage: "Remove a credential for a keystore file",
|
||||||
|
ArgsUsage: "<address>",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
logLevelFlag,
|
||||||
|
configdirFlag,
|
||||||
|
signerSecretFlag,
|
||||||
|
},
|
||||||
|
Description: `
|
||||||
|
The delpw command removes a password for a given address (keyfile).
|
||||||
`}
|
`}
|
||||||
gendocCommand = cli.Command{
|
gendocCommand = cli.Command{
|
||||||
Action: GenDoc,
|
Action: GenDoc,
|
||||||
|
@ -210,9 +220,9 @@ func init() {
|
||||||
advancedMode,
|
advancedMode,
|
||||||
}
|
}
|
||||||
app.Action = signer
|
app.Action = signer
|
||||||
app.Commands = []cli.Command{initCommand, attestCommand, setCredentialCommand, gendocCommand}
|
app.Commands = []cli.Command{initCommand, attestCommand, setCredentialCommand, delCredentialCommand, gendocCommand}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := app.Run(os.Args); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
@ -221,11 +231,20 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeSecrets(c *cli.Context) error {
|
func initializeSecrets(c *cli.Context) error {
|
||||||
|
// Get past the legal message
|
||||||
if err := initialize(c); err != nil {
|
if err := initialize(c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Ensure the master key does not yet exist, we're not willing to overwrite
|
||||||
configDir := c.GlobalString(configdirFlag.Name)
|
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)
|
masterSeed := make([]byte, 256)
|
||||||
num, err := io.ReadFull(rand.Reader, masterSeed)
|
num, err := io.ReadFull(rand.Reader, masterSeed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -234,18 +253,18 @@ func initializeSecrets(c *cli.Context) error {
|
||||||
if num != len(masterSeed) {
|
if num != len(masterSeed) {
|
||||||
return fmt.Errorf("failed to read enough random")
|
return fmt.Errorf("failed to read enough random")
|
||||||
}
|
}
|
||||||
|
|
||||||
n, p := keystore.StandardScryptN, keystore.StandardScryptP
|
n, p := keystore.StandardScryptN, keystore.StandardScryptP
|
||||||
if c.GlobalBool(utils.LightKDFFlag.Name) {
|
if c.GlobalBool(utils.LightKDFFlag.Name) {
|
||||||
n, p = keystore.LightScryptN, keystore.LightScryptP
|
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
|
var password string
|
||||||
for {
|
for {
|
||||||
password = getPassPhrase(text, true)
|
password = getPassPhrase(text, true)
|
||||||
if err := core.ValidatePasswordFormat(password); err != nil {
|
if err := core.ValidatePasswordFormat(password); err != nil {
|
||||||
fmt.Printf("invalid password: %v\n", err)
|
fmt.Printf("invalid password: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
|
fmt.Println()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,28 +272,27 @@ func initializeSecrets(c *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to encrypt master seed: %v", err)
|
return fmt.Errorf("failed to encrypt master seed: %v", err)
|
||||||
}
|
}
|
||||||
|
// Double check the master key path to ensure nothing wrote there in between
|
||||||
err = os.Mkdir(configDir, 0700)
|
if err = os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) {
|
||||||
if err != nil && !os.IsExist(err) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
location := filepath.Join(configDir, "masterseed.json")
|
|
||||||
if _, err := os.Stat(location); err == nil {
|
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)
|
// Write the file and print the usual warning message
|
||||||
if err != nil {
|
if err = ioutil.WriteFile(location, cipherSeed, 0400); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("A master seed has been generated into %s\n", location)
|
fmt.Printf("A master seed has been generated into %s\n", location)
|
||||||
fmt.Printf(`
|
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)
|
* Passwords for keystores (used by rule engine)
|
||||||
* Storage for javascript rules
|
* Storage for JavaScript auto-signing rules
|
||||||
* Hash of rule-file
|
* Hash of JavaScript rule-file
|
||||||
|
|
||||||
You should treat that file with utmost secrecy, and make a backup of it.
|
You should treat 'masterseed.json' with utmost secrecy and make a backup of it!
|
||||||
NOTE: This file does not contain your accounts. Those need to be backed up separately!
|
* 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
|
return nil
|
||||||
|
@ -305,14 +323,18 @@ func attestFile(ctx *cli.Context) error {
|
||||||
|
|
||||||
func setCredential(ctx *cli.Context) error {
|
func setCredential(ctx *cli.Context) error {
|
||||||
if len(ctx.Args()) < 1 {
|
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 {
|
if err := initialize(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
addr := ctx.Args().First()
|
||||||
address := ctx.Args().First()
|
if !common.IsHexAddress(addr) {
|
||||||
password := getPassPhrase("Enter a passphrase to store with this address.", true)
|
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)
|
stretchedKey, err := readMasterKey(ctx, nil)
|
||||||
if err != 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]))
|
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
|
||||||
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
|
pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
|
||||||
|
|
||||||
// Initialize the encrypted storages
|
|
||||||
pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
|
pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
|
||||||
pwStorage.Put(address, password)
|
pwStorage.Put(address.Hex(), password)
|
||||||
log.Info("Credential store updated", "key", address)
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,13 +390,17 @@ func initialize(c *cli.Context) error {
|
||||||
if !confirm(legalWarning) {
|
if !confirm(legalWarning) {
|
||||||
return fmt.Errorf("aborted by user")
|
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))))
|
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true))))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signer(c *cli.Context) error {
|
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 {
|
if err := initialize(c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -376,7 +430,7 @@ func signer(c *cli.Context) error {
|
||||||
|
|
||||||
configDir := c.GlobalString(configdirFlag.Name)
|
configDir := c.GlobalString(configdirFlag.Name)
|
||||||
if stretchedKey, err := readMasterKey(c, ui); err != nil {
|
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 {
|
} else {
|
||||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
|
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)
|
jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey)
|
||||||
configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey)
|
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 != "" {
|
if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" {
|
||||||
ruleJS, err := ioutil.ReadFile(c.GlobalString(ruleFile))
|
ruleJS, err := ioutil.ReadFile(ruleFile)
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
shasum := sha256.Sum256(ruleJS)
|
shasum := sha256.Sum256(ruleJS)
|
||||||
foundShaSum := hex.EncodeToString(shasum[:])
|
foundShaSum := hex.EncodeToString(shasum[:])
|
||||||
storedShasum := configStorage.Get("ruleset_sha256")
|
storedShasum, _ := configStorage.Get("ruleset_sha256")
|
||||||
if storedShasum != foundShaSum {
|
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 {
|
} else {
|
||||||
// Initialize rules
|
// Initialize rules
|
||||||
ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage)
|
ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage)
|
||||||
|
@ -452,7 +506,6 @@ func signer(c *cli.Context) error {
|
||||||
Version: "1.0"},
|
Version: "1.0"},
|
||||||
}
|
}
|
||||||
if c.GlobalBool(utils.RPCEnabledFlag.Name) {
|
if c.GlobalBool(utils.RPCEnabledFlag.Name) {
|
||||||
|
|
||||||
vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name))
|
vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name))
|
||||||
cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name))
|
cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name))
|
||||||
|
|
||||||
|
@ -469,7 +522,6 @@ func signer(c *cli.Context) error {
|
||||||
listener.Close()
|
listener.Close()
|
||||||
log.Info("HTTP endpoint closed", "url", httpEndpoint)
|
log.Info("HTTP endpoint closed", "url", httpEndpoint)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
}
|
}
|
||||||
if !c.GlobalBool(utils.IPCDisabledFlag.Name) {
|
if !c.GlobalBool(utils.IPCDisabledFlag.Name) {
|
||||||
if c.IsSet(utils.IPCPathFlag.Name) {
|
if c.IsSet(utils.IPCPathFlag.Name) {
|
||||||
|
@ -496,8 +548,8 @@ func signer(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
ui.OnSignerStartup(core.StartupInfo{
|
ui.OnSignerStartup(core.StartupInfo{
|
||||||
Info: map[string]interface{}{
|
Info: map[string]interface{}{
|
||||||
"extapi_version": core.ExternalAPIVersion,
|
|
||||||
"intapi_version": core.InternalAPIVersion,
|
"intapi_version": core.InternalAPIVersion,
|
||||||
|
"extapi_version": core.ExternalAPIVersion,
|
||||||
"extapi_http": extapiURL,
|
"extapi_http": extapiURL,
|
||||||
"extapi_ipc": ipcapiURL,
|
"extapi_ipc": ipcapiURL,
|
||||||
},
|
},
|
||||||
|
@ -592,7 +644,6 @@ func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) {
|
||||||
if len(masterSeed) < 256 {
|
if len(masterSeed) < 256 {
|
||||||
return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed))
|
return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create vault location
|
// Create vault location
|
||||||
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10]))
|
vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10]))
|
||||||
err = os.Mkdir(vaultLocation, 0700)
|
err = os.Mkdir(vaultLocation, 0700)
|
||||||
|
@ -620,13 +671,12 @@ func checkFile(filename string) error {
|
||||||
// confirm displays a text and asks for user confirmation
|
// confirm displays a text and asks for user confirmation
|
||||||
func confirm(text string) bool {
|
func confirm(text string) bool {
|
||||||
fmt.Printf(text)
|
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')
|
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Crit("Failed to read user input", "err", err)
|
log.Crit("Failed to read user input", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if text := strings.TrimSpace(text); text == "ok" {
|
if text := strings.TrimSpace(text); text == "ok" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -642,7 +692,7 @@ func testExternalUI(api *core.SignerAPI) {
|
||||||
|
|
||||||
a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef")
|
a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef")
|
||||||
addErr := func(errStr string) {
|
addErr := func(errStr string) {
|
||||||
log.Info("Test error", "error", errStr)
|
log.Info("Test error", "err", errStr)
|
||||||
errs = append(errs, 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" +
|
"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`"
|
"the user with the contents of the `message`"
|
||||||
sighash, msg := accounts.TextAndHash([]byte("hello world"))
|
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{
|
add("SignDataRequest", desc, &core.SignDataRequest{
|
||||||
Address: common.NewMixedcaseAddress(a),
|
Address: common.NewMixedcaseAddress(a),
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
ContentType: accounts.MimetypeTextPlain,
|
ContentType: accounts.MimetypeTextPlain,
|
||||||
Rawdata: []byte(msg),
|
Rawdata: []byte(msg),
|
||||||
Message: message,
|
Messages: messages,
|
||||||
Hash: sighash})
|
Hash: sighash})
|
||||||
}
|
}
|
||||||
{ // Sign plain text response
|
{ // Sign plain text response
|
||||||
|
@ -982,29 +1032,3 @@ These data types are defined in the channel between clef and the UI`)
|
||||||
fmt.Println(elem)
|
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/
|
|
||||||
|
|
||||||
|
|
||||||
**/
|
|
||||||
|
|
|
@ -42,7 +42,6 @@ class PipeTransport(ServerTransport):
|
||||||
self.output.write("\n")
|
self.output.write("\n")
|
||||||
|
|
||||||
class StdIOHandler():
|
class StdIOHandler():
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -76,7 +75,7 @@ class StdIOHandler():
|
||||||
:param transaction: transaction info
|
:param transaction: transaction info
|
||||||
:param call_info: info abou the call, e.g. if ABI info could not be
|
: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
|
:param meta: metadata about the request, e.g. where the call comes from
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
transaction = req.get('transaction')
|
transaction = req.get('transaction')
|
||||||
_from = req.get('from')
|
_from = req.get('from')
|
||||||
|
@ -158,8 +157,7 @@ class StdIOHandler():
|
||||||
return
|
return
|
||||||
|
|
||||||
def main(args):
|
def main(args):
|
||||||
|
cmd = ["clef", "--stdio-ui"]
|
||||||
cmd = ["./clef", "--stdio-ui"]
|
|
||||||
if len(args) > 0 and args[0] == "test":
|
if len(args) > 0 and args[0] == "test":
|
||||||
cmd.extend(["--stdio-ui-test"])
|
cmd.extend(["--stdio-ui-test"])
|
||||||
print("cmd: {}".format(" ".join(cmd)))
|
print("cmd: {}".format(" ".join(cmd)))
|
||||||
|
|
|
@ -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
|
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:
|
defined in the UI protocol. Example:
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
|
function asBig(str) {
|
||||||
function asBig(str){
|
if (str.slice(0, 2) == "0x") {
|
||||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
|
return new BigNumber(str.slice(2), 16)
|
||||||
return new BigNumber(str)
|
}
|
||||||
|
return new BigNumber(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approve transactions to a certain contract if value is below a certain limit
|
// Approve transactions to a certain contract if value is below a certain limit
|
||||||
function ApproveTx(req){
|
function ApproveTx(req) {
|
||||||
|
var limit = big.Newint("0xb1a2bc2ec50000")
|
||||||
var limit = big.Newint("0xb1a2bc2ec50000")
|
|
||||||
var value = asBig(req.transaction.value);
|
var value = asBig(req.transaction.value);
|
||||||
|
|
||||||
if(req.transaction.to.toLowerCase()=="0xae967917c465db8578ca9024c205720b1a3651a9")
|
if (req.transaction.to.toLowerCase() == "0xae967917c465db8578ca9024c205720b1a3651a9") && value.lt(limit)) {
|
||||||
&& value.lt(limit) ){
|
return "Approve"
|
||||||
return "Approve"
|
}
|
||||||
}
|
// If we return "Reject", it will be rejected.
|
||||||
// If we return "Reject", it will be rejected.
|
// By not returning anything, it will be passed to the next UI, for manual processing
|
||||||
// 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){
|
function ApproveListing(req){
|
||||||
if (req.metadata.scheme == "ipc"){ return "Approve"}
|
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
|
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
|
## Example 1: ruleset for a rate-limited window
|
||||||
|
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
|
function big(str) {
|
||||||
function big(str){
|
if (str.slice(0, 2) == "0x") {
|
||||||
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
|
return new BigNumber(str.slice(2), 16)
|
||||||
return new BigNumber(str)
|
|
||||||
}
|
}
|
||||||
|
return new BigNumber(str)
|
||||||
|
}
|
||||||
|
|
||||||
// Time window: 1 week
|
// Time window: 1 week
|
||||||
var window = 1000* 3600*24*7;
|
var window = 1000* 3600*24*7;
|
||||||
|
|
||||||
// Limit : 1 ether
|
// Limit : 1 ether
|
||||||
var limit = new BigNumber("1e18");
|
var limit = new BigNumber("1e18");
|
||||||
|
|
||||||
function isLimitOk(transaction){
|
function isLimitOk(transaction) {
|
||||||
var value = big(transaction.value)
|
var value = big(transaction.value)
|
||||||
// Start of our window function
|
// Start of our window function
|
||||||
var windowstart = new Date().getTime() - window;
|
var windowstart = new Date().getTime() - window;
|
||||||
|
|
||||||
var txs = [];
|
var txs = [];
|
||||||
var stored = storage.Get('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)
|
|
||||||
|
|
||||||
|
if (stored != "") {
|
||||||
|
txs = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
function ApproveTx(r){
|
// First, remove all that have passed out of the time-window
|
||||||
if (isLimitOk(r.transaction)){
|
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
|
||||||
return "Approve"
|
console.log(txs, newtxs.length);
|
||||||
}
|
|
||||||
return "Nope"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Secondly, aggregate the current sum
|
||||||
* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
|
sum = new BigNumber(0)
|
||||||
* '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));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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
|
## Example 2: allow destination
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
|
function ApproveTx(r) {
|
||||||
function ApproveTx(r){
|
if (r.transaction.from.toLowerCase() == "0x0000000000000000000000000000000000001337") {
|
||||||
if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
|
return "Approve"
|
||||||
if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
|
|
||||||
// Otherwise goes to manual processing
|
|
||||||
}
|
}
|
||||||
|
if (r.transaction.from.toLowerCase() == "0x000000000000000000000000000000000000dead") {
|
||||||
|
return "Reject"
|
||||||
|
}
|
||||||
|
// Otherwise goes to manual processing
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example 3: Allow listing
|
## Example 3: Allow listing
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
|
function ApproveListing() {
|
||||||
function ApproveListing(){
|
return "Approve"
|
||||||
return "Approve"
|
}
|
||||||
}
|
```
|
||||||
|
|
||||||
```
|
|
||||||
|
|
|
@ -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
|
```text
|
||||||
#./signer init
|
$ clef init
|
||||||
|
|
||||||
WARNING!
|
WARNING!
|
||||||
|
|
||||||
The signer is alpha software, and not yet publically released. This software has _not_ been audited, and there
|
Clef is an account management tool. It may, like any software, contain bugs.
|
||||||
are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software
|
|
||||||
unless you agree to take full responsibility for doing so, and know what you are doing.
|
|
||||||
|
|
||||||
TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE!
|
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:
|
Enter 'ok' to proceed:
|
||||||
>ok
|
> ok
|
||||||
A master seed has been generated into /home/martin/.signer/secrets.dat
|
|
||||||
|
|
||||||
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)
|
* Passwords for keystores (used by rule engine)
|
||||||
* Storage for javascript rules
|
* Storage for JavaScript auto-signing rules
|
||||||
* Hash of rule-file
|
* Hash of JavaScript rule-file
|
||||||
|
|
||||||
You should treat that file with utmost secrecy, and make a backup of it.
|
You should treat 'masterseed.json' with utmost secrecy and make a backup of it!
|
||||||
NOTE: This file does not contain your accounts. Those need to be backed up separately!
|
* 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
|
```text
|
||||||
function ApproveListing(){
|
$ 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"
|
return "Approve"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Get the `sha256` hash. If you have openssl, you can do `openssl sha256 rules.js`...
|
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
|
|
||||||
#sha256sum rules.js
|
|
||||||
6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72 rules.js
|
|
||||||
```
|
|
||||||
...now `attest` the file...
|
|
||||||
```text
|
|
||||||
#./signer attest 6c21d1737429d6d4f2e55146da0797782f3c0a0355227f19d702df377c165d72
|
|
||||||
|
|
||||||
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:
|
At this point, we can start Clef with the rule file:
|
||||||
```text
|
|
||||||
#cp $GOPATH/src/github.com/ethereum/go-ethereum/cmd/clef/4byte.json $PWD
|
|
||||||
```
|
|
||||||
|
|
||||||
At this point, we can start the signer with the rule-file:
|
|
||||||
```text
|
```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 [07-01|13:39:49.726] Rule engine configured file=rules.js
|
||||||
INFO [09-25|20:28:11.876] Loaded 4byte db signatures=5509 file=./4byte.json
|
INFO [07-01|13:39:49.726] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false
|
||||||
INFO [09-25|20:28:11.877] Rule engine configured file=./rules.js
|
DEBUG[07-01|13:39:49.726] FS scan times list=35.15µs set=4.251µs diff=2.766µs
|
||||||
DEBUG[09-25|20:28:11.877] FS scan times list=100.781µs set=13.253µs diff=5.761µs
|
DEBUG[07-01|13:39:49.727] Ledger support enabled
|
||||||
DEBUG[09-25|20:28:11.884] Ledger support enabled
|
DEBUG[07-01|13:39:49.727] Trezor support enabled via HID
|
||||||
DEBUG[09-25|20:28:11.888] Trezor support enabled
|
DEBUG[07-01|13:39:49.727] Trezor support enabled via WebUSB
|
||||||
INFO [09-25|20:28:11.888] Audit logs configured file=audit.log
|
INFO [07-01|13:39:49.728] Audit logs configured file=audit.log
|
||||||
DEBUG[09-25|20:28:11.888] HTTP registered namespace=account
|
DEBUG[07-01|13:39:49.728] IPC registered namespace=account
|
||||||
INFO [09-25|20:28:11.890] HTTP endpoint opened url=http://localhost:8550
|
INFO [07-01|13:39:49.728] IPC endpoint opened url=$HOME/.clef/clef.ipc
|
||||||
DEBUG[09-25|20:28:11.890] IPC registered namespace=account
|
|
||||||
INFO [09-25|20:28:11.890] IPC endpoint opened url=<nil>
|
|
||||||
------- Signer info -------
|
------- Signer info -------
|
||||||
* extapi_version : 2.0.0
|
* intapi_version : 7.0.0
|
||||||
* intapi_version : 2.0.0
|
* extapi_version : 6.0.0
|
||||||
* extapi_http : http://localhost:8550
|
* extapi_http : n/a
|
||||||
* extapi_ipc : <nil>
|
* 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
|
## Under the hood
|
||||||
|
|
||||||
While doing the operations above, these files have been created:
|
While doing the operations above, these files have been created:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
#ls -laR ~/.signer/
|
$ ls -laR ~/.clef/
|
||||||
/home/martin/.signer/:
|
|
||||||
total 16
|
|
||||||
drwx------ 3 martin martin 4096 feb 21 12:14 .
|
|
||||||
drwxr-xr-x 71 martin martin 4096 feb 21 12:12 ..
|
|
||||||
drwx------ 2 martin martin 4096 feb 21 12:14 43f73718397aa54d1b22
|
|
||||||
-rwx------ 1 martin martin 256 feb 21 12:12 secrets.dat
|
|
||||||
|
|
||||||
/home/martin/.signer/43f73718397aa54d1b22:
|
$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
|
total 12
|
||||||
drwx------ 2 martin martin 4096 feb 21 12:14 .
|
drwx------ 2 user user 4096 Jul 1 13:25 .
|
||||||
drwx------ 3 martin martin 4096 feb 21 12:14 ..
|
drwxr-x--x 3 user user 4096 Jul 1 13:45 ..
|
||||||
-rw------- 1 martin martin 159 feb 21 12:14 config.json
|
-rw------- 1 user user 159 Jul 1 13:25 config.json
|
||||||
|
|
||||||
#cat /home/martin/.signer/43f73718397aa54d1b22/config.json
|
|
||||||
{"ruleset_sha256":{"iv":"6v4W4tfJxj3zZFbl","c":"6dt5RTDiTq93yh1qDEjpsat/tsKG7cb+vr3sza26IPL2fvsQ6ZoqFx++CPUa8yy6fD9Bbq41L01ehkKHTG3pOAeqTW6zc/+t0wv3AB6xPmU="}}
|
|
||||||
|
|
||||||
|
$ 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`.
|
In `$HOME/.clef`, the `masterseed.json` file was created, containing the master seed. This seed was then used to derive a few other things:
|
||||||
The `master_seed` was then used to derive a few other things:
|
|
||||||
|
|
||||||
- `vault_location` : in this case `43f73718397aa54d1b22` .
|
- **Vault location**: in this case `02f90c0603f4f2f60188`.
|
||||||
- Thus, if you use a different `master_seed`, another `vault_location` will be used that does not conflict with each other.
|
- 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).
|
||||||
- Example: `signer --signersecret /path/to/afile ...`
|
- **`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.
|
||||||
- `config.json` which is the encrypted key/value storage for configuration data, containing the key `ruleset_sha256`.
|
|
||||||
|
|
||||||
|
## Advanced rules
|
||||||
|
|
||||||
## Adding credentials
|
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`.
|
||||||
|
|
||||||
In order to make more useful rules like signing transactions, the signer needs access to the passwords needed to unlock keystores.
|
|
||||||
|
|
||||||
```text
|
```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
|
```js
|
||||||
function ApproveListing(){
|
function ApproveListing() {
|
||||||
return "Approve"
|
return "Approve"
|
||||||
}
|
}
|
||||||
function ApproveSignData(r){
|
|
||||||
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
|
function ApproveSignData(req) {
|
||||||
{
|
if (req.address.toLowerCase() == "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3") {
|
||||||
if(r.message.indexOf("bazonk") >= 0){
|
if (req.messages[0].value.indexOf("bazonk") >= 0) {
|
||||||
return "Approve"
|
return "Approve"
|
||||||
}
|
}
|
||||||
return "Reject"
|
return "Reject"
|
||||||
}
|
}
|
||||||
// Otherwise goes to manual processing
|
// Otherwise goes to manual processing
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example:
|
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
|
```text
|
||||||
#sha256sum rules.js
|
$ sha256sum rules.js
|
||||||
2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f rules.js
|
f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178 rules.js
|
||||||
|
|
||||||
#./signer attest 2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f
|
$ clef attest f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178
|
||||||
|
Decrypt master seed of clef
|
||||||
INFO [02-21|14:36:30] Ruleset attestation updated sha256=2a0cb661dacfc804b6e95d935d813fd17c0997a7170e4092ffbc34ca976acd9f
|
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 [07-01|14:12:41.636] Rule engine configured file=rules.js
|
||||||
INFO [09-25|21:02:16.466] Loaded 4byte db signatures=5509 file=./4byte.json
|
INFO [07-01|14:12:41.636] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false
|
||||||
INFO [09-25|21:02:16.467] Rule engine configured file=./rules.js
|
DEBUG[07-01|14:12:41.636] FS scan times list=46.722µs set=4.47µs diff=2.157µs
|
||||||
DEBUG[09-25|21:02:16.468] FS scan times list=1.45262ms set=21.926µs diff=6.944µs
|
DEBUG[07-01|14:12:41.637] Ledger support enabled
|
||||||
DEBUG[09-25|21:02:16.473] Ledger support enabled
|
DEBUG[07-01|14:12:41.637] Trezor support enabled via HID
|
||||||
DEBUG[09-25|21:02:16.475] Trezor support enabled
|
DEBUG[07-01|14:12:41.638] Trezor support enabled via WebUSB
|
||||||
INFO [09-25|21:02:16.476] Audit logs configured file=audit.log
|
INFO [07-01|14:12:41.638] Audit logs configured file=audit.log
|
||||||
DEBUG[09-25|21:02:16.476] HTTP registered namespace=account
|
DEBUG[07-01|14:12:41.638] IPC registered namespace=account
|
||||||
INFO [09-25|21:02:16.478] HTTP endpoint opened url=http://localhost:8550
|
INFO [07-01|14:12:41.638] IPC endpoint opened url=$HOME/.clef/clef.ipc
|
||||||
DEBUG[09-25|21:02:16.478] IPC registered namespace=account
|
|
||||||
INFO [09-25|21:02:16.478] IPC endpoint opened url=<nil>
|
|
||||||
------- Signer info -------
|
------- Signer info -------
|
||||||
* extapi_version : 2.0.0
|
* intapi_version : 7.0.0
|
||||||
* intapi_version : 2.0.0
|
* extapi_version : 6.0.0
|
||||||
* extapi_http : http://localhost:8550
|
* extapi_http : n/a
|
||||||
* extapi_ipc : <nil>
|
* 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/
|
$ echo '{"id": 1, "jsonrpc":"2.0", "method":"account_signData", "params":["data/plain", "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", "0x202062617a6f6e6b2062617a2067617a0a"]}' | nc -U ~/.clef/clef.ipc
|
||||||
{"jsonrpc":"2.0","id":67,"result":"0x93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c"}
|
{"jsonrpc":"2.0","id":1,"result":"0x4f93e3457027f6be99b06b3392d0ebc60615ba448bb7544687ef1248dea4f5317f789002df783979c417d969836b6fda3710f5bffb296b4d51c8aaae6e2ac4831c"}
|
||||||
|
|
||||||
#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", "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
|
```text
|
||||||
INFO [02-21|14:42:41] Op approved
|
INFO [02-21|14:42:41] Op approved
|
||||||
INFO [02-21|14:42:56] Op rejected
|
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:
|
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
|
```text
|
||||||
#tail -n 4 audit.log
|
$ 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=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=2018-02-21T14:42:42+0100 lvl=info msg=Sign api=signer type=response data=93e6161840c3ae1efc26dc68dedab6e8fc233bb3fefa1b4645dbf6609b93dace160572ea4ab33240256bb6d3dadb60dcd9c515d6374d3cf614ee897408d41d541c error=nil
|
t=2019-07-01T15:52:14+0300 lvl=info msg=SignData api=signer type=response data=4f93e3457027f6be99b06b3392d0ebc60615ba448bb7544687ef1248dea4f5317f789002df783979c417d969836b6fda3710f5bffb296b4d51c8aaae6e2ac4831c 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=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=2018-02-21T14:42:56+0100 lvl=info msg=Sign api=signer type=response data= error="Request denied"
|
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 <API endpoint>` 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.*
|
||||||
|
|
|
@ -24,7 +24,6 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
|
@ -44,7 +43,7 @@ const (
|
||||||
// ExternalAPIVersion -- see extapi_changelog.md
|
// ExternalAPIVersion -- see extapi_changelog.md
|
||||||
ExternalAPIVersion = "6.0.0"
|
ExternalAPIVersion = "6.0.0"
|
||||||
// InternalAPIVersion -- see intapi_changelog.md
|
// InternalAPIVersion -- see intapi_changelog.md
|
||||||
InternalAPIVersion = "6.0.0"
|
InternalAPIVersion = "7.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExternalAPI defines the external API through which signing requests are made.
|
// ExternalAPI defines the external API through which signing requests are made.
|
||||||
|
@ -234,7 +233,7 @@ type (
|
||||||
ContentType string `json:"content_type"`
|
ContentType string `json:"content_type"`
|
||||||
Address common.MixedcaseAddress `json:"address"`
|
Address common.MixedcaseAddress `json:"address"`
|
||||||
Rawdata []byte `json:"raw_data"`
|
Rawdata []byte `json:"raw_data"`
|
||||||
Message []*NameValueType `json:"message"`
|
Messages []*NameValueType `json:"messages"`
|
||||||
Hash hexutil.Bytes `json:"hash"`
|
Hash hexutil.Bytes `json:"hash"`
|
||||||
Meta Metadata `json:"meta"`
|
Meta Metadata `json:"meta"`
|
||||||
}
|
}
|
||||||
|
@ -477,22 +476,24 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
|
||||||
return modified
|
return modified
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *SignerAPI) lookupPassword(address common.Address) string {
|
func (api *SignerAPI) lookupPassword(address common.Address) (string, error) {
|
||||||
return api.credentials.Get(strings.ToLower(address.String()))
|
return api.credentials.Get(address.Hex())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) {
|
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
|
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
|
// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form
|
||||||
|
|
|
@ -169,13 +169,12 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
|
||||||
|
|
||||||
fmt.Printf("-------- Sign data request--------------\n")
|
fmt.Printf("-------- Sign data request--------------\n")
|
||||||
fmt.Printf("Account: %s\n", request.Address.String())
|
fmt.Printf("Account: %s\n", request.Address.String())
|
||||||
fmt.Printf("message:\n")
|
fmt.Printf("messages:\n")
|
||||||
for _, nvt := range request.Message {
|
for _, nvt := range request.Messages {
|
||||||
fmt.Printf("%v\n", nvt.Pprint(1))
|
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("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")
|
fmt.Printf("-------------------------------------------\n")
|
||||||
showMetadata(request.Meta)
|
showMetadata(request.Meta)
|
||||||
if !ui.confirm() {
|
if !ui.confirm() {
|
||||||
|
@ -187,7 +186,6 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
|
||||||
// ApproveListing prompt the user for confirmation to list accounts
|
// ApproveListing prompt the user for confirmation to list accounts
|
||||||
// the list of accounts to list can be modified by the UI
|
// the list of accounts to list can be modified by the UI
|
||||||
func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) {
|
func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) {
|
||||||
|
|
||||||
ui.mu.Lock()
|
ui.mu.Lock()
|
||||||
defer ui.mu.Unlock()
|
defer ui.mu.Unlock()
|
||||||
|
|
||||||
|
|
|
@ -123,11 +123,10 @@ type TypedDataDomain struct {
|
||||||
var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`)
|
var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`)
|
||||||
|
|
||||||
// sign receives a request and produces a signature
|
// sign receives a request and produces a signature
|
||||||
|
//
|
||||||
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
// 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.
|
// 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) {
|
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
|
// We make the request prior to looking up if we actually have the account, to prevent
|
||||||
// account-enumeration via the API
|
// account-enumeration via the API
|
||||||
res, err := api.UI.ApproveSignData(req)
|
res, err := api.UI.ApproveSignData(req)
|
||||||
|
@ -169,7 +168,6 @@ func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr com
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
signature, err := api.sign(addr, req, transformV)
|
signature, err := api.sign(addr, req, transformV)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.UI.ShowError(err.Error())
|
api.UI.ShowError(err.Error())
|
||||||
|
@ -202,7 +200,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
|
||||||
return nil, useEthereumV, err
|
return nil, useEthereumV, err
|
||||||
}
|
}
|
||||||
sighash, msg := SignTextValidator(validatorData)
|
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)",
|
Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)",
|
||||||
Typ: "description",
|
Typ: "description",
|
||||||
|
@ -224,7 +222,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
|
||||||
Value: fmt.Sprintf("0x%x", msg),
|
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:
|
case ApplicationClique.Mime:
|
||||||
// Clique is the Ethereum PoA standard
|
// Clique is the Ethereum PoA standard
|
||||||
stringData, ok := data.(string)
|
stringData, ok := data.(string)
|
||||||
|
@ -251,7 +249,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, useEthereumV, err
|
return nil, useEthereumV, err
|
||||||
}
|
}
|
||||||
message := []*NameValueType{
|
messages := []*NameValueType{
|
||||||
{
|
{
|
||||||
Name: "Clique header",
|
Name: "Clique header",
|
||||||
Typ: "clique",
|
Typ: "clique",
|
||||||
|
@ -260,7 +258,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
|
||||||
}
|
}
|
||||||
// Clique uses V on the form 0 or 1
|
// Clique uses V on the form 0 or 1
|
||||||
useEthereumV = false
|
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:
|
default: // also case TextPlain.Mime:
|
||||||
// Calculates an Ethereum ECDSA signature for:
|
// Calculates an Ethereum ECDSA signature for:
|
||||||
// hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}")
|
// 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
|
return nil, useEthereumV, err
|
||||||
} else {
|
} else {
|
||||||
sighash, msg := accounts.TextAndHash(textData)
|
sighash, msg := accounts.TextAndHash(textData)
|
||||||
message := []*NameValueType{
|
messages := []*NameValueType{
|
||||||
{
|
{
|
||||||
Name: "message",
|
Name: "message",
|
||||||
Typ: accounts.MimetypeTextPlain,
|
Typ: accounts.MimetypeTextPlain,
|
||||||
Value: msg,
|
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.Address = addr
|
||||||
req.Meta = MetadataFromContext(ctx)
|
req.Meta = MetadataFromContext(ctx)
|
||||||
return req, useEthereumV, nil
|
return req, useEthereumV, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignTextWithValidator signs the given message which can be further recovered
|
// 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)))
|
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
|
||||||
sighash := crypto.Keccak256(rawData)
|
sighash := crypto.Keccak256(rawData)
|
||||||
message, err := typedData.Format()
|
messages, err := typedData.Format()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
signature, err := api.sign(addr, req, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.UI.ShowError(err.Error())
|
api.UI.ShowError(err.Error())
|
||||||
|
|
|
@ -74,12 +74,28 @@ func (r *rulesetUI) execute(jsfunc string, jsarg interface{}) (otto.Value, error
|
||||||
|
|
||||||
// Instantiate a fresh vm engine every time
|
// Instantiate a fresh vm engine every time
|
||||||
vm := otto.New()
|
vm := otto.New()
|
||||||
|
|
||||||
// Set the native callbacks
|
// Set the native callbacks
|
||||||
consoleObj, _ := vm.Get("console")
|
consoleObj, _ := vm.Get("console")
|
||||||
consoleObj.Object().Set("log", consoleOutput)
|
consoleObj.Object().Set("log", consoleOutput)
|
||||||
consoleObj.Object().Set("error", 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
|
// Load bootstrap libraries
|
||||||
script, err := vm.Compile("bignumber.js", BigNumber_JS)
|
script, err := vm.Compile("bignumber.js", BigNumber_JS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -301,23 +301,23 @@ func TestStorage(t *testing.T) {
|
||||||
|
|
||||||
js := `
|
js := `
|
||||||
function testStorage(){
|
function testStorage(){
|
||||||
storage.Put("mykey", "myvalue")
|
storage.put("mykey", "myvalue")
|
||||||
a = storage.Get("mykey")
|
a = storage.get("mykey")
|
||||||
|
|
||||||
storage.Put("mykey", ["a", "list"]) // Should result in "a,list"
|
storage.put("mykey", ["a", "list"]) // Should result in "a,list"
|
||||||
a += storage.Get("mykey")
|
a += storage.get("mykey")
|
||||||
|
|
||||||
|
|
||||||
storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]"
|
storage.put("mykey", {"an": "object"}) // Should result in "[object Object]"
|
||||||
a += storage.Get("mykey")
|
a += storage.get("mykey")
|
||||||
|
|
||||||
|
|
||||||
storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
|
storage.put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
|
||||||
a += storage.Get("mykey")
|
a += storage.get("mykey")
|
||||||
|
|
||||||
a += storage.Get("missingkey") //Missing keys should result in empty string
|
a += storage.get("missingkey") //Missing keys should result in empty string
|
||||||
storage.Put("","missing key==noop") // Can't store with 0-length key
|
storage.put("","missing key==noop") // Can't store with 0-length key
|
||||||
a += storage.Get("") // Should result in ''
|
a += storage.get("") // Should result in ''
|
||||||
|
|
||||||
var b = new BigNumber(2)
|
var b = new BigNumber(2)
|
||||||
var c = new BigNumber(16)//"0xf0",16)
|
var c = new BigNumber(16)//"0xf0",16)
|
||||||
|
@ -337,7 +337,6 @@ func TestStorage(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %v", err)
|
t.Errorf("Unexpected error %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
retval, err := v.ToString()
|
retval, err := v.ToString()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -369,7 +368,7 @@ const ExampleTxWindow = `
|
||||||
var windowstart = new Date().getTime() - window;
|
var windowstart = new Date().getTime() - window;
|
||||||
|
|
||||||
var txs = [];
|
var txs = [];
|
||||||
var stored = storage.Get('txs');
|
var stored = storage.get('txs');
|
||||||
|
|
||||||
if(stored != ""){
|
if(stored != ""){
|
||||||
txs = JSON.parse(stored)
|
txs = JSON.parse(stored)
|
||||||
|
@ -414,19 +413,18 @@ const ExampleTxWindow = `
|
||||||
var value = big(resp.tx.value)
|
var value = big(resp.tx.value)
|
||||||
var txs = []
|
var txs = []
|
||||||
// Load stored transactions
|
// Load stored transactions
|
||||||
var stored = storage.Get('txs');
|
var stored = storage.get('txs');
|
||||||
if(stored != ""){
|
if(stored != ""){
|
||||||
txs = JSON.parse(stored)
|
txs = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
// Add this to the storage
|
// Add this to the storage
|
||||||
txs.push({tstamp: new Date().getTime(), value: value});
|
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 {
|
func dummyTx(value hexutil.Big) *core.SignTxRequest {
|
||||||
|
|
||||||
to, _ := mixAddr("000000000000000000000000000000000000dead")
|
to, _ := mixAddr("000000000000000000000000000000000000dead")
|
||||||
from, _ := mixAddr("000000000000000000000000000000000000dead")
|
from, _ := mixAddr("000000000000000000000000000000000000dead")
|
||||||
n := hexutil.Uint64(3)
|
n := hexutil.Uint64(3)
|
||||||
|
@ -448,28 +446,27 @@ func dummyTx(value hexutil.Big) *core.SignTxRequest {
|
||||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
|
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)
|
v := big.NewInt(0).SetUint64(value)
|
||||||
h := hexutil.Big(*v)
|
h := hexutil.Big(*v)
|
||||||
return dummyTx(h)
|
return dummyTx(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dummySigned(value *big.Int) *types.Transaction {
|
func dummySigned(value *big.Int) *types.Transaction {
|
||||||
to := common.HexToAddress("000000000000000000000000000000000000dead")
|
to := common.HexToAddress("000000000000000000000000000000000000dead")
|
||||||
gas := uint64(21000)
|
gas := uint64(21000)
|
||||||
gasPrice := big.NewInt(2000000)
|
gasPrice := big.NewInt(2000000)
|
||||||
data := make([]byte, 0)
|
data := make([]byte, 0)
|
||||||
return types.NewTransaction(3, to, value, gas, gasPrice, data)
|
return types.NewTransaction(3, to, value, gas, gasPrice, data)
|
||||||
|
|
||||||
}
|
}
|
||||||
func TestLimitWindow(t *testing.T) {
|
|
||||||
|
|
||||||
|
func TestLimitWindow(t *testing.T) {
|
||||||
r, err := initRuleEngine(ExampleTxWindow)
|
r, err := initRuleEngine(ExampleTxWindow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Couldn't create evaluator %v", err)
|
t.Errorf("Couldn't create evaluator %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0.3 ether: 429D069189E0000 wei
|
// 0.3 ether: 429D069189E0000 wei
|
||||||
v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
|
v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
|
||||||
h := hexutil.Big(*v)
|
h := hexutil.Big(*v)
|
||||||
|
@ -496,7 +493,6 @@ func TestLimitWindow(t *testing.T) {
|
||||||
if resp.Approved {
|
if resp.Approved {
|
||||||
t.Errorf("Expected check to resolve to 'Reject'")
|
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
|
// 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")
|
d.t.Fatalf("Did not expect next-handler to be called")
|
||||||
return core.UserInputResponse{}, nil
|
return core.UserInputResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) {
|
func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -589,7 +586,7 @@ func TestSignData(t *testing.T) {
|
||||||
function ApproveSignData(r){
|
function ApproveSignData(r){
|
||||||
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
|
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
|
||||||
{
|
{
|
||||||
if(r.message[0].value.indexOf("bazonk") >= 0){
|
if(r.messages[0].value.indexOf("bazonk") >= 0){
|
||||||
return "Approve"
|
return "Approve"
|
||||||
}
|
}
|
||||||
return "Reject"
|
return "Reject"
|
||||||
|
@ -615,11 +612,11 @@ function ApproveSignData(r){
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
resp, err := r.ApproveSignData(&core.SignDataRequest{
|
resp, err := r.ApproveSignData(&core.SignDataRequest{
|
||||||
Address: *addr,
|
Address: *addr,
|
||||||
Message: nvt,
|
Messages: nvt,
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
|
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
|
||||||
Rawdata: []byte(rawdata),
|
Rawdata: []byte(rawdata),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error %v", err)
|
t.Fatalf("Unexpected error %v", err)
|
||||||
|
|
|
@ -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) {
|
func (s *AESEncryptedStorage) Put(key, value string) {
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return
|
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
|
// Get returns the previously stored value, or an error if it does not exist or
|
||||||
func (s *AESEncryptedStorage) Get(key string) string {
|
// key is of 0-length.
|
||||||
|
func (s *AESEncryptedStorage) Get(key string) (string, error) {
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return ""
|
return "", ErrZeroKey
|
||||||
}
|
}
|
||||||
data, err := s.readEncryptedStorage()
|
data, err := s.readEncryptedStorage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
|
||||||
return ""
|
return "", err
|
||||||
}
|
}
|
||||||
encrypted, exist := data[key]
|
encrypted, exist := data[key]
|
||||||
if !exist {
|
if !exist {
|
||||||
log.Warn("Key does not exist", "key", key)
|
log.Warn("Key does not exist", "key", key)
|
||||||
return ""
|
return "", ErrNotFound
|
||||||
}
|
}
|
||||||
entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key))
|
entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to decrypt key", "key", key)
|
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
|
// readEncryptedStorage reads the file with encrypted creds
|
||||||
|
|
|
@ -110,8 +110,8 @@ func TestEnd2End(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s1.Put("bazonk", "foobar")
|
s1.Put("bazonk", "foobar")
|
||||||
if v := s2.Get("bazonk"); v != "foobar" {
|
if v, err := s2.Get("bazonk"); v != "foobar" || err != nil {
|
||||||
t.Errorf("Expected bazonk->foobar, got '%v'", v)
|
t.Errorf("Expected bazonk->foobar (nil error), got '%v' (%v error)", v, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,11 +154,11 @@ func TestSwappedKeys(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
swap()
|
swap()
|
||||||
if v := s1.Get("k1"); v != "" {
|
if v, _ := s1.Get("k1"); v != "" {
|
||||||
t.Errorf("swapped value should return empty")
|
t.Errorf("swapped value should return empty")
|
||||||
}
|
}
|
||||||
swap()
|
swap()
|
||||||
if v := s1.Get("k1"); v != "v1" {
|
if v, _ := s1.Get("k1"); v != "v1" {
|
||||||
t.Errorf("double-swapped value should work fine")
|
t.Errorf("double-swapped value should work fine")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,26 @@
|
||||||
|
|
||||||
package storage
|
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 {
|
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)
|
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
|
// EphemeralStorage is an in-memory storage that does
|
||||||
|
@ -31,23 +46,29 @@ type EphemeralStorage struct {
|
||||||
namespace string
|
namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Put stores a value by key. 0-length keys results in noop.
|
||||||
func (s *EphemeralStorage) Put(key, value string) {
|
func (s *EphemeralStorage) Put(key, value string) {
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//fmt.Printf("storage: put %v -> %v\n", key, value)
|
|
||||||
s.data[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 {
|
if len(key) == 0 {
|
||||||
return ""
|
return "", ErrZeroKey
|
||||||
}
|
}
|
||||||
//fmt.Printf("storage: get %v\n", key)
|
if v, ok := s.data[key]; ok {
|
||||||
if v, exist := s.data[key]; exist {
|
return v, nil
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
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 {
|
func NewEphemeralStorage() Storage {
|
||||||
|
@ -61,6 +82,7 @@ func NewEphemeralStorage() Storage {
|
||||||
type NoStorage struct{}
|
type NoStorage struct{}
|
||||||
|
|
||||||
func (s *NoStorage) Put(key, value string) {}
|
func (s *NoStorage) Put(key, value string) {}
|
||||||
func (s *NoStorage) Get(key string) string {
|
func (s *NoStorage) Del(key string) {}
|
||||||
return ""
|
func (s *NoStorage) Get(key string) (string, error) {
|
||||||
|
return "", errors.New("I forgot")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue