feat(legacy): Implement SLIP-0025 CoinJoin accounts.

pull/2850/head
Andrew Kozlik 1 year ago committed by matejcik
parent 0b3216146e
commit 0466972f30

@ -0,0 +1 @@
Implement SLIP-0025 coinjoin accounts.

@ -487,7 +487,7 @@ static bool check_cointype(const CoinInfo *coin, uint32_t slip44, bool full) {
bool coin_path_check(const CoinInfo *coin, InputScriptType script_type,
uint32_t address_n_count, const uint32_t *address_n,
bool has_multisig, bool full_check) {
bool has_multisig, PathSchema unlock, bool full_check) {
// This function checks that the path is a recognized path for the given coin.
// Used by GetAddress to prevent ransom attacks where a user could be coerced
// to use an address with an unenumerable path and used by SignTx to ensure
@ -732,6 +732,29 @@ bool coin_path_check(const CoinInfo *coin, InputScriptType script_type,
return valid;
}
// m/10025' : SLIP25 CoinJoin
// m / purpose' / coin_type' / account' / script_type' / change /
// address_index
if (address_n[0] == PATH_SLIP25_PURPOSE) {
valid = valid && coin->has_taproot;
valid = valid && (coin->bech32_prefix != NULL);
valid = valid && (address_n_count == 6);
valid = valid && check_cointype(coin, address_n[1], full_check);
valid = valid && (address_n[2] == (PATH_HARDENED | 0)); // Only first acc.
valid = valid && (address_n[3] == (PATH_HARDENED | 1)); // Only SegWit v1.
valid = valid && (address_n[4] <= PATH_MAX_CHANGE);
valid = valid &&
((unlock == SCHEMA_SLIP25_TAPROOT) ||
(unlock == SCHEMA_SLIP25_TAPROOT_EXTERNAL && address_n[4] == 0));
valid = valid && (address_n[5] <= PATH_MAX_ADDRESS_INDEX);
if (full_check) {
// we do not support Multisig for CoinJoin
valid = valid && !has_multisig;
valid = valid && (script_type == InputScriptType_SPENDTAPROOT);
}
return valid;
}
// unknown path
return false;
}

@ -41,6 +41,12 @@
#define ser_length_size(len) ((len) < 253 ? 1 : (len) < 0x10000 ? 3 : 5)
typedef enum {
SCHEMA_NONE,
SCHEMA_SLIP25_TAPROOT,
SCHEMA_SLIP25_TAPROOT_EXTERNAL
} PathSchema;
typedef struct {
uint8_t data[64];
} Slip21Node;
@ -84,7 +90,7 @@ int cryptoIdentityFingerprint(const IdentityType *identity, uint8_t *hash);
bool coin_path_check(const CoinInfo *coin, InputScriptType script_type,
uint32_t address_n_count, const uint32_t *address_n,
bool has_multisig, bool full_check);
bool has_multisig, PathSchema unlock, bool full_check);
bool is_multisig_input_script_type(InputScriptType script_type);
bool is_multisig_output_script_type(OutputScriptType script_type);

@ -155,7 +155,8 @@ bool fsm_layoutVerifyMessage(const uint8_t *msg, uint32_t len);
bool fsm_layoutPathWarning(void);
bool fsm_checkCoinPath(const CoinInfo *coin, InputScriptType script_type,
uint32_t address_n_count, const uint32_t *address_n,
bool has_multisig, bool show_warning);
bool has_multisig, MessageType message_type,
bool show_warning);
bool fsm_getOwnershipId(uint8_t *script_pubkey, size_t script_pubkey_size,
uint8_t ownership_id[32]);

