From 09ddcc7ac9c722397f138f9cb5eb69ee5c53e5ec Mon Sep 17 00:00:00 2001 From: jmuravsky Date: Thu, 14 Jun 2018 16:28:50 +0200 Subject: [PATCH] cardano: add Cardano currency support --- SConscript.firmware | 1 + SConscript.unix | 1 + .../modtrezorcrypto/modtrezorcrypto-bip32.h | 73 +++++ .../modtrezorcrypto/modtrezorcrypto-ed25519.h | 28 ++ src/apps/cardano/__init__.py | 46 +++ src/apps/cardano/address.py | 104 +++++++ src/apps/cardano/cbor.py | 182 ++++++++++++ src/apps/cardano/get_address.py | 36 +++ src/apps/cardano/get_public_key.py | 57 ++++ src/apps/cardano/sign_message.py | 51 ++++ src/apps/cardano/sign_transaction.py | 263 ++++++++++++++++++ src/apps/cardano/ui/__init__.py | 54 ++++ src/apps/cardano/ui/progress.py | 28 ++ src/apps/cardano/verify_message.py | 36 +++ src/main.py | 3 + tests/test_apps.cardano.address.py | 175 ++++++++++++ tests/test_apps.cardano.cbor.py | 56 ++++ tests/test_apps.cardano.get_public_key.py | 53 ++++ tests/test_apps.cardano.sign_message.py | 25 ++ tests/test_apps.cardano.verify_message.py | 22 ++ tests/test_trezor.crypto.hashlib.blake2b.py | 2 +- vendor/trezor-crypto | 2 +- 22 files changed, 1296 insertions(+), 2 deletions(-) create mode 100644 src/apps/cardano/__init__.py create mode 100644 src/apps/cardano/address.py create mode 100644 src/apps/cardano/cbor.py create mode 100644 src/apps/cardano/get_address.py create mode 100644 src/apps/cardano/get_public_key.py create mode 100644 src/apps/cardano/sign_message.py create mode 100644 src/apps/cardano/sign_transaction.py create mode 100644 src/apps/cardano/ui/__init__.py create mode 100644 src/apps/cardano/ui/progress.py create mode 100644 src/apps/cardano/verify_message.py create mode 100644 tests/test_apps.cardano.address.py create mode 100644 tests/test_apps.cardano.cbor.py create mode 100644 tests/test_apps.cardano.get_public_key.py create mode 100644 tests/test_apps.cardano.sign_message.py create mode 100644 tests/test_apps.cardano.verify_message.py diff --git a/SConscript.firmware b/SConscript.firmware index 189c6090d..697705e19 100644 --- a/SConscript.firmware +++ b/SConscript.firmware @@ -27,6 +27,7 @@ CPPDEFINES_MOD += [ 'RAND_PLATFORM_INDEPENDENT', ('USE_KECCAK', '1'), ('USE_ETHEREUM', '1'), + ('USE_CARDANO', '1'), ('USE_NEM', '1'), ] SOURCE_MOD += [ diff --git a/SConscript.unix b/SConscript.unix index 5f62f7818..237275fea 100644 --- a/SConscript.unix +++ b/SConscript.unix @@ -25,6 +25,7 @@ CPPDEFINES_MOD += [ 'AES_192', ('USE_KECCAK', '1'), ('USE_ETHEREUM', '1'), + ('USE_CARDANO', '1'), ('USE_NEM', '1'), ] SOURCE_MOD += [ diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h b/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h index b4f94a29f..bf877e8e9 100644 --- a/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h @@ -22,6 +22,7 @@ #include "embed/extmod/trezorobj.h" #include "bip32.h" +#include "bip39.h" #include "curves.h" #include "memzero.h" #include "nem.h" @@ -156,6 +157,32 @@ STATIC mp_obj_t mod_trezorcrypto_HDNode_derive(size_t n_args, const mp_obj_t *ar } STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_HDNode_derive_obj, 2, 3, mod_trezorcrypto_HDNode_derive); +/// def derive_cardano(self, index: int) -> None: +/// ''' +/// Derive a BIP0032 child node in place using Cardano algorithm. +/// ''' +STATIC mp_obj_t mod_trezorcrypto_HDNode_derive_cardano(mp_obj_t self, mp_obj_t index) { + mp_obj_HDNode_t *o = MP_OBJ_TO_PTR(self); + uint32_t i = mp_obj_get_int_truncated(index); + uint32_t fp = hdnode_fingerprint(&o->hdnode); + int res; + // same as in derive + if (0 == memcmp(o->hdnode.private_key, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 32)) { + memzero(&o->hdnode, sizeof(o->hdnode)); + mp_raise_ValueError("Failed to derive, private key not set"); + } + // special for cardano + res = hdnode_private_ckd_cardano(&o->hdnode, i); + if (!res) { + memzero(&o->hdnode, sizeof(o->hdnode)); + mp_raise_ValueError("Failed to derive"); + } + o->fingerprint = fp; + + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_HDNode_derive_cardano_obj, mod_trezorcrypto_HDNode_derive_cardano); + /// def derive_path(self, path: List[int]) -> None: /// ''' /// Go through a list of indexes and iteratively derive a child node in place. @@ -289,6 +316,16 @@ STATIC mp_obj_t mod_trezorcrypto_HDNode_private_key(mp_obj_t self) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_HDNode_private_key_obj, mod_trezorcrypto_HDNode_private_key); +/// def private_key_ext(self) -> bytes: +/// ''' +/// Returns a private key extension of the HD node. +/// ''' +STATIC mp_obj_t mod_trezorcrypto_HDNode_private_key_ext(mp_obj_t self) { + mp_obj_HDNode_t *o = MP_OBJ_TO_PTR(self); + return mp_obj_new_bytes(o->hdnode.private_key_extension, sizeof(o->hdnode.private_key_extension)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_HDNode_private_key_ext_obj, mod_trezorcrypto_HDNode_private_key_ext); + /// def public_key(self) -> bytes: /// ''' /// Returns a public key of the HD node. @@ -385,6 +422,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_HDNode_ethereum_pubkeyhash_obj STATIC const mp_rom_map_elem_t mod_trezorcrypto_HDNode_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_derive), MP_ROM_PTR(&mod_trezorcrypto_HDNode_derive_obj) }, + { MP_ROM_QSTR(MP_QSTR_derive_cardano), MP_ROM_PTR(&mod_trezorcrypto_HDNode_derive_cardano_obj) }, { MP_ROM_QSTR(MP_QSTR_derive_path), MP_ROM_PTR(&mod_trezorcrypto_HDNode_derive_path_obj) }, { MP_ROM_QSTR(MP_QSTR_serialize_private), MP_ROM_PTR(&mod_trezorcrypto_HDNode_serialize_private_obj) }, { MP_ROM_QSTR(MP_QSTR_serialize_public), MP_ROM_PTR(&mod_trezorcrypto_HDNode_serialize_public_obj) }, @@ -395,6 +433,7 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_HDNode_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_child_num), MP_ROM_PTR(&mod_trezorcrypto_HDNode_child_num_obj) }, { MP_ROM_QSTR(MP_QSTR_chain_code), MP_ROM_PTR(&mod_trezorcrypto_HDNode_chain_code_obj) }, { MP_ROM_QSTR(MP_QSTR_private_key), MP_ROM_PTR(&mod_trezorcrypto_HDNode_private_key_obj) }, + { MP_ROM_QSTR(MP_QSTR_private_key_ext), MP_ROM_PTR(&mod_trezorcrypto_HDNode_private_key_ext_obj) }, { MP_ROM_QSTR(MP_QSTR_public_key), MP_ROM_PTR(&mod_trezorcrypto_HDNode_public_key_obj) }, { MP_ROM_QSTR(MP_QSTR_address), MP_ROM_PTR(&mod_trezorcrypto_HDNode_address_obj) }, { MP_ROM_QSTR(MP_QSTR_nem_address), MP_ROM_PTR(&mod_trezorcrypto_HDNode_nem_address_obj) }, @@ -462,11 +501,45 @@ STATIC mp_obj_t mod_trezorcrypto_bip32_from_seed(mp_obj_t seed, mp_obj_t curve_n } STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_bip32_from_seed_obj, mod_trezorcrypto_bip32_from_seed); +/// def from_mnemonic_cardano(mnemonic: str) -> bytes: +/// ''' +/// Convert mnemonic to hdnode +// ''' +STATIC mp_obj_t mod_trezorcrypto_bip32_from_mnemonic_cardano(mp_obj_t mnemonic) { + mp_buffer_info_t mnemo; + mp_get_buffer_raise(mnemonic, &mnemo, MP_BUFFER_READ); + HDNode hdnode; + const char *pmnemonic = mnemo.len > 0 ? mnemo.buf : ""; + uint8_t entropy[66]; + int entropy_len = mnemonic_to_entropy(pmnemonic, entropy + 2); + + if (entropy_len == 0) { + mp_raise_ValueError("Invalid mnemonic"); + } + + const int res = hdnode_from_seed_cardano(entropy, entropy_len / 8, &hdnode); + if (!res) { + mp_raise_ValueError("Secret key generation from mnemonic is looping forever"); + }else if(res == -1){ + mp_raise_ValueError("Invalid mnemonic"); + } + + mp_obj_HDNode_t *o = m_new_obj(mp_obj_HDNode_t); + o->base.type = &mod_trezorcrypto_HDNode_type; + o->hdnode = hdnode; + return MP_OBJ_FROM_PTR(o); +} + +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_bip32_from_mnemonic_cardano_obj, + mod_trezorcrypto_bip32_from_mnemonic_cardano); + + STATIC const mp_rom_map_elem_t mod_trezorcrypto_bip32_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_bip32) }, { MP_ROM_QSTR(MP_QSTR_HDNode), MP_ROM_PTR(&mod_trezorcrypto_HDNode_type) }, { MP_ROM_QSTR(MP_QSTR_deserialize), MP_ROM_PTR(&mod_trezorcrypto_bip32_deserialize_obj) }, { MP_ROM_QSTR(MP_QSTR_from_seed), MP_ROM_PTR(&mod_trezorcrypto_bip32_from_seed_obj) }, + { MP_ROM_QSTR(MP_QSTR_from_mnemonic_cardano), MP_ROM_PTR(&mod_trezorcrypto_bip32_from_mnemonic_cardano_obj) }, }; STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_bip32_globals, mod_trezorcrypto_bip32_globals_table); diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h b/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h index 683815f77..e042ca4b1 100644 --- a/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h @@ -91,6 +91,33 @@ STATIC mp_obj_t mod_trezorcrypto_ed25519_sign(size_t n_args, const mp_obj_t *arg } STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_ed25519_sign_obj, 2, 3, mod_trezorcrypto_ed25519_sign); +/// def sign_ext(secret_key: bytes, secret_extension: bytes, message: bytes) -> bytes: +/// ''' +/// Uses secret key to produce the cardano signature of message. +/// ''' +STATIC mp_obj_t mod_trezorcrypto_ed25519_sign_ext(mp_obj_t secret_key, mp_obj_t secret_extension, mp_obj_t message) { + mp_buffer_info_t sk, skext, msg; + mp_get_buffer_raise(secret_key, &sk, MP_BUFFER_READ); + mp_get_buffer_raise(secret_extension, &skext, MP_BUFFER_READ); + mp_get_buffer_raise(message, &msg, MP_BUFFER_READ); + if (sk.len != 32) { + mp_raise_ValueError("Invalid length of secret key"); + } + if (skext.len != 32) { + mp_raise_ValueError("Invalid length of secret key extension"); + } + if (msg.len == 0) { + mp_raise_ValueError("Empty data to sign"); + } + ed25519_public_key pk; + + ed25519_publickey_ext(*(const ed25519_secret_key *)sk.buf, *(const ed25519_secret_key *)skext.buf, pk); + uint8_t out[64]; + ed25519_sign_ext(msg.buf, msg.len, *(const ed25519_secret_key *)sk.buf, *(const ed25519_secret_key *)skext.buf, pk, *(ed25519_signature *)out); + return mp_obj_new_bytes(out, sizeof(out)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_3(mod_trezorcrypto_ed25519_sign_ext_obj, mod_trezorcrypto_ed25519_sign_ext); + /// def verify(public_key: bytes, signature: bytes, message: bytes) -> bool: /// ''' /// Uses public key to verify the signature of the message. @@ -207,6 +234,7 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_ed25519_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_generate_secret), MP_ROM_PTR(&mod_trezorcrypto_ed25519_generate_secret_obj) }, { MP_ROM_QSTR(MP_QSTR_publickey), MP_ROM_PTR(&mod_trezorcrypto_ed25519_publickey_obj) }, { MP_ROM_QSTR(MP_QSTR_sign), MP_ROM_PTR(&mod_trezorcrypto_ed25519_sign_obj) }, + { MP_ROM_QSTR(MP_QSTR_sign_ext), MP_ROM_PTR(&mod_trezorcrypto_ed25519_sign_ext_obj) }, { MP_ROM_QSTR(MP_QSTR_verify), MP_ROM_PTR(&mod_trezorcrypto_ed25519_verify_obj) }, { MP_ROM_QSTR(MP_QSTR_cosi_combine_publickeys), MP_ROM_PTR(&mod_trezorcrypto_ed25519_cosi_combine_publickeys_obj) }, { MP_ROM_QSTR(MP_QSTR_cosi_combine_signatures), MP_ROM_PTR(&mod_trezorcrypto_ed25519_cosi_combine_signatures_obj) }, diff --git a/src/apps/cardano/__init__.py b/src/apps/cardano/__init__.py new file mode 100644 index 000000000..05407fe23 --- /dev/null +++ b/src/apps/cardano/__init__.py @@ -0,0 +1,46 @@ +from trezor.messages.MessageType import ( + CardanoGetAddress, + CardanoGetPublicKey, + CardanoSignMessage, + CardanoSignTransaction, + CardanoVerifyMessage, +) +from trezor.wire import protobuf_workflow, register + + +def dispatch_CardanoGetAddress(*args, **kwargs): + from .get_address import cardano_get_address + + return cardano_get_address(*args, **kwargs) + + +def dispatch_CardanoGetPublicKey(*args, **kwargs): + from .get_public_key import cardano_get_public_key + + return cardano_get_public_key(*args, **kwargs) + + +def dispatch_CardanoSignMessage(*args, **kwargs): + from .sign_message import cardano_sign_message + + return cardano_sign_message(*args, **kwargs) + + +def dispatch_CardanoSignTransaction(*args, **kwargs): + from .sign_transaction import cardano_sign_transaction + + return cardano_sign_transaction(*args, **kwargs) + + +def dispatch_CardanoVerifyMessage(*args, **kwargs): + from .verify_message import cardano_verify_message + + return cardano_verify_message(*args, **kwargs) + + +def boot(): + register(CardanoGetAddress, protobuf_workflow, dispatch_CardanoGetAddress) + register(CardanoGetPublicKey, protobuf_workflow, dispatch_CardanoGetPublicKey) + register(CardanoSignMessage, protobuf_workflow, dispatch_CardanoSignMessage) + register(CardanoVerifyMessage, protobuf_workflow, dispatch_CardanoVerifyMessage) + register(CardanoSignTransaction, protobuf_workflow, dispatch_CardanoSignTransaction) diff --git a/src/apps/cardano/address.py b/src/apps/cardano/address.py new file mode 100644 index 000000000..220f5b65e --- /dev/null +++ b/src/apps/cardano/address.py @@ -0,0 +1,104 @@ +from micropython import const + +from trezor import wire +from trezor.crypto import base58, chacha20poly1305, crc, hashlib, pbkdf2 + +from . import cbor + +from apps.common import HARDENED, seed + + +def validate_derivation_path(path: list): + if len(path) < 2 or len(path) > 5: + raise wire.ProcessError("Derivation path must be composed from 2-5 indices") + + if path[0] != HARDENED | 44 or path[1] != HARDENED | 1815: + raise wire.ProcessError("This is not cardano derivation path") + + return path + + +def _derive_hd_passphrase(node) -> bytes: + iterations = const(500) + length = const(32) + passwd = seed.remove_ed25519_prefix(node.public_key()) + node.chain_code() + x = pbkdf2("hmac-sha512", passwd, b"address-hashing", iterations) + return x.key()[:length] + + +def _address_hash(data) -> bytes: + data = cbor.encode(data) + data = hashlib.sha3_256(data).digest() + res = hashlib.blake2b(data=data, outlen=28).digest() + return res + + +def _get_address_root(node, payload): + extpubkey = seed.remove_ed25519_prefix(node.public_key()) + node.chain_code() + if payload: + payload = {1: cbor.encode(payload)} + else: + payload = {} + return _address_hash([0, [0, extpubkey], payload]) + + +def _encrypt_derivation_path(path: list, hd_passphrase: bytes) -> bytes: + serialized = cbor.encode(cbor.IndefiniteLengthArray(path)) + ctx = chacha20poly1305(hd_passphrase, b"serokellfore") + data = ctx.encrypt(serialized) + tag = ctx.finish() + return data + tag + + +def derive_address_and_node(root_node, path: list): + validate_derivation_path(path) + + derived_node = root_node.clone() + + # this means empty derivation path m/44'/1815' + if len(path) == 2: + address_payload = None + address_attributes = {} + else: + if len(path) == 5: + p = [path[2], path[4]] + else: + p = [path[2]] + for indice in p: + derived_node.derive_cardano(indice) + + hd_passphrase = _derive_hd_passphrase(root_node) + address_payload = _encrypt_derivation_path(p, hd_passphrase) + address_attributes = {1: cbor.encode(address_payload)} + + address_root = _get_address_root(derived_node, address_payload) + address_type = 0 + address_data = [address_root, address_attributes, address_type] + address_data_encoded = cbor.encode(address_data) + + address = base58.encode( + cbor.encode( + [cbor.Tagged(24, address_data_encoded), crc.crc32(address_data_encoded)] + ) + ) + return (address, derived_node) + + +def _break_address_n_to_lines(address_n: list) -> list: + def path_item(i: int): + if i & HARDENED: + return str(i ^ HARDENED) + "'" + else: + return str(i) + + lines = [] + path_str = "m/" + "/".join([path_item(i) for i in address_n]) + + per_line = const(17) + while len(path_str) > per_line: + i = path_str[:per_line].rfind("/") + lines.append(path_str[:i]) + path_str = path_str[i:] + lines.append(path_str) + + return lines diff --git a/src/apps/cardano/cbor.py b/src/apps/cardano/cbor.py new file mode 100644 index 000000000..b8651338c --- /dev/null +++ b/src/apps/cardano/cbor.py @@ -0,0 +1,182 @@ +""" +Minimalistic CBOR implementation, supports only what we need in cardano. +""" + +import ustruct as struct +from micropython import const + +from trezor import log + +_CBOR_TYPE_MASK = const(0xE0) +_CBOR_INFO_BITS = const(0x1F) + +_CBOR_UNSIGNED_INT = const(0b000 << 5) +_CBOR_BYTE_STRING = const(0b010 << 5) +_CBOR_ARRAY = const(0b100 << 5) +_CBOR_MAP = const(0b101 << 5) +_CBOR_TAG = const(0b110 << 5) +_CBOR_PRIMITIVE = const(0b111 << 5) + +_CBOR_UINT8_FOLLOWS = const(0x18) +_CBOR_UINT16_FOLLOWS = const(0x19) +_CBOR_UINT32_FOLLOWS = const(0x1a) +_CBOR_UINT64_FOLLOWS = const(0x1b) +_CBOR_VAR_FOLLOWS = const(0x1f) + +_CBOR_BREAK = const(0x1f) +_CBOR_RAW_TAG = const(0x18) + + +def _header(typ, l: int): + if l < 24: + return struct.pack(">B", typ + l) + elif l < 2 ** 8: + return struct.pack(">BB", typ + 24, l) + elif l < 2 ** 16: + return struct.pack(">BH", typ + 25, l) + elif l < 2 ** 32: + return struct.pack(">BI", typ + 26, l) + elif l < 2 ** 64: + return struct.pack(">BQ", typ + 27, l) + else: + raise NotImplementedError("Length %d not suppported" % l) + + +def _cbor_encode(value): + if isinstance(value, int): + yield _header(_CBOR_UNSIGNED_INT, value) + elif isinstance(value, bytes): + yield _header(_CBOR_BYTE_STRING, len(value)) + yield value + elif isinstance(value, bytearray): + yield _header(_CBOR_BYTE_STRING, len(value)) + yield bytes(value) + elif isinstance(value, list): + # definite-length valued list + yield _header(_CBOR_ARRAY, len(value)) + for x in value: + yield from _cbor_encode(x) + elif isinstance(value, dict): + yield _header(_CBOR_MAP, len(value)) + for k, v in value.items(): + yield from _cbor_encode(k) + yield from _cbor_encode(v) + elif isinstance(value, Tagged): + yield _header(_CBOR_TAG, value.tag) + yield from _cbor_encode(value.value) + elif isinstance(value, IndefiniteLengthArray): + yield bytes([_CBOR_ARRAY + 31]) + for x in value.array: + yield from _cbor_encode(x) + yield bytes([_CBOR_PRIMITIVE + 31]) + elif isinstance(value, Raw): + yield value.value + else: + if __debug__: + log.debug(__name__, "not implemented (encode): %s", type(value)) + raise NotImplementedError() + + +def _read_length(cbor, aux): + if aux == _CBOR_UINT8_FOLLOWS: + return (cbor[0], cbor[1:]) + elif aux == _CBOR_UINT16_FOLLOWS: + res = cbor[1] + res += cbor[0] << 8 + return (res, cbor[2:]) + elif aux == _CBOR_UINT32_FOLLOWS: + res = cbor[3] + res += cbor[2] << 8 + res += cbor[1] << 16 + res += cbor[0] << 24 + return (res, cbor[4:]) + elif aux == _CBOR_UINT64_FOLLOWS: + res = cbor[7] + res += cbor[6] << 8 + res += cbor[5] << 16 + res += cbor[4] << 24 + res += cbor[3] << 32 + res += cbor[2] << 40 + res += cbor[1] << 48 + res += cbor[0] << 56 + return (res, cbor[8:]) + else: + raise NotImplementedError("Length %d not suppported" % aux) + + +def _cbor_decode(cbor): + fb = cbor[0] + data = b"" + fb_type = fb & _CBOR_TYPE_MASK + fb_aux = fb & _CBOR_INFO_BITS + if fb_type == _CBOR_UNSIGNED_INT: + if fb_aux < 0x18: + return (fb_aux, cbor[1:]) + else: + val, data = _read_length(cbor[1:], fb_aux) + return (int(val), data) + elif fb_type == _CBOR_BYTE_STRING: + ln, data = _read_length(cbor[1:], fb_aux) + return (data[0:ln], data[ln:]) + elif fb_type == _CBOR_ARRAY: + if fb_aux == _CBOR_VAR_FOLLOWS: + res = [] + data = cbor[1:] + while True: + item, data = _cbor_decode(data) + if item == _CBOR_PRIMITIVE + _CBOR_BREAK: + break + res.append(item) + return (res, data) + else: + if fb_aux < _CBOR_UINT8_FOLLOWS: + ln = fb_aux + data = cbor[1:] + else: + ln, data = _read_length(cbor[1:], fb_aux) + res = [] + for i in range(ln): + item, data = _cbor_decode(data) + res.append(item) + return (res, data) + elif fb_type == _CBOR_MAP: + return ({}, cbor[1:]) + elif fb_type == _CBOR_TAG: + if cbor[1] == _CBOR_RAW_TAG: # only tag 24 (0x18) is supported + return _cbor_decode(cbor[2:]) + else: + raise NotImplementedError() + elif fb_type == _CBOR_PRIMITIVE: # only break code is supported + return (cbor[0], cbor[1:]) + else: + if __debug__: + log.debug(__name__, "not implemented (decode): %s", cbor[0]) + raise NotImplementedError() + + +class Tagged: + def __init__(self, tag, value): + self.tag = tag + self.value = value + + +class Raw: + def __init__(self, value): + self.value = value + + +class IndefiniteLengthArray: + def __init__(self, array): + assert isinstance(array, list) + self.array = array + + +def encode(value): + return b"".join(_cbor_encode(value)) + + +def decode(cbor: bytes): + res, check = _cbor_decode(cbor) + if not (check == b""): + raise ValueError() + return res diff --git a/src/apps/cardano/get_address.py b/src/apps/cardano/get_address.py new file mode 100644 index 000000000..de3c0129e --- /dev/null +++ b/src/apps/cardano/get_address.py @@ -0,0 +1,36 @@ +from trezor import log, ui, wire +from trezor.crypto import bip32 +from trezor.messages.CardanoAddress import CardanoAddress + +from .address import _break_address_n_to_lines, derive_address_and_node +from .ui import show_swipable_with_confirmation + +from apps.common import storage + + +async def cardano_get_address(ctx, msg): + mnemonic = storage.get_mnemonic() + root_node = bip32.from_mnemonic_cardano(mnemonic) + + try: + address, _ = derive_address_and_node(root_node, msg.address_n) + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Deriving address failed") + mnemonic = None + root_node = None + + if msg.show_display: + if not await show_swipable_with_confirmation( + ctx, address, "Export address", icon=ui.ICON_SEND, icon_color=ui.GREEN + ): + raise wire.ActionCancelled("Exporting cancelled") + else: + lines = _break_address_n_to_lines(msg.address_n) + if not await show_swipable_with_confirmation( + ctx, lines, "For BIP32 path", icon=ui.ICON_SEND, icon_color=ui.GREEN + ): + raise wire.ActionCancelled("Exporting cancelled") + + return CardanoAddress(address=address) diff --git a/src/apps/cardano/get_public_key.py b/src/apps/cardano/get_public_key.py new file mode 100644 index 000000000..2b7b35a1a --- /dev/null +++ b/src/apps/cardano/get_public_key.py @@ -0,0 +1,57 @@ +from ubinascii import hexlify + +from trezor import log, wire +from trezor.crypto import bip32 +from trezor.messages.CardanoPublicKey import CardanoPublicKey +from trezor.messages.HDNodeType import HDNodeType + +from .address import ( + _break_address_n_to_lines, + _derive_hd_passphrase, + derive_address_and_node, +) +from .ui import show_swipable_with_confirmation + +from apps.common import seed, storage + + +async def cardano_get_public_key(ctx, msg): + mnemonic = storage.get_mnemonic() + root_node = bip32.from_mnemonic_cardano(mnemonic) + + try: + key = _get_public_key(root_node, msg.address_n) + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Deriving public key failed") + mnemonic = None + root_node = None + + lines = ["For BIP32 path: ", ""] + lines.extend(_break_address_n_to_lines(msg.address_n)) + if not await show_swipable_with_confirmation(ctx, lines, "Export xpub key"): + raise wire.ActionCancelled("Exporting cancelled") + + return key + + +def _get_public_key(root_node, derivation_path: list): + _, node = derive_address_and_node(root_node, derivation_path) + + public_key = hexlify(seed.remove_ed25519_prefix(node.public_key())).decode("utf8") + chain_code = hexlify(node.chain_code()).decode("utf8") + xpub_key = public_key + chain_code + root_hd_passphrase = hexlify(_derive_hd_passphrase(root_node)).decode("utf8") + + node_type = HDNodeType( + depth=node.depth(), + child_num=node.child_num(), + fingerprint=node.fingerprint(), + chain_code=node.chain_code(), + public_key=seed.remove_ed25519_prefix(node.public_key()), + ) + + return CardanoPublicKey( + node=node_type, xpub=xpub_key, root_hd_passphrase=root_hd_passphrase + ) diff --git a/src/apps/cardano/sign_message.py b/src/apps/cardano/sign_message.py new file mode 100644 index 000000000..724836e44 --- /dev/null +++ b/src/apps/cardano/sign_message.py @@ -0,0 +1,51 @@ +from trezor import log, ui, wire +from trezor.crypto import bip32 +from trezor.crypto.curve import ed25519 +from trezor.messages.CardanoMessageSignature import CardanoMessageSignature + +from .address import _break_address_n_to_lines, derive_address_and_node +from .ui import show_swipable_with_confirmation + +from apps.common import seed, storage + + +async def cardano_sign_message(ctx, msg): + mnemonic = storage.get_mnemonic() + root_node = bip32.from_mnemonic_cardano(mnemonic) + + try: + signature = _sign_message(root_node, msg.message, msg.address_n) + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Signing failed") + mnemonic = None + root_node = None + + if not await show_swipable_with_confirmation( + ctx, msg.message, "Signing message", ui.ICON_RECEIVE, ui.GREEN + ): + raise wire.ActionCancelled("Signing cancelled") + + if not await show_swipable_with_confirmation( + ctx, + _break_address_n_to_lines(msg.address_n), + "With address", + ui.ICON_RECEIVE, + ui.GREEN, + ): + raise wire.ActionCancelled("Signing cancelled") + + return signature + + +def _sign_message(root_node, message: str, derivation_path: list): + address, node = derive_address_and_node(root_node, derivation_path) + + signature = ed25519.sign_ext(node.private_key(), node.private_key_ext(), message) + + sig = CardanoMessageSignature() + sig.public_key = seed.remove_ed25519_prefix(node.public_key()) + sig.signature = signature + + return sig diff --git a/src/apps/cardano/sign_transaction.py b/src/apps/cardano/sign_transaction.py new file mode 100644 index 000000000..98afd35d9 --- /dev/null +++ b/src/apps/cardano/sign_transaction.py @@ -0,0 +1,263 @@ +from trezor import log, ui, wire +from trezor.crypto import base58, bip32, hashlib +from trezor.crypto.curve import ed25519 +from trezor.messages.CardanoTxRequest import CardanoTxRequest +from trezor.messages.MessageType import CardanoTxAck +from trezor.ui.text import BR + +from .address import _break_address_n_to_lines, derive_address_and_node +from .ui import progress, show_swipable_with_confirmation + +from apps.cardano import cbor +from apps.common import seed, storage +from apps.homescreen.homescreen import display_homescreen + + +async def show_tx( + ctx, + outputs: list, + outcoins: list, + change_derivation_paths: list, + change_coins: list, + fee: float, + tx_size: float, +) -> bool: + lines = ("%s ADA" % _micro_ada_to_ada(fee), BR, "Tx size:", "%s bytes" % tx_size) + if not await show_swipable_with_confirmation( + ctx, lines, "Confirm fee", ui.ICON_SEND, ui.GREEN + ): + return False + + for index, output in enumerate(outputs): + if not await show_swipable_with_confirmation( + ctx, output, "Confirm output", ui.ICON_SEND, ui.GREEN + ): + return False + + if not await show_swipable_with_confirmation( + ctx, + "%s ADA" % _micro_ada_to_ada(outcoins[index]), + "Confirm amount", + ui.ICON_SEND, + ui.GREEN, + ): + return False + + for index, change in enumerate(change_derivation_paths): + if not await show_swipable_with_confirmation( + ctx, + _break_address_n_to_lines(change), + "Confirm change", + ui.ICON_SEND, + ui.GREEN, + ): + return False + + if not await show_swipable_with_confirmation( + ctx, + "%s ADA" % _micro_ada_to_ada(change_coins[index]), + "Confirm amount", + ui.ICON_SEND, + ui.GREEN, + ): + return False + + return True + + +async def request_transaction(ctx, tx_req: CardanoTxRequest, index: int): + tx_req.tx_index = index + return await ctx.call(tx_req, CardanoTxAck) + + +async def cardano_sign_transaction(ctx, msg): + mnemonic = storage.get_mnemonic() + root_node = bip32.from_mnemonic_cardano(mnemonic) + + progress.init(msg.transactions_count, "Loading data") + + try: + # request transactions + transactions = [] + tx_req = CardanoTxRequest() + for index in range(msg.transactions_count): + progress.advance() + tx_ack = await request_transaction(ctx, tx_req, index) + transactions.append(tx_ack.transaction) + + # clear progress bar + display_homescreen() + + # sign the transaction bundle and prepare the result + transaction = Transaction(msg.inputs, msg.outputs, transactions, root_node) + tx_body, tx_hash = transaction.serialise_tx() + tx = CardanoTxRequest(tx_body=tx_body, tx_hash=tx_hash) + + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Signing failed") + + # display the transaction in UI + if not await show_tx( + ctx, + transaction.output_addresses, + transaction.outgoing_coins, + transaction.change_derivation_paths, + transaction.change_coins, + transaction.fee, + len(tx_body), + ): + raise wire.ActionCancelled("Signing cancelled") + + return tx + + +def _micro_ada_to_ada(amount: float) -> float: + return amount / 1000000 + + +class Transaction: + CARDANO_WITNESS_MAGIC_PREFIX = b"\x01\x1a\x2d\x96\x4a\x09\x58\x20" + + def __init__(self, inputs: list, outputs: list, transactions: list, root_node): + self.inputs = inputs + self.outputs = outputs + self.transactions = transactions + self.root_node = root_node + + # attributes have to be always empty in current Cardano + self.attributes = {} + + def _process_inputs(self): + input_coins = [] + input_hashes = [] + output_indexes = [] + types = [] + tx_data = {} + + for raw_transaction in self.transactions: + tx_hash = hashlib.blake2b(data=bytes(raw_transaction), outlen=32).digest() + tx_data[tx_hash] = cbor.decode(raw_transaction) + + for input in self.inputs: + input_hashes.append(input.prev_hash) + output_indexes.append(input.prev_index) + types.append(input.type or 0) + + nodes = [] + for input in self.inputs: + _, node = derive_address_and_node(self.root_node, input.address_n) + nodes.append(node) + + for index, output_index in enumerate(output_indexes): + tx_hash = bytes(input_hashes[index]) + if tx_hash in tx_data: + tx = tx_data[tx_hash] + outputs = tx[1] + amount = outputs[output_index][1] + input_coins.append(amount) + else: + raise wire.ProcessError("No tx data sent for input " + str(index)) + + self.input_coins = input_coins + self.nodes = nodes + self.types = types + self.input_hashes = input_hashes + self.output_indexes = output_indexes + + def _process_outputs(self): + change_addresses = [] + change_derivation_paths = [] + output_addresses = [] + outgoing_coins = [] + change_coins = [] + + for output in self.outputs: + if output.address_n: + address, _ = derive_address_and_node(self.root_node, output.address_n) + change_addresses.append(address) + change_derivation_paths.append(output.address_n) + change_coins.append(output.amount) + else: + if output.address is None: + raise wire.ProcessError( + "Each output must have address or address_n field!" + ) + + outgoing_coins.append(output.amount) + output_addresses.append(output.address) + + self.change_addresses = change_addresses + self.output_addresses = output_addresses + self.outgoing_coins = outgoing_coins + self.change_coins = change_coins + self.change_derivation_paths = change_derivation_paths + + def _build_witnesses(self, tx_aux_hash: str): + witnesses = [] + for index, node in enumerate(self.nodes): + message = self.CARDANO_WITNESS_MAGIC_PREFIX + tx_aux_hash + signature = ed25519.sign_ext( + node.private_key(), node.private_key_ext(), message + ) + extended_public_key = ( + seed.remove_ed25519_prefix(node.public_key()) + node.chain_code() + ) + witnesses.append( + [ + self.types[index], + cbor.Tagged(24, cbor.encode([extended_public_key, signature])), + ] + ) + + return witnesses + + @staticmethod + def compute_fee(input_coins: list, outgoing_coins: list, change_coins: list): + input_coins_sum = sum(input_coins) + outgoing_coins_sum = sum(outgoing_coins) + change_coins_sum = sum(change_coins) + + return input_coins_sum - outgoing_coins_sum - change_coins_sum + + def serialise_tx(self): + + self._process_inputs() + self._process_outputs() + + inputs_cbor = [] + for i, output_index in enumerate(self.output_indexes): + inputs_cbor.append( + [ + self.types[i], + cbor.Tagged(24, cbor.encode([self.input_hashes[i], output_index])), + ] + ) + + inputs_cbor = cbor.IndefiniteLengthArray(inputs_cbor) + + outputs_cbor = [] + for index, address in enumerate(self.output_addresses): + outputs_cbor.append( + [cbor.Raw(base58.decode(address)), self.outgoing_coins[index]] + ) + + for index, address in enumerate(self.change_addresses): + outputs_cbor.append( + [cbor.Raw(base58.decode(address)), self.change_coins[index]] + ) + + outputs_cbor = cbor.IndefiniteLengthArray(outputs_cbor) + + tx_aux_cbor = [inputs_cbor, outputs_cbor, self.attributes] + tx_hash = hashlib.blake2b(data=cbor.encode(tx_aux_cbor), outlen=32).digest() + + witnesses = self._build_witnesses(tx_hash) + tx_body = cbor.encode([tx_aux_cbor, witnesses]) + + self.fee = self.compute_fee( + self.input_coins, self.outgoing_coins, self.change_coins + ) + + return tx_body, tx_hash diff --git a/src/apps/cardano/ui/__init__.py b/src/apps/cardano/ui/__init__.py new file mode 100644 index 000000000..0a23329b6 --- /dev/null +++ b/src/apps/cardano/ui/__init__.py @@ -0,0 +1,54 @@ +from micropython import const + +from trezor import ui +from trezor.messages import ButtonRequestType, MessageType +from trezor.messages.ButtonRequest import ButtonRequest +from trezor.ui.confirm import CONFIRMED, ConfirmDialog +from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor.ui.text import Text +from trezor.utils import chunks + + +async def show_swipable_with_confirmation( + ctx, content, title: str, icon=ui.ICON_RESET, icon_color=ui.ORANGE +): + first_page = const(0) + lines_per_page = const(4) + + if isinstance(content, (list, tuple)): + lines = content + else: + lines = list(chunks(content, 17)) + pages = list(chunks(lines, lines_per_page)) + + await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck) + + paginator = paginate( + show_text_page, len(pages), first_page, pages, title, icon, icon_color + ) + return await ctx.wait(paginator) == CONFIRMED + + +@ui.layout +async def show_text_page( + page: int, + page_count: int, + pages: list, + title: str, + icon=ui.ICON_RESET, + icon_color=ui.ORANGE, +): + if page_count == 1: + page = 0 + + lines = pages[page] + content = Text(title, icon, icon_color=icon_color) + content.mono(*lines) + + content = Scrollpage(content, page, page_count) + + if page + 1 >= page_count: + return await ConfirmDialog(content) + else: + content.render() + await animate_swipe() diff --git a/src/apps/cardano/ui/progress.py b/src/apps/cardano/ui/progress.py new file mode 100644 index 000000000..0068f90c1 --- /dev/null +++ b/src/apps/cardano/ui/progress.py @@ -0,0 +1,28 @@ +from trezor import ui + +_progress = 0 +_steps = 0 + + +def init(total_steps, text): + global _progress, _steps + _progress = 0 + _steps = total_steps + report_init(text) + report() + + +def advance(): + global _progress + _progress += 1 + report() + + +def report_init(text): + ui.display.clear() + ui.header(text) + + +def report(): + p = int(1000 * _progress / _steps) + ui.display.loader(p, 18, ui.WHITE, ui.BG) diff --git a/src/apps/cardano/verify_message.py b/src/apps/cardano/verify_message.py new file mode 100644 index 000000000..27e014568 --- /dev/null +++ b/src/apps/cardano/verify_message.py @@ -0,0 +1,36 @@ +from ubinascii import hexlify + +from trezor import log, ui, wire +from trezor.crypto.curve import ed25519 +from trezor.messages.Failure import Failure +from trezor.messages.Success import Success + +from .ui import show_swipable_with_confirmation + + +async def cardano_verify_message(ctx, msg): + try: + res = _verify_message(msg.public_key, msg.signature, msg.message) + except ValueError as e: + if __debug__: + log.exception(__name__, e) + raise wire.ProcessError("Verifying failed") + + if not res: + return Failure(message="Invalid signature") + + if not await show_swipable_with_confirmation( + ctx, msg.message, "Verifying message", ui.ICON_RECEIVE, ui.GREEN + ): + raise wire.ActionCancelled("Verifying cancelled") + + if not await show_swipable_with_confirmation( + ctx, hexlify(msg.public_key), "With public key", ui.ICON_RECEIVE, ui.GREEN + ): + raise wire.ActionCancelled("Verifying cancelled") + + return Success(message="Message verified") + + +def _verify_message(public_key: bytes, signature: bytes, message: str): + return ed25519.verify(public_key, signature, message) diff --git a/src/main.py b/src/main.py index ce5f72abe..76e6aab41 100644 --- a/src/main.py +++ b/src/main.py @@ -16,6 +16,8 @@ import apps.ethereum import apps.lisk import apps.nem import apps.stellar +import apps.cardano + if __debug__: import apps.debug @@ -30,6 +32,7 @@ apps.ethereum.boot() apps.lisk.boot() apps.nem.boot() apps.stellar.boot() +apps.cardano.boot() if __debug__: apps.debug.boot() else: diff --git a/tests/test_apps.cardano.address.py b/tests/test_apps.cardano.address.py new file mode 100644 index 000000000..b41042d7d --- /dev/null +++ b/tests/test_apps.cardano.address.py @@ -0,0 +1,175 @@ +from common import * +from apps.common import seed +from trezor import wire + +from apps.cardano.address import ( + _derive_hd_passphrase, + _encrypt_derivation_path, + _get_address_root, + _address_hash, + validate_derivation_path, + derive_address_and_node +) +from trezor.crypto import bip32 + + +class TestCardanoAddress(unittest.TestCase): + def test_hardened_address_derivation(self): + mnemonic = "plastic that delay conduct police ticket swim gospel intact harsh obtain entire" + node = bip32.from_mnemonic_cardano(mnemonic) + + addresses = [ + "DdzFFzCqrhtDB6YEgPQqFiVnhKsfyEMe9MLQabhayVUL2WRN1dbLLFS7VfKYBy8n3uemZRcDyqMnv7STCU9vj2eAR8CgFgKMDG2mkQN7", + "DdzFFzCqrhtCGRQ2UYpcouvRgDnPsAYpmzWVtd5YLvaRrMAMoDmYsKhNMAWePbK7a1XbZ8ghTeyaSLZ2488extnB5F9SwHus4UFaFwkS", + "DdzFFzCqrhsqHyZLVLeFrgcxUrPA5YMJJRJCxkESHcPkV1EuuDKhKkJNPkEyrWXhPbuMHxSnz1cNYUCN8tJsLwaFiSxMz3ab19GEvaNP", + ] + + for i, expected in enumerate(addresses): + # 44'/1815'/0'/0/i' + address, _ = derive_address_and_node(node, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i]) + self.assertEqual(expected, address) + + nodes = [ + ( + "d4dd69a2f2a6374f3733f53e03f610d73dd4f1d5131169bc144e6d34c9bcbe04", + "21d97a697583630e2cef01e5fc1555ea4fd9625ff8fcde1fc72e67aa42f975ec", + "2df46e04ebf0816e242bfaa1c73e5ebe8863d05d7a96c8aac16f059975e63f30", + "057658de1308930ad4a5663e4f77477014b04954a9d488e62d73b04fc659a35c" + ), + ( + "3476630290051477e4cc206fd5f6587065d3c9558c9891cc1c0ed5a408d5b60c", + "3f1d4beaefd2ffff59a45cb75519960d02f4de62c076a165bc39a7d7b1fec168", + "35b0cc0b770e04d86a9cddb0e2068b3a242f6b6e93c9a9d3c4f0899bd62b4266", + "35bb811c631b3db3b10559bc15821a39969654ebcad80cedf544ac8bf2a73ce7" + ), + ( + "06a6f53baf84ac6713cd1c441081dff00d1c4abee33091dc5c5ebdec2044270c", + "4978871e479a3a58adabb030565162832c63a2909442d306c96eaf03823ff5c9", + "9f26aad725aef1bb0609085f2c961b4d2579bceccfb1b01f3c7d1dbdd02b50b1", + "70f72ce51d0c984c4bbddd0297f4ffe0b4710c2c3f9a7e17f7d7e3e1810b5c33" + ), + ] + + for i, (priv, ext, pub, chain) in enumerate(nodes): + _, n = derive_address_and_node(node, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i]) + self.assertEqual(unhexlify(priv), n.private_key()) + self.assertEqual(unhexlify(ext), n.private_key_ext()) + self.assertEqual(unhexlify(pub), seed.remove_ed25519_prefix(n.public_key())) + self.assertEqual(unhexlify(chain), n.chain_code()) + + def test_non_hardened_address_derivation(self): + mnemonic = "plastic that delay conduct police ticket swim gospel intact harsh obtain entire" + node = bip32.from_mnemonic_cardano(mnemonic) + + addresses = [ + "2w1sdSJu3GVezU6nw8LodErz7kSrEQ9hKQhsGLWk4JxTCxg7tkJvSowGKLFE7PMxknbkuYjtaWbpnJLhJgwmwNA98GPX2SGSN1t", + "2w1sdSJu3GVg7mRbtq2aGUFKxXnpFoP9hesA1n7KJrnQ9QEgyy7DGbLU52L2cytPqCoNNhkvRCF9ZsBLwMv1E35CVh6XBiWj2GE", + "2w1sdSJu3GVg193D2yhiiH947J9UwrbPAmNao6ciAZi3GeU7sG1D3fTAnQakzHSe1FVyuRdUjcx52Q7575LxBBNE8aCunKFA4kA", + ] + + for i, expected in enumerate(addresses): + # 44'/1815'/0'/0/i + address, _ = derive_address_and_node(node, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i]) + self.assertEqual(expected, address) + + nodes = [ + ( + "a75a851505db79ee8557a8cb3ef561ab7d6bd24d7cc0e97b8496654431fc2e0c", + "21fa8154e009a46a1c44709fe23b75735c8abc6256c44cc3c208c1c914f037ce", + "723fdc0eb1300fe7f2b9b6989216a831835a88695ba2c2d5c50c8470b7d1b239", + "ae09010e921de259b02f34ce7fd76f9c09ad224d436fe8fa38aa212177937ffe" + ), + ( + "48ded246510a563f759fde920016ad1356238ab5936869e45ccec5b4d8fcce0c", + "0216c5c777bfe196576b776bd9faf2ac1318966c820edb203754166d5a0f4d92", + "6dc82a0d40257cfc1ea5d728c6ccfa52ad5673c2dc4cfed239dff642d29fbc46", + "cd490ae08bd2ff18e8b61c39173f6bf0db85709130baa103b9f00e4160ec150f" + ), + ( + "8e651d540f55a4670bb5ec8cd0812731ce734a1e745059c4f445fd8cd8fcb604", + "ab7f8d9e7927a1a71b7b08eb3b871246dc4717d9e309b7682df0eee202a5a97a", + "e55323d6881ca92a0816695def558145ef22f0d0c4f6133aab7a8a3f2f98ef78", + "6c9313fcf93b55a977184514aefa1c778c1abadb2ba9f2c1351b587b7c1e1572" + ), + ] + + for i, (priv, ext, pub, chain) in enumerate(nodes): + _, n = derive_address_and_node(node, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i]) + self.assertEqual(unhexlify(priv), n.private_key()) + self.assertEqual(unhexlify(ext), n.private_key_ext()) + self.assertEqual(unhexlify(pub), seed.remove_ed25519_prefix(n.public_key())) + self.assertEqual(unhexlify(chain), n.chain_code()) + + + def test_root_address_derivation(self): + mnemonic = "plastic that delay conduct police ticket swim gospel intact harsh obtain entire" + node = bip32.from_mnemonic_cardano(mnemonic) + + # 44'/1815' + address, _ = derive_address_and_node(node, [0x80000000 | 44, 0x80000000 | 1815]) + self.assertEqual("Ae2tdPwUPEYygPo2ZNZ7Ve6ZExaFZvkGcQFZ5oSyqVNoJn5J65Foyz2XiSU", address) + + priv, ext, pub, chain = ( + "90bc16ad766aebce31b407f111db3ba95de2780c5bb760f3333dac1b3823ee53", + "10f20917dcfa2b3c295386413ae3564365e4a51f063da644d0945f4d3da57699", + "7d1de3f22f53904d007ff833fadd7cd6482ea1e83918b985b4ea33e63c16d183", + "7a04a6aab0ed12af562a26db4d10344454274d0bfa6e3581df1dc02f13c5fbe5" + ) + + _, n = derive_address_and_node(node, [0x80000000 | 44, 0x80000000 | 1815]) + self.assertEqual(unhexlify(priv), n.private_key()) + self.assertEqual(unhexlify(ext), n.private_key_ext()) + self.assertEqual(unhexlify(pub), seed.remove_ed25519_prefix(n.public_key())) + self.assertEqual(unhexlify(chain), n.chain_code()) + + def test_validate_derivation_path(self): + incorrect_derivation_paths = [ + [0x80000000 | 44], + [0x80000000 | 44, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815], + [0x80000000 | 43, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815], + [0x80000000 | 44, 0x80000000 | 1816, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815], + ] + + correct_derivation_paths = [ + [0x80000000 | 44, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815, 0x80000000 | 1815], + [0x80000000 | 44, 0x80000000 | 1815], + [0x80000000 | 44, 0x80000000 | 1815, 0x80000000], + [0x80000000 | 44, 0x80000000 | 1815, 0], + [0x80000000 | 44, 0x80000000 | 1815, 0, 0], + ] + + for derivation_path in incorrect_derivation_paths: + self.assertRaises(wire.ProcessError, validate_derivation_path, derivation_path) + + for derivation_path in correct_derivation_paths: + self.assertEqual(derivation_path, validate_derivation_path(derivation_path)) + + def test_derive_hd_passphrase(self): + mnemonic = "plastic that delay conduct police ticket swim gospel intact harsh obtain entire" + root_node = bip32.from_mnemonic_cardano(mnemonic) + + self.assertEqual(hexlify(_derive_hd_passphrase(root_node)).decode('utf8'), "8ee689a22e1ec569d2ada515c4ee712ad089901b7fe0afb94fe196de944ee814") + + def test_encrypt_derivation_path(self): + encrypted_path = _encrypt_derivation_path([0x80000000, 0x80000000], unhexlify("8ee689a22e1ec569d2ada515c4ee712ad089901b7fe0afb94fe196de944ee814")) + self.assertEqual(hexlify(encrypted_path).decode('utf8'), "722c7a75813fafde9ff9e6d4dec19adfd57f0d20194fa4c703770020") + + encrypted_path = _encrypt_derivation_path([0x80000000, 0], unhexlify("8ee689a22e1ec569d2ada515c4ee712ad089901b7fe0afb94fe196de944ee814")) + self.assertEqual(hexlify(encrypted_path).decode('utf8'), "722c7a75813fb5a13d916748b3fb0561c5c7b59f9bc644ea") + + def test_get_address_root(self): + mnemonic = "plastic that delay conduct police ticket swim gospel intact harsh obtain entire" + root_node = bip32.from_mnemonic_cardano(mnemonic) + + address_root = _get_address_root(root_node, {1: b'X\x1cr,zu\x81?\xaf\xde\x9f\xf9\xe4\xd4\x90\xadH$\xe9\xf3\x88\x16\xcb\xd2)\x02M\x0c#\xde'}) + self.assertEqual(address_root, b'\xca\x9bbQ\xa5\xaa}\x01U\xba\xe5\xa5\xaa~\x84M\x0b;\x1dM\xd8z\xe7Y\x01\xc8\x92\x91') + + def test_address_hash(self): + data = [0, [0, b"}\x1d\xe3\xf2/S\x90M\x00\x7f\xf83\xfa\xdd|\xd6H.\xa1\xe89\x18\xb9\x85\xb4\xea3\xe6<\x16\xd1\x83z\x04\xa6\xaa\xb0\xed\x12\xafV*&\xdbM\x104DT'M\x0b\xfan5\x81\xdf\x1d\xc0/\x13\xc5\xfb\xe5"], {}] + result = _address_hash(data) + + self.assertEqual(result, b'\x1c\xca\xee\xc9\x80\xaf}\xb0\x9a\xa8\x96E\xd6\xa4\xd1\xb4\x13\x85\xb9\xc2q\x1d5/{\x12"\xca') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.cardano.cbor.py b/tests/test_apps.cardano.cbor.py new file mode 100644 index 000000000..6381dc3b9 --- /dev/null +++ b/tests/test_apps.cardano.cbor.py @@ -0,0 +1,56 @@ +from common import * + +from apps.cardano.cbor import ( + Tagged, + IndefiniteLengthArray, + encode +) +from ubinascii import unhexlify + +class TestCardanoCbor(unittest.TestCase): + def test_cbor_encoding(self): + test_vectors = [ + # integers + (0, '00'), + (1, '01'), + (10, '0a'), + (23, '17'), + (24, '1818'), + (25, '1819'), + (100, '1864'), + (1000, '1903e8'), + (1000000, '1a000f4240'), + (1000000000000, '1b000000e8d4a51000'), + + # binary strings + (b'', '40'), + (unhexlify('01020304'), '4401020304'), + + # tags + (Tagged(1, 1363896240), 'c11a514b67b0'), + (Tagged(23, unhexlify('01020304')), 'd74401020304'), + + # arrays + ([], '80'), + ([1, 2, 3], '83010203'), + ([1, [2, 3], [4, 5]], '8301820203820405'), + (list(range(1, 26)), '98190102030405060708090a0b0c0d0e0f101112131415161718181819'), + + # maps + ({}, 'a0'), + + # Note: normal python dict doesn't have a fixed item ordering + ({1: 2, 3: 4}, 'a203040102'), + + # indefinite + (IndefiniteLengthArray([]), '9fff'), + (IndefiniteLengthArray([1, [2, 3], [4, 5]]), '9f01820203820405ff'), + (IndefiniteLengthArray([1, [2, 3], IndefiniteLengthArray([4, 5])]), + '9f018202039f0405ffff'), + ] + for val, expected in test_vectors: + encoded = encode(val) + self.assertEqual(unhexlify(expected), encoded) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.cardano.get_public_key.py b/tests/test_apps.cardano.get_public_key.py new file mode 100644 index 000000000..d467441f1 --- /dev/null +++ b/tests/test_apps.cardano.get_public_key.py @@ -0,0 +1,53 @@ +from common import * + +from apps.cardano.get_public_key import _get_public_key +from trezor.crypto import bip32 +from ubinascii import hexlify + + +class TestCardanoGetPublicKey(unittest.TestCase): + def test_get_public_key(self): + mnemonic = "plastic that delay conduct police ticket swim gospel intact harsh obtain entire" + node = bip32.from_mnemonic_cardano(mnemonic) + + derivation_paths = [ + [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000], + [0x80000000 | 44, 0x80000000 | 1815], + [0x80000000 | 44, 0x80000000 | 1815, 0, 0, 0], + [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 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(node, derivation_path) + + self.assertEqual(hexlify(key.node.public_key).decode('utf8'), public_keys[index]) + self.assertEqual(hexlify(key.node.chain_code).decode('utf8'), chain_codes[index]) + self.assertEqual(key.xpub, xpub_keys[index]) + self.assertEqual(key.root_hd_passphrase, root_hd_passphrase) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.cardano.sign_message.py b/tests/test_apps.cardano.sign_message.py new file mode 100644 index 000000000..e6e4cd7f5 --- /dev/null +++ b/tests/test_apps.cardano.sign_message.py @@ -0,0 +1,25 @@ +from common import * + +from apps.cardano.sign_message import _sign_message +from trezor.crypto import bip32 + + +class TestCardanoSignMessage(unittest.TestCase): + def test_sign_message(self): + mnemonic = "plastic that delay conduct police ticket swim gospel intact harsh obtain entire" + node = bip32.from_mnemonic_cardano(mnemonic) + + messages = [ + ('Test message to sign', [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000], '07f226da2a59c3083e80f01ef7e0ec46fc726ebe6bd15d5e9040031c342d8651bee9aee875019c41a7719674fd417ad43990988ffd371527604b6964df75960d'), + ('New Test message to sign', [0x80000000 | 44, 0x80000000 | 1815], '8fd3b9d8a4c30326b720de76f8de2bbf57b29b7593576eac4a3017ea23046812017136520dc2f24e9fb4da56bd87c77ea49265686653b36859b5e1e56ba9eb0f'), + ('Another Test message to sign', [0x80000000 | 44, 0x80000000 | 1815, 0, 0, 0], '89d63bd32c2eb92aa418b9ce0383a7cf489bc56284876c19246b70be72070d83d361fcb136e8e257b7e66029ef4a566405cda0143d251f851debd62c3c38c302'), + ('Just another Test message to sign', [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0], '49d948090d30e35a88a26d8fb07aca5d68936feba2d5bd49e0d0f7c027a0c8c2955b93a7c930a3b36d23c2502c18bf39cf9b17bbba1a0965090acfb4d10a9305'), + ] + + for (message, derivation_path, expected_signature) in messages: + signature = _sign_message(node, message, derivation_path) + self.assertEqual(expected_signature, hexlify(signature.signature).decode('utf8')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.cardano.verify_message.py b/tests/test_apps.cardano.verify_message.py new file mode 100644 index 000000000..8d60cb1ce --- /dev/null +++ b/tests/test_apps.cardano.verify_message.py @@ -0,0 +1,22 @@ +from common import * +from ubinascii import unhexlify + +from apps.cardano.verify_message import _verify_message + + +class TestCardanoVerifyMessage(unittest.TestCase): + def test_verify_message(self): + messages = [ + ('Test message to sign', '2df46e04ebf0816e242bfaa1c73e5ebe8863d05d7a96c8aac16f059975e63f30', '07f226da2a59c3083e80f01ef7e0ec46fc726ebe6bd15d5e9040031c342d8651bee9aee875019c41a7719674fd417ad43990988ffd371527604b6964df75960d'), + ('New Test message to sign', '7d1de3f22f53904d007ff833fadd7cd6482ea1e83918b985b4ea33e63c16d183', '8fd3b9d8a4c30326b720de76f8de2bbf57b29b7593576eac4a3017ea23046812017136520dc2f24e9fb4da56bd87c77ea49265686653b36859b5e1e56ba9eb0f'), + ('Another Test message to sign', 'f59a28d704df090d8fc641248bdb27d0d001da13ddb332a79cfba8a9fa7233e7', '89d63bd32c2eb92aa418b9ce0383a7cf489bc56284876c19246b70be72070d83d361fcb136e8e257b7e66029ef4a566405cda0143d251f851debd62c3c38c302'), + ('Just another Test message to sign', '723fdc0eb1300fe7f2b9b6989216a831835a88695ba2c2d5c50c8470b7d1b239', '49d948090d30e35a88a26d8fb07aca5d68936feba2d5bd49e0d0f7c027a0c8c2955b93a7c930a3b36d23c2502c18bf39cf9b17bbba1a0965090acfb4d10a9305'), + ] + + for (message, public_key, signature) in messages: + result = _verify_message(unhexlify(public_key), unhexlify(signature), message) + self.assertEqual(result, True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_trezor.crypto.hashlib.blake2b.py b/tests/test_trezor.crypto.hashlib.blake2b.py index ad04645dd..5f666246d 100644 --- a/tests/test_trezor.crypto.hashlib.blake2b.py +++ b/tests/test_trezor.crypto.hashlib.blake2b.py @@ -18,7 +18,7 @@ class TestCryptoBlake2b(unittest.TestCase): def test_digest(self): key = unhexlify('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f') for d, h in self.vectors: - self.assertEqual(hashlib.blake2b(unhexlify(d), key=key).digest(), unhexlify(h)) + self.assertEqual(hashlib.blake2b(data=unhexlify(d), key=key).digest(), unhexlify(h)) def test_update(self): key = unhexlify('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f') diff --git a/vendor/trezor-crypto b/vendor/trezor-crypto index f586155d8..ff001a0f1 160000 --- a/vendor/trezor-crypto +++ b/vendor/trezor-crypto @@ -1 +1 @@ -Subproject commit f586155d808be7467e31da907b0106b4c31a0d1d +Subproject commit ff001a0f12565a0d7d51ad3ce5e11e98db6afc25