Lab 6: Signing messages
Today’s lab is about adding digital signatures to messages on the GoatChain.
New testing tools!
There are two new servers running that will hopefully help you with testing your own work:
Server with unchanging head
Host horse
port 5002
hosts a server with about 20 valid nodes, including version 0 and version 1 nodes, and version 1 signed nodes following today’s lab.
This server is fully function and will handle /head
, /fetch
, and /push
requests, except the head will never be updated. That way, you know the chain won’t grow crazy-long and the blocks served are always consistent. At the same time, since it accepts /push
requests for new blocks (which will be cached), you can test that your programs to create new nodes also work. Note that any kind of valid block (version 0, version 1, or version 1 signed) are accepted.
Checking server
Host horse
port 5000
hosts a special server that will connect to another server of your choosing and perform a bunch of tests on that.
To use it, you type something like this at the command line:
curl http://horse:5000/check/HOST/PORT
where you would replace HOST with your own hostname (e.g. cat
) and PORT with the port your server is running on (e.g. 5002
).
Please don’t abuse this service; if everyone is constantly running this to check their server, then horse
will grind to a halt. Think of this as a way to check yourself AFTER you have done your own testing and feel like your server is working well.
Background: Digital signatures with PyNaCl
We will use ed25519 digital signatures, where:
- A signing key is always 32 bytes
- A verification key is always 32 bytes
- A signature is always 64 bytes
- Messages can be any length
There is an excellent implementation of ed25519 signatures in the PyNaCl library. Similar packages based on libsodium are available for many other programming languages as well.
Here is the documentation for how to do message signing in PyNaCl, complete with examples!
There are just a few things to look out for when using this for our lab:
- Signing and verification keys are Python objects. To get the actual hex encoding, you have to call
.encode().hex()
. - To re-load a key from its hex encoding, you have to first encode the hex string to bytes using
bytes.fromhex(hex_string)
, and then call the constructor likeSigningKey(bytes_object)
. - The
sign()
method returns a signed message object, that includes the signature as well as the original message. We will be keeping those separate, so you will want to do.signature.hex()
to get the (hex-encoded) signature only.
Signatures in payloads
We are going to add digital signatures to the "payload"
field of blocks in the class blockchain. Right now, a valid block (version 1) might look something like this:
{
"payload": {
"chat": "Roses are red"
},
"prev_hash": "000000a5919b38ddc98f4fdadd10ab507141f95908c908cd6994ea58e4e034b3",
"version": 1,
"nonce": 1285490
}
The problem is, we don’t know who sent this chat message! So, we will add two additional fields inside the payload dictionary:
chatid
is a hex-encoded ed25519 public keychatsig
is a hex-encoded ed25519 signature of the"chat"
message, which should verify with the given public key
Here is what that might look like:
{
"payload": {
"chat": "Violets are blue",
"chatid": "96cb3cc996ae8807d59e38c982f0588a5824fa6aa83e8f436abbe123f0f9778a",
"chatsig": "33a2ed0b41431c85a5a5cc016cd318daf0db6ae788009b0d1bb5fa9195950353b7d83497bf223c5156509cb4e94a9d7bbe0a8ac929881e3bd8950a0bb0a8b200"
},
"prev_hash": "00000072ecd04753e28f65f6b0c0d8424c1403b9a0413a4d251b42c57ebda34b",
"version": 1,
"nonce": 24838047
}
These are real block values - you should be able to verify this signature using PyNaCl. I encourage you to actually try it and make sure you understand how PyNaCl works!
Part 1: Update server
Update your server.py
so that is checks signatures during new /push
requests
Specifically, update the part of your code that verifies a block is valid, according to these new rules:
- If any payload has a
chatid
orchatsig
field, then the payload must have both of those as well aschat
- If any payload has those three fields, then the
chatsig
must be a valid (64-byte) ed25519 signature of the message in thechat
field, as verified by the verification key in thechatid
field.
Check the links above on how to use PyNaCl to verify ed25519 signatures. After this, your server should pass the “server checks” from horse:5000
- use that feedback and ask if you have questions!
Part 2: Generate your own keys
Write a new program keygen.py
that works like this:
- It takes a single filename as a command-line argument
- If the file doesn’t exist yet, then generate a new signing key (using PyNaCl), write the hex of that key to the file, and then print out the public verifiaction key hex to standard out.
- If that file already exists, then it reads the private signing key hex string from the file, then uses PyNaCl to display the corresponding public verification key in hex.
Notice: the secret private signing key should never be printed out to the terminal directly from your program - that should just reside in the file itself.
Here is a transcript of me running this. Of course, you should get different key values randomly when you run it yourself!
$ ./keygen.py mykey.txt
generating mykey.txt...
mykey.txt 82fbb5f7568c32e4cf43079039f24b1d8d921a1ef4ee6ba92c620d41eb1e8f86
$ cat mykey.txt
ef60d806c6d9036a072b43d7743042bee47dd8471485f80669da89e1368c150b
$ ./keygen.py mykey.txt
mykey.txt 82fbb5f7568c32e4cf43079039f24b1d8d921a1ef4ee6ba92c620d41eb1e8f86
Notice: the first time it actually generates a new file, and the second time it is just reading from the existing file. Also notice that the signing key (inside the file) and verification key (printed on the terminal) are both 32 bytes, but they are not the same!
Part 3: Send signed messages
Update your send_chat.py
program so that it uses a signing key (provided as a command-line argument) to generate a valid signed chat message, and then push that to all servers that will take it.
You should only have to change a small part of your code here, just the part that generates the payload inside the block! The rest of the logic should stay exactly the same.
Part 4: Show signed messages
Make a new program show_signed.py
that works similarly to show_chat.py
from previous labs, except that:
- Don’t bother with
current.json
or ignoring previously-seen messages. - Only blocks that contain a (valid) signed message should be displayed We don’t trust those unsigned chat messages, so don’t show them!
- Along with each valid signed message, show the public key hash of the signer. These are like usernames in a public chat window!
Again, this should not be very complicated if you already have the previous parts completed. It should be very similar to the previous show_chat.py
, except with an extra if statement in the loop to print out the messages.
(If you have designed things well, you should be able to re-use your work to verify signatures from part 1!)