From bae05e80a068bee066919072345780d0cb02fc6d Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Tue, 28 Feb 2017 11:27:55 +0100 Subject: [PATCH] sign whole transaction body instead of partial transaction --- bigchaindb/common/transaction.py | 86 ++++++++----------- .../source/data-models/transaction-model.rst | 2 +- tests/common/test_transaction.py | 19 ++-- 3 files changed, 44 insertions(+), 63 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 9da2421a..ebef6987 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -686,22 +686,17 @@ class Transaction(object): key_pairs = {gen_public_key(PrivateKey(private_key)): PrivateKey(private_key) for private_key in private_keys} - for index, input_ in enumerate(self.inputs): - # NOTE: We clone the current transaction but only add the output - # and input we're currently working on plus all - # previously signed ones. - tx_partial = Transaction(self.operation, self.asset, [input_], - self.outputs, self.metadata, - self.version) - - tx_partial_dict = tx_partial.to_dict() - tx_partial_dict = Transaction._remove_signatures(tx_partial_dict) - tx_serialized = Transaction._to_str(tx_partial_dict) - self._sign_input(input_, index, tx_serialized, key_pairs) + tx_dict = self.to_dict() + tx_dict = Transaction._remove_signatures(tx_dict) + tx_serialized = Transaction._to_str(tx_dict) + for i, input_ in enumerate(self.inputs): + message = '%s:%s' % (i, tx_serialized) + self.inputs[i] = self._sign_input(input_, message, key_pairs) return self - def _sign_input(self, input_, index, tx_serialized, key_pairs): - """Signs a single Input with a partial Transaction as message. + @classmethod + def _sign_input(cls, input_, message, key_pairs): + """Signs a single Input. Note: This method works only for the following Cryptoconditions @@ -712,31 +707,27 @@ class Transaction(object): Args: input_ (:class:`~bigchaindb.common.transaction. Input`) The Input to be signed. - index (int): The index of the input to be signed. - tx_serialized (str): The Transaction to be used as message. + message (str): The message to be signed key_pairs (dict): The keys to sign the Transaction with. """ if isinstance(input_.fulfillment, Ed25519Fulfillment): - self._sign_simple_signature_fulfillment(input_, index, - tx_serialized, key_pairs) + return cls._sign_simple_signature_fulfillment(input_, message, + key_pairs) elif isinstance(input_.fulfillment, ThresholdSha256Fulfillment): - self._sign_threshold_signature_fulfillment(input_, index, - tx_serialized, - key_pairs) + return cls._sign_threshold_signature_fulfillment(input_, message, + key_pairs) else: raise ValueError("Fulfillment couldn't be matched to " 'Cryptocondition fulfillment type.') - def _sign_simple_signature_fulfillment(self, input_, index, - tx_serialized, key_pairs): + @classmethod + def _sign_simple_signature_fulfillment(cls, input_, message, key_pairs): """Signs a Ed25519Fulfillment. Args: input_ (:class:`~bigchaindb.common.transaction. Input`) The input to be signed. - index (int): The index of the input to be - signed. - tx_serialized (str): The Transaction to be used as message. + message (str): The message to be signed key_pairs (dict): The keys to sign the Transaction with. """ # NOTE: To eliminate the dangers of accidentally signing a condition by @@ -748,23 +739,21 @@ class Transaction(object): try: # cryptoconditions makes no assumptions of the encoding of the # message to sign or verify. It only accepts bytestrings - input_.fulfillment.sign(tx_serialized.encode(), key_pairs[public_key]) + input_.fulfillment.sign(message.encode(), key_pairs[public_key]) except KeyError: raise KeypairMismatchException('Public key {} is not a pair to ' 'any of the private keys' .format(public_key)) - self.inputs[index] = input_ + return input_ - def _sign_threshold_signature_fulfillment(self, input_, index, - tx_serialized, key_pairs): + @classmethod + def _sign_threshold_signature_fulfillment(cls, input_, message, key_pairs): """Signs a ThresholdSha256Fulfillment. Args: input_ (:class:`~bigchaindb.common.transaction. Input`) The Input to be signed. - index (int): The index of the Input to be - signed. - tx_serialized (str): The Transaction to be used as message. + message (str): The message to be signed key_pairs (dict): The keys to sign the Transaction with. """ input_ = deepcopy(input_) @@ -794,8 +783,8 @@ class Transaction(object): # cryptoconditions makes no assumptions of the encoding of the # message to sign or verify. It only accepts bytestrings - subffill.sign(tx_serialized.encode(), private_key) - self.inputs[index] = input_ + subffill.sign(message.encode(), private_key) + return input_ def inputs_valid(self, outputs=None): """Validates the Inputs in the Transaction against given @@ -848,24 +837,19 @@ class Transaction(object): raise ValueError('Inputs and ' 'output_condition_uris must have the same count') - def gen_tx(input_, output, output_condition_uri=None): - """Splits multiple IO Transactions into partial single IO - Transactions. - """ - tx = Transaction(self.operation, self.asset, [input_], - self.outputs, self.metadata, self.version) - tx_dict = tx.to_dict() - tx_dict = Transaction._remove_signatures(tx_dict) - tx_serialized = Transaction._to_str(tx_dict) + tx_dict = self.to_dict() + tx_dict = Transaction._remove_signatures(tx_dict) + tx_serialized = Transaction._to_str(tx_dict) - return self.__class__._input_valid(input_, - self.operation, - tx_serialized, - output_condition_uri) + def validate(i, output_condition_uri=None): + """ Validate input against output condition URI """ + message = '%s:%s' % (i, tx_serialized) - partial_transactions = map(gen_tx, self.inputs, - self.outputs, output_condition_uris) - return all(partial_transactions) + return self._input_valid(self.inputs[i], self.operation, message, + output_condition_uri) + + return all(validate(i, cond) + for i, cond in enumerate(output_condition_uris)) @staticmethod def _input_valid(input_, operation, tx_serialized, output_condition_uri=None): diff --git a/docs/server/source/data-models/transaction-model.rst b/docs/server/source/data-models/transaction-model.rst index 0ce10d03..3610d1fe 100644 --- a/docs/server/source/data-models/transaction-model.rst +++ b/docs/server/source/data-models/transaction-model.rst @@ -49,4 +49,4 @@ Here's some explanation of the contents of a :ref:`transaction `: Later, when we get to the models for the block and the vote, we'll see that both include a signature (from the node which created it). You may wonder why transactions don't have signatures... The answer is that they do! They're just hidden inside the ``fulfillment`` string of each input. A creation transaction is signed by whoever created it. A transfer transaction is signed by whoever currently controls or owns it. -What gets signed? For each input in the transaction, the "fullfillment message" that gets signed includes the ``operation``, ``data``, ``version``, ``id``, corresponding ``condition``, and the fulfillment itself, except with its fulfillment string set to ``null``. The computed signature goes into creating the ``fulfillment`` string of the input. +What gets signed? For each input in the transaction, the "fullfillment message" that gets signed includes the JSON serialized body of the transaction, minus any fulfillment strings, and with "n:" prepended where n is the index of the input being signed. The computed signature goes into creating the ``fulfillment`` string of the input. diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index a2782583..12c71497 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -496,7 +496,8 @@ def test_validate_tx_simple_create_signature(user_input, user_output, user_priv, tx = Transaction(Transaction.CREATE, asset_definition, [user_input], [user_output]) expected = deepcopy(user_output) - expected.fulfillment.sign(str(tx).encode(), PrivateKey(user_priv)) + message = ('0:' + str(tx)).encode() + expected.fulfillment.sign(message, PrivateKey(user_priv)) tx.sign([user_priv]) assert tx.inputs[0].to_dict()['fulfillment'] == \ @@ -513,7 +514,6 @@ def test_invoke_simple_signature_fulfillment_with_invalid_params(utx, with raises(KeypairMismatchException): invalid_key_pair = {'wrong_pub_key': 'wrong_priv_key'} utx._sign_simple_signature_fulfillment(user_input, - 0, 'somemessage', invalid_key_pair) @@ -524,13 +524,11 @@ def test_sign_threshold_with_invalid_params(utx, user_user2_threshold_input, with raises(KeypairMismatchException): utx._sign_threshold_signature_fulfillment(user_user2_threshold_input, - 0, 'somemessage', {user3_pub: user3_priv}) with raises(KeypairMismatchException): user_user2_threshold_input.owners_before = ['somewrongvalue'] utx._sign_threshold_signature_fulfillment(user_user2_threshold_input, - 0, 'somemessage', None) @@ -560,13 +558,11 @@ def test_validate_multiple_inputs(user_input, user_output, user_priv, expected_first = deepcopy(tx) expected_second = deepcopy(tx) - expected_first.inputs = [expected_first.inputs[0]] - expected_second.inputs = [expected_second.inputs[1]] - expected_first_bytes = str(expected_first).encode() + expected_first_bytes = ('0:' + str(tx)).encode() expected_first.inputs[0].fulfillment.sign(expected_first_bytes, PrivateKey(user_priv)) - expected_second_bytes = str(expected_second).encode() + expected_second_bytes = ('1:' + str(tx)).encode() expected_second.inputs[0].fulfillment.sign(expected_second_bytes, PrivateKey(user_priv)) tx.sign([user_priv]) @@ -596,10 +592,11 @@ def test_validate_tx_threshold_create_signature(user_user2_threshold_input, tx = Transaction(Transaction.CREATE, asset_definition, [user_user2_threshold_input], [user_user2_threshold_output]) + message = ('0:' + str(tx)).encode() expected = deepcopy(user_user2_threshold_output) - expected.fulfillment.subconditions[0]['body'].sign(str(tx).encode(), + expected.fulfillment.subconditions[0]['body'].sign(message, PrivateKey(user_priv)) - expected.fulfillment.subconditions[1]['body'].sign(str(tx).encode(), + expected.fulfillment.subconditions[1]['body'].sign(message, PrivateKey(user2_priv)) tx.sign([user_priv, user2_priv]) @@ -861,7 +858,7 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub, expected_input = deepcopy(inputs[0]) expected['id'] = transfer_tx['id'] - expected_input.fulfillment.sign(serialize(expected).encode(), + expected_input.fulfillment.sign(('0:' + serialize(expected)).encode(), PrivateKey(user_priv)) expected_ffill = expected_input.fulfillment.serialize_uri() transfer_ffill = transfer_tx['inputs'][0]['fulfillment']