diff --git a/trezorctl b/trezorctl index 6734bde8f..e1256cc67 100755 --- a/trezorctl +++ b/trezorctl @@ -32,6 +32,7 @@ import click from trezorlib import ( btc, + cardano, coins, cosi, debuglink, @@ -1078,6 +1079,86 @@ def ethereum_sign_tx( return "Signed raw transaction: %s" % tx_hex +# +# ADA functions +# + + +@cli.command(help="Sign Cardano transaction.") +@click.option( + "-f", + "--file", + type=click.File("r"), + required=True, + help="Transaction in JSON format", +) +@click.pass_obj +def cardano_sign_tx(connect, file): + client = connect() + + transaction = json.load(file) + + inputs = [cardano.create_input(input) for input in transaction["inputs"]] + outputs = [cardano.create_output(output) for output in transaction["outputs"]] + transactions = transaction["transactions"] + + signed_transaction = cardano.sign_tx(client, inputs, outputs, transactions) + + return { + "tx_hash": binascii.hexlify(signed_transaction.tx_hash).decode(), + "tx_body": binascii.hexlify(signed_transaction.tx_body).decode(), + } + + +@cli.command(help="Get Cardano address.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def cardano_get_address(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + + return cardano.get_address(client, address_n, show_display) + + +@cli.command(help="Get Cardano public key.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +) +@click.pass_obj +def cardano_get_public_key(connect, address): + client = connect() + address_n = tools.parse_path(address) + + return cardano.get_public_key(client, address_n) + + +@cli.command(help="Sign Cardano message.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +) +@click.option("-m", "--message", required=True, help="String message to sign") +@click.pass_obj +def cardano_sign_message(connect, address, message): + client = connect() + address_n = tools.parse_path(address) + + return cardano.sign_message(client, address_n, message) + + +@cli.command(help="Verify Cardano message.") +@click.option("-p", "--public_key", required=True, help="Public key as hex string") +@click.option("-s", "--signature", required=True, help="Signature as hex string") +@click.option("-m", "--message", required=True, help="String message which was signed") +@click.pass_obj +def cardano_verify_message(connect, public_key, signature, message): + client = connect() + + return cardano.verify_message(client, public_key, signature, message) + + # # NEM functions # diff --git a/trezorlib/cardano.py b/trezorlib/cardano.py new file mode 100644 index 000000000..9af65ad1d --- /dev/null +++ b/trezorlib/cardano.py @@ -0,0 +1,115 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import binascii +from typing import List + +from . import messages, tools +from .tools import CallException, expect, session + +REQUIRED_FIELDS_TRANSACTION = ("inputs", "outputs", "transactions") +REQUIRED_FIELDS_INPUT = ("path", "prev_hash", "prev_index", "type") + + +@expect(messages.CardanoAddress, field="address") +def get_address(client, address_n, show_display=False): + return client.call( + messages.CardanoGetAddress(address_n=address_n, show_display=show_display) + ) + + +@expect(messages.CardanoPublicKey) +def get_public_key(client, address_n): + return client.call(messages.CardanoGetPublicKey(address_n=address_n)) + + +@expect(messages.CardanoMessageSignature) +def sign_message(client, address_n, message): + return client.call( + messages.CardanoSignMessage(address_n=address_n, message=message.encode()) + ) + + +def verify_message(client, public_key, signature, message): + try: + response = client.call( + messages.CardanoVerifyMessage( + public_key=binascii.unhexlify(public_key), + signature=binascii.unhexlify(signature), + message=message.encode(), + ) + ) + + except CallException as error: + response = error + + if isinstance(response, messages.Success): + return True + + return False + + +@session +def sign_tx( + client, + inputs: List[messages.CardanoTxInputType], + outputs: List[messages.CardanoTxOutputType], + transactions: List[bytes], +): + response = client.call( + messages.CardanoSignTx( + inputs=inputs, outputs=outputs, transactions_count=len(transactions) + ) + ) + + while isinstance(response, messages.CardanoTxRequest): + tx_index = response.tx_index + + transaction_data = binascii.unhexlify(transactions[tx_index]) + ack_message = messages.CardanoTxAck(transaction=transaction_data) + response = client.call(ack_message) + + return response + + +def create_input(input) -> messages.CardanoTxInputType: + if not all(input.get(k) is not None for k in REQUIRED_FIELDS_INPUT): + raise ValueError("The input is missing some fields") + + path = input["path"] + + return messages.CardanoTxInputType( + address_n=tools.parse_path(path), + prev_hash=binascii.unhexlify(input["prev_hash"]), + prev_index=input["prev_index"], + type=input["type"], + ) + + +def create_output(output) -> messages.CardanoTxOutputType: + if not output.get("amount") or not (output.get("address") or output.get("path")): + raise ValueError("The output is missing some fields") + + if output.get("path"): + path = output["path"] + + return messages.CardanoTxOutputType( + address_n=tools.parse_path(path), amount=int(output["amount"]) + ) + + return messages.CardanoTxOutputType( + address=output["address"], amount=int(output["amount"]) + ) diff --git a/trezorlib/tests/device_tests/test_msg_cardano_get_address.py b/trezorlib/tests/device_tests/test_msg_cardano_get_address.py new file mode 100644 index 000000000..3330a8e2f --- /dev/null +++ b/trezorlib/tests/device_tests/test_msg_cardano_get_address.py @@ -0,0 +1,53 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + + +import pytest + +from trezorlib.cardano import get_address +from trezorlib.tools import parse_path + +from .common import TrezorTest +from .conftest import TREZOR_VERSION + + +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.xfail(TREZOR_VERSION == 2, reason="T2 support is not yet finished") +class TestMsgCardanoGetAddress(TrezorTest): + @pytest.mark.parametrize( + "path,expected_address", + [ + ( + "m/44'/1815'/0'/0/0", + "Ae2tdPwUPEZLCq3sFv4wVYxwqjMH2nUzBVt1HFr4v87snYrtYq3d3bq2PUQ", + ), + ( + "m/44'/1815'/0'/0/1", + "Ae2tdPwUPEZEY6pVJoyuNNdLp7VbMB7U7qfebeJ7XGunk5Z2eHarkcN1bHK", + ), + ( + "m/44'/1815'/0'/0/2", + "Ae2tdPwUPEZ3gZD1QeUHvAqadAV59Zid6NP9VCR9BG5LLAja9YtBUgr6ttK", + ), + ], + ) + def test_cardano_get_address(self, path, expected_address): + # data from https://iancoleman.io/bip39/#english + self.setup_mnemonic_nopin_nopassphrase() + + address = get_address(self.client, parse_path(path)) + assert address == expected_address diff --git a/trezorlib/tests/device_tests/test_msg_cardano_get_public_key.py b/trezorlib/tests/device_tests/test_msg_cardano_get_public_key.py new file mode 100644 index 000000000..c43e3f366 --- /dev/null +++ b/trezorlib/tests/device_tests/test_msg_cardano_get_public_key.py @@ -0,0 +1,67 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from binascii import hexlify + +import pytest + +from trezorlib.cardano import get_public_key +from trezorlib.tools import parse_path + +from .common import TrezorTest +from .conftest import TREZOR_VERSION + + +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.xfail(TREZOR_VERSION == 2, reason="T2 support is not yet finished") +class TestMsgCardanoGetPublicKey(TrezorTest): + @pytest.mark.parametrize( + "path,public_key,chain_code", + [ + ( + "m/44'/1815'/0'", + "c0fce1839f1a84c4e770293ac2f5e0875141b29017b7f56ab135352d00ad6966", + "07faa161c9f5464315d2855f70fdf1431d5fa39eb838767bf17b69772137452f", + ), + ( + "m/44'/1815'/1'", + "ea5dde31b9f551e08a5b6b2f98b8c42c726f726c9ce0a7072102ead53bd8f21e", + "70f131bb799fd659c997221ad8cae7dcce4e8da701f8101cf15307fd3a3712a1", + ), + ( + "m/44'/1815'/2'", + "076338cee5ab3dae19f06ccaa80e3d4428cf0e1bdc04243e41bba7be63a90da7", + "5dcdf129f6f2d108292e615c4b67a1fc41a64e6a96130f5c981e5e8e046a6cd7", + ), + ( + "m/44'/1815'/3'", + "5f769380dc6fd17a4e0f2d23aa359442a712e5e96d7838ebb91eb020003cccc3", + "1197ea234f528987cbac9817ebc31344395b837a3bb7c2332f87e095e70550a5", + ), + ], + ) + def test_cardano_get_public_key(self, path, public_key, chain_code): + self.setup_mnemonic_allallall() + + root_hd_passphrase = None + + key = get_public_key(self.client, parse_path(path)) + + assert hexlify(key.node.public_key).decode("utf8") == public_key + assert hexlify(key.node.chain_code).decode("utf8") == chain_code + assert key.xpub == public_key + chain_code + assert key.root_hd_passphrase == root_hd_passphrase diff --git a/trezorlib/tests/device_tests/test_msg_cardano_sign_message.py b/trezorlib/tests/device_tests/test_msg_cardano_sign_message.py new file mode 100644 index 000000000..cd1b49c46 --- /dev/null +++ b/trezorlib/tests/device_tests/test_msg_cardano_sign_message.py @@ -0,0 +1,61 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from binascii import hexlify + +import pytest + +from trezorlib.cardano import sign_message +from trezorlib.tools import parse_path + +from .common import TrezorTest +from .conftest import TREZOR_VERSION + + +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.xfail(TREZOR_VERSION == 2, reason="T2 support is not yet finished") +class TestMsgCardanoSignMessage(TrezorTest): + @pytest.mark.parametrize( + "message,path,expected_signature", + [ + ( + "Test message to sign", + "m/44'/1815'/0'/0/0", + "dfb89d2b22c20ac7270e7640f9b27fee030c30d72afc342f83f6cb79a2522e17142597dbfb979462fc9fbf6ea17b4eba3b7cbf582e41b6ac31cb491e7cd1e308", + ), + ( + "New Test message to sign", + "m/44'/1815'/0'/0/1", + "d2c68818859f94138ad28a59aa3419a96394008bd38657fe5e74b299df33e70ff7de1b2091ba4a4351153ce4b6beb7eb7316d917ed9303b9f7de57f76e4e1307", + ), + ( + "Another Test message to sign", + "m/44'/1815'/0'/0/2", + "cfb1a8f76e566d387ed727e3eefbb3a0d280917045f2fc82ff381f296a17344d520c00882bc0656bf04c9e95f8138540d4b6d10ddf34d80e27704d1b0cbd0f05", + ), + ( + "Just another Test message to sign", + "m/44'/1815'/0'/0/3", + "a1aadbea98fc4075affb0e0b166b71934ac19420688b80e2ac2cfe3cf0d66404da19a0ab4a9f23335c080dc4cc76d1fd4fdfbb44289a50707d3fcf122a96060d", + ), + ], + ) + def test_cardano_sign_message(self, message, path, expected_signature): + self.setup_mnemonic_allallall() + + signature = sign_message(self.client, parse_path(path), message) + assert expected_signature == hexlify(signature.signature).decode("utf8") diff --git a/trezorlib/tests/device_tests/test_msg_cardano_sign_transaction.py b/trezorlib/tests/device_tests/test_msg_cardano_sign_transaction.py new file mode 100644 index 000000000..56565f55f --- /dev/null +++ b/trezorlib/tests/device_tests/test_msg_cardano_sign_transaction.py @@ -0,0 +1,113 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import binascii +import time + +import pytest + +from trezorlib import messages +from trezorlib.cardano import create_input, create_output + +from .common import TrezorTest +from .conftest import TREZOR_VERSION + + +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.xfail(TREZOR_VERSION == 2, reason="T2 support is not yet finished") +class TestMsgCardanoSignTx(TrezorTest): + def test_cardano_sign_tx(self): + self.setup_mnemonic_allallall() + + transaction = { + "inputs": [ + { + "path": "m/44'/1815'/0'/0/1", + "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "prev_index": 0, + "type": 0, + } + ], + "outputs": [ + { + "address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2", + "amount": "3003112", + } + ], + "transactions": [ + "839f8200d818582482582008abb575fac4c39d5bf80683f7f0c37e48f4e3d96e37d1f6611919a7241b456600ff9f8282d818582183581cda4da43db3fca93695e71dab839e72271204d28b9d964d306b8800a8a0001a7a6916a51a00305becffa0" + ], + } + + inputs = [create_input(input) for input in transaction["inputs"]] + outputs = [create_output(output) for output in transaction["outputs"]] + transactions = transaction["transactions"] + + self.client.transport.write( + messages.CardanoSignTx( + inputs=inputs, outputs=outputs, transactions_count=len(transactions) + ) + ) + response = self.client.transport.read() + + assert isinstance(response, messages.CardanoTxRequest) + assert response.tx_index == 0 + + # Upload first transaction + transaction_data = binascii.unhexlify(transactions[0]) + ack_message = messages.CardanoTxAck(transaction=transaction_data) + self.client.transport.write(ack_message) + + # Confirm fee + response = self.client.transport.read() + assert isinstance(response, messages.ButtonRequest) + assert response.code == messages.ButtonRequestType.Other + + self.client.debug.press_yes() + self.client.transport.write(messages.ButtonAck()) + time.sleep(1) + + # Confirm Output + response = self.client.transport.read() + assert isinstance(response, messages.ButtonRequest) + assert response.code == messages.ButtonRequestType.Other + + self.client.debug.press_yes() + self.client.transport.write(messages.ButtonAck()) + time.sleep(1) + self.client.debug.swipe_down() + time.sleep(1) + + # Confirm amount + response = self.client.transport.read() + assert isinstance(response, messages.ButtonRequest) + assert response.code == messages.ButtonRequestType.Other + + self.client.debug.press_yes() + self.client.transport.write(messages.ButtonAck()) + + response = self.client.transport.read() + assert isinstance(response, messages.CardanoSignedTx) + + assert ( + binascii.hexlify(response.tx_hash) + == b"799c65e8a2c0b1dc4232611728c09d3f3eb0d811c077f8e9798f84605ef1b23d" + ) + assert ( + binascii.hexlify(response.tx_body) + == b"82839f8200d81858248258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00ff9f8282d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e8ffa0818200d818588582584089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea26308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a6355840312c01c27317415b0b8acc86aa789da877fe7e15c65b7ea4c4565d8739117f5f6d9d38bf5d058f7be809b2b9b06c1d79fc6b20f9a4d76d8c89bae333edf5680c" + ) diff --git a/trezorlib/tests/device_tests/test_msg_cardano_verify_message.py b/trezorlib/tests/device_tests/test_msg_cardano_verify_message.py new file mode 100644 index 000000000..15e3bd5ce --- /dev/null +++ b/trezorlib/tests/device_tests/test_msg_cardano_verify_message.py @@ -0,0 +1,85 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import pytest + +from trezorlib.cardano import verify_message + +from .common import TrezorTest +from .conftest import TREZOR_VERSION + + +@pytest.mark.cardano +@pytest.mark.skip_t1 # T1 support is not planned +@pytest.mark.xfail(TREZOR_VERSION == 2, reason="T2 support is not yet finished") +class TestMsgCardanoVerifyMessage(TrezorTest): + # https://github.com/trezor/trezor-core/blob/master/tests/test_apps.cardano.verify_message.py + @pytest.mark.parametrize( + "message,public_key,signature,expected", + [ + ( + "Test message to sign", + "2df46e04ebf0816e242bfaa1c73e5ebe8863d05d7a96c8aac16f059975e63f30", + "07f226da2a59c3083e80f01ef7e0ec46fc726ebe6bd15d5e9040031c342d8651bee9aee875019c41a7719674fd417ad43990988ffd371527604b6964df75960d", + True, + ), + ( + "New Test message to sign", + "7d1de3f22f53904d007ff833fadd7cd6482ea1e83918b985b4ea33e63c16d183", + "8fd3b9d8a4c30326b720de76f8de2bbf57b29b7593576eac4a3017ea23046812017136520dc2f24e9fb4da56bd87c77ea49265686653b36859b5e1e56ba9eb0f", + True, + ), + ( + "Another Test message to sign", + "f59a28d704df090d8fc641248bdb27d0d001da13ddb332a79cfba8a9fa7233e7", + "89d63bd32c2eb92aa418b9ce0383a7cf489bc56284876c19246b70be72070d83d361fcb136e8e257b7e66029ef4a566405cda0143d251f851debd62c3c38c302", + True, + ), + ( + "Just another Test message to sign", + "723fdc0eb1300fe7f2b9b6989216a831835a88695ba2c2d5c50c8470b7d1b239", + "49d948090d30e35a88a26d8fb07aca5d68936feba2d5bd49e0d0f7c027a0c8c2955b93a7c930a3b36d23c2502c18bf39cf9b17bbba1a0965090acfb4d10a9305", + True, + ), + ( + "Test message to sign fail", + "2df46e04ebf0816e242bfaa1c73e5ebe8863d05d7a96c8aac16f059975e63f30", + "07f226da2a59c3083e80f01ef7e0ec46fc726ebe6bd15d5e9040031c342d8651bee9aee875019c41a7719674fd417ad43990988ffd371527604b6964df75960d", + False, + ), + ( + "New Test message to sign", + "7d1de3f22f53904d007ff833fadd7cd6482ea1e83918b985b4ea33e63c16d183", + "20d3b9d8a4c30326b720de76f8de2bbf57b29b7593576eac4a3017ea23046812017136520dc2f24e9fb4da56bd87c77ea49265686653b36859b5e1e56ba9eb0f", + False, + ), + ( + "Another Test message to sign", + "209a28d704df090d8fc641248bdb27d0d001da13ddb332a79cfba8a9fa7233e7", + "89d63bd32c2eb92aa418b9ce0383a7cf489bc56284876c19246b70be72070d83d361fcb136e8e257b7e66029ef4a566405cda0143d251f851debd62c3c38c302", + False, + ), + ( + "Just another Test message to sign fail", + "223fdc0eb1300fe7f2b9b6989216a831835a88695ba2c2d5c50c8470b7d1b239", + "49d948090d30e35a88a26d8fb07aca5d68936feba2d5bd49e0d0f7c027a0c8c2955b93a7c930a3b36d23c2502c18bf39cf9b17bbba1a0965090acfb4d10a9305", + False, + ), + ], + ) + def test_cardano_verify_message(self, message, public_key, signature, expected): + result = verify_message(self.client, public_key, signature, message) + assert result == expected