From 9dc767318e631717d93dc9f59c8d3916a03fa64a Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 16 Mar 2016 16:27:40 +0100 Subject: [PATCH 01/16] - abstracted crypto scheme - added ED25519 - added tests --- .gitignore | 2 + bigchaindb/client.py | 6 +- bigchaindb/commands/bigchain.py | 12 +-- bigchaindb/core.py | 18 ++-- bigchaindb/crypto/__init__.py | 0 bigchaindb/crypto/core.py | 69 ++++++++++++ bigchaindb/{crypto.py => crypto/ecdsa.py} | 36 +++---- bigchaindb/crypto/ed25519.py | 102 ++++++++++++++++++ bigchaindb/util.py | 8 +- setup.py | 1 + tests/crypto/__init__.py | 0 tests/crypto/test_crypto.py | 121 ++++++++++++++++++++++ tests/db/test_bigchain_api.py | 46 +------- tests/db/test_voter.py | 10 +- tests/test_commands.py | 2 +- tests/web/test_basic_views.py | 9 +- 16 files changed, 344 insertions(+), 98 deletions(-) create mode 100644 bigchaindb/crypto/__init__.py create mode 100644 bigchaindb/crypto/core.py rename bigchaindb/{crypto.py => crypto/ecdsa.py} (89%) create mode 100644 bigchaindb/crypto/ed25519.py create mode 100644 tests/crypto/__init__.py create mode 100644 tests/crypto/test_crypto.py diff --git a/.gitignore b/.gitignore index d8010917..8f679478 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ target/ # pyenv .python-version +# IDE related +.idea diff --git a/bigchaindb/client.py b/bigchaindb/client.py index 0a9eed01..67f73f28 100644 --- a/bigchaindb/client.py +++ b/bigchaindb/client.py @@ -1,10 +1,10 @@ import requests import bigchaindb -from bigchaindb import util from bigchaindb import config_utils from bigchaindb import exceptions -from bigchaindb import crypto +from bigchaindb import util +from bigchaindb.crypto import core class Client: @@ -92,6 +92,6 @@ def temp_client(): A client initialized with a keypair generated on the fly. """ - private_key, public_key = crypto.generate_key_pair() + private_key, public_key = core.generate_key_pair() return Client(private_key=private_key, public_key=public_key, api_endpoint='http://localhost:5000/api/v1') diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 35b2de2f..0447fb13 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -1,19 +1,17 @@ """Command line interface for the `bigchain` command.""" - -import os -import logging import argparse import copy +import logging +import os import bigchaindb import bigchaindb.config_utils from bigchaindb import db -from bigchaindb.exceptions import DatabaseAlreadyExists from bigchaindb.commands.utils import base_parser, start +from bigchaindb.crypto import core +from bigchaindb.exceptions import DatabaseAlreadyExists from bigchaindb.processes import Processes -from bigchaindb import crypto - logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -52,7 +50,7 @@ def run_configure(args, skip_if_exists=False): conf = copy.deepcopy(bigchaindb._config) print('Generating keypair') - conf['keypair']['private'], conf['keypair']['public'] = crypto.generate_key_pair() + conf['keypair']['private'], conf['keypair']['public'] = core.generate_key_pair() if not args.yes: for key in ('host', 'port', 'name'): diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 3b2a29d1..265a8616 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -1,17 +1,15 @@ -import rethinkdb as r import random -import json import rapidjson +import rethinkdb as r import bigchaindb -from bigchaindb import util from bigchaindb import config_utils from bigchaindb import exceptions -from bigchaindb import crypto +from bigchaindb import util +from bigchaindb.crypto import core from bigchaindb.monitor import Monitor - monitor = Monitor() @@ -98,7 +96,7 @@ class Bigchain(object): signature = data.pop('signature') public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = crypto.PublicKey(public_key_base58) + public_key = core.PublicKey(public_key_base58) return public_key.verify(util.serialize(data), signature) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) @@ -331,8 +329,8 @@ class Bigchain(object): # Calculate the hash of the new block block_data = util.serialize(block) - block_hash = crypto.hash_data(block_data) - block_signature = crypto.PrivateKey(self.me_private).sign(block_data) + block_hash = core.hash_data(block_data) + block_signature = core.PrivateKey(self.me_private).sign(block_data) block = { 'id': block_hash, @@ -357,7 +355,7 @@ class Bigchain(object): """ # 1. Check if current hash is correct - calculated_hash = crypto.hash_data(util.serialize(block['block'])) + calculated_hash = core.hash_data(util.serialize(block['block'])) if calculated_hash != block['id']: raise exceptions.InvalidHash() @@ -452,7 +450,7 @@ class Bigchain(object): } vote_data = util.serialize(vote) - signature = crypto.PrivateKey(self.me_private).sign(vote_data) + signature = core.PrivateKey(self.me_private).sign(vote_data) vote_signed = { 'node_pubkey': self.me, diff --git a/bigchaindb/crypto/__init__.py b/bigchaindb/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bigchaindb/crypto/core.py b/bigchaindb/crypto/core.py new file mode 100644 index 00000000..807021a6 --- /dev/null +++ b/bigchaindb/crypto/core.py @@ -0,0 +1,69 @@ +# Separate all crypto code so that we can easily test several implementations +from abc import ABCMeta, abstractmethod + +import sha3 + + +class PrivateKey(metaclass=ABCMeta): + """ + PrivateKey instance + """ + + @abstractmethod + def sign(self, data): + """ + Sign data with private key + """ + raise NotImplementedError + + @staticmethod + @abstractmethod + def encode(private_value): + """ + Encode the decimal number private_value to base58 + """ + raise NotImplementedError + + @staticmethod + @abstractmethod + def decode(key): + """ + Decode the base58 private_value to decimale + """ + raise NotImplementedError + + +class PublicKey(metaclass=ABCMeta): + + @abstractmethod + def verify(self, data, signature): + raise NotImplementedError + + @staticmethod + @abstractmethod + def encode(public_value_x, public_value_y): + """ + Encode the public key represented by the decimal values x and y to base58 + """ + raise NotImplementedError + + @staticmethod + @abstractmethod + def decode(public_value_compressed_base58): + """ + Decode the base58 public_value to the decimal x and y values + """ + raise NotImplementedError + + +def hash_data(data): + """Hash the provided data using SHA3-256""" + + return sha3.sha3_256(data.encode()).hexdigest() + + +from bigchaindb.crypto.ecdsa import ECDSAPrivateKey, ECDSAPublicKey, ecdsa_generate_key_pair + +PrivateKey = ECDSAPrivateKey +PublicKey = ECDSAPublicKey +generate_key_pair = ecdsa_generate_key_pair diff --git a/bigchaindb/crypto.py b/bigchaindb/crypto/ecdsa.py similarity index 89% rename from bigchaindb/crypto.py rename to bigchaindb/crypto/ecdsa.py index bcbe0863..023bfbae 100644 --- a/bigchaindb/crypto.py +++ b/bigchaindb/crypto/ecdsa.py @@ -1,18 +1,18 @@ # Separate all crypto code so that we can easily test several implementations import binascii + import base58 - -import sha3 import bitcoin - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives import hashes from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec + +from bigchaindb.crypto.core import PrivateKey, PublicKey -class PrivateKey(object): +class ECDSAPrivateKey(PrivateKey): """ PrivateKey instance """ @@ -65,7 +65,7 @@ class PrivateKey(object): Return an instance of cryptography PrivateNumbers from the decimal private_value """ public_value_x, public_value_y = self._private_value_to_public_values(private_value) - public_numbers = PublicKey._public_values_to_cryptography_public_numbers(public_value_x, public_value_y) + public_numbers = ECDSAPublicKey._public_values_to_cryptography_public_numbers(public_value_x, public_value_y) private_numbers = ec.EllipticCurvePrivateNumbers(private_value, public_numbers) return private_numbers @@ -77,7 +77,7 @@ class PrivateKey(object): return private_numbers.private_key(default_backend()) -class PublicKey(object): +class ECDSAPublicKey(PublicKey): def __init__(self, key): """ @@ -85,7 +85,7 @@ class PublicKey(object): """ public_value_x, public_value_y = self.decode(key) public_numbers = self._public_values_to_cryptography_public_numbers(public_value_x, public_value_y) - self.public_key = self._criptography_public_key_from_public_numbers(public_numbers) + self.public_key = self._cryptography_public_key_from_public_numbers(public_numbers) def verify(self, data, signature): verifier = self.public_key.verifier(binascii.unhexlify(signature), ec.ECDSA(hashes.SHA256())) @@ -123,33 +123,25 @@ class PublicKey(object): public_numbers = ec.EllipticCurvePublicNumbers(public_value_x, public_value_y, ec.SECP256K1()) return public_numbers - def _criptography_public_key_from_public_numbers(self, public_numbers): + def _cryptography_public_key_from_public_numbers(self, public_numbers): """ Return an instance of cryptography PublicKey from a cryptography instance of PublicNumbers """ return public_numbers.public_key(default_backend()) -def generate_key_pair(): +def ecdsa_generate_key_pair(): """ Generate a new key pair and return the pair encoded in base58 """ # Private key private_key = ec.generate_private_key(ec.SECP256K1, default_backend()) private_value = private_key.private_numbers().private_value - private_value_base58 = PrivateKey.encode(private_value) + private_value_base58 = ECDSAPrivateKey.encode(private_value) # Public key public_key = private_key.public_key() public_value_x, public_value_y = public_key.public_numbers().x, public_key.public_numbers().y - public_value_compressed_base58 = PublicKey.encode(public_value_x, public_value_y) + public_value_compressed_base58 = ECDSAPublicKey.encode(public_value_x, public_value_y) return (private_value_base58, public_value_compressed_base58) - - -def hash_data(data): - """Hash the provided data using SHA3-256""" - - return sha3.sha3_256(data.encode()).hexdigest() - - diff --git a/bigchaindb/crypto/ed25519.py b/bigchaindb/crypto/ed25519.py new file mode 100644 index 00000000..bdd83d26 --- /dev/null +++ b/bigchaindb/crypto/ed25519.py @@ -0,0 +1,102 @@ +# Separate all crypto code so that we can easily test several implementations + +import base64 + +import base58 +import ed25519 + +from bigchaindb.crypto.core import PrivateKey, PublicKey + + +class ED25519PrivateKey(PrivateKey): + """ + PrivateKey instance + """ + + def __init__(self, key): + """ + Instantiate the private key with the private_value encoded in base58 + """ + private_base64 = self.decode(key) + self.private_key = self._private_key_from_private_base64(private_base64) + + def sign(self, data): + """ + Sign data with private key + """ + return self.private_key.sign(data.encode('utf-8'), encoding="base64") + + @staticmethod + def encode(private_base64): + """ + Encode the base64 number private_base64 to base58 + """ + return base58.b58encode(base64.b64decode(private_base64)) + + @staticmethod + def decode(key): + """ + Decode the base58 private_value to base64 + """ + return base64.b64encode(base58.b58decode(key)) + + @staticmethod + def _private_key_from_private_base64(private_base64): + """ + Return an instance of a ED25519 SignigKey from a base64 key + """ + return ed25519.SigningKey(private_base64, encoding='base64') + + +class ED25519PublicKey(PublicKey): + + def __init__(self, key): + """ + Instantiate the public key with the compressed public value encoded in base58 + """ + public_base64 = self.decode(key) + self.public_key = self._public_key_from_public_base64(public_base64) + + def verify(self, data, signature, encoding='base64'): + try: + if encoding: + data = data.encode('utf-8') + self.public_key.verify(signature, data, encoding=encoding) + except ed25519.BadSignatureError: + return False + + return True + + @staticmethod + def encode(public_base64): + """ + Encode the public key represented by base64 to base58 + """ + return ED25519PrivateKey.encode(public_base64) + + @staticmethod + def decode(public_value_compressed_base58): + """ + Decode the base58 public_value to base64 + """ + return ED25519PrivateKey.decode(public_value_compressed_base58) + + def _public_key_from_public_base64(self, public_base64): + """ + Return an instance of ED25519 VerifyingKey from a base64 + """ + return ed25519.VerifyingKey(public_base64, encoding='base64') + + +def ed25519_generate_key_pair(): + """ + Generate a new key pair and return the pair encoded in base58 + """ + sk, vk = ed25519.create_keypair() + # Private key + private_value_base58 = base58.b58encode(sk.to_bytes()) + + # Public key + public_value_compressed_base58 = base58.b58encode(vk.to_bytes()) + + return (private_value_base58, public_value_compressed_base58) diff --git a/bigchaindb/util.py b/bigchaindb/util.py index d8ddc9ce..761f8c92 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -1,12 +1,11 @@ - import json -import time import multiprocessing as mp +import time from datetime import datetime import bigchaindb from bigchaindb import exceptions -from bigchaindb.crypto import PrivateKey, PublicKey, hash_data +from bigchaindb.crypto.core import PrivateKey, PublicKey, hash_data class ProcessGroup(object): @@ -129,6 +128,9 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None): 'input': tx_input, 'operation': operation, 'timestamp': timestamp(), + 'execution_condition': { + + }, 'data': data } diff --git a/setup.py b/setup.py index 8d888e6a..74ffc546 100644 --- a/setup.py +++ b/setup.py @@ -76,6 +76,7 @@ setup( 'bitcoin==1.1.42', 'flask==0.10.1', 'requests==2.9', + 'ed25519==1.4', ], setup_requires=['pytest-runner'], tests_require=tests_require, diff --git a/tests/crypto/__init__.py b/tests/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/crypto/test_crypto.py b/tests/crypto/test_crypto.py new file mode 100644 index 00000000..3ac24046 --- /dev/null +++ b/tests/crypto/test_crypto.py @@ -0,0 +1,121 @@ +import base64 + +from bigchaindb.crypto.ecdsa import ECDSAPrivateKey, ECDSAPublicKey, ecdsa_generate_key_pair +from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey, ed25519_generate_key_pair + + +class TestBigchainCryptoED25519(object): + PRIVATE_B64 = b'xSrGKGxeJYVlVrk84f29wcPazoTV+y8fzM7P0iFsSdg=' + PRIVATE_BYTES = b'\xc5*\xc6(l^%\x85eV\xb9<\xe1\xfd\xbd\xc1\xc3\xda\xce\x84\xd5\xfb/\x1f\xcc\xce\xcf\xd2!lI\xd8v\x08\xfb\x03\x15y/&\xcd^O\xa9\xb8\xd2\x8a\x89\x8d\xf94\x9b\xbe\xb1\xe7\xdb~\x95!o\xde\xa2{\xa5' + PRIVATE_B58 = 'EGf9UJzryLpZaBguyf5f4QAefFnairNbHLkhht8BZ57m' + + PUBLIC_B64 = b'dgj7AxV5LybNXk+puNKKiY35NJu+sefbfpUhb96ie6U=' + PUBLIC_BYTES = b'v\x08\xfb\x03\x15y/&\xcd^O\xa9\xb8\xd2\x8a\x89\x8d\xf94\x9b\xbe\xb1\xe7\xdb~\x95!o\xde\xa2{\xa5' + PUBLIC_B58 = '8wm3wiqsoujkDJvk8FMZkHijb9eZdUqMuZsnRee4eRz4' + + PUBLIC_B64_ILP = 'Lvf3YtnHLMER+VHT0aaeEJF+7WQcvp4iKZAdvMVto7c=' + MSG_SHA512_ILP = 'claZQU7qkFz7smkAVtQp9ekUCc5LgoeN9W3RItIzykNEDbGSvzeHvOk9v/vrPpm+XWx5VFjd/sVbM2SLnCpxLw==' + SIG_B64_ILP = 'sd0RahwuJJgeNfg8HvWHtYf4uqNgCOqIbseERacqs8G0kXNQQnhfV6gWAnMb+0RIlY3e0mqbrQiUwbRYJvRBAw==' + + def test_private_key_encode(self): + private_value_base58 = ED25519PrivateKey.encode(self.PRIVATE_B64) + assert private_value_base58 == self.PRIVATE_B58 + + def test_private_key_init(self): + sk = ED25519PrivateKey(self.PRIVATE_B58) + assert sk.private_key.to_ascii(encoding='base64') == self.PRIVATE_B64[:-1] + assert sk.private_key.to_bytes() == self.PRIVATE_BYTES + + def test_private_key_decode(self): + private_value = ED25519PrivateKey.decode(self.PRIVATE_B58) + assert private_value == self.PRIVATE_B64 + + def test_public_key_encode(self): + public_value_base58 = ED25519PublicKey.encode(self.PUBLIC_B64) + assert public_value_base58 == self.PUBLIC_B58 + + def test_public_key_init(self): + vk = ED25519PublicKey(self.PUBLIC_B58) + assert vk.public_key.to_ascii(encoding='base64') == self.PUBLIC_B64[:-1] + assert vk.public_key.to_bytes() == self.PUBLIC_BYTES + + def test_public_key_decode(self): + public_value = ED25519PublicKey.decode(self.PUBLIC_B58) + assert public_value == self.PUBLIC_B64 + + def test_sign_verify(self): + message = 'Hello World!' + sk = ED25519PrivateKey(self.PRIVATE_B58) + vk = ED25519PublicKey(self.PUBLIC_B58) + assert vk.verify(message, sk.sign(message)) is True + assert vk.verify(message, sk.sign(message + 'dummy')) is False + assert vk.verify(message + 'dummy', sk.sign(message)) is False + vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + assert vk.verify(message, sk.sign(message)) is False + + def test_valid_condition_valid_signature_ilp(self): + vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + msg = self.MSG_SHA512_ILP + sig = self.SIG_B64_ILP + assert vk.verify(base64.b64decode(msg), base64.b64decode(sig), encoding=None) is True + + def test_valid_condition_invalid_signature_ilp(self): + vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + msg = self.MSG_SHA512_ILP + sig = self.MSG_SHA512_ILP + assert vk.verify(base64.b64decode(msg), base64.b64decode(sig), encoding=None) is False + + def test_generate_key_pair(self): + sk, vk = ed25519_generate_key_pair() + assert ED25519PrivateKey.encode(ED25519PrivateKey.decode(sk)) == sk + assert ED25519PublicKey.encode(ED25519PublicKey.decode(vk)) == vk + + def test_generate_sign_verify(self): + sk, vk = ed25519_generate_key_pair() + sk = ED25519PrivateKey(sk) + vk = ED25519PublicKey(vk) + message = 'Hello World!' + assert vk.verify(message, sk.sign(message)) is True + assert vk.verify(message, sk.sign(message + 'dummy')) is False + assert vk.verify(message + 'dummy', sk.sign(message)) is False + vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + assert vk.verify(message, sk.sign(message)) is False + + +class TestBigchainCryptoECDSA(object): + PRIVATE_VALUE = 64328150571824492670917070117568709277186368319388887463636481841106388379832 + PUBLIC_VALUE_X = 48388170575736684074633245566225141536152842355597159440179742847497614196929 + PUBLIC_VALUE_Y = 65233479152484407841598798165960909560839872511163322973341535484598825150846 + + PRIVATE_VALUE_B58 = 'AaAp4xBavbe6VGeQF2mWdSKNM1r6HfR2Z1tAY6aUkwdq' + PUBLIC_VALUE_COMPRESSED_B58 = 'ifEi3UuTDT4CqUUKiS5omgeDodhu2aRFHVp6LoahbEVe' + + def test_private_key_encode(self): + private_value_base58 = ECDSAPrivateKey.encode(self.PRIVATE_VALUE) + assert private_value_base58 == self.PRIVATE_VALUE_B58 + + def test_private_key_decode(self): + private_value = ECDSAPrivateKey.decode(self.PRIVATE_VALUE_B58) + assert private_value == self.PRIVATE_VALUE + + def test_public_key_encode(self): + public_value_compressed_base58 = ECDSAPublicKey.encode(self.PUBLIC_VALUE_X, self.PUBLIC_VALUE_Y) + assert public_value_compressed_base58 == self.PUBLIC_VALUE_COMPRESSED_B58 + + def test_public_key_decode(self): + public_value_x, public_value_y = ECDSAPublicKey.decode(self.PUBLIC_VALUE_COMPRESSED_B58) + assert public_value_x == self.PUBLIC_VALUE_X + assert public_value_y == self.PUBLIC_VALUE_Y + + def test_sign_verify(self): + message = 'Hello World!' + public_key = ECDSAPublicKey(self.PUBLIC_VALUE_COMPRESSED_B58) + private_key = ECDSAPrivateKey(self.PRIVATE_VALUE_B58) + assert public_key.verify(message, private_key.sign(message)) is True + + def test_generate_key_pair(self): + private_value_base58, public_value_compressed_base58 = ecdsa_generate_key_pair() + assert ECDSAPrivateKey.encode( + ECDSAPrivateKey.decode(private_value_base58)) == private_value_base58 + assert ECDSAPublicKey.encode( + *ECDSAPublicKey.decode(public_value_compressed_base58)) == public_value_compressed_base58 diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 1fed8800..227ec9dd 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -6,12 +6,11 @@ import pytest import rethinkdb as r import bigchaindb -from bigchaindb import util from bigchaindb import exceptions -from bigchaindb.crypto import PrivateKey, PublicKey, generate_key_pair, hash_data -from bigchaindb.voter import Voter +from bigchaindb import util from bigchaindb.block import Block - +from bigchaindb.crypto.core import PrivateKey, PublicKey, generate_key_pair, hash_data +from bigchaindb.voter import Voter @pytest.mark.skipif(reason='Some tests throw a ResourceWarning that might result in some weird ' @@ -408,45 +407,6 @@ class TestBlockValidation(object): assert b.is_valid_block(block) -class TestBigchainCrypto(object): - PRIVATE_VALUE = 64328150571824492670917070117568709277186368319388887463636481841106388379832 - PUBLIC_VALUE_X = 48388170575736684074633245566225141536152842355597159440179742847497614196929 - PUBLIC_VALUE_Y = 65233479152484407841598798165960909560839872511163322973341535484598825150846 - - PRIVATE_VALUE_B58 = 'AaAp4xBavbe6VGeQF2mWdSKNM1r6HfR2Z1tAY6aUkwdq' - PUBLIC_VALUE_COMPRESSED_B58 = 'ifEi3UuTDT4CqUUKiS5omgeDodhu2aRFHVp6LoahbEVe' - - def test_private_key_encode(self): - private_value_base58 = PrivateKey.encode(self.PRIVATE_VALUE) - assert private_value_base58 == self.PRIVATE_VALUE_B58 - - def test_private_key_decode(self): - private_value = PrivateKey.decode(self.PRIVATE_VALUE_B58) - assert private_value == self.PRIVATE_VALUE - - def test_public_key_encode(self): - public_value_compressed_base58 = PublicKey.encode(self.PUBLIC_VALUE_X, self.PUBLIC_VALUE_Y) - assert public_value_compressed_base58 == self.PUBLIC_VALUE_COMPRESSED_B58 - - def test_public_key_decode(self): - public_value_x, public_value_y = PublicKey.decode(self.PUBLIC_VALUE_COMPRESSED_B58) - assert public_value_x == self.PUBLIC_VALUE_X - assert public_value_y == self.PUBLIC_VALUE_Y - - def test_sign_verify(self): - message = 'Hello World!' - public_key = PublicKey(self.PUBLIC_VALUE_COMPRESSED_B58) - private_key = PrivateKey(self.PRIVATE_VALUE_B58) - assert public_key.verify(message, private_key.sign(message)) is True - - def test_generate_key_pair(self): - private_value_base58, public_value_compressed_base58 = generate_key_pair() - assert PrivateKey.encode( - PrivateKey.decode(private_value_base58)) == private_value_base58 - assert PublicKey.encode( - *PublicKey.decode(public_value_compressed_base58)) == public_value_compressed_base58 - - class TestBigchainVoter(object): def test_valid_block_voting(self, b): diff --git a/tests/db/test_voter.py b/tests/db/test_voter.py index d8146829..69eae0df 100644 --- a/tests/db/test_voter.py +++ b/tests/db/test_voter.py @@ -1,12 +1,12 @@ -import pytest -import time -import rethinkdb as r import multiprocessing as mp +import time + +import pytest +import rethinkdb as r from bigchaindb import util - +from bigchaindb.crypto.core import PublicKey, generate_key_pair from bigchaindb.voter import Voter, BlockStream -from bigchaindb.crypto import PublicKey, generate_key_pair class TestBigchainVoter(object): diff --git a/tests/test_commands.py b/tests/test_commands.py index 6e731c13..179ed45e 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -52,7 +52,7 @@ def mock_rethink_db_drop(monkeypatch): @pytest.fixture def mock_generate_key_pair(monkeypatch): - monkeypatch.setattr('bigchaindb.crypto.generate_key_pair', lambda: ('privkey', 'pubkey')) + monkeypatch.setattr('bigchaindb.crypto.core.generate_key_pair', lambda: ('privkey', 'pubkey')) @pytest.fixture diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index 04a1c292..88189b7b 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -1,8 +1,9 @@ import json import pytest -from bigchaindb import crypto + from bigchaindb import util +from bigchaindb.crypto import core TX_ENDPOINT = '/api/v1/transactions/' @@ -17,7 +18,7 @@ def test_get_transaction_endpoint(b, client, user_public_key): def test_post_create_transaction_endpoint(b, client): - keypair = crypto.generate_key_pair() + keypair = core.generate_key_pair() tx = util.create_and_sign_tx(keypair[0], keypair[1], keypair[1], None, 'CREATE') @@ -27,8 +28,8 @@ def test_post_create_transaction_endpoint(b, client): def test_post_transfer_transaction_endpoint(b, client): - from_keypair = crypto.generate_key_pair() - to_keypair = crypto.generate_key_pair() + from_keypair = core.generate_key_pair() + to_keypair = core.generate_key_pair() tx = util.create_and_sign_tx(from_keypair[0], from_keypair[1], from_keypair[1], None, 'CREATE') res = client.post(TX_ENDPOINT, data=json.dumps(tx)) From 4255c1780b56e95eb8678d373d6f23a495c90093 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 16 Mar 2016 16:40:45 +0100 Subject: [PATCH 02/16] renamed crypto.core to crypto.asymmetric --- bigchaindb/client.py | 4 ++-- bigchaindb/commands/bigchain.py | 4 ++-- bigchaindb/core.py | 12 ++++++------ bigchaindb/crypto/{core.py => asymmetric.py} | 0 bigchaindb/crypto/ecdsa.py | 2 +- bigchaindb/crypto/ed25519.py | 2 +- bigchaindb/util.py | 2 +- tests/db/test_bigchain_api.py | 2 +- tests/db/test_voter.py | 2 +- tests/web/test_basic_views.py | 8 ++++---- 10 files changed, 19 insertions(+), 19 deletions(-) rename bigchaindb/crypto/{core.py => asymmetric.py} (100%) diff --git a/bigchaindb/client.py b/bigchaindb/client.py index 67f73f28..156dc6b1 100644 --- a/bigchaindb/client.py +++ b/bigchaindb/client.py @@ -4,7 +4,7 @@ import bigchaindb from bigchaindb import config_utils from bigchaindb import exceptions from bigchaindb import util -from bigchaindb.crypto import core +from bigchaindb.crypto import asymmetric class Client: @@ -92,6 +92,6 @@ def temp_client(): A client initialized with a keypair generated on the fly. """ - private_key, public_key = core.generate_key_pair() + private_key, public_key = asymmetric.generate_key_pair() return Client(private_key=private_key, public_key=public_key, api_endpoint='http://localhost:5000/api/v1') diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 0447fb13..b79b11b8 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -9,7 +9,7 @@ import bigchaindb import bigchaindb.config_utils from bigchaindb import db from bigchaindb.commands.utils import base_parser, start -from bigchaindb.crypto import core +from bigchaindb.crypto import asymmetric from bigchaindb.exceptions import DatabaseAlreadyExists from bigchaindb.processes import Processes @@ -50,7 +50,7 @@ def run_configure(args, skip_if_exists=False): conf = copy.deepcopy(bigchaindb._config) print('Generating keypair') - conf['keypair']['private'], conf['keypair']['public'] = core.generate_key_pair() + conf['keypair']['private'], conf['keypair']['public'] = asymmetric.generate_key_pair() if not args.yes: for key in ('host', 'port', 'name'): diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 265a8616..be66ecf2 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -7,7 +7,7 @@ import bigchaindb from bigchaindb import config_utils from bigchaindb import exceptions from bigchaindb import util -from bigchaindb.crypto import core +from bigchaindb.crypto import asymmetric from bigchaindb.monitor import Monitor monitor = Monitor() @@ -96,7 +96,7 @@ class Bigchain(object): signature = data.pop('signature') public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = core.PublicKey(public_key_base58) + public_key = asymmetric.PublicKey(public_key_base58) return public_key.verify(util.serialize(data), signature) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) @@ -329,8 +329,8 @@ class Bigchain(object): # Calculate the hash of the new block block_data = util.serialize(block) - block_hash = core.hash_data(block_data) - block_signature = core.PrivateKey(self.me_private).sign(block_data) + block_hash = asymmetric.hash_data(block_data) + block_signature = asymmetric.PrivateKey(self.me_private).sign(block_data) block = { 'id': block_hash, @@ -355,7 +355,7 @@ class Bigchain(object): """ # 1. Check if current hash is correct - calculated_hash = core.hash_data(util.serialize(block['block'])) + calculated_hash = asymmetric.hash_data(util.serialize(block['block'])) if calculated_hash != block['id']: raise exceptions.InvalidHash() @@ -450,7 +450,7 @@ class Bigchain(object): } vote_data = util.serialize(vote) - signature = core.PrivateKey(self.me_private).sign(vote_data) + signature = asymmetric.PrivateKey(self.me_private).sign(vote_data) vote_signed = { 'node_pubkey': self.me, diff --git a/bigchaindb/crypto/core.py b/bigchaindb/crypto/asymmetric.py similarity index 100% rename from bigchaindb/crypto/core.py rename to bigchaindb/crypto/asymmetric.py diff --git a/bigchaindb/crypto/ecdsa.py b/bigchaindb/crypto/ecdsa.py index 023bfbae..29bf6e45 100644 --- a/bigchaindb/crypto/ecdsa.py +++ b/bigchaindb/crypto/ecdsa.py @@ -9,7 +9,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec -from bigchaindb.crypto.core import PrivateKey, PublicKey +from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey class ECDSAPrivateKey(PrivateKey): diff --git a/bigchaindb/crypto/ed25519.py b/bigchaindb/crypto/ed25519.py index bdd83d26..823a6643 100644 --- a/bigchaindb/crypto/ed25519.py +++ b/bigchaindb/crypto/ed25519.py @@ -5,7 +5,7 @@ import base64 import base58 import ed25519 -from bigchaindb.crypto.core import PrivateKey, PublicKey +from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey class ED25519PrivateKey(PrivateKey): diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 761f8c92..9e49b083 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -5,7 +5,7 @@ from datetime import datetime import bigchaindb from bigchaindb import exceptions -from bigchaindb.crypto.core import PrivateKey, PublicKey, hash_data +from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey, hash_data class ProcessGroup(object): diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 227ec9dd..0ed02a8b 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -9,7 +9,7 @@ import bigchaindb from bigchaindb import exceptions from bigchaindb import util from bigchaindb.block import Block -from bigchaindb.crypto.core import PrivateKey, PublicKey, generate_key_pair, hash_data +from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey, generate_key_pair, hash_data from bigchaindb.voter import Voter diff --git a/tests/db/test_voter.py b/tests/db/test_voter.py index 69eae0df..e4ae8efa 100644 --- a/tests/db/test_voter.py +++ b/tests/db/test_voter.py @@ -5,7 +5,7 @@ import pytest import rethinkdb as r from bigchaindb import util -from bigchaindb.crypto.core import PublicKey, generate_key_pair +from bigchaindb.crypto.asymmetric import PublicKey, generate_key_pair from bigchaindb.voter import Voter, BlockStream diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index 88189b7b..1db8c7a9 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -3,7 +3,7 @@ import json import pytest from bigchaindb import util -from bigchaindb.crypto import core +from bigchaindb.crypto import asymmetric TX_ENDPOINT = '/api/v1/transactions/' @@ -18,7 +18,7 @@ def test_get_transaction_endpoint(b, client, user_public_key): def test_post_create_transaction_endpoint(b, client): - keypair = core.generate_key_pair() + keypair = asymmetric.generate_key_pair() tx = util.create_and_sign_tx(keypair[0], keypair[1], keypair[1], None, 'CREATE') @@ -28,8 +28,8 @@ def test_post_create_transaction_endpoint(b, client): def test_post_transfer_transaction_endpoint(b, client): - from_keypair = core.generate_key_pair() - to_keypair = core.generate_key_pair() + from_keypair = asymmetric.generate_key_pair() + to_keypair = asymmetric.generate_key_pair() tx = util.create_and_sign_tx(from_keypair[0], from_keypair[1], from_keypair[1], None, 'CREATE') res = client.post(TX_ENDPOINT, data=json.dumps(tx)) From 828ed4e98be49041760e9616ec4eef3876a0bd5d Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 16 Mar 2016 16:44:11 +0100 Subject: [PATCH 03/16] updated tests --- tests/test_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 179ed45e..d338248a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -52,7 +52,7 @@ def mock_rethink_db_drop(monkeypatch): @pytest.fixture def mock_generate_key_pair(monkeypatch): - monkeypatch.setattr('bigchaindb.crypto.core.generate_key_pair', lambda: ('privkey', 'pubkey')) + monkeypatch.setattr('bigchaindb.crypto.asymmetric.generate_key_pair', lambda: ('privkey', 'pubkey')) @pytest.fixture From 8425214c2a7d67d24299221991d8b227451187a2 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 16 Mar 2016 16:46:09 +0100 Subject: [PATCH 04/16] reverted bigchaindb/util --- bigchaindb/util.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 9e49b083..23d47bc8 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -128,9 +128,6 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None): 'input': tx_input, 'operation': operation, 'timestamp': timestamp(), - 'execution_condition': { - - }, 'data': data } From 46cd3323e8deb71028284b0533b56f5d805303fe Mon Sep 17 00:00:00 2001 From: diminator Date: Fri, 18 Mar 2016 09:58:54 +0100 Subject: [PATCH 05/16] ILP conditions and fulfillment basis ed25519 fulfillment tests --- bigchaindb/crypto/bitmark_registry.py | 56 +++ bigchaindb/crypto/condition.py | 163 +++++++ bigchaindb/crypto/ed25519.py | 6 +- bigchaindb/crypto/fulfillment.py | 161 +++++++ bigchaindb/crypto/fulfillments/__init__.py | 0 bigchaindb/crypto/fulfillments/base_sha256.py | 25 + .../crypto/fulfillments/ed25519_sha256.py | 254 ++++++++++ bigchaindb/crypto/iostream.py | 437 ++++++++++++++++++ tests/crypto/test_fulfillment.py | 115 +++++ 9 files changed, 1215 insertions(+), 2 deletions(-) create mode 100644 bigchaindb/crypto/bitmark_registry.py create mode 100644 bigchaindb/crypto/condition.py create mode 100644 bigchaindb/crypto/fulfillment.py create mode 100644 bigchaindb/crypto/fulfillments/__init__.py create mode 100644 bigchaindb/crypto/fulfillments/base_sha256.py create mode 100644 bigchaindb/crypto/fulfillments/ed25519_sha256.py create mode 100644 bigchaindb/crypto/iostream.py create mode 100644 tests/crypto/test_fulfillment.py diff --git a/bigchaindb/crypto/bitmark_registry.py b/bigchaindb/crypto/bitmark_registry.py new file mode 100644 index 00000000..5e3b071f --- /dev/null +++ b/bigchaindb/crypto/bitmark_registry.py @@ -0,0 +1,56 @@ +from bigchaindb.crypto.iostream import MAX_SAFE_INTEGER + + +class BitmaskRegistry: + registered_types = [] + + @staticmethod + def get_class_from_typebit(bitmask): + """ + Determine fulfillment implementation class from a bitmask. + + Returns the class implementing a fulfillment type that matches a certain bitmask. + + Args: + bitmask (int): fulfillment bitmask + + Return: + Class implementing the given fulfillment type. + """ + # Determine type of condition + if bitmask > MAX_SAFE_INTEGER: + raise ValueError('Bitmask {} is not supported'.format(bitmask)) + + for registered_type in BitmaskRegistry.registered_types: + if bitmask == registered_type['bitmask']: + return registered_type['class'] + + raise ValueError('Bitmask {} is not supported'.format(bitmask)) + + @staticmethod + def register_type(cls): + """ + Add a new fulfillment type. + + This can be used to extend this cryptocondition implementation with new + fulfillment types that it does not yet support. But mostly it is used + internally to register the built-in types. + + In this method, we expect a regular fulfillment type, for information on + registering meta types please see `registerMetaType`. + + Args: + cls: Implementation of a fulfillment type. + """ + # TODO Do some sanity checks on Class + + BitmaskRegistry.registered_types.append( + { + 'bitmask': cls().bitmask, + 'class': cls + }) + + +from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment + +BitmaskRegistry.register_type(Ed25519Sha256Fulfillment) diff --git a/bigchaindb/crypto/condition.py b/bigchaindb/crypto/condition.py new file mode 100644 index 00000000..201c2eab --- /dev/null +++ b/bigchaindb/crypto/condition.py @@ -0,0 +1,163 @@ +import base64 +import re +from abc import ABCMeta + +from six import string_types + +from bigchaindb.crypto.iostream import base64_add_padding, base64_remove_padding + +CONDITION_REGEX = r'^cc:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]{43}:[1-9][0-9]{0,50}$' + + +class Condition(metaclass=ABCMeta): + _bitmask = None + _hash = None + _max_fulfillment_length = None + + @staticmethod + def from_uri(serialized_condition): + """ + Create a Condition object from a URI. + + This method will parse a condition URI and construct a corresponding Condition object. + + Args: + serialized_condition (str): URI representing the condition + + Returns: + Condition: Resulting object + """ + if not isinstance(serialized_condition, string_types): + raise TypeError('Serialized condition must be a string') + + pieces = serialized_condition.split(':') + if not pieces[0] == 'cc': + raise ValueError('Serialized condition must start with "cc:"') + + if not pieces[1] == '1': + raise ValueError('Condition must be version 1') + + if not re.match(CONDITION_REGEX, serialized_condition): + raise ValueError('Invalid condition format') + + condition = Condition() + condition.bitmask = int(pieces[2]) + condition.hash = base64.urlsafe_b64decode(base64_add_padding(pieces[3])) + condition.max_fulfillment_length = int(pieces[4]) + + return condition + + @property + def bitmask(self): + """ + Return the bitmask of this condition. + + For simple condition types this is simply the bit representing this type. + For meta-conditions, these are the bits representing the types of the subconditions. + + Return: + int: Bitmask corresponding to this condition. + """ + return self._bitmask + + @bitmask.setter + def bitmask(self, value): + """ + Set the bitmask. + + Sets the required bitmask to validate a fulfillment for this condition. + + Args: + value (int): representation of bitmask. + """ + self._bitmask = value + + @property + def hash(self): + """ + Return the hash of the condition. + + A primary component of all conditions is the hash. It encodes the static + properties of the condition. This method enables the conditions to be + constant size, no matter how complex they actually are. The data used to + generate the hash consists of all the static properties of the condition + and is provided later as part of the fulfillment. + + Return: + Hash of the condition + """ + if not self._hash: + raise ValueError + return self._hash + + @hash.setter + def hash(self, value): + """ + Validate and set the hash of this condition. + + Typically conditions are generated from fulfillments and the hash is + calculated automatically. However, sometimes it may be necessary to + construct a condition URI from a known hash. This method enables that case. + + Args: + value (Buffer): Hash as binary. + """ + # TODO: value must be Buffer + # if not isinstance(value, Buffer): + # raise ValueError + self._hash = value + + @property + def max_fulfillment_length(self): + """ + Return the maximum fulfillment length. + + The maximum fulfillment length is the maximum allowed length for any + fulfillment payload to fulfill this condition. + + The condition defines a maximum fulfillment length which all + implementations will enforce. This allows implementations to verify that + their local maximum fulfillment size is guaranteed to accomodate any + possible fulfillment for this condition. + + Otherwise an attacker could craft a fulfillment which exceeds the maximum + size of one implementation, but meets the maximum size of another, thereby + violating the fundamental property that fulfillments are either valid + everywhere or nowhere. + + Return: + (int) Maximum length (in bytes) of any fulfillment payload that fulfills this condition.. + """ + if not self._max_fulfillment_length: + raise ValueError + return self._max_fulfillment_length + + @max_fulfillment_length.setter + def max_fulfillment_length(self, value): + """ + Set the maximum fulfillment length. + + The maximum fulfillment length is normally calculated automatically, when + calling `Fulfillment#getCondition`. However, when + + Args: + value (int): Maximum fulfillment payload length in bytes. + """ + self._max_fulfillment_length = value + + def serialize_uri(self): + """ + Generate the URI form encoding of this condition. + + Turns the condition into a URI containing only URL-safe characters. This + format is convenient for passing around conditions in URLs, JSON and other text-based formats. + + Returns: + string: Condition as a URI + """ + + return 'cc:1:{}:{}:{}'.format(self.bitmask, + base64_remove_padding( + base64.urlsafe_b64encode(self.hash) + ).decode('utf-8'), + self.max_fulfillment_length) diff --git a/bigchaindb/crypto/ed25519.py b/bigchaindb/crypto/ed25519.py index 823a6643..66cec100 100644 --- a/bigchaindb/crypto/ed25519.py +++ b/bigchaindb/crypto/ed25519.py @@ -20,11 +20,13 @@ class ED25519PrivateKey(PrivateKey): private_base64 = self.decode(key) self.private_key = self._private_key_from_private_base64(private_base64) - def sign(self, data): + def sign(self, data, encoding="base64"): """ Sign data with private key """ - return self.private_key.sign(data.encode('utf-8'), encoding="base64") + if not isinstance(data, bytes): + data = data.encode('utf-8') + return self.private_key.sign(data, encoding=encoding) @staticmethod def encode(private_base64): diff --git a/bigchaindb/crypto/fulfillment.py b/bigchaindb/crypto/fulfillment.py new file mode 100644 index 00000000..fd32c19c --- /dev/null +++ b/bigchaindb/crypto/fulfillment.py @@ -0,0 +1,161 @@ +import base64 +import re +from abc import ABCMeta, abstractmethod + +from six import string_types + +from bigchaindb.crypto.condition import Condition +from bigchaindb.crypto.iostream import Writer, base64_remove_padding, Reader, base64_add_padding + +FULFILLMENT_REGEX = r'^cf:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]+$' + + +class Fulfillment(metaclass=ABCMeta): + _bitmask = None + + @staticmethod + def from_uri(serialized_fulfillment): + """ + Create a Fulfillment object from a URI. + + This method will parse a fulfillment URI and construct a corresponding Fulfillment object. + + Args: + serialized_fulfillment (str): URI representing the fulfillment + + Return: + Fulfillment: Resulting object + """ + if not isinstance(serialized_fulfillment, string_types): + raise TypeError('Serialized fulfillment must be a string') + + pieces = serialized_fulfillment.split(':') + if not pieces[0] == 'cf': + raise ValueError('Serialized fulfillment must start with "cf:"') + + if not pieces[1] == '1': + raise ValueError('Fulfillment must be version 1') + + if not re.match(FULFILLMENT_REGEX, serialized_fulfillment): + raise ValueError('Invalid fulfillment format') + + bitmask = int(pieces[2]) + + from bigchaindb.crypto.bitmark_registry import BitmaskRegistry + + cls = BitmaskRegistry.get_class_from_typebit(bitmask) + fulfillment = cls() + + payload = Reader.from_source( + base64.urlsafe_b64decode( + base64_add_padding(pieces[3]))) + + fulfillment.parse_payload(payload) + + return fulfillment + + @property + def bitmask(self): + """ + Return the bitmask of this fulfillment. + + For simple fulfillment types this is simply the bit representing this type. + + For meta-fulfillments, these are the bits representing the types of the subconditions. + + Returns: + int: Bitmask corresponding to this fulfillment. + + """ + + return self._bitmask + + @property + def condition(self): + """ + Generate condition corresponding to this fulfillment. + + An important property of crypto-conditions is that the condition can always + be derived from the fulfillment. This makes it very easy to post + fulfillments to a system without having to specify which condition the + relate to. The system can keep an index of conditions and look up any + matching events related to that condition. + + Return: + Condition: Condition corresponding to this fulfillment. + + """ + condition = Condition() + condition.bitmask = self.bitmask + condition.hash = self.generate_hash() + condition.max_fulfillment_length = self.calculate_max_fulfillment_length() + return condition + + def serialize_uri(self): + """ + Generate the URI form encoding of this fulfillment. + + Turns the fulfillment into a URI containing only URL-safe characters. This + format is convenient for passing around fulfillments in URLs, JSON and + other text-based formats. + + Return: + string: Fulfillment as a URI + """ + return 'cf:1:{}:{}'.format(self.bitmask, + base64_remove_padding( + base64.urlsafe_b64encode( + b''.join(self.serialize_payload().components) + ) + ).decode('utf-8')) + + def serialize_payload(self): + """ + Return the fulfillment payload as a buffer. + + Note that the fulfillment payload is not the standard format for passing + fulfillments in binary protocols. Use `serializeBinary` for that. The + fulfillment payload is purely the type-specific data and does not include the bitmask. + + Return: + Buffer: Fulfillment payload + """ + return self.write_payload(Writer()) + + def calculate_max_fulfillment_length(self): + """ + Calculate the maximum length of the fulfillment payload. + + This implementation works by measuring the length of the fulfillment. + Condition types that do not have a constant length will override this + method with one that calculates the maximum possible length. + + Return: + {Number} Maximum fulfillment length + """ + # TODO: Predictor + # predictor = Predictor() + predictor = None + self.write_payload(predictor) + return predictor.size + + @abstractmethod + def write_payload(self, writer): + raise NotImplementedError + + @abstractmethod + def parse_payload(self, reader): + raise NotImplementedError + + @abstractmethod + def generate_hash(self): + """ + Generate the hash of the fulfillment. + + This method is a stub and will be overridden by subclasses. + """ + raise NotImplementedError + + @abstractmethod + def validate(self): + raise NotImplementedError diff --git a/bigchaindb/crypto/fulfillments/__init__.py b/bigchaindb/crypto/fulfillments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bigchaindb/crypto/fulfillments/base_sha256.py b/bigchaindb/crypto/fulfillments/base_sha256.py new file mode 100644 index 00000000..570b0d7f --- /dev/null +++ b/bigchaindb/crypto/fulfillments/base_sha256.py @@ -0,0 +1,25 @@ +from abc import abstractmethod + +from bigchaindb.crypto.fulfillment import Fulfillment +from bigchaindb.crypto.iostream import Hasher + + +class BaseSha256Fulfillment(Fulfillment): + + def generate_hash(self): + """ + Calculate condition hash. + + This method is called internally by `condition`. It calculates the + condition hash by hashing the hash payload. + + Return: + Buffer: Result from hashing the hash payload. + """ + hasher = Hasher('sha256') + self.write_hash_payload(hasher) + return hasher.digest() # remove padding + + @abstractmethod + def write_hash_payload(self, hasher): + raise NotImplementedError diff --git a/bigchaindb/crypto/fulfillments/ed25519_sha256.py b/bigchaindb/crypto/fulfillments/ed25519_sha256.py new file mode 100644 index 00000000..388a27cf --- /dev/null +++ b/bigchaindb/crypto/fulfillments/ed25519_sha256.py @@ -0,0 +1,254 @@ +import base58 + +from bigchaindb.crypto.ed25519 import ED25519PublicKey +from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment +from bigchaindb.crypto.iostream import Predictor + + +class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): + + _bitmask = 0x08 + _message_prefix = None + _max_dynamic_message_length = None + _public_key = None + _message = None + _signature = None + + @property + def message_prefix(self): + return self._message_prefix + + @message_prefix.setter + def message_prefix(self, value): + """ + Set the fixed message prefix. + + The fixed prefix is the portion of the message that is determined when the + condition is first created. + + Args: + value (Buffer): Static portion of the message + """ + if not isinstance(value, bytes): + value = value.encode() + self._message_prefix = value + + @property + def max_dynamic_message_length(self): + return self._max_dynamic_message_length + + @max_dynamic_message_length.setter + def max_dynamic_message_length(self, value): + """ + Set the maximum length of the dynamic message component. + + The dynamic message is the part of the signed message that is determined at + fulfillment time. However, when the condition is first created, we need to + know the maximum fulfillment length, which in turn requires us to put a + limit on the length of the dynamic message component. + + If this method is not called, the maximum dynamic message length defaults to zero. + + Args: + value (int): Maximum length in bytes + """ + self._max_dynamic_message_length = value + + @property + def public_key(self): + return self._public_key + + @public_key.setter + def public_key(self, value): + """ + Set the public publicKey. + + This is the Ed25519 public key. It has to be provided as a buffer. + + Args: + value (Buffer): publicKey Public Ed25519 publicKey + """ + # TODO: Buffer + # if not isinstance(value, Buffer): + # raise ValueError("public key must be a Buffer") + if not isinstance(value, ED25519PublicKey): + raise TypeError + self._public_key = value + + @property + def message(self): + return self._message + + @message.setter + def message(self, value): + """ + Set the dynamic message portion. + + Part of the signed message (the suffix) can be determined when the condition is being fulfilled. + + Length may not exceed the maximum dynamic message length. + + Args: + value (Buffer): Binary form of dynamic message. + """ + # TODO: Buffer + # if not isinstance(value, Buffer): + # raise ValueError("message must be a Buffer") + if not isinstance(value, bytes): + value = value.encode() + self._message = value + + @property + def signature(self): + return self._signature + + @signature.setter + def signature(self, value): + """ + Set the signature. + + Instead of using the private key to sign using the sign() method, we can also generate the signature elsewhere + and pass it in. + + Args: + value (Buffer): 64-byte signature. + """ + # TODO: Buffer + # if not isinstance(value, Buffer): + # raise ValueError("signature must be a Buffer") + self._signature = value + + def write_common_header(self, writer): + """ + Write static header fields. + + Some fields are common between the hash and the fulfillment payload. This + method writes those field to anything implementing the Writer interface. + It is used internally when generating the hash of the condition, when + generating the fulfillment payload and when calculating the maximum fulfillment size. + + Args: + writer (Writer|Hasher|Predictor): Target for outputting the header. + """ + if not self.public_key: + raise ValueError + + writer.write_var_bytes(bytearray(self.public_key.public_key.to_bytes())) + writer.write_var_bytes(self.message_prefix) + writer.write_var_uint(self.max_dynamic_message_length) + return writer + + def parse_payload(self, reader): + """ + Parse the payload of an Ed25519 fulfillment. + + Read a fulfillment payload from a Reader and populate this object with that fulfillment. + + Args: + reader (Reader): Source to read the fulfillment payload from. + """ + self.public_key = \ + ED25519PublicKey( + base58.b58encode( + reader.read_var_bytes())) + self.message_prefix = reader.read_var_bytes() + self.max_dynamic_message_length = reader.read_var_uint() + self.message = reader.read_var_bytes() + self.signature = reader.read_var_bytes() + print(self.signature) + + def write_payload(self, writer): + """ + Generate the fulfillment payload. + + This writes the fulfillment payload to a Writer. + + Args: + writer (Writer): Subject for writing the fulfillment payload. + """ + self.write_common_header(writer) + writer.write_var_bytes(self.message) + writer.write_var_bytes(self.signature) + return writer + + def write_hash_payload(self, hasher): + """ + Generate the contents of the condition hash. + + Writes the contents of the condition hash to a Hasher. Used internally by `condition`. + + Args: + hasher (Hasher): Destination where the hash payload will be written. + """ + hasher.write_var_uint(self.bitmask) + return self.write_common_header(hasher) + + def calculate_max_fulfillment_length(self): + """ + Calculates the longest possible fulfillment length. + + The longest fulfillment for an Ed25519 condition is the length of a + fulfillment where the dynamic message length equals its maximum length. + + Return: + Maximum length of the fulfillment payload + """ + + predictor = Predictor() + + if not self.public_key: + raise ValueError('Requires a public key') + + # Calculate the length that the common header would have + self.write_common_header(predictor) + + # Message suffix + predictor.write_var_uint(self.max_dynamic_message_length) + predictor.skip(self.max_dynamic_message_length) + + # Signature + predictor.write_var_bytes(self.public_key.public_key.to_bytes()) + + return predictor.size + + def sign(self, private_key): + """ + Sign the message. + + This method will take the currently configured values for the message + prefix and suffix and create a signature using the provided Ed25519 private key. + + Args: + private_key (string) Ed25519 private key + """ + # TODO: Buffer + sk = private_key + vk = ED25519PublicKey( + base58.b58encode( + sk.private_key.get_verifying_key().to_bytes())) + + self.public_key = vk + + message = self.message_prefix + self.message + + # This would be the Ed25519ph version (JavaScript ES7): + # const message = crypto.createHash('sha512') + # .update(Buffer.concat([this.messagePrefix, this.message])) + # .digest() + + self.signature = sk.sign(message, encoding=None) + + def validate(self): + """ + Verify the signature of this Ed25519 fulfillment. + + The signature of this Ed25519 fulfillment is verified against the provided message and public key. + + Return: + boolean: Whether this fulfillment is valid. + """ + if not (self.message and self.signature): + return False + + message = self.message_prefix + self.message + return self.public_key.verify(message, self.signature, encoding=None) diff --git a/bigchaindb/crypto/iostream.py b/bigchaindb/crypto/iostream.py new file mode 100644 index 00000000..2dd76a68 --- /dev/null +++ b/bigchaindb/crypto/iostream.py @@ -0,0 +1,437 @@ +import base64 +import hashlib +from math import ceil + +import binascii + +from six import string_types + +MSB = 0x80 +REST = 0x7F +MSBALL = ~REST +INT = 2**31 + +# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER +MAX_SAFE_INTEGER = 2**53-1 + + +class UnsignedLEB128: + """ + Adapter for DWARF unsigned LEB128. + + A VARUINT is a variable length integer encoded as base128 where the highest + bit indicates that another byte is following. The first byte contains the + seven least significant bits of the number represented. + + see: https://en.wikipedia.org/wiki/LEB128 (ULEB128) + see http://grokbase.com/t/python/python-list/112e5jpc16/encoding + + """ + @staticmethod + def encode(obj): + out = [] + value = int(obj) + while value > INT: + out.append((value & REST) | MSB) + value /= 7 + + while value & MSBALL: + out.append((value & REST) | MSB) + value >>= 7 + + out.append(value | 0) + return out + + @staticmethod + def decode(obj): + value = 0 + for b in reversed(obj): + value = value * 128 + (ord(b) & 0x7F) + return value + + +class Writer: + + def __init__(self): + self.components = [] + + def write_var_uint(self, value): + """ + Write a VARUINT to the stream. + + Args: + value (int): Integer to represent. + """ + out = UnsignedLEB128.encode(value) + self.write(out) + + def write_var_bytes(self, buffer): + """ + Write a VARBYTES. + + A VARBYTES field consists of a VARUINT followed by that many bytes. + + Args: + buffer (Buffer): Contents of the VARBYTES. + """ + self.write_var_uint(len(buffer)) + self.write(buffer) + + def write(self, in_bytes): + """ + Write a series of raw bytes. + + Adds the given bytes to the output buffer. + + Args: + in_bytes (Buffer): Bytes to write. + """ + out = in_bytes + if isinstance(out, (list, bytearray)): + out = binascii.unhexlify(''.join('{:02x}'.format(x) for x in out)) + if not isinstance(out, bytes): + out = out.encode('utf-8') + self.components.append(out) + + +class Hasher(Writer): + + hash = None + + def __init__(self, algorithm): + if algorithm == 'sha256': + self.hash = hashlib.sha256() + else: + raise NotImplementedError + super(Writer, self).__init__() + + def write(self, in_bytes): + """ + Adds bytes to the hash input. + + The hasher will pass these bytes into the hashing function. By overriding + the Writer class and implementing this method, the Hasher supports any of + the datatypes that a Writer can write. + + Args: + in_bytes (Buffer): Bytes to add to the hash. + """ + out = in_bytes + if isinstance(out, (list, bytearray)): + out = binascii.unhexlify(''.join('{:02x}'.format(x) for x in out)) + if not isinstance(out, bytes): + out = out.encode('utf-8') + self.hash.update(out) + + def digest(self): + """ + Return the hash. + + Returns the finished hash based on what has been written to the Hasher so far. + + Return: + Buffer: Resulting hash. + """ + return self.hash.digest() + + @staticmethod + def length(algorithm): + """ + Get digest length for hashing algorithm. + + Args: + algorithm (string): Hashing algorithm identifier. + + Return: + int: Digest length in bytes. + """ + return len(Hasher(algorithm).digest()) + + +class Predictor: + size = 0 + + def write_var_uint(self, val): + """ + Calculate the size of a VARUINT. + + A VARUINT is a variable length integer encoded as base128 where the highest + bit indicates that another byte is following. The first byte contains the + seven least significant bits of the number represented. + + Args: + val (int): Integer to be encoded + """ + + if val == 0: + self.size += 1 + elif val < 0: + raise ValueError('Variable length integer cannot be negative') + elif val > MAX_SAFE_INTEGER: + raise ValueError('Variable length integer too large') + else: + # Calculate number of bits divided by seven + self.size += ceil(len('{:02b}'.format(val)) / 7) + + def write_var_bytes(self, val): + """ + Calculate the size of a VARBYTES. + + A VARBYTES field consists of a VARUINT followed by that many bytes. + + Args: + val (varbytes): Contents for VARBYTES + """ + self.write_var_uint(len(val)) + self.size += len(val) + + def skip(self, in_bytes): + """ + Add this many bytes to the predicted size. + + Args: + in_bytes (int): Number of bytes to pretend to write. + """ + self.size += in_bytes + + +class Reader: + + def __init__(self, buffer): + self.buffer = buffer + self.cursor = 0 + self.bookmarks = [] + + @staticmethod + def from_source(source): + """ + Create a Reader from a source of bytes. + + Currently, this method only allows the creation of a Reader from a Buffer. + + If the object provided is already a Reader, that reader is returned as is. + + Args: + source (Reader|Buffer): Source of binary data. + Return: + Reader: Instance of Reader + """ + # if (Buffer.isBuffer(source)) { + # return new Reader(source) + # } else { + # throw new Error('Reader must be given a Buffer') + if isinstance(source, Reader): + return source + return Reader(source) + + def bookmark(self): + """ + Store the current cursor position on a stack. + """ + self.bookmarks.append(self.cursor) + + def restore(self): + """ + Pop the most recently bookmarked cursor position off the stack. + """ + self.cursor = self.bookmarks.pop() + + def ensure_available(self, num_bytes): + """ + Ensure this number of bytes is buffered. + + This method checks that the given number of bytes is buffered and available + for reading. If insufficient bytes are available, the method throws an `OverflowError`. + + Args: + num_bytes (int): Number of bytes that should be available. + """ + if len(self.buffer) < self.cursor + num_bytes: + raise OverflowError('Tried to read {} bytes, but only {} bytes available' + .format(num_bytes, len(self.buffer.length) - self.cursor)) + + def read_uint8(self): + """ + Read a single unsigned 8 byte integer. + + Return: {Number} Contents of next byte. + """ + self.ensure_available(1) + value = self.buffer[self.cursor] + self.cursor += 1 + return value + + def peek_uint8(self): + """ + Look at the next byte, but don't advance the cursor. + + Return: {Number} Contents of the next byte. + """ + self.ensure_available(1) + return self.buffer.read_uint8(self.cursor) + + def skip_uint8(self): + """ + Advance cursor by one byte. + """ + self.cursor += 1 + + def read_var_uint(self): + """ + Read a VARUINT at the cursor position. + + A VARUINT is a variable length integer encoded as base128 where the highest + bit indicates that another byte is following. The first byte contains the + seven least significant bits of the number represented. + + Return the VARUINT and advances the cursor accordingly. + + Return: {Number} Value of the VARUINT. + """ + shift = 0 + result = 0 + + while True: + in_byte = self.read_uint8() + + result += (in_byte & REST) << shift if shift < 28 else (in_byte & REST) * (2 ** shift) + + shift += 7 + + # Don't allow numbers greater than Number.MAX_SAFE_INTEGER + if shift > 45: + raise ValueError('Too large variable integer') + + if not (in_byte & MSB): + break + + return result + + def peek_var_uint(self): + """ + Read the next VARUINT, but don't advance the cursor. + + Return: {Number} VARUINT at the cursor position. + """ + self.bookmark() + value = self.read_var_uint() + self.restore() + + return value + + def skip_var_uint(self): + """ + Skip past the VARUINT at the cursor position. + """ + # Read variable integer and ignore output + self.read_var_uint() + + def read_var_bytes(self): + """ + Read a VARBYTES. + + A VARBYTES field consists of a VARUINT followed by that many bytes. + + Return: {Buffer} Contents of the VARBYTES. + """ + return self.read(self.read_var_uint()) + + def peek_var_bytes(self): + """ + Read a VARBYTES, but do not advance cursor position. + + Return: {Buffer} Contents of the VARBYTES. + """ + self.bookmark() + value = self.read_var_bytes() + self.restore() + + return value + + def skip_var_bytes(self): + """ + Skip a VARBYTES. + """ + self.skip(len(self.read_var_bytes())) + + def read(self, num_bytes): + """ + Read a given number of bytes. + + Returns this many bytes starting at the cursor position and advances the + cursor. + + Args: + num_bytes (int): Number of bytes to read. + + Return: + Contents of bytes read. + """ + self.ensure_available(num_bytes) + + value = self.buffer[self.cursor:self.cursor + num_bytes] + self.cursor += num_bytes + + return value + + def peek(self, num_bytes): + """ + Read bytes, but do not advance cursor. + + Args: + num_bytes (int): Number of bytes to read. + + Return: + Contents of bytes read. + """ + + self.ensure_available(num_bytes) + + return self.buffer.slice(self.cursor, self.cursor + num_bytes) + + def skip(self, num_bytes): + """ + Skip a number of bytes. + + Advances the cursor by this many bytes. + + Args: + num_bytes (int): Number of bytes to advance the cursor by. + """ + self.ensure_available(num_bytes) + + self.cursor += num_bytes + + +def base64_add_padding(data): + """ + Add enough padding for base64 encoding such that length is a multiple of 4 + + Args: + data: unpadded string or bytes + Return: + bytes: The padded bytes + + """ + + if isinstance(data, string_types): + data = data.encode('utf-8') + missing_padding = 4 - len(data) % 4 + if missing_padding: + data += b'=' * missing_padding + return data + + +def base64_remove_padding(data): + """ + Remove padding from base64 encoding + + Args: + data: fully padded base64 data + Return: + base64: Unpadded base64 bytes + + """ + if isinstance(data, string_types): + data = data.encode('utf-8') + return data.replace(b'=', b'') diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py new file mode 100644 index 00000000..33ae1150 --- /dev/null +++ b/tests/crypto/test_fulfillment.py @@ -0,0 +1,115 @@ +import binascii + +from bigchaindb.crypto.condition import Condition +from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey +from bigchaindb.crypto.fulfillment import Fulfillment +from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment + + +class TestBigchainILPFulfillmentEd25519Sha256: + PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf' + PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' + PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' + + PRIVATE_HEX_ILP = b'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42' + PRIVATE_B64_ILP = b'gz/mJAkje51i7HdYdSCRHpp1nOwdGXVbfakBuW3KPUI' + PRIVATE_B58_ILP = '9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs' + + CONDITION_ED25519_ILP = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' + FULFILLMENT_ED25519_ILP = \ + 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531' \ + 'PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' + HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' + + def test_ilp_keys(self): + sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) + assert sk.private_key.to_ascii(encoding='base64') == self.PRIVATE_B64_ILP + assert binascii.hexlify(sk.private_key.to_bytes()[:32]) == self.PRIVATE_HEX_ILP + + vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + assert vk.public_key.to_ascii(encoding='base64') == self.PUBLIC_B64_ILP + assert binascii.hexlify(vk.public_key.to_bytes()) == self.PUBLIC_HEX_ILP + + def test_serialize_condition_and_validate_fulfillment(self): + sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) + vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + + fulfillment = Ed25519Sha256Fulfillment() + fulfillment.public_key = vk + fulfillment.message_prefix = 'Hello world!' + fulfillment.max_dynamic_message_length = 32 # defaults to 0 + + assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP + assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP + + fulfillment.message = ' Conditions are here!' + + # ED25519-SHA256 condition not fulfilled + assert fulfillment.validate() == False + + # Fulfill an ED25519-SHA256 condition + fulfillment.sign(sk) + + assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP + assert fulfillment.validate() + + def test_deserialize_condition(self): + + deserialized_condition = Condition.from_uri(self.CONDITION_ED25519_ILP) + + assert deserialized_condition.serialize_uri() == self.CONDITION_ED25519_ILP + assert binascii.hexlify(deserialized_condition.hash) == self.HASH_ED25519_HEX_ILP + + def test_serialize_deserialize_condition(self): + vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + + fulfillment = Ed25519Sha256Fulfillment() + fulfillment.public_key = vk + fulfillment.message_prefix = 'Hello world!' + fulfillment.max_dynamic_message_length = 32 + + condition = fulfillment.condition + deserialized_condition = Condition.from_uri(condition.serialize_uri()) + + assert deserialized_condition.bitmask == condition.bitmask + assert deserialized_condition.hash == condition.hash + assert deserialized_condition.max_fulfillment_length == condition.max_fulfillment_length + assert deserialized_condition.serialize_uri() == condition.serialize_uri() + + def test_deserialize_fulfillment(self): + fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + + assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP + assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP + assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP + assert fulfillment.public_key.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP + assert fulfillment.validate() + + def test_serializer_deserialize_fulfillment(self): + sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) + vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + + fulfillment = Ed25519Sha256Fulfillment() + fulfillment.public_key = vk + fulfillment.message_prefix = 'Hello world!' + fulfillment.max_dynamic_message_length = 32 # defaults to 0 + fulfillment.message = ' Conditions are here!' + fulfillment.sign(sk) + + assert fulfillment.validate() + + deserialized_fulfillment = Fulfillment.from_uri(fulfillment.serialize_uri()) + assert deserialized_fulfillment.serialize_uri() == fulfillment.serialize_uri() + assert deserialized_fulfillment.condition.serialize_uri() == fulfillment.condition.serialize_uri() + assert deserialized_fulfillment.public_key.public_key.to_bytes() == fulfillment.public_key.public_key.to_bytes() + assert deserialized_fulfillment.validate() + + +class TestBigchainILPConditionSha256: + + CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' + + def test_deserialize_condition(self): + example_condition = self.CONDITION_SHA256_ILP + condition = Condition.from_uri(example_condition) + assert condition.serialize_uri() == self.CONDITION_SHA256_ILP From 821ca9f8e68d6971f19a548305d8e7ffd3834686 Mon Sep 17 00:00:00 2001 From: diminator Date: Fri, 18 Mar 2016 18:46:22 +0100 Subject: [PATCH 06/16] threshold fulfillment tests --- bigchaindb/crypto/bitmark_registry.py | 2 + bigchaindb/crypto/condition.py | 74 +++++- bigchaindb/crypto/fulfillment.py | 58 ++++- .../crypto/fulfillments/ed25519_sha256.py | 13 +- .../crypto/fulfillments/threshold_sha256.py | 231 ++++++++++++++++++ bigchaindb/crypto/iostream.py | 8 +- tests/crypto/test_fulfillment.py | 141 ++++++++++- 7 files changed, 498 insertions(+), 29 deletions(-) create mode 100644 bigchaindb/crypto/fulfillments/threshold_sha256.py diff --git a/bigchaindb/crypto/bitmark_registry.py b/bigchaindb/crypto/bitmark_registry.py index 5e3b071f..d5d06abe 100644 --- a/bigchaindb/crypto/bitmark_registry.py +++ b/bigchaindb/crypto/bitmark_registry.py @@ -51,6 +51,8 @@ class BitmaskRegistry: }) +from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment +BitmaskRegistry.register_type(ThresholdSha256Fulfillment) BitmaskRegistry.register_type(Ed25519Sha256Fulfillment) diff --git a/bigchaindb/crypto/condition.py b/bigchaindb/crypto/condition.py index 201c2eab..d218926c 100644 --- a/bigchaindb/crypto/condition.py +++ b/bigchaindb/crypto/condition.py @@ -4,7 +4,7 @@ from abc import ABCMeta from six import string_types -from bigchaindb.crypto.iostream import base64_add_padding, base64_remove_padding +from bigchaindb.crypto.iostream import base64_add_padding, base64_remove_padding, Writer, Reader CONDITION_REGEX = r'^cc:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]{43}:[1-9][0-9]{0,50}$' @@ -47,6 +47,27 @@ class Condition(metaclass=ABCMeta): return condition + @staticmethod + def from_binary(reader): + """ + * Create a Condition object from a binary blob. + * + * This method will parse a stream of binary data and construct a + * corresponding Condition object. + * + Args: + reader (Reader): Binary stream implementing the Reader interface + Returns: + Condition: Resulting object + """ + reader = Reader.from_source(reader) + + # Instantiate condition + condition = Condition() + condition.parse_binary(reader) + + return condition + @property def bitmask(self): """ @@ -152,12 +173,53 @@ class Condition(metaclass=ABCMeta): Turns the condition into a URI containing only URL-safe characters. This format is convenient for passing around conditions in URLs, JSON and other text-based formats. + "cc:" BASE10(VERSION) ":" BASE16(TYPE_BITMASK) ":" BASE64URL(HASH) ":" BASE10(MAX_FULFILLMENT_LENGTH) + Returns: string: Condition as a URI """ - return 'cc:1:{}:{}:{}'.format(self.bitmask, - base64_remove_padding( - base64.urlsafe_b64encode(self.hash) - ).decode('utf-8'), - self.max_fulfillment_length) + return 'cc:1:{:x}:{}:{}'.format(self.bitmask, + base64_remove_padding( + base64.urlsafe_b64encode(self.hash) + ).decode('utf-8'), + self.max_fulfillment_length) + + def serialize_binary(self): + """ + Serialize condition to a buffer. + + Encodes the condition as a string of bytes. This is used internally for + encoding subconditions, but can also be used to passing around conditions + in a binary protocol for instance. + + CONDITION = + VARUINT TYPE_BITMASK + VARBYTES HASH + VARUINT MAX_FULFILLMENT_LENGTH + + Return: + Serialized condition + """ + writer = Writer() + writer.write_var_uint(self.bitmask) + writer.write_var_bytes(self.hash) + writer.write_var_uint(self.max_fulfillment_length) + return b''.join(writer.components) + + + def parse_binary(self, reader): + """ + * Parse any condition in binary format. + * + * Will populate the condition object with data from the provided binary + * stream. + * + Args: + reader (Reader): Binary stream containing the condition. + """ + self.bitmask = reader.read_var_uint() + + # TODO: Ensure bitmask is supported? + self.hash = reader.read_var_bytes() + self.max_fulfillment_length = reader.read_var_uint() diff --git a/bigchaindb/crypto/fulfillment.py b/bigchaindb/crypto/fulfillment.py index fd32c19c..bed4dc45 100644 --- a/bigchaindb/crypto/fulfillment.py +++ b/bigchaindb/crypto/fulfillment.py @@ -54,6 +54,30 @@ class Fulfillment(metaclass=ABCMeta): return fulfillment + @staticmethod + def from_binary(reader): + """ + Create a Fulfillment object from a binary blob. + + This method will parse a stream of binary data and construct a + corresponding Fulfillment object. + + Args: + reader (Reader): Binary stream implementing the Reader interface + Returns: + Fulfillment: Resulting object + """ + reader = Reader.from_source(reader) + + from bigchaindb.crypto.bitmark_registry import BitmaskRegistry + + cls = BitmaskRegistry.get_class_from_typebit(reader.read_var_uint()) + + fulfillment = cls() + fulfillment.parse_payload(reader) + + return fulfillment + @property def bitmask(self): """ @@ -99,15 +123,37 @@ class Fulfillment(metaclass=ABCMeta): format is convenient for passing around fulfillments in URLs, JSON and other text-based formats. + "cf:" BASE10(VERSION) ":" BASE16(TYPE_BIT) ":" BASE64URL(FULFILLMENT_PAYLOAD) + Return: string: Fulfillment as a URI """ - return 'cf:1:{}:{}'.format(self.bitmask, - base64_remove_padding( - base64.urlsafe_b64encode( - b''.join(self.serialize_payload().components) - ) - ).decode('utf-8')) + return 'cf:1:{:x}:{}'.format(self._bitmask, + base64_remove_padding( + base64.urlsafe_b64encode( + b''.join(self.serialize_payload().components) + ) + ).decode('utf-8')) + + def serialize_binary(self): + """ + Serialize fulfillment to a buffer. + + Encodes the fulfillment as a string of bytes. This is used internally for + encoding subfulfillments, but can also be used to passing around + fulfillments in a binary protocol for instance. + + FULFILLMENT = + VARUINT TYPE_BIT + FULFILLMENT_PAYLOAD + + Return: + Serialized fulfillment + """ + writer = Writer() + writer.write_var_uint(self.bitmask) + self.write_payload(writer) + return b''.join(writer.components) def serialize_payload(self): """ diff --git a/bigchaindb/crypto/fulfillments/ed25519_sha256.py b/bigchaindb/crypto/fulfillments/ed25519_sha256.py index 388a27cf..321e41dc 100644 --- a/bigchaindb/crypto/fulfillments/ed25519_sha256.py +++ b/bigchaindb/crypto/fulfillments/ed25519_sha256.py @@ -8,11 +8,13 @@ from bigchaindb.crypto.iostream import Predictor class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): _bitmask = 0x08 - _message_prefix = None - _max_dynamic_message_length = None - _public_key = None - _message = None - _signature = None + + def __init__(self): + self._message_prefix = None + self._max_dynamic_message_length = None + self._public_key = None + self._message = None + self._signature = None @property def message_prefix(self): @@ -155,7 +157,6 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): self.max_dynamic_message_length = reader.read_var_uint() self.message = reader.read_var_bytes() self.signature = reader.read_var_bytes() - print(self.signature) def write_payload(self, writer): """ diff --git a/bigchaindb/crypto/fulfillments/threshold_sha256.py b/bigchaindb/crypto/fulfillments/threshold_sha256.py new file mode 100644 index 00000000..f0edad89 --- /dev/null +++ b/bigchaindb/crypto/fulfillments/threshold_sha256.py @@ -0,0 +1,231 @@ +from bigchaindb.crypto.condition import Condition +from bigchaindb.crypto.fulfillment import Fulfillment +from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment +from bigchaindb.crypto.iostream import Predictor, Reader, Writer + + +class ThresholdSha256Fulfillment(BaseSha256Fulfillment): + _bitmask = 0x04 + + def __init__(self): + self._threshold = None + + self.subconditions = [] + self.subfulfillments = [] + + def add_subcondition(self, subcondition): + """ + Add a subcondition (unfulfilled). + + This can be used to generate a new threshold condition from a set of + subconditions or to provide a non-fulfilled subcondition when creating a threshold fulfillment. + + Args: + subcondition (Condition): Condition to add + """ + if not isinstance(subcondition, Condition): + raise TypeError('Subconditions must be objects of type Condition') + self.subconditions.append(subcondition) + + def add_subfulfillment(self, subfulfillment): + """ + Add a fulfilled subcondition. + + When constructing a threshold fulfillment, this method allows you to + provide a fulfillment for one of the subconditions. + + Note that you do **not** have to add the subcondition if you're adding the + fulfillment. The condition can be calculated from the fulfillment and will + be added automatically. + + Args: + subfulfillment (Fulfillment): Fulfillment to add + """ + if not isinstance(subfulfillment, Fulfillment): + raise TypeError('Subfulfillments must be objects of type Fulfillment') + + self.subfulfillments.append(subfulfillment) + + def get_all_subconditions(self): + """ + Returns all subconditions including fulfilled ones. + + This method returns the subconditions plus all subfulfillments, converted to conditions. + + @return {Condition[]} Set of subconditions + """ + return self.subconditions + [f.condition for f in self.subfulfillments] + + @property + def threshold(self): + return self._threshold + + @threshold.setter + def threshold(self, value): + """ + Set the threshold. + + Determines the weighted threshold that is used to consider this condition + fulfilled. If the added weight of all valid subfulfillments is greater or + equal to this number, the threshold condition is considered to be fulfilled. + + Args: + value (int): Integer threshold + """ + self._threshold = value + + @property + def bitmask(self): + """ + Get full bitmask. + + This is a type of condition that can contain subconditions. A complete + bitmask must contain the set of types that must be supported in order to + validate this fulfillment. Therefore, we need to calculate the bitwise OR + of this condition's TYPE_BIT and all subcondition's and subfulfillment's bitmasks. + + @return {Number} Complete bitmask for this fulfillment. + """ + bitmask = self._bitmask + + for cond in self.subconditions: + bitmask |= cond.bitmask + + for f in self.subfulfillments: + bitmask |= f.bitmask + + return bitmask + + def write_hash_payload(self, hasher): + """ + Produce the contents of the condition hash. + + This function is called internally by the `getCondition` method. + + Args: + hasher (Hasher): Hash generator + """ + if not (len(self.subconditions) or len(self.subfulfillments)): + raise ValueError('Requires subconditions') + + subconditions = [c.serialize_binary() for c in self.get_all_subconditions()] + subconditions.sort(key=len) + + hasher.write_var_uint(ThresholdSha256Fulfillment()._bitmask) + hasher.write_var_uint(self.threshold) + hasher.write_var_uint(len(subconditions)) + for cond in subconditions: + hasher.write(cond) + return hasher + + def calculate_max_fulfillment_length(self): + """ + Calculates the longest possible fulfillment length. + + In a threshold condition, the maximum length of the fulfillment depends on + the maximum lengths of the fulfillments of the subconditions. However, + usually not all subconditions must be fulfilled to meet the threshold. This + means we only need to consider the worst case where the largest number of + largest fulfillments are provided and the smaller fulfillments are not. + + The algorithm to calculate the worst case fulfillment size is not trivial, + however, it does not need to provide the exact worst-case fulfillment + length, only an upper bound for it. + + @return {Number} Maximum length of the fulfillment payload + + """ + # TODO: Currently wrong + + predictor = Predictor() + + # Calculate length of longest fulfillments + max_fulfillments_length = [c.max_fulfillment_length for c in self.get_all_subconditions()] + max_fulfillments_length.sort() + worst_case_fulfillments_length = sum(max_fulfillments_length[-self.threshold:]) + + predictor.write_var_uint(2) + predictor.skip(worst_case_fulfillments_length) + + return predictor.size + + def parse_payload(self, reader): + """ + Parse a fulfillment payload. + + Read a fulfillment payload from a Reader and populate this object with that fulfillment. + + Args: + reader (Reader): Source to read the fulfillment payload from. + """ + if not isinstance(reader, Reader): + raise TypeError('reader must be a Reader instance') + self.threshold = reader.read_var_uint() + + fulfillment_count = reader.read_var_uint() + for i in range(fulfillment_count): + # TODO: Read weights + # const weight = 1 + reader.skip_var_uint() + self.add_subfulfillment(Fulfillment.from_binary(reader)) + + condition_count = reader.read_var_uint() + for i in range(condition_count): + # TODO: Read weights + # const weight = 1 + reader.skip_var_uint() + self.add_subcondition(Condition.from_binary(reader)) + + def write_payload(self, writer): + """ + Generate the fulfillment payload. + + This writes the fulfillment payload to a Writer. + + Args: + writer (Writer): Subject for writing the fulfillment payload. + """ + if not isinstance(writer, Writer): + raise TypeError('writer must be a Writer instance') + conditions = [c.serialize_binary() for c in self.subconditions] + + # Get as many fulfillments as possible + fulfillments = [{'fulfillment': f, 'binary': f.serialize_binary()} for f in self.subfulfillments] + + # Prefer shorter fulfillments + fulfillments.sort(key=lambda f: len(f['binary'])) + + if len(fulfillments) < self.threshold: + raise ValueError('Not enough subfulfillments') + + while len(fulfillments) > self.threshold: + # TODO: only for valid fulfillments? + conditions.append(fulfillments.pop()['fulfillment'].condition.serialize_binary()) + + writer.write_var_uint(self.threshold) + + writer.write_var_uint(len(fulfillments)) + for fulfillment in fulfillments: + # TODO: Support custom weights + writer.write_var_uint(1) + writer.write(fulfillment['binary']) + + writer.write_var_uint(len(conditions)) + for condition in conditions: + # TODO: Support custom weights + writer.write_var_uint(1) + writer.write(condition) + + return writer + + def validate(self): + """ + Check whether this fulfillment meets all validation criteria. + + This will validate the subfulfillments and verify that there are enough + subfulfillments to meet the threshold. + + @return {Boolean} Whether this fulfillment is valid. + """ + validations = [f.validate() for f in self.subfulfillments] + return len([v for v in validations]) >= self.threshold diff --git a/bigchaindb/crypto/iostream.py b/bigchaindb/crypto/iostream.py index 2dd76a68..6ac51705 100644 --- a/bigchaindb/crypto/iostream.py +++ b/bigchaindb/crypto/iostream.py @@ -96,7 +96,9 @@ class Writer: class Hasher(Writer): - hash = None + def __init__(self): + self.hash = None + super().__init__() def __init__(self, algorithm): if algorithm == 'sha256': @@ -149,7 +151,9 @@ class Hasher(Writer): class Predictor: - size = 0 + + def __init__(self): + self.size = 0 def write_var_uint(self, val): """ diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index 33ae1150..cfa26b31 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -1,12 +1,26 @@ import binascii +from math import floor, ceil + +import pytest + from bigchaindb.crypto.condition import Condition from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey from bigchaindb.crypto.fulfillment import Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment +from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment -class TestBigchainILPFulfillmentEd25519Sha256: +class TestBigchainILPSha256Condition: + CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' + + def test_deserialize_condition(self): + example_condition = self.CONDITION_SHA256_ILP + condition = Condition.from_uri(example_condition) + assert condition.serialize_uri() == self.CONDITION_SHA256_ILP + + +class TestBigchainILPEd25519Sha256Fulfillment: PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf' PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' @@ -54,7 +68,6 @@ class TestBigchainILPFulfillmentEd25519Sha256: assert fulfillment.validate() def test_deserialize_condition(self): - deserialized_condition = Condition.from_uri(self.CONDITION_ED25519_ILP) assert deserialized_condition.serialize_uri() == self.CONDITION_ED25519_ILP @@ -85,7 +98,7 @@ class TestBigchainILPFulfillmentEd25519Sha256: assert fulfillment.public_key.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP assert fulfillment.validate() - def test_serializer_deserialize_fulfillment(self): + def test_serialize_deserialize_fulfillment(self): sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) vk = ED25519PublicKey(self.PUBLIC_B58_ILP) @@ -105,11 +118,121 @@ class TestBigchainILPFulfillmentEd25519Sha256: assert deserialized_fulfillment.validate() -class TestBigchainILPConditionSha256: +class TestBigchainILPThresholdSha256Fulfillment: + PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' + PRIVATE_B58_ILP = '9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs' - CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' + CONDITION_ED25519_ILP = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' + FULFILLMENT_ED25519_ILP = \ + 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531' \ + 'PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' - def test_deserialize_condition(self): - example_condition = self.CONDITION_SHA256_ILP - condition = Condition.from_uri(example_condition) - assert condition.serialize_uri() == self.CONDITION_SHA256_ILP + CONDITION_ED25519_ILP_2 = 'cc:1:8:_WzTrHvFnv4I-H0cAKWZ6Q3g3Y0Du3aW01nIsaAsio8:116' + FULFILLMENT_ED25519_ILP_2 = \ + 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_D0hlbGxvIHVuaXZlcnNlISAbIENvbmRpdGlvbnMgYXJlIGV2ZXJ5d2hlc' \ + 'mUhQNmD2Cvk7e3EFOo-arA2TKYTP-474Z4okhbYmKij6XxObIbRsDScjXILAJ6mV5hP7Xyqkg5fcSsZbfRYypzlsAM' + HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' + + CONDITION_THRESHOLD_ED25519_ILP_2 = 'cc:1:c:IZgoTeE1Weg6tfGMLWGe2JmS-waBN-CUrlbhtI9GBcQ:230' + FULFILLMENT_THRESHOLD_ED25519_ILP_2 = \ + 'cf:1:4:AgIBCCDsFyuTrV5WO_STLHDhJFA0w1Rn7y79TWTr-BloNGfivwxIZWxsbyB3b3JsZCEgFSBDb25kaXRpb25zIGFyZSBoZXJlIUBDW' \ + '6ped9T2wiZUVLyoz-epNFyiTDqyBqNheurnrk7UZ2KyQdrdmbbXX1zOIMw__O3h9Z2U6buK05AMfNYUnacCAQgg7Bcrk61eVjv0kyxw4SRQN' \ + 'MNUZ-8u_U1k6_gZaDRn4r8MSGVsbG8gd29ybGQhIBUgQ29uZGl0aW9ucyBhcmUgaGVyZSFAQ1uqXnfU9sImVFS8qM_nqTRcokw6sgajYXrq5' \ + '65O1GdiskHa3Zm2119cziDMP_zt4fWdlOm7itOQDHzWFJ2nAgEBCCD9bNOse8We_gj4fRwApZnpDeDdjQO7dpbTWcixoCyKj3Q' + + def create_fulfillment_ed25519sha256(self): + sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) + vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + + fulfillment = Ed25519Sha256Fulfillment() + fulfillment.public_key = vk + fulfillment.message_prefix = 'Hello world!' + fulfillment.max_dynamic_message_length = 32 # defaults to 0 + fulfillment.message = ' Conditions are here!' + fulfillment.sign(sk) + return fulfillment + + def test_serialize_condition_and_validate_fulfillment(self): + ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + ilp_fulfillment_2 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) + + assert ilp_fulfillment.validate() == True + assert ilp_fulfillment_2.validate() == True + + THRESHOLD = 2 + + # Create a threshold condition + fulfillment = ThresholdSha256Fulfillment() + fulfillment.add_subfulfillment(ilp_fulfillment_2) + fulfillment.add_subfulfillment(ilp_fulfillment) + fulfillment.add_subfulfillment(ilp_fulfillment) + fulfillment.threshold = THRESHOLD # defaults to subconditions.length + + assert fulfillment.condition.serialize_uri() == self.CONDITION_THRESHOLD_ED25519_ILP_2 + # Note: If there are more than enough fulfilled subconditions, shorter + # fulfillments will be chosen over longer ones. + # thresholdFulfillmentUri.length === 65 + assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2 + assert fulfillment.validate() + + def test_deserialize_fulfillment(self): + NUM_FULFILLMENTS = 3 + THRESHOLD = 2 + + fulfillment = Fulfillment.from_uri(self.FULFILLMENT_THRESHOLD_ED25519_ILP_2) + assert fulfillment.threshold == THRESHOLD + assert len(fulfillment.subfulfillments) == THRESHOLD + assert len(fulfillment.get_all_subconditions()) == NUM_FULFILLMENTS + assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2 + assert fulfillment.validate() + + def test_serialize_deserialize_fulfillment(self): + ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + NUM_FULFILLMENTS = 100 + THRESHOLD = ceil(NUM_FULFILLMENTS * 2 / 3) + + # Create a threshold condition + fulfillment = ThresholdSha256Fulfillment() + for i in range(NUM_FULFILLMENTS): + fulfillment.add_subfulfillment(ilp_fulfillment) + fulfillment.threshold = THRESHOLD + + fulfillment_uri = fulfillment.serialize_uri() + + assert fulfillment.validate() + deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri) + + assert deserialized_fulfillment.threshold == THRESHOLD + assert len(deserialized_fulfillment.subfulfillments) == THRESHOLD + assert len(deserialized_fulfillment.get_all_subconditions()) == NUM_FULFILLMENTS + assert deserialized_fulfillment.serialize_uri() == fulfillment_uri + assert deserialized_fulfillment.validate() + + def test_fulfillment_didnt_reach_threshold(self): + ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + THRESHOLD = 10 + + # Create a threshold condition + fulfillment = ThresholdSha256Fulfillment() + fulfillment.threshold = THRESHOLD + + for i in range(THRESHOLD - 1): + fulfillment.add_subfulfillment(ilp_fulfillment) + + with pytest.raises(ValueError): + fulfillment.serialize_uri() + + assert fulfillment.validate() is False + + fulfillment.add_subfulfillment(ilp_fulfillment) + + fulfillment_uri = fulfillment.serialize_uri() + assert fulfillment.validate() + + deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri) + + assert deserialized_fulfillment.threshold == THRESHOLD + assert len(deserialized_fulfillment.subfulfillments) == THRESHOLD + assert len(deserialized_fulfillment.get_all_subconditions()) == THRESHOLD + assert deserialized_fulfillment.serialize_uri() == fulfillment_uri + assert deserialized_fulfillment.validate() From 9bd1e9ff8bf745a970ca58d5759847301282cd48 Mon Sep 17 00:00:00 2001 From: diminator Date: Fri, 18 Mar 2016 20:01:47 +0100 Subject: [PATCH 07/16] sha256 fulfillment tests --- bigchaindb/crypto/bitmark_registry.py | 2 + bigchaindb/crypto/fulfillment.py | 6 +- .../crypto/fulfillments/ed25519_sha256.py | 21 +++++ bigchaindb/crypto/fulfillments/sha256.py | 94 +++++++++++++++++++ .../crypto/fulfillments/threshold_sha256.py | 18 ++++ tests/crypto/test_fulfillment.py | 58 ++++++++++-- 6 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 bigchaindb/crypto/fulfillments/sha256.py diff --git a/bigchaindb/crypto/bitmark_registry.py b/bigchaindb/crypto/bitmark_registry.py index d5d06abe..686bf845 100644 --- a/bigchaindb/crypto/bitmark_registry.py +++ b/bigchaindb/crypto/bitmark_registry.py @@ -51,8 +51,10 @@ class BitmaskRegistry: }) +from bigchaindb.crypto.fulfillments.sha256 import Sha256Fulfillment from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment +BitmaskRegistry.register_type(Sha256Fulfillment) BitmaskRegistry.register_type(ThresholdSha256Fulfillment) BitmaskRegistry.register_type(Ed25519Sha256Fulfillment) diff --git a/bigchaindb/crypto/fulfillment.py b/bigchaindb/crypto/fulfillment.py index bed4dc45..b2297a47 100644 --- a/bigchaindb/crypto/fulfillment.py +++ b/bigchaindb/crypto/fulfillment.py @@ -5,7 +5,7 @@ from abc import ABCMeta, abstractmethod from six import string_types from bigchaindb.crypto.condition import Condition -from bigchaindb.crypto.iostream import Writer, base64_remove_padding, Reader, base64_add_padding +from bigchaindb.crypto.iostream import Writer, base64_remove_padding, Reader, base64_add_padding, Predictor FULFILLMENT_REGEX = r'^cf:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]+$' @@ -179,9 +179,7 @@ class Fulfillment(metaclass=ABCMeta): Return: {Number} Maximum fulfillment length """ - # TODO: Predictor - # predictor = Predictor() - predictor = None + predictor = Predictor() self.write_payload(predictor) return predictor.size diff --git a/bigchaindb/crypto/fulfillments/ed25519_sha256.py b/bigchaindb/crypto/fulfillments/ed25519_sha256.py index 321e41dc..c3fb4bfe 100644 --- a/bigchaindb/crypto/fulfillments/ed25519_sha256.py +++ b/bigchaindb/crypto/fulfillments/ed25519_sha256.py @@ -164,6 +164,17 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): This writes the fulfillment payload to a Writer. + COMMON_HEADER = + VARBYTES PUBLIC_KEY + VARBYTES MESSAGE_ID + VARBYTES FIXED_PREFIX + VARUINT DYNAMIC_MESSAGE_LENGTH + + FULFILLMENT_PAYLOAD = + COMMON_HEADER + VARBYTES DYNAMIC_MESSAGE + VARBYTES SIGNATURE + Args: writer (Writer): Subject for writing the fulfillment payload. """ @@ -178,6 +189,16 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): Writes the contents of the condition hash to a Hasher. Used internally by `condition`. + COMMON_HEADER = + VARBYTES PUBLIC_KEY + VARBYTES MESSAGE_ID + VARBYTES FIXED_PREFIX + VARUINT DYNAMIC_MESSAGE_LENGTH + + HASH = SHA256( + COMMON_HEADER + ) + Args: hasher (Hasher): Destination where the hash payload will be written. """ diff --git a/bigchaindb/crypto/fulfillments/sha256.py b/bigchaindb/crypto/fulfillments/sha256.py new file mode 100644 index 00000000..a24e4087 --- /dev/null +++ b/bigchaindb/crypto/fulfillments/sha256.py @@ -0,0 +1,94 @@ +from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment +from bigchaindb.crypto.iostream import Hasher, Reader, Writer, Predictor + + +class Sha256Fulfillment(BaseSha256Fulfillment): + + _bitmask = 0x01 + + def __init__(self): + self._preimage = None + + @property + def preimage(self): + return self._preimage + + @preimage.setter + def preimage(self, value): + """ + Provide a preimage. + + The preimage is the only input to a SHA256 hashlock condition. + + Note that the preimage should contain enough (pseudo-random) data in order + to be difficult to guess. A sufficiently large secret seed and a + cryptographically secure pseudo-random number generator (CSPRNG) can be + used to avoid having to store each individual preimage. + + Args: + value: Secret data that will be hashed to form the condition. + """ + # TODO: Verify preimage + self._preimage = value + + def write_hash_payload(self, hasher): + """ + Generate the contents of the condition hash. + + Writes the contents of the condition hash to a Hasher. Used internally by `getCondition`. + + HASH = SHA256(PREIMAGE) + + Args: + hasher (Hasher): Destination where the hash payload will be written. + """ + if not isinstance(hasher, Hasher): + raise TypeError('hasher must be a Hasher instance') + if self.preimage is None: + raise ValueError('Could not calculate hash, no preimage provided') + hasher.write(self.preimage) + + def parse_payload(self, reader): + """ + Parse the payload of a SHA256 hashlock fulfillment. + + Read a fulfillment payload from a Reader and populate this object with that fulfillment. + + FULFILLMENT_PAYLOAD = + VARBYTES PREIMAGE + + Args: + reader (Reader): Source to read the fulfillment payload from. + """ + if not isinstance(reader, Reader): + raise TypeError('reader must be a Reader instance') + self.preimage = reader.read_var_bytes() + + def write_payload(self, writer): + """ + Generate the fulfillment payload. + + This writes the fulfillment payload to a Writer. + + Args: + writer (Writer): Subject for writing the fulfillment payload. + """ + if not isinstance(writer, (Writer, Predictor)): + raise TypeError('writer must be a Writer instance') + if self.preimage is None: + raise ValueError('Preimage must be specified') + + writer.write_var_bytes(self.preimage) + return writer + + def validate(self): + """ + Validate this fulfillment. + + For a SHA256 hashlock fulfillment, successful parsing implies that the + fulfillment is valid, so this method is a no-op. + + Returns: + boolean: Validation result + """ + return True diff --git a/bigchaindb/crypto/fulfillments/threshold_sha256.py b/bigchaindb/crypto/fulfillments/threshold_sha256.py index f0edad89..bb22361d 100644 --- a/bigchaindb/crypto/fulfillments/threshold_sha256.py +++ b/bigchaindb/crypto/fulfillments/threshold_sha256.py @@ -102,6 +102,14 @@ class ThresholdSha256Fulfillment(BaseSha256Fulfillment): This function is called internally by the `getCondition` method. + HASH = SHA256( + VARUINT TYPE_BIT + VARUINT THRESHOLD + VARARRAY + VARUINT WEIGHT + CONDITION + ) + Args: hasher (Hasher): Hash generator """ @@ -182,6 +190,15 @@ class ThresholdSha256Fulfillment(BaseSha256Fulfillment): This writes the fulfillment payload to a Writer. + FULFILLMENT_PAYLOAD = + VARUINT THRESHOLD + VARARRAY + VARUINT WEIGHT + FULFILLMENT + VARARRAY + VARUINT WEIGHT + CONDITION + Args: writer (Writer): Subject for writing the fulfillment payload. """ @@ -195,6 +212,7 @@ class ThresholdSha256Fulfillment(BaseSha256Fulfillment): # Prefer shorter fulfillments fulfillments.sort(key=lambda f: len(f['binary'])) + # Cut off unnecessary fulfillments if len(fulfillments) < self.threshold: raise ValueError('Not enough subfulfillments') diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index cfa26b31..30d74e0b 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -8,6 +8,7 @@ from bigchaindb.crypto.condition import Condition from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey from bigchaindb.crypto.fulfillment import Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment +from bigchaindb.crypto.fulfillments.sha256 import Sha256Fulfillment from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment @@ -20,6 +21,38 @@ class TestBigchainILPSha256Condition: assert condition.serialize_uri() == self.CONDITION_SHA256_ILP +class TestBigchainILPSha256Fulfillment: + CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' + FULFILLMENT_SHA256_ILP = 'cf:1:1:AA' + + def test_deserialize_and_validate_fulfillment(self): + fulfillment = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) + assert fulfillment.serialize_uri() == self.FULFILLMENT_SHA256_ILP + assert fulfillment.condition.serialize_uri() == self.CONDITION_SHA256_ILP + assert fulfillment.validate() + + def test_deserialize_condition_and_validate_fulfillment(self): + condition = Condition.from_uri(self.CONDITION_SHA256_ILP) + fulfillment = Sha256Fulfillment() + fulfillment.preimage = '' + assert fulfillment.serialize_uri() == self.FULFILLMENT_SHA256_ILP + assert fulfillment.condition.serialize_uri() == condition.serialize_uri() + assert fulfillment.validate() + + def test_condition_from_fulfillment(self): + fulfillment = Sha256Fulfillment() + with pytest.raises(ValueError): + fulfillment.condition + + fulfillment.preimage = 'Hello World!' + condition = fulfillment.condition + + verify_fulfillment = Sha256Fulfillment() + verify_fulfillment.preimage = 'Hello World!' + assert verify_fulfillment.condition.serialize_uri() == condition.serialize_uri() + assert verify_fulfillment.validate() + + class TestBigchainILPEd25519Sha256Fulfillment: PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf' PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' @@ -133,12 +166,14 @@ class TestBigchainILPThresholdSha256Fulfillment: 'mUhQNmD2Cvk7e3EFOo-arA2TKYTP-474Z4okhbYmKij6XxObIbRsDScjXILAJ6mV5hP7Xyqkg5fcSsZbfRYypzlsAM' HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' - CONDITION_THRESHOLD_ED25519_ILP_2 = 'cc:1:c:IZgoTeE1Weg6tfGMLWGe2JmS-waBN-CUrlbhtI9GBcQ:230' + CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' + FULFILLMENT_SHA256_ILP = 'cf:1:1:AA' + + CONDITION_THRESHOLD_ED25519_ILP_2 = 'cc:1:d:fDM51fekeLlbeF9yj9W1KT76jtqa7u0vMlJAbM4EyiE:230' FULFILLMENT_THRESHOLD_ED25519_ILP_2 = \ - 'cf:1:4:AgIBCCDsFyuTrV5WO_STLHDhJFA0w1Rn7y79TWTr-BloNGfivwxIZWxsbyB3b3JsZCEgFSBDb25kaXRpb25zIGFyZSBoZXJlIUBDW' \ - '6ped9T2wiZUVLyoz-epNFyiTDqyBqNheurnrk7UZ2KyQdrdmbbXX1zOIMw__O3h9Z2U6buK05AMfNYUnacCAQgg7Bcrk61eVjv0kyxw4SRQN' \ - 'MNUZ-8u_U1k6_gZaDRn4r8MSGVsbG8gd29ybGQhIBUgQ29uZGl0aW9ucyBhcmUgaGVyZSFAQ1uqXnfU9sImVFS8qM_nqTRcokw6sgajYXrq5' \ - '65O1GdiskHa3Zm2119cziDMP_zt4fWdlOm7itOQDHzWFJ2nAgEBCCD9bNOse8We_gj4fRwApZnpDeDdjQO7dpbTWcixoCyKj3Q' + 'cf:1:4:AgIBAQABCCDsFyuTrV5WO_STLHDhJFA0w1Rn7y79TWTr-BloNGfivwxIZWxsbyB3b3JsZCEgFSBDb25kaXRpb25zIGFyZSBoZXJlI' \ + 'UBDW6ped9T2wiZUVLyoz-epNFyiTDqyBqNheurnrk7UZ2KyQdrdmbbXX1zOIMw__O3h9Z2U6buK05AMfNYUnacCAQEIIP1s06x7xZ7-CPh9H' \ + 'AClmekN4N2NA7t2ltNZyLGgLIqPdA' def create_fulfillment_ed25519sha256(self): sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) @@ -153,8 +188,9 @@ class TestBigchainILPThresholdSha256Fulfillment: return fulfillment def test_serialize_condition_and_validate_fulfillment(self): - ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) - ilp_fulfillment_2 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) + ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) + ilp_fulfillment_2 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + ilp_fulfillment_3 = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) assert ilp_fulfillment.validate() == True assert ilp_fulfillment_2.validate() == True @@ -163,9 +199,9 @@ class TestBigchainILPThresholdSha256Fulfillment: # Create a threshold condition fulfillment = ThresholdSha256Fulfillment() + fulfillment.add_subfulfillment(ilp_fulfillment) fulfillment.add_subfulfillment(ilp_fulfillment_2) - fulfillment.add_subfulfillment(ilp_fulfillment) - fulfillment.add_subfulfillment(ilp_fulfillment) + fulfillment.add_subfulfillment(ilp_fulfillment_3) fulfillment.threshold = THRESHOLD # defaults to subconditions.length assert fulfillment.condition.serialize_uri() == self.CONDITION_THRESHOLD_ED25519_ILP_2 @@ -185,6 +221,10 @@ class TestBigchainILPThresholdSha256Fulfillment: assert len(fulfillment.get_all_subconditions()) == NUM_FULFILLMENTS assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2 assert fulfillment.validate() + assert isinstance(fulfillment.subfulfillments[0], Sha256Fulfillment) + assert isinstance(fulfillment.subfulfillments[1], Ed25519Sha256Fulfillment) + assert fulfillment.subfulfillments[0].condition.serialize_uri() == self.CONDITION_SHA256_ILP + assert fulfillment.subfulfillments[1].condition.serialize_uri() == self.CONDITION_ED25519_ILP def test_serialize_deserialize_fulfillment(self): ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) From bdc2475bddb89f39020997daadbb49150a4b1ca6 Mon Sep 17 00:00:00 2001 From: diminator Date: Fri, 18 Mar 2016 20:20:18 +0100 Subject: [PATCH 08/16] cleanup --- bigchaindb/crypto/condition.py | 21 ++++----- .../crypto/fulfillments/threshold_sha256.py | 12 +++-- bigchaindb/crypto/iostream.py | 40 +++++++--------- tests/crypto/test_fulfillment.py | 47 ++++++++++--------- 4 files changed, 61 insertions(+), 59 deletions(-) diff --git a/bigchaindb/crypto/condition.py b/bigchaindb/crypto/condition.py index d218926c..9c1fd6f8 100644 --- a/bigchaindb/crypto/condition.py +++ b/bigchaindb/crypto/condition.py @@ -50,11 +50,11 @@ class Condition(metaclass=ABCMeta): @staticmethod def from_binary(reader): """ - * Create a Condition object from a binary blob. - * - * This method will parse a stream of binary data and construct a - * corresponding Condition object. - * + Create a Condition object from a binary blob. + + This method will parse a stream of binary data and construct a + corresponding Condition object. + Args: reader (Reader): Binary stream implementing the Reader interface Returns: @@ -207,14 +207,13 @@ class Condition(metaclass=ABCMeta): writer.write_var_uint(self.max_fulfillment_length) return b''.join(writer.components) - def parse_binary(self, reader): """ - * Parse any condition in binary format. - * - * Will populate the condition object with data from the provided binary - * stream. - * + Parse any condition in binary format. + + Will populate the condition object with data from the provided binary + stream. + Args: reader (Reader): Binary stream containing the condition. """ diff --git a/bigchaindb/crypto/fulfillments/threshold_sha256.py b/bigchaindb/crypto/fulfillments/threshold_sha256.py index bb22361d..037d58a8 100644 --- a/bigchaindb/crypto/fulfillments/threshold_sha256.py +++ b/bigchaindb/crypto/fulfillments/threshold_sha256.py @@ -52,7 +52,8 @@ class ThresholdSha256Fulfillment(BaseSha256Fulfillment): This method returns the subconditions plus all subfulfillments, converted to conditions. - @return {Condition[]} Set of subconditions + Returns: + [Condition]: Set of subconditions """ return self.subconditions + [f.condition for f in self.subfulfillments] @@ -84,7 +85,8 @@ class ThresholdSha256Fulfillment(BaseSha256Fulfillment): validate this fulfillment. Therefore, we need to calculate the bitwise OR of this condition's TYPE_BIT and all subcondition's and subfulfillment's bitmasks. - @return {Number} Complete bitmask for this fulfillment. + Returns: + int: Complete bitmask for this fulfillment. """ bitmask = self._bitmask @@ -140,7 +142,8 @@ class ThresholdSha256Fulfillment(BaseSha256Fulfillment): however, it does not need to provide the exact worst-case fulfillment length, only an upper bound for it. - @return {Number} Maximum length of the fulfillment payload + Return: + int Maximum length of the fulfillment payload """ # TODO: Currently wrong @@ -243,7 +246,8 @@ class ThresholdSha256Fulfillment(BaseSha256Fulfillment): This will validate the subfulfillments and verify that there are enough subfulfillments to meet the threshold. - @return {Boolean} Whether this fulfillment is valid. + Returns: + boolean: Whether this fulfillment is valid. """ validations = [f.validate() for f in self.subfulfillments] return len([v for v in validations]) >= self.threshold diff --git a/bigchaindb/crypto/iostream.py b/bigchaindb/crypto/iostream.py index 6ac51705..5bcfc98b 100644 --- a/bigchaindb/crypto/iostream.py +++ b/bigchaindb/crypto/iostream.py @@ -9,10 +9,10 @@ from six import string_types MSB = 0x80 REST = 0x7F MSBALL = ~REST -INT = 2**31 +INT = 2 ** 31 # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER -MAX_SAFE_INTEGER = 2**53-1 +MAX_SAFE_INTEGER = 2 ** 53 - 1 class UnsignedLEB128: @@ -27,6 +27,7 @@ class UnsignedLEB128: see http://grokbase.com/t/python/python-list/112e5jpc16/encoding """ + @staticmethod def encode(obj): out = [] @@ -51,7 +52,6 @@ class UnsignedLEB128: class Writer: - def __init__(self): self.components = [] @@ -96,17 +96,13 @@ class Writer: class Hasher(Writer): - def __init__(self): - self.hash = None - super().__init__() - def __init__(self, algorithm): if algorithm == 'sha256': self.hash = hashlib.sha256() else: raise NotImplementedError - super(Writer, self).__init__() - + super().__init__() + def write(self, in_bytes): """ Adds bytes to the hash input. @@ -151,7 +147,6 @@ class Hasher(Writer): class Predictor: - def __init__(self): self.size = 0 @@ -166,7 +161,7 @@ class Predictor: Args: val (int): Integer to be encoded """ - + if val == 0: self.size += 1 elif val < 0: @@ -200,7 +195,6 @@ class Predictor: class Reader: - def __init__(self, buffer): self.buffer = buffer self.cursor = 0 @@ -294,18 +288,18 @@ class Reader: """ shift = 0 result = 0 - + while True: in_byte = self.read_uint8() - + result += (in_byte & REST) << shift if shift < 28 else (in_byte & REST) * (2 ** shift) - + shift += 7 - + # Don't allow numbers greater than Number.MAX_SAFE_INTEGER if shift > 45: raise ValueError('Too large variable integer') - + if not (in_byte & MSB): break @@ -320,7 +314,7 @@ class Reader: self.bookmark() value = self.read_var_uint() self.restore() - + return value def skip_var_uint(self): @@ -339,7 +333,7 @@ class Reader: Return: {Buffer} Contents of the VARBYTES. """ return self.read(self.read_var_uint()) - + def peek_var_bytes(self): """ Read a VARBYTES, but do not advance cursor position. @@ -372,12 +366,12 @@ class Reader: Contents of bytes read. """ self.ensure_available(num_bytes) - + value = self.buffer[self.cursor:self.cursor + num_bytes] self.cursor += num_bytes - + return value - + def peek(self, num_bytes): """ Read bytes, but do not advance cursor. @@ -403,7 +397,7 @@ class Reader: num_bytes (int): Number of bytes to advance the cursor by. """ self.ensure_available(num_bytes) - + self.cursor += num_bytes diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index 30d74e0b..efea4925 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -1,6 +1,6 @@ import binascii -from math import floor, ceil +from math import ceil import pytest @@ -125,6 +125,7 @@ class TestBigchainILPEd25519Sha256Fulfillment: def test_deserialize_fulfillment(self): fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + assert isinstance(fulfillment, Ed25519Sha256Fulfillment) assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP @@ -145,6 +146,7 @@ class TestBigchainILPEd25519Sha256Fulfillment: assert fulfillment.validate() deserialized_fulfillment = Fulfillment.from_uri(fulfillment.serialize_uri()) + assert isinstance(deserialized_fulfillment, Ed25519Sha256Fulfillment) assert deserialized_fulfillment.serialize_uri() == fulfillment.serialize_uri() assert deserialized_fulfillment.condition.serialize_uri() == fulfillment.condition.serialize_uri() assert deserialized_fulfillment.public_key.public_key.to_bytes() == fulfillment.public_key.public_key.to_bytes() @@ -195,14 +197,14 @@ class TestBigchainILPThresholdSha256Fulfillment: assert ilp_fulfillment.validate() == True assert ilp_fulfillment_2.validate() == True - THRESHOLD = 2 + threshold = 2 # Create a threshold condition fulfillment = ThresholdSha256Fulfillment() fulfillment.add_subfulfillment(ilp_fulfillment) fulfillment.add_subfulfillment(ilp_fulfillment_2) fulfillment.add_subfulfillment(ilp_fulfillment_3) - fulfillment.threshold = THRESHOLD # defaults to subconditions.length + fulfillment.threshold = threshold # defaults to subconditions.length assert fulfillment.condition.serialize_uri() == self.CONDITION_THRESHOLD_ED25519_ILP_2 # Note: If there are more than enough fulfilled subconditions, shorter @@ -212,13 +214,14 @@ class TestBigchainILPThresholdSha256Fulfillment: assert fulfillment.validate() def test_deserialize_fulfillment(self): - NUM_FULFILLMENTS = 3 - THRESHOLD = 2 + num_fulfillments = 3 + threshold = 2 fulfillment = Fulfillment.from_uri(self.FULFILLMENT_THRESHOLD_ED25519_ILP_2) - assert fulfillment.threshold == THRESHOLD - assert len(fulfillment.subfulfillments) == THRESHOLD - assert len(fulfillment.get_all_subconditions()) == NUM_FULFILLMENTS + assert isinstance(fulfillment, ThresholdSha256Fulfillment) + assert fulfillment.threshold == threshold + assert len(fulfillment.subfulfillments) == threshold + assert len(fulfillment.get_all_subconditions()) == num_fulfillments assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2 assert fulfillment.validate() assert isinstance(fulfillment.subfulfillments[0], Sha256Fulfillment) @@ -228,35 +231,36 @@ class TestBigchainILPThresholdSha256Fulfillment: def test_serialize_deserialize_fulfillment(self): ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) - NUM_FULFILLMENTS = 100 - THRESHOLD = ceil(NUM_FULFILLMENTS * 2 / 3) + num_fulfillments = 100 + threshold = ceil(num_fulfillments * 2 / 3) # Create a threshold condition fulfillment = ThresholdSha256Fulfillment() - for i in range(NUM_FULFILLMENTS): + for i in range(num_fulfillments): fulfillment.add_subfulfillment(ilp_fulfillment) - fulfillment.threshold = THRESHOLD + fulfillment.threshold = threshold fulfillment_uri = fulfillment.serialize_uri() assert fulfillment.validate() deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri) - assert deserialized_fulfillment.threshold == THRESHOLD - assert len(deserialized_fulfillment.subfulfillments) == THRESHOLD - assert len(deserialized_fulfillment.get_all_subconditions()) == NUM_FULFILLMENTS + assert isinstance(deserialized_fulfillment, ThresholdSha256Fulfillment) + assert deserialized_fulfillment.threshold == threshold + assert len(deserialized_fulfillment.subfulfillments) == threshold + assert len(deserialized_fulfillment.get_all_subconditions()) == num_fulfillments assert deserialized_fulfillment.serialize_uri() == fulfillment_uri assert deserialized_fulfillment.validate() def test_fulfillment_didnt_reach_threshold(self): ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) - THRESHOLD = 10 + threshold = 10 # Create a threshold condition fulfillment = ThresholdSha256Fulfillment() - fulfillment.threshold = THRESHOLD + fulfillment.threshold = threshold - for i in range(THRESHOLD - 1): + for i in range(threshold - 1): fulfillment.add_subfulfillment(ilp_fulfillment) with pytest.raises(ValueError): @@ -271,8 +275,9 @@ class TestBigchainILPThresholdSha256Fulfillment: deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri) - assert deserialized_fulfillment.threshold == THRESHOLD - assert len(deserialized_fulfillment.subfulfillments) == THRESHOLD - assert len(deserialized_fulfillment.get_all_subconditions()) == THRESHOLD + assert isinstance(deserialized_fulfillment, ThresholdSha256Fulfillment) + assert deserialized_fulfillment.threshold == threshold + assert len(deserialized_fulfillment.subfulfillments) == threshold + assert len(deserialized_fulfillment.get_all_subconditions()) == threshold assert deserialized_fulfillment.serialize_uri() == fulfillment_uri assert deserialized_fulfillment.validate() From f273e48c112e5d9370d37067574c50fe19129036 Mon Sep 17 00:00:00 2001 From: diminator Date: Fri, 18 Mar 2016 22:23:32 +0100 Subject: [PATCH 09/16] - Ed25519 inheritance - Renamed Private/PublicKey to Signing/VerifyingKey --- bigchaindb/core.py | 6 +- bigchaindb/crypto/asymmetric.py | 39 +++++++---- bigchaindb/crypto/bitmark_registry.py | 2 +- bigchaindb/crypto/{iostream.py => buffer.py} | 0 bigchaindb/crypto/condition.py | 5 +- bigchaindb/crypto/ecdsa.py | 26 ++++++-- bigchaindb/crypto/ed25519.py | 49 +++++++------- bigchaindb/crypto/fulfillment.py | 2 +- bigchaindb/crypto/fulfillments/base_sha256.py | 2 +- .../crypto/fulfillments/ed25519_sha256.py | 26 +++----- bigchaindb/crypto/fulfillments/sha256.py | 2 +- .../crypto/fulfillments/threshold_sha256.py | 2 +- bigchaindb/util.py | 6 +- tests/crypto/test_crypto.py | 64 +++++++++---------- tests/crypto/test_fulfillment.py | 36 +++++------ tests/db/test_bigchain_api.py | 14 ++-- tests/db/test_voter.py | 14 ++-- 17 files changed, 157 insertions(+), 138 deletions(-) rename bigchaindb/crypto/{iostream.py => buffer.py} (100%) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index be66ecf2..610aae46 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -96,7 +96,7 @@ class Bigchain(object): signature = data.pop('signature') public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = asymmetric.PublicKey(public_key_base58) + public_key = asymmetric.VerifyingKey(public_key_base58) return public_key.verify(util.serialize(data), signature) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) @@ -330,7 +330,7 @@ class Bigchain(object): # Calculate the hash of the new block block_data = util.serialize(block) block_hash = asymmetric.hash_data(block_data) - block_signature = asymmetric.PrivateKey(self.me_private).sign(block_data) + block_signature = asymmetric.SigningKey(self.me_private).sign(block_data) block = { 'id': block_hash, @@ -450,7 +450,7 @@ class Bigchain(object): } vote_data = util.serialize(vote) - signature = asymmetric.PrivateKey(self.me_private).sign(vote_data) + signature = asymmetric.SigningKey(self.me_private).sign(vote_data) vote_signed = { 'node_pubkey': self.me, diff --git a/bigchaindb/crypto/asymmetric.py b/bigchaindb/crypto/asymmetric.py index 807021a6..05d3dcfd 100644 --- a/bigchaindb/crypto/asymmetric.py +++ b/bigchaindb/crypto/asymmetric.py @@ -4,7 +4,7 @@ from abc import ABCMeta, abstractmethod import sha3 -class PrivateKey(metaclass=ABCMeta): +class SigningKey(metaclass=ABCMeta): """ PrivateKey instance """ @@ -13,6 +13,9 @@ class PrivateKey(metaclass=ABCMeta): def sign(self, data): """ Sign data with private key + + Args: + data: """ raise NotImplementedError @@ -20,20 +23,26 @@ class PrivateKey(metaclass=ABCMeta): @abstractmethod def encode(private_value): """ - Encode the decimal number private_value to base58 + Encode the internal private_value to base58 + + Args: + private_value: """ raise NotImplementedError @staticmethod @abstractmethod - def decode(key): + def decode(private_base58): """ - Decode the base58 private_value to decimale + Decode the base58 private value to internal value + + Args: + private_base58 (base58): """ raise NotImplementedError -class PublicKey(metaclass=ABCMeta): +class VerifyingKey(metaclass=ABCMeta): @abstractmethod def verify(self, data, signature): @@ -41,17 +50,23 @@ class PublicKey(metaclass=ABCMeta): @staticmethod @abstractmethod - def encode(public_value_x, public_value_y): + def encode(public_value): """ - Encode the public key represented by the decimal values x and y to base58 + Encode the public key to base58 represented by the internal values + + Args: + public_value """ raise NotImplementedError @staticmethod @abstractmethod - def decode(public_value_compressed_base58): + def decode(public_base58): """ - Decode the base58 public_value to the decimal x and y values + Decode the base58 public_value to internal value + + Args: + public_base58 (base58): """ raise NotImplementedError @@ -62,8 +77,8 @@ def hash_data(data): return sha3.sha3_256(data.encode()).hexdigest() -from bigchaindb.crypto.ecdsa import ECDSAPrivateKey, ECDSAPublicKey, ecdsa_generate_key_pair +from bigchaindb.crypto.ecdsa import EcdsaSigningKey, EcdsaVerifyingKey, ecdsa_generate_key_pair -PrivateKey = ECDSAPrivateKey -PublicKey = ECDSAPublicKey +SigningKey = EcdsaSigningKey +VerifyingKey = EcdsaVerifyingKey generate_key_pair = ecdsa_generate_key_pair diff --git a/bigchaindb/crypto/bitmark_registry.py b/bigchaindb/crypto/bitmark_registry.py index 686bf845..2f75b158 100644 --- a/bigchaindb/crypto/bitmark_registry.py +++ b/bigchaindb/crypto/bitmark_registry.py @@ -1,4 +1,4 @@ -from bigchaindb.crypto.iostream import MAX_SAFE_INTEGER +from bigchaindb.crypto.buffer import MAX_SAFE_INTEGER class BitmaskRegistry: diff --git a/bigchaindb/crypto/iostream.py b/bigchaindb/crypto/buffer.py similarity index 100% rename from bigchaindb/crypto/iostream.py rename to bigchaindb/crypto/buffer.py diff --git a/bigchaindb/crypto/condition.py b/bigchaindb/crypto/condition.py index 9c1fd6f8..94ee4f89 100644 --- a/bigchaindb/crypto/condition.py +++ b/bigchaindb/crypto/condition.py @@ -4,7 +4,7 @@ from abc import ABCMeta from six import string_types -from bigchaindb.crypto.iostream import base64_add_padding, base64_remove_padding, Writer, Reader +from bigchaindb.crypto.buffer import base64_add_padding, base64_remove_padding, Writer, Reader CONDITION_REGEX = r'^cc:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]{43}:[1-9][0-9]{0,50}$' @@ -123,9 +123,6 @@ class Condition(metaclass=ABCMeta): Args: value (Buffer): Hash as binary. """ - # TODO: value must be Buffer - # if not isinstance(value, Buffer): - # raise ValueError self._hash = value @property diff --git a/bigchaindb/crypto/ecdsa.py b/bigchaindb/crypto/ecdsa.py index 29bf6e45..864b8c86 100644 --- a/bigchaindb/crypto/ecdsa.py +++ b/bigchaindb/crypto/ecdsa.py @@ -9,10 +9,10 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec -from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey +from bigchaindb.crypto.asymmetric import SigningKey, VerifyingKey -class ECDSAPrivateKey(PrivateKey): +class EcdsaSigningKey(SigningKey): """ PrivateKey instance """ @@ -34,7 +34,6 @@ class ECDSAPrivateKey(PrivateKey): signature = signer.finalize() return binascii.hexlify(signature).decode('utf-8') - @staticmethod def encode(private_value): """ @@ -65,7 +64,7 @@ class ECDSAPrivateKey(PrivateKey): Return an instance of cryptography PrivateNumbers from the decimal private_value """ public_value_x, public_value_y = self._private_value_to_public_values(private_value) - public_numbers = ECDSAPublicKey._public_values_to_cryptography_public_numbers(public_value_x, public_value_y) + public_numbers = EcdsaVerifyingKey._public_values_to_cryptography_public_numbers(public_value_x, public_value_y) private_numbers = ec.EllipticCurvePrivateNumbers(private_value, public_numbers) return private_numbers @@ -77,11 +76,14 @@ class ECDSAPrivateKey(PrivateKey): return private_numbers.private_key(default_backend()) -class ECDSAPublicKey(PublicKey): +class EcdsaVerifyingKey(VerifyingKey): def __init__(self, key): """ Instantiate the public key with the compressed public value encoded in base58 + + Args: + key """ public_value_x, public_value_y = self.decode(key) public_numbers = self._public_values_to_cryptography_public_numbers(public_value_x, public_value_y) @@ -101,6 +103,10 @@ class ECDSAPublicKey(PublicKey): def encode(public_value_x, public_value_y): """ Encode the public key represented by the decimal values x and y to base58 + + Args: + public_value_x: + public_value_y: """ public_value_compressed_hex = bitcoin.encode_pubkey([public_value_x, public_value_y], 'hex_compressed') public_value_compressed_base58 = base58.b58encode(bytes.fromhex(public_value_compressed_hex)) @@ -110,6 +116,9 @@ class ECDSAPublicKey(PublicKey): def decode(public_value_compressed_base58): """ Decode the base58 public_value to the decimal x and y values + + Args: + public_value_compressed_base58: """ public_value_compressed_hex = binascii.hexlify(base58.b58decode(public_value_compressed_base58)) public_value_x, public_value_y = bitcoin.decode_pubkey(public_value_compressed_hex.decode()) @@ -126,6 +135,9 @@ class ECDSAPublicKey(PublicKey): def _cryptography_public_key_from_public_numbers(self, public_numbers): """ Return an instance of cryptography PublicKey from a cryptography instance of PublicNumbers + + Args: + public_numbers """ return public_numbers.public_key(default_backend()) @@ -137,11 +149,11 @@ def ecdsa_generate_key_pair(): # Private key private_key = ec.generate_private_key(ec.SECP256K1, default_backend()) private_value = private_key.private_numbers().private_value - private_value_base58 = ECDSAPrivateKey.encode(private_value) + private_value_base58 = EcdsaSigningKey.encode(private_value) # Public key public_key = private_key.public_key() public_value_x, public_value_y = public_key.public_numbers().x, public_key.public_numbers().y - public_value_compressed_base58 = ECDSAPublicKey.encode(public_value_x, public_value_y) + public_value_compressed_base58 = EcdsaVerifyingKey.encode(public_value_x, public_value_y) return (private_value_base58, public_value_compressed_base58) diff --git a/bigchaindb/crypto/ed25519.py b/bigchaindb/crypto/ed25519.py index 66cec100..0dccc214 100644 --- a/bigchaindb/crypto/ed25519.py +++ b/bigchaindb/crypto/ed25519.py @@ -5,10 +5,10 @@ import base64 import base58 import ed25519 -from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey +from bigchaindb.crypto.asymmetric import SigningKey, VerifyingKey -class ED25519PrivateKey(PrivateKey): +class Ed25519SigningKey(ed25519.SigningKey, SigningKey): """ PrivateKey instance """ @@ -16,22 +16,31 @@ class ED25519PrivateKey(PrivateKey): def __init__(self, key): """ Instantiate the private key with the private_value encoded in base58 + + Args: + key (base58): base58 encoded private key """ private_base64 = self.decode(key) - self.private_key = self._private_key_from_private_base64(private_base64) + super().__init__(private_base64, encoding='base64') def sign(self, data, encoding="base64"): """ Sign data with private key + + Args: + data (str, bytes): data to sign + encoding (str): base64, hex """ if not isinstance(data, bytes): data = data.encode('utf-8') - return self.private_key.sign(data, encoding=encoding) + return super().sign(data, encoding=encoding) @staticmethod def encode(private_base64): """ Encode the base64 number private_base64 to base58 + Args: + private_base64: """ return base58.b58encode(base64.b64decode(private_base64)) @@ -39,31 +48,27 @@ class ED25519PrivateKey(PrivateKey): def decode(key): """ Decode the base58 private_value to base64 + + Args: + key: """ return base64.b64encode(base58.b58decode(key)) - @staticmethod - def _private_key_from_private_base64(private_base64): - """ - Return an instance of a ED25519 SignigKey from a base64 key - """ - return ed25519.SigningKey(private_base64, encoding='base64') - -class ED25519PublicKey(PublicKey): +class Ed25519VerifyingKey(ed25519.VerifyingKey, VerifyingKey): def __init__(self, key): """ Instantiate the public key with the compressed public value encoded in base58 """ public_base64 = self.decode(key) - self.public_key = self._public_key_from_public_base64(public_base64) + super().__init__(public_base64, encoding='base64') def verify(self, data, signature, encoding='base64'): try: if encoding: data = data.encode('utf-8') - self.public_key.verify(signature, data, encoding=encoding) + super().verify(signature, data, encoding=encoding) except ed25519.BadSignatureError: return False @@ -73,21 +78,21 @@ class ED25519PublicKey(PublicKey): def encode(public_base64): """ Encode the public key represented by base64 to base58 + + Args: + public_base64 """ - return ED25519PrivateKey.encode(public_base64) + return Ed25519SigningKey.encode(public_base64) @staticmethod - def decode(public_value_compressed_base58): + def decode(public_base58): """ Decode the base58 public_value to base64 - """ - return ED25519PrivateKey.decode(public_value_compressed_base58) - def _public_key_from_public_base64(self, public_base64): + Args: + public_base58 """ - Return an instance of ED25519 VerifyingKey from a base64 - """ - return ed25519.VerifyingKey(public_base64, encoding='base64') + return Ed25519SigningKey.decode(public_base58) def ed25519_generate_key_pair(): diff --git a/bigchaindb/crypto/fulfillment.py b/bigchaindb/crypto/fulfillment.py index b2297a47..d43ade8c 100644 --- a/bigchaindb/crypto/fulfillment.py +++ b/bigchaindb/crypto/fulfillment.py @@ -5,7 +5,7 @@ from abc import ABCMeta, abstractmethod from six import string_types from bigchaindb.crypto.condition import Condition -from bigchaindb.crypto.iostream import Writer, base64_remove_padding, Reader, base64_add_padding, Predictor +from bigchaindb.crypto.buffer import Writer, base64_remove_padding, Reader, base64_add_padding, Predictor FULFILLMENT_REGEX = r'^cf:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]+$' diff --git a/bigchaindb/crypto/fulfillments/base_sha256.py b/bigchaindb/crypto/fulfillments/base_sha256.py index 570b0d7f..4c4c590d 100644 --- a/bigchaindb/crypto/fulfillments/base_sha256.py +++ b/bigchaindb/crypto/fulfillments/base_sha256.py @@ -1,7 +1,7 @@ from abc import abstractmethod from bigchaindb.crypto.fulfillment import Fulfillment -from bigchaindb.crypto.iostream import Hasher +from bigchaindb.crypto.buffer import Hasher class BaseSha256Fulfillment(Fulfillment): diff --git a/bigchaindb/crypto/fulfillments/ed25519_sha256.py b/bigchaindb/crypto/fulfillments/ed25519_sha256.py index c3fb4bfe..a9a0cf18 100644 --- a/bigchaindb/crypto/fulfillments/ed25519_sha256.py +++ b/bigchaindb/crypto/fulfillments/ed25519_sha256.py @@ -1,8 +1,8 @@ import base58 -from bigchaindb.crypto.ed25519 import ED25519PublicKey +from bigchaindb.crypto.ed25519 import Ed25519VerifyingKey from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment -from bigchaindb.crypto.iostream import Predictor +from bigchaindb.crypto.buffer import Predictor class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): @@ -70,10 +70,7 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): Args: value (Buffer): publicKey Public Ed25519 publicKey """ - # TODO: Buffer - # if not isinstance(value, Buffer): - # raise ValueError("public key must be a Buffer") - if not isinstance(value, ED25519PublicKey): + if not isinstance(value, Ed25519VerifyingKey): raise TypeError self._public_key = value @@ -93,9 +90,6 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): Args: value (Buffer): Binary form of dynamic message. """ - # TODO: Buffer - # if not isinstance(value, Buffer): - # raise ValueError("message must be a Buffer") if not isinstance(value, bytes): value = value.encode() self._message = value @@ -115,9 +109,6 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): Args: value (Buffer): 64-byte signature. """ - # TODO: Buffer - # if not isinstance(value, Buffer): - # raise ValueError("signature must be a Buffer") self._signature = value def write_common_header(self, writer): @@ -135,7 +126,7 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): if not self.public_key: raise ValueError - writer.write_var_bytes(bytearray(self.public_key.public_key.to_bytes())) + writer.write_var_bytes(bytearray(self.public_key.to_bytes())) writer.write_var_bytes(self.message_prefix) writer.write_var_uint(self.max_dynamic_message_length) return writer @@ -150,7 +141,7 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): reader (Reader): Source to read the fulfillment payload from. """ self.public_key = \ - ED25519PublicKey( + Ed25519VerifyingKey( base58.b58encode( reader.read_var_bytes())) self.message_prefix = reader.read_var_bytes() @@ -229,7 +220,7 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): predictor.skip(self.max_dynamic_message_length) # Signature - predictor.write_var_bytes(self.public_key.public_key.to_bytes()) + predictor.write_var_bytes(self.public_key.to_bytes()) return predictor.size @@ -243,11 +234,10 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): Args: private_key (string) Ed25519 private key """ - # TODO: Buffer sk = private_key - vk = ED25519PublicKey( + vk = Ed25519VerifyingKey( base58.b58encode( - sk.private_key.get_verifying_key().to_bytes())) + sk.get_verifying_key().to_bytes())) self.public_key = vk diff --git a/bigchaindb/crypto/fulfillments/sha256.py b/bigchaindb/crypto/fulfillments/sha256.py index a24e4087..2c433ce3 100644 --- a/bigchaindb/crypto/fulfillments/sha256.py +++ b/bigchaindb/crypto/fulfillments/sha256.py @@ -1,5 +1,5 @@ from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment -from bigchaindb.crypto.iostream import Hasher, Reader, Writer, Predictor +from bigchaindb.crypto.buffer import Hasher, Reader, Writer, Predictor class Sha256Fulfillment(BaseSha256Fulfillment): diff --git a/bigchaindb/crypto/fulfillments/threshold_sha256.py b/bigchaindb/crypto/fulfillments/threshold_sha256.py index 037d58a8..cd291841 100644 --- a/bigchaindb/crypto/fulfillments/threshold_sha256.py +++ b/bigchaindb/crypto/fulfillments/threshold_sha256.py @@ -1,7 +1,7 @@ from bigchaindb.crypto.condition import Condition from bigchaindb.crypto.fulfillment import Fulfillment from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment -from bigchaindb.crypto.iostream import Predictor, Reader, Writer +from bigchaindb.crypto.buffer import Predictor, Reader, Writer class ThresholdSha256Fulfillment(BaseSha256Fulfillment): diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 23d47bc8..90091ffe 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -5,7 +5,7 @@ from datetime import datetime import bigchaindb from bigchaindb import exceptions -from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey, hash_data +from bigchaindb.crypto.asymmetric import SigningKey, VerifyingKey, hash_data class ProcessGroup(object): @@ -157,7 +157,7 @@ def sign_tx(transaction, private_key): dict: transaction with the `signature` field included. """ - private_key = PrivateKey(private_key) + private_key = SigningKey(private_key) signature = private_key.sign(serialize(transaction)) signed_transaction = transaction.copy() signed_transaction.update({'signature': signature}) @@ -200,7 +200,7 @@ def verify_signature(signed_transaction): signature = data.pop('signature') public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = PublicKey(public_key_base58) + public_key = VerifyingKey(public_key_base58) return public_key.verify(serialize(data), signature) diff --git a/tests/crypto/test_crypto.py b/tests/crypto/test_crypto.py index 3ac24046..ad015071 100644 --- a/tests/crypto/test_crypto.py +++ b/tests/crypto/test_crypto.py @@ -1,7 +1,7 @@ import base64 -from bigchaindb.crypto.ecdsa import ECDSAPrivateKey, ECDSAPublicKey, ecdsa_generate_key_pair -from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey, ed25519_generate_key_pair +from bigchaindb.crypto.ecdsa import EcdsaSigningKey, EcdsaVerifyingKey, ecdsa_generate_key_pair +from bigchaindb.crypto.ed25519 import Ed25519SigningKey, Ed25519VerifyingKey, ed25519_generate_key_pair class TestBigchainCryptoED25519(object): @@ -18,67 +18,67 @@ class TestBigchainCryptoED25519(object): SIG_B64_ILP = 'sd0RahwuJJgeNfg8HvWHtYf4uqNgCOqIbseERacqs8G0kXNQQnhfV6gWAnMb+0RIlY3e0mqbrQiUwbRYJvRBAw==' def test_private_key_encode(self): - private_value_base58 = ED25519PrivateKey.encode(self.PRIVATE_B64) + private_value_base58 = Ed25519SigningKey.encode(self.PRIVATE_B64) assert private_value_base58 == self.PRIVATE_B58 def test_private_key_init(self): - sk = ED25519PrivateKey(self.PRIVATE_B58) - assert sk.private_key.to_ascii(encoding='base64') == self.PRIVATE_B64[:-1] - assert sk.private_key.to_bytes() == self.PRIVATE_BYTES + sk = Ed25519SigningKey(self.PRIVATE_B58) + assert sk.to_ascii(encoding='base64') == self.PRIVATE_B64[:-1] + assert sk.to_bytes() == self.PRIVATE_BYTES def test_private_key_decode(self): - private_value = ED25519PrivateKey.decode(self.PRIVATE_B58) + private_value = Ed25519SigningKey.decode(self.PRIVATE_B58) assert private_value == self.PRIVATE_B64 def test_public_key_encode(self): - public_value_base58 = ED25519PublicKey.encode(self.PUBLIC_B64) + public_value_base58 = Ed25519VerifyingKey.encode(self.PUBLIC_B64) assert public_value_base58 == self.PUBLIC_B58 def test_public_key_init(self): - vk = ED25519PublicKey(self.PUBLIC_B58) - assert vk.public_key.to_ascii(encoding='base64') == self.PUBLIC_B64[:-1] - assert vk.public_key.to_bytes() == self.PUBLIC_BYTES + vk = Ed25519VerifyingKey(self.PUBLIC_B58) + assert vk.to_ascii(encoding='base64') == self.PUBLIC_B64[:-1] + assert vk.to_bytes() == self.PUBLIC_BYTES def test_public_key_decode(self): - public_value = ED25519PublicKey.decode(self.PUBLIC_B58) + public_value = Ed25519VerifyingKey.decode(self.PUBLIC_B58) assert public_value == self.PUBLIC_B64 def test_sign_verify(self): message = 'Hello World!' - sk = ED25519PrivateKey(self.PRIVATE_B58) - vk = ED25519PublicKey(self.PUBLIC_B58) + sk = Ed25519SigningKey(self.PRIVATE_B58) + vk = Ed25519VerifyingKey(self.PUBLIC_B58) assert vk.verify(message, sk.sign(message)) is True assert vk.verify(message, sk.sign(message + 'dummy')) is False assert vk.verify(message + 'dummy', sk.sign(message)) is False - vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + vk = Ed25519VerifyingKey(Ed25519VerifyingKey.encode(self.PUBLIC_B64_ILP)) assert vk.verify(message, sk.sign(message)) is False def test_valid_condition_valid_signature_ilp(self): - vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + vk = Ed25519VerifyingKey(Ed25519VerifyingKey.encode(self.PUBLIC_B64_ILP)) msg = self.MSG_SHA512_ILP sig = self.SIG_B64_ILP assert vk.verify(base64.b64decode(msg), base64.b64decode(sig), encoding=None) is True def test_valid_condition_invalid_signature_ilp(self): - vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + vk = Ed25519VerifyingKey(Ed25519VerifyingKey.encode(self.PUBLIC_B64_ILP)) msg = self.MSG_SHA512_ILP sig = self.MSG_SHA512_ILP assert vk.verify(base64.b64decode(msg), base64.b64decode(sig), encoding=None) is False def test_generate_key_pair(self): sk, vk = ed25519_generate_key_pair() - assert ED25519PrivateKey.encode(ED25519PrivateKey.decode(sk)) == sk - assert ED25519PublicKey.encode(ED25519PublicKey.decode(vk)) == vk + assert Ed25519SigningKey.encode(Ed25519SigningKey.decode(sk)) == sk + assert Ed25519VerifyingKey.encode(Ed25519VerifyingKey.decode(vk)) == vk def test_generate_sign_verify(self): sk, vk = ed25519_generate_key_pair() - sk = ED25519PrivateKey(sk) - vk = ED25519PublicKey(vk) + sk = Ed25519SigningKey(sk) + vk = Ed25519VerifyingKey(vk) message = 'Hello World!' assert vk.verify(message, sk.sign(message)) is True assert vk.verify(message, sk.sign(message + 'dummy')) is False assert vk.verify(message + 'dummy', sk.sign(message)) is False - vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP)) + vk = Ed25519VerifyingKey(Ed25519VerifyingKey.encode(self.PUBLIC_B64_ILP)) assert vk.verify(message, sk.sign(message)) is False @@ -91,31 +91,31 @@ class TestBigchainCryptoECDSA(object): PUBLIC_VALUE_COMPRESSED_B58 = 'ifEi3UuTDT4CqUUKiS5omgeDodhu2aRFHVp6LoahbEVe' def test_private_key_encode(self): - private_value_base58 = ECDSAPrivateKey.encode(self.PRIVATE_VALUE) + private_value_base58 = EcdsaSigningKey.encode(self.PRIVATE_VALUE) assert private_value_base58 == self.PRIVATE_VALUE_B58 def test_private_key_decode(self): - private_value = ECDSAPrivateKey.decode(self.PRIVATE_VALUE_B58) + private_value = EcdsaSigningKey.decode(self.PRIVATE_VALUE_B58) assert private_value == self.PRIVATE_VALUE def test_public_key_encode(self): - public_value_compressed_base58 = ECDSAPublicKey.encode(self.PUBLIC_VALUE_X, self.PUBLIC_VALUE_Y) + public_value_compressed_base58 = EcdsaVerifyingKey.encode(self.PUBLIC_VALUE_X, self.PUBLIC_VALUE_Y) assert public_value_compressed_base58 == self.PUBLIC_VALUE_COMPRESSED_B58 def test_public_key_decode(self): - public_value_x, public_value_y = ECDSAPublicKey.decode(self.PUBLIC_VALUE_COMPRESSED_B58) + public_value_x, public_value_y = EcdsaVerifyingKey.decode(self.PUBLIC_VALUE_COMPRESSED_B58) assert public_value_x == self.PUBLIC_VALUE_X assert public_value_y == self.PUBLIC_VALUE_Y def test_sign_verify(self): message = 'Hello World!' - public_key = ECDSAPublicKey(self.PUBLIC_VALUE_COMPRESSED_B58) - private_key = ECDSAPrivateKey(self.PRIVATE_VALUE_B58) + public_key = EcdsaVerifyingKey(self.PUBLIC_VALUE_COMPRESSED_B58) + private_key = EcdsaSigningKey(self.PRIVATE_VALUE_B58) assert public_key.verify(message, private_key.sign(message)) is True def test_generate_key_pair(self): private_value_base58, public_value_compressed_base58 = ecdsa_generate_key_pair() - assert ECDSAPrivateKey.encode( - ECDSAPrivateKey.decode(private_value_base58)) == private_value_base58 - assert ECDSAPublicKey.encode( - *ECDSAPublicKey.decode(public_value_compressed_base58)) == public_value_compressed_base58 + assert EcdsaSigningKey.encode( + EcdsaSigningKey.decode(private_value_base58)) == private_value_base58 + assert EcdsaVerifyingKey.encode( + *EcdsaVerifyingKey.decode(public_value_compressed_base58)) == public_value_compressed_base58 diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index efea4925..0d59ffdc 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -5,7 +5,7 @@ from math import ceil import pytest from bigchaindb.crypto.condition import Condition -from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey +from bigchaindb.crypto.ed25519 import Ed25519SigningKey, Ed25519VerifyingKey from bigchaindb.crypto.fulfillment import Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment from bigchaindb.crypto.fulfillments.sha256 import Sha256Fulfillment @@ -56,11 +56,11 @@ class TestBigchainILPSha256Fulfillment: class TestBigchainILPEd25519Sha256Fulfillment: PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf' PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' - PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' + PUBLIC_B58_ILP = b'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' PRIVATE_HEX_ILP = b'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42' PRIVATE_B64_ILP = b'gz/mJAkje51i7HdYdSCRHpp1nOwdGXVbfakBuW3KPUI' - PRIVATE_B58_ILP = '9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs' + PRIVATE_B58_ILP = b'9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs' CONDITION_ED25519_ILP = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' FULFILLMENT_ED25519_ILP = \ @@ -69,17 +69,17 @@ class TestBigchainILPEd25519Sha256Fulfillment: HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' def test_ilp_keys(self): - sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) - assert sk.private_key.to_ascii(encoding='base64') == self.PRIVATE_B64_ILP - assert binascii.hexlify(sk.private_key.to_bytes()[:32]) == self.PRIVATE_HEX_ILP + sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) + assert sk.to_ascii(encoding='base64') == self.PRIVATE_B64_ILP + assert binascii.hexlify(sk.to_bytes()[:32]) == self.PRIVATE_HEX_ILP - vk = ED25519PublicKey(self.PUBLIC_B58_ILP) - assert vk.public_key.to_ascii(encoding='base64') == self.PUBLIC_B64_ILP - assert binascii.hexlify(vk.public_key.to_bytes()) == self.PUBLIC_HEX_ILP + vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) + assert vk.to_ascii(encoding='base64') == self.PUBLIC_B64_ILP + assert binascii.hexlify(vk.to_bytes()) == self.PUBLIC_HEX_ILP def test_serialize_condition_and_validate_fulfillment(self): - sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) - vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) + vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk @@ -107,7 +107,7 @@ class TestBigchainILPEd25519Sha256Fulfillment: assert binascii.hexlify(deserialized_condition.hash) == self.HASH_ED25519_HEX_ILP def test_serialize_deserialize_condition(self): - vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk @@ -129,12 +129,12 @@ class TestBigchainILPEd25519Sha256Fulfillment: assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP - assert fulfillment.public_key.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP + assert fulfillment.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP assert fulfillment.validate() def test_serialize_deserialize_fulfillment(self): - sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) - vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) + vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk @@ -149,7 +149,7 @@ class TestBigchainILPEd25519Sha256Fulfillment: assert isinstance(deserialized_fulfillment, Ed25519Sha256Fulfillment) assert deserialized_fulfillment.serialize_uri() == fulfillment.serialize_uri() assert deserialized_fulfillment.condition.serialize_uri() == fulfillment.condition.serialize_uri() - assert deserialized_fulfillment.public_key.public_key.to_bytes() == fulfillment.public_key.public_key.to_bytes() + assert deserialized_fulfillment.public_key.to_bytes() == fulfillment.public_key.to_bytes() assert deserialized_fulfillment.validate() @@ -178,8 +178,8 @@ class TestBigchainILPThresholdSha256Fulfillment: 'AClmekN4N2NA7t2ltNZyLGgLIqPdA' def create_fulfillment_ed25519sha256(self): - sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) - vk = ED25519PublicKey(self.PUBLIC_B58_ILP) + sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) + vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 0ed02a8b..2a7cb1da 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -9,7 +9,7 @@ import bigchaindb from bigchaindb import exceptions from bigchaindb import util from bigchaindb.block import Block -from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey, generate_key_pair, hash_data +from bigchaindb.crypto.asymmetric import SigningKey, VerifyingKey, generate_key_pair, hash_data from bigchaindb.voter import Voter @@ -188,7 +188,7 @@ class TestBigchainApi(object): assert new_block['block']['voters'] == [b.me] assert new_block['block']['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(new_block['block']), new_block['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(new_block['block']), new_block['signature']) is True assert new_block['id'] == block_hash assert new_block['votes'] == [] @@ -371,7 +371,7 @@ class TestBlockValidation(object): block_data = util.serialize(block) block_hash = hash_data(block_data) - block_signature = PrivateKey(b.me_private).sign(block_data) + block_signature = SigningKey(b.me_private).sign(block_data) block = { 'id': block_hash, @@ -443,7 +443,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_invalid_block_voting(self, b, user_public_key): # create queue and voter @@ -484,7 +484,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_valid(self, b): # create valid block @@ -498,7 +498,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_invalid(self, b): # create valid block @@ -512,7 +512,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True class TestBigchainBlock(object): diff --git a/tests/db/test_voter.py b/tests/db/test_voter.py index e4ae8efa..2eeaf7f0 100644 --- a/tests/db/test_voter.py +++ b/tests/db/test_voter.py @@ -5,7 +5,7 @@ import pytest import rethinkdb as r from bigchaindb import util -from bigchaindb.crypto.asymmetric import PublicKey, generate_key_pair +from bigchaindb.crypto.asymmetric import VerifyingKey, generate_key_pair from bigchaindb.voter import Voter, BlockStream @@ -45,7 +45,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_valid_block_voting_with_create_transaction(self, b): q_new_block = mp.Queue() @@ -87,7 +87,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_valid_block_voting_with_transfer_transactions(self, b): q_new_block = mp.Queue() @@ -158,7 +158,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_invalid_block_voting(self, b, user_public_key): # create queue and voter @@ -197,7 +197,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_valid(self, b): # create valid block @@ -211,7 +211,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_invalid(self, b): # create valid block @@ -225,7 +225,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True + assert VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_voter_considers_unvoted_blocks_when_single_node(self, b): # simulate a voter going donw in a single node environment From a852f5f66d82e4656cb4bc91be23649065f63567 Mon Sep 17 00:00:00 2001 From: diminator Date: Sat, 19 Mar 2016 18:23:29 +0100 Subject: [PATCH 10/16] - Ed25519: get_verifying_key and to_ascii - documentation --- bigchaindb/crypto/asymmetric.py | 4 + bigchaindb/crypto/buffer.py | 2 +- bigchaindb/crypto/crypto.md | 408 +++++++++++++++++++++++++++++++ bigchaindb/crypto/ed25519.py | 16 ++ tests/crypto/test_crypto.py | 14 ++ tests/crypto/test_fulfillment.py | 11 + 6 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 bigchaindb/crypto/crypto.md diff --git a/bigchaindb/crypto/asymmetric.py b/bigchaindb/crypto/asymmetric.py index 05d3dcfd..f0522ed5 100644 --- a/bigchaindb/crypto/asymmetric.py +++ b/bigchaindb/crypto/asymmetric.py @@ -19,6 +19,10 @@ class SigningKey(metaclass=ABCMeta): """ raise NotImplementedError + @abstractmethod + def get_verifying_key(self): + raise NotImplementedError + @staticmethod @abstractmethod def encode(private_value): diff --git a/bigchaindb/crypto/buffer.py b/bigchaindb/crypto/buffer.py index 5bcfc98b..99a45b02 100644 --- a/bigchaindb/crypto/buffer.py +++ b/bigchaindb/crypto/buffer.py @@ -432,4 +432,4 @@ def base64_remove_padding(data): """ if isinstance(data, string_types): data = data.encode('utf-8') - return data.replace(b'=', b'') + return data.rstrip(b'=') diff --git a/bigchaindb/crypto/crypto.md b/bigchaindb/crypto/crypto.md new file mode 100644 index 00000000..300776ae --- /dev/null +++ b/bigchaindb/crypto/crypto.md @@ -0,0 +1,408 @@ +# Crypto Conditions + +This spec is from the [**Interledger Protocol (ILP)**] +(https://github.com/interledger/five-bells-condition/tree/feature/binary-merkle-v2) + +## Motivation + +We would like a way to describe a signed message such that multiple actors in a distributed system can all verify the same signed message and agree on whether it matches the description. + +This provides a useful primitive for distributed, event-based systems since we can describe events (represented by signed messages) and therefore define generic authenticated event handlers. + +## Terminology + +* ##### Condition + A condition is the hash of a description of a signed message. + +* ##### Fulfillment + A fulfillment consists of a description of a signed message and a signed message that matches the description. + + The description can be hashed and compared to a condition. If the message matches the description and the hash of the description matches the condition, we say that the fulfillment **fulfills** the condition. + +* ##### Hashlock + A tuple consisting of a bytestring and its hash where the hash is published first and the publication of the corresponding bytestring acts as a one-bit, one-time signature. + +# Basic Format + +## Bitmask + +Any system accepting crypto-conditions must be able to state its supported +algorithms. It must be possible to verify that all algorithms used in a certain +condition are indeed supported even if the fulfillment is not available yet. + +In order to meet these design goals, we define a bitmask to express the supported primitives. + +The following bits are assigned: + +|Type Bit |Exp. |Int.|Condition Type | +|--------:|------------:|---:|-----------------| +| 1|20| 1|SHA-256 | +| 10|21| 2|RSA-SHA-256 | +| 100|22| 4|THRESHOLD-SHA-256| +| 1000|23| 8|ED25519-SHA-256 | + +Conditions contain a bitmask of types they require the implementation to support. Implementations provide a bitmask of types they support. + +### ILP Features + +Crypto-conditions are a simple multi-algorithm, multi-message, multi-level, multi-signature standard. + +* **Multi-algorithm** + + Crypto-conditions can support several different signature and hash algorithms and support for new ones can be added in the future. + + Implementations can state their supported algorithms simply by providing a bitmask. It is easy to verify that a given implementation will be able to verify the fulfillment to a given condition, by verifying that the condition's bitmask `condition` and its own bitmask of supported algorithms `supported` satisfies `condition & ~supported == 0` where `&` is the bitwise AND operator and `~` is the bitwise NOT operator. + + Any new high bit can redefine the meaning of any existing lower bits as long as it is set. This can be used to remove obsolete algorithms. + + The bitmask is encoded as a varint to minimize space usage. + +* **Multi-signature** + + Crypto-conditions can abstract away many of the details of multi-sign. When a party provides a condition, other parties can treat it opaquely and do not need to know about its internal structure. That allows parties to define arbitrary multi-signature setups without breaking compatibility. + + Protocol designers can use crypto-conditions as a drop-in replacement for public key signature algorithms and add multi-signature support to their protocols without adding any additional complexity. + +* **Multi-level** + + Basic multi-sign is single-level and does not support more complex trust relationships such as "I trust Alice and Bob, but only when Candice also agrees". In single level 2-of-3 Alice and Bob could sign on their own, without Candice's approval. + + Crypto-conditions add that flexibility elegantly, by applying thresholds not just to signatures, but to conditions which can be signatures or further conditions. That allows the creation of an arbitrary threshold boolean circuit of signatures. + +* **Multi-message** + + Crypto-conditions can sign not just one, but multiple messages at the same time and by different people. These messages can then be used as inputs for other algorithms. + + This allows resource-controlling systems to perform their functions without knowing the details of the higher-level protocols that these functions are a part of. + +## Usage + +```python +import binascii +from bigchaindb.crypto.condition import Condition +from bigchaindb.crypto.fulfillment import Fulfillment +from bigchaindb.crypto.fulfillments.sha256 import Sha256Fulfillment + +# Parse a condition from a URI +example_condition_uri = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' +parsed_condition = Condition.from_uri(example_condition_uri) +print(isinstance(parsed_condition, Condition)) +# prints True + +print(binascii.hexlify(parsed_condition.hash)) +# prints b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + +# Compile a condition +parsed_condition_uri = parsed_condition.serialize_uri() +print(parsed_condition_uri) +# prints 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' +print(parsed_condition_uri == example_condition_uri) +# prints True + +# Parse a fulfillment +example_fulfillment_uri = 'cf:1:1:AA' +parsed_fulfillment = Fulfillment.from_uri(example_fulfillment_uri) +print(isinstance(parsed_fulfillment, Sha256Fulfillment)) +# prints True +# Note: Merely parsing a fulfillment DOES NOT validate it. + +# Validate a fulfillment +parsed_fulfillment.validate() +# prints True +``` + +## ILP Encoding + +### Binary types + +* **VARUINT** + + Unsigned variable-length integer. Implementation matches [Base128 Varints](https://developers.google.com/protocol-buffers/docs/encoding#varints) in Protocol Buffers. Implementations MAY define different maximum lengths for their varints, as long as that length is long enough to cover their bitmask and their maximum supported fulfillment length. (This is safe, because no larger varuint can appear in a valid crypto-condition.) + +* **VARBYTES** + + Consists of a `VARUINT` length field followed by that many bytes. + +* **VARARRAY** + + Consists of a `VARUINT` length fields followed by that many bytes filled with elements of the array. + +### String types + +* **BASE10** + + Variable-length integer encoded as a base-10 (decimal) number. Implementations MUST reject encodings that are too large for them to parse. Implementations MUST be tested for overflows. + +* **BASE16** + + Variable-length integer encoded as a base-16 (hexadecimal) number. Implementations MUST reject encodings that are too large for them to parse. Implementations MUST be tested for overflows. No leading zeros. + +* **BASE64URL** + + Base64-URL encoding. See [RFC4648 Section 5](https://tools.ietf.org/html/rfc4648#section-5). + +### Condition + +Conditions are ASCII encoded as: + +``` +"cc:" BASE10(VERSION) ":" BASE16(TYPE_BITMASK) ":" BASE64URL(HASH) ":" BASE10(MAX_FULFILLMENT_LENGTH) +``` + +Conditions are binary encoded as: + +``` +CONDITION = + VARUINT TYPE_BITMASK + VARBYTES HASH + VARUINT MAX_FULFILLMENT_LENGTH +``` + +The `TYPE_BITMASK` is the boolean OR of the `TYPE_BIT`s of the condition type and all subcondition types, recursively. + +### Fulfillment + +Fulfillments are ASCII encoded as: + +``` +"cf:" BASE10(VERSION) ":" BASE16(TYPE_BIT) ":" BASE64URL(FULFILLMENT_PAYLOAD) +``` + +Fulfillments are binary encoded as: + +``` +FULFILLMENT = + VARUINT TYPE_BIT + FULFILLMENT_PAYLOAD +``` + +The `TYPE_BIT` is the single bit representing the top level condition type. + +# Condition Types + +## SHA-256 + +SHA-256 is assigned the type bit 20 = 0x01. + +### Notes + +This type of condition is also called a hashlock. We can use revealing the preimage as a type of one bit signature. + +Bitcoin supports this type of condition via the `OP_HASH256` operator + +### Condition + +``` +HASH = SHA256(PREIMAGE) +``` + +### Fulfillment + +``` +FULFILLMENT_PAYLOAD = + VARBYTES PREIMAGE +``` + +### Usage + +```python +import binascii, hashlib +from bigchaindb.crypto.condition import Condition +from bigchaindb.crypto.fulfillments.sha256 import Sha256Fulfillment + +secret = '' +puzzle = binascii.hexlify(hashlib.sha256(secret.encode()).digest()) +print(puzzle) +# prints b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + +# Create a SHA256 condition +sha256condition = Condition() +sha256condition.bitmask = 0x01 +sha256condition.hash = binascii.unhexlify(puzzle) +sha256condition.max_fulfillment_length = 1 +sha256condition_uri = sha256condition.serialize_uri() +print(sha256condition_uri) +# prints 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' + +# Create a fulfillment +sha256fulfillment = Sha256Fulfillment() + +# Create a condition from fulfillment +sha256fulfillment.condition +# raises ValueError: Could not calculate hash, no preimage provided +sha256fulfillment.preimage = secret +print(sha256fulfillment.condition.serialize_uri() == sha256condition_uri) +# prints True + +# Compile a fulfillment +print(sha256fulfillment.serialize_uri()) +# prints 'cf:1:1:AA' + +# Even better: verify that the fulfillment matches the condition +print(sha256fulfillment.validate() and \ + sha256fulfillment.condition.serialize_uri() == sha256condition.serialize_uri()) +# prints True +``` + +## RSA-SHA-256 + +RSA-SHA-256 is assigned the type bit 21 = 0x02. + +**Warning:** not (yet) implemented in BigchainDB, for info see the [**ILP specification**](https://github.com/interledger/five-bells-condition/blob/feature/binary-merkle-v2/docs/spec.md) + + +## ED25519-SHA-256 + +ED25519-SHA-256 is assigned the type bit 23 = 0x08. + +### Condition + +``` +HASH = SHA256( + VARBYTES PUBLIC_KEY + VARBYTES MESSAGE_ID + VARBYTES FIXED_PREFIX + VARUINT DYNAMIC_MESSAGE_LENGTH +) +``` + +### Fulfillment + +``` +FULFILLMENT_PAYLOAD = + VARBYTES PUBLIC_KEY + VARBYTES MESSAGE_ID + VARBYTES FIXED_PREFIX + VARUINT DYNAMIC_MESSAGE_LENGTH + VARBYTES DYNAMIC_MESSAGE + VARBYTES SIGNATURE +``` + +The `DYNAMIC_MESSAGE_LENGTH` is included to provide a maximum length for `DYNAMIC_MESSAGE` even if the actual message suffix length is different. This value is used to calculate the `MAX_FULFILLMENT_LENGTH` in the condition. + +The `MESSAGE_ID` represents an identifier for the message. All messages in a cryptocondition that have a common identifier must match, otherwise the condition is invalid. Implementations may return messages as a map of `MESSAGE_ID` => `MESSAGE` pairs. + +The message to be signed is the concatenation of the `FIXED_PREFIX` and `DYNAMIC_MESSAGE`. + +The `MESSAGE_ID`, `FIXED_PREFIX`, `DYNAMIC_MESSAGE_LENGTH` and `DYNAMIC_MESSAGE` fields have the same meaning as in the [**RSA-SHA-256 condition type**](https://github.com/interledger/five-bells-condition/blob/feature/binary-merkle-v2/docs/spec.md). + +### Usage + +```python +from bigchaindb.crypto.ed25519 import Ed25519SigningKey, Ed25519VerifyingKey +from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment + +# We use base58 key encoding +sk = Ed25519SigningKey(b'9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs') +vk = sk.get_verifying_key() + +# Create an ED25519-SHA256 condition +ed25519_fulfillment = Ed25519Sha256Fulfillment() +ed25519_fulfillment.public_key = vk +ed25519_fulfillment.message_prefix = 'Hello world!' +ed25519_fulfillment.max_dynamic_message_length = 32 # defaults to 0 +ed25519_condition_uri = ed25519_fulfillment.condition.serialize_uri() +print (ed25519_condition_uri) +# prints 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' + +# ED25519-SHA256 condition not fulfilled +print(ed25519_fulfillment.validate()) +# prints False + +# Fulfill an ED25519-SHA256 condition +ed25519_fulfillment.message = ' Conditions are here!' +ed25519_fulfillment.sign(sk) +print(ed25519_fulfillment.validate()) +# prints True + +print(ed25519_fulfillment.serialize_uri()) +# prints 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' +print (ed25519_fulfillment.condition.serialize_uri()) + +# Parse a fulfillment URI +parsed_ed25519_fulfillment = Ed25519Sha256Fulfillment.from_uri('cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI') + +print(parsed_ed25519_fulfillment.validate()) +# prints True +print(parsed_ed25519_fulfillment.condition.serialize_uri()) +# prints 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' +``` + +### Implementation + +The exact algorithm and encodings used for `PUBLIC_KEY` and `SIGNATURE` are Ed25519 as defined in [draft-irtf-cfrg-eddsa-04](https://datatracker.ietf.org/doc/draft-irtf-cfrg-eddsa/). + +## THRESHOLD-SHA-256 + +THRESHOLD-SHA-256 is assigned the type bit 22 = 0x04. + +### Condition + +``` +HASH = SHA256( + VARUINT TYPE_BIT + VARUINT THRESHOLD + VARARRAY + VARUINT WEIGHT + CONDITION +) +``` + +The `TYPE_BIT` is `0x04`. The reason we need this is because threshold conditions are a structural condition. Structural conditions can have subconditions, meaning their TYPE_BITMASK can have multiple bits set, including other structural conditions. This `TYPE_BIT` prevents the possibility that two different structural fulfillments could ever generate the exact same condition. + +The `VARARRAY` of conditions is sorted first based on length, shortest first. Elements of the same length are sorted in lexicographic (big-endian) order, smallest first. + +### Fulfillment + +``` +FULFILLMENT_PAYLOAD = + VARUINT THRESHOLD + VARARRAY + VARUINT WEIGHT + FULFILLMENT + VARARRAY + VARUINT WEIGHT + CONDITION +``` + +### Usage + +```python +from bigchaindb.crypto.fulfillments.sha256 import Sha256Fulfillment +from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment +from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment + +# Parse some fulfillments +sha256_fulfillment = Sha256Fulfillment.from_uri('cf:1:1:AA') +ed25519_fulfillment = Ed25519Sha256Fulfillment.from_uri('cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI') + +# Create a threshold condition +theshold_fulfillment = ThresholdSha256Fulfillment() +theshold_fulfillment.add_subfulfillment(sha256_fulfillment) +theshold_fulfillment.add_subfulfillment(ed25519_fulfillment) +theshold_fulfillment.threshold = 1 +print(theshold_fulfillment.condition.serialize_uri()) +# prints 'cc:1:d:9DdkQtOl2m9yjqZzCg6ck5b2zM3tAPLlJMaHsKkszIA:114' + +# Compile a threshold fulfillment +theshold_fulfillment_uri = theshold_fulfillment.serialize_uri() +# Note: If there are more than enough fulfilled subconditions, shorter +# fulfillments will be chosen over longer ones. +print(theshold_fulfillment_uri) +# prints 'cf:1:4:AQEBAQABAQggqQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPVx' + +# Validate fulfillment +print(theshold_fulfillment.validate()) +# prints True + +# Parse the fulfillment +reparsed_fulfillment = ThresholdSha256Fulfillment.from_uri(theshold_fulfillment_uri) +print(reparsed_fulfillment.validate()) +# prints True + +# Increase threshold +theshold_fulfillment.threshold = 3 +print(theshold_fulfillment.validate()) +# prints False + +``` \ No newline at end of file diff --git a/bigchaindb/crypto/ed25519.py b/bigchaindb/crypto/ed25519.py index 0dccc214..5ba5328b 100644 --- a/bigchaindb/crypto/ed25519.py +++ b/bigchaindb/crypto/ed25519.py @@ -23,6 +23,16 @@ class Ed25519SigningKey(ed25519.SigningKey, SigningKey): private_base64 = self.decode(key) super().__init__(private_base64, encoding='base64') + def get_verifying_key(self): + vk = super().get_verifying_key() + return Ed25519VerifyingKey(base58.b58encode(vk.to_bytes())) + + def to_ascii(self, prefix="", encoding='base58'): + if encoding == 'base58': + return base58.b58encode(self.to_seed()).encode('ascii').decode('ascii').rstrip("=") + else: + return super().to_ascii(prefix=prefix, encoding=encoding) + def sign(self, data, encoding="base64"): """ Sign data with private key @@ -74,6 +84,12 @@ class Ed25519VerifyingKey(ed25519.VerifyingKey, VerifyingKey): return True + def to_ascii(self, prefix="", encoding='base58'): + if encoding == 'base58': + return base58.b58encode(self.vk_s).encode('ascii').decode('ascii').rstrip("=") + else: + return super().to_ascii(prefix=prefix, encoding=encoding) + @staticmethod def encode(public_base64): """ diff --git a/tests/crypto/test_crypto.py b/tests/crypto/test_crypto.py index ad015071..94c96207 100644 --- a/tests/crypto/test_crypto.py +++ b/tests/crypto/test_crypto.py @@ -53,6 +53,20 @@ class TestBigchainCryptoED25519(object): vk = Ed25519VerifyingKey(Ed25519VerifyingKey.encode(self.PUBLIC_B64_ILP)) assert vk.verify(message, sk.sign(message)) is False + def test_to_ascii(self): + sk = Ed25519SigningKey(self.PRIVATE_B58) + assert sk.to_ascii(encoding='base58') == self.PRIVATE_B58 + assert sk.to_ascii(encoding='base64') == self.PRIVATE_B64.rstrip(b'=') + vk = Ed25519VerifyingKey(self.PUBLIC_B58) + assert vk.to_ascii(encoding='base58') == self.PUBLIC_B58 + assert vk.to_ascii(encoding='base64') == self.PUBLIC_B64.rstrip(b'=') + + def test_get_verifying_key(self): + sk = Ed25519SigningKey(self.PRIVATE_B58) + vk = Ed25519VerifyingKey(self.PUBLIC_B58) + vk_from_sk = sk.get_verifying_key() + assert vk.to_bytes() == vk_from_sk.to_bytes() + def test_valid_condition_valid_signature_ilp(self): vk = Ed25519VerifyingKey(Ed25519VerifyingKey.encode(self.PUBLIC_B64_ILP)) msg = self.MSG_SHA512_ILP diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index 0d59ffdc..4e40ab88 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -14,12 +14,21 @@ from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfi class TestBigchainILPSha256Condition: CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' + CONDITION_SHA256_HASH = b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' def test_deserialize_condition(self): example_condition = self.CONDITION_SHA256_ILP condition = Condition.from_uri(example_condition) + print(binascii.hexlify(condition.hash)) assert condition.serialize_uri() == self.CONDITION_SHA256_ILP + def test_create_condition(self): + sha256condition = Condition() + sha256condition.bitmask = Sha256Fulfillment._bitmask + sha256condition.hash = binascii.unhexlify(self.CONDITION_SHA256_HASH) + sha256condition.max_fulfillment_length = 1 + assert sha256condition.serialize_uri() == self.CONDITION_SHA256_ILP + class TestBigchainILPSha256Fulfillment: CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' @@ -38,6 +47,8 @@ class TestBigchainILPSha256Fulfillment: assert fulfillment.serialize_uri() == self.FULFILLMENT_SHA256_ILP assert fulfillment.condition.serialize_uri() == condition.serialize_uri() assert fulfillment.validate() + assert fulfillment.validate() \ + and fulfillment.condition.serialize_uri() == condition.serialize_uri() def test_condition_from_fulfillment(self): fulfillment = Sha256Fulfillment() From c47d6b258c215b5bbac36580704175a88a060439 Mon Sep 17 00:00:00 2001 From: diminator Date: Sat, 19 Mar 2016 18:29:20 +0100 Subject: [PATCH 11/16] fixed tests by providing abstract methods to ecdsa.py --- bigchaindb/crypto/asymmetric.py | 8 ++++++++ bigchaindb/crypto/ecdsa.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/bigchaindb/crypto/asymmetric.py b/bigchaindb/crypto/asymmetric.py index f0522ed5..15d5c5f3 100644 --- a/bigchaindb/crypto/asymmetric.py +++ b/bigchaindb/crypto/asymmetric.py @@ -23,6 +23,10 @@ class SigningKey(metaclass=ABCMeta): def get_verifying_key(self): raise NotImplementedError + @abstractmethod + def to_ascii(self, prefix, encoding): + raise NotImplementedError + @staticmethod @abstractmethod def encode(private_value): @@ -52,6 +56,10 @@ class VerifyingKey(metaclass=ABCMeta): def verify(self, data, signature): raise NotImplementedError + @abstractmethod + def to_ascii(self, prefix, encoding): + raise NotImplementedError + @staticmethod @abstractmethod def encode(public_value): diff --git a/bigchaindb/crypto/ecdsa.py b/bigchaindb/crypto/ecdsa.py index 864b8c86..3139a3ec 100644 --- a/bigchaindb/crypto/ecdsa.py +++ b/bigchaindb/crypto/ecdsa.py @@ -34,6 +34,12 @@ class EcdsaSigningKey(SigningKey): signature = signer.finalize() return binascii.hexlify(signature).decode('utf-8') + def get_verifying_key(self): + raise NotImplementedError + + def to_ascii(self, prefix='', encoding='base58'): + raise NotImplementedError + @staticmethod def encode(private_value): """ @@ -99,6 +105,9 @@ class EcdsaVerifyingKey(VerifyingKey): return True + def to_ascii(self, prefix='', encoding='base58'): + raise NotImplementedError + @staticmethod def encode(public_value_x, public_value_y): """ From 892ab74accc41f5cea320f7b6abb100e4d8797be Mon Sep 17 00:00:00 2001 From: diminator Date: Mon, 21 Mar 2016 09:55:37 +0100 Subject: [PATCH 12/16] fix: resolved merge --- bigchaindb/consensus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index b65ab9ad..a1d37be4 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -2,7 +2,7 @@ from abc import ABCMeta, abstractmethod import bigchaindb.exceptions as exceptions from bigchaindb import util -from bigchaindb.crypto import hash_data, PublicKey +from bigchaindb.crypto.asymmetric import hash_data class AbstractConsensusRules(metaclass=ABCMeta): From bb88279b64461e9bf373f2d6691dce8dee9af172 Mon Sep 17 00:00:00 2001 From: diminator Date: Mon, 21 Mar 2016 10:49:39 +0100 Subject: [PATCH 13/16] fix: use _bitmask for seriallizing thresholdconditions --- bigchaindb/crypto/bitmark_registry.py | 4 ++-- bigchaindb/crypto/buffer.py | 8 ++++--- bigchaindb/crypto/crypto.md | 8 +++---- bigchaindb/crypto/fulfillment.py | 5 ++-- tests/crypto/test_fulfillment.py | 34 +++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/bigchaindb/crypto/bitmark_registry.py b/bigchaindb/crypto/bitmark_registry.py index 2f75b158..7ae345f7 100644 --- a/bigchaindb/crypto/bitmark_registry.py +++ b/bigchaindb/crypto/bitmark_registry.py @@ -1,4 +1,4 @@ -from bigchaindb.crypto.buffer import MAX_SAFE_INTEGER +from bigchaindb.crypto.buffer import MAX_SAFE_INTEGER_JS class BitmaskRegistry: @@ -18,7 +18,7 @@ class BitmaskRegistry: Class implementing the given fulfillment type. """ # Determine type of condition - if bitmask > MAX_SAFE_INTEGER: + if bitmask > MAX_SAFE_INTEGER_JS: raise ValueError('Bitmask {} is not supported'.format(bitmask)) for registered_type in BitmaskRegistry.registered_types: diff --git a/bigchaindb/crypto/buffer.py b/bigchaindb/crypto/buffer.py index 99a45b02..f3d641a6 100644 --- a/bigchaindb/crypto/buffer.py +++ b/bigchaindb/crypto/buffer.py @@ -12,7 +12,9 @@ MSBALL = ~REST INT = 2 ** 31 # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER -MAX_SAFE_INTEGER = 2 ** 53 - 1 +# we don't use sys.maxint (= 2 ** 63 - 1) as this spec is inline with the ILP JavaScript reference implementation +# see https://interledger.org/ +MAX_SAFE_INTEGER_JS = 2 ** 53 - 1 class UnsignedLEB128: @@ -24,7 +26,7 @@ class UnsignedLEB128: seven least significant bits of the number represented. see: https://en.wikipedia.org/wiki/LEB128 (ULEB128) - see http://grokbase.com/t/python/python-list/112e5jpc16/encoding + see: http://grokbase.com/t/python/python-list/112e5jpc16/encoding """ @@ -166,7 +168,7 @@ class Predictor: self.size += 1 elif val < 0: raise ValueError('Variable length integer cannot be negative') - elif val > MAX_SAFE_INTEGER: + elif val > MAX_SAFE_INTEGER_JS: raise ValueError('Variable length integer too large') else: # Calculate number of bits divided by seven diff --git a/bigchaindb/crypto/crypto.md b/bigchaindb/crypto/crypto.md index 300776ae..a2771234 100644 --- a/bigchaindb/crypto/crypto.md +++ b/bigchaindb/crypto/crypto.md @@ -1,7 +1,7 @@ # Crypto Conditions This spec is from the [**Interledger Protocol (ILP)**] -(https://github.com/interledger/five-bells-condition/tree/feature/binary-merkle-v2) +(https://interledger.org/five-bells-condition/spec.html) ## Motivation @@ -248,7 +248,7 @@ print(sha256fulfillment.validate() and \ RSA-SHA-256 is assigned the type bit 21 = 0x02. -**Warning:** not (yet) implemented in BigchainDB, for info see the [**ILP specification**](https://github.com/interledger/five-bells-condition/blob/feature/binary-merkle-v2/docs/spec.md) +**Warning:** not (yet) implemented in BigchainDB, for info see the [**ILP specification**](https://interledger.org/five-bells-condition/spec.html) ## ED25519-SHA-256 @@ -284,7 +284,7 @@ The `MESSAGE_ID` represents an identifier for the message. All messages in a cry The message to be signed is the concatenation of the `FIXED_PREFIX` and `DYNAMIC_MESSAGE`. -The `MESSAGE_ID`, `FIXED_PREFIX`, `DYNAMIC_MESSAGE_LENGTH` and `DYNAMIC_MESSAGE` fields have the same meaning as in the [**RSA-SHA-256 condition type**](https://github.com/interledger/five-bells-condition/blob/feature/binary-merkle-v2/docs/spec.md). +The `MESSAGE_ID`, `FIXED_PREFIX`, `DYNAMIC_MESSAGE_LENGTH` and `DYNAMIC_MESSAGE` fields have the same meaning as in the [**RSA-SHA-256 condition type**](https://interledger.org/five-bells-condition/spec.html). ### Usage @@ -297,7 +297,7 @@ sk = Ed25519SigningKey(b'9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs') vk = sk.get_verifying_key() # Create an ED25519-SHA256 condition -ed25519_fulfillment = Ed25519Sha256Fulfillment() +ed25519_fulfill`nt = Ed25519Sha256Fulfillment() ed25519_fulfillment.public_key = vk ed25519_fulfillment.message_prefix = 'Hello world!' ed25519_fulfillment.max_dynamic_message_length = 32 # defaults to 0 diff --git a/bigchaindb/crypto/fulfillment.py b/bigchaindb/crypto/fulfillment.py index d43ade8c..5a00b33c 100644 --- a/bigchaindb/crypto/fulfillment.py +++ b/bigchaindb/crypto/fulfillment.py @@ -71,7 +71,8 @@ class Fulfillment(metaclass=ABCMeta): from bigchaindb.crypto.bitmark_registry import BitmaskRegistry - cls = BitmaskRegistry.get_class_from_typebit(reader.read_var_uint()) + cls_type = reader.read_var_uint() + cls = BitmaskRegistry.get_class_from_typebit(cls_type) fulfillment = cls() fulfillment.parse_payload(reader) @@ -151,7 +152,7 @@ class Fulfillment(metaclass=ABCMeta): Serialized fulfillment """ writer = Writer() - writer.write_var_uint(self.bitmask) + writer.write_var_uint(self._bitmask) self.write_payload(writer) return b''.join(writer.components) diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index 4e40ab88..383ca28b 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -292,3 +292,37 @@ class TestBigchainILPThresholdSha256Fulfillment: assert len(deserialized_fulfillment.get_all_subconditions()) == threshold assert deserialized_fulfillment.serialize_uri() == fulfillment_uri assert deserialized_fulfillment.validate() + + def test_fulfillment_nested_and_or(self): + ilp_fulfillment_sha = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) + ilp_fulfillment_ed1 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) + ilp_fulfillment_ed2 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + + # 2-of-2 (AND with 2 inputs) + fulfillment = ThresholdSha256Fulfillment() + fulfillment.threshold = 2 + fulfillment.add_subfulfillment(ilp_fulfillment_sha) + + assert fulfillment.validate() is False + + # 1-of-2 (OR with 2 inputs) + nested_fulfillment = ThresholdSha256Fulfillment() + nested_fulfillment.threshold = 1 + nested_fulfillment.add_subfulfillment(ilp_fulfillment_ed1) + assert nested_fulfillment.validate() is True + nested_fulfillment.add_subfulfillment(ilp_fulfillment_ed2) + assert nested_fulfillment.validate() is True + + fulfillment.add_subfulfillment(nested_fulfillment) + assert fulfillment.validate() is True + + fulfillment_uri = fulfillment.serialize_uri() + deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri) + + assert isinstance(deserialized_fulfillment, ThresholdSha256Fulfillment) + assert deserialized_fulfillment.threshold == 2 + assert len(deserialized_fulfillment.subfulfillments) == 2 + assert len(deserialized_fulfillment.subfulfillments[1].subfulfillments) == 1 + assert len(deserialized_fulfillment.get_all_subconditions()) == 2 + assert deserialized_fulfillment.serialize_uri() == fulfillment_uri + assert deserialized_fulfillment.validate() \ No newline at end of file From 9c0e9f62315cc22f2b50d09b510665e1e4f3d165 Mon Sep 17 00:00:00 2001 From: diminator Date: Mon, 21 Mar 2016 11:12:40 +0100 Subject: [PATCH 14/16] updated docs for nested conditions --- bigchaindb/crypto/crypto.md | 47 +++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/bigchaindb/crypto/crypto.md b/bigchaindb/crypto/crypto.md index a2771234..e478773d 100644 --- a/bigchaindb/crypto/crypto.md +++ b/bigchaindb/crypto/crypto.md @@ -316,7 +316,10 @@ print(ed25519_fulfillment.validate()) # prints True print(ed25519_fulfillment.serialize_uri()) -# prints 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' +# prints +# 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkI +# SAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66u +# euTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' print (ed25519_fulfillment.condition.serialize_uri()) # Parse a fulfillment URI @@ -377,32 +380,50 @@ sha256_fulfillment = Sha256Fulfillment.from_uri('cf:1:1:AA') ed25519_fulfillment = Ed25519Sha256Fulfillment.from_uri('cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI') # Create a threshold condition -theshold_fulfillment = ThresholdSha256Fulfillment() -theshold_fulfillment.add_subfulfillment(sha256_fulfillment) -theshold_fulfillment.add_subfulfillment(ed25519_fulfillment) -theshold_fulfillment.threshold = 1 -print(theshold_fulfillment.condition.serialize_uri()) +threshold_fulfillment = ThresholdSha256Fulfillment() +threshold_fulfillment.add_subfulfillment(sha256_fulfillment) +threshold_fulfillment.add_subfulfillment(ed25519_fulfillment) +threshold_fulfillment.threshold = 1 # OR gate +print(threshold_fulfillment.condition.serialize_uri()) # prints 'cc:1:d:9DdkQtOl2m9yjqZzCg6ck5b2zM3tAPLlJMaHsKkszIA:114' # Compile a threshold fulfillment -theshold_fulfillment_uri = theshold_fulfillment.serialize_uri() +threshold_fulfillment_uri = threshold_fulfillment.serialize_uri() # Note: If there are more than enough fulfilled subconditions, shorter # fulfillments will be chosen over longer ones. -print(theshold_fulfillment_uri) +print(threshold_fulfillment_uri) # prints 'cf:1:4:AQEBAQABAQggqQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPVx' # Validate fulfillment -print(theshold_fulfillment.validate()) +print(threshold_fulfillment.validate()) # prints True # Parse the fulfillment -reparsed_fulfillment = ThresholdSha256Fulfillment.from_uri(theshold_fulfillment_uri) +reparsed_fulfillment = \ + ThresholdSha256Fulfillment.from_uri(threshold_fulfillment_uri) print(reparsed_fulfillment.validate()) # prints True -# Increase threshold -theshold_fulfillment.threshold = 3 -print(theshold_fulfillment.validate()) +# Increase threshold to a 3-port AND gate +threshold_fulfillment.threshold = 3 +print(threshold_fulfillment.validate()) # prints False +# Create a nested threshold condition +# VALID = SHA and DSA and (DSA or DSA) +nested_fulfillment = ThresholdSha256Fulfillment() +nested_fulfillment.add_subfulfillment(ed25519_fulfillment) +nested_fulfillment.add_subfulfillment(ed25519_fulfillment) +nested_fulfillment.threshold = 1 # OR gate + +threshold_fulfillment.add_subfulfillment(nested_fulfillment) +print(threshold_fulfillment.serialize_uri()) +# prints +# 'cf:1:4:AwMBAQABCCDsFyuTrV5WO_STLHDhJFA0w1Rn7y79TWTr-BloNGfivwxIZWxsby +# B3b3JsZCEgFSBDb25kaXRpb25zIGFyZSBoZXJlIUBDW6ped9T2wiZUVLyoz-epNFyiTDq +# yBqNheurnrk7UZ2KyQdrdmbbXX1zOIMw__O3h9Z2U6buK05AMfNYUnacCAQQBAQEIIOwX +# K5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpd +# GlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t +# 2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwIBAQggqQINW2um59C4DB9JSVXH1igqAma +# YGGqryllHUgCpfPVxAA' ``` \ No newline at end of file From f9e5b3459152c594acca12fbd13fa9b677eaf8c2 Mon Sep 17 00:00:00 2001 From: diminator Date: Mon, 21 Mar 2016 11:38:14 +0100 Subject: [PATCH 15/16] added nested tests --- tests/crypto/test_fulfillment.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index 383ca28b..1452aa59 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -19,7 +19,6 @@ class TestBigchainILPSha256Condition: def test_deserialize_condition(self): example_condition = self.CONDITION_SHA256_ILP condition = Condition.from_uri(example_condition) - print(binascii.hexlify(condition.hash)) assert condition.serialize_uri() == self.CONDITION_SHA256_ILP def test_create_condition(self): @@ -325,4 +324,33 @@ class TestBigchainILPThresholdSha256Fulfillment: assert len(deserialized_fulfillment.subfulfillments[1].subfulfillments) == 1 assert len(deserialized_fulfillment.get_all_subconditions()) == 2 assert deserialized_fulfillment.serialize_uri() == fulfillment_uri - assert deserialized_fulfillment.validate() \ No newline at end of file + assert deserialized_fulfillment.validate() + + def test_fulfillment_nested(self): + ilp_fulfillment_sha = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) + ilp_fulfillment_ed1 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) + + # 2-of-2 (AND with 2 inputs) + fulfillment = ThresholdSha256Fulfillment() + fulfillment.threshold = 2 + fulfillment.add_subfulfillment(ilp_fulfillment_sha) + + max_depth = 10 + + def add_nested_fulfillment(parent, current_depth=0): + current_depth += 1 + child = ThresholdSha256Fulfillment() + child.threshold = 1 + if current_depth < max_depth: + add_nested_fulfillment(child, current_depth) + else: + child.add_subfulfillment(ilp_fulfillment_ed1) + parent.add_subfulfillment(child) + return parent + + fulfillment = add_nested_fulfillment(fulfillment) + + assert fulfillment.validate() is True + assert len(fulfillment.subfulfillments) == 2 + assert isinstance(fulfillment.subfulfillments[1], ThresholdSha256Fulfillment) + assert isinstance(fulfillment.subfulfillments[1].subfulfillments[0], ThresholdSha256Fulfillment) \ No newline at end of file From 9f5ff683b3d04ba7c6ba467e42fd32f0541ef4c3 Mon Sep 17 00:00:00 2001 From: diminator Date: Mon, 21 Mar 2016 16:22:44 +0100 Subject: [PATCH 16/16] conftest for fulfillments abstract class without NotImplementedError --- bigchaindb/crypto/asymmetric.py | 38 ++++-- bigchaindb/crypto/condition.py | 3 +- tests/crypto/conftest.py | 87 ++++++++++++++ tests/crypto/test_fulfillment.py | 200 +++++++++++++++---------------- 4 files changed, 213 insertions(+), 115 deletions(-) create mode 100644 tests/crypto/conftest.py diff --git a/bigchaindb/crypto/asymmetric.py b/bigchaindb/crypto/asymmetric.py index 15d5c5f3..bd499878 100644 --- a/bigchaindb/crypto/asymmetric.py +++ b/bigchaindb/crypto/asymmetric.py @@ -17,15 +17,25 @@ class SigningKey(metaclass=ABCMeta): Args: data: """ - raise NotImplementedError @abstractmethod def get_verifying_key(self): - raise NotImplementedError + """ + Get the associated verifying key + + Returns: + A VerifyingKey object + """ @abstractmethod def to_ascii(self, prefix, encoding): - raise NotImplementedError + """ + Encode the external value + + Args: + prefix: + encoding: + """ @staticmethod @abstractmethod @@ -36,7 +46,6 @@ class SigningKey(metaclass=ABCMeta): Args: private_value: """ - raise NotImplementedError @staticmethod @abstractmethod @@ -54,11 +63,26 @@ class VerifyingKey(metaclass=ABCMeta): @abstractmethod def verify(self, data, signature): - raise NotImplementedError + """ + Check the if the signature matches the data and this verifyingkey + + Args: + data: + signature: + + Returns: + boolean: + """ @abstractmethod def to_ascii(self, prefix, encoding): - raise NotImplementedError + """ + Encode the external value + + Args: + prefix: + encoding: + """ @staticmethod @abstractmethod @@ -69,7 +93,6 @@ class VerifyingKey(metaclass=ABCMeta): Args: public_value """ - raise NotImplementedError @staticmethod @abstractmethod @@ -80,7 +103,6 @@ class VerifyingKey(metaclass=ABCMeta): Args: public_base58 (base58): """ - raise NotImplementedError def hash_data(data): diff --git a/bigchaindb/crypto/condition.py b/bigchaindb/crypto/condition.py index 94ee4f89..fe28e28b 100644 --- a/bigchaindb/crypto/condition.py +++ b/bigchaindb/crypto/condition.py @@ -2,6 +2,7 @@ import base64 import re from abc import ABCMeta +import binascii from six import string_types from bigchaindb.crypto.buffer import base64_add_padding, base64_remove_padding, Writer, Reader @@ -41,7 +42,7 @@ class Condition(metaclass=ABCMeta): raise ValueError('Invalid condition format') condition = Condition() - condition.bitmask = int(pieces[2]) + condition.bitmask = int(pieces[2], 16) condition.hash = base64.urlsafe_b64decode(base64_add_padding(pieces[3])) condition.max_fulfillment_length = int(pieces[4]) diff --git a/tests/crypto/conftest.py b/tests/crypto/conftest.py new file mode 100644 index 00000000..8d246c24 --- /dev/null +++ b/tests/crypto/conftest.py @@ -0,0 +1,87 @@ + +import pytest + + +VK_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf' +VK_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' +VK_B58_ILP = b'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' + +SK_HEX_ILP = b'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42' +SK_B64_ILP = b'gz/mJAkje51i7HdYdSCRHpp1nOwdGXVbfakBuW3KPUI' +SK_B58_ILP = b'9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs' + +CONDITION_SHA256_URI = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' +CONDITION_SHA256_HASH = b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' +FULFILLMENT_SHA256_URI = 'cf:1:1:AA' + +CONDITION_ED25519_URI = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' +CONDITION_ED25519_HASH = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' +FULFILLMENT_ED25519_URI = \ + 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531' \ + 'PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' + +CONDITION_ED25519_URI_2 = 'cc:1:8:_WzTrHvFnv4I-H0cAKWZ6Q3g3Y0Du3aW01nIsaAsio8:116' +CONDITION_ED25519_HASH_2 = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' +FULFILLMENT_ED25519_URI_2 = \ + 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_D0hlbGxvIHVuaXZlcnNlISAbIENvbmRpdGlvbnMgYXJlIGV2ZXJ5d2hlc' \ + 'mUhQNmD2Cvk7e3EFOo-arA2TKYTP-474Z4okhbYmKij6XxObIbRsDScjXILAJ6mV5hP7Xyqkg5fcSsZbfRYypzlsAM' + +CONDITION_THRESHOLD_ED25519_URI = 'cc:1:d:fDM51fekeLlbeF9yj9W1KT76jtqa7u0vMlJAbM4EyiE:230' +FULFILLMENT_THRESHOLD_ED25519_URI = \ + 'cf:1:4:AgIBAQABCCDsFyuTrV5WO_STLHDhJFA0w1Rn7y79TWTr-BloNGfivwxIZWxsbyB3b3JsZCEgFSBDb25kaXRpb25zIGFyZSBoZXJlI' \ + 'UBDW6ped9T2wiZUVLyoz-epNFyiTDqyBqNheurnrk7UZ2KyQdrdmbbXX1zOIMw__O3h9Z2U6buK05AMfNYUnacCAQEIIP1s06x7xZ7-CPh9H' \ + 'AClmekN4N2NA7t2ltNZyLGgLIqPdA' + + +@pytest.fixture(scope='module') +def vk_ilp(): + return { + 'hex': VK_HEX_ILP, + 'b64': VK_B64_ILP, + 'b58': VK_B58_ILP + } + + +@pytest.fixture(scope='module') +def sk_ilp(): + return { + 'hex': SK_HEX_ILP, + 'b64': SK_B64_ILP, + 'b58': SK_B58_ILP + } + + +@pytest.fixture(scope='module') +def fulfillment_sha256(): + return { + 'condition_uri': CONDITION_SHA256_URI, + 'condition_hash': CONDITION_SHA256_HASH, + 'fulfillment_uri': FULFILLMENT_SHA256_URI + } + + +@pytest.fixture(scope='module') +def fulfillment_ed25519(): + return { + 'condition_uri': CONDITION_ED25519_URI, + 'condition_hash': CONDITION_ED25519_HASH, + 'fulfillment_uri': FULFILLMENT_ED25519_URI + } + + +@pytest.fixture(scope='module') +def fulfillment_ed25519_2(): + return { + 'condition_uri': CONDITION_ED25519_URI_2, + 'condition_hash': CONDITION_ED25519_HASH_2, + 'fulfillment_uri': FULFILLMENT_ED25519_URI_2 + } + + +@pytest.fixture(scope='module') +def fulfillment_threshold(): + return { + 'condition_uri': CONDITION_THRESHOLD_ED25519_URI, + 'condition_hash': None, + 'fulfillment_uri': FULFILLMENT_THRESHOLD_ED25519_URI + } diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index 1452aa59..9c5bc71c 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -13,37 +13,33 @@ from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfi class TestBigchainILPSha256Condition: - CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' - CONDITION_SHA256_HASH = b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' - def test_deserialize_condition(self): - example_condition = self.CONDITION_SHA256_ILP + def test_deserialize_condition(self, fulfillment_sha256): + example_condition = fulfillment_sha256['condition_uri'] condition = Condition.from_uri(example_condition) - assert condition.serialize_uri() == self.CONDITION_SHA256_ILP + assert condition.serialize_uri() == fulfillment_sha256['condition_uri'] - def test_create_condition(self): + def test_create_condition(self, fulfillment_sha256): sha256condition = Condition() sha256condition.bitmask = Sha256Fulfillment._bitmask - sha256condition.hash = binascii.unhexlify(self.CONDITION_SHA256_HASH) + sha256condition.hash = binascii.unhexlify(fulfillment_sha256['condition_hash']) sha256condition.max_fulfillment_length = 1 - assert sha256condition.serialize_uri() == self.CONDITION_SHA256_ILP + assert sha256condition.serialize_uri() == fulfillment_sha256['condition_uri'] class TestBigchainILPSha256Fulfillment: - CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' - FULFILLMENT_SHA256_ILP = 'cf:1:1:AA' - def test_deserialize_and_validate_fulfillment(self): - fulfillment = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) - assert fulfillment.serialize_uri() == self.FULFILLMENT_SHA256_ILP - assert fulfillment.condition.serialize_uri() == self.CONDITION_SHA256_ILP + def test_deserialize_and_validate_fulfillment(self, fulfillment_sha256): + fulfillment = Fulfillment.from_uri(fulfillment_sha256['fulfillment_uri']) + assert fulfillment.serialize_uri() == fulfillment_sha256['fulfillment_uri'] + assert fulfillment.condition.serialize_uri() == fulfillment_sha256['condition_uri'] assert fulfillment.validate() - def test_deserialize_condition_and_validate_fulfillment(self): - condition = Condition.from_uri(self.CONDITION_SHA256_ILP) + def test_deserialize_condition_and_validate_fulfillment(self, fulfillment_sha256): + condition = Condition.from_uri(fulfillment_sha256['condition_uri']) fulfillment = Sha256Fulfillment() fulfillment.preimage = '' - assert fulfillment.serialize_uri() == self.FULFILLMENT_SHA256_ILP + assert fulfillment.serialize_uri() == fulfillment_sha256['fulfillment_uri'] assert fulfillment.condition.serialize_uri() == condition.serialize_uri() assert fulfillment.validate() assert fulfillment.validate() \ @@ -64,40 +60,27 @@ class TestBigchainILPSha256Fulfillment: class TestBigchainILPEd25519Sha256Fulfillment: - PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf' - PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' - PUBLIC_B58_ILP = b'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' - PRIVATE_HEX_ILP = b'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42' - PRIVATE_B64_ILP = b'gz/mJAkje51i7HdYdSCRHpp1nOwdGXVbfakBuW3KPUI' - PRIVATE_B58_ILP = b'9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs' + def test_ilp_keys(self, sk_ilp, vk_ilp): + sk = Ed25519SigningKey(sk_ilp['b58']) + assert sk.to_ascii(encoding='base64') == sk_ilp['b64'] + assert binascii.hexlify(sk.to_bytes()[:32]) == sk_ilp['hex'] - CONDITION_ED25519_ILP = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' - FULFILLMENT_ED25519_ILP = \ - 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531' \ - 'PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' - HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' + vk = Ed25519VerifyingKey(vk_ilp['b58']) + assert vk.to_ascii(encoding='base64') == vk_ilp['b64'] + assert binascii.hexlify(vk.to_bytes()) == vk_ilp['hex'] - def test_ilp_keys(self): - sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) - assert sk.to_ascii(encoding='base64') == self.PRIVATE_B64_ILP - assert binascii.hexlify(sk.to_bytes()[:32]) == self.PRIVATE_HEX_ILP - - vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) - assert vk.to_ascii(encoding='base64') == self.PUBLIC_B64_ILP - assert binascii.hexlify(vk.to_bytes()) == self.PUBLIC_HEX_ILP - - def test_serialize_condition_and_validate_fulfillment(self): - sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) - vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) + def test_serialize_condition_and_validate_fulfillment(self, sk_ilp, vk_ilp, fulfillment_ed25519): + sk = Ed25519SigningKey(sk_ilp['b58']) + vk = Ed25519VerifyingKey(vk_ilp['b58']) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk fulfillment.message_prefix = 'Hello world!' fulfillment.max_dynamic_message_length = 32 # defaults to 0 - assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP - assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP + assert fulfillment.condition.serialize_uri() == fulfillment_ed25519['condition_uri'] + assert binascii.hexlify(fulfillment.condition.hash) == fulfillment_ed25519['condition_hash'] fulfillment.message = ' Conditions are here!' @@ -107,17 +90,17 @@ class TestBigchainILPEd25519Sha256Fulfillment: # Fulfill an ED25519-SHA256 condition fulfillment.sign(sk) - assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP + assert fulfillment.serialize_uri() == fulfillment_ed25519['fulfillment_uri'] assert fulfillment.validate() - def test_deserialize_condition(self): - deserialized_condition = Condition.from_uri(self.CONDITION_ED25519_ILP) + def test_deserialize_condition(self, fulfillment_ed25519): + deserialized_condition = Condition.from_uri(fulfillment_ed25519['condition_uri']) - assert deserialized_condition.serialize_uri() == self.CONDITION_ED25519_ILP - assert binascii.hexlify(deserialized_condition.hash) == self.HASH_ED25519_HEX_ILP + assert deserialized_condition.serialize_uri() == fulfillment_ed25519['condition_uri'] + assert binascii.hexlify(deserialized_condition.hash) == fulfillment_ed25519['condition_hash'] - def test_serialize_deserialize_condition(self): - vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) + def test_serialize_deserialize_condition(self, vk_ilp): + vk = Ed25519VerifyingKey(vk_ilp['b58']) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk @@ -132,19 +115,19 @@ class TestBigchainILPEd25519Sha256Fulfillment: assert deserialized_condition.max_fulfillment_length == condition.max_fulfillment_length assert deserialized_condition.serialize_uri() == condition.serialize_uri() - def test_deserialize_fulfillment(self): - fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + def test_deserialize_fulfillment(self, vk_ilp, fulfillment_ed25519): + fulfillment = Fulfillment.from_uri(fulfillment_ed25519['fulfillment_uri']) assert isinstance(fulfillment, Ed25519Sha256Fulfillment) - assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP - assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP - assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP - assert fulfillment.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP + assert fulfillment.serialize_uri() == fulfillment_ed25519['fulfillment_uri'] + assert fulfillment.condition.serialize_uri() == fulfillment_ed25519['condition_uri'] + assert binascii.hexlify(fulfillment.condition.hash) == fulfillment_ed25519['condition_hash'] + assert fulfillment.public_key.to_ascii(encoding='hex') == vk_ilp['hex'] assert fulfillment.validate() - def test_serialize_deserialize_fulfillment(self): - sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) - vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) + def test_serialize_deserialize_fulfillment(self, sk_ilp, vk_ilp): + sk = Ed25519SigningKey(sk_ilp['b58']) + vk = Ed25519VerifyingKey(vk_ilp['b58']) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk @@ -164,32 +147,10 @@ class TestBigchainILPEd25519Sha256Fulfillment: class TestBigchainILPThresholdSha256Fulfillment: - PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' - PRIVATE_B58_ILP = '9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs' - CONDITION_ED25519_ILP = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113' - FULFILLMENT_ED25519_ILP = \ - 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531' \ - 'PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI' - - CONDITION_ED25519_ILP_2 = 'cc:1:8:_WzTrHvFnv4I-H0cAKWZ6Q3g3Y0Du3aW01nIsaAsio8:116' - FULFILLMENT_ED25519_ILP_2 = \ - 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_D0hlbGxvIHVuaXZlcnNlISAbIENvbmRpdGlvbnMgYXJlIGV2ZXJ5d2hlc' \ - 'mUhQNmD2Cvk7e3EFOo-arA2TKYTP-474Z4okhbYmKij6XxObIbRsDScjXILAJ6mV5hP7Xyqkg5fcSsZbfRYypzlsAM' - HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5' - - CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' - FULFILLMENT_SHA256_ILP = 'cf:1:1:AA' - - CONDITION_THRESHOLD_ED25519_ILP_2 = 'cc:1:d:fDM51fekeLlbeF9yj9W1KT76jtqa7u0vMlJAbM4EyiE:230' - FULFILLMENT_THRESHOLD_ED25519_ILP_2 = \ - 'cf:1:4:AgIBAQABCCDsFyuTrV5WO_STLHDhJFA0w1Rn7y79TWTr-BloNGfivwxIZWxsbyB3b3JsZCEgFSBDb25kaXRpb25zIGFyZSBoZXJlI' \ - 'UBDW6ped9T2wiZUVLyoz-epNFyiTDqyBqNheurnrk7UZ2KyQdrdmbbXX1zOIMw__O3h9Z2U6buK05AMfNYUnacCAQEIIP1s06x7xZ7-CPh9H' \ - 'AClmekN4N2NA7t2ltNZyLGgLIqPdA' - - def create_fulfillment_ed25519sha256(self): - sk = Ed25519SigningKey(self.PRIVATE_B58_ILP) - vk = Ed25519VerifyingKey(self.PUBLIC_B58_ILP) + def create_fulfillment_ed25519sha256(self, sk_ilp, vk_ilp): + sk = Ed25519SigningKey(sk_ilp['b58']) + vk = Ed25519VerifyingKey(vk_ilp['b58']) fulfillment = Ed25519Sha256Fulfillment() fulfillment.public_key = vk @@ -199,10 +160,14 @@ class TestBigchainILPThresholdSha256Fulfillment: fulfillment.sign(sk) return fulfillment - def test_serialize_condition_and_validate_fulfillment(self): - ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) - ilp_fulfillment_2 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) - ilp_fulfillment_3 = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) + def test_serialize_condition_and_validate_fulfillment(self, + fulfillment_sha256, + fulfillment_ed25519, + fulfillment_ed25519_2, + fulfillment_threshold): + ilp_fulfillment = Fulfillment.from_uri(fulfillment_ed25519_2['fulfillment_uri']) + ilp_fulfillment_2 = Fulfillment.from_uri(fulfillment_ed25519['fulfillment_uri']) + ilp_fulfillment_3 = Fulfillment.from_uri(fulfillment_sha256['fulfillment_uri']) assert ilp_fulfillment.validate() == True assert ilp_fulfillment_2.validate() == True @@ -216,31 +181,35 @@ class TestBigchainILPThresholdSha256Fulfillment: fulfillment.add_subfulfillment(ilp_fulfillment_3) fulfillment.threshold = threshold # defaults to subconditions.length - assert fulfillment.condition.serialize_uri() == self.CONDITION_THRESHOLD_ED25519_ILP_2 + assert fulfillment.condition.serialize_uri() == fulfillment_threshold['condition_uri'] # Note: If there are more than enough fulfilled subconditions, shorter # fulfillments will be chosen over longer ones. # thresholdFulfillmentUri.length === 65 - assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2 + assert fulfillment.serialize_uri() == fulfillment_threshold['fulfillment_uri'] assert fulfillment.validate() - def test_deserialize_fulfillment(self): + def test_deserialize_fulfillment(self, + fulfillment_sha256, + fulfillment_ed25519, + fulfillment_threshold): num_fulfillments = 3 threshold = 2 - fulfillment = Fulfillment.from_uri(self.FULFILLMENT_THRESHOLD_ED25519_ILP_2) + fulfillment = Fulfillment.from_uri(fulfillment_threshold['fulfillment_uri']) assert isinstance(fulfillment, ThresholdSha256Fulfillment) assert fulfillment.threshold == threshold assert len(fulfillment.subfulfillments) == threshold assert len(fulfillment.get_all_subconditions()) == num_fulfillments - assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2 + assert fulfillment.serialize_uri() == fulfillment_threshold['fulfillment_uri'] assert fulfillment.validate() assert isinstance(fulfillment.subfulfillments[0], Sha256Fulfillment) assert isinstance(fulfillment.subfulfillments[1], Ed25519Sha256Fulfillment) - assert fulfillment.subfulfillments[0].condition.serialize_uri() == self.CONDITION_SHA256_ILP - assert fulfillment.subfulfillments[1].condition.serialize_uri() == self.CONDITION_ED25519_ILP + assert fulfillment.subfulfillments[0].condition.serialize_uri() == fulfillment_sha256['condition_uri'] + assert fulfillment.subfulfillments[1].condition.serialize_uri() == fulfillment_ed25519['condition_uri'] - def test_serialize_deserialize_fulfillment(self): - ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + def test_serialize_deserialize_fulfillment(self, + fulfillment_ed25519): + ilp_fulfillment = Fulfillment.from_uri(fulfillment_ed25519['fulfillment_uri']) num_fulfillments = 100 threshold = ceil(num_fulfillments * 2 / 3) @@ -262,8 +231,8 @@ class TestBigchainILPThresholdSha256Fulfillment: assert deserialized_fulfillment.serialize_uri() == fulfillment_uri assert deserialized_fulfillment.validate() - def test_fulfillment_didnt_reach_threshold(self): - ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + def test_fulfillment_didnt_reach_threshold(self, fulfillment_ed25519): + ilp_fulfillment = Fulfillment.from_uri(fulfillment_ed25519['fulfillment_uri']) threshold = 10 # Create a threshold condition @@ -292,10 +261,13 @@ class TestBigchainILPThresholdSha256Fulfillment: assert deserialized_fulfillment.serialize_uri() == fulfillment_uri assert deserialized_fulfillment.validate() - def test_fulfillment_nested_and_or(self): - ilp_fulfillment_sha = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) - ilp_fulfillment_ed1 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) - ilp_fulfillment_ed2 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP) + def test_fulfillment_nested_and_or(self, + fulfillment_sha256, + fulfillment_ed25519, + fulfillment_ed25519_2): + ilp_fulfillment_sha = Fulfillment.from_uri(fulfillment_sha256['fulfillment_uri']) + ilp_fulfillment_ed1 = Fulfillment.from_uri(fulfillment_ed25519_2['fulfillment_uri']) + ilp_fulfillment_ed2 = Fulfillment.from_uri(fulfillment_ed25519['fulfillment_uri']) # 2-of-2 (AND with 2 inputs) fulfillment = ThresholdSha256Fulfillment() @@ -318,6 +290,9 @@ class TestBigchainILPThresholdSha256Fulfillment: fulfillment_uri = fulfillment.serialize_uri() deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri) + condition_uri = fulfillment.condition.serialize_uri() + deserialized_condition = Condition.from_uri(condition_uri) + assert isinstance(deserialized_fulfillment, ThresholdSha256Fulfillment) assert deserialized_fulfillment.threshold == 2 assert len(deserialized_fulfillment.subfulfillments) == 2 @@ -325,10 +300,13 @@ class TestBigchainILPThresholdSha256Fulfillment: assert len(deserialized_fulfillment.get_all_subconditions()) == 2 assert deserialized_fulfillment.serialize_uri() == fulfillment_uri assert deserialized_fulfillment.validate() + assert deserialized_condition.serialize_uri() == condition_uri - def test_fulfillment_nested(self): - ilp_fulfillment_sha = Fulfillment.from_uri(self.FULFILLMENT_SHA256_ILP) - ilp_fulfillment_ed1 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2) + def test_fulfillment_nested(self, + fulfillment_sha256, + fulfillment_ed25519_2,): + ilp_fulfillment_sha = Fulfillment.from_uri(fulfillment_sha256['fulfillment_uri']) + ilp_fulfillment_ed1 = Fulfillment.from_uri(fulfillment_ed25519_2['fulfillment_uri']) # 2-of-2 (AND with 2 inputs) fulfillment = ThresholdSha256Fulfillment() @@ -353,4 +331,14 @@ class TestBigchainILPThresholdSha256Fulfillment: assert fulfillment.validate() is True assert len(fulfillment.subfulfillments) == 2 assert isinstance(fulfillment.subfulfillments[1], ThresholdSha256Fulfillment) - assert isinstance(fulfillment.subfulfillments[1].subfulfillments[0], ThresholdSha256Fulfillment) \ No newline at end of file + assert isinstance(fulfillment.subfulfillments[1].subfulfillments[0], ThresholdSha256Fulfillment) + + fulfillment_uri = fulfillment.serialize_uri() + deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri) + + condition_uri = fulfillment.condition.serialize_uri() + deserialized_condition = Condition.from_uri(condition_uri) + + assert deserialized_fulfillment.serialize_uri() == fulfillment_uri + assert deserialized_fulfillment.validate() is True + assert deserialized_condition.serialize_uri() == condition_uri