What changed, and why it matters
This commit adds a new 'coin purchase memo' feature to BitBox02 payment requests. It lets a payment request include details about a separate coin purchase (e.g., '0.25 ETH to address 0x...') and verifies that the listed Ethereum address really belongs to the wallet by deriving it from a provided keypath. The change is mostly a feature addition, but it introduces cross-currency validation logic and a TODO noting that the user-interface confirmation for this new memo type is not yet implemented.
Treat this as a normal feature commit, but review the TODO in user_verify before release to ensure users are explicitly shown CoinPurchaseMemo details before signing. Audit that the new Ethereum keypath validation cannot be bypassed and that the Disabled error path in signtx.rs is handled correctly in production builds.
Security signals we found
New payment-request memo type with address ownership verification via keypath derivation
TODO left in user_verify indicating UI confirmation for CoinPurchaseMemo is not yet implemented
Cross-module dependency introduced: Bitcoin payment-request validation now calls Ethereum address derivation
Compile-time feature gating (app-ethereum) for the new validation path
Sighash explicitly excludes address_derivation, which is noted as BitBox-specific and not part of SLIP-24
Evidence from the diff
The commit extends the BTC payment request protobuf with a new Memo variant, CoinPurchaseMemo, containing coin_type, amount, address, and an optional address_derivation (currently only EthAddressDerivation with a keypath). Rust code computes a SLIP-24 sighash over the new fields, derives the Ethereum address from the supplied keypath using a new shared derive_address helper, and rejects the payment request if the derived address does not match the memo’s address. The user_verify function has a TODO placeholder for the new memo’s on-screen confirmation and currently falls through to an error for CoinPurchaseMemo. The feature is gated by the app-ethereum compile-time feature; without it, validation returns ValidationError::Disabled.
Changed components
messages/btc.protopy/bitbox02/bitbox02/communication/generated/btc_pb2.pypy/bitbox02/bitbox02/communication/generated/btc_pb2.pyisrc/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rssrc/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rssrc/rust/bitbox02-rust/src/hww/api/ethereum.rssrc/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rssrc/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rsInspect captured patch +419 / −41
diff --git a/messages/btc.proto b/messages/btc.proto
index bee46ad..97ed257 100644
--- a/messages/btc.proto
+++ b/messages/btc.proto
@@ -246,8 +246,22 @@ message BTCPaymentRequestRequest {
message TextMemo {
string note = 1;
}
+ message CoinPurchaseMemo {
+ uint32 coin_type = 1; // SLIP-44 coin type
+ string amount = 2; // Human-readable amount (e.g. "0.25 ETH")
+ string address = 3; // Address to send the purchased coins to
+ // Derivation info for verifying address ownership.
+ // NOT part of the SLIP-24 sighash.
+ message EthAddressDerivation {
+ repeated uint32 keypath = 1; // Keypath to the address
+ }
+ oneof address_derivation {
+ EthAddressDerivation eth = 4;
+ }
+ }
oneof memo {
TextMemo text_memo = 1;
+ CoinPurchaseMemo coin_purchase_memo = 2;
}
}
diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py
index 251ae66..25d1e6d 100644
--- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py
+++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py
@@ -15,17 +15,17 @@ from . import common_pb2 as common__pb2
from . import antiklepto_pb2 as antiklepto__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tbtc.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xc6\x04\n\x0f\x42TCScriptConfig\x12G\n\x0bsimple_type\x18\x01 \x01(\x0e\x32\x30.shiftcrypto.bitbox02.BTCScriptConfig.SimpleTypeH\x00\x12\x42\n\x08multisig\x18\x02 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCScriptConfig.MultisigH\x00\x12>\n\x06policy\x18\x03 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCScriptConfig.PolicyH\x00\x1a\xd9\x01\n\x08Multisig\x12\x11\n\tthreshold\x18\x01 \x01(\r\x12)\n\x05xpubs\x18\x02 \x03(\x0b\x32\x1a.shiftcrypto.bitbox02.XPub\x12\x16\n\x0eour_xpub_index\x18\x03 \x01(\r\x12N\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.BTCScriptConfig.Multisig.ScriptType\"\'\n\nScriptType\x12\t\n\x05P2WSH\x10\x00\x12\x0e\n\nP2WSH_P2SH\x10\x01\x1aK\n\x06Policy\x12\x0e\n\x06policy\x18\x01 \x01(\t\x12\x31\n\x04keys\x18\x02 \x03(\x0b\x32#.shiftcrypto.bitbox02.KeyOriginInfo\"3\n\nSimpleType\x12\x0f\n\x0bP2WPKH_P2SH\x10\x00\x12\n\n\x06P2WPKH\x10\x01\x12\x08\n\x04P2TR\x10\x02\x42\x08\n\x06\x63onfig\"\xfc\x02\n\rBTCPubRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x41\n\txpub_type\x18\x03 \x01(\x0e\x32,.shiftcrypto.bitbox02.BTCPubRequest.XPubTypeH\x00\x12>\n\rscript_config\x18\x04 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfigH\x00\x12\x0f\n\x07\x64isplay\x18\x05 \x01(\x08\"\x8e\x01\n\x08XPubType\x12\x08\n\x04TPUB\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04YPUB\x10\x02\x12\x08\n\x04ZPUB\x10\x03\x12\x08\n\x04VPUB\x10\x04\x12\x08\n\x04UPUB\x10\x05\x12\x10\n\x0c\x43\x41PITAL_VPUB\x10\x06\x12\x10\n\x0c\x43\x41PITAL_ZPUB\x10\x07\x12\x10\n\x0c\x43\x41PITAL_UPUB\x10\x08\x12\x10\n\x0c\x43\x41PITAL_YPUB\x10\tB\x08\n\x06output\"\xdf\x01\n\x0f\x42TCXpubsRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x41\n\txpub_type\x18\x02 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCXpubsRequest.XPubType\x12/\n\x08keypaths\x18\x03 \x03(\x0b\x32\x1d.shiftcrypto.bitbox02.Keypath\"+\n\x08XPubType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04TPUB\x10\x02\"k\n\x1a\x42TCScriptConfigWithKeypath\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\xbf\x03\n\x12\x42TCSignInitRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12H\n\x0escript_configs\x18\x02 \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0f\n\x07version\x18\x04 \x01(\r\x12\x12\n\nnum_inputs\x18\x05 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x06 \x01(\r\x12\x10\n\x08locktime\x18\x07 \x01(\r\x12H\n\x0b\x66ormat_unit\x18\x08 \x01(\x0e\x32\x33.shiftcrypto.bitbox02.BTCSignInitRequest.FormatUnit\x12\'\n\x1f\x63ontains_silent_payment_outputs\x18\t \x01(\x08\x12O\n\x15output_script_configs\x18\n \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\"\xc4\x03\n\x13\x42TCSignNextResponse\x12<\n\x04type\x18\x01 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCSignNextResponse.Type\x12\r\n\x05index\x18\x02 \x01(\r\x12\x15\n\rhas_signature\x18\x03 \x01(\x08\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x12\n\nprev_index\x18\x05 \x01(\r\x12W\n\x1d\x61nti_klepto_signer_commitment\x18\x06 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitment\x12!\n\x19generated_output_pkscript\x18\x07 \x01(\x0c\x12!\n\x19silent_payment_dleq_proof\x18\x08 \x01(\x0c\"\x82\x01\n\x04Type\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01\x12\x08\n\x04\x44ONE\x10\x02\x12\x0f\n\x0bPREVTX_INIT\x10\x03\x12\x10\n\x0cPREVTX_INPUT\x10\x04\x12\x11\n\rPREVTX_OUTPUT\x10\x05\x12\x0e\n\nHOST_NONCE\x10\x06\x12\x13\n\x0fPAYMENT_REQUEST\x10\x07\"\xea\x01\n\x13\x42TCSignInputRequest\x12\x13\n\x0bprevOutHash\x18\x01 \x01(\x0c\x12\x14\n\x0cprevOutIndex\x18\x02 \x01(\r\x12\x14\n\x0cprevOutValue\x18\x03 \x01(\x04\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x0f\n\x07keypath\x18\x06 \x03(\r\x12\x1b\n\x13script_config_index\x18\x07 \x01(\r\x12R\n\x15host_nonce_commitment\x18\x08 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"\x9f\x03\n\x14\x42TCSignOutputRequest\x12\x0c\n\x04ours\x18\x01 \x01(\x08\x12\x31\n\x04type\x18\x02 \x01(\x0e\x32#.shiftcrypto.bitbox02.BTCOutputType\x12\r\n\x05value\x18\x03 \x01(\x04\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x0f\n\x07keypath\x18\x05 \x03(\r\x12\x1b\n\x13script_config_index\x18\x06 \x01(\r\x12\"\n\x15payment_request_index\x18\x07 \x01(\rH\x00\x88\x01\x01\x12P\n\x0esilent_payment\x18\x08 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCSignOutputRequest.SilentPayment\x12\'\n\x1aoutput_script_config_index\x18\t \x01(\rH\x01\x88\x01\x01\x1a \n\rSilentPayment\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x18\n\x16_payment_request_indexB\x1d\n\x1b_output_script_config_index\"\x99\x01\n\x1b\x42TCScriptConfigRegistration\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\x0c\n\nBTCSuccess\"m\n\"BTCIsScriptConfigRegisteredRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\"<\n#BTCIsScriptConfigRegisteredResponse\x12\x15\n\ris_registered\x18\x01 \x01(\x08\"\xfc\x01\n\x1e\x42TCRegisterScriptConfigRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\x12\x0c\n\x04name\x18\x02 \x01(\t\x12P\n\txpub_type\x18\x03 \x01(\x0e\x32=.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequest.XPubType\"1\n\x08XPubType\x12\x11\n\rAUTO_ELECTRUM\x10\x00\x12\x12\n\x0e\x41UTO_XPUB_TPUB\x10\x01\"b\n\x14\x42TCPrevTxInitRequest\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x12\n\nnum_inputs\x18\x02 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\"r\n\x15\x42TCPrevTxInputRequest\x12\x15\n\rprev_out_hash\x18\x01 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x02 \x01(\r\x12\x18\n\x10signature_script\x18\x03 \x01(\x0c\x12\x10\n\x08sequence\x18\x04 \x01(\r\">\n\x16\x42TCPrevTxOutputRequest\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x15\n\rpubkey_script\x18\x02 \x01(\x0c\"\xab\x02\n\x18\x42TCPaymentRequestRequest\x12\x16\n\x0erecipient_name\x18\x01 \x01(\t\x12\x42\n\x05memos\x18\x02 \x03(\x0b\x32\x33.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x14\n\x0ctotal_amount\x18\x04 \x01(\x04\x12\x11\n\tsignature\x18\x05 \x01(\x0c\x1a{\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\tB\x06\n\x04memo\"\xee\x01\n\x15\x42TCSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12G\n\rscript_config\x18\x02 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"+\n\x16\x42TCSignMessageResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xb9\x05\n\nBTCRequest\x12_\n\x1bis_script_config_registered\x18\x01 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredRequestH\x00\x12V\n\x16register_script_config\x18\x02 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequestH\x00\x12\x41\n\x0bprevtx_init\x18\x03 \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCPrevTxInitRequestH\x00\x12\x43\n\x0cprevtx_input\x18\x04 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCPrevTxInputRequestH\x00\x12\x45\n\rprevtx_output\x18\x05 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCPrevTxOutputRequestH\x00\x12\x43\n\x0csign_message\x18\x06 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x07 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12I\n\x0fpayment_request\x18\x08 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCPaymentRequestRequestH\x00\x12\x36\n\x05xpubs\x18\t \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCXpubsRequestH\x00\x42\t\n\x07request\"\xc4\x03\n\x0b\x42TCResponse\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCSuccessH\x00\x12`\n\x1bis_script_config_registered\x18\x02 \x01(\x0b\x32\x39.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredResponseH\x00\x12>\n\tsign_next\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x44\n\x0csign_message\x18\x04 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCSignMessageResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12\x32\n\x04pubs\x18\x06 \x01(\x0b\x32\".shiftcrypto.bitbox02.PubsResponseH\x00\x42\n\n\x08response*9\n\x07\x42TCCoin\x12\x07\n\x03\x42TC\x10\x00\x12\x08\n\x04TBTC\x10\x01\x12\x07\n\x03LTC\x10\x02\x12\x08\n\x04TLTC\x10\x03\x12\x08\n\x04RBTC\x10\x04*a\n\rBTCOutputType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05P2PKH\x10\x01\x12\x08\n\x04P2SH\x10\x02\x12\n\n\x06P2WPKH\x10\x03\x12\t\n\x05P2WSH\x10\x04\x12\x08\n\x04P2TR\x10\x05\x12\r\n\tOP_RETURN\x10\x06\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tbtc.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xc6\x04\n\x0f\x42TCScriptConfig\x12G\n\x0bsimple_type\x18\x01 \x01(\x0e\x32\x30.shiftcrypto.bitbox02.BTCScriptConfig.SimpleTypeH\x00\x12\x42\n\x08multisig\x18\x02 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCScriptConfig.MultisigH\x00\x12>\n\x06policy\x18\x03 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCScriptConfig.PolicyH\x00\x1a\xd9\x01\n\x08Multisig\x12\x11\n\tthreshold\x18\x01 \x01(\r\x12)\n\x05xpubs\x18\x02 \x03(\x0b\x32\x1a.shiftcrypto.bitbox02.XPub\x12\x16\n\x0eour_xpub_index\x18\x03 \x01(\r\x12N\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.BTCScriptConfig.Multisig.ScriptType\"\'\n\nScriptType\x12\t\n\x05P2WSH\x10\x00\x12\x0e\n\nP2WSH_P2SH\x10\x01\x1aK\n\x06Policy\x12\x0e\n\x06policy\x18\x01 \x01(\t\x12\x31\n\x04keys\x18\x02 \x03(\x0b\x32#.shiftcrypto.bitbox02.KeyOriginInfo\"3\n\nSimpleType\x12\x0f\n\x0bP2WPKH_P2SH\x10\x00\x12\n\n\x06P2WPKH\x10\x01\x12\x08\n\x04P2TR\x10\x02\x42\x08\n\x06\x63onfig\"\xfc\x02\n\rBTCPubRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x41\n\txpub_type\x18\x03 \x01(\x0e\x32,.shiftcrypto.bitbox02.BTCPubRequest.XPubTypeH\x00\x12>\n\rscript_config\x18\x04 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfigH\x00\x12\x0f\n\x07\x64isplay\x18\x05 \x01(\x08\"\x8e\x01\n\x08XPubType\x12\x08\n\x04TPUB\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04YPUB\x10\x02\x12\x08\n\x04ZPUB\x10\x03\x12\x08\n\x04VPUB\x10\x04\x12\x08\n\x04UPUB\x10\x05\x12\x10\n\x0c\x43\x41PITAL_VPUB\x10\x06\x12\x10\n\x0c\x43\x41PITAL_ZPUB\x10\x07\x12\x10\n\x0c\x43\x41PITAL_UPUB\x10\x08\x12\x10\n\x0c\x43\x41PITAL_YPUB\x10\tB\x08\n\x06output\"\xdf\x01\n\x0f\x42TCXpubsRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x41\n\txpub_type\x18\x02 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCXpubsRequest.XPubType\x12/\n\x08keypaths\x18\x03 \x03(\x0b\x32\x1d.shiftcrypto.bitbox02.Keypath\"+\n\x08XPubType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04TPUB\x10\x02\"k\n\x1a\x42TCScriptConfigWithKeypath\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\xbf\x03\n\x12\x42TCSignInitRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12H\n\x0escript_configs\x18\x02 \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0f\n\x07version\x18\x04 \x01(\r\x12\x12\n\nnum_inputs\x18\x05 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x06 \x01(\r\x12\x10\n\x08locktime\x18\x07 \x01(\r\x12H\n\x0b\x66ormat_unit\x18\x08 \x01(\x0e\x32\x33.shiftcrypto.bitbox02.BTCSignInitRequest.FormatUnit\x12\'\n\x1f\x63ontains_silent_payment_outputs\x18\t \x01(\x08\x12O\n\x15output_script_configs\x18\n \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\"\xc4\x03\n\x13\x42TCSignNextResponse\x12<\n\x04type\x18\x01 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCSignNextResponse.Type\x12\r\n\x05index\x18\x02 \x01(\r\x12\x15\n\rhas_signature\x18\x03 \x01(\x08\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x12\n\nprev_index\x18\x05 \x01(\r\x12W\n\x1d\x61nti_klepto_signer_commitment\x18\x06 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitment\x12!\n\x19generated_output_pkscript\x18\x07 \x01(\x0c\x12!\n\x19silent_payment_dleq_proof\x18\x08 \x01(\x0c\"\x82\x01\n\x04Type\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01\x12\x08\n\x04\x44ONE\x10\x02\x12\x0f\n\x0bPREVTX_INIT\x10\x03\x12\x10\n\x0cPREVTX_INPUT\x10\x04\x12\x11\n\rPREVTX_OUTPUT\x10\x05\x12\x0e\n\nHOST_NONCE\x10\x06\x12\x13\n\x0fPAYMENT_REQUEST\x10\x07\"\xea\x01\n\x13\x42TCSignInputRequest\x12\x13\n\x0bprevOutHash\x18\x01 \x01(\x0c\x12\x14\n\x0cprevOutIndex\x18\x02 \x01(\r\x12\x14\n\x0cprevOutValue\x18\x03 \x01(\x04\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x0f\n\x07keypath\x18\x06 \x03(\r\x12\x1b\n\x13script_config_index\x18\x07 \x01(\r\x12R\n\x15host_nonce_commitment\x18\x08 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"\x9f\x03\n\x14\x42TCSignOutputRequest\x12\x0c\n\x04ours\x18\x01 \x01(\x08\x12\x31\n\x04type\x18\x02 \x01(\x0e\x32#.shiftcrypto.bitbox02.BTCOutputType\x12\r\n\x05value\x18\x03 \x01(\x04\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x0f\n\x07keypath\x18\x05 \x03(\r\x12\x1b\n\x13script_config_index\x18\x06 \x01(\r\x12\"\n\x15payment_request_index\x18\x07 \x01(\rH\x00\x88\x01\x01\x12P\n\x0esilent_payment\x18\x08 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCSignOutputRequest.SilentPayment\x12\'\n\x1aoutput_script_config_index\x18\t \x01(\rH\x01\x88\x01\x01\x1a \n\rSilentPayment\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x18\n\x16_payment_request_indexB\x1d\n\x1b_output_script_config_index\"\x99\x01\n\x1b\x42TCScriptConfigRegistration\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\x0c\n\nBTCSuccess\"m\n\"BTCIsScriptConfigRegisteredRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\"<\n#BTCIsScriptConfigRegisteredResponse\x12\x15\n\ris_registered\x18\x01 \x01(\x08\"\xfc\x01\n\x1e\x42TCRegisterScriptConfigRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\x12\x0c\n\x04name\x18\x02 \x01(\t\x12P\n\txpub_type\x18\x03 \x01(\x0e\x32=.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequest.XPubType\"1\n\x08XPubType\x12\x11\n\rAUTO_ELECTRUM\x10\x00\x12\x12\n\x0e\x41UTO_XPUB_TPUB\x10\x01\"b\n\x14\x42TCPrevTxInitRequest\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x12\n\nnum_inputs\x18\x02 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\"r\n\x15\x42TCPrevTxInputRequest\x12\x15\n\rprev_out_hash\x18\x01 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x02 \x01(\r\x12\x18\n\x10signature_script\x18\x03 \x01(\x0c\x12\x10\n\x08sequence\x18\x04 \x01(\r\">\n\x16\x42TCPrevTxOutputRequest\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x15\n\rpubkey_script\x18\x02 \x01(\x0c\"\x82\x05\n\x18\x42TCPaymentRequestRequest\x12\x16\n\x0erecipient_name\x18\x01 \x01(\t\x12\x42\n\x05memos\x18\x02 \x03(\x0b\x32\x33.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x14\n\x0ctotal_amount\x18\x04 \x01(\x04\x12\x11\n\tsignature\x18\x05 \x01(\x0c\x1a\xd1\x03\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x12\x62\n\x12\x63oin_purchase_memo\x18\x02 \x01(\x0b\x32\x44.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\t\x1a\xef\x01\n\x10\x43oinPurchaseMemo\x12\x11\n\tcoin_type\x18\x01 \x01(\r\x12\x0e\n\x06\x61mount\x18\x02 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12h\n\x03\x65th\x18\x04 \x01(\x0b\x32Y.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.EthAddressDerivationH\x00\x1a\'\n\x14\x45thAddressDerivation\x12\x0f\n\x07keypath\x18\x01 \x03(\rB\x14\n\x12\x61\x64\x64ress_derivationB\x06\n\x04memo\"\xee\x01\n\x15\x42TCSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12G\n\rscript_config\x18\x02 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"+\n\x16\x42TCSignMessageResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xb9\x05\n\nBTCRequest\x12_\n\x1bis_script_config_registered\x18\x01 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredRequestH\x00\x12V\n\x16register_script_config\x18\x02 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequestH\x00\x12\x41\n\x0bprevtx_init\x18\x03 \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCPrevTxInitRequestH\x00\x12\x43\n\x0cprevtx_input\x18\x04 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCPrevTxInputRequestH\x00\x12\x45\n\rprevtx_output\x18\x05 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCPrevTxOutputRequestH\x00\x12\x43\n\x0csign_message\x18\x06 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x07 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12I\n\x0fpayment_request\x18\x08 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCPaymentRequestRequestH\x00\x12\x36\n\x05xpubs\x18\t \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCXpubsRequestH\x00\x42\t\n\x07request\"\xc4\x03\n\x0b\x42TCResponse\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCSuccessH\x00\x12`\n\x1bis_script_config_registered\x18\x02 \x01(\x0b\x32\x39.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredResponseH\x00\x12>\n\tsign_next\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x44\n\x0csign_message\x18\x04 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCSignMessageResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12\x32\n\x04pubs\x18\x06 \x01(\x0b\x32\".shiftcrypto.bitbox02.PubsResponseH\x00\x42\n\n\x08response*9\n\x07\x42TCCoin\x12\x07\n\x03\x42TC\x10\x00\x12\x08\n\x04TBTC\x10\x01\x12\x07\n\x03LTC\x10\x02\x12\x08\n\x04TLTC\x10\x03\x12\x08\n\x04RBTC\x10\x04*a\n\rBTCOutputType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05P2PKH\x10\x01\x12\x08\n\x04P2SH\x10\x02\x12\n\n\x06P2WPKH\x10\x03\x12\t\n\x05P2WSH\x10\x04\x12\x08\n\x04P2TR\x10\x05\x12\r\n\tOP_RETURN\x10\x06\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
- _BTCCOIN._serialized_start=5551
- _BTCCOIN._serialized_end=5608
- _BTCOUTPUTTYPE._serialized_start=5610
- _BTCOUTPUTTYPE._serialized_end=5707
+ _BTCCOIN._serialized_start=5894
+ _BTCCOIN._serialized_end=5951
+ _BTCOUTPUTTYPE._serialized_start=5953
+ _BTCOUTPUTTYPE._serialized_end=6050
_BTCSCRIPTCONFIG._serialized_start=68
_BTCSCRIPTCONFIG._serialized_end=650
_BTCSCRIPTCONFIG_MULTISIG._serialized_start=293
@@ -79,17 +79,21 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_BTCPREVTXOUTPUTREQUEST._serialized_start=3744
_BTCPREVTXOUTPUTREQUEST._serialized_end=3806
_BTCPAYMENTREQUESTREQUEST._serialized_start=3809
- _BTCPAYMENTREQUESTREQUEST._serialized_end=4108
- _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_start=3985
- _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=4108
- _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_start=4076
- _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_end=4100
- _BTCSIGNMESSAGEREQUEST._serialized_start=4111
- _BTCSIGNMESSAGEREQUEST._serialized_end=4349
- _BTCSIGNMESSAGERESPONSE._serialized_start=4351
- _BTCSIGNMESSAGERESPONSE._serialized_end=4394
- _BTCREQUEST._serialized_start=4397
- _BTCREQUEST._serialized_end=5094
- _BTCRESPONSE._serialized_start=5097
- _BTCRESPONSE._serialized_end=5549
+ _BTCPAYMENTREQUESTREQUEST._serialized_end=4451
+ _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_start=3986
+ _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=4451
+ _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_start=4177
+ _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_end=4201
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_start=4204
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_end=4443
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_start=4382
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_end=4421
+ _BTCSIGNMESSAGEREQUEST._serialized_start=4454
+ _BTCSIGNMESSAGEREQUEST._serialized_end=4692
+ _BTCSIGNMESSAGERESPONSE._serialized_start=4694
+ _BTCSIGNMESSAGERESPONSE._serialized_end=4737
+ _BTCREQUEST._serialized_start=4740
+ _BTCREQUEST._serialized_end=5437
+ _BTCRESPONSE._serialized_start=5440
+ _BTCRESPONSE._serialized_end=5892
# @@protoc_insertion_point(module_scope)
diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
index 954d5e7..611723b 100644
--- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
@@ -760,17 +760,69 @@ class BTCPaymentRequestRequest(google.protobuf.message.Message):
) -> None: ...
def ClearField(self, field_name: typing.Literal["note", b"note"]) -> None: ...
+ @typing.final
+ class CoinPurchaseMemo(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ @typing.final
+ class EthAddressDerivation(google.protobuf.message.Message):
+ """Derivation info for verifying address ownership.
+ NOT part of the SLIP-24 sighash.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEYPATH_FIELD_NUMBER: builtins.int
+ @property
+ def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
+ """Keypath to the address"""
+
+ def __init__(
+ self,
+ *,
+ keypath: collections.abc.Iterable[builtins.int] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["keypath", b"keypath"]) -> None: ...
+
+ COIN_TYPE_FIELD_NUMBER: builtins.int
+ AMOUNT_FIELD_NUMBER: builtins.int
+ ADDRESS_FIELD_NUMBER: builtins.int
+ ETH_FIELD_NUMBER: builtins.int
+ coin_type: builtins.int
+ """SLIP-44 coin type"""
+ amount: builtins.str
+ """Human-readable amount (e.g. "0.25 ETH")"""
+ address: builtins.str
+ """Address to send the purchased coins to"""
+ @property
+ def eth(self) -> global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.EthAddressDerivation: ...
+ def __init__(
+ self,
+ *,
+ coin_type: builtins.int = ...,
+ amount: builtins.str = ...,
+ address: builtins.str = ...,
+ eth: global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.EthAddressDerivation | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["address_derivation", b"address_derivation", "eth", b"eth"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["address", b"address", "address_derivation", b"address_derivation", "amount", b"amount", "coin_type", b"coin_type", "eth", b"eth"]) -> None: ...
+ def WhichOneof(self, oneof_group: typing.Literal["address_derivation", b"address_derivation"]) -> typing.Literal["eth"] | None: ...
+
TEXT_MEMO_FIELD_NUMBER: builtins.int
+ COIN_PURCHASE_MEMO_FIELD_NUMBER: builtins.int
@property
def text_memo(self) -> global___BTCPaymentRequestRequest.Memo.TextMemo: ...
+ @property
+ def coin_purchase_memo(self) -> global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo: ...
def __init__(
self,
*,
text_memo: global___BTCPaymentRequestRequest.Memo.TextMemo | None = ...,
+ coin_purchase_memo: global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo | None = ...,
) -> None: ...
- def HasField(self, field_name: typing.Literal["memo", b"memo", "text_memo", b"text_memo"]) -> builtins.bool: ...
- def ClearField(self, field_name: typing.Literal["memo", b"memo", "text_memo", b"text_memo"]) -> None: ...
- def WhichOneof(self, oneof_group: typing.Literal["memo", b"memo"]) -> typing.Literal["text_memo"] | None: ...
+ def HasField(self, field_name: typing.Literal["coin_purchase_memo", b"coin_purchase_memo", "memo", b"memo", "text_memo", b"text_memo"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["coin_purchase_memo", b"coin_purchase_memo", "memo", b"memo", "text_memo", b"text_memo"]) -> None: ...
+ def WhichOneof(self, oneof_group: typing.Literal["memo", b"memo"]) -> typing.Literal["text_memo", "coin_purchase_memo"] | None: ...
RECIPIENT_NAME_FIELD_NUMBER: builtins.int
MEMOS_FIELD_NUMBER: builtins.int
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
index 2fa1037..aad3ab9 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
@@ -83,6 +83,7 @@ pub async fn user_verify(
verify_message::verify(hal, "Memo", "Memo", text_memo.note.as_bytes(), false)
.await?;
}
+ // TODO: add CoinPurchaseMemo arm when SwapKit UI is finalized
_ => return Err(Error::InvalidInput),
}
}
@@ -93,6 +94,10 @@ pub async fn user_verify(
pub enum ValidationError {
UnknownRecipient,
InvalidSignature,
+ #[cfg(feature = "app-ethereum")]
+ AddressMismatch,
+ #[cfg(not(feature = "app-ethereum"))]
+ Disabled,
Other,
}
@@ -128,6 +133,17 @@ fn compute_sighash(
sighash.update(1u32.to_le_bytes());
hash_data_lenprefixed(&mut sighash, text_memo.note.as_bytes());
}
+ #[cfg(feature = "app-ethereum")]
+ Memo {
+ memo: Some(memo::Memo::CoinPurchaseMemo(coin_purchase_memo)),
+ } => {
+ // Only hash SLIP-24 fields. address_derivation is BitBox-specific and not
+ // part of the signed payload.
+ sighash.update(3u32.to_le_bytes()); // CoinPurchaseMemo type, not Protobuf field.
+ sighash.update(coin_purchase_memo.coin_type.to_le_bytes());
+ hash_data_lenprefixed(&mut sighash, coin_purchase_memo.amount.as_bytes());
+ hash_data_lenprefixed(&mut sighash, coin_purchase_memo.address.as_bytes());
+ }
_ => return Err(ValidationError::Other),
}
}
@@ -173,6 +189,7 @@ fn ecdsa_verify(sig64: &[u8], msg32: &[u8], pubkey33: &[u8]) -> Result<(), Valid
/// Validate the payment request: amount, signature, etc.
pub fn validate(
+ #[cfg_attr(not(feature = "app-ethereum"), allow(unused_variables))] hal: &mut impl crate::hal::Hal,
coin_params: ¶ms::Params,
payment_request: &pb::BtcPaymentRequestRequest,
output_value: u64,
@@ -190,6 +207,29 @@ pub fn validate(
if payment_request.memos.len() > MAX_MEMOS_NUM {
return Err(ValidationError::Other);
}
+ for memo in payment_request.memos.iter() {
+ if let Memo {
+ memo: Some(memo::Memo::CoinPurchaseMemo(coin_purchase_memo)),
+ } = memo
+ {
+ match &coin_purchase_memo.address_derivation {
+ Some(memo::coin_purchase_memo::AddressDerivation::Eth(_eth)) => {
+ #[cfg(feature = "app-ethereum")]
+ {
+ let derived_address =
+ super::super::ethereum::derive_address(hal, &_eth.keypath)
+ .map_err(|_| ValidationError::Other)?;
+ if derived_address != coin_purchase_memo.address {
+ return Err(ValidationError::AddressMismatch);
+ }
+ }
+ #[cfg(not(feature = "app-ethereum"))]
+ return Err(ValidationError::Disabled);
+ }
+ None => return Err(ValidationError::Other),
+ }
+ }
+ }
let sighash = compute_sighash(coin_params, payment_request, output_value, output_address)?;
ecdsa_verify(&payment_request.signature, &sighash, identity.public_key)
}
@@ -197,6 +237,7 @@ pub fn validate(
#[cfg(test)]
mod tests {
use super::*;
+ use crate::hal::testing::TestingHal;
fn make_text_memo(note: &str) -> Memo {
Memo {
@@ -204,6 +245,39 @@ mod tests {
}
}
+ #[cfg(feature = "app-ethereum")]
+ fn make_coin_purchase_memo(
+ coin_type: u32,
+ amount: &str,
+ address: &str,
+ address_derivation: Option<memo::coin_purchase_memo::AddressDerivation>,
+ ) -> Memo {
+ Memo {
+ memo: Some(memo::Memo::CoinPurchaseMemo(memo::CoinPurchaseMemo {
+ coin_type,
+ amount: amount.into(),
+ address: address.into(),
+ address_derivation,
+ })),
+ }
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ fn dummy_eth_address_derivation(valid: bool) -> memo::coin_purchase_memo::AddressDerivation {
+ let coin_type = if valid { 60 } else { 0 };
+ memo::coin_purchase_memo::AddressDerivation::Eth(
+ memo::coin_purchase_memo::EthAddressDerivation {
+ keypath: vec![
+ 44 + util::bip32::HARDENED,
+ coin_type + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ],
+ },
+ )
+ }
+
#[test]
fn test_sighash() {
let coin_params = params::get(pb::BtcCoin::Tbtc);
@@ -243,11 +317,63 @@ mod tests {
hex::encode(sighash),
"9303ef0189ab78e92b7518ebf9851bf567ca06ddce242fb33220c3b31a489251"
);
+
+ #[cfg(feature = "app-ethereum")]
+ {
+ // Verify that keypath does not influence the sighash.
+ let payment_request_without = pb::BtcPaymentRequestRequest {
+ recipient_name: "Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0xabc1234567890",
+ None,
+ )],
+ nonce: vec![],
+ total_amount: 123456,
+ signature: vec![],
+ };
+ let sighash_without = compute_sighash(
+ coin_params,
+ &payment_request_without,
+ 123456,
+ "tb1q2q0j6gmfxynj40p0kxsr9jkagcvgpuqvqynnup",
+ )
+ .unwrap();
+
+ let payment_request_with = pb::BtcPaymentRequestRequest {
+ recipient_name: "Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0xabc1234567890",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: 123456,
+ signature: vec![],
+ };
+ let sighash_with = compute_sighash(
+ coin_params,
+ &payment_request_with,
+ 123456,
+ "tb1q2q0j6gmfxynj40p0kxsr9jkagcvgpuqvqynnup",
+ )
+ .unwrap();
+
+ assert_eq!(sighash_without, sighash_with);
+
+ assert_eq!(
+ hex::encode(sighash_without),
+ "1806caf7c518aad69eb38f25fd418d507c6a3e01719a7d77be94cd50a2790872"
+ );
+ }
}
#[test]
fn test_validate() {
let coin_params = params::get(pb::BtcCoin::Tbtc);
+ let mut mock_hal = TestingHal::new();
let value = 123456u64;
let address = "tb1q2q0j6gmfxynj40p0kxsr9jkagcvgpuqvqynnup";
@@ -261,11 +387,107 @@ mod tests {
};
tst_sign_payment_request(coin_params, &mut payment_request, value, address);
- assert!(validate(coin_params, &payment_request, value, address).is_ok());
+ assert!(validate(&mut mock_hal, coin_params, &payment_request, value, address).is_ok());
+
+ #[cfg(feature = "app-ethereum")]
+ {
+ // CoinPurchase memo with matching keypath and address. The
+ // address results from the keypath which is used in dummy_eth_address_derivation().
+ // See src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
+ use crate::keystore::testing::mock_unlocked;
+ mock_unlocked();
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0x773A77b9D32589be03f9132AF759e294f7851be9",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(validate(&mut mock_hal, coin_params, &payment_request, value, address).is_ok());
+ }
// Unhappy cases:
- // Unnown recipient
+ #[cfg(feature = "app-ethereum")]
+ {
+ // Invalid ETH keypath in CoinPurchaseMemo
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0xabc1234567890",
+ Some(dummy_eth_address_derivation(/*valid=*/ false)),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ // Sign it so the only failure reason is the keypath validation.
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::Other)
+ ));
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ {
+ // Valid keypath but address mismatch
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0xWRONG_ADDRESS_THAT_DOESNT_MATCH",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(
+ &mut TestingHal::new(),
+ coin_params,
+ &payment_request,
+ value,
+ address
+ ),
+ Err(ValidationError::AddressMismatch)
+ ));
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ {
+ // Missing address_derivation in CoinPurchaseMemo
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0xabc1234567890",
+ None,
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::Other)
+ ));
+ }
+
+ // Unknown recipient
let payment_request = pb::BtcPaymentRequestRequest {
recipient_name: "Unknown Merchant".into(),
memos: vec![make_text_memo("TextMemo")],
@@ -274,7 +496,7 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(coin_params, &payment_request, value, address),
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
Err(ValidationError::UnknownRecipient)
));
@@ -287,7 +509,13 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(coin_params, &payment_request, value + 1, address),
+ validate(
+ &mut mock_hal,
+ coin_params,
+ &payment_request,
+ value + 1,
+ address
+ ),
Err(ValidationError::Other)
));
@@ -300,7 +528,7 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(coin_params, &payment_request, value, address),
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
Err(ValidationError::Other)
));
@@ -313,7 +541,7 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(coin_params, &payment_request, value, address),
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
Err(ValidationError::InvalidSignature)
));
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
index f3cad9d..e4db136 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
@@ -970,16 +970,22 @@ async fn _process(
get_payment_request(output_payment_request_index, &mut next_response).await?;
payment_request::user_verify(hal, coin_params, &payment_request, format_unit)
.await?;
- if payment_request::validate(
+ match payment_request::validate(
+ hal,
coin_params,
&payment_request,
tx_output.value,
&address()?,
- )
- .is_err()
- {
- hal.ui().status("Invalid\npayment request", true).await;
- return Err(Error::InvalidInput);
+ ) {
+ Ok(()) => {}
+ #[cfg(not(feature = "app-ethereum"))]
+ Err(payment_request::ValidationError::Disabled) => {
+ return Err(Error::Disabled);
+ }
+ Err(_) => {
+ hal.ui().status("Invalid\npayment request", true).await;
+ return Err(Error::InvalidInput);
+ }
}
payment_request_seen = true;
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
index 4a78534..ad001a7 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
@@ -23,6 +23,19 @@ use pb::eth_response::Response;
use core::convert::TryInto;
+pub(crate) fn derive_address(
+ hal: &mut impl crate::hal::Hal,
+ keypath: &[u32],
+) -> Result<alloc::string::String, Error> {
+ if !keypath::is_valid_keypath_address(keypath) {
+ return Err(Error::InvalidInput);
+ }
+ let pubkey = crate::keystore::get_xpub_twice(hal, keypath)
+ .or(Err(Error::InvalidInput))?
+ .pubkey_uncompressed()?;
+ Ok(address::from_pubkey(&pubkey))
+}
+
/// Like `hww::next_request`, but for Ethereum requests/responses.
pub async fn next_request(response: Response) -> Result<Request, Error> {
let request = crate::hww::next_request(pb::response::Response::Eth(pb::EthResponse {
@@ -79,3 +92,33 @@ pub async fn process_api(
Request::TypedMsgValue(_) => Err(Error::InvalidInput),
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::hal::testing::TestingHal;
+ use util::bip32::HARDENED;
+
+ #[test]
+ fn test_derive_address() {
+ let mut hal = TestingHal::new();
+
+ // Standard Ethereum keypath
+ let keypath = vec![44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
+ let address = derive_address(&mut hal, &keypath).unwrap();
+
+ // This is the expected address for the mock keystore seed with this keypath
+ assert_eq!(address, "0x773A77b9D32589be03f9132AF759e294f7851be9");
+ }
+
+ #[test]
+ fn test_derive_address_invalid_keypath() {
+ let mut hal = TestingHal::new();
+
+ // Invalid keypath (too short)
+ let keypath = vec![44 + HARDENED, 60 + HARDENED];
+ let result = derive_address(&mut hal, &keypath);
+
+ assert!(matches!(result, Err(Error::InvalidInput)));
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
index d3323e2..96a8f65 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/pubrequest.rs
@@ -30,13 +30,7 @@ async fn process_address(
Some(erc20_params::get(params.chain_id, address).ok_or(Error::InvalidInput)?)
};
- if !super::keypath::is_valid_keypath_address(&request.keypath) {
- return Err(Error::InvalidInput);
- }
- let pubkey = crate::keystore::get_xpub_twice(hal, &request.keypath)
- .or(Err(Error::InvalidInput))?
- .pubkey_uncompressed()?;
- let address = super::address::from_pubkey(&pubkey);
+ let address = super::derive_address(hal, &request.keypath)?;
if request.display {
let title = match erc20_params {
diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
index 28612df..15dd9ee 100644
--- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
+++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
@@ -928,7 +928,7 @@ pub mod btc_payment_request_request {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Memo {
- #[prost(oneof = "memo::Memo", tags = "1")]
+ #[prost(oneof = "memo::Memo", tags = "1, 2")]
pub memo: ::core::option::Option<memo::Memo>,
}
/// Nested message and enum types in `Memo`.
@@ -940,10 +940,47 @@ pub mod btc_payment_request_request {
pub note: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
+ #[derive(Clone, PartialEq, ::prost::Message)]
+ pub struct CoinPurchaseMemo {
+ /// SLIP-44 coin type
+ #[prost(uint32, tag = "1")]
+ pub coin_type: u32,
+ /// Human-readable amount (e.g. "0.25 ETH")
+ #[prost(string, tag = "2")]
+ pub amount: ::prost::alloc::string::String,
+ /// Address to send the purchased coins to
+ #[prost(string, tag = "3")]
+ pub address: ::prost::alloc::string::String,
+ #[prost(oneof = "coin_purchase_memo::AddressDerivation", tags = "4")]
+ pub address_derivation: ::core::option::Option<
+ coin_purchase_memo::AddressDerivation,
+ >,
+ }
+ /// Nested message and enum types in `CoinPurchaseMemo`.
+ pub mod coin_purchase_memo {
+ /// Derivation info for verifying address ownership.
+ /// NOT part of the SLIP-24 sighash.
+ #[allow(clippy::derive_partial_eq_without_eq)]
+ #[derive(Clone, PartialEq, ::prost::Message)]
+ pub struct EthAddressDerivation {
+ /// Keypath to the address
+ #[prost(uint32, repeated, tag = "1")]
+ pub keypath: ::prost::alloc::vec::Vec<u32>,
+ }
+ #[allow(clippy::derive_partial_eq_without_eq)]
+ #[derive(Clone, PartialEq, ::prost::Oneof)]
+ pub enum AddressDerivation {
+ #[prost(message, tag = "4")]
+ Eth(EthAddressDerivation),
+ }
+ }
+ #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Memo {
#[prost(message, tag = "1")]
TextMemo(TextMemo),
+ #[prost(message, tag = "2")]
+ CoinPurchaseMemo(CoinPurchaseMemo),
}
}
}
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.