From 19d5c43ce50fd194b3cc4c1d034b9db86879b061 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 24 May 2022 19:08:04 +0200 Subject: [PATCH] feat(core): Implement SLIP-0025 CoinJoin accounts. --- common/protob/messages-management.proto | 1 + core/.changelog.d/2289.added | 1 + core/src/apps/bitcoin/authorization.py | 6 ++++- core/src/apps/bitcoin/authorize_coinjoin.py | 19 ++++++++----- core/src/apps/bitcoin/get_public_key.py | 25 +++++++++++++++-- core/src/apps/bitcoin/keychain.py | 30 ++++++++++++++++----- core/src/apps/common/authorization.py | 6 ++++- 7 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 core/.changelog.d/2289.added diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index 8cab05d26..fe46f52ff 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -441,6 +441,7 @@ message DoPreauthorized { * @start * @next SignTx * @next GetOwnershipProof + * @next GetPublicKey */ message PreauthorizedRequest { } diff --git a/core/.changelog.d/2289.added b/core/.changelog.d/2289.added new file mode 100644 index 000000000..3f2663d7a --- /dev/null +++ b/core/.changelog.d/2289.added @@ -0,0 +1 @@ +Add SLIP-0025 CoinJoin accounts. diff --git a/core/src/apps/bitcoin/authorization.py b/core/src/apps/bitcoin/authorization.py index b1b0fb365..dc6da1de3 100644 --- a/core/src/apps/bitcoin/authorization.py +++ b/core/src/apps/bitcoin/authorization.py @@ -2,7 +2,7 @@ from micropython import const from typing import TYPE_CHECKING from trezor import utils, wire -from trezor.messages import AuthorizeCoinJoin +from trezor.messages import AuthorizeCoinJoin, GetPublicKey from apps.common import authorization @@ -26,6 +26,10 @@ class CoinJoinAuthorization: def __init__(self, params: AuthorizeCoinJoin) -> None: self.params = params + def check_get_public_key(self, msg: GetPublicKey) -> bool: + # Check whether the current params matches the parameters of the XPUB request. + return self.params.address_n == msg.address_n + def check_get_ownership_proof(self, msg: GetOwnershipProof) -> bool: # Check whether the current params matches the parameters of the request. coordinator = utils.empty_bytearray(1 + len(self.params.coordinator.encode())) diff --git a/core/src/apps/bitcoin/authorize_coinjoin.py b/core/src/apps/bitcoin/authorize_coinjoin.py index fb50a07f5..accbdf633 100644 --- a/core/src/apps/bitcoin/authorize_coinjoin.py +++ b/core/src/apps/bitcoin/authorize_coinjoin.py @@ -2,17 +2,17 @@ from micropython import const from typing import TYPE_CHECKING from trezor import ui, wire -from trezor.enums import ButtonRequestType +from trezor.enums import ButtonRequestType, InputScriptType from trezor.messages import AuthorizeCoinJoin, Success from trezor.strings import format_amount from trezor.ui.layouts import confirm_action, confirm_coinjoin, confirm_metadata from apps.common import authorization, safety_checks -from apps.common.paths import validate_path +from apps.common.paths import PathSchema, validate_path from .authorization import FEE_RATE_DECIMALS from .common import BIP32_WALLET_DEPTH -from .keychain import validate_path_against_script_type, with_keychain +from .keychain import PATTERN_SLIP25, with_keychain if TYPE_CHECKING: from apps.common.coininfo import CoinInfo @@ -47,6 +47,13 @@ async def authorize_coinjoin( if not msg.address_n: raise wire.DataError("Empty path not allowed.") + validation_path = msg.address_n + [0] * BIP32_WALLET_DEPTH + if ( + not PathSchema.parse(PATTERN_SLIP25, coin.slip44).match(validation_path) + or msg.script_type != InputScriptType.SPENDTAPROOT + ) and safety_checks.is_strict(): + raise wire.DataError("Forbidden path.") + await confirm_action( ctx, "coinjoin_coordinator", @@ -60,14 +67,12 @@ async def authorize_coinjoin( max_fee_per_vbyte = format_amount(msg.max_fee_per_kvbyte, 3) await confirm_coinjoin(ctx, coin.coin_name, msg.max_rounds, max_fee_per_vbyte) - validation_path = msg.address_n + [0] * BIP32_WALLET_DEPTH await validate_path( ctx, keychain, validation_path, - validate_path_against_script_type( - coin, address_n=validation_path, script_type=msg.script_type - ), + PathSchema.parse(PATTERN_SLIP25, coin.slip44).match(validation_path), + msg.script_type == InputScriptType.SPENDTAPROOT, ) if msg.max_fee_per_kvbyte > coin.maxfee_kb: diff --git a/core/src/apps/bitcoin/get_public_key.py b/core/src/apps/bitcoin/get_public_key.py index 5db6a4fa5..c1e2dde2d 100644 --- a/core/src/apps/bitcoin/get_public_key.py +++ b/core/src/apps/bitcoin/get_public_key.py @@ -2,21 +2,42 @@ from typing import TYPE_CHECKING from trezor import wire from trezor.enums import InputScriptType -from trezor.messages import HDNodeType, PublicKey +from trezor.messages import AuthorizeCoinJoin, HDNodeType, PublicKey +from trezor.ui.layouts import confirm_action from apps.common import coininfo, paths from apps.common.keychain import get_keychain +from . import authorization + if TYPE_CHECKING: from trezor.messages import GetPublicKey +_SLIP25_PURPOSE = 10025 | paths.HARDENED + -async def get_public_key(ctx: wire.Context, msg: GetPublicKey) -> PublicKey: +async def get_public_key( + ctx: wire.Context, + msg: GetPublicKey, + auth_msg: AuthorizeCoinJoin | None = None, +) -> PublicKey: coin_name = msg.coin_name or "Bitcoin" script_type = msg.script_type or InputScriptType.SPENDADDRESS coin = coininfo.by_name(coin_name) curve_name = msg.ecdsa_curve_name or coin.curve_name + # Require confirmation to access SLIP25 paths unless already authorized. + if auth_msg: + if not authorization.from_cached_message(auth_msg).check_get_public_key(msg): + raise wire.ProcessError("Unauthorized operation") + elif msg.address_n and msg.address_n[0] == _SLIP25_PURPOSE: + await confirm_action( + ctx, + "confirm_coinjoin_xpub", + title="CoinJoin XPUB", + description="Do you want to allow access to your CoinJoin account?", + ) + keychain = await get_keychain(ctx, curve_name, [paths.AlwaysMatchingSchema]) node = keychain.derive(msg.address_n) diff --git a/core/src/apps/bitcoin/keychain.py b/core/src/apps/bitcoin/keychain.py index 59176b236..e6f62692d 100644 --- a/core/src/apps/bitcoin/keychain.py +++ b/core/src/apps/bitcoin/keychain.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from trezor import wire from trezor.enums import InputScriptType +from trezor.messages import AuthorizeCoinJoin, SignTx from apps.common import coininfo from apps.common.keychain import get_keychain @@ -19,13 +20,11 @@ if TYPE_CHECKING: from trezor.protobuf import MessageType from trezor.messages import ( - AuthorizeCoinJoin, GetAddress, GetOwnershipId, GetOwnershipProof, GetPublicKey, SignMessage, - SignTx, VerifyMessage, ) @@ -66,6 +65,10 @@ PATTERN_BIP49 = "m/49'/coin_type'/account'/change/address_index" PATTERN_BIP84 = "m/84'/coin_type'/account'/change/address_index" # BIP-86 for taproot: https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki PATTERN_BIP86 = "m/86'/coin_type'/account'/change/address_index" +# SLIP-25 for CoinJoin: https://github.com/satoshilabs/slips/blob/master/slip-0025.md +# Only account=0 and script_type=1 are supported for now. +PATTERN_SLIP25 = "m/10025'/coin_type'/0'/1'/change/address_index" +PATTERN_SLIP25_EXTERNAL = "m/10025'/coin_type'/0'/1'/0/address_index" # compatibility patterns, will be removed in the future PATTERN_GREENADDRESS_A = "m/[1,4]/address_index" @@ -151,13 +154,16 @@ def validate_path_against_script_type( elif coin.taproot and script_type == InputScriptType.SPENDTAPROOT: patterns.append(PATTERN_BIP86) + patterns.append(PATTERN_SLIP25) return any( PathSchema.parse(pattern, coin.slip44).match(address_n) for pattern in patterns ) -def get_schemas_for_coin(coin: coininfo.CoinInfo) -> Iterable[PathSchema]: +def get_schemas_for_coin( + coin: coininfo.CoinInfo, allow_slip25_internal: bool = False +) -> Iterable[PathSchema]: # basic patterns patterns = [ PATTERN_BIP44, @@ -205,6 +211,10 @@ def get_schemas_for_coin(coin: coininfo.CoinInfo) -> Iterable[PathSchema]: # taproot patterns if coin.taproot: patterns.append(PATTERN_BIP86) + if allow_slip25_internal: + patterns.append(PATTERN_SLIP25) + else: + patterns.append(PATTERN_SLIP25_EXTERNAL) schemas = [PathSchema.parse(pattern, coin.slip44) for pattern in patterns] @@ -234,10 +244,12 @@ def get_coin_by_name(coin_name: str | None) -> coininfo.CoinInfo: async def get_keychain_for_coin( - ctx: wire.Context, coin_name: str | None + ctx: wire.Context, + coin_name: str | None, + allow_slip25_internal: bool = False, ) -> tuple[Keychain, coininfo.CoinInfo]: coin = get_coin_by_name(coin_name) - schemas = get_schemas_for_coin(coin) + schemas = get_schemas_for_coin(coin, allow_slip25_internal) slip21_namespaces = [[b"SLIP-0019"], [b"SLIP-0024"]] keychain = await get_keychain(ctx, coin.curve_name, schemas, slip21_namespaces) return keychain, coin @@ -249,7 +261,13 @@ def with_keychain(func: HandlerWithCoinInfo[MsgOut]) -> Handler[MsgIn, MsgOut]: msg: MsgIn, auth_msg: MessageType | None = None, ) -> MsgOut: - keychain, coin = await get_keychain_for_coin(ctx, msg.coin_name) + # Allow access to the SLIP25 internal chain (CoinJoin) for SignTx and preauthorized CoinJoin operations. + allow_slip25_internal = SignTx.is_type_of(msg) or ( + auth_msg is not None and AuthorizeCoinJoin.is_type_of(auth_msg) + ) + keychain, coin = await get_keychain_for_coin( + ctx, msg.coin_name, allow_slip25_internal + ) if auth_msg: auth_obj = authorization.from_cached_message(auth_msg) return await func(ctx, msg, keychain, coin, auth_obj) diff --git a/core/src/apps/common/authorization.py b/core/src/apps/common/authorization.py index 5e084099e..7084a075d 100644 --- a/core/src/apps/common/authorization.py +++ b/core/src/apps/common/authorization.py @@ -6,7 +6,11 @@ from trezor.enums import MessageType from trezor.utils import ensure WIRE_TYPES: dict[int, tuple[int, ...]] = { - MessageType.AuthorizeCoinJoin: (MessageType.SignTx, MessageType.GetOwnershipProof), + MessageType.AuthorizeCoinJoin: ( + MessageType.SignTx, + MessageType.GetOwnershipProof, + MessageType.GetPublicKey, + ), }