@ -128,6 +128,36 @@ void fsm_msgGetPublicKey(const GetPublicKey *msg) {
layoutHome();
}
static PathSchema fsm_getUnlockedSchema(MessageType message_type) {
if (message_type == MessageType_MessageType_AuthorizeCoinJoin) {
// Grant full access to SLIP-25 account.
return SCHEMA_SLIP25_TAPROOT;
}
if (authorization_type == MessageType_MessageType_AuthorizeCoinJoin) {
const AuthorizeCoinJoin *authorization = config_getCoinJoinAuthorization();
if (authorization == NULL ||
authorization->address_n[0] != PATH_SLIP25_PURPOSE) {
return SCHEMA_NONE;
}
// SLIP-25 access unlocked.
} else if (unlock_path == PATH_SLIP25_PURPOSE) {
// SLIP-25 access unlocked.
} else {
return SCHEMA_NONE;
}
switch (message_type) {
case MessageType_MessageType_GetOwnershipProof:
case MessageType_MessageType_SignTx:
// Grant full access to SLIP-25 account.
return SCHEMA_SLIP25_TAPROOT;
default:
// Grant access to SLIP-25 account's external chain.
return SCHEMA_SLIP25_TAPROOT_EXTERNAL;
}
}
void fsm_msgSignTx(const SignTx *msg) {
CHECK_INITIALIZED
@ -140,6 +170,8 @@ void fsm_msgSignTx(const SignTx *msg) {
CHECK_PIN
PathSchema unlock = fsm_getUnlockedSchema(MessageType_MessageType_SignTx);
const CoinInfo *coin = fsm_getCoin(msg->has_coin_name, msg->coin_name);
if (!coin) return;
@ -152,7 +184,7 @@ void fsm_msgSignTx(const SignTx *msg) {
const HDNode *node = fsm_getDerivedNode(coin->curve_name, NULL, 0, NULL);
if (!node) return;
signing_init(msg, coin, node);
signing_init(msg, coin, node, unlock);
}
void fsm_msgTxAck(TxAck *msg) {
@ -165,15 +197,18 @@ void fsm_msgTxAck(TxAck *msg) {
bool fsm_checkCoinPath(const CoinInfo *coin, InputScriptType script_type,
uint32_t address_n_count, const uint32_t *address_n,
bool has_multisig, bool show_warning) {
bool has_multisig, MessageType message_type,
bool show_warning) {
PathSchema unlock = fsm_getUnlockedSchema(message_type);
if (coin_path_check(coin, script_type, address_n_count, address_n,
has_multisig, true)) {
has_multisig, unlock, true)) {
return true;
}
if (config_getSafetyCheckLevel() == SafetyCheckLevel_Strict &&
!coin_path_check(coin, script_type, address_n_count, address_n,
has_multisig, false)) {
has_multisig, unlock, false)) {
fsm_sendFailure(FailureType_Failure_DataError, _("Forbidden key path"));
return false;
}
@ -218,6 +253,7 @@ void fsm_msgGetAddress(const GetAddress *msg) {
if (!fsm_checkCoinPath(coin, msg->script_type, msg->address_n_count,
msg->address_n, msg->has_multisig,
MessageType_MessageType_GetAddress,
msg->show_display)) {
layoutHome();
return;
@ -304,7 +340,8 @@ void fsm_msgSignMessage(const SignMessage *msg) {
if (!coin) return;
if (!fsm_checkCoinPath(coin, msg->script_type, msg->address_n_count,
msg->address_n, false, true)) {
msg->address_n, false,
MessageType_MessageType_SignMessage, true)) {
layoutHome();
return;
}
@ -424,7 +461,8 @@ void fsm_msgGetOwnershipId(const GetOwnershipId *msg) {
if (!coin) return;
if (!fsm_checkCoinPath(coin, msg->script_type, msg->address_n_count,
msg->address_n, msg->has_multisig, false)) {
msg->address_n, msg->has_multisig,
MessageType_MessageType_GetOwnershipId, false)) {
layoutHome();
return;
}
@ -662,7 +700,9 @@ void fsm_msgAuthorizeCoinJoin(const AuthorizeCoinJoin *msg) {
}
if (!fsm_checkCoinPath(coin, msg->script_type, msg->address_n_count + 2,
msg->address_n, false, !path_warning_shown)) {
msg->address_n, false,
MessageType_MessageType_AuthorizeCoinJoin,
!path_warning_shown)) {
layoutHome();
return;
}

@ -68,8 +68,6 @@ static const char *slip44_extras(uint32_t coin_type) {
#endif
#define BIP32_MAX_LAST_ELEMENT 1000000
static const char *address_n_str(const uint32_t *address_n,
size_t address_n_count,
bool address_is_account) {
@ -80,30 +78,47 @@ static const char *address_n_str(const uint32_t *address_n,
return _("Path: m");
}
enum {
ACCOUNT_NONE,
ACCOUNT_BIP44,
ACCOUNT_BIP49,
ACCOUNT_BIP84,
ACCOUNT_BIP86,
ACCOUNT_SLIP25
} account_type = ACCOUNT_NONE;
if ((address_n[1] & PATH_HARDENED) && (address_n[2] & PATH_HARDENED) &&
(address_n[address_n_count - 2] <= PATH_MAX_CHANGE) &&
(address_n[address_n_count - 1] <= PATH_MAX_ADDRESS_INDEX)) {
if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 44) {
account_type = ACCOUNT_BIP44;
} else if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 49) {
account_type = ACCOUNT_BIP49;
} else if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 84) {
account_type = ACCOUNT_BIP84;
} else if (address_n_count == 5 && address_n[0] == PATH_HARDENED + 86) {
account_type = ACCOUNT_BIP86;
} else if (address_n_count == 6 && address_n[0] == PATH_SLIP25_PURPOSE &&
(address_n[3] & PATH_HARDENED)) {
account_type = ACCOUNT_SLIP25;
}
}
// known BIP44/49/84/86 path
static char path[100];
if (address_n_count == 5 &&
(address_n[0] == (PATH_HARDENED + 44) ||
address_n[0] == (PATH_HARDENED + 49) ||
address_n[0] == (PATH_HARDENED + 84) ||
address_n[0] == (PATH_HARDENED + 86)) &&
(address_n[1] & PATH_HARDENED) && (address_n[2] & PATH_HARDENED) &&
(address_n[3] <= 1) && (address_n[4] <= BIP32_MAX_LAST_ELEMENT)) {
bool taproot = (address_n[0] == (PATH_HARDENED + 86));
bool native_segwit = (address_n[0] == (PATH_HARDENED + 84));
bool p2sh_segwit = (address_n[0] == (PATH_HARDENED + 49));
if (account_type != ACCOUNT_NONE) {
bool legacy = false;
const CoinInfo *coin = coinBySlip44(address_n[1]);
const char *abbr = 0;
if (taproot) {
if (account_type == ACCOUNT_BIP86 || account_type == ACCOUNT_SLIP25) {
if (coin && coin->has_taproot && coin->bech32_prefix) {
abbr = coin->coin_shortcut;
}
} else if (native_segwit) {
} else if (account_type == ACCOUNT_BIP84) {
if (coin && coin->has_segwit && coin->bech32_prefix) {
abbr = coin->coin_shortcut;
}
} else if (p2sh_segwit) {
} else if (account_type == ACCOUNT_BIP49) {
if (coin && coin->has_segwit) {
abbr = coin->coin_shortcut;
}
@ -125,19 +140,21 @@ static const char *address_n_str(const uint32_t *address_n,
if (abbr && accnum < 100) {
memzero(path, sizeof(path));
strlcpy(path, abbr, sizeof(path));
// account naming:
// "Legacy", "Legacy SegWit", "SegWit" and "Taproot"
// for BIP44/P2PKH, BIP49/P2SH-P2WPKH, BIP84/P2WPKH and BIP86/P2TR
// respectively.
// For non-segwit coins we use only BIP44 with no special naming.
// Account naming:
// "Legacy", "Legacy SegWit", "SegWit", "Taproot" and "Coinjoin" for
// BIP44/P2PKH, BIP49/P2SH-P2WPKH, BIP84/P2WPKH, BIP86/P2TR, SLIP25/P2TR
// respectively. For non-segwit coins we use only BIP44 with no special
// naming.
if (legacy) {
strlcat(path, " Legacy", sizeof(path));
} else if (p2sh_segwit) {
} else if (account_type == ACCOUNT_BIP49) {
strlcat(path, " L.SegWit", sizeof(path));
} else if (native_segwit) {
} else if (account_type == ACCOUNT_BIP84) {
strlcat(path, " SegWit", sizeof(path));
} else if (taproot) {
} else if (account_type == ACCOUNT_BIP86) {
strlcat(path, " Taproot", sizeof(path));
} else if (account_type == ACCOUNT_SLIP25) {
strlcat(path, " Coinjoin", sizeof(path));
}
if (address_is_account) {
strlcat(path, " address #", sizeof(path));

@ -104,6 +104,7 @@ static uint64_t orig_total_in, orig_external_in, orig_total_out,
orig_change_out;
static uint32_t progress, progress_step, progress_meta_step;
static uint32_t tx_weight;
PathSchema unlocked_schema;
typedef struct {
uint32_t inputs_count;
@ -922,7 +923,8 @@ static bool fill_input_script_pubkey(TxInputType *in) {
static bool derive_node(TxInputType *tinput) {
if (!coin_path_check(coin, tinput->script_type, tinput->address_n_count,
tinput->address_n, tinput->has_multisig, false) &&
tinput->address_n, tinput->has_multisig, unlocked_schema,
false) &&
config_getSafetyCheckLevel() == SafetyCheckLevel_Strict) {
fsm_sendFailure(FailureType_Failure_DataError, _("Forbidden key path"));
signing_abort();
@ -935,7 +937,8 @@ static bool derive_node(TxInputType *tinput) {
// through a warning screen before we sign the input.
if (!foreign_address_confirmed &&
!coin_path_check(coin, tinput->script_type, tinput->address_n_count,
tinput->address_n, tinput->has_multisig, true)) {
tinput->address_n, tinput->has_multisig, unlocked_schema,
true)) {
fsm_sendFailure(FailureType_Failure_ProcessError,
_("Transaction has changed during signing"));
signing_abort();
@ -1098,8 +1101,8 @@ static bool tx_info_init(TxInfo *tx_info, uint32_t inputs_count,
return true;
}
void signing_init(const SignTx *msg, const CoinInfo *_coin,
const HDNode *_root) {
void signing_init(const SignTx *msg, const CoinInfo *_coin, const HDNode *_root,
PathSchema unlock) {
coin = _coin;
amount_unit = msg->has_amount_unit ? msg->amount_unit : AmountUnit_BITCOIN;
serialize = msg->has_serialize ? msg->serialize : true;
@ -1150,6 +1153,7 @@ void signing_init(const SignTx *msg, const CoinInfo *_coin,
memzero(&output, sizeof(TxOutputType));
memzero(&resp, sizeof(TxRequest));
is_replacement = false;
unlocked_schema = unlock;
signing = true;
progress = 0;
// we step by 500/inputs_count per input in phase1 and phase2

@ -24,11 +24,12 @@
#include <stdint.h>
#include "bip32.h"
#include "coins.h"
#include "crypto.h"
#include "hasher.h"
#include "messages-bitcoin.pb.h"
void signing_init(const SignTx *msg, const CoinInfo *_coin,
const HDNode *_root);
void signing_init(const SignTx *msg, const CoinInfo *_coin, const HDNode *_root,
PathSchema unlock);
void signing_abort(void);
void signing_txack(TransactionType *tx);

@ -399,7 +399,6 @@ def test_sign_tx_large(client: Client):
assert delay <= max_expected_delay
@pytest.mark.skip_t1
def test_sign_tx_spend(client: Client):
# NOTE: FAKE input tx
@ -440,6 +439,7 @@ def test_sign_tx_spend(client: Client):
)
with client:
tt = client.features.model == "T"
client.set_expected_responses(
[
messages.ButtonRequest(code=B.Other),
@ -448,9 +448,9 @@ def test_sign_tx_spend(client: Client):
request_output(0),
request_output(1),
messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.ConfirmOutput),
messages.ButtonRequest(code=B.SignTx),
(tt, messages.ButtonRequest(code=B.ConfirmOutput)),
messages.ButtonRequest(code=B.SignTx),
(tt, messages.ButtonRequest(code=B.SignTx)),
request_input(0),
request_output(0),
request_output(1),
@ -612,7 +612,6 @@ def test_get_public_key(client: Client):
assert resp.xpub == EXPECTED_XPUB
@pytest.mark.skip_t1
def test_get_address(client: Client):
# Ensure that the SLIP-0025 external chain is inaccessible without user confirmation.
with pytest.raises(TrezorFailure, match="Forbidden key path"):

@ -1,7 +1,9 @@
{
"T1": {
"device_tests": {
"T1_bitcoin-test_authorize_coinjoin.py::test_get_address": "402c3f89f6ad5fd3bc78f804b376c36c918fc685cc2c77b38c6ae030af738d22",
"T1_bitcoin-test_authorize_coinjoin.py::test_get_public_key": "9b3c916759b79048a4ab3e3fe8ce0ea0cf8d4ae6cfb66a5d712f21edfdb01782",
"T1_bitcoin-test_authorize_coinjoin.py::test_sign_tx_spend": "edeb75022cc6bff15d1274ba9bac4cf41dd8ea5771436010ae07fd441dc73b69",
"T1_bitcoin-test_bcash.py::test_attack_change_input": "6111e313995d38c3970c92e48047fe4088c83666c64c6c859f69a232ad62829b",
"T1_bitcoin-test_bcash.py::test_send_bch_change": "6111e313995d38c3970c92e48047fe4088c83666c64c6c859f69a232ad62829b",
"T1_bitcoin-test_bcash.py::test_send_bch_multisig_change": "0962a2e630e06b6d20282cc241be40f41bc1648d0a26247c7c008f32a197d0cb",

Loading…
Cancel
Save