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)