Merge 9f5ff683b3d04ba7c6ba467e42fd32f0541ef4c3 into 955fd86a7f4323f61ae7c30ada53dccdbed575aa

This commit is contained in:
Dimitri De Jonghe 2016-03-22 09:59:54 +00:00
commit 310d395c54
29 changed files with 2883 additions and 109 deletions

2
.gitignore vendored
View File

@ -65,3 +65,5 @@ target/
# pyenv # pyenv
.python-version .python-version
# IDE related
.idea

View File

@ -1,10 +1,10 @@
import requests import requests
import bigchaindb import bigchaindb
from bigchaindb import util
from bigchaindb import config_utils from bigchaindb import config_utils
from bigchaindb import exceptions from bigchaindb import exceptions
from bigchaindb import crypto from bigchaindb import util
from bigchaindb.crypto import asymmetric
class Client: class Client:
@ -111,6 +111,6 @@ def temp_client():
A client initialized with a keypair generated on the fly. 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') return Client(private_key=private_key, public_key=public_key, api_endpoint='http://localhost:5000/api/v1')

View File

@ -1,19 +1,17 @@
"""Command line interface for the `bigchain` command.""" """Command line interface for the `bigchain` command."""
import os
import logging
import argparse import argparse
import copy import copy
import logging
import os
import bigchaindb import bigchaindb
import bigchaindb.config_utils import bigchaindb.config_utils
from bigchaindb import db from bigchaindb import db
from bigchaindb.exceptions import DatabaseAlreadyExists
from bigchaindb.commands.utils import base_parser, start 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.processes import Processes
from bigchaindb import crypto
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,7 +50,7 @@ def run_configure(args, skip_if_exists=False):
conf = copy.deepcopy(bigchaindb._config) conf = copy.deepcopy(bigchaindb._config)
print('Generating keypair') 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: if not args.yes:
for key in ('host', 'port', 'name'): for key in ('host', 'port', 'name'):

View File

@ -2,7 +2,7 @@ from abc import ABCMeta, abstractmethod
import bigchaindb.exceptions as exceptions import bigchaindb.exceptions as exceptions
from bigchaindb import util from bigchaindb import util
from bigchaindb.crypto import hash_data, PublicKey from bigchaindb.crypto.asymmetric import hash_data
class AbstractConsensusRules(metaclass=ABCMeta): class AbstractConsensusRules(metaclass=ABCMeta):

View File

@ -5,13 +5,12 @@ import rapidjson
import bigchaindb import bigchaindb
from bigchaindb import util
from bigchaindb import config_utils from bigchaindb import config_utils
from bigchaindb import exceptions from bigchaindb import exceptions
from bigchaindb import crypto from bigchaindb import util
from bigchaindb.crypto import asymmetric
from bigchaindb.monitor import Monitor from bigchaindb.monitor import Monitor
monitor = Monitor() monitor = Monitor()
@ -297,8 +296,8 @@ class Bigchain(object):
# Calculate the hash of the new block # Calculate the hash of the new block
block_data = util.serialize(block) block_data = util.serialize(block)
block_hash = crypto.hash_data(block_data) block_hash = asymmetric.hash_data(block_data)
block_signature = crypto.PrivateKey(self.me_private).sign(block_data) block_signature = asymmetric.SigningKey(self.me_private).sign(block_data)
block = { block = {
'id': block_hash, 'id': block_hash,
@ -419,7 +418,7 @@ class Bigchain(object):
} }
vote_data = util.serialize(vote) 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 = { vote_signed = {
'node_pubkey': self.me, 'node_pubkey': self.me,

View File

View File

@ -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

View File

@ -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)

437
bigchaindb/crypto/buffer.py Normal file
View File

@ -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'=')

View File

@ -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()

429
bigchaindb/crypto/crypto.md Normal file
View File

