diff --git a/messages/btc.proto b/messages/btc.proto index 6aa0727259..524ebc2f9e 100644 --- a/messages/btc.proto +++ b/messages/btc.proto @@ -116,6 +116,11 @@ message BTCSignInitRequest { // used script configs for outputs that send to an address of the same keystore, but not // necessarily the same account (as defined by `script_configs` above). repeated BTCScriptConfigWithKeypath output_script_configs = 10; + // BIP-322: If set, this is a BIP-322 message signing request. The device will + // verify the virtual transaction structure and show message-signing UI. + // Carries the message from the PSBT global field + // PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (0x09) defined in BIP-322 v1.0.0. + optional bytes bip322_message = 11; } message BTCSignNextResponse { diff --git a/py/bitbox02/bitbox02/bitbox02/bitbox02.py b/py/bitbox02/bitbox02/bitbox02/bitbox02.py index 3122865e5c..8f0f340337 100644 --- a/py/bitbox02/bitbox02/bitbox02/bitbox02.py +++ b/py/bitbox02/bitbox02/bitbox02/bitbox02.py @@ -436,6 +436,7 @@ def btc_sign( locktime: int = 0, format_unit: "btc.BTCSignInitRequest.FormatUnit.V" = btc.BTCSignInitRequest.FormatUnit.DEFAULT, output_script_configs: Optional[Sequence[btc.BTCScriptConfigWithKeypath]] = None, + bip322_message: Optional[bytes] = None, ) -> Sequence[Tuple[int, bytes]]: """ coin: the first element of all provided keypaths must match the coin: @@ -452,16 +453,20 @@ def btc_sign( if `btc_sign_needs_prevtxs()` returns True. outputs: transaction outputs. Can be an external output (BTCOutputExternal) or an internal output for change (BTCOutputInternal). - version, locktime: reserved for future use. + version, locktime: reserved for future use. For BIP-322 message signing, must be 0 or 2. format_unit: defines in which unit amounts will be displayed output_script_configs: script types for outputs belonging to the same keystore + bip322_message: if set, treat this as a BIP-322 generic message signing request. The + inputs/outputs must match the BIP-322 to_sign virtual transaction (first input spends + to_spend, single OP_RETURN output). Carries the message from PSBT global field + PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (0x09). Returns: list of (input index, signature) tuples. Raises Bitbox02Exception with ERR_USER_ABORT on user abort. """ # pylint: disable=no-member,too-many-branches,too-many-statements - # Reserved for future use. - assert version in (1, 2) + # Reserved for future use. BIP-322 also allows version 0. + assert version in (0, 1, 2) if any(map(is_taproot, script_configs)): self._require_atleast(semver.VersionInfo(9, 10, 0)) @@ -496,6 +501,7 @@ def btc_sign( locktime=locktime, format_unit=format_unit, output_script_configs=output_script_configs, + bip322_message=bip322_message, ) ) next_response = self._msg_query(request, expected_response="btc_sign_next").btc_sign_next @@ -650,7 +656,8 @@ def btc_sign_msg( ) -> Tuple[bytes, int, bytes]: """ Returns a 64 byte sig, the recoverable id, and a 65 byte signature containing - the recid, compatible with Electrum. + the recid, compatible with Electrum, for legacy types. + For P2TR script configs, it returns the BIP-322 simple-encoded witness. """ # pylint: disable=no-member @@ -663,6 +670,13 @@ def btc_sign_msg( btc.BTCSignMessageRequest(coin=coin, script_config=script_config, msg=msg) ) + if is_taproot(script_config=script_config): + signature = self._btc_msg_query( + request, expected_response="sign_message" + ).sign_message.signature + return (signature, 0, signature) + + # Legacy message signing for non-taproot types. supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0) if supports_antiklepto: host_nonce = os.urandom(32) diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py index bbaebcb78f..a7ee000a24 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 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\"\xcd\x06\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\x9c\x05\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\xba\x03\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\x12h\n\x03\x62tc\x18\x05 \x01(\x0b\x32Y.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.BtcAddressDerivationH\x00\x1a\'\n\x14\x45thAddressDerivation\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x1a_\n\x14\x42tcAddressDerivation\x12G\n\rscript_config\x18\x01 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypathB\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') +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\"\xef\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\x12\x1b\n\x0e\x62ip322_message\x18\x0b \x01(\x0cH\x00\x88\x01\x01\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\x42\x11\n\x0f_bip322_message\"\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\"\xcd\x06\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\x9c\x05\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\xba\x03\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\x12h\n\x03\x62tc\x18\x05 \x01(\x0b\x32Y.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.BtcAddressDerivationH\x00\x1a\'\n\x14\x45thAddressDerivation\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x1a_\n\x14\x42tcAddressDerivation\x12G\n\rscript_config\x18\x01 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypathB\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=6097 - _BTCCOIN._serialized_end=6154 - _BTCOUTPUTTYPE._serialized_start=6156 - _BTCOUTPUTTYPE._serialized_end=6253 + _BTCCOIN._serialized_start=6145 + _BTCCOIN._serialized_end=6202 + _BTCOUTPUTTYPE._serialized_start=6204 + _BTCOUTPUTTYPE._serialized_end=6301 _BTCSCRIPTCONFIG._serialized_start=68 _BTCSCRIPTCONFIG._serialized_end=650 _BTCSCRIPTCONFIG_MULTISIG._serialized_start=293 @@ -47,55 +47,55 @@ _BTCSCRIPTCONFIGWITHKEYPATH._serialized_start=1261 _BTCSCRIPTCONFIGWITHKEYPATH._serialized_end=1368 _BTCSIGNINITREQUEST._serialized_start=1371 - _BTCSIGNINITREQUEST._serialized_end=1818 - _BTCSIGNINITREQUEST_FORMATUNIT._serialized_start=1784 - _BTCSIGNINITREQUEST_FORMATUNIT._serialized_end=1818 - _BTCSIGNNEXTRESPONSE._serialized_start=1821 - _BTCSIGNNEXTRESPONSE._serialized_end=2273 - _BTCSIGNNEXTRESPONSE_TYPE._serialized_start=2143 - _BTCSIGNNEXTRESPONSE_TYPE._serialized_end=2273 - _BTCSIGNINPUTREQUEST._serialized_start=2276 - _BTCSIGNINPUTREQUEST._serialized_end=2510 - _BTCSIGNOUTPUTREQUEST._serialized_start=2513 - _BTCSIGNOUTPUTREQUEST._serialized_end=2928 - _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_start=2839 - _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_end=2871 - _BTCSCRIPTCONFIGREGISTRATION._serialized_start=2931 - _BTCSCRIPTCONFIGREGISTRATION._serialized_end=3084 - _BTCSUCCESS._serialized_start=3086 - _BTCSUCCESS._serialized_end=3098 - _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_start=3100 - _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_end=3209 - _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_start=3211 - _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_end=3271 - _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_start=3274 - _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_end=3526 - _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_start=3477 - _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_end=3526 - _BTCPREVTXINITREQUEST._serialized_start=3528 - _BTCPREVTXINITREQUEST._serialized_end=3626 - _BTCPREVTXINPUTREQUEST._serialized_start=3628 - _BTCPREVTXINPUTREQUEST._serialized_end=3742 - _BTCPREVTXOUTPUTREQUEST._serialized_start=3744 - _BTCPREVTXOUTPUTREQUEST._serialized_end=3806 - _BTCPAYMENTREQUESTREQUEST._serialized_start=3809 - _BTCPAYMENTREQUESTREQUEST._serialized_end=4654 - _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_start=3986 - _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=4654 - _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_start=4177 - _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_end=4201 - _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_start=4204 - _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_end=4646 - _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_start=4488 - _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_end=4527 - _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_BTCADDRESSDERIVATION._serialized_start=4529 - _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_BTCADDRESSDERIVATION._serialized_end=4624 - _BTCSIGNMESSAGEREQUEST._serialized_start=4657 - _BTCSIGNMESSAGEREQUEST._serialized_end=4895 - _BTCSIGNMESSAGERESPONSE._serialized_start=4897 - _BTCSIGNMESSAGERESPONSE._serialized_end=4940 - _BTCREQUEST._serialized_start=4943 - _BTCREQUEST._serialized_end=5640 - _BTCRESPONSE._serialized_start=5643 - _BTCRESPONSE._serialized_end=6095 + _BTCSIGNINITREQUEST._serialized_end=1866 + _BTCSIGNINITREQUEST_FORMATUNIT._serialized_start=1813 + _BTCSIGNINITREQUEST_FORMATUNIT._serialized_end=1847 + _BTCSIGNNEXTRESPONSE._serialized_start=1869 + _BTCSIGNNEXTRESPONSE._serialized_end=2321 + _BTCSIGNNEXTRESPONSE_TYPE._serialized_start=2191 + _BTCSIGNNEXTRESPONSE_TYPE._serialized_end=2321 + _BTCSIGNINPUTREQUEST._serialized_start=2324 + _BTCSIGNINPUTREQUEST._serialized_end=2558 + _BTCSIGNOUTPUTREQUEST._serialized_start=2561 + _BTCSIGNOUTPUTREQUEST._serialized_end=2976 + _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_start=2887 + _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_end=2919 + _BTCSCRIPTCONFIGREGISTRATION._serialized_start=2979 + _BTCSCRIPTCONFIGREGISTRATION._serialized_end=3132 + _BTCSUCCESS._serialized_start=3134 + _BTCSUCCESS._serialized_end=3146 + _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_start=3148 + _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_end=3257 + _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_start=3259 + _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_end=3319 + _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_start=3322 + _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_end=3574 + _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_start=3525 + _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_end=3574 + _BTCPREVTXINITREQUEST._serialized_start=3576 + _BTCPREVTXINITREQUEST._serialized_end=3674 + _BTCPREVTXINPUTREQUEST._serialized_start=3676 + _BTCPREVTXINPUTREQUEST._serialized_end=3790 + _BTCPREVTXOUTPUTREQUEST._serialized_start=3792 + _BTCPREVTXOUTPUTREQUEST._serialized_end=3854 + _BTCPAYMENTREQUESTREQUEST._serialized_start=3857 + _BTCPAYMENTREQUESTREQUEST._serialized_end=4702 + _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_start=4034 + _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=4702 + _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_start=4225 + _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_end=4249 + _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_start=4252 + _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_end=4694 + _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_start=4536 + _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_end=4575 + _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_BTCADDRESSDERIVATION._serialized_start=4577 + _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_BTCADDRESSDERIVATION._serialized_end=4672 + _BTCSIGNMESSAGEREQUEST._serialized_start=4705 + _BTCSIGNMESSAGEREQUEST._serialized_end=4943 + _BTCSIGNMESSAGERESPONSE._serialized_start=4945 + _BTCSIGNMESSAGERESPONSE._serialized_end=4988 + _BTCREQUEST._serialized_start=4991 + _BTCREQUEST._serialized_end=5688 + _BTCRESPONSE._serialized_start=5691 + _BTCRESPONSE._serialized_end=6143 # @@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 a207ee6c85..8497067c8d 100644 --- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi +++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi @@ -339,6 +339,7 @@ class BTCSignInitRequest(google.protobuf.message.Message): FORMAT_UNIT_FIELD_NUMBER: builtins.int CONTAINS_SILENT_PAYMENT_OUTPUTS_FIELD_NUMBER: builtins.int OUTPUT_SCRIPT_CONFIGS_FIELD_NUMBER: builtins.int + BIP322_MESSAGE_FIELD_NUMBER: builtins.int coin: global___BTCCoin.ValueType version: builtins.int """must be 1 or 2""" @@ -348,6 +349,12 @@ class BTCSignInitRequest(google.protobuf.message.Message): """must be <500000000""" format_unit: global___BTCSignInitRequest.FormatUnit.ValueType contains_silent_payment_outputs: builtins.bool + bip322_message: builtins.bytes + """BIP-322: If set, this is a BIP-322 message signing request. The device will + verify the virtual transaction structure and show message-signing UI. + Carries the message from the PSBT global field + PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (0x09) defined in BIP-322 v1.0.0. + """ @property def script_configs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BTCScriptConfigWithKeypath]: """used script configs in inputs and changes""" @@ -370,8 +377,11 @@ class BTCSignInitRequest(google.protobuf.message.Message): format_unit: global___BTCSignInitRequest.FormatUnit.ValueType = ..., contains_silent_payment_outputs: builtins.bool = ..., output_script_configs: collections.abc.Iterable[global___BTCScriptConfigWithKeypath] | None = ..., + bip322_message: builtins.bytes | None = ..., ) -> None: ... - def ClearField(self, field_name: typing.Literal["coin", b"coin", "contains_silent_payment_outputs", b"contains_silent_payment_outputs", "format_unit", b"format_unit", "locktime", b"locktime", "num_inputs", b"num_inputs", "num_outputs", b"num_outputs", "output_script_configs", b"output_script_configs", "script_configs", b"script_configs", "version", b"version"]) -> None: ... + def HasField(self, field_name: typing.Literal["_bip322_message", b"_bip322_message", "bip322_message", b"bip322_message"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_bip322_message", b"_bip322_message", "bip322_message", b"bip322_message", "coin", b"coin", "contains_silent_payment_outputs", b"contains_silent_payment_outputs", "format_unit", b"format_unit", "locktime", b"locktime", "num_inputs", b"num_inputs", "num_outputs", b"num_outputs", "output_script_configs", b"output_script_configs", "script_configs", b"script_configs", "version", b"version"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_bip322_message", b"_bip322_message"]) -> typing.Literal["bip322_message"] | None: ... global___BTCSignInitRequest = BTCSignInitRequest diff --git a/py/send_message.py b/py/send_message.py index 642ac9db39..04cd9c55b6 100755 --- a/py/send_message.py +++ b/py/send_message.py @@ -6,10 +6,12 @@ # pylint: disable=too-many-lines import argparse +import hashlib import socket import pprint import sys from typing import List, Any, Optional, Callable, Union, Tuple, Sequence +import base58 import base64 import binascii import textwrap @@ -55,6 +57,37 @@ def eprint(*args: Any, **kwargs: Any) -> None: print(*args, **kwargs) +def _bip322_to_spend_txid(msg: bytes, script_pubkey: bytes) -> bytes: + """Compute the BIP-322 to_spend.txid for the given message and scriptPubKey. + + See BIP-322 v1.0.0 §"Full" for the to_spend transaction structure. Returns the raw + double-SHA256 (txid in internal byte order, matching the firmware's `prev_out_hash` field). + """ + # BIP-340 tagged hash with tag "BIP0322-signed-message". + tag = hashlib.sha256(b"BIP0322-signed-message").digest() + msg_hash = hashlib.sha256(tag + tag + msg).digest() + + # Serialize the to_spend transaction: + # nVersion=0, vin[0]={null prevout, scriptSig=OP_0 PUSH32 , nSequence=0}, + # vout[0]={value=0, scriptPubKey=script_pubkey}, nLockTime=0. + tx = bytearray() + tx += (0).to_bytes(4, "little") # nVersion + tx += b"\x01" # vin count + tx += b"\x00" * 32 # prevout.hash + tx += (0xFFFFFFFF).to_bytes(4, "little") # prevout.n + tx += b"\x22" # scriptSig length: 34 bytes + tx += b"\x00\x20" + msg_hash # OP_0 PUSH32 + tx += (0).to_bytes(4, "little") # nSequence + tx += b"\x01" # vout count + tx += (0).to_bytes(8, "little") # vout.value + assert len(script_pubkey) < 0xFD, "scriptPubKey too long for single-byte varint" + tx += bytes([len(script_pubkey)]) # scriptPubKey length + tx += script_pubkey + tx += (0).to_bytes(4, "little") # nLockTime + + return hashlib.sha256(hashlib.sha256(bytes(tx)).digest()).digest() + + def ask_user( choices: Sequence[Tuple[str, Callable[[], None]]], ) -> Union[Callable[[], None], bool, None]: @@ -947,6 +980,7 @@ def sign( coin: "bitbox02.btc.BTCCoin.V", keypath: Sequence[int], script_config: bitbox02.btc.BTCScriptConfig, + taproot: bool = False, ) -> None: address = self._device.btc_address( coin=coin, keypath=keypath, script_config=script_config, display=False @@ -968,10 +1002,88 @@ def sign( ), msg_bytes, ) + + # Taproot uses BIP-322 which already comes fully encoded. + if taproot: + print("Signature:", sig65.decode("ascii")) + return + print("Signature:", base64.b64encode(sig65).decode("ascii")) except UserAbortException: print("Aborted by user") + def sign_via_signtx_p2sh_p2wpkh( + coin: "bitbox02.btc.BTCCoin.V", keypath: Sequence[int] + ) -> None: + """Sign a BIP-322 message for a P2SH-P2WPKH address through the signtx + streaming flow (host sends `bip322_message` in the init request). + """ + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + ) + address = self._device.btc_address( + coin=coin, + keypath=keypath, + script_config=script_config, + display=False, + ) + print("Address:", address) + + msg = input(r"Message to sign (\n = newline): ") + if msg.startswith("0x"): + msg_bytes = binascii.unhexlify(msg[2:]) + else: + msg_bytes = msg.replace(r"\n", "\n").encode("utf-8") + + # Derive the scriptPubKey from the P2SH address: OP_HASH160 PUSH20 OP_EQUAL. + # base58check_decode gives [version_byte || 20-byte-hash]. + decoded = base58.b58decode_check(address) + assert len(decoded) == 21 + p2sh_hash = decoded[1:] + script_pubkey = b"\xa9\x14" + p2sh_hash + b"\x87" + + # Compute to_spend.txid (per BIP-322). + to_spend_txid = _bip322_to_spend_txid(msg_bytes, script_pubkey) + + # The signtx flow uses the BIP-44 account-level keypath in the script_configs and + # the full keypath in the input. + account_keypath = list(keypath[:3]) + tx_input: bitbox02.BTCInputType = { + "prev_out_hash": to_spend_txid, + "prev_out_index": 0, + "prev_out_value": 0, + "sequence": 0, + "keypath": list(keypath), + "script_config_index": 0, + "prev_tx": None, + } + op_return_output = bitbox02.BTCOutputExternal( + output_type=bitbox02.btc.BTCOutputType.OP_RETURN, + output_payload=b"", + value=0, + ) + try: + sigs = self._device.btc_sign( + coin, + [ + bitbox02.btc.BTCScriptConfigWithKeypath( + script_config=script_config, keypath=account_keypath + ) + ], + inputs=[tx_input], + outputs=[op_return_output], + version=0, + locktime=0, + bip322_message=msg_bytes, + ) + _, sig = sigs[0] + # The device returns the raw 64-byte ECDSA compact signature for the to_sign + # input. To produce a full BIP-322 "ful" signature, the host assembles the + # signed to_sign transaction and base64-encodes it with the `ful` prefix. + print("ECDSA signature (R||S):", sig.hex()) + except UserAbortException: + print("Aborted by user") + def sign_mainnet() -> None: keypath = [49 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0] script_config = bitbox02.btc.BTCScriptConfig( @@ -979,6 +1091,19 @@ def sign_mainnet() -> None: ) sign(bitbox02.btc.BTC, keypath, script_config) + def sign_mainnet_tr() -> None: + keypath = [86 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0] + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2TR + ) + sign(bitbox02.btc.BTC, keypath, script_config, True) + + def sign_mainnet_p2sh_p2wpkh_signtx() -> None: + sign_via_signtx_p2sh_p2wpkh( + bitbox02.btc.BTC, + [49 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0], + ) + def sign_testnet() -> None: keypath = [49 + HARDENED, 1 + HARDENED, 0 + HARDENED, 0, 0] script_config = bitbox02.btc.BTCScriptConfig( @@ -986,9 +1111,19 @@ def sign_testnet() -> None: ) sign(bitbox02.btc.TBTC, keypath, script_config) + def sign_testnet_tr() -> None: + keypath = [86 + HARDENED, 1 + HARDENED, 0 + HARDENED, 0, 0] + script_config = bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2TR + ) + sign(bitbox02.btc.TBTC, keypath, script_config, True) + choices = ( ("Mainnet", sign_mainnet), + ("Mainnet TR (BIP-322 via signmsg)", sign_mainnet_tr), + ("Mainnet P2SH-P2WPKH (BIP-322 via signtx)", sign_mainnet_p2sh_p2wpkh_signtx), ("Testnet", sign_testnet), + ("Testnet TR (BIP-322 via signmsg)", sign_testnet_tr), ) choice = ask_user(choices) if callable(choice): diff --git a/src/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rs b/src/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rs index 4cddb3a85e..e7d5bd714c 100644 --- a/src/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rs +++ b/src/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rs @@ -558,6 +558,12 @@ pub struct BtcSignInitRequest { /// necessarily the same account (as defined by `script_configs` above). #[prost(message, repeated, tag = "10")] pub output_script_configs: ::prost::alloc::vec::Vec, + /// BIP-322: If set, this is a BIP-322 message signing request. The device will + /// verify the virtual transaction structure and show message-signing UI. + /// Carries the message from the PSBT global field + /// PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE (0x09) defined in BIP-322 v1.0.0. + #[prost(bytes = "vec", optional, tag = "11")] + pub bip322_message: ::core::option::Option<::prost::alloc::vec::Vec>, } /// Nested message and enum types in `BTCSignInitRequest`. pub mod btc_sign_init_request { diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs index 2d2aaa3124..981d188d03 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs @@ -5,6 +5,7 @@ use crate::hal::ui::ConfirmParams; compile_error!("Bitcoin code is being compiled even though the app-bitcoin feature is not enabled"); mod bip143; +mod bip322; mod bip341; pub mod common; pub mod keypath; diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/bip322.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/bip322.rs new file mode 100644 index 0000000000..8065868612 --- /dev/null +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/bip322.rs @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! BIP-322 generic message signing. +//! +//! Uses the `bitcoin` crate for transaction construction and the shared tagged hash engine from +//! `bip341` for the BIP-322 message hash. Can be replaced by the public `bip322` crate +//! () if it gains no_std support in the future. + +use alloc::vec; +use alloc::vec::Vec; +use sha2::Digest; + +use bitcoin::hashes::Hash; +use bitcoin::sighash::{EcdsaSighashType, SighashCache}; +use bitcoin::{ + Amount, OutPoint, Script, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, Txid, + Witness, absolute::LockTime, transaction::Version, +}; + +use super::bip341; +use super::pb; +use super::Error; + +/// The BIP-322 tag used for the tagged message hash. +const BIP322_TAG: &[u8] = b"BIP0322-signed-message"; + +/// Compute the BIP-322 tagged message hash. +/// +/// `SHA256(SHA256(tag) || SHA256(tag) || msg)` where tag = "BIP0322-signed-message". +pub fn tagged_hash(msg: &[u8]) -> [u8; 32] { + let mut ctx = bip341::tagged_hash_engine(BIP322_TAG); + ctx.update(msg); + ctx.finalize().into() +} + +/// Build the BIP-322 `to_spend` virtual transaction and return its txid. +/// +/// The `to_spend` transaction commits to the message and the signer's scriptPubKey: +/// - nVersion=0, nLockTime=0 +/// - vin[0]: prevout=(0x00..00, 0xFFFFFFFF), scriptSig=`OP_0 PUSH32 `, +/// nSequence=0 +/// - vout[0]: nValue=0, scriptPubKey=`script_pubkey` +pub fn create_to_spend_txid(msg: &[u8], script_pubkey: &[u8]) -> [u8; 32] { + let msg_hash = tagged_hash(msg); + + let script_sig = bitcoin::script::Builder::new() + .push_int(0) + .push_slice(msg_hash) + .into_script(); + + let to_spend = Transaction { + version: Version(0), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(Txid::all_zeros(), 0xFFFFFFFF), + script_sig, + sequence: Sequence::ZERO, + witness: Witness::default(), + }], + output: vec![TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::from_bytes(script_pubkey.to_vec()), + }], + }; + + to_spend.compute_txid().to_byte_array() +} + +/// Which sighash algorithm to use for the BIP-322 `to_sign` virtual transaction. +/// +/// The choice has to match what a BIP-322 verifier runs through the standard script +/// interpreter: BIP-341 for taproot inputs and BIP-143 for v0 segwit (P2WPKH, P2WPKH-P2SH, +/// P2WSH, P2WSH-P2SH). +pub enum SighashMode<'a> { + /// BIP-341 taproot key-path spend (SIGHASH_DEFAULT, no annex, no tapleaf). + Taproot, + /// BIP-143 v0 segwit (P2WPKH/P2WSH and their P2SH-wrapped forms). `script_code` is the + /// script that goes into BIP-143 step 5 (the P2PKH-form `0x76a91488ac` for P2WPKH, + /// or the witness script for P2WSH). + SegwitV0 { script_code: &'a [u8] }, +} + +/// Compute the sighash for the BIP-322 `to_sign` virtual transaction. +/// +/// The `to_sign` transaction spends the `to_spend` output: +/// - vin[0]: prevout=(to_spend.txid(), 0), scriptSig=empty +/// - vout[0]: nValue=0, scriptPubKey=OP_RETURN +/// +/// `version`, `locktime` and `sequence` come from the host request: they are 0 for the simple +/// format, but full-format signers may set them (e.g. version=2, non-zero locktime/sequence for +/// timelocks). +pub fn sighash( + msg: &[u8], + script_pubkey: &[u8], + version: u32, + locktime: u32, + sequence: u32, + mode: SighashMode<'_>, +) -> [u8; 32] { + let txid = create_to_spend_txid(msg, script_pubkey); + + let to_sign = Transaction { + version: Version(version as i32), + lock_time: LockTime::from_consensus(locktime), + input: vec![TxIn { + previous_output: OutPoint::new(Txid::from_byte_array(txid), 0), + script_sig: ScriptBuf::new(), + sequence: Sequence::from_consensus(sequence), + witness: Witness::default(), + }], + output: vec![TxOut { + value: Amount::ZERO, + script_pubkey: bitcoin::script::Builder::new() + .push_opcode(bitcoin::opcodes::all::OP_RETURN) + .into_script(), + }], + }; + + let mut cache = SighashCache::new(&to_sign); + match mode { + SighashMode::Taproot => { + let prevout = TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::from_bytes(script_pubkey.to_vec()), + }; + cache + .taproot_key_spend_signature_hash( + 0, + &bitcoin::sighash::Prevouts::All(&[prevout]), + TapSighashType::Default, + ) + .expect("sighash computation failed") + .to_byte_array() + } + SighashMode::SegwitV0 { script_code } => { + // `p2wsh_signature_hash` is a thin wrapper over `segwit_v0_encode_signing_data_to` + // that uses its `witness_script` argument as the BIP-143 script_code unchanged, so + // it works as a generic v0 segwit sighash for both P2WPKH (with P2PKH-form script + // code) and P2WSH (with the witness script). + cache + .p2wsh_signature_hash( + 0, + Script::from_bytes(script_code), + Amount::ZERO, + EcdsaSighashType::All, + ) + .expect("sighash computation failed") + .to_byte_array() + } + } +} + +/// Validate the BIP-322 init request against the spec-level rules for `to_sign`. +/// +/// Per BIP-322 v1.0.0: +/// - version must be 0 or 2 (upgradeable rule §4) +/// - exactly one output (the OP_RETURN) +/// - locktime is unrestricted (full format may set it for timelocks) +/// +/// `num_inputs` is currently restricted to 1: the streaming flow only signs the first input, +/// so accepting more would silently drop signatures for the additional inputs. This restriction +/// can be lifted once Proof-of-Funds (multi-input) signing is implemented. +pub fn validate_init(request: &pb::BtcSignInitRequest) -> Result<(), Error> { + if request.version != 0 && request.version != 2 { + return Err(Error::InvalidInput); + } + if request.num_outputs != 1 { + return Err(Error::InvalidInput); + } + // TODO: relax to `>= 1` when multi-input (Proof-of-Funds) signing is implemented. + if request.num_inputs != 1 { + return Err(Error::InvalidInput); + } + Ok(()) +} + +/// Validate the BIP-322 first input. +/// +/// Checks: prevOutIndex=0, prevOutValue=0, and prevOutHash matches the computed +/// to_spend txid from the message and scriptPubKey. `sequence` is unrestricted (full format +/// may set it for timelocks). +pub fn validate_input( + input: &pb::BtcSignInputRequest, + message: &[u8], + script_pubkey: &[u8], +) -> Result<(), Error> { + if input.prev_out_index != 0 { + return Err(Error::InvalidInput); + } + if input.prev_out_value != 0 { + return Err(Error::InvalidInput); + } + let expected_txid = create_to_spend_txid(message, script_pubkey); + if input.prev_out_hash.as_slice() != expected_txid { + return Err(Error::InvalidInput); + } + Ok(()) +} + +/// Validate the BIP-322 output. +/// +/// Checks: value=0, type=OP_RETURN. +pub fn validate_output(output: &pb::BtcSignOutputRequest) -> Result<(), Error> { + if output.value != 0 { + return Err(Error::InvalidInput); + } + if pb::BtcOutputType::try_from(output.r#type)? != pb::BtcOutputType::OpReturn { + return Err(Error::InvalidInput); + } + Ok(()) +} + +/// The variant prefix for the "simple" BIP-322 signature format. +/// +/// Per BIP-322 v1.0.0 §"Types of Signatures", a simple signature is prefixed with `smp`. +pub const SIMPLE_PREFIX: &[u8; 3] = b"smp"; + +/// Encode a signature as a BIP-322 "simple" signature. +/// +/// Per BIP-322 v1.0.0: the witness stack +/// is consensus encoded as +/// `varint(num_items) || varint(item_len) || item_data` for each item, base64-encoded, and +/// prefixed with `smp`. +pub fn encode_simple_witness(sig: &[u8]) -> Vec { + // Consensus-encoded witness stack: varint(1) || varint(sig_len) || sig. + let mut witness_stack = Vec::with_capacity(2 + sig.len()); + witness_stack.push(0x01); // 1 witness stack item + witness_stack.push(sig.len() as u8); // item length + witness_stack.extend_from_slice(sig); + + // Base64-encode: output length is ceil(input_len / 3) * 4. We allocate one extra byte to + // work around a bug in `binascii::b64encode` which writes past the calculated end when the + // input length is a multiple of 3. + let b64_len = witness_stack.len().div_ceil(3) * 4; + let mut b64_buf = vec![0u8; b64_len + 1]; + let b64 = binascii::b64encode(&witness_stack, &mut b64_buf) + .expect("base64 output buffer too small"); + + let mut out = Vec::with_capacity(SIMPLE_PREFIX.len() + b64.len()); + out.extend_from_slice(SIMPLE_PREFIX); + out.extend_from_slice(b64); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tagged_hash() { + use sha2::Sha256; + let hash = tagged_hash(b""); + let tag = Sha256::digest(BIP322_TAG); + let mut ctx = Sha256::new(); + ctx.update(&tag); + ctx.update(&tag); + let expected: [u8; 32] = ctx.finalize().into(); + assert_eq!(hash, expected); + } + + #[test] + fn test_create_to_spend_txid() { + let script_pubkey = hex_lit::hex!( + "5120a60869f0dbcf1dc659c9cecbee8b89cea43c4a2906acdb10a681b4bbaef14274" + ); + let txid = create_to_spend_txid(b"", &script_pubkey); + assert_eq!(txid.len(), 32); + // Deterministic. + assert_eq!(txid, create_to_spend_txid(b"", &script_pubkey)); + // Different message produces different txid. + assert_ne!(txid, create_to_spend_txid(b"hello", &script_pubkey)); + } + + #[test] + fn test_sighash_taproot() { + let script_pubkey = hex_lit::hex!( + "5120a60869f0dbcf1dc659c9cecbee8b89cea43c4a2906acdb10a681b4bbaef14274" + ); + let hash = sighash(b"", &script_pubkey, 0, 0, 0, SighashMode::Taproot); + assert_eq!(hash.len(), 32); + // Deterministic. + assert_eq!(hash, sighash(b"", &script_pubkey, 0, 0, 0, SighashMode::Taproot)); + // Different message produces different sighash. + assert_ne!(hash, sighash(b"hello", &script_pubkey, 0, 0, 0, SighashMode::Taproot)); + // Different version/locktime/sequence produce different sighashes (full format). + assert_ne!(hash, sighash(b"", &script_pubkey, 2, 0, 0, SighashMode::Taproot)); + assert_ne!(hash, sighash(b"", &script_pubkey, 0, 1, 0, SighashMode::Taproot)); + assert_ne!(hash, sighash(b"", &script_pubkey, 0, 0, 1, SighashMode::Taproot)); + } + + #[test] + fn test_sighash_segwit_v0() { + // P2SH-P2WPKH challenge: scriptPubKey is OP_HASH160 PUSH20 OP_EQUAL. + let script_pubkey = + hex_lit::hex!("a91464096dffec1b1b52addf3020a9f01be8b812c3f987"); + // The BIP-143 script_code for a P2WPKH(-P2SH) input is the P2PKH-form script: + // 0x76a914 88ac. + let script_code = + hex_lit::hex!("76a91464096dffec1b1b52addf3020a9f01be8b812c3f988ac"); + let mode = SighashMode::SegwitV0 { + script_code: &script_code, + }; + let hash = sighash(b"", &script_pubkey, 0, 0, 0, mode); + assert_eq!(hash.len(), 32); + // Deterministic. + let hash2 = sighash( + b"", + &script_pubkey, + 0, + 0, + 0, + SighashMode::SegwitV0 { + script_code: &script_code, + }, + ); + assert_eq!(hash, hash2); + // Different message produces different sighash. + let hash3 = sighash( + b"hello", + &script_pubkey, + 0, + 0, + 0, + SighashMode::SegwitV0 { + script_code: &script_code, + }, + ); + assert_ne!(hash, hash3); + // Taproot mode produces a different sighash for the same inputs. + let hash_tr = sighash(b"", &script_pubkey, 0, 0, 0, SighashMode::Taproot); + assert_ne!(hash, hash_tr); + } + + #[test] + fn test_validate_init_ok() { + // Simple format defaults. + let request = pb::BtcSignInitRequest { + version: 0, + locktime: 0, + num_inputs: 1, + num_outputs: 1, + ..Default::default() + }; + assert!(validate_init(&request).is_ok()); + + // Full format: version=2, non-zero locktime is allowed. + let request = pb::BtcSignInitRequest { + version: 2, + locktime: 100, + num_inputs: 1, + num_outputs: 1, + ..Default::default() + }; + assert!(validate_init(&request).is_ok()); + } + + #[test] + fn test_validate_init_bad_version() { + // Per spec, only version 0 or 2 is allowed. + let request = pb::BtcSignInitRequest { + version: 1, + locktime: 0, + num_inputs: 1, + num_outputs: 1, + ..Default::default() + }; + assert!(validate_init(&request).is_err()); + + let request = pb::BtcSignInitRequest { + version: 3, + locktime: 0, + num_inputs: 1, + num_outputs: 1, + ..Default::default() + }; + assert!(validate_init(&request).is_err()); + } + + #[test] + fn test_validate_init_bad_num_inputs() { + let request = pb::BtcSignInitRequest { + version: 0, + locktime: 0, + num_inputs: 2, + num_outputs: 1, + ..Default::default() + }; + assert!(validate_init(&request).is_err()); + } + + #[test] + fn test_validate_input_ok() { + let script_pubkey = hex_lit::hex!( + "5120a60869f0dbcf1dc659c9cecbee8b89cea43c4a2906acdb10a681b4bbaef14274" + ); + let txid = create_to_spend_txid(b"hello", &script_pubkey); + let input = pb::BtcSignInputRequest { + prev_out_hash: txid.to_vec(), + prev_out_index: 0, + prev_out_value: 0, + sequence: 0, + ..Default::default() + }; + assert!(validate_input(&input, b"hello", &script_pubkey).is_ok()); + } + + #[test] + fn test_validate_input_bad_txid() { + let script_pubkey = hex_lit::hex!( + "5120a60869f0dbcf1dc659c9cecbee8b89cea43c4a2906acdb10a681b4bbaef14274" + ); + let input = pb::BtcSignInputRequest { + prev_out_hash: vec![0u8; 32], // wrong txid + prev_out_index: 0, + prev_out_value: 0, + sequence: 0, + ..Default::default() + }; + assert!(validate_input(&input, b"hello", &script_pubkey).is_err()); + } + + #[test] + fn test_validate_output_ok() { + let output = pb::BtcSignOutputRequest { + value: 0, + r#type: pb::BtcOutputType::OpReturn as _, + ..Default::default() + }; + assert!(validate_output(&output).is_ok()); + } + + #[test] + fn test_validate_output_bad_value() { + let output = pb::BtcSignOutputRequest { + value: 100, + r#type: pb::BtcOutputType::OpReturn as _, + ..Default::default() + }; + assert!(validate_output(&output).is_err()); + } + + #[test] + fn test_validate_output_bad_type() { + let output = pb::BtcSignOutputRequest { + value: 0, + r#type: pb::BtcOutputType::P2tr as _, + ..Default::default() + }; + assert!(validate_output(&output).is_err()); + } + + #[test] + fn test_encode_simple_witness_schnorr() { + // 64-byte signature (Schnorr / P2TR with SIGHASH_DEFAULT). + let sig = [0xABu8; 64]; + let encoded = encode_simple_witness(&sig); + // Prefix "smp" (3 bytes) + base64 of (1 + 1 + 64 = 66 bytes) = 3 + 88 = 91 bytes. + assert_eq!(encoded.len(), 91); + assert_eq!(&encoded[..3], b"smp"); + // Witness stack: 01 40 AB...(64x). Base64 of "01 40 ABAB...AB" is deterministic. + // 0xABAB...AB in groups of 3 bytes: 0xABABAB = base64 "q6ur" (repeated). + // The full base64 string starts with the encoded header bytes 01 40 AB... + // Verify it ends with base64 padding for 66 mod 3 = 0 (no padding). + assert!(!encoded.ends_with(b"=")); + } + + #[test] + fn test_encode_simple_witness_ecdsa() { + // ECDSA DER signature can be 70-72 bytes. + let sig = [0xCDu8; 71]; + let encoded = encode_simple_witness(&sig); + // Prefix "smp" + base64 of (1 + 1 + 71 = 73 bytes) = 3 + 100 = 103 bytes. + assert_eq!(encoded.len(), 103); + assert_eq!(&encoded[..3], b"smp"); + // 73 mod 3 = 1, so the base64 ends with "==" padding. + assert!(encoded.ends_with(b"==")); + } +} diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/bip341.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/bip341.rs index 122b41b809..b5925d863c 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/bip341.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/bip341.rs @@ -3,6 +3,19 @@ use sha2::Digest; use sha2::Sha256; +/// Return a SHA-256 engine pre-loaded with the BIP-340 tagged hash prefix: +/// `SHA256(tag) || SHA256(tag)`. +/// +/// Callers feed their message into the returned engine and finalize it to obtain +/// `SHA256(SHA256(tag) || SHA256(tag) || msg)`. +pub fn tagged_hash_engine(tag: &[u8]) -> Sha256 { + let tag_hash = Sha256::digest(tag); + let mut ctx = Sha256::new(); + ctx.update(&tag_hash); + ctx.update(&tag_hash); + ctx +} + /// https://github.com/bitcoin/bips/blob/bb8dc57da9b3c6539b88378348728a2ff43f7e9c/bip-0341.mediawiki#common-signature-message pub struct Args { // Transaction data: @@ -26,10 +39,7 @@ pub struct Args { /// /// The hash_type is assumed 0 (`SIGHASH_DEFAULT`). `annex` is assumed to be not present. pub fn sighash(args: &Args) -> [u8; 32] { - let tag = Sha256::digest(b"TapSighash"); - let mut ctx = Sha256::new(); - ctx.update(tag); - ctx.update(tag); + let mut ctx = tagged_hash_engine(b"TapSighash"); // Sighash epoch 0 ctx.update(0u8.to_le_bytes()); // Control: diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs index 3245b47dad..4bdf5665a7 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signmsg.rs @@ -18,13 +18,112 @@ use crate::keystore; use crate::hal::Ui; use crate::workflow::verify_message; use bitcoin::consensus::encode::{VarInt, serialize}; +use bitcoin::hashes::Hash as _; + +use super::{bip322, common, params}; +use crate::keystore::Compute; const MAX_MESSAGE_SIZE: usize = 1024; +/// Compute the BIP-322 signature for a P2TR key-path spend. +/// +/// Uses bip322::sighash() for the sighash and bip322::encode_simple_witness() for encoding. +async fn sign_bip322_p2tr( + hal: &mut impl crate::hal::Hal, + coin: BtcCoin, + keypath: &[u32], + msg: &[u8], +) -> Result, Error> { + let coin_params = params::get(coin); + let mut xpub_cache = crate::xpubcache::XpubCache::new(Compute::Twice); + + let script_pubkey = + common::Payload::from_simple(hal, &mut xpub_cache, coin_params, SimpleType::P2tr, keypath) + .await? + .pk_script(coin_params)?; + + // Simple format: version=0, locktime=0, sequence=0. + let sighash = bip322::sighash(msg, &script_pubkey, 0, 0, 0, bip322::SighashMode::Taproot); + + // BIP-86 key-path spend: tweak private key by hash of public key (no merkle root). + let xpub = xpub_cache.get_xpub(hal, keypath).await?; + let pubkey = bitcoin::PublicKey::from_slice(xpub.public_key()).map_err(|_| Error::Generic)?; + let tweak = bitcoin::TapTweakHash::from_key_and_tweak(pubkey.into(), None); + + let sig = + keystore::secp256k1_schnorr_sign(hal, keypath, &sighash, Some(tweak.as_byte_array())) + .await?; + + Ok(bip322::encode_simple_witness(&sig)) +} + +/// Compute the legacy "Bitcoin Signed Message" signature (ECDSA). +/// +/// Returns a 65-byte signature: 64 bytes secp256k1 compact (R, S) + 1 byte recovery id. +async fn sign_legacy( + hal: &mut impl crate::hal::Hal, + keypath: &[u32], + msg: &[u8], + host_nonce_commitment: &Option, +) -> Result, Error> { + // See + // https://github.com/spesmilo/electrum/blob/84dc181b6e7bb20e88ef6b98fb8925c5f645a765/electrum/ecc.py#L355-L358. + // This is the message format that is widespread for p2pkh addresses. + // Electrum re-used it for p2wpkh-p2sh and p2wpkh addresses. + let mut extended_msg: Vec = Vec::new(); + extended_msg.extend(b"\x18Bitcoin Signed Message:\n"); + extended_msg.extend(serialize(&VarInt(msg.len() as _))); + extended_msg.extend(msg); + + let sighash: [u8; 32] = Sha256::digest(Sha256::digest(extended_msg)).into(); + + let host_nonce = match host_nonce_commitment { + // Engage in the anti-klepto protocol if the host sends a host nonce commitment. + Some(pb::AntiKleptoHostNonceCommitment { commitment }) => { + let signer_commitment = crate::secp256k1::secp256k1_nonce_commit( + keystore::secp256k1_get_private_key(hal, keypath) + .await? + .as_slice() + .try_into() + .unwrap(), + &sighash, + commitment + .as_slice() + .try_into() + .or(Err(Error::InvalidInput))?, + )?; + + // Send signer commitment to host and wait for the host nonce from the host. + super::antiklepto_get_host_nonce(signer_commitment).await? + } + + // Return signature directly without the anti-klepto protocol, for backwards compatibility. + None => [0; 32], + }; + + let sign_result = crate::secp256k1::secp256k1_sign( + keystore::secp256k1_get_private_key(hal, keypath) + .await? + .as_slice() + .try_into() + .unwrap(), + &sighash, + Some(&host_nonce), + )?; + let mut signature: Vec = sign_result.signature.to_vec(); + signature.push(sign_result.recid); + Ok(signature) +} + /// Process a sign message request. /// -/// The result contains a 65 byte signature. The first 64 bytes are the secp256k1 signature in -/// compact format (R and S values), and the last byte is the recoverable id (recid). +/// For non-taproot types, the result contains a 65 byte signature. The first 64 bytes are the +/// secp256k1 signature in compact format (R and S values), and the last byte is the recoverable id +/// (recid). +/// +/// For P2TR (taproot), the result is a BIP-322 "simple" encoded signature: the 3-byte ASCII +/// variant prefix `smp` followed by a serialized witness containing a single 64-byte Schnorr +/// signature (69 bytes total). pub async fn process( hal: &mut impl crate::hal::Hal, request: &pb::BtcSignMessageRequest, @@ -43,9 +142,6 @@ pub async fn process( }) => (keypath, SimpleType::try_from(*simple_type)?), _ => return Err(Error::InvalidInput), }; - if simple_type == SimpleType::P2tr { - return Err(Error::InvalidInput); - } if request.msg.len() > MAX_MESSAGE_SIZE { return Err(Error::InvalidInput); } @@ -81,53 +177,12 @@ pub async fn process( verify_message::verify(hal, "Sign message", "Sign", &request.msg, true).await?; - // See - // https://github.com/spesmilo/electrum/blob/84dc181b6e7bb20e88ef6b98fb8925c5f645a765/electrum/ecc.py#L355-L358. - // This is the message format that is widespread for p2pkh addresses. - // Electrum re-used it for p2wpkh-p2sh and p2wpkh addresses. - let mut msg: Vec = Vec::new(); - msg.extend(b"\x18Bitcoin Signed Message:\n"); - msg.extend(serialize(&VarInt(request.msg.len() as _))); - msg.extend(&request.msg); - - let sighash: [u8; 32] = Sha256::digest(Sha256::digest(msg)).into(); - - let host_nonce = match request.host_nonce_commitment { - // Engage in the anti-klepto protocol if the host sends a host nonce commitment. - Some(pb::AntiKleptoHostNonceCommitment { ref commitment }) => { - let signer_commitment = crate::secp256k1::secp256k1_nonce_commit( - keystore::secp256k1_get_private_key(hal, keypath) - .await? - .as_slice() - .try_into() - .unwrap(), - &sighash, - commitment - .as_slice() - .try_into() - .or(Err(Error::InvalidInput))?, - )?; - - // Send signer commitment to host and wait for the host nonce from the host. - super::antiklepto_get_host_nonce(signer_commitment).await? - } - - // Return signature directly without the anti-klepto protocol, for backwards compatibility. - None => [0; 32], + let signature = if simple_type == SimpleType::P2tr { + sign_bip322_p2tr(hal, coin, keypath, &request.msg).await? + } else { + sign_legacy(hal, keypath, &request.msg, &request.host_nonce_commitment).await? }; - let sign_result = crate::secp256k1::secp256k1_sign( - keystore::secp256k1_get_private_key(hal, keypath) - .await? - .as_slice() - .try_into() - .unwrap(), - &sighash, - Some(&host_nonce), - )?; - let mut signature: Vec = sign_result.signature.to_vec(); - signature.push(sign_result.recid); - Ok(Response::SignMessage(pb::BtcSignMessageResponse { signature, })) @@ -272,6 +327,55 @@ mod tests { ); } + #[async_test::test] + pub async fn test_p2tr() { + let request = pb::BtcSignMessageRequest { + coin: BtcCoin::Btc as _, + script_config: Some(pb::BtcScriptConfigWithKeypath { + script_config: Some(pb::BtcScriptConfig { + config: Some(Config::SimpleType(SimpleType::P2tr as _)), + }), + keypath: vec![86 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0], + }), + msg: MESSAGE.as_bytes().to_vec(), + host_nonce_commitment: None, + }; + + mock_unlocked(); + let mut mock_hal = TestingHal::new(); + let result = process(&mut mock_hal, &request).await; + assert!(result.is_ok()); + match result.unwrap() { + Response::SignMessage(pb::BtcSignMessageResponse { signature }) => { + // BIP-322 simple encoding: "smp" prefix (3 bytes) + base64 of consensus-encoded + // witness stack (0x01 || 0x40 || 64-byte sig = 66 bytes => 88 base64 chars). + assert_eq!(signature.len(), 91); + assert_eq!(&signature[..3], b"smp"); + } + _ => panic!("expected SignMessage response"), + } + assert_eq!( + mock_hal.ui.screens, + vec![ + Screen::Confirm { + title: "Sign message".into(), + body: "Coin: Bitcoin".into(), + longtouch: false, + }, + Screen::Confirm { + title: "Address".into(), + body: "bc1p z6xe mnzk 9nzj pt5a z3v8 27st 6e72 emt3 364v z6u3 p7gl rg56 r39q et59 hc".into(), + longtouch: false, + }, + Screen::Confirm { + title: "Sign message".into(), + body: MESSAGE.into(), + longtouch: true, + }, + ] + ); + } + #[async_test::test] pub async fn test_process_user_aborted() { let request = pb::BtcSignMessageRequest { @@ -367,26 +471,6 @@ mod tests { Err(Error::InvalidInput) ); - // Invalid script type (taproot not supported) - assert_eq!( - process( - &mut TestingHal::new(), - &pb::BtcSignMessageRequest { - coin: BtcCoin::Btc as _, - script_config: Some(pb::BtcScriptConfigWithKeypath { - script_config: Some(pb::BtcScriptConfig { - config: Some(Config::SimpleType(SimpleType::P2tr as _)), - }), - keypath: vec![86 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0], - }), - msg: MESSAGE.as_bytes().to_vec(), - host_nonce_commitment: None, - } - ) - .await, - Err(Error::InvalidInput) - ); - // Invalid script type (multisig not supported) assert_eq!( process( 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 7d71735132..47ce91ac09 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs @@ -8,12 +8,12 @@ use super::super::payment_request; use super::common::format_amount; use super::policies::TaprootSpendInfo; use super::script_configs::{ValidatedScriptConfig, ValidatedScriptConfigWithKeypath}; -use super::{bip143, bip341, common, keypath}; +use super::{bip143, bip322, bip341, common, keypath}; use crate::hal::Ui; use crate::keystore::Compute; use crate::secp256k1::SECP256K1; -use crate::workflow::transaction; +use crate::workflow::{transaction, verify_message}; use crate::xpubcache::Bip32XpubCache; use alloc::string::String; @@ -491,6 +491,7 @@ async fn validate_input_script_configs<'a>( hal: &mut impl crate::hal::Hal, coin_params: &super::params::Params, script_configs: &'a [pb::BtcScriptConfigWithKeypath], + is_bip322: bool, ) -> Result>, Error> { if script_configs.is_empty() { return Err(Error::InvalidInput); @@ -498,6 +499,13 @@ async fn validate_input_script_configs<'a>( let script_configs = validate_script_configs(hal, coin_params, script_configs).await?; + // BIP-322 is message signing, not spending — the to_sign virtual transaction is just a + // sighash carrier and nothing leaves the wallet. Using "Spend from" as the title for the + // multisig/policy confirmation in that flow would be misleading; reuse the same + // "Sign message" header that process_bip322 puts on the address and message screens so + // the whole BIP-322 confirmation sequence reads consistently. + let confirm_title = if is_bip322 { "Sign message" } else { "Spend from" }; + // If there are multiple script configs, only SimpleType (single sig, no additional inputs) // configs are allowed, so e.g. mixing p2wpkh and pw2wpkh-p2sh is okay, but mixing p2wpkh with // multisig-pw2sh is not. @@ -511,7 +519,7 @@ async fn validate_input_script_configs<'a>( }, ] = script_configs.as_slice() { - super::multisig::confirm(hal, "Spend from", coin_params, name, multisig).await?; + super::multisig::confirm(hal, confirm_title, coin_params, name, multisig).await?; return Ok(script_configs); } @@ -537,7 +545,7 @@ async fn validate_input_script_configs<'a>( parsed_policy .confirm( hal, - "Spend from", + confirm_title, coin_params, name, super::policies::Mode::Basic, @@ -644,6 +652,267 @@ impl<'a> TryFrom<&'a ValidatedScriptConfigWithKeypath<'a>> } } +/// Compute the TaprootSpendInfo for a taproot input. +/// +/// For SimpleType::P2tr this is a BIP-86 key-path spend (tweak by hash of pubkey, no merkle root). +/// For Policy with a Taproot descriptor, it delegates to the parsed policy. +async fn get_taproot_spend_info( + hal: &mut impl crate::hal::Hal, + xpub_cache: &mut Bip32XpubCache, + script_config_account: &ValidatedScriptConfigWithKeypath<'_>, + keypath: &[u32], +) -> Result { + match &script_config_account.config { + ValidatedScriptConfig::SimpleType(SimpleType::P2tr) => { + // This is a BIP-86 spend, so we tweak the private key by the hash of the public + // key only, as there is no Taproot merkle root. + let xpub = xpub_cache.get_xpub(hal, keypath).await?; + let pubkey = + bitcoin::PublicKey::from_slice(xpub.public_key()).map_err(|_| Error::Generic)?; + Ok(TaprootSpendInfo::KeySpend( + bitcoin::TapTweakHash::from_key_and_tweak(pubkey.into(), None), + )) + } + ValidatedScriptConfig::Policy { parsed_policy, .. } => { + // Get the Taproot tweak based on whether we spend using the internal key (key + // path spend) or if we spend using a leaf script. For key path spends, we must + // first tweak the private key to match the Taproot output key. For leaf + // scripts, we do not tweak. + + Ok(parsed_policy + .taproot_spend_info(hal, xpub_cache, keypath) + .await?) + } + _ => return Err(Error::Generic), + } +} + +/// Sign a taproot input given its sighash and spend info. +/// +/// Returns the 64-byte Schnorr signature. +async fn sign_taproot_input( + hal: &mut impl crate::hal::Hal, + keypath: &[u32], + sighash: &[u8; 32], + spend_info: &TaprootSpendInfo, +) -> Result, Error> { + Ok(crate::keystore::secp256k1_schnorr_sign( + hal, + keypath, + &sighash, + if let TaprootSpendInfo::KeySpend(tweak_hash) = &spend_info { + Some(tweak_hash.as_byte_array()) + } else { + None + }, + ) + .await? + .to_vec()) +} + +/// Sign an ECDSA input given its sighash, handling the anti-klepto protocol if needed. +/// +/// Returns the 64-byte compact signature (R, S). +async fn sign_ecdsa_input( + hal: &mut impl crate::hal::Hal, + keypath: &[u32], + sighash: &[u8; 32], + host_nonce_commitment: &Option, + input_index: u32, + next_response: &mut NextResponse, +) -> Result, Error> { + let private_key = crate::keystore::secp256k1_get_private_key(hal, keypath).await?; + // Engage in the Anti-Klepto protocol if the host sends a host nonce commitment. + let host_nonce: [u8; 32] = match host_nonce_commitment { + Some(pb::AntiKleptoHostNonceCommitment { commitment }) => { + let signer_commitment = crate::secp256k1::secp256k1_nonce_commit( + private_key.as_slice().try_into().unwrap(), + sighash, + commitment + .as_slice() + .try_into() + .or(Err(Error::InvalidInput))?, + )?; + next_response.next.anti_klepto_signer_commitment = + Some(pb::AntiKleptoSignerCommitment { + commitment: signer_commitment.to_vec(), + }); + + get_antiklepto_host_nonce(input_index, next_response) + .await? + .host_nonce + .as_slice() + .try_into() + .or(Err(Error::InvalidInput))? + } + // Return signature directly without the anti-klepto protocol, for backwards compatibility. + None => [0; 32], + }; + + let sign_result = crate::secp256k1::secp256k1_sign( + private_key.as_slice().try_into().unwrap(), + sighash, + Some(&host_nonce), + )?; + drop(private_key); + Ok(sign_result.signature.to_vec()) +} + +/// Handle a BIP-322 message signing request received through the signtx streaming protocol. +/// +/// This is called from `_process` when `bip322_message` is set in the init request. +/// It follows the same streaming protocol (get_tx_input/get_tx_output) but shows +/// message-signing UI instead of transaction UI. +async fn process_bip322( + hal: &mut impl crate::hal::Hal, + request: &pb::BtcSignInitRequest, + message: &[u8], + coin: pb::BtcCoin, + validated_script_configs: &[ValidatedScriptConfigWithKeypath<'_>], + mut xpub_cache: Bip32XpubCache, + mut next_response: NextResponse, +) -> Result { + bip322::validate_init(request)?; + + let coin_params = super::params::get(coin); + + // Phase 1: Receive the single input. + let tx_input = get_tx_input(0, &mut next_response).await?; + let script_config_account = validated_script_configs + .get(tx_input.script_config_index as usize) + .ok_or(Error::InvalidInput)?; + + // Validate keypath. + validate_keypath( + coin_params, + script_config_account, + &tx_input.keypath, + keypath::ReceiveSpend::Spend, + )?; + + // Derive the scriptPubKey from the script config to validate the to_spend txid. + let script_pubkey = common::Payload::from( + hal, + &mut xpub_cache, + coin_params, + &tx_input.keypath, + script_config_account, + ) + .await? + .pk_script(coin_params)?; + + bip322::validate_input(&tx_input, message, &script_pubkey)?; + + // Phase 2: Receive and validate the single OP_RETURN output. + let tx_output = get_tx_output(0, &mut next_response).await?; + bip322::validate_output(&tx_output)?; + + // Phase 3: Message signing UI. + let address = common::Payload::from( + hal, + &mut xpub_cache, + coin_params, + &tx_input.keypath, + script_config_account, + ) + .await? + .address(coin_params)?; + let address_formatted = util::strings::format_address(&address); + + let basic_info = format!("Coin: {}", coin_params.name); + hal.ui() + .confirm(&ConfirmParams { + title: "Sign message", + body: &basic_info, + accept_is_nextarrow: true, + ..Default::default() + }) + .await?; + + hal.ui() + .confirm(&ConfirmParams { + title: "Address", + body: &address_formatted, + scrollable: true, + accept_is_nextarrow: true, + ..Default::default() + }) + .await?; + + verify_message::verify(hal, "Sign message", "Sign", message, true).await?; + + // Phase 4: Sign the input (second pass). + let tx_input = get_tx_input(0, &mut next_response).await?; + validate_keypath( + coin_params, + script_config_account, + &tx_input.keypath, + keypath::ReceiveSpend::Spend, + )?; + + // The sighash algorithm has to match what a BIP-322 verifier will use when it runs the + // resulting signature through the standard script interpreter: BIP-341 for taproot, BIP-143 + // for v0 segwit (P2WPKH, P2WPKH-P2SH, P2WSH, P2WSH-P2SH). Signing the BIP-341 sighash with + // ECDSA against a P2WPKH script would silently produce a signature that no verifier + // accepts. + // + // Pass through version/locktime/sequence from the request so that full-format BIP-322 + // signatures (e.g. with timelocks) compute the correct sighash. For the simple format the + // host sends version=0, locktime=0, sequence=0. + if is_taproot(script_config_account) { + if tx_input.host_nonce_commitment.is_some() { + return Err(Error::InvalidInput); + } + let sighash = bip322::sighash( + message, + &script_pubkey, + request.version, + request.locktime, + tx_input.sequence, + bip322::SighashMode::Taproot, + ); + let spend_info = get_taproot_spend_info( + hal, + &mut xpub_cache, + script_config_account, + &tx_input.keypath, + ) + .await?; + next_response.next.signature = + sign_taproot_input(hal, &tx_input.keypath, &sighash, &spend_info).await?; + } else { + let script_code = + sighash_script(hal, &mut xpub_cache, script_config_account, &tx_input.keypath).await?; + let sighash = bip322::sighash( + message, + &script_pubkey, + request.version, + request.locktime, + tx_input.sequence, + bip322::SighashMode::SegwitV0 { + script_code: &script_code, + }, + ); + + // sign_ecdsa_input may engage the anti-klepto exchange which resets next_response.next + // via get_request. We therefore set has_signature only AFTER it returns. + next_response.next.signature = sign_ecdsa_input( + hal, + &tx_input.keypath, + &sighash, + &tx_input.host_nonce_commitment, + 0, + &mut next_response, + ) + .await?; + } + next_response.next.has_signature = true; + + // Phase 5: Done. + next_response.next.r#type = NextType::Done as _; + Ok(next_response.to_protobuf()) +} + /// Singing flow: /// /// init @@ -699,26 +968,48 @@ async fn _process( let coin_params = super::params::get(coin); // Validate the format_unit. let format_unit = FormatUnit::try_from(request.format_unit)?; + + let is_bip322 = request.bip322_message.is_some(); + // Currently we do not support time-based nlocktime if request.locktime >= 500000000 { return Err(Error::InvalidInput); } - // Currently only support version 1 or version 2 tx. + // Version 1 or 2 for normal tx, version 0 allowed for BIP-322. // Version 2: https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki - if request.version != 1 && request.version != 2 { + if request.version != 1 && request.version != 2 && !(is_bip322 && request.version == 0) { return Err(Error::InvalidInput); } if request.num_inputs < 1 || request.num_outputs < 1 { return Err(Error::InvalidInput); } let validated_script_configs = - validate_input_script_configs(hal, coin_params, &request.script_configs).await?; + validate_input_script_configs(hal, coin_params, &request.script_configs, is_bip322) + .await?; let validated_output_script_configs = validate_script_configs(hal, coin_params, &request.output_script_configs).await?; let mut xpub_cache = Bip32XpubCache::new(Compute::Once); setup_xpub_cache(&mut xpub_cache, &request.script_configs); + // BIP-322 message signing: branch into dedicated handler. + if let Some(ref message) = request.bip322_message { + let next_response = NextResponse { + next: Default::default(), + wrap: false, + }; + return process_bip322( + hal, + request, + message, + coin, + &validated_script_configs, + xpub_cache, + next_response, + ) + .await; + } + // For now we only allow one payment request with one output per transaction. In the future, // this could be extended to allow multiple outputs per payment request (payment request // requests payout to multiple addresses/outputs), as well as multiple payment requests per @@ -1199,30 +1490,13 @@ async fn _process( return Err(Error::InvalidInput); } - let spend_info = match &script_config_account.config { - ValidatedScriptConfig::SimpleType(SimpleType::P2tr) => { - // This is a BIP-86 spend, so we tweak the private key by the hash of the public - // key only, as there is no Taproot merkle root. - let xpub = xpub_cache.get_xpub(hal, &tx_input.keypath).await?; - let pubkey = bitcoin::PublicKey::from_slice(xpub.public_key()) - .map_err(|_| Error::Generic)?; - TaprootSpendInfo::KeySpend(bitcoin::TapTweakHash::from_key_and_tweak( - pubkey.into(), - None, - )) - } - ValidatedScriptConfig::Policy { parsed_policy, .. } => { - // Get the Taproot tweak based on whether we spend using the internal key (key - // path spend) or if we spend using a leaf script. For key path spends, we must - // first tweak the private key to match the Taproot output key. For leaf - // scripts, we do not tweak. - - parsed_policy - .taproot_spend_info(hal, &mut xpub_cache, &tx_input.keypath) - .await? - } - _ => return Err(Error::Generic), - }; + let spend_info = get_taproot_spend_info( + hal, + &mut xpub_cache, + script_config_account, + &tx_input.keypath, + ) + .await?; let sighash = bip341::sighash(&bip341::Args { version: request.version, locktime: request.locktime, @@ -1239,19 +1513,9 @@ async fn _process( }, }); + next_response.next.signature = + sign_taproot_input(hal, &tx_input.keypath, &sighash, &spend_info).await?; next_response.next.has_signature = true; - next_response.next.signature = crate::keystore::secp256k1_schnorr_sign( - hal, - &tx_input.keypath, - &sighash, - if let TaprootSpendInfo::KeySpend(tweak_hash) = &spend_info { - Some(tweak_hash.as_byte_array()) - } else { - None - }, - ) - .await? - .to_vec(); } else { // Sign all other supported inputs. @@ -1276,43 +1540,18 @@ async fn _process( sighash_flags: SIGHASH_ALL, }); - let private_key = - crate::keystore::secp256k1_get_private_key(hal, &tx_input.keypath).await?; - // Engage in the Anti-Klepto protocol if the host sends a host nonce commitment. - let host_nonce: [u8; 32] = match tx_input.host_nonce_commitment { - Some(pb::AntiKleptoHostNonceCommitment { ref commitment }) => { - let signer_commitment = crate::secp256k1::secp256k1_nonce_commit( - private_key.as_slice().try_into().unwrap(), - &sighash, - commitment - .as_slice() - .try_into() - .or(Err(Error::InvalidInput))?, - )?; - next_response.next.anti_klepto_signer_commitment = - Some(pb::AntiKleptoSignerCommitment { - commitment: signer_commitment.to_vec(), - }); - - get_antiklepto_host_nonce(input_index, &mut next_response) - .await? - .host_nonce - .as_slice() - .try_into() - .or(Err(Error::InvalidInput))? - } - // Return signature directly without the anti-klepto protocol, for backwards compatibility. - None => [0; 32], - }; - - let sign_result = crate::secp256k1::secp256k1_sign( - private_key.as_slice().try_into().unwrap(), + // sign_ecdsa_input may engage the anti-klepto exchange which resets next_response.next + // via get_request. We therefore set has_signature only AFTER it returns. + next_response.next.signature = sign_ecdsa_input( + hal, + &tx_input.keypath, &sighash, - Some(&host_nonce), - )?; - drop(private_key); + &tx_input.host_nonce_commitment, + input_index, + &mut next_response, + ) + .await?; next_response.next.has_signature = true; - next_response.next.signature = sign_result.signature.to_vec(); } // Update progress. @@ -1641,6 +1880,7 @@ mod tests { .outputs .iter() .any(|output| output.silent_payment.is_some()), + bip322_message: None, } } @@ -1664,6 +1904,7 @@ mod tests { locktime: self.locktime, format_unit: FormatUnit::Default as _, contains_silent_payment_outputs: false, + bip322_message: None, } } @@ -1748,6 +1989,7 @@ mod tests { locktime: 0, format_unit: FormatUnit::Default as _, contains_silent_payment_outputs: false, + bip322_message: None, }; { @@ -1956,6 +2198,7 @@ mod tests { locktime: 0, format_unit: FormatUnit::Default as _, contains_silent_payment_outputs: false, + bip322_message: None, } ) .await, @@ -2281,6 +2524,95 @@ mod tests { ); } + /// Test BIP-322 message signing through the signtx streaming flow for a P2SH-P2WPKH + /// (BIP-49 nested SegWit) address. The host sends a `bip322_message` in the init request, + /// then streams the to_sign virtual transaction (single input spending to_spend, single + /// OP_RETURN output). The device signs with ECDSA and returns a 64-byte compact signature. + #[async_test::test] + pub async fn test_bip322_p2sh_p2wpkh() { + let coin = pb::BtcCoin::Btc; + let bip44_coin = super::super::params::get(coin).bip44_coin; + let keypath_account = vec![49 + HARDENED, bip44_coin, 0 + HARDENED]; + let keypath = vec![49 + HARDENED, bip44_coin, 0 + HARDENED, 0, 0]; + let message = b"BIP-322 streaming test message".to_vec(); + + mock_unlocked(); + + // Derive the scriptPubKey for the P2SH-P2WPKH address at the signing keypath, then + // compute the expected to_spend.txid the firmware will check against `prev_out_hash`. + let coin_params = super::super::params::get(coin); + let mut helper_hal = TestingHal::new(); + let mut helper_xpub_cache = Bip32XpubCache::new(Compute::Twice); + let script_pubkey = common::Payload::from_simple( + &mut helper_hal, + &mut helper_xpub_cache, + coin_params, + SimpleType::P2wpkhP2sh, + &keypath, + ) + .await + .unwrap() + .pk_script(coin_params) + .unwrap(); + let to_spend_txid = bip322::create_to_spend_txid(&message, &script_pubkey); + + let input = pb::BtcSignInputRequest { + prev_out_hash: to_spend_txid.to_vec(), + prev_out_index: 0, + prev_out_value: 0, + sequence: 0, + keypath: keypath.clone(), + script_config_index: 0, + host_nonce_commitment: None, + }; + let output = pb::BtcSignOutputRequest { + ours: false, + r#type: pb::BtcOutputType::OpReturn as _, + value: 0, + ..Default::default() + }; + + *crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() = + Some(Box::new(move |response: Response| { + let next = extract_next(&response); + match NextType::try_from(next.r#type).unwrap() { + NextType::Input => Ok(Request::BtcSignInput(input.clone())), + NextType::Output => Ok(Request::BtcSignOutput(output.clone())), + other => panic!("unexpected next type: {:?}", other), + } + })); + + let init_request = pb::BtcSignInitRequest { + coin: coin as _, + script_configs: vec![pb::BtcScriptConfigWithKeypath { + script_config: Some(pb::BtcScriptConfig { + config: Some(pb::btc_script_config::Config::SimpleType( + SimpleType::P2wpkhP2sh as _, + )), + }), + keypath: keypath_account, + }], + output_script_configs: vec![], + version: 0, + num_inputs: 1, + num_outputs: 1, + locktime: 0, + format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, + bip322_message: Some(message), + }; + + let result = process(&mut TestingHal::new(), &init_request).await; + match result { + Ok(Response::BtcSignNext(next)) => { + assert!(next.has_signature); + // ECDSA compact signature: R (32 bytes) + S (32 bytes), no recid. + assert_eq!(next.signature.len(), 64); + } + _ => panic!("wrong result: {:?}", result), + } + } + /// Test signing UTXOs with high keypath address indices. Even though we don't support verifying /// receive addresses at these indices (to mitigate ransom attacks), we should still be able to /// spend them. @@ -2986,6 +3318,7 @@ mod tests { locktime: tx.locktime, format_unit: FormatUnit::Default as _, contains_silent_payment_outputs: false, + bip322_message: None, } }; @@ -3083,6 +3416,7 @@ mod tests { locktime: tx.locktime, format_unit: FormatUnit::Default as _, contains_silent_payment_outputs: false, + bip322_message: None, } }; assert_eq!( @@ -3156,6 +3490,7 @@ mod tests { locktime: tx.locktime, format_unit: FormatUnit::Default as _, contains_silent_payment_outputs: false, + bip322_message: None, } }; let result = process(&mut mock_hal, &init_request).await; @@ -3240,6 +3575,7 @@ mod tests { locktime: tx.locktime, format_unit: FormatUnit::Default as _, contains_silent_payment_outputs: false, + bip322_message: None, } }; let result = process(&mut mock_hal, &init_request).await;