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 f7b37ad4..b4e8d024 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 asymmetric class Client: @@ -111,6 +111,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 = 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 35b2de2f..b79b11b8 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 asymmetric +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'] = asymmetric.generate_key_pair() if not args.yes: for key in ('host', 'port', 'name'): 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): diff --git a/bigchaindb/core.py b/bigchaindb/core.py index f76dd2d6..d7721fb4 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -5,13 +5,12 @@ import rapidjson 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 asymmetric from bigchaindb.monitor import Monitor - monitor = Monitor() @@ -297,8 +296,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 = asymmetric.hash_data(block_data) + block_signature = asymmetric.SigningKey(self.me_private).sign(block_data) block = { 'id': block_hash, @@ -419,7 +418,7 @@ class Bigchain(object): } vote_data = util.serialize(vote) - signature = crypto.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/__init__.py b/bigchaindb/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bigchaindb/crypto/asymmetric.py b/bigchaindb/crypto/asymmetric.py new file mode 100644 index 00000000..bd499878 --- /dev/null +++ b/bigchaindb/crypto/asymmetric.py @@ -0,0 +1,118 @@ +# Separate all crypto code so that we can easily test several implementations +from abc import ABCMeta, abstractmethod + +import sha3 + + +class SigningKey(metaclass=ABCMeta): + """ + PrivateKey instance + """ + + @abstractmethod + def sign(self, data): + """ + Sign data with private key + + Args: + data: + """ + + @abstractmethod + def get_verifying_key(self): + """ + Get the associated verifying key + + Returns: + A VerifyingKey object + """ + + @abstractmethod + def to_ascii(self, prefix, encoding): + """ + Encode the external value + + Args: + prefix: + encoding: + """ + + @staticmethod + @abstractmethod + def encode(private_value): + """ + Encode the internal private_value to base58 + + Args: + private_value: + """ + + @staticmethod + @abstractmethod + def decode(private_base58): + """ + Decode the base58 private value to internal value + + Args: + private_base58 (base58): + """ + raise NotImplementedError + + +class VerifyingKey(metaclass=ABCMeta): + + @abstractmethod + def verify(self, data, signature): + """ + Check the if the signature matches the data and this verifyingkey + + Args: + data: + signature: + + Returns: + boolean: + """ + + @abstractmethod + def to_ascii(self, prefix, encoding): + """ + Encode the external value + + Args: + prefix: + encoding: + """ + + @staticmethod + @abstractmethod + def encode(public_value): + """ + Encode the public key to base58 represented by the internal values + + Args: + public_value + """ + + @staticmethod + @abstractmethod + def decode(public_base58): + """ + Decode the base58 public_value to internal value + + Args: + public_base58 (base58): + """ + + +def hash_data(data): + """Hash the provided data using SHA3-256""" + + return sha3.sha3_256(data.encode()).hexdigest() + + +from bigchaindb.crypto.ecdsa import EcdsaSigningKey, EcdsaVerifyingKey, ecdsa_generate_key_pair + +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 new file mode 100644 index 00000000..7ae345f7 --- /dev/null +++ b/bigchaindb/crypto/bitmark_registry.py @@ -0,0 +1,60 @@ +from bigchaindb.crypto.buffer import MAX_SAFE_INTEGER_JS + + +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_JS: + 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.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/buffer.py b/bigchaindb/crypto/buffer.py new file mode 100644 index 00000000..f3d641a6 --- /dev/null +++ b/bigchaindb/crypto/buffer.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 +# 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: + """ + 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): + + def __init__(self, algorithm): + if algorithm == 'sha256': + self.hash = hashlib.sha256() + else: + raise NotImplementedError + super().__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: + def __init__(self): + self.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_JS: + 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.rstrip(b'=') diff --git a/bigchaindb/crypto/condition.py b/bigchaindb/crypto/condition.py new file mode 100644 index 00000000..fe28e28b --- /dev/null +++ b/bigchaindb/crypto/condition.py @@ -0,0 +1,222 @@ +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 + +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], 16) + condition.hash = base64.urlsafe_b64decode(base64_add_padding(pieces[3])) + condition.max_fulfillment_length = int(pieces[4]) + + 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): + """ + 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. + """ + 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. + + "cc:" BASE10(VERSION) ":" BASE16(TYPE_BITMASK) ":" BASE64URL(HASH) ":" BASE10(MAX_FULFILLMENT_LENGTH) + + Returns: + string: Condition as a URI + """ + + 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/crypto.md b/bigchaindb/crypto/crypto.md new file mode 100644 index 00000000..e478773d --- /dev/null +++ b/bigchaindb/crypto/crypto.md @@ -0,0 +1,429 @@ +# Crypto Conditions + +This spec is from the [**Interledger Protocol (ILP)**] +(https://interledger.org/five-bells-condition/spec.html) + +## 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://interledger.org/five-bells-condition/spec.html) + + +## 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://interledger.org/five-bells-condition/spec.html). + +### 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_fulfill`nt = 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_DEhlbGxvIHdvcmxkI +# SAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531PbCJlRUvKjP56k0XKJMOrIGo2F66u +# euTtRnYrJB2t2ZttdfXM4gzD_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 +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 +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(threshold_fulfillment_uri) +# prints 'cf:1:4:AQEBAQABAQggqQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPVx' + +# Validate fulfillment +print(threshold_fulfillment.validate()) +# prints True + +# Parse the fulfillment +reparsed_fulfillment = \ + ThresholdSha256Fulfillment.from_uri(threshold_fulfillment_uri) +print(reparsed_fulfillment.validate()) +# prints True + +# 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 diff --git a/bigchaindb/crypto.py b/bigchaindb/crypto/ecdsa.py similarity index 83% rename from bigchaindb/crypto.py rename to bigchaindb/crypto/ecdsa.py index bcbe0863..3139a3ec 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.asymmetric import SigningKey, VerifyingKey -class PrivateKey(object): +class EcdsaSigningKey(SigningKey): """ PrivateKey instance """ @@ -34,6 +34,11 @@ class PrivateKey(object): 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): @@ -65,7 +70,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 = 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,15 +82,18 @@ class PrivateKey(object): return private_numbers.private_key(default_backend()) -class PublicKey(object): +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) - 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())) @@ -97,10 +105,17 @@ class PublicKey(object): return True + def to_ascii(self, prefix='', encoding='base58'): + raise NotImplementedError + @staticmethod 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 +125,9 @@ class PublicKey(object): 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()) @@ -123,33 +141,28 @@ 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 + + Args: + public_numbers """ 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 = 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 = PublicKey.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) - - -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..5ba5328b --- /dev/null +++ b/bigchaindb/crypto/ed25519.py @@ -0,0 +1,125 @@ +# Separate all crypto code so that we can easily test several implementations + +import base64 + +import base58 +import ed25519 + +from bigchaindb.crypto.asymmetric import SigningKey, VerifyingKey + + +class Ed25519SigningKey(ed25519.SigningKey, SigningKey): + """ + PrivateKey instance + """ + + 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) + 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 + + Args: + data (str, bytes): data to sign + encoding (str): base64, hex + """ + if not isinstance(data, bytes): + data = data.encode('utf-8') + 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)) + + @staticmethod + def decode(key): + """ + Decode the base58 private_value to base64 + + Args: + key: + """ + return base64.b64encode(base58.b58decode(key)) + + +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) + super().__init__(public_base64, encoding='base64') + + def verify(self, data, signature, encoding='base64'): + try: + if encoding: + data = data.encode('utf-8') + super().verify(signature, data, encoding=encoding) + except ed25519.BadSignatureError: + return False + + 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): + """ + Encode the public key represented by base64 to base58 + + Args: + public_base64 + """ + return Ed25519SigningKey.encode(public_base64) + + @staticmethod + def decode(public_base58): + """ + Decode the base58 public_value to base64 + + Args: + public_base58 + """ + return Ed25519SigningKey.decode(public_base58) + + +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/crypto/fulfillment.py b/bigchaindb/crypto/fulfillment.py new file mode 100644 index 00000000..5a00b33c --- /dev/null +++ b/bigchaindb/crypto/fulfillment.py @@ -0,0 +1,206 @@ +import base64 +import re +from abc import ABCMeta, abstractmethod + +from six import string_types + +from bigchaindb.crypto.condition import Condition +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_-]+$' + + +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 + + @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_type = reader.read_var_uint() + cls = BitmaskRegistry.get_class_from_typebit(cls_type) + + fulfillment = cls() + fulfillment.parse_payload(reader) + + 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. + + "cf:" BASE10(VERSION) ":" BASE16(TYPE_BIT) ":" BASE64URL(FULFILLMENT_PAYLOAD) + + Return: + string: Fulfillment as a URI + """ + 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): + """ + 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 + """ + predictor = Predictor() + 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..4c4c590d --- /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.buffer 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..a9a0cf18 --- /dev/null +++ b/bigchaindb/crypto/fulfillments/ed25519_sha256.py @@ -0,0 +1,266 @@ +import base58 + +from bigchaindb.crypto.ed25519 import Ed25519VerifyingKey +from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment +from bigchaindb.crypto.buffer import Predictor + + +class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): + + _bitmask = 0x08 + + 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): + 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 + """ + if not isinstance(value, Ed25519VerifyingKey): + 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. + """ + 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. + """ + 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.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 = \ + Ed25519VerifyingKey( + 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() + + def write_payload(self, writer): + """ + Generate the fulfillment payload. + + 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. + """ + 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`. + + 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. + """ + 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.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 + """ + sk = private_key + vk = Ed25519VerifyingKey( + base58.b58encode( + sk.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/fulfillments/sha256.py b/bigchaindb/crypto/fulfillments/sha256.py new file mode 100644 index 00000000..2c433ce3 --- /dev/null +++ b/bigchaindb/crypto/fulfillments/sha256.py @@ -0,0 +1,94 @@ +from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment +from bigchaindb.crypto.buffer 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 new file mode 100644 index 00000000..cd291841 --- /dev/null +++ b/bigchaindb/crypto/fulfillments/threshold_sha256.py @@ -0,0 +1,253 @@ +from bigchaindb.crypto.condition import Condition +from bigchaindb.crypto.fulfillment import Fulfillment +from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment +from bigchaindb.crypto.buffer 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. + + Returns: + [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. + + Returns: + int: 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. + + HASH = SHA256( + VARUINT TYPE_BIT + VARUINT THRESHOLD + VARARRAY + VARUINT WEIGHT + CONDITION + ) + + 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: + int 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. + + FULFILLMENT_PAYLOAD = + VARUINT THRESHOLD + VARARRAY + VARUINT WEIGHT + FULFILLMENT + VARARRAY + VARUINT WEIGHT + CONDITION + + 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'])) + + # Cut off unnecessary fulfillments + 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. + + 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/util.py b/bigchaindb/util.py index d8ddc9ce..90091ffe 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.asymmetric import SigningKey, VerifyingKey, hash_data class ProcessGroup(object): @@ -158,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}) @@ -201,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/setup.py b/setup.py index 6156b417..c5457a84 100644 --- a/setup.py +++ b/setup.py @@ -79,6 +79,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/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_crypto.py b/tests/crypto/test_crypto.py new file mode 100644 index 00000000..94c96207 --- /dev/null +++ b/tests/crypto/test_crypto.py @@ -0,0 +1,135 @@ +import base64 + +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): + 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 = Ed25519SigningKey.encode(self.PRIVATE_B64) + assert private_value_base58 == self.PRIVATE_B58 + + def test_private_key_init(self): + 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 = Ed25519SigningKey.decode(self.PRIVATE_B58) + assert private_value == self.PRIVATE_B64 + + def test_public_key_encode(self): + public_value_base58 = Ed25519VerifyingKey.encode(self.PUBLIC_B64) + assert public_value_base58 == self.PUBLIC_B58 + + def test_public_key_init(self): + 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 = Ed25519VerifyingKey.decode(self.PUBLIC_B58) + assert public_value == self.PUBLIC_B64 + + def test_sign_verify(self): + message = 'Hello World!' + 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 = 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 + 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 = 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 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 = 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 = Ed25519VerifyingKey(Ed25519VerifyingKey.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 = EcdsaSigningKey.encode(self.PRIVATE_VALUE) + assert private_value_base58 == self.PRIVATE_VALUE_B58 + + def test_private_key_decode(self): + private_value = EcdsaSigningKey.decode(self.PRIVATE_VALUE_B58) + assert private_value == self.PRIVATE_VALUE + + def test_public_key_encode(self): + 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 = 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 = 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 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 new file mode 100644 index 00000000..9c5bc71c --- /dev/null +++ b/tests/crypto/test_fulfillment.py @@ -0,0 +1,344 @@ +import binascii + +from math import ceil + +import pytest + +from bigchaindb.crypto.condition import Condition +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 +from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment + + +class TestBigchainILPSha256Condition: + + def test_deserialize_condition(self, fulfillment_sha256): + example_condition = fulfillment_sha256['condition_uri'] + condition = Condition.from_uri(example_condition) + assert condition.serialize_uri() == fulfillment_sha256['condition_uri'] + + def test_create_condition(self, fulfillment_sha256): + sha256condition = Condition() + sha256condition.bitmask = Sha256Fulfillment._bitmask + sha256condition.hash = binascii.unhexlify(fulfillment_sha256['condition_hash']) + sha256condition.max_fulfillment_length = 1 + assert sha256condition.serialize_uri() == fulfillment_sha256['condition_uri'] + + +class TestBigchainILPSha256Fulfillment: + + 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, fulfillment_sha256): + condition = Condition.from_uri(fulfillment_sha256['condition_uri']) + fulfillment = Sha256Fulfillment() + fulfillment.preimage = '' + assert fulfillment.serialize_uri() == fulfillment_sha256['fulfillment_uri'] + 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() + 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: + + 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'] + + 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_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() == fulfillment_ed25519['condition_uri'] + assert binascii.hexlify(fulfillment.condition.hash) == fulfillment_ed25519['condition_hash'] + + 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() == fulfillment_ed25519['fulfillment_uri'] + assert fulfillment.validate() + + def test_deserialize_condition(self, fulfillment_ed25519): + deserialized_condition = Condition.from_uri(fulfillment_ed25519['condition_uri']) + + 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_ilp): + vk = Ed25519VerifyingKey(vk_ilp['b58']) + + 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, vk_ilp, fulfillment_ed25519): + fulfillment = Fulfillment.from_uri(fulfillment_ed25519['fulfillment_uri']) + + assert isinstance(fulfillment, Ed25519Sha256Fulfillment) + 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_ilp, vk_ilp): + 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 + fulfillment.message = ' Conditions are here!' + fulfillment.sign(sk) + + 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.to_bytes() == fulfillment.public_key.to_bytes() + assert deserialized_fulfillment.validate() + + +class TestBigchainILPThresholdSha256Fulfillment: + + 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 + 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, + 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 + + 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 + + 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() == fulfillment_threshold['fulfillment_uri'] + assert fulfillment.validate() + + def test_deserialize_fulfillment(self, + fulfillment_sha256, + fulfillment_ed25519, + fulfillment_threshold): + num_fulfillments = 3 + threshold = 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() == 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() == fulfillment_sha256['condition_uri'] + assert fulfillment.subfulfillments[1].condition.serialize_uri() == fulfillment_ed25519['condition_uri'] + + 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) + + # 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 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, fulfillment_ed25519): + ilp_fulfillment = Fulfillment.from_uri(fulfillment_ed25519['fulfillment_uri']) + 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 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() + + 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() + 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) + + 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 + 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() + assert deserialized_condition.serialize_uri() == condition_uri + + 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() + 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) + + 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 diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 1fed8800..2a7cb1da 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.asymmetric import SigningKey, VerifyingKey, 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 ' @@ -189,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'] == [] @@ -372,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, @@ -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): @@ -483,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 @@ -524,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 @@ -538,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 @@ -552,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 d8146829..2eeaf7f0 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.asymmetric import VerifyingKey, generate_key_pair from bigchaindb.voter import Voter, BlockStream -from bigchaindb.crypto import PublicKey, generate_key_pair class TestBigchainVoter(object): @@ -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 diff --git a/tests/test_commands.py b/tests/test_commands.py index 6e731c13..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.generate_key_pair', lambda: ('privkey', 'pubkey')) + monkeypatch.setattr('bigchaindb.crypto.asymmetric.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..1db8c7a9 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 asymmetric 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 = asymmetric.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 = 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))