Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions messages/btc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 18 additions & 4 deletions py/bitbox02/bitbox02/bitbox02/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
112 changes: 56 additions & 56 deletions py/bitbox02/bitbox02/communication/generated/btc_pb2.py

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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"""
Expand All @@ -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

Expand Down
135 changes: 135 additions & 0 deletions py/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <msg_hash>, 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 <msg_hash>
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]:
Expand Down Expand Up @@ -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
Expand All @@ -968,27 +1002,128 @@ 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 <hash> 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(
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
)
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(
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
)
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):
Expand Down
6 changes: 6 additions & 0 deletions src/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading