From 9dc767318e631717d93dc9f59c8d3916a03fa64a Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 16 Mar 2016 16:27:40 +0100 Subject: [PATCH 1/5] - 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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