@ -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|2<sup>0</sup>| 1|SHA-256 |
| 10|2<sup>1</sup>| 2|RSA-SHA-256 |
| 100|2<sup>2</sup>| 4|THRESHOLD-SHA-256|
| 1000|2<sup>3</sup>| 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 2<sup>0</sup> = 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 2<sup>1</sup> = 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 2<sup>3</sup> = 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 2<sup>2</sup> = 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'
```

View File

@ -1,18 +1,18 @@
# Separate all crypto code so that we can easily test several implementations # Separate all crypto code so that we can easily test several implementations
import binascii import binascii
import base58 import base58
import sha3
import bitcoin 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.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 PrivateKey instance
""" """
@ -34,6 +34,11 @@ class PrivateKey(object):
signature = signer.finalize() signature = signer.finalize()
return binascii.hexlify(signature).decode('utf-8') return binascii.hexlify(signature).decode('utf-8')
def get_verifying_key(self):
raise NotImplementedError
def to_ascii(self, prefix='', encoding='base58'):
raise NotImplementedError
@staticmethod @staticmethod
def encode(private_value): def encode(private_value):
@ -65,7 +70,7 @@ class PrivateKey(object):
Return an instance of cryptography PrivateNumbers from the decimal private_value 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_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) private_numbers = ec.EllipticCurvePrivateNumbers(private_value, public_numbers)
return private_numbers return private_numbers
@ -77,15 +82,18 @@ class PrivateKey(object):
return private_numbers.private_key(default_backend()) return private_numbers.private_key(default_backend())
class PublicKey(object): class EcdsaVerifyingKey(VerifyingKey):
def __init__(self, key): def __init__(self, key):
""" """
Instantiate the public key with the compressed public value encoded in base58 Instantiate the public key with the compressed public value encoded in base58
Args:
key
""" """
public_value_x, public_value_y = self.decode(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) 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): def verify(self, data, signature):
verifier = self.public_key.verifier(binascii.unhexlify(signature), ec.ECDSA(hashes.SHA256())) verifier = self.public_key.verifier(binascii.unhexlify(signature), ec.ECDSA(hashes.SHA256()))
@ -97,10 +105,17 @@ class PublicKey(object):
return True return True
def to_ascii(self, prefix='', encoding='base58'):
raise NotImplementedError
@staticmethod @staticmethod
def encode(public_value_x, public_value_y): def encode(public_value_x, public_value_y):
""" """
Encode the public key represented by the decimal values x and y to base58 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_hex = bitcoin.encode_pubkey([public_value_x, public_value_y], 'hex_compressed')
public_value_compressed_base58 = base58.b58encode(bytes.fromhex(public_value_compressed_hex)) 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): def decode(public_value_compressed_base58):
""" """
Decode the base58 public_value to the decimal x and y values 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_compressed_hex = binascii.hexlify(base58.b58decode(public_value_compressed_base58))
public_value_x, public_value_y = bitcoin.decode_pubkey(public_value_compressed_hex.decode()) 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()) public_numbers = ec.EllipticCurvePublicNumbers(public_value_x, public_value_y, ec.SECP256K1())
return public_numbers return public_numbers
def _criptography_public_key_from_public_numbers(self, public_numbers): def _cryptography_public_key_from_public_numbers(self, public_numbers):
""" """
Return an instance of cryptography PublicKey from a cryptography instance of PublicNumbers Return an instance of cryptography PublicKey from a cryptography instance of PublicNumbers
Args:
public_numbers
""" """
return public_numbers.public_key(default_backend()) 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 Generate a new key pair and return the pair encoded in base58
""" """
# Private key # Private key
private_key = ec.generate_private_key(ec.SECP256K1, default_backend()) private_key = ec.generate_private_key(ec.SECP256K1, default_backend())
private_value = private_key.private_numbers().private_value 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
public_key = private_key.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_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) 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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -1,12 +1,11 @@
import json import json
import time
import multiprocessing as mp import multiprocessing as mp
import time
from datetime import datetime from datetime import datetime
import bigchaindb import bigchaindb
from bigchaindb import exceptions from bigchaindb import exceptions
from bigchaindb.crypto import PrivateKey, PublicKey, hash_data from bigchaindb.crypto.asymmetric import SigningKey, VerifyingKey, hash_data
class ProcessGroup(object): class ProcessGroup(object):
@ -158,7 +157,7 @@ def sign_tx(transaction, private_key):
dict: transaction with the `signature` field included. dict: transaction with the `signature` field included.
""" """
private_key = PrivateKey(private_key) private_key = SigningKey(private_key)
signature = private_key.sign(serialize(transaction)) signature = private_key.sign(serialize(transaction))
signed_transaction = transaction.copy() signed_transaction = transaction.copy()
signed_transaction.update({'signature': signature}) signed_transaction.update({'signature': signature})
@ -201,7 +200,7 @@ def verify_signature(signed_transaction):
signature = data.pop('signature') signature = data.pop('signature')
public_key_base58 = signed_transaction['transaction']['current_owner'] 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) return public_key.verify(serialize(data), signature)

View File

@ -79,6 +79,7 @@ setup(
'bitcoin==1.1.42', 'bitcoin==1.1.42',
'flask==0.10.1', 'flask==0.10.1',
'requests==2.9', 'requests==2.9',
'ed25519==1.4',
], ],
setup_requires=['pytest-runner'], setup_requires=['pytest-runner'],
tests_require=tests_require, tests_require=tests_require,

0
tests/crypto/__init__.py Normal file
View File

87
tests/crypto/conftest.py Normal file
View File

@ -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
}

135
tests/crypto/test_crypto.py Normal file
View File

@ -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

View File

@ -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

View File

@ -6,12 +6,11 @@ import pytest
import rethinkdb as r import rethinkdb as r
import bigchaindb import bigchaindb
from bigchaindb import util
from bigchaindb import exceptions from bigchaindb import exceptions
from bigchaindb.crypto import PrivateKey, PublicKey, generate_key_pair, hash_data from bigchaindb import util
from bigchaindb.voter import Voter
from bigchaindb.block import Block 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 ' @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']['voters'] == [b.me]
assert new_block['block']['node_pubkey'] == 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['id'] == block_hash
assert new_block['votes'] == [] assert new_block['votes'] == []
@ -372,7 +371,7 @@ class TestBlockValidation(object):
block_data = util.serialize(block) block_data = util.serialize(block)
block_hash = hash_data(block_data) 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 = { block = {
'id': block_hash, 'id': block_hash,
@ -408,45 +407,6 @@ class TestBlockValidation(object):
assert b.is_valid_block(block) 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): class TestBigchainVoter(object):
def test_valid_block_voting(self, b): 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']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_invalid_block_voting(self, b, user_public_key):
# create queue and voter # create queue and voter
@ -524,7 +484,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is False assert vote['vote']['is_block_valid'] is False
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_vote_creation_valid(self, b):
# create valid block # create valid block
@ -538,7 +498,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is True assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_vote_creation_invalid(self, b):
# create valid block # create valid block
@ -552,7 +512,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is False assert vote['vote']['is_block_valid'] is False
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): class TestBigchainBlock(object):

View File

@ -1,12 +1,12 @@
import pytest
import time
import rethinkdb as r
import multiprocessing as mp import multiprocessing as mp
import time
import pytest
import rethinkdb as r
from bigchaindb import util from bigchaindb import util
from bigchaindb.crypto.asymmetric import VerifyingKey, generate_key_pair
from bigchaindb.voter import Voter, BlockStream from bigchaindb.voter import Voter, BlockStream
from bigchaindb.crypto import PublicKey, generate_key_pair
class TestBigchainVoter(object): class TestBigchainVoter(object):
@ -45,7 +45,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is True assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_valid_block_voting_with_create_transaction(self, b):
q_new_block = mp.Queue() q_new_block = mp.Queue()
@ -87,7 +87,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is True assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_valid_block_voting_with_transfer_transactions(self, b):
q_new_block = mp.Queue() q_new_block = mp.Queue()
@ -158,7 +158,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is True assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_invalid_block_voting(self, b, user_public_key):
# create queue and voter # create queue and voter
@ -197,7 +197,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is False assert vote['vote']['is_block_valid'] is False
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_vote_creation_valid(self, b):
# create valid block # create valid block
@ -211,7 +211,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is True assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_vote_creation_invalid(self, b):
# create valid block # create valid block
@ -225,7 +225,7 @@ class TestBigchainVoter(object):
assert vote['vote']['is_block_valid'] is False assert vote['vote']['is_block_valid'] is False
assert vote['vote']['invalid_reason'] is None assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me 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): def test_voter_considers_unvoted_blocks_when_single_node(self, b):
# simulate a voter going donw in a single node environment # simulate a voter going donw in a single node environment

View File

@ -52,7 +52,7 @@ def mock_rethink_db_drop(monkeypatch):
@pytest.fixture @pytest.fixture
def mock_generate_key_pair(monkeypatch): 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 @pytest.fixture

View File

@ -1,8 +1,9 @@
import json import json
import pytest import pytest
from bigchaindb import crypto
from bigchaindb import util from bigchaindb import util
from bigchaindb.crypto import asymmetric
TX_ENDPOINT = '/api/v1/transactions/' 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): 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') 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): def test_post_transfer_transaction_endpoint(b, client):
from_keypair = crypto.generate_key_pair() from_keypair = asymmetric.generate_key_pair()
to_keypair = crypto.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') 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)) res = client.post(TX_ENDPOINT, data=json.dumps(tx))