mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Merge 46cd3323e8deb71028284b0533b56f5d805303fe into 55ad60097da46da44a33892d9c4f1f8ce797e774
This commit is contained in:
commit
4636434b1f
2
.gitignore
vendored
2
.gitignore
vendored
@ -65,3 +65,5 @@ target/
|
|||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
|
# IDE related
|
||||||
|
.idea
|
||||||
|
|||||||
@ -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:
|
||||||
@ -92,6 +92,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')
|
||||||
|
|
||||||
|
|||||||
@ -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'):
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import rethinkdb as r
|
|
||||||
import random
|
import random
|
||||||
import json
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
|
import rethinkdb as r
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
@ -98,7 +96,7 @@ class Bigchain(object):
|
|||||||
|
|
||||||
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 = crypto.PublicKey(public_key_base58)
|
public_key = asymmetric.PublicKey(public_key_base58)
|
||||||
return public_key.verify(util.serialize(data), signature)
|
return public_key.verify(util.serialize(data), signature)
|
||||||
|
|
||||||
@monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate'])
|
@monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate'])
|
||||||
@ -331,8 +329,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.PrivateKey(self.me_private).sign(block_data)
|
||||||
|
|
||||||
block = {
|
block = {
|
||||||
'id': block_hash,
|
'id': block_hash,
|
||||||
@ -357,7 +355,7 @@ class Bigchain(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 1. Check if current hash is correct
|
# 1. Check if current hash is correct
|
||||||
calculated_hash = crypto.hash_data(util.serialize(block['block']))
|
calculated_hash = asymmetric.hash_data(util.serialize(block['block']))
|
||||||
if calculated_hash != block['id']:
|
if calculated_hash != block['id']:
|
||||||
raise exceptions.InvalidHash()
|
raise exceptions.InvalidHash()
|
||||||
|
|
||||||
@ -452,7 +450,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.PrivateKey(self.me_private).sign(vote_data)
|
||||||
|
|
||||||
vote_signed = {
|
vote_signed = {
|
||||||
'node_pubkey': self.me,
|
'node_pubkey': self.me,
|
||||||
|
|||||||
0
bigchaindb/crypto/__init__.py
Normal file
0
bigchaindb/crypto/__init__.py
Normal file
69
bigchaindb/crypto/asymmetric.py
Normal file
69
bigchaindb/crypto/asymmetric.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Separate all crypto code so that we can easily test several implementations
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
import sha3
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateKey(metaclass=ABCMeta):
|
||||||
|
"""
|
||||||
|
PrivateKey instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def sign(self, data):
|
||||||
|
"""
|
||||||
|
Sign data with private key
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def encode(private_value):
|
||||||
|
"""
|
||||||
|
Encode the decimal number private_value to base58
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def decode(key):
|
||||||
|
"""
|
||||||
|
Decode the base58 private_value to decimale
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class PublicKey(metaclass=ABCMeta):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def verify(self, data, signature):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def encode(public_value_x, public_value_y):
|
||||||
|
"""
|
||||||
|
Encode the public key represented by the decimal values x and y to base58
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def decode(public_value_compressed_base58):
|
||||||
|
"""
|
||||||
|
Decode the base58 public_value to the decimal x and y values
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def hash_data(data):
|
||||||
|
"""Hash the provided data using SHA3-256"""
|
||||||
|
|
||||||
|
return sha3.sha3_256(data.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
from bigchaindb.crypto.ecdsa import ECDSAPrivateKey, ECDSAPublicKey, ecdsa_generate_key_pair
|
||||||
|
|
||||||
|
PrivateKey = ECDSAPrivateKey
|
||||||
|
PublicKey = ECDSAPublicKey
|
||||||
|
generate_key_pair = ecdsa_generate_key_pair
|
||||||
56
bigchaindb/crypto/bitmark_registry.py
Normal file
56
bigchaindb/crypto/bitmark_registry.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from bigchaindb.crypto.iostream import MAX_SAFE_INTEGER
|
||||||
|
|
||||||
|
|
||||||
|
class BitmaskRegistry:
|
||||||
|
registered_types = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_class_from_typebit(bitmask):
|
||||||
|
"""
|
||||||
|
Determine fulfillment implementation class from a bitmask.
|
||||||
|
|
||||||
|
Returns the class implementing a fulfillment type that matches a certain bitmask.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bitmask (int): fulfillment bitmask
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Class implementing the given fulfillment type.
|
||||||
|
"""
|
||||||
|
# Determine type of condition
|
||||||
|
if bitmask > MAX_SAFE_INTEGER:
|
||||||
|
raise ValueError('Bitmask {} is not supported'.format(bitmask))
|
||||||
|
|
||||||
|
for registered_type in BitmaskRegistry.registered_types:
|
||||||
|
if bitmask == registered_type['bitmask']:
|
||||||
|
return registered_type['class']
|
||||||
|
|
||||||
|
raise ValueError('Bitmask {} is not supported'.format(bitmask))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def register_type(cls):
|
||||||
|
"""
|
||||||
|
Add a new fulfillment type.
|
||||||
|
|
||||||
|
This can be used to extend this cryptocondition implementation with new
|
||||||
|
fulfillment types that it does not yet support. But mostly it is used
|
||||||
|
internally to register the built-in types.
|
||||||
|
|
||||||
|
In this method, we expect a regular fulfillment type, for information on
|
||||||
|
registering meta types please see `registerMetaType`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cls: Implementation of a fulfillment type.
|
||||||
|
"""
|
||||||
|
# TODO Do some sanity checks on Class
|
||||||
|
|
||||||
|
BitmaskRegistry.registered_types.append(
|
||||||
|
{
|
||||||
|
'bitmask': cls().bitmask,
|
||||||
|
'class': cls
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment
|
||||||
|
|
||||||
|
BitmaskRegistry.register_type(Ed25519Sha256Fulfillment)
|
||||||
163
bigchaindb/crypto/condition.py
Normal file
163
bigchaindb/crypto/condition.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import base64
|
||||||
|
import re
|
||||||
|
from abc import ABCMeta
|
||||||
|
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
from bigchaindb.crypto.iostream import base64_add_padding, base64_remove_padding
|
||||||
|
|
||||||
|
CONDITION_REGEX = r'^cc:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]{43}:[1-9][0-9]{0,50}$'
|
||||||
|
|
||||||
|
|
||||||
|
class Condition(metaclass=ABCMeta):
|
||||||
|
_bitmask = None
|
||||||
|
_hash = None
|
||||||
|
_max_fulfillment_length = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_uri(serialized_condition):
|
||||||
|
"""
|
||||||
|
Create a Condition object from a URI.
|
||||||
|
|
||||||
|
This method will parse a condition URI and construct a corresponding Condition object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
serialized_condition (str): URI representing the condition
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Condition: Resulting object
|
||||||
|
"""
|
||||||
|
if not isinstance(serialized_condition, string_types):
|
||||||
|
raise TypeError('Serialized condition must be a string')
|
||||||
|
|
||||||
|
pieces = serialized_condition.split(':')
|
||||||
|
if not pieces[0] == 'cc':
|
||||||
|
raise ValueError('Serialized condition must start with "cc:"')
|
||||||
|
|
||||||
|
if not pieces[1] == '1':
|
||||||
|
raise ValueError('Condition must be version 1')
|
||||||
|
|
||||||
|
if not re.match(CONDITION_REGEX, serialized_condition):
|
||||||
|
raise ValueError('Invalid condition format')
|
||||||
|
|
||||||
|
condition = Condition()
|
||||||
|
condition.bitmask = int(pieces[2])
|
||||||
|
condition.hash = base64.urlsafe_b64decode(base64_add_padding(pieces[3]))
|
||||||
|
condition.max_fulfillment_length = int(pieces[4])
|
||||||
|
|
||||||
|
return condition
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bitmask(self):
|
||||||
|
"""
|
||||||
|
Return the bitmask of this condition.
|
||||||
|
|
||||||
|
For simple condition types this is simply the bit representing this type.
|
||||||
|
For meta-conditions, these are the bits representing the types of the subconditions.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
int: Bitmask corresponding to this condition.
|
||||||
|
"""
|
||||||
|
return self._bitmask
|
||||||
|
|
||||||
|
@bitmask.setter
|
||||||
|
def bitmask(self, value):
|
||||||
|
"""
|
||||||
|
Set the bitmask.
|
||||||
|
|
||||||
|
Sets the required bitmask to validate a fulfillment for this condition.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (int): representation of bitmask.
|
||||||
|
"""
|
||||||
|
self._bitmask = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hash(self):
|
||||||
|
"""
|
||||||
|
Return the hash of the condition.
|
||||||
|
|
||||||
|
A primary component of all conditions is the hash. It encodes the static
|
||||||
|
properties of the condition. This method enables the conditions to be
|
||||||
|
constant size, no matter how complex they actually are. The data used to
|
||||||
|
generate the hash consists of all the static properties of the condition
|
||||||
|
and is provided later as part of the fulfillment.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Hash of the condition
|
||||||
|
"""
|
||||||
|
if not self._hash:
|
||||||
|
raise ValueError
|
||||||
|
return self._hash
|
||||||
|
|
||||||
|
@hash.setter
|
||||||
|
def hash(self, value):
|
||||||
|
"""
|
||||||
|
Validate and set the hash of this condition.
|
||||||
|
|
||||||
|
Typically conditions are generated from fulfillments and the hash is
|
||||||
|
calculated automatically. However, sometimes it may be necessary to
|
||||||
|
construct a condition URI from a known hash. This method enables that case.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Buffer): Hash as binary.
|
||||||
|
"""
|
||||||
|
# TODO: value must be Buffer
|
||||||
|
# if not isinstance(value, Buffer):
|
||||||
|
# raise ValueError
|
||||||
|
self._hash = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_fulfillment_length(self):
|
||||||
|
"""
|
||||||
|
Return the maximum fulfillment length.
|
||||||
|
|
||||||
|
The maximum fulfillment length is the maximum allowed length for any
|
||||||
|
fulfillment payload to fulfill this condition.
|
||||||
|
|
||||||
|
The condition defines a maximum fulfillment length which all
|
||||||
|
implementations will enforce. This allows implementations to verify that
|
||||||
|
their local maximum fulfillment size is guaranteed to accomodate any
|
||||||
|
possible fulfillment for this condition.
|
||||||
|
|
||||||
|
Otherwise an attacker could craft a fulfillment which exceeds the maximum
|
||||||
|
size of one implementation, but meets the maximum size of another, thereby
|
||||||
|
violating the fundamental property that fulfillments are either valid
|
||||||
|
everywhere or nowhere.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
(int) Maximum length (in bytes) of any fulfillment payload that fulfills this condition..
|
||||||
|
"""
|
||||||
|
if not self._max_fulfillment_length:
|
||||||
|
raise ValueError
|
||||||
|
return self._max_fulfillment_length
|
||||||
|
|
||||||
|
@max_fulfillment_length.setter
|
||||||
|
def max_fulfillment_length(self, value):
|
||||||
|
"""
|
||||||
|
Set the maximum fulfillment length.
|
||||||
|
|
||||||
|
The maximum fulfillment length is normally calculated automatically, when
|
||||||
|
calling `Fulfillment#getCondition`. However, when
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (int): Maximum fulfillment payload length in bytes.
|
||||||
|
"""
|
||||||
|
self._max_fulfillment_length = value
|
||||||
|
|
||||||
|
def serialize_uri(self):
|
||||||
|
"""
|
||||||
|
Generate the URI form encoding of this condition.
|
||||||
|
|
||||||
|
Turns the condition into a URI containing only URL-safe characters. This
|
||||||
|
format is convenient for passing around conditions in URLs, JSON and other text-based formats.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string: Condition as a URI
|
||||||
|
"""
|
||||||
|
|
||||||
|
return 'cc:1:{}:{}:{}'.format(self.bitmask,
|
||||||
|
base64_remove_padding(
|
||||||
|
base64.urlsafe_b64encode(self.hash)
|
||||||
|
).decode('utf-8'),
|
||||||
|
self.max_fulfillment_length)
|
||||||
@ -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 PrivateKey, PublicKey
|
||||||
|
|
||||||
|
|
||||||
class PrivateKey(object):
|
class ECDSAPrivateKey(PrivateKey):
|
||||||
"""
|
"""
|
||||||
PrivateKey instance
|
PrivateKey instance
|
||||||
"""
|
"""
|
||||||
@ -65,7 +65,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 = ECDSAPublicKey._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,7 +77,7 @@ class PrivateKey(object):
|
|||||||
return private_numbers.private_key(default_backend())
|
return private_numbers.private_key(default_backend())
|
||||||
|
|
||||||
|
|
||||||
class PublicKey(object):
|
class ECDSAPublicKey(PublicKey):
|
||||||
|
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
"""
|
"""
|
||||||
@ -85,7 +85,7 @@ class PublicKey(object):
|
|||||||
"""
|
"""
|
||||||
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()))
|
||||||
@ -123,33 +123,25 @@ 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
|
||||||
"""
|
"""
|
||||||
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 = ECDSAPrivateKey.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 = ECDSAPublicKey.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()
|
|
||||||
|
|
||||||
|
|
||||||
104
bigchaindb/crypto/ed25519.py
Normal file
104
bigchaindb/crypto/ed25519.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Separate all crypto code so that we can easily test several implementations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import base58
|
||||||
|
import ed25519
|
||||||
|
|
||||||
|
from bigchaindb.crypto.asymmetric import PrivateKey, PublicKey
|
||||||
|
|
||||||
|
|
||||||
|
class ED25519PrivateKey(PrivateKey):
|
||||||
|
"""
|
||||||
|
PrivateKey instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, key):
|
||||||
|
"""
|
||||||
|
Instantiate the private key with the private_value encoded in base58
|
||||||
|
"""
|
||||||
|
private_base64 = self.decode(key)
|
||||||
|
self.private_key = self._private_key_from_private_base64(private_base64)
|
||||||
|
|
||||||
|
def sign(self, data, encoding="base64"):
|
||||||
|
"""
|
||||||
|
Sign data with private key
|
||||||
|
"""
|
||||||
|
if not isinstance(data, bytes):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
return self.private_key.sign(data, encoding=encoding)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode(private_base64):
|
||||||
|
"""
|
||||||
|
Encode the base64 number private_base64 to base58
|
||||||
|
"""
|
||||||
|
return base58.b58encode(base64.b64decode(private_base64))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(key):
|
||||||
|
"""
|
||||||
|
Decode the base58 private_value to base64
|
||||||
|
"""
|
||||||
|
return base64.b64encode(base58.b58decode(key))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _private_key_from_private_base64(private_base64):
|
||||||
|
"""
|
||||||
|
Return an instance of a ED25519 SignigKey from a base64 key
|
||||||
|
"""
|
||||||
|
return ed25519.SigningKey(private_base64, encoding='base64')
|
||||||
|
|
||||||
|
|
||||||
|
class ED25519PublicKey(PublicKey):
|
||||||
|
|
||||||
|
def __init__(self, key):
|
||||||
|
"""
|
||||||
|
Instantiate the public key with the compressed public value encoded in base58
|
||||||
|
"""
|
||||||
|
public_base64 = self.decode(key)
|
||||||
|
self.public_key = self._public_key_from_public_base64(public_base64)
|
||||||
|
|
||||||
|
def verify(self, data, signature, encoding='base64'):
|
||||||
|
try:
|
||||||
|
if encoding:
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
self.public_key.verify(signature, data, encoding=encoding)
|
||||||
|
except ed25519.BadSignatureError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode(public_base64):
|
||||||
|
"""
|
||||||
|
Encode the public key represented by base64 to base58
|
||||||
|
"""
|
||||||
|
return ED25519PrivateKey.encode(public_base64)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(public_value_compressed_base58):
|
||||||
|
"""
|
||||||
|
Decode the base58 public_value to base64
|
||||||
|
"""
|
||||||
|
return ED25519PrivateKey.decode(public_value_compressed_base58)
|
||||||
|
|
||||||
|
def _public_key_from_public_base64(self, public_base64):
|
||||||
|
"""
|
||||||
|
Return an instance of ED25519 VerifyingKey from a base64
|
||||||
|
"""
|
||||||
|
return ed25519.VerifyingKey(public_base64, encoding='base64')
|
||||||
|
|
||||||
|
|
||||||
|
def ed25519_generate_key_pair():
|
||||||
|
"""
|
||||||
|
Generate a new key pair and return the pair encoded in base58
|
||||||
|
"""
|
||||||
|
sk, vk = ed25519.create_keypair()
|
||||||
|
# Private key
|
||||||
|
private_value_base58 = base58.b58encode(sk.to_bytes())
|
||||||
|
|
||||||
|
# Public key
|
||||||
|
public_value_compressed_base58 = base58.b58encode(vk.to_bytes())
|
||||||
|
|
||||||
|
return (private_value_base58, public_value_compressed_base58)
|
||||||
161
bigchaindb/crypto/fulfillment.py
Normal file
161
bigchaindb/crypto/fulfillment.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import base64
|
||||||
|
import re
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
from bigchaindb.crypto.condition import Condition
|
||||||
|
from bigchaindb.crypto.iostream import Writer, base64_remove_padding, Reader, base64_add_padding
|
||||||
|
|
||||||
|
FULFILLMENT_REGEX = r'^cf:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]+$'
|
||||||
|
|
||||||
|
|
||||||
|
class Fulfillment(metaclass=ABCMeta):
|
||||||
|
_bitmask = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_uri(serialized_fulfillment):
|
||||||
|
"""
|
||||||
|
Create a Fulfillment object from a URI.
|
||||||
|
|
||||||
|
This method will parse a fulfillment URI and construct a corresponding Fulfillment object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
serialized_fulfillment (str): URI representing the fulfillment
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Fulfillment: Resulting object
|
||||||
|
"""
|
||||||
|
if not isinstance(serialized_fulfillment, string_types):
|
||||||
|
raise TypeError('Serialized fulfillment must be a string')
|
||||||
|
|
||||||
|
pieces = serialized_fulfillment.split(':')
|
||||||
|
if not pieces[0] == 'cf':
|
||||||
|
raise ValueError('Serialized fulfillment must start with "cf:"')
|
||||||
|
|
||||||
|
if not pieces[1] == '1':
|
||||||
|
raise ValueError('Fulfillment must be version 1')
|
||||||
|
|
||||||
|
if not re.match(FULFILLMENT_REGEX, serialized_fulfillment):
|
||||||
|
raise ValueError('Invalid fulfillment format')
|
||||||
|
|
||||||
|
bitmask = int(pieces[2])
|
||||||
|
|
||||||
|
from bigchaindb.crypto.bitmark_registry import BitmaskRegistry
|
||||||
|
|
||||||
|
cls = BitmaskRegistry.get_class_from_typebit(bitmask)
|
||||||
|
fulfillment = cls()
|
||||||
|
|
||||||
|
payload = Reader.from_source(
|
||||||
|
base64.urlsafe_b64decode(
|
||||||
|
base64_add_padding(pieces[3])))
|
||||||
|
|
||||||
|
fulfillment.parse_payload(payload)
|
||||||
|
|
||||||
|
return fulfillment
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bitmask(self):
|
||||||
|
"""
|
||||||
|
Return the bitmask of this fulfillment.
|
||||||
|
|
||||||
|
For simple fulfillment types this is simply the bit representing this type.
|
||||||
|
|
||||||
|
For meta-fulfillments, these are the bits representing the types of the subconditions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Bitmask corresponding to this fulfillment.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._bitmask
|
||||||
|
|
||||||
|
@property
|
||||||
|
def condition(self):
|
||||||
|
"""
|
||||||
|
Generate condition corresponding to this fulfillment.
|
||||||
|
|
||||||
|
An important property of crypto-conditions is that the condition can always
|
||||||
|
be derived from the fulfillment. This makes it very easy to post
|
||||||
|
fulfillments to a system without having to specify which condition the
|
||||||
|
relate to. The system can keep an index of conditions and look up any
|
||||||
|
matching events related to that condition.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Condition: Condition corresponding to this fulfillment.
|
||||||
|
|
||||||
|
"""
|
||||||
|
condition = Condition()
|
||||||
|
condition.bitmask = self.bitmask
|
||||||
|
condition.hash = self.generate_hash()
|
||||||
|
condition.max_fulfillment_length = self.calculate_max_fulfillment_length()
|
||||||
|
return condition
|
||||||
|
|
||||||
|
def serialize_uri(self):
|
||||||
|
"""
|
||||||
|
Generate the URI form encoding of this fulfillment.
|
||||||
|
|
||||||
|
Turns the fulfillment into a URI containing only URL-safe characters. This
|
||||||
|
format is convenient for passing around fulfillments in URLs, JSON and
|
||||||
|
other text-based formats.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
string: Fulfillment as a URI
|
||||||
|
"""
|
||||||
|
return 'cf:1:{}:{}'.format(self.bitmask,
|
||||||
|
base64_remove_padding(
|
||||||
|
base64.urlsafe_b64encode(
|
||||||
|
b''.join(self.serialize_payload().components)
|
||||||
|
)
|
||||||
|
).decode('utf-8'))
|
||||||
|
|
||||||
|
def serialize_payload(self):
|
||||||
|
"""
|
||||||
|
Return the fulfillment payload as a buffer.
|
||||||
|
|
||||||
|
Note that the fulfillment payload is not the standard format for passing
|
||||||
|
fulfillments in binary protocols. Use `serializeBinary` for that. The
|
||||||
|
fulfillment payload is purely the type-specific data and does not include the bitmask.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Buffer: Fulfillment payload
|
||||||
|
"""
|
||||||
|
return self.write_payload(Writer())
|
||||||
|
|
||||||
|
def calculate_max_fulfillment_length(self):
|
||||||
|
"""
|
||||||
|
Calculate the maximum length of the fulfillment payload.
|
||||||
|
|
||||||
|
This implementation works by measuring the length of the fulfillment.
|
||||||
|
Condition types that do not have a constant length will override this
|
||||||
|
method with one that calculates the maximum possible length.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
{Number} Maximum fulfillment length
|
||||||
|
"""
|
||||||
|
# TODO: Predictor
|
||||||
|
# predictor = Predictor()
|
||||||
|
predictor = None
|
||||||
|
self.write_payload(predictor)
|
||||||
|
return predictor.size
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def write_payload(self, writer):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_payload(self, reader):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate_hash(self):
|
||||||
|
"""
|
||||||
|
Generate the hash of the fulfillment.
|
||||||
|
|
||||||
|
This method is a stub and will be overridden by subclasses.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate(self):
|
||||||
|
raise NotImplementedError
|
||||||
0
bigchaindb/crypto/fulfillments/__init__.py
Normal file
0
bigchaindb/crypto/fulfillments/__init__.py
Normal file
25
bigchaindb/crypto/fulfillments/base_sha256.py
Normal file
25
bigchaindb/crypto/fulfillments/base_sha256.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from bigchaindb.crypto.fulfillment import Fulfillment
|
||||||
|
from bigchaindb.crypto.iostream import Hasher
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSha256Fulfillment(Fulfillment):
|
||||||
|
|
||||||
|
def generate_hash(self):
|
||||||
|
"""
|
||||||
|
Calculate condition hash.
|
||||||
|
|
||||||
|
This method is called internally by `condition`. It calculates the
|
||||||
|
condition hash by hashing the hash payload.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Buffer: Result from hashing the hash payload.
|
||||||
|
"""
|
||||||
|
hasher = Hasher('sha256')
|
||||||
|
self.write_hash_payload(hasher)
|
||||||
|
return hasher.digest() # remove padding
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def write_hash_payload(self, hasher):
|
||||||
|
raise NotImplementedError
|
||||||
254
bigchaindb/crypto/fulfillments/ed25519_sha256.py
Normal file
254
bigchaindb/crypto/fulfillments/ed25519_sha256.py
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import base58
|
||||||
|
|
||||||
|
from bigchaindb.crypto.ed25519 import ED25519PublicKey
|
||||||
|
from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment
|
||||||
|
from bigchaindb.crypto.iostream import Predictor
|
||||||
|
|
||||||
|
|
||||||
|
class Ed25519Sha256Fulfillment(BaseSha256Fulfillment):
|
||||||
|
|
||||||
|
_bitmask = 0x08
|
||||||
|
_message_prefix = None
|
||||||
|
_max_dynamic_message_length = None
|
||||||
|
_public_key = None
|
||||||
|
_message = None
|
||||||
|
_signature = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message_prefix(self):
|
||||||
|
return self._message_prefix
|
||||||
|
|
||||||
|
@message_prefix.setter
|
||||||
|
def message_prefix(self, value):
|
||||||
|
"""
|
||||||
|
Set the fixed message prefix.
|
||||||
|
|
||||||
|
The fixed prefix is the portion of the message that is determined when the
|
||||||
|
condition is first created.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Buffer): Static portion of the message
|
||||||
|
"""
|
||||||
|
if not isinstance(value, bytes):
|
||||||
|
value = value.encode()
|
||||||
|
self._message_prefix = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_dynamic_message_length(self):
|
||||||
|
return self._max_dynamic_message_length
|
||||||
|
|
||||||
|
@max_dynamic_message_length.setter
|
||||||
|
def max_dynamic_message_length(self, value):
|
||||||
|
"""
|
||||||
|
Set the maximum length of the dynamic message component.
|
||||||
|
|
||||||
|
The dynamic message is the part of the signed message that is determined at
|
||||||
|
fulfillment time. However, when the condition is first created, we need to
|
||||||
|
know the maximum fulfillment length, which in turn requires us to put a
|
||||||
|
limit on the length of the dynamic message component.
|
||||||
|
|
||||||
|
If this method is not called, the maximum dynamic message length defaults to zero.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (int): Maximum length in bytes
|
||||||
|
"""
|
||||||
|
self._max_dynamic_message_length = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_key(self):
|
||||||
|
return self._public_key
|
||||||
|
|
||||||
|
@public_key.setter
|
||||||
|
def public_key(self, value):
|
||||||
|
"""
|
||||||
|
Set the public publicKey.
|
||||||
|
|
||||||
|
This is the Ed25519 public key. It has to be provided as a buffer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Buffer): publicKey Public Ed25519 publicKey
|
||||||
|
"""
|
||||||
|
# TODO: Buffer
|
||||||
|
# if not isinstance(value, Buffer):
|
||||||
|
# raise ValueError("public key must be a Buffer")
|
||||||
|
if not isinstance(value, ED25519PublicKey):
|
||||||
|
raise TypeError
|
||||||
|
self._public_key = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return self._message
|
||||||
|
|
||||||
|
@message.setter
|
||||||
|
def message(self, value):
|
||||||
|
"""
|
||||||
|
Set the dynamic message portion.
|
||||||
|
|
||||||
|
Part of the signed message (the suffix) can be determined when the condition is being fulfilled.
|
||||||
|
|
||||||
|
Length may not exceed the maximum dynamic message length.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Buffer): Binary form of dynamic message.
|
||||||
|
"""
|
||||||
|
# TODO: Buffer
|
||||||
|
# if not isinstance(value, Buffer):
|
||||||
|
# raise ValueError("message must be a Buffer")
|
||||||
|
if not isinstance(value, bytes):
|
||||||
|
value = value.encode()
|
||||||
|
self._message = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signature(self):
|
||||||
|
return self._signature
|
||||||
|
|
||||||
|
@signature.setter
|
||||||
|
def signature(self, value):
|
||||||
|
"""
|
||||||
|
Set the signature.
|
||||||
|
|
||||||
|
Instead of using the private key to sign using the sign() method, we can also generate the signature elsewhere
|
||||||
|
and pass it in.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Buffer): 64-byte signature.
|
||||||
|
"""
|
||||||
|
# TODO: Buffer
|
||||||
|
# if not isinstance(value, Buffer):
|
||||||
|
# raise ValueError("signature must be a Buffer")
|
||||||
|
self._signature = value
|
||||||
|
|
||||||
|
def write_common_header(self, writer):
|
||||||
|
"""
|
||||||
|
Write static header fields.
|
||||||
|
|
||||||
|
Some fields are common between the hash and the fulfillment payload. This
|
||||||
|
method writes those field to anything implementing the Writer interface.
|
||||||
|
It is used internally when generating the hash of the condition, when
|
||||||
|
generating the fulfillment payload and when calculating the maximum fulfillment size.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
writer (Writer|Hasher|Predictor): Target for outputting the header.
|
||||||
|
"""
|
||||||
|
if not self.public_key:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
writer.write_var_bytes(bytearray(self.public_key.public_key.to_bytes()))
|
||||||
|
writer.write_var_bytes(self.message_prefix)
|
||||||
|
writer.write_var_uint(self.max_dynamic_message_length)
|
||||||
|
return writer
|
||||||
|
|
||||||
|
def parse_payload(self, reader):
|
||||||
|
"""
|
||||||
|
Parse the payload of an Ed25519 fulfillment.
|
||||||
|
|
||||||
|
Read a fulfillment payload from a Reader and populate this object with that fulfillment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reader (Reader): Source to read the fulfillment payload from.
|
||||||
|
"""
|
||||||
|
self.public_key = \
|
||||||
|
ED25519PublicKey(
|
||||||
|
base58.b58encode(
|
||||||
|
reader.read_var_bytes()))
|
||||||
|
self.message_prefix = reader.read_var_bytes()
|
||||||
|
self.max_dynamic_message_length = reader.read_var_uint()
|
||||||
|
self.message = reader.read_var_bytes()
|
||||||
|
self.signature = reader.read_var_bytes()
|
||||||
|
print(self.signature)
|
||||||
|
|
||||||
|
def write_payload(self, writer):
|
||||||
|
"""
|
||||||
|
Generate the fulfillment payload.
|
||||||
|
|
||||||
|
This writes the fulfillment payload to a Writer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
writer (Writer): Subject for writing the fulfillment payload.
|
||||||
|
"""
|
||||||
|
self.write_common_header(writer)
|
||||||
|
writer.write_var_bytes(self.message)
|
||||||
|
writer.write_var_bytes(self.signature)
|
||||||
|
return writer
|
||||||
|
|
||||||
|
def write_hash_payload(self, hasher):
|
||||||
|
"""
|
||||||
|
Generate the contents of the condition hash.
|
||||||
|
|
||||||
|
Writes the contents of the condition hash to a Hasher. Used internally by `condition`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hasher (Hasher): Destination where the hash payload will be written.
|
||||||
|
"""
|
||||||
|
hasher.write_var_uint(self.bitmask)
|
||||||
|
return self.write_common_header(hasher)
|
||||||
|
|
||||||
|
def calculate_max_fulfillment_length(self):
|
||||||
|
"""
|
||||||
|
Calculates the longest possible fulfillment length.
|
||||||
|
|
||||||
|
The longest fulfillment for an Ed25519 condition is the length of a
|
||||||
|
fulfillment where the dynamic message length equals its maximum length.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Maximum length of the fulfillment payload
|
||||||
|
"""
|
||||||
|
|
||||||
|
predictor = Predictor()
|
||||||
|
|
||||||
|
if not self.public_key:
|
||||||
|
raise ValueError('Requires a public key')
|
||||||
|
|
||||||
|
# Calculate the length that the common header would have
|
||||||
|
self.write_common_header(predictor)
|
||||||
|
|
||||||
|
# Message suffix
|
||||||
|
predictor.write_var_uint(self.max_dynamic_message_length)
|
||||||
|
predictor.skip(self.max_dynamic_message_length)
|
||||||
|
|
||||||
|
# Signature
|
||||||
|
predictor.write_var_bytes(self.public_key.public_key.to_bytes())
|
||||||
|
|
||||||
|
return predictor.size
|
||||||
|
|
||||||
|
def sign(self, private_key):
|
||||||
|
"""
|
||||||
|
Sign the message.
|
||||||
|
|
||||||
|
This method will take the currently configured values for the message
|
||||||
|
prefix and suffix and create a signature using the provided Ed25519 private key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
private_key (string) Ed25519 private key
|
||||||
|
"""
|
||||||
|
# TODO: Buffer
|
||||||
|
sk = private_key
|
||||||
|
vk = ED25519PublicKey(
|
||||||
|
base58.b58encode(
|
||||||
|
sk.private_key.get_verifying_key().to_bytes()))
|
||||||
|
|
||||||
|
self.public_key = vk
|
||||||
|
|
||||||
|
message = self.message_prefix + self.message
|
||||||
|
|
||||||
|
# This would be the Ed25519ph version (JavaScript ES7):
|
||||||
|
# const message = crypto.createHash('sha512')
|
||||||
|
# .update(Buffer.concat([this.messagePrefix, this.message]))
|
||||||
|
# .digest()
|
||||||
|
|
||||||
|
self.signature = sk.sign(message, encoding=None)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
"""
|
||||||
|
Verify the signature of this Ed25519 fulfillment.
|
||||||
|
|
||||||
|
The signature of this Ed25519 fulfillment is verified against the provided message and public key.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
boolean: Whether this fulfillment is valid.
|
||||||
|
"""
|
||||||
|
if not (self.message and self.signature):
|
||||||
|
return False
|
||||||
|
|
||||||
|
message = self.message_prefix + self.message
|
||||||
|
return self.public_key.verify(message, self.signature, encoding=None)
|
||||||
437
bigchaindb/crypto/iostream.py
Normal file
437
bigchaindb/crypto/iostream.py
Normal 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
|
||||||
|
MAX_SAFE_INTEGER = 2**53-1
|
||||||
|
|
||||||
|
|
||||||
|
class UnsignedLEB128:
|
||||||
|
"""
|
||||||
|
Adapter for DWARF unsigned LEB128.
|
||||||
|
|
||||||
|
A VARUINT is a variable length integer encoded as base128 where the highest
|
||||||
|
bit indicates that another byte is following. The first byte contains the
|
||||||
|
seven least significant bits of the number represented.
|
||||||
|
|
||||||
|
see: https://en.wikipedia.org/wiki/LEB128 (ULEB128)
|
||||||
|
see http://grokbase.com/t/python/python-list/112e5jpc16/encoding
|
||||||
|
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def encode(obj):
|
||||||
|
out = []
|
||||||
|
value = int(obj)
|
||||||
|
while value > INT:
|
||||||
|
out.append((value & REST) | MSB)
|
||||||
|
value /= 7
|
||||||
|
|
||||||
|
while value & MSBALL:
|
||||||
|
out.append((value & REST) | MSB)
|
||||||
|
value >>= 7
|
||||||
|
|
||||||
|
out.append(value | 0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(obj):
|
||||||
|
value = 0
|
||||||
|
for b in reversed(obj):
|
||||||
|
value = value * 128 + (ord(b) & 0x7F)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class Writer:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.components = []
|
||||||
|
|
||||||
|
def write_var_uint(self, value):
|
||||||
|
"""
|
||||||
|
Write a VARUINT to the stream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (int): Integer to represent.
|
||||||
|
"""
|
||||||
|
out = UnsignedLEB128.encode(value)
|
||||||
|
self.write(out)
|
||||||
|
|
||||||
|
def write_var_bytes(self, buffer):
|
||||||
|
"""
|
||||||
|
Write a VARBYTES.
|
||||||
|
|
||||||
|
A VARBYTES field consists of a VARUINT followed by that many bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer (Buffer): Contents of the VARBYTES.
|
||||||
|
"""
|
||||||
|
self.write_var_uint(len(buffer))
|
||||||
|
self.write(buffer)
|
||||||
|
|
||||||
|
def write(self, in_bytes):
|
||||||
|
"""
|
||||||
|
Write a series of raw bytes.
|
||||||
|
|
||||||
|
Adds the given bytes to the output buffer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
in_bytes (Buffer): Bytes to write.
|
||||||
|
"""
|
||||||
|
out = in_bytes
|
||||||
|
if isinstance(out, (list, bytearray)):
|
||||||
|
out = binascii.unhexlify(''.join('{:02x}'.format(x) for x in out))
|
||||||
|
if not isinstance(out, bytes):
|
||||||
|
out = out.encode('utf-8')
|
||||||
|
self.components.append(out)
|
||||||
|
|
||||||
|
|
||||||
|
class Hasher(Writer):
|
||||||
|
|
||||||
|
hash = None
|
||||||
|
|
||||||
|
def __init__(self, algorithm):
|
||||||
|
if algorithm == 'sha256':
|
||||||
|
self.hash = hashlib.sha256()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
super(Writer, self).__init__()
|
||||||
|
|
||||||
|
def write(self, in_bytes):
|
||||||
|
"""
|
||||||
|
Adds bytes to the hash input.
|
||||||
|
|
||||||
|
The hasher will pass these bytes into the hashing function. By overriding
|
||||||
|
the Writer class and implementing this method, the Hasher supports any of
|
||||||
|
the datatypes that a Writer can write.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
in_bytes (Buffer): Bytes to add to the hash.
|
||||||
|
"""
|
||||||
|
out = in_bytes
|
||||||
|
if isinstance(out, (list, bytearray)):
|
||||||
|
out = binascii.unhexlify(''.join('{:02x}'.format(x) for x in out))
|
||||||
|
if not isinstance(out, bytes):
|
||||||
|
out = out.encode('utf-8')
|
||||||
|
self.hash.update(out)
|
||||||
|
|
||||||
|
def digest(self):
|
||||||
|
"""
|
||||||
|
Return the hash.
|
||||||
|
|
||||||
|
Returns the finished hash based on what has been written to the Hasher so far.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Buffer: Resulting hash.
|
||||||
|
"""
|
||||||
|
return self.hash.digest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def length(algorithm):
|
||||||
|
"""
|
||||||
|
Get digest length for hashing algorithm.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algorithm (string): Hashing algorithm identifier.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
int: Digest length in bytes.
|
||||||
|
"""
|
||||||
|
return len(Hasher(algorithm).digest())
|
||||||
|
|
||||||
|
|
||||||
|
class Predictor:
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
def write_var_uint(self, val):
|
||||||
|
"""
|
||||||
|
Calculate the size of a VARUINT.
|
||||||
|
|
||||||
|
A VARUINT is a variable length integer encoded as base128 where the highest
|
||||||
|
bit indicates that another byte is following. The first byte contains the
|
||||||
|
seven least significant bits of the number represented.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
val (int): Integer to be encoded
|
||||||
|
"""
|
||||||
|
|
||||||
|
if val == 0:
|
||||||
|
self.size += 1
|
||||||
|
elif val < 0:
|
||||||
|
raise ValueError('Variable length integer cannot be negative')
|
||||||
|
elif val > MAX_SAFE_INTEGER:
|
||||||
|
raise ValueError('Variable length integer too large')
|
||||||
|
else:
|
||||||
|
# Calculate number of bits divided by seven
|
||||||
|
self.size += ceil(len('{:02b}'.format(val)) / 7)
|
||||||
|
|
||||||
|
def write_var_bytes(self, val):
|
||||||
|
"""
|
||||||
|
Calculate the size of a VARBYTES.
|
||||||
|
|
||||||
|
A VARBYTES field consists of a VARUINT followed by that many bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
val (varbytes): Contents for VARBYTES
|
||||||
|
"""
|
||||||
|
self.write_var_uint(len(val))
|
||||||
|
self.size += len(val)
|
||||||
|
|
||||||
|
def skip(self, in_bytes):
|
||||||
|
"""
|
||||||
|
Add this many bytes to the predicted size.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
in_bytes (int): Number of bytes to pretend to write.
|
||||||
|
"""
|
||||||
|
self.size += in_bytes
|
||||||
|
|
||||||
|
|
||||||
|
class Reader:
|
||||||
|
|
||||||
|
def __init__(self, buffer):
|
||||||
|
self.buffer = buffer
|
||||||
|
self.cursor = 0
|
||||||
|
self.bookmarks = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_source(source):
|
||||||
|
"""
|
||||||
|
Create a Reader from a source of bytes.
|
||||||
|
|
||||||
|
Currently, this method only allows the creation of a Reader from a Buffer.
|
||||||
|
|
||||||
|
If the object provided is already a Reader, that reader is returned as is.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source (Reader|Buffer): Source of binary data.
|
||||||
|
Return:
|
||||||
|
Reader: Instance of Reader
|
||||||
|
"""
|
||||||
|
# if (Buffer.isBuffer(source)) {
|
||||||
|
# return new Reader(source)
|
||||||
|
# } else {
|
||||||
|
# throw new Error('Reader must be given a Buffer')
|
||||||
|
if isinstance(source, Reader):
|
||||||
|
return source
|
||||||
|
return Reader(source)
|
||||||
|
|
||||||
|
def bookmark(self):
|
||||||
|
"""
|
||||||
|
Store the current cursor position on a stack.
|
||||||
|
"""
|
||||||
|
self.bookmarks.append(self.cursor)
|
||||||
|
|
||||||
|
def restore(self):
|
||||||
|
"""
|
||||||
|
Pop the most recently bookmarked cursor position off the stack.
|
||||||
|
"""
|
||||||
|
self.cursor = self.bookmarks.pop()
|
||||||
|
|
||||||
|
def ensure_available(self, num_bytes):
|
||||||
|
"""
|
||||||
|
Ensure this number of bytes is buffered.
|
||||||
|
|
||||||
|
This method checks that the given number of bytes is buffered and available
|
||||||
|
for reading. If insufficient bytes are available, the method throws an `OverflowError`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_bytes (int): Number of bytes that should be available.
|
||||||
|
"""
|
||||||
|
if len(self.buffer) < self.cursor + num_bytes:
|
||||||
|
raise OverflowError('Tried to read {} bytes, but only {} bytes available'
|
||||||
|
.format(num_bytes, len(self.buffer.length) - self.cursor))
|
||||||
|
|
||||||
|
def read_uint8(self):
|
||||||
|
"""
|
||||||
|
Read a single unsigned 8 byte integer.
|
||||||
|
|
||||||
|
Return: {Number} Contents of next byte.
|
||||||
|
"""
|
||||||
|
self.ensure_available(1)
|
||||||
|
value = self.buffer[self.cursor]
|
||||||
|
self.cursor += 1
|
||||||
|
return value
|
||||||
|
|
||||||
|
def peek_uint8(self):
|
||||||
|
"""
|
||||||
|
Look at the next byte, but don't advance the cursor.
|
||||||
|
|
||||||
|
Return: {Number} Contents of the next byte.
|
||||||
|
"""
|
||||||
|
self.ensure_available(1)
|
||||||
|
return self.buffer.read_uint8(self.cursor)
|
||||||
|
|
||||||
|
def skip_uint8(self):
|
||||||
|
"""
|
||||||
|
Advance cursor by one byte.
|
||||||
|
"""
|
||||||
|
self.cursor += 1
|
||||||
|
|
||||||
|
def read_var_uint(self):
|
||||||
|
"""
|
||||||
|
Read a VARUINT at the cursor position.
|
||||||
|
|
||||||
|
A VARUINT is a variable length integer encoded as base128 where the highest
|
||||||
|
bit indicates that another byte is following. The first byte contains the
|
||||||
|
seven least significant bits of the number represented.
|
||||||
|
|
||||||
|
Return the VARUINT and advances the cursor accordingly.
|
||||||
|
|
||||||
|
Return: {Number} Value of the VARUINT.
|
||||||
|
"""
|
||||||
|
shift = 0
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
in_byte = self.read_uint8()
|
||||||
|
|
||||||
|
result += (in_byte & REST) << shift if shift < 28 else (in_byte & REST) * (2 ** shift)
|
||||||
|
|
||||||
|
shift += 7
|
||||||
|
|
||||||
|
# Don't allow numbers greater than Number.MAX_SAFE_INTEGER
|
||||||
|
if shift > 45:
|
||||||
|
raise ValueError('Too large variable integer')
|
||||||
|
|
||||||
|
if not (in_byte & MSB):
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def peek_var_uint(self):
|
||||||
|
"""
|
||||||
|
Read the next VARUINT, but don't advance the cursor.
|
||||||
|
|
||||||
|
Return: {Number} VARUINT at the cursor position.
|
||||||
|
"""
|
||||||
|
self.bookmark()
|
||||||
|
value = self.read_var_uint()
|
||||||
|
self.restore()
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def skip_var_uint(self):
|
||||||
|
"""
|
||||||
|
Skip past the VARUINT at the cursor position.
|
||||||
|
"""
|
||||||
|
# Read variable integer and ignore output
|
||||||
|
self.read_var_uint()
|
||||||
|
|
||||||
|
def read_var_bytes(self):
|
||||||
|
"""
|
||||||
|
Read a VARBYTES.
|
||||||
|
|
||||||
|
A VARBYTES field consists of a VARUINT followed by that many bytes.
|
||||||
|
|
||||||
|
Return: {Buffer} Contents of the VARBYTES.
|
||||||
|
"""
|
||||||
|
return self.read(self.read_var_uint())
|
||||||
|
|
||||||
|
def peek_var_bytes(self):
|
||||||
|
"""
|
||||||
|
Read a VARBYTES, but do not advance cursor position.
|
||||||
|
|
||||||
|
Return: {Buffer} Contents of the VARBYTES.
|
||||||
|
"""
|
||||||
|
self.bookmark()
|
||||||
|
value = self.read_var_bytes()
|
||||||
|
self.restore()
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def skip_var_bytes(self):
|
||||||
|
"""
|
||||||
|
Skip a VARBYTES.
|
||||||
|
"""
|
||||||
|
self.skip(len(self.read_var_bytes()))
|
||||||
|
|
||||||
|
def read(self, num_bytes):
|
||||||
|
"""
|
||||||
|
Read a given number of bytes.
|
||||||
|
|
||||||
|
Returns this many bytes starting at the cursor position and advances the
|
||||||
|
cursor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_bytes (int): Number of bytes to read.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Contents of bytes read.
|
||||||
|
"""
|
||||||
|
self.ensure_available(num_bytes)
|
||||||
|
|
||||||
|
value = self.buffer[self.cursor:self.cursor + num_bytes]
|
||||||
|
self.cursor += num_bytes
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def peek(self, num_bytes):
|
||||||
|
"""
|
||||||
|
Read bytes, but do not advance cursor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_bytes (int): Number of bytes to read.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Contents of bytes read.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.ensure_available(num_bytes)
|
||||||
|
|
||||||
|
return self.buffer.slice(self.cursor, self.cursor + num_bytes)
|
||||||
|
|
||||||
|
def skip(self, num_bytes):
|
||||||
|
"""
|
||||||
|
Skip a number of bytes.
|
||||||
|
|
||||||
|
Advances the cursor by this many bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_bytes (int): Number of bytes to advance the cursor by.
|
||||||
|
"""
|
||||||
|
self.ensure_available(num_bytes)
|
||||||
|
|
||||||
|
self.cursor += num_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def base64_add_padding(data):
|
||||||
|
"""
|
||||||
|
Add enough padding for base64 encoding such that length is a multiple of 4
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: unpadded string or bytes
|
||||||
|
Return:
|
||||||
|
bytes: The padded bytes
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(data, string_types):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
missing_padding = 4 - len(data) % 4
|
||||||
|
if missing_padding:
|
||||||
|
data += b'=' * missing_padding
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def base64_remove_padding(data):
|
||||||
|
"""
|
||||||
|
Remove padding from base64 encoding
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: fully padded base64 data
|
||||||
|
Return:
|
||||||
|
base64: Unpadded base64 bytes
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(data, string_types):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
return data.replace(b'=', b'')
|
||||||
@ -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 PrivateKey, PublicKey, hash_data
|
||||||
|
|
||||||
|
|
||||||
class ProcessGroup(object):
|
class ProcessGroup(object):
|
||||||
|
|||||||
1
setup.py
1
setup.py
@ -76,6 +76,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
0
tests/crypto/__init__.py
Normal file
121
tests/crypto/test_crypto.py
Normal file
121
tests/crypto/test_crypto.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
from bigchaindb.crypto.ecdsa import ECDSAPrivateKey, ECDSAPublicKey, ecdsa_generate_key_pair
|
||||||
|
from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey, ed25519_generate_key_pair
|
||||||
|
|
||||||
|
|
||||||
|
class TestBigchainCryptoED25519(object):
|
||||||
|
PRIVATE_B64 = b'xSrGKGxeJYVlVrk84f29wcPazoTV+y8fzM7P0iFsSdg='
|
||||||
|
PRIVATE_BYTES = b'\xc5*\xc6(l^%\x85eV\xb9<\xe1\xfd\xbd\xc1\xc3\xda\xce\x84\xd5\xfb/\x1f\xcc\xce\xcf\xd2!lI\xd8v\x08\xfb\x03\x15y/&\xcd^O\xa9\xb8\xd2\x8a\x89\x8d\xf94\x9b\xbe\xb1\xe7\xdb~\x95!o\xde\xa2{\xa5'
|
||||||
|
PRIVATE_B58 = 'EGf9UJzryLpZaBguyf5f4QAefFnairNbHLkhht8BZ57m'
|
||||||
|
|
||||||
|
PUBLIC_B64 = b'dgj7AxV5LybNXk+puNKKiY35NJu+sefbfpUhb96ie6U='
|
||||||
|
PUBLIC_BYTES = b'v\x08\xfb\x03\x15y/&\xcd^O\xa9\xb8\xd2\x8a\x89\x8d\xf94\x9b\xbe\xb1\xe7\xdb~\x95!o\xde\xa2{\xa5'
|
||||||
|
PUBLIC_B58 = '8wm3wiqsoujkDJvk8FMZkHijb9eZdUqMuZsnRee4eRz4'
|
||||||
|
|
||||||
|
PUBLIC_B64_ILP = 'Lvf3YtnHLMER+VHT0aaeEJF+7WQcvp4iKZAdvMVto7c='
|
||||||
|
MSG_SHA512_ILP = 'claZQU7qkFz7smkAVtQp9ekUCc5LgoeN9W3RItIzykNEDbGSvzeHvOk9v/vrPpm+XWx5VFjd/sVbM2SLnCpxLw=='
|
||||||
|
SIG_B64_ILP = 'sd0RahwuJJgeNfg8HvWHtYf4uqNgCOqIbseERacqs8G0kXNQQnhfV6gWAnMb+0RIlY3e0mqbrQiUwbRYJvRBAw=='
|
||||||
|
|
||||||
|
def test_private_key_encode(self):
|
||||||
|
private_value_base58 = ED25519PrivateKey.encode(self.PRIVATE_B64)
|
||||||
|
assert private_value_base58 == self.PRIVATE_B58
|
||||||
|
|
||||||
|
def test_private_key_init(self):
|
||||||
|
sk = ED25519PrivateKey(self.PRIVATE_B58)
|
||||||
|
assert sk.private_key.to_ascii(encoding='base64') == self.PRIVATE_B64[:-1]
|
||||||
|
assert sk.private_key.to_bytes() == self.PRIVATE_BYTES
|
||||||
|
|
||||||
|
def test_private_key_decode(self):
|
||||||
|
private_value = ED25519PrivateKey.decode(self.PRIVATE_B58)
|
||||||
|
assert private_value == self.PRIVATE_B64
|
||||||
|
|
||||||
|
def test_public_key_encode(self):
|
||||||
|
public_value_base58 = ED25519PublicKey.encode(self.PUBLIC_B64)
|
||||||
|
assert public_value_base58 == self.PUBLIC_B58
|
||||||
|
|
||||||
|
def test_public_key_init(self):
|
||||||
|
vk = ED25519PublicKey(self.PUBLIC_B58)
|
||||||
|
assert vk.public_key.to_ascii(encoding='base64') == self.PUBLIC_B64[:-1]
|
||||||
|
assert vk.public_key.to_bytes() == self.PUBLIC_BYTES
|
||||||
|
|
||||||
|
def test_public_key_decode(self):
|
||||||
|
public_value = ED25519PublicKey.decode(self.PUBLIC_B58)
|
||||||
|
assert public_value == self.PUBLIC_B64
|
||||||
|
|
||||||
|
def test_sign_verify(self):
|
||||||
|
message = 'Hello World!'
|
||||||
|
sk = ED25519PrivateKey(self.PRIVATE_B58)
|
||||||
|
vk = ED25519PublicKey(self.PUBLIC_B58)
|
||||||
|
assert vk.verify(message, sk.sign(message)) is True
|
||||||
|
assert vk.verify(message, sk.sign(message + 'dummy')) is False
|
||||||
|
assert vk.verify(message + 'dummy', sk.sign(message)) is False
|
||||||
|
vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP))
|
||||||
|
assert vk.verify(message, sk.sign(message)) is False
|
||||||
|
|
||||||
|
def test_valid_condition_valid_signature_ilp(self):
|
||||||
|
vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP))
|
||||||
|
msg = self.MSG_SHA512_ILP
|
||||||
|
sig = self.SIG_B64_ILP
|
||||||
|
assert vk.verify(base64.b64decode(msg), base64.b64decode(sig), encoding=None) is True
|
||||||
|
|
||||||
|
def test_valid_condition_invalid_signature_ilp(self):
|
||||||
|
vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP))
|
||||||
|
msg = self.MSG_SHA512_ILP
|
||||||
|
sig = self.MSG_SHA512_ILP
|
||||||
|
assert vk.verify(base64.b64decode(msg), base64.b64decode(sig), encoding=None) is False
|
||||||
|
|
||||||
|
def test_generate_key_pair(self):
|
||||||
|
sk, vk = ed25519_generate_key_pair()
|
||||||
|
assert ED25519PrivateKey.encode(ED25519PrivateKey.decode(sk)) == sk
|
||||||
|
assert ED25519PublicKey.encode(ED25519PublicKey.decode(vk)) == vk
|
||||||
|
|
||||||
|
def test_generate_sign_verify(self):
|
||||||
|
sk, vk = ed25519_generate_key_pair()
|
||||||
|
sk = ED25519PrivateKey(sk)
|
||||||
|
vk = ED25519PublicKey(vk)
|
||||||
|
message = 'Hello World!'
|
||||||
|
assert vk.verify(message, sk.sign(message)) is True
|
||||||
|
assert vk.verify(message, sk.sign(message + 'dummy')) is False
|
||||||
|
assert vk.verify(message + 'dummy', sk.sign(message)) is False
|
||||||
|
vk = ED25519PublicKey(ED25519PublicKey.encode(self.PUBLIC_B64_ILP))
|
||||||
|
assert vk.verify(message, sk.sign(message)) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestBigchainCryptoECDSA(object):
|
||||||
|
PRIVATE_VALUE = 64328150571824492670917070117568709277186368319388887463636481841106388379832
|
||||||
|
PUBLIC_VALUE_X = 48388170575736684074633245566225141536152842355597159440179742847497614196929
|
||||||
|
PUBLIC_VALUE_Y = 65233479152484407841598798165960909560839872511163322973341535484598825150846
|
||||||
|
|
||||||
|
PRIVATE_VALUE_B58 = 'AaAp4xBavbe6VGeQF2mWdSKNM1r6HfR2Z1tAY6aUkwdq'
|
||||||
|
PUBLIC_VALUE_COMPRESSED_B58 = 'ifEi3UuTDT4CqUUKiS5omgeDodhu2aRFHVp6LoahbEVe'
|
||||||
|
|
||||||
|
def test_private_key_encode(self):
|
||||||
|
private_value_base58 = ECDSAPrivateKey.encode(self.PRIVATE_VALUE)
|
||||||
|
assert private_value_base58 == self.PRIVATE_VALUE_B58
|
||||||
|
|
||||||
|
def test_private_key_decode(self):
|
||||||
|
private_value = ECDSAPrivateKey.decode(self.PRIVATE_VALUE_B58)
|
||||||
|
assert private_value == self.PRIVATE_VALUE
|
||||||
|
|
||||||
|
def test_public_key_encode(self):
|
||||||
|
public_value_compressed_base58 = ECDSAPublicKey.encode(self.PUBLIC_VALUE_X, self.PUBLIC_VALUE_Y)
|
||||||
|
assert public_value_compressed_base58 == self.PUBLIC_VALUE_COMPRESSED_B58
|
||||||
|
|
||||||
|
def test_public_key_decode(self):
|
||||||
|
public_value_x, public_value_y = ECDSAPublicKey.decode(self.PUBLIC_VALUE_COMPRESSED_B58)
|
||||||
|
assert public_value_x == self.PUBLIC_VALUE_X
|
||||||
|
assert public_value_y == self.PUBLIC_VALUE_Y
|
||||||
|
|
||||||
|
def test_sign_verify(self):
|
||||||
|
message = 'Hello World!'
|
||||||
|
public_key = ECDSAPublicKey(self.PUBLIC_VALUE_COMPRESSED_B58)
|
||||||
|
private_key = ECDSAPrivateKey(self.PRIVATE_VALUE_B58)
|
||||||
|
assert public_key.verify(message, private_key.sign(message)) is True
|
||||||
|
|
||||||
|
def test_generate_key_pair(self):
|
||||||
|
private_value_base58, public_value_compressed_base58 = ecdsa_generate_key_pair()
|
||||||
|
assert ECDSAPrivateKey.encode(
|
||||||
|
ECDSAPrivateKey.decode(private_value_base58)) == private_value_base58
|
||||||
|
assert ECDSAPublicKey.encode(
|
||||||
|
*ECDSAPublicKey.decode(public_value_compressed_base58)) == public_value_compressed_base58
|
||||||
115
tests/crypto/test_fulfillment.py
Normal file
115
tests/crypto/test_fulfillment.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import binascii
|
||||||
|
|
||||||
|
from bigchaindb.crypto.condition import Condition
|
||||||
|
from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey
|
||||||
|
from bigchaindb.crypto.fulfillment import Fulfillment
|
||||||
|
from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment
|
||||||
|
|
||||||
|
|
||||||
|
class TestBigchainILPFulfillmentEd25519Sha256:
|
||||||
|
PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf'
|
||||||
|
PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8'
|
||||||
|
PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU'
|
||||||
|
|
||||||
|
PRIVATE_HEX_ILP = b'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42'
|
||||||
|
PRIVATE_B64_ILP = b'gz/mJAkje51i7HdYdSCRHpp1nOwdGXVbfakBuW3KPUI'
|
||||||
|
PRIVATE_B58_ILP = '9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs'
|
||||||
|
|
||||||
|
CONDITION_ED25519_ILP = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113'
|
||||||
|
FULFILLMENT_ED25519_ILP = \
|
||||||
|
'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531' \
|
||||||
|
'PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI'
|
||||||
|
HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5'
|
||||||
|
|
||||||
|
def test_ilp_keys(self):
|
||||||
|
sk = ED25519PrivateKey(self.PRIVATE_B58_ILP)
|
||||||
|
assert sk.private_key.to_ascii(encoding='base64') == self.PRIVATE_B64_ILP
|
||||||
|
assert binascii.hexlify(sk.private_key.to_bytes()[:32]) == self.PRIVATE_HEX_ILP
|
||||||
|
|
||||||
|
vk = ED25519PublicKey(self.PUBLIC_B58_ILP)
|
||||||
|
assert vk.public_key.to_ascii(encoding='base64') == self.PUBLIC_B64_ILP
|
||||||
|
assert binascii.hexlify(vk.public_key.to_bytes()) == self.PUBLIC_HEX_ILP
|
||||||
|
|
||||||
|
def test_serialize_condition_and_validate_fulfillment(self):
|
||||||
|
sk = ED25519PrivateKey(self.PRIVATE_B58_ILP)
|
||||||
|
vk = ED25519PublicKey(self.PUBLIC_B58_ILP)
|
||||||
|
|
||||||
|
fulfillment = Ed25519Sha256Fulfillment()
|
||||||
|
fulfillment.public_key = vk
|
||||||
|
fulfillment.message_prefix = 'Hello world!'
|
||||||
|
fulfillment.max_dynamic_message_length = 32 # defaults to 0
|
||||||
|
|
||||||
|
assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP
|
||||||
|
assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP
|
||||||
|
|
||||||
|
fulfillment.message = ' Conditions are here!'
|
||||||
|
|
||||||
|
# ED25519-SHA256 condition not fulfilled
|
||||||
|
assert fulfillment.validate() == False
|
||||||
|
|
||||||
|
# Fulfill an ED25519-SHA256 condition
|
||||||
|
fulfillment.sign(sk)
|
||||||
|
|
||||||
|
assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP
|
||||||
|
assert fulfillment.validate()
|
||||||
|
|
||||||
|
def test_deserialize_condition(self):
|
||||||
|
|
||||||
|
deserialized_condition = Condition.from_uri(self.CONDITION_ED25519_ILP)
|
||||||
|
|
||||||
|
assert deserialized_condition.serialize_uri() == self.CONDITION_ED25519_ILP
|
||||||
|
assert binascii.hexlify(deserialized_condition.hash) == self.HASH_ED25519_HEX_ILP
|
||||||
|
|
||||||
|
def test_serialize_deserialize_condition(self):
|
||||||
|
vk = ED25519PublicKey(self.PUBLIC_B58_ILP)
|
||||||
|
|
||||||
|
fulfillment = Ed25519Sha256Fulfillment()
|
||||||
|
fulfillment.public_key = vk
|
||||||
|
fulfillment.message_prefix = 'Hello world!'
|
||||||
|
fulfillment.max_dynamic_message_length = 32
|
||||||
|
|
||||||
|
condition = fulfillment.condition
|
||||||
|
deserialized_condition = Condition.from_uri(condition.serialize_uri())
|
||||||
|
|
||||||
|
assert deserialized_condition.bitmask == condition.bitmask
|
||||||
|
assert deserialized_condition.hash == condition.hash
|
||||||
|
assert deserialized_condition.max_fulfillment_length == condition.max_fulfillment_length
|
||||||
|
assert deserialized_condition.serialize_uri() == condition.serialize_uri()
|
||||||
|
|
||||||
|
def test_deserialize_fulfillment(self):
|
||||||
|
fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP)
|
||||||
|
|
||||||
|
assert fulfillment.serialize_uri() == self.FULFILLMENT_ED25519_ILP
|
||||||
|
assert fulfillment.condition.serialize_uri() == self.CONDITION_ED25519_ILP
|
||||||
|
assert binascii.hexlify(fulfillment.condition.hash) == self.HASH_ED25519_HEX_ILP
|
||||||
|
assert fulfillment.public_key.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP
|
||||||
|
assert fulfillment.validate()
|
||||||
|
|
||||||
|
def test_serializer_deserialize_fulfillment(self):
|
||||||
|
sk = ED25519PrivateKey(self.PRIVATE_B58_ILP)
|
||||||
|
vk = ED25519PublicKey(self.PUBLIC_B58_ILP)
|
||||||
|
|
||||||
|
fulfillment = Ed25519Sha256Fulfillment()
|
||||||
|
fulfillment.public_key = vk
|
||||||
|
fulfillment.message_prefix = 'Hello world!'
|
||||||
|
fulfillment.max_dynamic_message_length = 32 # defaults to 0
|
||||||
|
fulfillment.message = ' Conditions are here!'
|
||||||
|
fulfillment.sign(sk)
|
||||||
|
|
||||||
|
assert fulfillment.validate()
|
||||||
|
|
||||||
|
deserialized_fulfillment = Fulfillment.from_uri(fulfillment.serialize_uri())
|
||||||
|
assert deserialized_fulfillment.serialize_uri() == fulfillment.serialize_uri()
|
||||||
|
assert deserialized_fulfillment.condition.serialize_uri() == fulfillment.condition.serialize_uri()
|
||||||
|
assert deserialized_fulfillment.public_key.public_key.to_bytes() == fulfillment.public_key.public_key.to_bytes()
|
||||||
|
assert deserialized_fulfillment.validate()
|
||||||
|
|
||||||
|
|
||||||
|
class TestBigchainILPConditionSha256:
|
||||||
|
|
||||||
|
CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1'
|
||||||
|
|
||||||
|
def test_deserialize_condition(self):
|
||||||
|
example_condition = self.CONDITION_SHA256_ILP
|
||||||
|
condition = Condition.from_uri(example_condition)
|
||||||
|
assert condition.serialize_uri() == self.CONDITION_SHA256_ILP
|
||||||
@ -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 PrivateKey, PublicKey, generate_key_pair, hash_data
|
||||||
|
from bigchaindb.voter import Voter
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(reason='Some tests throw a ResourceWarning that might result in some weird '
|
@pytest.mark.skipif(reason='Some tests throw a ResourceWarning that might result in some weird '
|
||||||
@ -408,45 +407,6 @@ class TestBlockValidation(object):
|
|||||||
assert b.is_valid_block(block)
|
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):
|
||||||
|
|||||||
@ -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 PublicKey, 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):
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user