Toycoin Part 1: Crypto Basics
Note: the toycoin
series of posts is for learning / illustrative purposes only; no part of it should be considered secure or useful for real-world purposes.
As I started to learn a bit about blockchain technology recently, I decided to try a toy blockchain implementation (in Python), ideally with some visualizations of network activity (in Elm).
As with so many things, the theory is very interesting, but the details of implementation require deeper thought. Since this is a learning exercise, I’m mostly trying to read just the basic theories and independently come up with something minimal that “works”, rather then copying too much from reference implementations.
Crypto
The pycrypto
library has been deprecated for a while, with pycryptodome
and cryptography
as alternatives recommended by the community. I chose cryptography
as it seems to emphasize a sound approach to cryptography (don’t do it yourself!), but implementation correctness / security is not so much a concern for this learning exercise.
Hashing
Hasning is a primitive that’s required for blockchain operations.
Wrapping the library hash
function with a default algorithm of SHA-512:
from cryptography.hazmat.primitives import hashes # typing: ignore
Digest = bytes
def hash(msg: bytes, algo = hashes.SHA512()) -> Digest:
"""Hash given msg."""
digest = hashes.Hash(algo)
digest.update(msg)
return digest.finalize()
This works as expected (verified against an independent online source and added as a test case):
In [1]: from toycoin import hash
In [2]: hash.hash(b'hello world').hex()
Out[2]: '309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f'
Digital Signatures
RSA keys can be used ot sign and verify messages (or more specifically, in Nakamoto’s Bitcoin paper, transactions from one Bitcoin owner to another).
Following the docs:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
Signature = bytes
def gen_priv_key(key_size: int = 2048) -> rsa.RSAPrivateKey:
"""Generate RSA key with modulus of given size in bits."""
return rsa.generate_private_key(public_exponent=65537, key_size=key_size)
def get_pub_key(priv_key: rsa.RSAPrivateKey) -> rsa.RSAPublicKey:
"""Get RSA pubic key from private key."""
return priv_key.public_key()
Next, again following the docs, we specify signing and verifying messages. The library recommendation is to use PSS as the padding sheme, and we hard code SHA512 as the hashing algorithm.
HASH = hashes.SHA512()
PADDING = padding.PSS(mgf=padding.MGF1(HASH),
salt_length=padding.PSS.MAX_LENGTH)
def sign(priv_key: rsa.RSAPrivateKey, msg: bytes) -> Signature:
"""Sign msg with RSA private key."""
return priv_key.sign(msg, PADDING, HASH)
def verify(signature: Signature, pub_key: rsa.RSAPublicKey, msg: bytes) -> bool:
"""Verify msg signature with RSA public key."""
try:
pub_key.verify(signature, msg, PADDING, HASH)
except:
return False
return True
The default behavior for verify
is to raise an exception, so we just wrap that into a boolean check instead (a bit more functional maybe). We can write a test case to verify that the round trip (signing and verifying) works:
def test_roundtrip(self):
"""Test sign and verify."""
msg = b'hello world'
priv_key = signature.gen_priv_key()
pub_key = signature.get_pub_key(priv_
s = signature.sign(priv_key, msg)
assert signature.verify(s, pub_key, msg) is True
assert signature.verify(s[:-1], pub_key, msg) is False
assert signature.verify(s, pub_key, msg[:-1]) is False
Note that all test cases for toycoin
are written for pytest
.
Wrapping Up
This was mostly just lightly wrapping cryptography
functions, but it was helpful to read the docs a bit and work out the type signatures.