From 6da6bdc863abb7ece4044618fb32d1080b6b195f Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Thu, 24 Dec 2020 01:55:58 -0300 Subject: [PATCH 1/3] adds jsonrpc_channel_sign signing api --- lbry/extras/daemon/daemon.py | 36 +++++++++++++++++++ .../blockchain/test_claim_commands.py | 20 ++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 27d4f152a..f2ab2184c 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -21,6 +21,8 @@ import base58 from aiohttp import web from prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter from google.protobuf.message import DecodeError + +from lbry.crypto.hash import sha256 from lbry.wallet import ( Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic, Transaction, Output, Input, Account, database @@ -2788,6 +2790,40 @@ class Daemon(metaclass=JSONRPCServerType): return tx + @requires(WALLET_COMPONENT) + async def jsonrpc_channel_sign( + self, channel_name=None, channel_id=None, hexdata=None, channel_account_id=None, wallet_id=None): + """ + Signs data using the specified channel signing key. + + Usage: + channel_sign [ | --channel_name=] + [ | --channel_id=] [ | --hexdata=] + [--channel_account_id=...] [--wallet_id=] + + Options: + --channel_name= : (str) name of channel used to sign (or use channel id) + --channel_id= : (str) claim id of channel used to sign (or use channel name) + --hexdata= : (str) data to sign, encoded as hexadecimal + --channel_account_id=: (str) one or more account ids for accounts to look in + for channel certificates, defaults to all accounts. + --wallet_id= : (str) restrict operation to specific wallet + + Returns: {} + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first." + signing_channel = await self.get_channel_or_error( + wallet, channel_account_id, channel_id, channel_name, for_signing=True + ) + digest = sha256(unhexlify(hexdata)) + signature = signing_channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) + return { + "signature": hexlify(signature), + "digest": hexlify(digest), + "signing_channel": signing_channel + } + @requires(WALLET_COMPONENT) async def jsonrpc_channel_abandon( self, claim_id=None, txid=None, nout=None, account_id=None, wallet_id=None, diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py index a9c0591ec..f6b044050 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -5,11 +5,13 @@ import asyncio from binascii import unhexlify from urllib.request import urlopen +import ecdsa + from lbry.error import InsufficientFundsError from lbry.extras.daemon.daemon import DEFAULT_PAGE_SIZE from lbry.testcase import CommandTestCase -from lbry.wallet.transaction import Transaction +from lbry.wallet.transaction import Transaction, Output from lbry.wallet.util import satoshis_to_coins as lbc @@ -1004,6 +1006,22 @@ class ChannelCommands(CommandTestCase): self.assertItemCount(await self.daemon.jsonrpc_channel_list(account_id=self.account.id), 2) self.assertItemCount(await self.daemon.jsonrpc_channel_list(account_id=account2_id), 1) + async def test_sign_hex_encoded_data(self): + data_to_sign = "CAFEBABE" + # claim new name + await self.channel_create('@someotherchan') + channel_tx = await self.channel_create('@signer') + channel_id = self.get_claim_id(channel_tx) + signature1 = await self.out(self.daemon.jsonrpc_channel_sign(channel_name='@signer', hexdata=data_to_sign)) + signature2 = await self.out(self.daemon.jsonrpc_channel_sign(channel_id=channel_id, hexdata=data_to_sign)) + self.assertEqual(signature2, signature1) + key = unhexlify(channel_tx['outputs'][0]['value']['public_key']) + signature = signature1["signature"] + r = int(signature[:int(len(signature)/2)], 16) + s = int(signature[int(len(signature)/2):], 16) + signature = ecdsa.util.sigencode_der(r, s, len(signature)*4) + self.assertTrue(Output.is_signature_valid(signature, unhexlify(signature1["digest"]), key)) + async def test_channel_export_import_before_sending_channel(self): # export tx = await self.channel_create('@foo', '1.0') From 0a53ad57219969412d453bff4929551576dfa501 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Thu, 24 Dec 2020 02:55:47 -0300 Subject: [PATCH 2/3] use the comment api signing implementation --- lbry/extras/daemon/comment_client.py | 47 ++++++++++--------- lbry/extras/daemon/daemon.py | 9 +--- .../blockchain/test_claim_commands.py | 21 ++++----- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/lbry/extras/daemon/comment_client.py b/lbry/extras/daemon/comment_client.py index 434e41f75..3791f91f6 100644 --- a/lbry/extras/daemon/comment_client.py +++ b/lbry/extras/daemon/comment_client.py @@ -26,42 +26,45 @@ def is_comment_signed_by_channel(comment: dict, channel: Output, sign_comment_id if isinstance(channel, Output): try: signing_field = comment['comment_id'] if sign_comment_id else comment['comment'] - pieces = [ - comment['signing_ts'].encode(), - cid2hash(comment['channel_id']), - signing_field.encode() - ] - return Output.is_signature_valid( - get_encoded_signature(comment['signature']), - sha256(b''.join(pieces)), - channel.claim.channel.public_key_bytes - ) + return verify(channel, signing_field.encode(), comment) except KeyError: pass return False +def verify(channel, data, signature): + pieces = [ + signature['signing_ts'].encode(), + channel.claim_hash, + data + ] + return Output.is_signature_valid( + get_encoded_signature(signature['signature']), + sha256(b''.join(pieces)), + channel.claim.channel.public_key_bytes + ) + + def sign_comment(comment: dict, channel: Output, sign_comment_id=False): - timestamp = str(int(time.time())) signing_field = comment['comment_id'] if sign_comment_id else comment['comment'] - pieces = [timestamp.encode(), channel.claim_hash, signing_field.encode()] + comment.update(sign(channel, signing_field.encode())) + + +def sign(channel, data): + timestamp = str(int(time.time())) + pieces = [timestamp.encode(), channel.claim_hash, data] digest = sha256(b''.join(pieces)) signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) - comment.update({ + return { 'signature': binascii.hexlify(signature).decode(), 'signing_ts': timestamp - }) + } + def sign_reaction(reaction: dict, channel: Output): - timestamp = str(int(time.time())) signing_field = reaction['channel_name'] - pieces = [timestamp.encode(), channel.claim_hash, signing_field.encode()] - digest = sha256(b''.join(pieces)) - signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) - reaction.update({ - 'signature': binascii.hexlify(signature).decode(), - 'signing_ts': timestamp - }) + reaction.update(sign(channel, signing_field.encode())) + async def jsonrpc_post(url: str, method: str, params: dict = None, **kwargs) -> any: params = params or {} diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index f2ab2184c..8b098e451 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -22,7 +22,6 @@ from aiohttp import web from prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter from google.protobuf.message import DecodeError -from lbry.crypto.hash import sha256 from lbry.wallet import ( Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic, Transaction, Output, Input, Account, database @@ -2816,13 +2815,7 @@ class Daemon(metaclass=JSONRPCServerType): signing_channel = await self.get_channel_or_error( wallet, channel_account_id, channel_id, channel_name, for_signing=True ) - digest = sha256(unhexlify(hexdata)) - signature = signing_channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) - return { - "signature": hexlify(signature), - "digest": hexlify(digest), - "signing_channel": signing_channel - } + return comment_client.sign(signing_channel, unhexlify(hexdata)) @requires(WALLET_COMPONENT) async def jsonrpc_channel_abandon( diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py index f6b044050..17a322ed5 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -5,13 +5,12 @@ import asyncio from binascii import unhexlify from urllib.request import urlopen -import ecdsa - from lbry.error import InsufficientFundsError +from lbry.extras.daemon.comment_client import verify from lbry.extras.daemon.daemon import DEFAULT_PAGE_SIZE from lbry.testcase import CommandTestCase -from lbry.wallet.transaction import Transaction, Output +from lbry.wallet.transaction import Transaction from lbry.wallet.util import satoshis_to_coins as lbc @@ -1010,17 +1009,13 @@ class ChannelCommands(CommandTestCase): data_to_sign = "CAFEBABE" # claim new name await self.channel_create('@someotherchan') - channel_tx = await self.channel_create('@signer') - channel_id = self.get_claim_id(channel_tx) + channel_tx = await self.daemon.jsonrpc_channel_create('@signer', '0.1') + await self.confirm_tx(channel_tx.id) + channel = channel_tx.outputs[0] signature1 = await self.out(self.daemon.jsonrpc_channel_sign(channel_name='@signer', hexdata=data_to_sign)) - signature2 = await self.out(self.daemon.jsonrpc_channel_sign(channel_id=channel_id, hexdata=data_to_sign)) - self.assertEqual(signature2, signature1) - key = unhexlify(channel_tx['outputs'][0]['value']['public_key']) - signature = signature1["signature"] - r = int(signature[:int(len(signature)/2)], 16) - s = int(signature[int(len(signature)/2):], 16) - signature = ecdsa.util.sigencode_der(r, s, len(signature)*4) - self.assertTrue(Output.is_signature_valid(signature, unhexlify(signature1["digest"]), key)) + signature2 = await self.out(self.daemon.jsonrpc_channel_sign(channel_id=channel.claim_id, hexdata=data_to_sign)) + self.assertTrue(verify(channel, unhexlify(data_to_sign), signature1)) + self.assertTrue(verify(channel, unhexlify(data_to_sign), signature2)) async def test_channel_export_import_before_sending_channel(self): # export From 2a177872424332d6d2a4f07c861486d2b9c8c738 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Thu, 24 Dec 2020 03:06:57 -0300 Subject: [PATCH 3/3] fix test --- lbry/extras/daemon/comment_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lbry/extras/daemon/comment_client.py b/lbry/extras/daemon/comment_client.py index 3791f91f6..c836978da 100644 --- a/lbry/extras/daemon/comment_client.py +++ b/lbry/extras/daemon/comment_client.py @@ -26,16 +26,16 @@ def is_comment_signed_by_channel(comment: dict, channel: Output, sign_comment_id if isinstance(channel, Output): try: signing_field = comment['comment_id'] if sign_comment_id else comment['comment'] - return verify(channel, signing_field.encode(), comment) + return verify(channel, signing_field.encode(), comment, cid2hash(comment['channel_id'])) except KeyError: pass return False -def verify(channel, data, signature): +def verify(channel, data, signature, channel_hash=None): pieces = [ signature['signing_ts'].encode(), - channel.claim_hash, + channel_hash or channel.claim_hash, data ] return Output.is_signature_valid(