Skip to content
This repository has been archived by the owner on Jun 5, 2019. It is now read-only.

Commit

Permalink
Add cardano support to trezorctl and some tests
Browse files Browse the repository at this point in the history
CR fix
  • Loading branch information
ddeath committed Aug 22, 2018
1 parent b5ef996 commit 5355759
Show file tree
Hide file tree
Showing 7 changed files with 546 additions and 0 deletions.
67 changes: 67 additions & 0 deletions trezorctl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ from trezorlib import protobuf
from trezorlib import stellar
from trezorlib import tools
from trezorlib import ripple
from trezorlib import cardano


class ChoiceType(click.Choice):
Expand Down Expand Up @@ -890,6 +891,72 @@ def ethereum_sign_tx(connect, host, chain_id, address, value, gas_limit, gas_pri
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
#
Expand Down
124 changes: 124 additions & 0 deletions trezorlib/cardano.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# 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 <https://www.gnu.org/licenses/lgpl-3.0.html>.

import base64
import struct
import binascii
from typing import List

from . import messages
from . import tools
from .client import CallException
from .client import field
from .client import expect
from .client import session


REQUIRED_FIELDS_TRANSACTION = ('inputs', 'outputs', 'transactions')
REQUIRED_FIELDS_INPUT = ('path', 'prev_hash', 'prev_index', 'type')


@expect(messages.CardanoAddress)
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'])
)
51 changes: 51 additions & 0 deletions trezorlib/tests/device_tests/test_msg_cardano_get_address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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 <https://www.gnu.org/licenses/lgpl-3.0.html>.

import pytest

from .common import TrezorTest
from binascii import hexlify
from trezorlib.cardano import get_address
from trezorlib.tools import parse_path


@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
class TestMsgCardanoGetAddress(TrezorTest):

def test_cardano_get_address(self):
# data from https://iancoleman.io/bip39/#english
self.setup_mnemonic_nopin_nopassphrase()

address = get_address(self.client, parse_path("m/44'/1815'/0'/0/0'")).address
assert address == 'DdzFFzCqrhst5m97LJq6P8x5ixpQJUAEurzas1kfTiV1uVyx5bKBqhRLjpTNaKKNsyKFCxfcoxVnDkbodL927Fjske2eEuyyXF4McGqd'
address = get_address(self.client, parse_path("m/44'/1815'/0'/0/1'")).address
assert address == 'DdzFFzCqrhstUcpS5DpN2JAJNmkcAcnFFHtHs3QsGykWcjo9jSVXFefSmD3X9JL1VicnGbKwWZMPg9UUjahWa7X6EzEPsqKuwKB79Xyk'
address = get_address(self.client, parse_path("m/44'/1815'/0'/0/2'")).address
assert address == 'DdzFFzCqrhsq4JDAmGAYV5ZvjbEfbJyBWp91BTNY4kDLNiNZxCi8L2CRGN9HgZDH4JZw8KTCcNr17Eqo5WLe9FwxnHQRi53aKph1MZQy'

def test_cardano_get_address_other(self):
# https://github.com/trezor/trezor-core/blob/master/tests/test_apps.cardano.address.py
self.client.load_device_by_mnemonic(
mnemonic='plastic that delay conduct police ticket swim gospel intact harsh obtain entire',
pin='',
passphrase_protection=False,
label='test',
language='english')
address = get_address(self.client, parse_path("m/44'/1815'/0'/0/0'")).address
assert address == 'DdzFFzCqrhtDB6YEgPQqFiVnhKsfyEMe9MLQabhayVUL2WRN1dbLLFS7VfKYBy8n3uemZRcDyqMnv7STCU9vj2eAR8CgFgKMDG2mkQN7'
address = get_address(self.client, parse_path("m/44'/1815'/0'/0/1'")).address
assert address == 'DdzFFzCqrhtCGRQ2UYpcouvRgDnPsAYpmzWVtd5YLvaRrMAMoDmYsKhNMAWePbK7a1XbZ8ghTeyaSLZ2488extnB5F9SwHus4UFaFwkS'
74 changes: 74 additions & 0 deletions trezorlib/tests/device_tests/test_msg_cardano_get_public_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# 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 <https://www.gnu.org/licenses/lgpl-3.0.html>.

import pytest

from .common import TrezorTest
from binascii import hexlify
from trezorlib.cardano import get_public_key
from trezorlib.tools import parse_path


@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
class TestMsgCardanoGetPublicKey(TrezorTest):

def test_cardano_get_public_key(self):
# https://github.com/trezor/trezor-core/blob/master/tests/test_apps.cardano.get_public_key.py
self.client.load_device_by_mnemonic(
mnemonic='plastic that delay conduct police ticket swim gospel intact harsh obtain entire',
pin='',
passphrase_protection=False,
label='test',
language='english')

