From 8e615b9ea6cad7635d51e1c8f3dad751c5e055aa Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 14 Mar 2016 19:17:21 -0700 Subject: [PATCH 1/7] Raise InvalidSignature instead of KeyError if no signature present on transaction during verification --- bigchaindb/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigchaindb/util.py b/bigchaindb/util.py index d8ddc9ce..7b6aad84 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -199,7 +199,9 @@ def verify_signature(signed_transaction): if 'assignee' in data: data.pop('assignee') - signature = data.pop('signature') + signature = data.pop('signature', None) + if not signature: return False + public_key_base58 = signed_transaction['transaction']['current_owner'] public_key = PublicKey(public_key_base58) return public_key.verify(serialize(data), signature) From 513ed9c93f5464222c5177c0cb8f8f7cae1f09c1 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 14 Mar 2016 19:19:22 -0700 Subject: [PATCH 2/7] Return 404 Not Found when trying to GET an unknown transaction --- bigchaindb/web/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bigchaindb/web/views.py b/bigchaindb/web/views.py index 53db6f66..d09bbb79 100644 --- a/bigchaindb/web/views.py +++ b/bigchaindb/web/views.py @@ -37,6 +37,7 @@ def get_transaction(tx_id): bigchain = current_app.config['bigchain'] tx = bigchain.get_transaction(tx_id) + if not tx: flask.abort(404) return flask.jsonify(**tx) @@ -51,8 +52,8 @@ def create_transaction(): val = {} - # `force` will try to format the body of the POST request even if the `content-type` header is not - # set to `application/json` + # `force` will try to format the body of the POST request even if the + # `content-type` header is not set to `application/json` tx = request.get_json(force=True) if tx['transaction']['operation'] == 'CREATE': From 78a91d8be9d501cda9b73211040744b8c1211dfb Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 14 Mar 2016 20:03:39 -0700 Subject: [PATCH 3/7] Added validate_transaction function in web API and client. --- bigchaindb/client.py | 18 ++++++++++++++++++ bigchaindb/web/views.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/bigchaindb/client.py b/bigchaindb/client.py index f7b37ad4..5d512ecf 100644 --- a/bigchaindb/client.py +++ b/bigchaindb/client.py @@ -90,6 +90,24 @@ class Client: tx, private_key=self.private_key) return self._push(signed_tx) + def validate(self, tx): + """Validate a transaction object. + + If tx is a `CREATE` transaction, this method will return (True, '') even + without the federation signature as long as the transaction is otherwise + valid. + + Args: + tx (dict): the transaction object to be validated + + Return: + (bool, str): (True, '') if the tx is valid, else (False, errormsg) + """ + + res = requests.post(self.api_endpoint + '/transactions/validate/', + json=tx) + return (res.json()['valid'], res.json()['error']) + def _push(self, tx): """Submit a transaction to the Federation. diff --git a/bigchaindb/web/views.py b/bigchaindb/web/views.py index d09bbb79..589adfb7 100644 --- a/bigchaindb/web/views.py +++ b/bigchaindb/web/views.py @@ -7,7 +7,7 @@ For more information please refer to the documentation in Apiary: import flask from flask import current_app, request, Blueprint -from bigchaindb import util +from bigchaindb import util, exceptions basic_views = Blueprint('basic_views', __name__) @@ -68,3 +68,30 @@ def create_transaction(): return flask.jsonify(**tx) +@basic_views.route('/transactions/validate/', methods=['POST']) +def validate_transaction(): + """API endpoint to validate transactions without pushing them to the + Federation. No federation node signature is required for `CREATE` + transactions. + + Return: + A JSON object with the `valid` field populated with a boolean value + and the `error` field populated with an error message or an empty str + """ + bigchain = current_app.config['bigchain'] + + tx = request.get_json(force=True) + + if tx['transaction']['operation'] == 'CREATE': + tx = util.transform_create(tx) + + try: + bigchain.validate_transaction(tx) + except exceptions.InvalidSignature as e: + # We skipped signing CREATEs with the node's private key, so expect this + if tx['transaction']['operation'] != 'CREATE': + return flask.jsonify({'valid': False, 'error': repr(e)}) + except Exception as e: + return flask.jsonify({'valid': False, 'error': repr(e)}) + + return flask.jsonify({'valid': True, 'error': ''}) From 40e70243a45b48898905f926f2ab9e8415bbe25e Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Tue, 15 Mar 2016 16:54:22 -0700 Subject: [PATCH 4/7] Change with notes to the validation behavior for CREATE transactions: transform and ignore signature if the signature field is not present, otherwise validate the signature as well. --- bigchaindb/web/views.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bigchaindb/web/views.py b/bigchaindb/web/views.py index 589adfb7..3d8f824e 100644 --- a/bigchaindb/web/views.py +++ b/bigchaindb/web/views.py @@ -82,14 +82,21 @@ def validate_transaction(): tx = request.get_json(force=True) - if tx['transaction']['operation'] == 'CREATE': + # Always validate TRANSFER signatures; but only validate CREATE signatures + # if present. + validate_sig = True + + # If a CREATE doesn't have the signature populated, then we treat it as + # an input to the `create` function and transform it. + if tx['transaction']['operation'] == 'CREATE' and 'signature' not in tx: + validate_sig = False tx = util.transform_create(tx) try: bigchain.validate_transaction(tx) except exceptions.InvalidSignature as e: # We skipped signing CREATEs with the node's private key, so expect this - if tx['transaction']['operation'] != 'CREATE': + if validate_sig: return flask.jsonify({'valid': False, 'error': repr(e)}) except Exception as e: return flask.jsonify({'valid': False, 'error': repr(e)}) From 73796c302ff5c60688e24b348263eb8eaf85b539 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Tue, 15 Mar 2016 16:55:06 -0700 Subject: [PATCH 5/7] Stub test for client.validate --- tests/test_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index f5e15cad..a70594cf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -57,3 +57,11 @@ def test_client_can_transfer_assets(mock_requests_post, client): assert util.verify_signature(tx) + +def test_client_can_validate_transaction(mock_requests_post, client): + from bigchaindb import util + + assert client.validate({'valid': True, + 'error': ''}) == (True, '') + assert client.validate({'valid': False, + 'error': 'Some Error'}) == (False, 'Some Error') From e3441aa011ed43ab570ac6cd59512b2a64f08ace Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Tue, 15 Mar 2016 16:55:40 -0700 Subject: [PATCH 6/7] web api transactions/validate/ tests --- tests/web/test_basic_views.py | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index 04a1c292..550cbaaa 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -6,8 +6,38 @@ from bigchaindb import util TX_ENDPOINT = '/api/v1/transactions/' +VALIDATE_ENDPOINT = '/api/v1/transactions/validate/' +@pytest.fixture +def valid_create_transaction(user_public_key): + return util.create_tx( + current_owner=None, + new_owner=user_public_key, + tx_input=None, + operation='CREATE', + payload={ + 'IPFS_key': 'QmfQ5QAjvg4GtA3wg3adpnDJug8ktA1BxurVqBD8rtgVjP', + 'creator': 'Johnathan Plunkett', + 'title': 'The Winds of Plast'}) + + +@pytest.fixture +def valid_transfer_transaction(user_public_key, user_private_key): + # Requires an tx_input param to create a *valid* transfer tx + def make_tx(tx_input): + return util.create_and_sign_tx( + private_key=user_private_key, + current_owner=user_public_key, + new_owner=user_public_key, + tx_input=tx_input, #Fill_me_in + operation='TRANSFER', + payload={ + 'IPFS_key': 'QmfQ5QAjvg4GtA3wg3adpnDJug8ktA1BxurVqBD8rtgVjP', + 'creator': 'Johnathan Plunkett', + 'title': 'The Winds of Plast 2: The Plastening'}) + return make_tx + @pytest.mark.usefixtures('inputs') def test_get_transaction_endpoint(b, client, user_public_key): input_tx = b.get_owned_ids(user_public_key).pop() @@ -40,3 +70,34 @@ def test_post_transfer_transaction_endpoint(b, client): assert res.json['transaction']['current_owner'] == from_keypair[1] assert res.json['transaction']['new_owner'] == to_keypair[1] + +@pytest.mark.usefixtures('inputs') +def test_post_validate_transaction_endpoint(b, client, user_public_key, + valid_create_transaction, + valid_transfer_transaction): + # Validate valid CREATE tx + res = client.post(VALIDATE_ENDPOINT, + data=json.dumps(valid_create_transaction)) + assert res.json['valid'] == True + assert res.json['error'] == '' + + # Validate invalid CREATE tx + valid_create_transaction.update({'signature': 'junk'}) + res = client.post(VALIDATE_ENDPOINT, + data=json.dumps(valid_create_transaction)) + assert res.json['valid'] == False + assert res.json['error'] == \ + "OperationError('Only federation nodes can use the operation `CREATE`',)" + + # Validate valid TRANSFER tx + res = client.post(VALIDATE_ENDPOINT, data=json.dumps( + valid_transfer_transaction(b.get_owned_ids(user_public_key).pop()))) + assert res.json['valid'] == True + assert res.json['error'] == '' + + # Validate invalid TRANSFER tx + res = client.post(VALIDATE_ENDPOINT, data=json.dumps( + valid_transfer_transaction(None))) + assert res.json['valid'] == False + assert res.json['error'] == \ + "ValueError('Only `CREATE` transactions can have null inputs',)" From 5046de270eed28db2480c0cda6ab118708fee7ad Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Tue, 15 Mar 2016 19:22:18 -0700 Subject: [PATCH 7/7] Switch to more compact jsonify argument syntax --- bigchaindb/web/views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bigchaindb/web/views.py b/bigchaindb/web/views.py index 3d8f824e..23d98911 100644 --- a/bigchaindb/web/views.py +++ b/bigchaindb/web/views.py @@ -48,6 +48,7 @@ def create_transaction(): Return: A JSON string containing the data about the transaction. """ + bigchain = current_app.config['bigchain'] val = {} @@ -78,6 +79,7 @@ def validate_transaction(): A JSON object with the `valid` field populated with a boolean value and the `error` field populated with an error message or an empty str """ + bigchain = current_app.config['bigchain'] tx = request.get_json(force=True) @@ -97,8 +99,8 @@ def validate_transaction(): except exceptions.InvalidSignature as e: # We skipped signing CREATEs with the node's private key, so expect this if validate_sig: - return flask.jsonify({'valid': False, 'error': repr(e)}) + return flask.jsonify(valid=False, error=repr(e)) except Exception as e: - return flask.jsonify({'valid': False, 'error': repr(e)}) + return flask.jsonify(valid=False, error=repr(e)) - return flask.jsonify({'valid': True, 'error': ''}) + return flask.jsonify(valid=True, error='')