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
25 changes: 24 additions & 1 deletion hwilib/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
getdescriptors,
prompt_pin,
toggle_passphrase,
register,
restore_device,
send_pin,
setup_device,
Expand All @@ -22,6 +23,7 @@
)
from .common import (
AddressType,
BIP388Policy,
Chain,
)
from .errors import (
Expand Down Expand Up @@ -59,6 +61,10 @@ def backup_device_handler(args: argparse.Namespace, client: HardwareWalletClient
def displayaddress_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]:
return displayaddress(client, desc=args.desc, path=args.path, addr_type=args.addr_type)

def register_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, str]:
policy = BIP388Policy(name=args.name, descriptor_template=args.desc, keys_info=args.key)
return register(client, bip388_policy=policy)

def enumerate_handler(args: argparse.Namespace) -> List[Dict[str, Any]]:
return enumerate(password=args.password, expert=args.expert, chain=args.chain, allow_emulators=args.allow_emulators)

Expand Down Expand Up @@ -88,7 +94,13 @@ def signmessage_handler(args: argparse.Namespace, client: HardwareWalletClient)
return signmessage(client, message=args.message, path=args.path)

def signtx_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, Union[bool, str]]:
return signtx(client, psbt=args.psbt)
policy = BIP388Policy(
name=args.policy_name,
descriptor_template=args.policy_desc,
keys_info=args.key,
hmac=args.hmac
)
return signtx(client, psbt=args.psbt, bip388_policy=policy)

def wipe_device_handler(args: argparse.Namespace, client: HardwareWalletClient) -> Dict[str, bool]:
return wipe_device(client)
Expand Down Expand Up @@ -161,6 +173,11 @@ def get_parser() -> HWIArgumentParser:

signtx_parser = subparsers.add_parser('signtx', help='Sign a PSBT')
signtx_parser.add_argument('psbt', help='The Partially Signed Bitcoin Transaction to sign')
signtx_policy_group = signtx_parser.add_argument_group("BIP388 policy")
signtx_policy_group.add_argument('--policy-name', help='Registered policy name')
signtx_policy_group.add_argument('--policy-desc', help='Registered policy descriptor template')
signtx_policy_group.add_argument('--key', help='Registered policy key information', action='append')
signtx_policy_group.add_argument('--hmac', help='Registered policy hmac, obtained via register command')
signtx_parser.set_defaults(func=signtx_handler)

getxpub_parser = subparsers.add_parser('getxpub', help='Get an extended public key')
Expand Down Expand Up @@ -197,6 +214,12 @@ def get_parser() -> HWIArgumentParser:
displayaddr_parser.add_argument("--addr-type", help="The address type to display", type=AddressType.argparse, choices=list(AddressType), default=AddressType.WIT) # type: ignore
displayaddr_parser.set_defaults(func=displayaddress_handler)

register_parser = subparsers.add_parser('register', help='Register a BIP388 wallet policy')
register_parser.add_argument('--name', help='Name for the policy')
register_parser.add_argument('--desc', help='Descriptor template, e.g. tr(musig(@0,@1)')
register_parser.add_argument('--key', help='Key information, e.g. [00000000/84h/0h/0h]xpub...', action='append')
register_parser.set_defaults(func=register_handler)

