diff --git a/legacy/firmware/.changelog.d/2718.added.1 b/legacy/firmware/.changelog.d/2718.added.1 new file mode 100644 index 0000000000..2cbac75c01 --- /dev/null +++ b/legacy/firmware/.changelog.d/2718.added.1 @@ -0,0 +1 @@ +Implement SLIP-0019 proofs of ownership for native SegWit. diff --git a/legacy/firmware/fsm.c b/legacy/firmware/fsm.c index 84e02a2677..414514223d 100644 --- a/legacy/firmware/fsm.c +++ b/legacy/firmware/fsm.c @@ -371,6 +371,14 @@ bool fsm_layoutVerifyMessage(const uint8_t *msg, uint32_t len) { } } +bool fsm_layoutCommitmentData(const uint8_t *msg, uint32_t len) { + if (is_valid_ascii(msg, len)) { + return fsm_layoutPaginated(_("Commitment data"), msg, len, true); + } else { + return fsm_layoutPaginated(_("Binary commitment data"), msg, len, false); + } +} + void fsm_msgRebootToBootloader(void) { layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, _("Do you want to"), _("restart device in"), diff --git a/legacy/firmware/fsm.h b/legacy/firmware/fsm.h index eb8518fb8c..642339470e 100644 --- a/legacy/firmware/fsm.h +++ b/legacy/firmware/fsm.h @@ -80,6 +80,8 @@ void fsm_msgTxAck( void fsm_msgGetAddress(const GetAddress *msg); void fsm_msgSignMessage(const SignMessage *msg); void fsm_msgVerifyMessage(const VerifyMessage *msg); +void fsm_msgGetOwnershipId(const GetOwnershipId *msg); +void fsm_msgGetOwnershipProof(const GetOwnershipProof *msg); // crypto void fsm_msgCipherKeyValue(const CipherKeyValue *msg); diff --git a/legacy/firmware/fsm_msg_coin.h b/legacy/firmware/fsm_msg_coin.h index a7f0a05aa4..538bd336f2 100644 --- a/legacy/firmware/fsm_msg_coin.h +++ b/legacy/firmware/fsm_msg_coin.h @@ -183,6 +183,27 @@ bool fsm_checkCoinPath(const CoinInfo *coin, InputScriptType script_type, return true; } +bool fsm_checkScriptType(const CoinInfo *coin, InputScriptType script_type) { + if (!is_internal_input_script_type(script_type)) { + fsm_sendFailure(FailureType_Failure_DataError, _("Invalid script type")); + return false; + } + + if (is_segwit_input_script_type(script_type) && !coin->has_segwit) { + fsm_sendFailure(FailureType_Failure_DataError, + _("Segwit not enabled on this coin")); + return false; + } + + if (script_type == InputScriptType_SPENDTAPROOT && !coin->has_taproot) { + fsm_sendFailure(FailureType_Failure_DataError, + _("Taproot not enabled on this coin")); + return false; + } + + return true; +} + void fsm_msgGetAddress(const GetAddress *msg) { RESP_INIT(Address); @@ -373,3 +394,166 @@ void fsm_msgVerifyMessage(const VerifyMessage *msg) { } layoutHome(); } + +bool fsm_getOwnershipId(uint8_t *script_pubkey, size_t script_pubkey_size, + uint8_t ownership_id[OWNERSHIP_ID_SIZE]) { + const char *OWNERSHIP_ID_KEY_PATH[] = {"SLIP-0019", + "Ownership identification key"}; + + uint8_t ownership_id_key[32] = {0}; + if (!fsm_getSlip21Key(OWNERSHIP_ID_KEY_PATH, 2, ownership_id_key)) { + return false; + } + + hmac_sha256(ownership_id_key, sizeof(ownership_id_key), script_pubkey, + script_pubkey_size, ownership_id); + + return true; +} + +void fsm_msgGetOwnershipId(const GetOwnershipId *msg) { + RESP_INIT(OwnershipId); + + CHECK_INITIALIZED + + CHECK_PIN + + const CoinInfo *coin = fsm_getCoin(msg->has_coin_name, msg->coin_name); + if (!coin) return; + + if (!fsm_checkCoinPath(coin, msg->script_type, msg->address_n_count, + msg->address_n, msg->has_multisig, false)) { + layoutHome(); + return; + } + + if (!fsm_checkScriptType(coin, msg->script_type)) { + layoutHome(); + return; + } + + HDNode *node = fsm_getDerivedNode(coin->curve_name, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + + uint8_t script_pubkey[520] = {0}; + pb_size_t script_pubkey_size = 0; + if (!get_script_pubkey(coin, node, msg->has_multisig, &msg->multisig, + msg->script_type, script_pubkey, + &script_pubkey_size)) { + fsm_sendFailure(FailureType_Failure_ProcessError, + _("Failed to derive scriptPubKey")); + layoutHome(); + return; + } + + if (!fsm_getOwnershipId(script_pubkey, script_pubkey_size, + resp->ownership_id.bytes)) { + return; + } + + resp->ownership_id.size = 32; + + msg_write(MessageType_MessageType_OwnershipId, resp); + layoutHome(); +} + +void fsm_msgGetOwnershipProof(const GetOwnershipProof *msg) { + RESP_INIT(OwnershipProof); + + CHECK_INITIALIZED + + CHECK_PIN + + if (msg->has_multisig) { + // The legacy implementation currently only supports singlesig native segwit + // v0 and v1, the bare minimum for CoinJoin. + fsm_sendFailure(FailureType_Failure_DataError, + _("Multisig not supported.")); + layoutHome(); + return; + } + + const CoinInfo *coin = fsm_getCoin(msg->has_coin_name, msg->coin_name); + if (!coin) return; + + if (!fsm_checkCoinPath(coin, msg->script_type, msg->address_n_count, + msg->address_n, msg->has_multisig, false)) { + layoutHome(); + return; + } + + if (!fsm_checkScriptType(coin, msg->script_type)) { + layoutHome(); + return; + } + + HDNode *node = fsm_getDerivedNode(coin->curve_name, msg->address_n, + msg->address_n_count, NULL); + if (!node) return; + + uint8_t script_pubkey[520] = {0}; + pb_size_t script_pubkey_size = 0; + if (!get_script_pubkey(coin, node, msg->has_multisig, &msg->multisig, + msg->script_type, script_pubkey, + &script_pubkey_size)) { + fsm_sendFailure(FailureType_Failure_ProcessError, + _("Failed to derive scriptPubKey")); + layoutHome(); + return; + } + + uint8_t ownership_id[OWNERSHIP_ID_SIZE] = {0}; + if (!fsm_getOwnershipId(script_pubkey, script_pubkey_size, ownership_id)) { + return; + } + + // Providing an ownership ID is optional in case of singlesig, but if one is + // provided, then it should match. + if (msg->ownership_ids_count) { + if (msg->ownership_ids_count != 1 || + msg->ownership_ids[0].size != sizeof(ownership_id) || + memcmp(ownership_id, msg->ownership_ids[0].bytes, + sizeof(ownership_id)) != 0) { + fsm_sendFailure(FailureType_Failure_DataError, + _("Invalid ownership identifier")); + layoutHome(); + return; + } + } + + // In order to set the "user confirmation" bit in the proof, the user must + // actually confirm. + uint8_t flags = 0; + if (msg->user_confirmation) { + flags |= 1; + layoutConfirmOwnershipProof(); + if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + + if (msg->has_commitment_data) { + if (!fsm_layoutCommitmentData(msg->commitment_data.bytes, + msg->commitment_data.size)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL); + layoutHome(); + return; + } + } + } + + if (!get_ownership_proof(coin, msg->script_type, node, flags, ownership_id, + script_pubkey, script_pubkey_size, + msg->commitment_data.bytes, + msg->commitment_data.size, resp)) { + fsm_sendFailure(FailureType_Failure_ProcessError, _("Signing failed")); + + layoutHome(); + return; + } + + msg_write(MessageType_MessageType_OwnershipProof, resp); + layoutHome(); +} diff --git a/legacy/firmware/layout2.c b/legacy/firmware/layout2.c index 37046f3cac..f7c4624c2c 100644 --- a/legacy/firmware/layout2.c +++ b/legacy/firmware/layout2.c @@ -1315,3 +1315,9 @@ void layoutConfirmHash(const BITMAP *icon, const char *description, layoutButtonYes(_("Confirm"), &bmp_btn_confirm); oledRefresh(); } + +void layoutConfirmOwnershipProof(void) { + layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL, + _("Do you want to"), _("create a proof of"), + _("ownership?"), NULL, NULL, NULL); +} diff --git a/legacy/firmware/layout2.h b/legacy/firmware/layout2.h index 6a0195680f..5934d288d0 100644 --- a/legacy/firmware/layout2.h +++ b/legacy/firmware/layout2.h @@ -115,6 +115,8 @@ void layoutConfirmSafetyChecks(SafetyCheckLevel safety_checks_level); void layoutConfirmHash(const BITMAP *icon, const char *description, const uint8_t *hash, uint32_t len); +void layoutConfirmOwnershipProof(void); + const char **split_message(const uint8_t *msg, uint32_t len, uint32_t rowlen); const char **split_message_hex(const uint8_t *msg, uint32_t len); diff --git a/legacy/firmware/protob/Makefile b/legacy/firmware/protob/Makefile index a00dd41a76..fc9e131cd4 100644 --- a/legacy/firmware/protob/Makefile +++ b/legacy/firmware/protob/Makefile @@ -4,7 +4,7 @@ endif SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn \ DebugLinkRecordScreen DebugLinkEraseSdCard DebugLinkWatchLayout \ - GetOwnershipProof OwnershipProof GetOwnershipId OwnershipId AuthorizeCoinJoin DoPreauthorized \ + AuthorizeCoinJoin DoPreauthorized \ CancelAuthorization DebugLinkLayout GetNonce SetBusy UnlockPath \ TxAckInput TxAckOutput TxAckPrev TxAckPaymentRequest \ EthereumSignTypedData EthereumTypedDataStructRequest EthereumTypedDataStructAck \ diff --git a/legacy/firmware/protob/messages-bitcoin.options b/legacy/firmware/protob/messages-bitcoin.options index 020bb69c85..f5dd2b736d 100644 --- a/legacy/firmware/protob/messages-bitcoin.options +++ b/legacy/firmware/protob/messages-bitcoin.options @@ -34,8 +34,8 @@ TxInputType.address_n max_count:8 TxInputType.prev_hash max_size:32 TxInputType.script_sig max_size:1650 TxInputType.witness max_size:109 -TxInputType.ownership_proof max_size:171 -TxInputType.commitment_data max_size:32 +TxInputType.ownership_proof max_size:147 +TxInputType.commitment_data max_size:70 TxInputType.orig_hash max_size:32 TxInputType.script_pubkey max_size:520 @@ -62,8 +62,8 @@ TxInput.address_n max_count:8 TxInput.prev_hash max_size:32 TxInput.script_sig max_size:1650 TxInput.witness max_size:109 -TxInput.ownership_proof max_size:171 -TxInput.commitment_data max_size:32 +TxInput.ownership_proof max_size:147 +TxInput.commitment_data max_size:70 TxInput.orig_hash max_size:32 TxInput.script_pubkey max_size:520 @@ -79,12 +79,21 @@ PrevOutput.script_pubkey max_size:520 TxAckPrevExtraDataWrapper.extra_data_chunk type:FT_IGNORE +GetOwnershipId.address_n max_count:8 +GetOwnershipId.coin_name max_size:21 + +OwnershipId.ownership_id max_size:32 + +GetOwnershipProof.address_n max_count:8 +GetOwnershipProof.coin_name max_size:21 +GetOwnershipProof.ownership_ids max_count:15 max_size:32 +GetOwnershipProof.commitment_data max_size:70 + +OwnershipProof.ownership_proof max_size:147 +OwnershipProof.signature max_size:71 + # Unused messages. AuthorizeCoinJoin skip_message:true -GetOwnershipId skip_message:true -OwnershipId skip_message:true -GetOwnershipProof skip_message:true -OwnershipProof skip_message:true TxAckPaymentRequest skip_message:true PaymentRequestMemo skip_message:true CoinJoinRequest skip_message:true diff --git a/legacy/firmware/transaction.c b/legacy/firmware/transaction.c index 59ea094620..731f7788e5 100644 --- a/legacy/firmware/transaction.c +++ b/legacy/firmware/transaction.c @@ -79,6 +79,8 @@ static const uint8_t segwit_header[2] = {0, 1}; +static const uint8_t SLIP19_VERSION_MAGIC[] = {0x53, 0x4c, 0x00, 0x19}; + static inline uint32_t op_push_size(uint32_t i) { if (i < 0x4C) { return 1; @@ -413,6 +415,20 @@ int compile_output(const CoinInfo *coin, AmountUnit amount_unit, return out->script_pubkey.size; } +int get_script_pubkey(const CoinInfo *coin, HDNode *node, bool has_multisig, + const MultisigRedeemScriptType *multisig, + InputScriptType script_type, uint8_t *script_pubkey, + pb_size_t *script_pubkey_size) { + char address[MAX_ADDR_SIZE] = {0}; + bool res = true; + res = res && (hdnode_fill_public_key(node) == 0); + res = res && compute_address(coin, script_type, node, has_multisig, multisig, + address); + res = res && address_to_script_pubkey(coin, address, script_pubkey, + script_pubkey_size); + return res; +} + int fill_input_script_pubkey(const CoinInfo *coin, const HDNode *root, TxInputType *in) { if (in->script_type == InputScriptType_EXTERNAL) { @@ -422,16 +438,13 @@ int fill_input_script_pubkey(const CoinInfo *coin, const HDNode *root, static CONFIDENTIAL HDNode node; memcpy(&node, root, sizeof(HDNode)); - char address[MAX_ADDR_SIZE] = {0}; - bool res = true; + int res = true; res = res && hdnode_private_ckd_cached(&node, in->address_n, in->address_n_count, NULL); - res = res && (hdnode_fill_public_key(&node) == 0); - res = res && compute_address(coin, in->script_type, &node, in->has_multisig, - &in->multisig, address); + res = res && get_script_pubkey(coin, &node, in->has_multisig, &in->multisig, + in->script_type, in->script_pubkey.bytes, + &in->script_pubkey.size); memzero(&node, sizeof(node)); - res = res && address_to_script_pubkey(coin, address, in->script_pubkey.bytes, - &in->script_pubkey.size); in->has_script_pubkey = res; return res; } @@ -1222,3 +1235,78 @@ uint32_t tx_decred_witness_weight(const TxInputType *txinput) { return 4 * size; } #endif + +bool get_ownership_proof(const CoinInfo *coin, InputScriptType script_type, + const HDNode *node, uint8_t flags, + const uint8_t ownership_id[OWNERSHIP_ID_SIZE], + const uint8_t *script_pubkey, + size_t script_pubkey_size, + const uint8_t *commitment_data, + size_t commitment_data_size, OwnershipProof *out) { + size_t r = 0; + + // Write versionMagic (4 bytes). + memcpy(out->ownership_proof.bytes + r, SLIP19_VERSION_MAGIC, + sizeof(SLIP19_VERSION_MAGIC)); + r += sizeof(SLIP19_VERSION_MAGIC); + + // Write flags (1 byte). + out->ownership_proof.bytes[r] = flags; + r += 1; + + // Write number of ownership IDs (1 byte). + r += ser_length(1, out->ownership_proof.bytes + r); + + // Write ownership ID (32 bytes). + memcpy(out->ownership_proof.bytes + r, ownership_id, OWNERSHIP_ID_SIZE); + r += OWNERSHIP_ID_SIZE; + + // Compute sighash = SHA-256(proofBody || proofFooter). + Hasher hasher = {0}; + uint8_t sighash[SHA256_DIGEST_LENGTH] = {0}; + hasher_InitParam(&hasher, HASHER_SHA2, NULL, 0); + hasher_Update(&hasher, out->ownership_proof.bytes, r); + tx_script_hash(&hasher, script_pubkey_size, script_pubkey); + tx_script_hash(&hasher, commitment_data_size, commitment_data); + hasher_Final(&hasher, sighash); + + // Write proofSignature. + if (script_type == InputScriptType_SPENDWITNESS) { + if (!tx_sign_ecdsa(coin->curve->params, node->private_key, sighash, + out->signature.bytes, &out->signature.size)) { + return false; + } + // Write length-prefixed empty scriptSig (1 byte). + r += ser_length(0, out->ownership_proof.bytes + r); + + // Write + // 1. number of stack items (1 byte) + // 2. signature + sighash type length (1 byte) + // 3. DER-encoded signature (max. 71 bytes) + // 4. sighash type (1 byte) + // 5. public key length (1 byte) + // 6. public key (33 bytes) + r += serialize_p2wpkh_witness(out->signature.bytes, out->signature.size, + node->public_key, 33, SIGHASH_ALL, + out->ownership_proof.bytes + r); + } else if (script_type == InputScriptType_SPENDTAPROOT) { + if (!tx_sign_bip340(node->private_key, sighash, out->signature.bytes, + &out->signature.size)) { + return false; + } + // Write length-prefixed empty scriptSig (1 byte). + r += ser_length(0, out->ownership_proof.bytes + r); + + // Write + // 1. number of stack items (1 byte) + // 2. signature length (1 byte) + // 3. signature (64 bytes) + r += serialize_p2tr_witness(out->signature.bytes, out->signature.size, 0, + out->ownership_proof.bytes + r); + } else { + return false; + } + + out->ownership_proof.size = r; + return true; +} diff --git a/legacy/firmware/transaction.h b/legacy/firmware/transaction.h index 03f59aeec4..e9641b561a 100644 --- a/legacy/firmware/transaction.h +++ b/legacy/firmware/transaction.h @@ -30,6 +30,8 @@ #define TX_OVERWINTERED 0x80000000 +#define OWNERSHIP_ID_SIZE 32 + enum { // Signature hash type with the same semantics as SIGHASH_ALL, but instead of // having to include the byte in the signature, it is implied. @@ -102,6 +104,10 @@ bool tx_sign_bip340(const uint8_t *private_key, const uint8_t *hash, int compile_output(const CoinInfo *coin, AmountUnit amount_unit, const HDNode *root, TxOutputType *in, TxOutputBinType *out, bool needs_confirm); +int get_script_pubkey(const CoinInfo *coin, HDNode *node, bool has_multisig, + const MultisigRedeemScriptType *multisig, + InputScriptType script_type, uint8_t *script_pubkey, + pb_size_t *script_pubkey_size); int fill_input_script_pubkey(const CoinInfo *coin, const HDNode *root, TxInputType *in); @@ -139,5 +145,12 @@ void tx_hash_final(TxStruct *t, uint8_t *hash, bool reverse); uint32_t tx_input_weight(const CoinInfo *coin, const TxInputType *txinput); uint32_t tx_output_weight(const CoinInfo *coin, const TxOutputType *txoutput); uint32_t tx_decred_witness_weight(const TxInputType *txinput); +bool get_ownership_proof(const CoinInfo *coin, InputScriptType script_type, + const HDNode *node, uint8_t flags, + const uint8_t ownership_id[OWNERSHIP_ID_SIZE], + const uint8_t *script_pubkey, + size_t script_pubkey_size, + const uint8_t *commitment_data, + size_t commitment_data_size, OwnershipProof *out); #endif diff --git a/tests/device_tests/bitcoin/test_getownershipproof.py b/tests/device_tests/bitcoin/test_getownershipproof.py index ba197a9501..b21fe944b0 100644 --- a/tests/device_tests/bitcoin/test_getownershipproof.py +++ b/tests/device_tests/bitcoin/test_getownershipproof.py @@ -21,8 +21,6 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client from trezorlib.exceptions import TrezorFailure from trezorlib.tools import parse_path -pytestmark = pytest.mark.skip_t1 - def test_p2wpkh_ownership_id(client: Client): ownership_id = btc.get_ownership_id( diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index dc458289dd..06384cf6a0 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -101,6 +101,14 @@ "T1_bitcoin-test_getaddress_show.py::test_show_multisig_15": "40f652d0e899d528605f472f47ec6cb727ced8fe90588a8904b46ed39c2088e8", "T1_bitcoin-test_getaddress_show.py::test_show_multisig_3": "05e4e5cd014bf96373788e4563a48f5cdb4c54d358a88e8a29247c2825c5669e", "T1_bitcoin-test_getaddress_show.py::test_show_unrecognized_path": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", +"T1_bitcoin-test_getownershipproof.py::test_attack_ownership_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", +"T1_bitcoin-test_getownershipproof.py::test_confirm_ownership_proof": "7e6fcf4df33bf876103f0f8b547c0bd7e5babd34cfe5be43d62c08fbc67f525b", +"T1_bitcoin-test_getownershipproof.py::test_confirm_ownership_proof_with_data": "c0c6509ae54e7199cb457ababbc71cdb0ef1c53d535ba4b9fbf59db2cb5171cd", +"T1_bitcoin-test_getownershipproof.py::test_fake_ownership_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", +"T1_bitcoin-test_getownershipproof.py::test_p2tr_ownership_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", +"T1_bitcoin-test_getownershipproof.py::test_p2tr_ownership_proof": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", +"T1_bitcoin-test_getownershipproof.py::test_p2wpkh_ownership_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", +"T1_bitcoin-test_getownershipproof.py::test_p2wpkh_ownership_proof": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "T1_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path0-xpub6BiVtCpG9fQPx-40a56ca3": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "T1_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path1-xpub6BiVtCpG9fQQR-1abafc98": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "T1_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path2-xpub6FVDRC1jiWNTu-47a67414": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",