From d0c3a6a2fa4941883a520510741be60f82f8eaa3 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Wed, 3 Feb 2021 15:58:00 +0100 Subject: [PATCH] chore(core): Add mac field to Address message. [no changelog] --- common/protob/messages-bitcoin.proto | 1 + core/src/all_modules.py | 2 ++ core/src/apps/bitcoin/get_address.py | 12 ++++++- core/src/apps/bitcoin/keychain.py | 2 +- core/src/apps/common/address_mac.py | 32 +++++++++++++++++++ core/src/trezor/messages.py | 2 ++ core/tests/test_apps.bitcoin.address.py | 21 ++++++++++++ .../firmware/protob/messages-bitcoin.options | 1 + python/src/trezorlib/btc.py | 7 +++- python/src/trezorlib/messages.py | 3 ++ 10 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 core/src/apps/common/address_mac.py diff --git a/common/protob/messages-bitcoin.proto b/common/protob/messages-bitcoin.proto index 3331c87fd..0b8263608 100644 --- a/common/protob/messages-bitcoin.proto +++ b/common/protob/messages-bitcoin.proto @@ -118,6 +118,7 @@ message GetAddress { */ message Address { required string address = 1; // Coin address in Base58 encoding + optional bytes mac = 2; // Address authentication code } /** diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 7162c284a..dbd7dcb1d 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -292,6 +292,8 @@ apps.bitcoin.writers import apps.bitcoin.writers apps.common import apps.common +apps.common.address_mac +import apps.common.address_mac apps.common.address_type import apps.common.address_type apps.common.authorization diff --git a/core/src/apps/bitcoin/get_address.py b/core/src/apps/bitcoin/get_address.py index 76059a147..e706749f6 100644 --- a/core/src/apps/bitcoin/get_address.py +++ b/core/src/apps/bitcoin/get_address.py @@ -5,6 +5,7 @@ from trezor.enums import InputScriptType from trezor.messages import Address from trezor.ui.layouts import show_address +from apps.common.address_mac import get_address_mac from apps.common.paths import address_n_to_str, validate_path from . import addresses @@ -64,6 +65,7 @@ async def get_address( else: address_qr = address # base58 address + mac: bytes | None = None multisig_xpub_magic = coin.xpub_magic if msg.multisig: if coin.segwit and not msg.ignore_xpub_magic: @@ -77,6 +79,14 @@ async def get_address( and coin.xpub_magic_multisig_segwit_p2sh is not None ): multisig_xpub_magic = coin.xpub_magic_multisig_segwit_p2sh + else: + # Attach a MAC for single-sig addresses, but only if the path is standard + # or if the user explicitly confirms a non-standard path. + if msg.show_display or ( + keychain.is_in_keychain(msg.address_n) + and validate_path_against_script_type(coin, msg) + ): + mac = get_address_mac(address, coin.slip44, keychain) if msg.show_display: if msg.multisig: @@ -101,4 +111,4 @@ async def get_address( ctx, address=address_short, address_qr=address_qr, title=title ) - return Address(address=address) + return Address(address=address, mac=mac) diff --git a/core/src/apps/bitcoin/keychain.py b/core/src/apps/bitcoin/keychain.py index 6d9eb6ff6..59176b236 100644 --- a/core/src/apps/bitcoin/keychain.py +++ b/core/src/apps/bitcoin/keychain.py @@ -238,7 +238,7 @@ async def get_keychain_for_coin( ) -> tuple[Keychain, coininfo.CoinInfo]: coin = get_coin_by_name(coin_name) schemas = get_schemas_for_coin(coin) - slip21_namespaces = [[b"SLIP-0019"]] + slip21_namespaces = [[b"SLIP-0019"], [b"SLIP-0024"]] keychain = await get_keychain(ctx, coin.curve_name, schemas, slip21_namespaces) return keychain, coin diff --git a/core/src/apps/common/address_mac.py b/core/src/apps/common/address_mac.py new file mode 100644 index 000000000..12a9d91ee --- /dev/null +++ b/core/src/apps/common/address_mac.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +from trezor import utils, wire +from trezor.crypto import hashlib, hmac + +from .writers import write_bitcoin_varint, write_bytes_unchecked, write_uint32_le + +if TYPE_CHECKING: + from apps.common.keychain import Keychain + +_ADDRESS_MAC_KEY_PATH = [b"SLIP-0024", b"Address MAC key"] + + +def check_address_mac( + address: str, mac: bytes, slip44: int, keychain: Keychain +) -> None: + expected_mac = get_address_mac(address, slip44, keychain) + if len(mac) != hashlib.sha256.digest_size or not utils.consteq(expected_mac, mac): + raise wire.DataError("Invalid address MAC.") + + +def get_address_mac(address: str, slip44: int, keychain: Keychain) -> bytes: + # k = Key(m/"SLIP-0024"/"Address MAC key") + node = keychain.derive_slip21(_ADDRESS_MAC_KEY_PATH) + + # mac = HMAC-SHA256(key = k, msg = slip44 || address) + mac = utils.HashWriter(hmac(hmac.SHA256, node.key())) + address_bytes = address.encode() + write_uint32_le(mac, slip44) + write_bitcoin_varint(mac, len(address_bytes)) + write_bytes_unchecked(mac, address_bytes) + return mac.get_digest() diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 392f196bf..b97d256cf 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -474,11 +474,13 @@ if TYPE_CHECKING: class Address(protobuf.MessageType): address: "str" + mac: "bytes | None" def __init__( self, *, address: "str", + mac: "bytes | None" = None, ) -> None: pass diff --git a/core/tests/test_apps.bitcoin.address.py b/core/tests/test_apps.bitcoin.address.py index 643866c93..091902924 100644 --- a/core/tests/test_apps.bitcoin.address.py +++ b/core/tests/test_apps.bitcoin.address.py @@ -226,6 +226,27 @@ class TestAddress(unittest.TestCase): for path, input_type in incorrect_derivation_paths: self.assertFalse(self.validate(path, coin, input_type)) + def test_address_mac(self): + from apps.common.address_mac import check_address_mac, get_address_mac + from apps.common.keychain import Keychain + from apps.common.paths import AlwaysMatchingSchema + + VECTORS = ( + ('Bitcoin', '1DyHzbQUoQEsLxJn6M7fMD8Xdt1XvNiwNE', '9cf7c230041d6ed95b8273bd32e023d3f227ec8c44257f6463c743a4b4add028'), + ('Testnet', 'mm6kLYbGEL1tGe4ZA8xacfgRPdW1NLjCbZ', '4375089e50423505dc3480e6e85b0ba37a52bd1e009db5d260b6329f22c950d9') + ) + seed = bip39.seed(' '.join(['all'] * 12), '') + + for coin_name, address, mac in VECTORS: + coin = coins.by_name(coin_name) + mac = unhexlify(mac) + keychain = Keychain(seed, coin.curve_name, [AlwaysMatchingSchema], slip21_namespaces=[[b"SLIP-0024"]]) + self.assertEqual(get_address_mac(address, coin.slip44, keychain), mac) + check_address_mac(address, mac, coin.slip44, keychain) + with self.assertRaises(wire.DataError): + mac = bytes([mac[0]^1]) + mac[1:] + check_address_mac(address, mac, coin.slip44, keychain) + if __name__ == '__main__': unittest.main() diff --git a/legacy/firmware/protob/messages-bitcoin.options b/legacy/firmware/protob/messages-bitcoin.options index c7c7a5d04..aff252c67 100644 --- a/legacy/firmware/protob/messages-bitcoin.options +++ b/legacy/firmware/protob/messages-bitcoin.options @@ -8,6 +8,7 @@ GetAddress.address_n max_count:8 GetAddress.coin_name max_size:21 Address.address max_size:130 +Address.mac type:FT_IGNORE SignTx.coin_name max_size:21 diff --git a/python/src/trezorlib/btc.py b/python/src/trezorlib/btc.py index 82765ec1e..4fe7b9e77 100644 --- a/python/src/trezorlib/btc.py +++ b/python/src/trezorlib/btc.py @@ -128,7 +128,12 @@ def get_public_node( @expect(messages.Address, field="address", ret_type=str) -def get_address( +def get_address(*args: Any, **kwargs: Any): + return get_authenticated_address(*args, **kwargs) + + +@expect(messages.Address) +def get_authenticated_address( client: "TrezorClient", coin_name: str, n: "Address", diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 75067b9ef..94a351684 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -1024,14 +1024,17 @@ class Address(protobuf.MessageType): MESSAGE_WIRE_TYPE = 30 FIELDS = { 1: protobuf.Field("address", "string", repeated=False, required=True), + 2: protobuf.Field("mac", "bytes", repeated=False, required=False), } def __init__( self, *, address: "str", + mac: Optional["bytes"] = None, ) -> None: self.address = address + self.mac = mac class GetOwnershipId(protobuf.MessageType):