derivation_paths = [
"m/44'/1815'/0'/0/0'",
"m/44'/1815'",
"m/44'/1815'/0/0/0",
"m/44'/1815'/0'/0/0",
]

root_hd_passphrase = '8ee689a22e1ec569d2ada515c4ee712ad089901b7fe0afb94fe196de944ee814'

public_keys = [
'2df46e04ebf0816e242bfaa1c73e5ebe8863d05d7a96c8aac16f059975e63f30',
'7d1de3f22f53904d007ff833fadd7cd6482ea1e83918b985b4ea33e63c16d183',
'f59a28d704df090d8fc641248bdb27d0d001da13ddb332a79cfba8a9fa7233e7',
'723fdc0eb1300fe7f2b9b6989216a831835a88695ba2c2d5c50c8470b7d1b239',
]

chain_codes = [
'057658de1308930ad4a5663e4f77477014b04954a9d488e62d73b04fc659a35c',
'7a04a6aab0ed12af562a26db4d10344454274d0bfa6e3581df1dc02f13c5fbe5',
'7f01fc65468ed420e135535261b03845d97b9098f8f08245197c9526d80994f6',
'ae09010e921de259b02f34ce7fd76f9c09ad224d436fe8fa38aa212177937ffe',
]

xpub_keys = [
'2df46e04ebf0816e242bfaa1c73e5ebe8863d05d7a96c8aac16f059975e63f30057658de1308930ad4a5663e4f77477014b04954a9d488e62d73b04fc659a35c',
'7d1de3f22f53904d007ff833fadd7cd6482ea1e83918b985b4ea33e63c16d1837a04a6aab0ed12af562a26db4d10344454274d0bfa6e3581df1dc02f13c5fbe5',
'f59a28d704df090d8fc641248bdb27d0d001da13ddb332a79cfba8a9fa7233e77f01fc65468ed420e135535261b03845d97b9098f8f08245197c9526d80994f6',
'723fdc0eb1300fe7f2b9b6989216a831835a88695ba2c2d5c50c8470b7d1b239ae09010e921de259b02f34ce7fd76f9c09ad224d436fe8fa38aa212177937ffe',
]

for index, derivation_path in enumerate(derivation_paths):
key = get_public_key(self.client, parse_path(derivation_path))

assert hexlify(key.node.public_key).decode('utf8') == public_keys[index]
assert hexlify(key.node.chain_code).decode('utf8') == chain_codes[index]
assert key.xpub == xpub_keys[index]
assert key.root_hd_passphrase == root_hd_passphrase
47 changes: 47 additions & 0 deletions trezorlib/tests/device_tests/test_msg_cardano_sign_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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 <https://www.gnu.org/licenses/lgpl-3.0.html>.

import pytest

from .common import TrezorTest
from binascii import hexlify
from trezorlib.cardano import sign_message
from trezorlib.tools import parse_path


@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
class TestMsgCardanoGetPublicKey(TrezorTest):

def test_cardano_get_public_key(self):
# https://github.com/trezor/trezor-core/blob/master/tests/test_apps.cardano.sign_message.py
self.client.load_device_by_mnemonic(
mnemonic='plastic that delay conduct police ticket swim gospel intact harsh obtain entire',
pin='',
passphrase_protection=False,
label='test',
language='english')

messages = [
('Test message to sign', "m/44'/1815'/0'/0/0'", '07f226da2a59c3083e80f01ef7e0ec46fc726ebe6bd15d5e9040031c342d8651bee9aee875019c41a7719674fd417ad43990988ffd371527604b6964df75960d'),
('New Test message to sign', "m/44'/1815'", '8fd3b9d8a4c30326b720de76f8de2bbf57b29b7593576eac4a3017ea23046812017136520dc2f24e9fb4da56bd87c77ea49265686653b36859b5e1e56ba9eb0f'),
('Another Test message to sign', "m/44'/1815'/0/0/0", '89d63bd32c2eb92aa418b9ce0383a7cf489bc56284876c19246b70be72070d83d361fcb136e8e257b7e66029ef4a566405cda0143d251f851debd62c3c38c302'),
('Just another Test message to sign', "m/44'/1815'/0'/0/0", '49d948090d30e35a88a26d8fb07aca5d68936feba2d5bd49e0d0f7c027a0c8c2955b93a7c930a3b36d23c2502c18bf39cf9b17bbba1a0965090acfb4d10a9305'),
]

for (message, derivation_path, expected_signature) in messages:
signature = sign_message(self.client, parse_path(derivation_path), message)
assert expected_signature == hexlify(signature.signature).decode('utf8')
Loading

0 comments on commit 5355759

Please sign in to comment.