mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-23 06:48:16 +00:00
feat(legacy): Implement SLIP-0025 CoinJoin accounts.
This commit is contained in:
parent
0b3216146e
commit
0466972f30
1
legacy/firmware/.changelog.d/2718.added.3
Normal file
1
legacy/firmware/.changelog.d/2718.added.3
Normal file
@ -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…
Reference in New Issue
Block a user