setupdev_parser = subparsers.add_parser('setup', help='Setup a device. Passphrase protection uses the password given by -p. Requires interactive mode')
setupdev_parser.add_argument('--label', '-l', help='The name to give to the device', default='')
setupdev_parser.add_argument('--backup_passphrase', '-b', help='The passphrase to use for the backup, if applicable', default='')
Expand Down
26 changes: 24 additions & 2 deletions hwilib/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from .devices import __all__ as all_devs
from .common import (
AddressType,
BIP388Policy,
Chain,
)
from .hwwclient import HardwareWalletClient
Expand Down Expand Up @@ -183,7 +184,11 @@ def getmasterxpub(client: HardwareWalletClient, addrtype: AddressType = AddressT
"""
return {"xpub": client.get_master_xpub(addrtype, account).to_string()}

def signtx(client: HardwareWalletClient, psbt: str) -> Dict[str, Union[bool, str]]:
def signtx(
client: HardwareWalletClient,
psbt: str,
bip388_policy: Optional[BIP388Policy]
) -> Dict[str, Union[bool, str]]:
"""
Sign a Partially Signed Bitcoin Transaction (PSBT) with the client.

Expand All @@ -195,7 +200,7 @@ def signtx(client: HardwareWalletClient, psbt: str) -> Dict[str, Union[bool, str
# Deserialize the transaction
tx = PSBT()
tx.deserialize(psbt)
result = client.sign_tx(tx).serialize()
result = client.sign_tx(tx, bip388_policy).serialize()
return {"psbt": result, "signed": result != psbt}

def getxpub(client: HardwareWalletClient, path: str, expert: bool = False) -> Dict[str, Any]:
Expand Down Expand Up @@ -494,6 +499,23 @@ def displayaddress(
return {"address": client.display_singlesig_address(pubkey.get_full_derivation_path(0), addr_type)}
raise BadArgumentError("Missing both path and descriptor")

def register(
client: HardwareWalletClient,
bip388_policy: BIP388Policy,
) -> Dict[str, str]:
"""
Register a BIP388 policy on the device for client.

:param name: Name for the policy
:param desc: Descriptor template
:return: A dictionary containing policy HMAC.
Returned as ``{"hmac": <hex string>}``.
:raises: BadArgumentError: if an argument is malformed, missing, or conflicts.
"""
assert bip388_policy.hmac is None

return {"hmac": client.register_bip388_policy(bip388_policy)}

def setup_device(client: HardwareWalletClient, label: str = "", backup_passphrase: str = "") -> Dict[str, bool]:
"""
Setup a device that has not yet been initialized.
Expand Down
18 changes: 16 additions & 2 deletions hwilib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
****************************
"""

from dataclasses import dataclass

import hashlib

from enum import Enum

from typing import Union

from typing import (
List,
Optional,
Union,
)

class Chain(Enum):
"""
Expand Down Expand Up @@ -56,6 +61,15 @@ def argparse(s: str) -> Union['AddressType', str]:
except KeyError:
return s

@dataclass
class BIP388Policy:
"""
Serialization agnostic BIP388 policy.
"""
name: str
descriptor_template: str
keys_info: List[str]
hmac: Optional[str] = None

def sha256(s: bytes) -> bytes:
"""
Expand Down
7 changes: 6 additions & 1 deletion hwilib/devices/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
)
from ..common import (
AddressType,
BIP388Policy,
Chain,
)

Expand Down Expand Up @@ -563,7 +564,11 @@ def display_multisig_address(
return address

@bitbox02_exception
def sign_tx(self, psbt: PSBT) -> PSBT:
def sign_tx(
self,
psbt: PSBT,
__: Optional[BIP388Policy],
) -> PSBT:
"""
Sign a transaction with the BitBox02.

Expand Down
19 changes: 12 additions & 7 deletions hwilib/devices/coldcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
)
from ..common import (
AddressType,
BIP388Policy,
Chain,
)
from functools import wraps
Expand Down Expand Up @@ -116,7 +117,11 @@ def get_master_fingerprint(self) -> bytes:
return struct.pack('<I', self.device.master_fingerprint)

@coldcard_exception
def sign_tx(self, tx: PSBT) -> PSBT:
def sign_tx(
self,
psbt: PSBT,
__: Optional[BIP388Policy],
) -> PSBT:
"""
Sign a transaction with the Coldcard.

Expand All @@ -132,7 +137,7 @@ def sign_tx(self, tx: PSBT) -> PSBT:

# For multisigs, we may need to do multiple passes if we appear in an input multiple times
passes = 1
for psbt_in in tx.inputs:
for psbt_in in psbt.inputs:
our_keys = 0
for key in psbt_in.hd_keypaths.keys():
keypath = psbt_in.hd_keypaths[key]
Expand All @@ -143,8 +148,8 @@ def sign_tx(self, tx: PSBT) -> PSBT:

for _ in range(passes):
# Get psbt in hex and then make binary
tx.convert_to_v0()
fd = io.BytesIO(base64.b64decode(tx.serialize()))
psbt.convert_to_v0()
fd = io.BytesIO(base64.b64decode(psbt.serialize()))

# learn size (portable way)
sz = fd.seek(0, 2)
Expand Down Expand Up @@ -190,10 +195,10 @@ def sign_tx(self, tx: PSBT) -> PSBT:

result = self.device.download_file(result_len, result_sha, file_number=1)

tx = PSBT()
tx.deserialize(base64.b64encode(result).decode())
psbt = PSBT()
psbt.deserialize(base64.b64encode(result).decode())

return tx
return psbt

@coldcard_exception
def sign_message(self, message: Union[str, bytes], keypath: str) -> str:
Expand Down
17 changes: 11 additions & 6 deletions hwilib/devices/digitalbitbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from ..common import (
AddressType,
BIP388Policy,
Chain,
hash256,
)
Expand Down Expand Up @@ -387,17 +388,21 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey:
return xpub

@digitalbitbox_exception
def sign_tx(self, tx: PSBT) -> PSBT:
def sign_tx(
self,
psbt: PSBT,
__: Optional[BIP388Policy],
) -> PSBT:

# Create a transaction with all scriptsigs blanked out
blank_tx = tx.get_unsigned_tx()
blank_tx = psbt.get_unsigned_tx()

# Get the master key fingerprint
master_fp = self.get_master_fingerprint()

# create sighashes
sighash_tuples = []
for txin, psbt_in, i_num in zip(blank_tx.vin, tx.inputs, range(len(blank_tx.vin))):
for txin, psbt_in, i_num in zip(blank_tx.vin, psbt.inputs, range(len(blank_tx.vin))):
sighash = b""
utxo = None
if psbt_in.witness_utxo:
Expand Down Expand Up @@ -493,7 +498,7 @@ def sign_tx(self, tx: PSBT) -> PSBT:

# Return early if nothing to do
if len(sighash_tuples) == 0:
return tx
return psbt

for i in range(0, len(sighash_tuples), 15):
tups = sighash_tuples[i:i + 15]
Expand Down Expand Up @@ -533,9 +538,9 @@ def sign_tx(self, tx: PSBT) -> PSBT:

# add sigs to tx
for tup, sig in zip(tups, der_sigs):
tx.inputs[tup[2]].partial_sigs[tup[3]] = sig
psbt.inputs[tup[2]].partial_sigs[tup[3]] = sig

return tx
return psbt

@digitalbitbox_exception
def sign_message(self, message: Union[str, bytes], keypath: str) -> str:
Expand Down
12 changes: 9 additions & 3 deletions hwilib/devices/jade.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
)
from ..common import (
AddressType,
BIP388Policy,
Chain,
sha256
)
Expand Down Expand Up @@ -370,16 +371,21 @@ def _split_at_last_hardened_element(path: Sequence[int]) -> Tuple[Sequence[int],
# Sign tx PSBT - newer Jade firmware supports native PSBT signing, but old firmwares require
# mapping to the legacy 'sign_tx' structures.
@jade_exception
def sign_tx(self, tx: PSBT) -> PSBT:
def sign_tx(
self,
psbt: PSBT,
__: Optional[BIP388Policy],
) -> PSBT:
"""
Sign a transaction with the Blockstream Jade.
"""

# Old firmware does not have native PSBT handling - use legacy method
if self.PSBT_SUPPORTED_FW_VERSION > self.fw_version.finalize_version():
return self.legacy_sign_tx(tx)
return self.legacy_sign_tx(psbt)

# Firmware 0.1.47 (March 2023) and later support native PSBT signing
psbt_b64 = tx.serialize()
psbt_b64 = psbt.serialize()
psbt_bytes = base64.b64decode(psbt_b64.strip())

# NOTE: sign_psbt() does not use AE signatures, so sticks with default (rfc6979)
Expand Down
Loading