From b1aed98e64a34575d51def318a0be02678ae5991 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 30 Jul 2025 18:29:38 +0200 Subject: [PATCH 1/2] Introduce BIP388Policy dataclass --- hwilib/common.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/hwilib/common.py b/hwilib/common.py index 0c5c00606..862b155a6 100644 --- a/hwilib/common.py +++ b/hwilib/common.py @@ -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): """ @@ -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: """ From 011fe8b5a9cdb1e49404f356274facea602cdac4 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 30 Jul 2025 21:03:17 +0200 Subject: [PATCH 2/2] Add register command for BIP388 policies --- hwilib/_cli.py | 12 ++++++++++++ hwilib/commands.py | 18 ++++++++++++++++++ hwilib/devices/ledger.py | 19 +++++++++++++++++++ hwilib/hwwclient.py | 17 ++++++++++++++++- 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/hwilib/_cli.py b/hwilib/_cli.py index e0afa7dd2..b7b439f17 100644 --- a/hwilib/_cli.py +++ b/hwilib/_cli.py @@ -12,6 +12,7 @@ getdescriptors, prompt_pin, toggle_passphrase, + register, restore_device, send_pin, setup_device, @@ -22,6 +23,7 @@ ) from .common import ( AddressType, + BIP388Policy, Chain, ) from .errors import ( @@ -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) @@ -197,6 +203,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='') diff --git a/hwilib/commands.py b/hwilib/commands.py index 6d192aa5f..5416f72f0 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -52,6 +52,7 @@ from .devices import __all__ as all_devs from .common import ( AddressType, + BIP388Policy, Chain, ) from .hwwclient import HardwareWalletClient @@ -494,6 +495,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": }``. + :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. diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index d3cf46325..fd5b8d640 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -31,6 +31,7 @@ ) from ..common import ( AddressType, + BIP388Policy, Chain, ) from .ledger_bitcoin.client import ( @@ -479,6 +480,24 @@ def format_key_info(pubkey: PubkeyProvider) -> str: return self.client.get_wallet_address(multisig_wallet, registered_hmac, change, address_index, True) + @ledger_exception + def register_bip388_policy( + self, + bip388_policy: BIP388Policy, + ) -> str: + if isinstance(self.client, LegacyClient): + raise BadArgumentError("Registering a BIP388 policy not supported by this version of the Bitcoin App") + + wallet_policy = WalletPolicy( + name=bip388_policy.name, + descriptor_template=bip388_policy.descriptor_template, + keys_info=bip388_policy.keys_info + ) + + _, registered_hmac = self.client.register_wallet(wallet_policy) + + return registered_hmac.hex() + def setup_device(self, label: str = "", passphrase: str = "") -> bool: """ Ledgers do not support setup via software. diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index 565afcf44..72b4e75ca 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -17,7 +17,11 @@ get_bip44_chain, ) from .psbt import PSBT -from .common import AddressType, Chain +from .common import ( + AddressType, + BIP388Policy, + Chain, +) class HardwareWalletClient(object): @@ -135,6 +139,17 @@ def display_multisig_address( raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") + def register_bip388_policy( + self, + bip388_policy: BIP388Policy, + ) -> str: + """ + Register a BIP388 policy. + + :return: The policy HMAC + """ + raise NotImplementedError("This device does not support BIP388 policies or it's not yet implemented") + def wipe_device(self) -> bool: """ Wipe the device.