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.
pycrypto library has been deprecated for a while, with
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.
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 : from toycoin import hash In : hash.hash(b'hello world').hex() Out: '309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f'
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
This was mostly just lightly wrapping
cryptography functions, but it was helpful to read the docs a bit and work out the type signatures.