SI 486I Spring 2022 / Labs


This is the archived website of SI 486I from the Spring 2022 semester. Feel free to browse around; you may also find more recent offerings at my teaching page.

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 like SigningKey(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 key
  • chatsig 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 or chatsig field, then the payload must have both of those as well as chat
  • If any payload has those three fields, then the chatsig must be a valid (64-byte) ed25519 signature of the message in the chat field, as verified by the verification key in the chatid 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!)