From 00d884f570781c36ae1fe476bd6fa2c5f43f8a99 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 20 Jun 2022 10:16:21 +0200 Subject: [PATCH] docs/clef: docs about clef/clique signing (#25121) * docs/clef: docs about clef/clique signing * Update CliqueSigning.md Co-authored-by: Marius van der Wijden --- docs/_clef/CliqueSigning.md | 393 ++++++++++++++++++++++++++++++++++++ docs/_clef/Setup.md | 2 +- docs/_clef/apis.md | 2 +- docs/_clef/datatypes.md | 2 +- 4 files changed, 396 insertions(+), 3 deletions(-) create mode 100644 docs/_clef/CliqueSigning.md diff --git a/docs/_clef/CliqueSigning.md b/docs/_clef/CliqueSigning.md new file mode 100644 index 0000000000000..a4e4cf33ccf04 --- /dev/null +++ b/docs/_clef/CliqueSigning.md @@ -0,0 +1,393 @@ +--- +title: Clique-signing with Clef +sort_key: C +--- + +The 'classic' way to sign PoA blocks is to use the "unlock"-feature of `geth`. This is a highly dangerous thing to do, because "unlock" is totally un-discriminatory. Meaning: if an account is unlocked and an attacker obtains access to the RPC api, the attacker can have anything signed by that account, without supplying a password. + +The idea with `clef` was to remove the `unlock` capability, yet still provide sufficient usability to make it possible to automate some things while maintaining a high level of security. This post will show how to integrate `clef` as a sealer of clique-blocks. + +## Part 0: Prepping a Clique network + +Feel free to skip this section if you already have a Clique-network. + +First of all, we'll set up a rudimentary testnet to have something to sign on. We create a new keystore (password `testtesttest`) +``` +$ geth account new --datadir ./ddir +INFO [06-16|11:10:39.600] Maximum peer count ETH=50 LES=0 total=50 +Your new account is locked with a password. Please give a password. Do not forget this password. +Password: +Repeat password: + +Your new key was generated + +Public address of the key: 0x9CD932F670F7eDe5dE86F756A6D02548e5899f47 +Path of the secret key file: ddir/keystore/UTC--2022-06-16T09-10-48.578523828Z--9cd932f670f7ede5de86f756a6d02548e5899f47 + +- You can share your public address with anyone. Others need it to interact with you. +- You must NEVER share the secret key with anyone! The key controls access to your funds! +- You must BACKUP your key file! Without the key, it's impossible to access account funds! +- You must REMEMBER your password! Without the password, it's impossible to decrypt the key! +``` + +And create a genesis with that account as a sealer: +```json +{ + "config": { + "chainId": 15, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "clique": { + "period": 30, + "epoch": 30000 + } + }, + "difficulty": "1", + "gasLimit": "8000000", + "extradata": "0x00000000000000000000000000000000000000000000000000000000000000009CD932F670F7eDe5dE86F756A6D02548e5899f470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "alloc": { + "0x9CD932F670F7eDe5dE86F756A6D02548e5899f47": { + "balance": "300000000000000000000000000000000" + } + } +} +``` +And init `geth` +``` +$ geth --datadir ./ddir init genesis.json +... +INFO [06-16|11:14:54.123] Writing custom genesis block +INFO [06-16|11:14:54.125] Persisted trie from memory database nodes=1 size=153.00B time="64.715µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B +INFO [06-16|11:14:54.125] Successfully wrote genesis state database=lightchaindata hash=187412..4deb98 +``` + +At this point, we have a Clique network which we can start sealing on. + +## Part 1: Prepping Clef + +In order to make use of `clef` for signing, we need to do a couple of things. + +1. Make sure that `clef` knows the password for the keystore. +2. Make sure that `clef` auto-approves clique signing requests. + +These two things are independent of each other. First of all, however, we need to `init` clef (for this test I use the password `clefclefclef`) + +``` +$ clef --keystore ./ddir/keystore --configdir ./clef --chainid 15 --suppress-bootwarn init + +The master seed of clef will be locked with a password. +Please specify a password. Do not forget this password! +Password: +Repeat password: + +A master seed has been generated into clef/masterseed.json + +This is required to be able to store credentials, such as: +* Passwords for keystores (used by rule engine) +* Storage for JavaScript auto-signing rules +* Hash of JavaScript rule-file + +You should treat 'masterseed.json' with utmost secrecy and make a backup of it! +* The password is necessary but not enough, you need to back up the master seed too! +* The master seed does not contain your accounts, those need to be backed up separately! +``` + +After this operation, `clef` has it's own vault where it can store secrets and attestations, which we will utilize going forward. + +### Storing passwords in `clef` + +With that done, we can now make `clef` aware of the password. We invoke `setpw
` to store a password for a given address. `clef` asks for the password, and it also asks for the clef master-password, in order to update and store the new secrets inside clef vault. + +``` +$ clef --keystore ./ddir/keystore --configdir ./clef --chainid 15 --suppress-bootwarn setpw 0x9CD932F670F7eDe5dE86F756A6D02548e5899f47 + +Please enter a password to store for this address: +Password: +Repeat password: + +Decrypt master seed of clef +Password: +INFO [06-16|11:27:09.153] Credential store updated set=0x9CD932F670F7eDe5dE86F756A6D02548e5899f47 + +``` + +At this point, if we were to use clef as a sealer, we would be forced to manually click Approve for each block, but we would not be required to provide the password. + +#### Testing stored password + +Let's test using the stored password when sealing Clique-blocks. Start `clef` with +``` +$ clef --keystore ./ddir/keystore --configdir ./clef --chainid 15 --suppress-bootwarn +``` +And start `geth` with +``` +$ geth --datadir ./ddir --signer ./clef/clef.ipc --mine +``` + +Geth will ask what accounts are present, to which we need to manually enter `y` to approve: + +``` +-------- List Account request-------------- +A request has been made to list all accounts. +You can select which accounts the caller can see + [x] 0x9CD932F670F7eDe5dE86F756A6D02548e5899f47 + URL: keystore:///home/user/tmp/clique_clef/ddir/keystore/UTC--2022-06-16T09-10-48.578523828Z--9cd932f670f7ede5de86f756a6d02548e5899f47 +------------------------------------------- +Request context: + NA -> ipc -> NA + +Additional HTTP header data, provided by the external caller: + User-Agent: "" + Origin: "" +Approve? [y/N]: +> y +DEBUG[06-16|11:36:42.499] Served account_list reqid=2 duration=3.213768195s +``` + +After this, `geth` will start asking `clef` to sign things: + +``` +-------- Sign data request-------------- +Account: 0x9CD932F670F7eDe5dE86F756A6D02548e5899f47 [chksum ok] +messages: +  Clique header [clique]: "clique header 1 [0x9b08fa3705e8b6e1b327d84f7936c21a3cb11810d9344dc4473f78f8da71e571]" +raw data: + "\xf9\x02\x14\xa0\x18t\x12:\x91f\xa2\x90U\b\xf9\xac\xc02i\xffs\x9f\xf4\xc9⮷!\x0f\x16\xaa?#M똠\x1d\xccM\xe8\xde\xc7]z\xab\x85\xb5g\xb6\xcc\xd4\x1a\xd3\x12E\x1b\x94\x8at\x13\xf0\xa1B\xfd@ԓG\x94\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0]1%\n\xfc\xee'\xd0e\xce\xc7t\xcc\\?\t4v\x8f\x06\xcb\xf8\xa0P5\xfeN\xea\x0ff\xfe\x9c\xa0V\xe8\x1f\x17\x1b\xccU\xa6\xff\x83E\xe6\x92\xc0\xf8n[H\xe0\x1b\x99l\xad\xc0\x01b/\xb5\xe3c\xb4!\xa0V\xe8\x1f\x17\x1b\xccU\xa6\xff\x83E\xe6\x92\xc0\xf8n[H\xe0\x1b\x99l\xad\xc0\x01b/\xb5\xe3c\xb4!\xb9\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x01\x83z0\x83\x80\x84b\xaa\xf9\xaa\xa0\u0603\x01\n\x14\x84geth\x88go1.18.1\x85linux\x00\x00\x00\x00\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88\x00\x00\x00\x00\x00\x00\x00\x00" +data hash: 0x9589ed81e959db6330b3d70e5f8e426fb683d03512f203009f7e41fc70662d03 +------------------------------------------- +Request context: + NA -> ipc -> NA + +Additional HTTP header data, provided by the external caller: + User-Agent: "" + Origin: "" +Approve? [y/N]: +> y +``` +And indeed, after approving with `y`, we are not required to provide the password -- the signed block is returned to geth: +``` +INFO [06-16|11:36:46.714] Successfully sealed new block number=1 sealhash=9589ed..662d03 hash=bd20b9..af8b87 elapsed=4.214s +``` +This mode of operation is somewhat unusable, since we'd need to keep "Approving" each block to be sealed. So let's fix that too. + +### Using rules to approve blocks + +The basic idea with clef rules, is to let a piece of javascript take over the Approve/Deny decision. The javascript snippet has access to the same information as the manual operator. + +Let's try with a simplistic first approach, which approves listing, and spits out the request data for `ApproveListing` + +```js +function ApproveListing(){ + return "Approve" +} + +function ApproveSignData(r){ + console.log("In Approve Sign data") + console.log(JSON.stringify(r)) +} +``` +In order to use a certain rule-file, we must first `attest` it. This is to prevent someone from modifying a ruleset-file on disk after creation. +``` +$ clef --keystore ./ddir/keystore --configdir ./clef --chainid 15 --suppress-bootwarn attest `sha256sum rules.js | cut -f1` +Decrypt master seed of clef +Password: +INFO [06-16|13:49:00.298] Ruleset attestation updated sha256=54aae496c3f0eda063a62c73ee284ca9fae3f43b401da847ef30ea30e85e35d1 +``` +And then we can start clef, pointing out the `rules.js` file. OBS: if you later modify this file, you need to redo the `attest`-step. +``` +$ clef --keystore ./ddir/keystore --configdir ./clef --chainid 15 --suppress-bootwarn --rules ./rules.js +``` + +Once `geth` starts asking it to seal blocks, we will now see the data. And from that, we can decide on how to make a rule which allows signing clique headers but nothing else. + +The actual data that gets passed to the js environment (and which our ruleset spit out to the console) looks like this: +```json +{ + "content_type": "application/x-clique-header", + "address": "0x9CD932F670F7eDe5dE86F756A6D02548e5899f47", + "raw_data": "+QIUoL0guY+66jZpzZh1wDX4Si/ycX4zD8FQqF/1Apy/r4uHoB3MTejex116q4W1Z7bM1BrTEkUblIp0E/ChQv1A1JNHlAAAAAAAAAAAAAAAAAAAAAAAAAAAoF0xJQr87ifQZc7HdMxcPwk0do8Gy/igUDX+TuoPZv6coFboHxcbzFWm/4NF5pLA+G5bSOAbmWytwAFiL7XjY7QhoFboHxcbzFWm/4NF5pLA+G5bSOAbmWytwAFiL7XjY7QhuQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICg3pPDoCEYqsY1qDYgwEKFIRnZXRoiGdvMS4xOC4xhWxpbnV4AAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAA==", + "messages": [ + { + "name": "Clique header", + "value": "clique header 2 [0xae525b65bc7f711bc136f502650039cd6959c3abc28fdf0ebfe2a5f85c92f3b6]", + "type": "clique" + } + ], + "call_info": null, + "hash": "0x8ca6c78af7d5ae67ceb4a1e465a8b639b9fbdec4b78e4d19cd9b1232046fbbf4", + "meta": { + "remote": "NA", + "local": "NA", + "scheme": "ipc", + "User-Agent": "", + "Origin": "" + } +} +``` + +If we wanted our js to be extremely trustless/paranoid, we could (inside the javascript) take the `raw_data` and verify that it's the rlp structure for a clique header: + +``` + echo "+QIUoL0guY+66jZpzZh1wDX4Si/ycX4zD8FQqF/1Apy/r4uHoB3MTejex116q4W1Z7bM1BrTEkUblIp0E/ChQv1A1JNHlAAAAAAAAAAAAAAAAAAAAAAAAAAAoF0xJQr87ifQZc7HdMxcPwk0do8Gy/igUDX+TuoPZv6coFboHxcbzFWm/4NF5pLA+G5bSOAbmWytwAFiL7XjY7QhoFboHxcbzFWm/4NF5pLA+G5bSOAbmWytwAFiL7XjY7QhuQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICg3pPDoCEYqsY1qDYgwEKFIRnZXRoiGdvMS4xOC4xhWxpbnV4AAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAA==" | base64 -d | rlpdump +[ + bd20b98fbaea3669cd9875c035f84a2ff2717e330fc150a85ff5029cbfaf8b87, + 1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, + 0000000000000000000000000000000000000000, + 5d31250afcee27d065cec774cc5c3f0934768f06cbf8a05035fe4eea0f66fe9c, + 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421, + 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421, + 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, + 02, + 02, + 7a4f0e, + "", + 62ab18d6, + d883010a14846765746888676f312e31382e31856c696e757800000000000000, + 0000000000000000000000000000000000000000000000000000000000000000, + 0000000000000000, +] +``` +However, we can also use the `messages`. They do not come from the external caller, but are generated from the `clef` internals: `clef` parsed the incoming request and verified the Clique wellformedness of the content. So we let's just check for such a message: + +```js +function OnSignerStartup(info){} + +function ApproveListing(){ + return "Approve" +} + +function ApproveSignData(r){ + if (r.content_type == "application/x-clique-header"){ + for(var i = 0; i < r.messages.length; i++){ + var msg = r.messages[i] + if (msg.name=="Clique header" && msg.type == "clique"){ + return "Approve" + } + } + } + return "Reject" +} +``` +Attest +``` +$ clef --keystore ./ddir/keystore --configdir ./clef --chainid 15 --suppress-bootwarn attest `sha256sum rules.js | cut -f1` +Decrypt master seed of clef +Password: +INFO [06-16|14:18:53.476] Ruleset attestation updated sha256=7d5036d22d1cc66599e7050fb1877f4e48b89453678c38eea06e3525996c2379 +``` +Run clef +``` +$ clef --keystore ./ddir/keystore --configdir ./clef --chainid 15 --suppress-bootwarn --rules ./rules.js + +``` +Run geth +``` +$ geth --datadir ./ddir --signer ./clef/clef.ipc --mine +``` +And you should now see `clef` happily signing blocks: +``` +DEBUG[06-16|14:20:02.136] Served account_version reqid=1 duration="131.38µs" +INFO [06-16|14:20:02.289] Op approved +DEBUG[06-16|14:20:02.289] Served account_list reqid=2 duration=4.672441ms +INFO [06-16|14:20:02.303] Op approved +DEBUG[06-16|14:20:03.450] Served account_signData reqid=3 duration=1.152074109s +INFO [06-16|14:20:03.456] Op approved +DEBUG[06-16|14:20:04.267] Served account_signData reqid=4 duration=815.874746ms +INFO [06-16|14:20:32.823] Op approved +DEBUG[06-16|14:20:33.584] Served account_signData reqid=5 duration=766.840681ms + +``` +### Further refinements + + +If an attacker find the clef "external" interface (which would only happen if you start it with `http` enabled) , he +- cannot make it sign arbitrary transactions, +- cannot sign arbitrary data message, + +However, he could still make it sign e.g. 1000 versions of a certain block height, making the chain very unstable. + +It is possible for rule execution to be stateful -- storing data. In this case, one could for example store what block heights have been sealed, and thus reject sealing a particular block height twice. In other words, we can use these rules to build our own version of an Execution-Layer slashing-db. + +We simply split the `clique header 2 [0xae525b65bc7f711bc136f502650039cd6959c3abc28fdf0ebfe2a5f85c92f3b6]` line, and store/check the number, using `storage.get` and `storage.put`: + +```js +function OnSignerStartup(info){} + +function ApproveListing(){ + return "Approve" +} + +function ApproveSignData(r){ + + if (r.content_type != "application/x-clique-header"){ + return "Reject" + } + for(var i = 0; i < r.messages.length; i++){ + var msg = r.messages[i] + if (msg.name=="Clique header" && msg.type == "clique"){ + var number = parseInt(msg.value.split(" ")[2]) + var latest = storage.get("lastblock") || 0 + console.log("number", number, "latest", latest) + if ( number > latest ){ + storage.put("lastblock", number) + return "Approve" + } + } + } + return "Reject" +} +``` +Running with this ruleset: +``` +JS:> number 45 latest 44 +INFO [06-16|22:26:43.023] Op approved +DEBUG[06-16|22:26:44.305] Served account_signData reqid=3 duration=1.287465394s +JS:> number 46 latest 45 +INFO [06-16|22:26:44.313] Op approved +DEBUG[06-16|22:26:45.317] Served account_signData reqid=4 duration=1.010612774s +``` +This might be a bit over-the-top, security-wise, and may cause problems, if for some reason a clique-deadlock needs to be resolved by rolling back and continuing on a side-chain. It is mainly meant as a demonstration that rules can use javascript and statefulness to construct very intricate signing logic. + + +### TLDR quick-version + +Creation and attestation is a one-off event: +```bash +## Create the rules-file +cat << END > rules.js +function OnSignerStartup(info){} + +function ApproveListing(){ + return "Approve" +} + +function ApproveSignData(r){ + if (r.content_type == "application/x-clique-header"){ + for(var i = 0; i < r.messages.length; i++){ + var msg = r.messages[i] + if (msg.name=="Clique header" && msg.type == "clique"){ + return "Approve" + } + } + } + return "Reject" +} +END +## Attest it, assumes clef master password is in `./clefpw` +clef --keystore ./ddir/keystore \ + --configdir ./clef --chainid 15 \ + --suppress-bootwarn --signersecret ./clefpw \ + attest `sha256sum rules.js | cut -f1` +``` +The normal startup command for `clef`: +```bash +clef --keystore ./ddir/keystore \ + --configdir ./clef --chainid 15 \ + --suppress-bootwarn --signersecret ./clefpw --rules ./rules.js +``` +For `geth`, the only change is to provide `--signer `. diff --git a/docs/_clef/Setup.md b/docs/_clef/Setup.md index 948cb17057dbe..ded3652eeea04 100644 --- a/docs/_clef/Setup.md +++ b/docs/_clef/Setup.md @@ -1,6 +1,6 @@ --- title: Advanced setup -sort_key: B +sort_key: D --- diff --git a/docs/_clef/apis.md b/docs/_clef/apis.md index 3284cc52f0f46..d8556e799bfb4 100644 --- a/docs/_clef/apis.md +++ b/docs/_clef/apis.md @@ -1,6 +1,6 @@ --- title: Communication APIs -sort_key: C +sort_key: E --- ### External API diff --git a/docs/_clef/datatypes.md b/docs/_clef/datatypes.md index db3901ea82c1f..bdf8e2782df51 100644 --- a/docs/_clef/datatypes.md +++ b/docs/_clef/datatypes.md @@ -1,6 +1,6 @@ --- title: Communication data types -sort_key: C +sort_key: F --- ## UI Client interface