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:
Péter Szilágyi 2019-07-02 14:01:47 +03:00 committed by GitHub
parent 6bf5555c4f
commit a0943b8932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 709 additions and 526 deletions

View File

@ -1,31 +1,27 @@
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 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.
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), 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.
or a separate VM in a [QubesOS](https://www.qubes-os.org/) type os setup.
Check out 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.
* the [tutorial](tutorial.md) for some concrete examples on how the signer works. Check out the
* 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 * [CLI tutorial](tutorial.md) for some concrete examples on how Clef works.
clef and an external UI * [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
@ -33,9 +29,10 @@ 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
@ -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:

View File

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

View File

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

View File

@ -1,4 +1,19 @@
### Changelog for internal API (ui-api) ## Changelog for internal API (ui-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.
### 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 ### 6.0.0
@ -51,7 +66,7 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth
* The type `Account` was modified (the json-field `type` was removed), to consist of * The type `Account` was modified (the json-field `type` was removed), to consist of
```golang ```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
@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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