Skip to content
Open
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
14 changes: 13 additions & 1 deletion hwilib/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from .common import (
AddressType,
BIP388Policy,
Chain,
)
from .errors import (
Expand Down Expand Up @@ -88,7 +89,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 +168,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
9 changes: 7 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
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
42 changes: 34 additions & 8 deletions hwilib/devices/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from ..common import (
AddressType,
Chain,
BIP388Policy,
)
from .ledger_bitcoin.client import (
createClient,
Expand Down Expand Up @@ -185,7 +186,11 @@ def get_pubkey_at_path(self, path: str) -> ExtendedKey:
return ExtendedKey.deserialize(xpub_str)

@ledger_exception
def sign_tx(self, tx: PSBT) -> PSBT:
def sign_tx(
self,
psbt: PSBT,
bip388_policy: Optional[BIP388Policy]
) -> PSBT:
"""
Sign a transaction with a Ledger device. Not all transactions can be signed by a Ledger.

Expand All @@ -198,6 +203,8 @@ def sign_tx(self, tx: PSBT) -> PSBT:
For application versions 2.1.x and above:

- Only keys derived with standard BIP 44, 49, 84, and 86 derivation paths are supported for single signature addresses.

BIP388: for basic descriptors this is optional, but if provided name must be empty
"""
master_fp = self.get_master_fingerprint()

Expand All @@ -206,20 +213,20 @@ def legacy_sign_tx() -> PSBT:
if not isinstance(client, LegacyClient):
client = LegacyClient(self.transport_client, self.chain)
wallet = WalletPolicy("", "wpkh(@0/**)", [""])
legacy_input_sigs = client.sign_psbt(tx, wallet, None)
legacy_input_sigs = client.sign_psbt(psbt, wallet, None)

for idx, pubkey, sig in legacy_input_sigs:
psbt_in = tx.inputs[idx]
psbt_in = psbt.inputs[idx]
psbt_in.partial_sigs[pubkey] = sig
return tx
return psbt

if isinstance(self.client, LegacyClient):
return legacy_sign_tx()

# Make a deepcopy of this psbt. We will need to modify it to get signing to work,
# which will affect the caller's detection for whether signing occured.
psbt2 = copy.deepcopy(tx)
if tx.version != 2:
psbt2 = copy.deepcopy(psbt)
if psbt.version != 2:
psbt2.convert_to_v2()

# Figure out which wallets are signing
Expand Down Expand Up @@ -264,6 +271,25 @@ def legacy_sign_tx() -> PSBT:
else:
continue

if bip388_policy is not None:
policy = WalletPolicy(
name=bip388_policy.name,
descriptor_template=bip388_policy.descriptor_template,
keys_info=bip388_policy.keys_info
)
if policy.id not in wallets:
if bip388_policy.hmac is None:
raise BadArgumentError("Missing --hmac")
wallets[policy.id] = (
signing_priority[script_addrtype],
script_addrtype,
policy,
bytes.fromhex(bip388_policy.hmac),
)
continue

# No BIP388 policy provided, construct on the fly

# Check if P2WSH
if is_p2wsh(scriptcode):
if len(psbt_in.witness_script) == 0:
Expand Down Expand Up @@ -369,13 +395,13 @@ def process_origin(origin: KeyOriginInfo) -> None:
psbt_in.partial_sigs[pubkey] = sig

# Extract the sigs from psbt2 and put them into tx
for sig_in, psbt_in in zip(psbt2.inputs, tx.inputs):
for sig_in, psbt_in in zip(psbt2.inputs, psbt.inputs):
psbt_in.partial_sigs.update(sig_in.partial_sigs)
psbt_in.tap_script_sigs.update(sig_in.tap_script_sigs)
if len(sig_in.tap_key_sig) != 0 and len(psbt_in.tap_key_sig) == 0:
psbt_in.tap_key_sig = sig_in.tap_key_sig

return tx
return psbt

@ledger_exception
def sign_message(self, message: Union[str, bytes], keypath: str) -> str:
Expand Down
Loading