diff --git a/lbry/extras/daemon/comment_client.py b/lbry/extras/daemon/comment_client.py index 434e41f75..c836978da 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, cid2hash(comment['channel_id'])) except KeyError: pass return False +def verify(channel, data, signature, channel_hash=None): + pieces = [ + signature['signing_ts'].encode(), + channel_hash or 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 27d4f152a..8b098e451 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -21,6 +21,7 @@ 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.wallet import ( Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic, Transaction, Output, Input, Account, database @@ -2788,6 +2789,34 @@ 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 + ) + return comment_client.sign(signing_channel, unhexlify(hexdata)) + @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..17a322ed5 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -6,6 +6,7 @@ from binascii import unhexlify from urllib.request import urlopen 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 @@ -1004,6 +1005,18 @@ 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.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.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 tx = await self.channel_create('@foo', '1.0')