Skip to content

Commit 374948b

Browse files
committed
ledger: have sign_psbt return SignPsbtYieldedObject
Taken from LedgerHQ/app-bitcoin-new at 2.4.1
1 parent 830f8b5 commit 374948b

File tree

4 files changed

+73
-27
lines changed

4 files changed

+73
-27
lines changed

hwilib/devices/ledger.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,9 @@ def legacy_sign_tx() -> PSBT:
215215
wallet = WalletPolicy("", "wpkh(@0/**)", [""])
216216
legacy_input_sigs = client.sign_psbt(psbt, wallet, None)
217217

218-
for idx, pubkey, sig in legacy_input_sigs:
218+
for idx, partial_sig in legacy_input_sigs:
219219
psbt_in = psbt.inputs[idx]
220-
psbt_in.partial_sigs[pubkey] = sig
220+
psbt_in.partial_sigs[partial_sig.pubkey] = partial_sig.signature
221221
return psbt
222222

223223
if isinstance(self.client, LegacyClient):
@@ -374,7 +374,7 @@ def process_origin(origin: KeyOriginInfo) -> None:
374374

375375
input_sigs = self.client.sign_psbt(psbt2, wallet, wallet_hmac)
376376

377-
for idx, pubkey, sig in input_sigs:
377+
for idx, yielded in input_sigs:
378378
psbt_in = psbt2.inputs[idx]
379379

380380
utxo = None
@@ -390,9 +390,9 @@ def process_origin(origin: KeyOriginInfo) -> None:
390390
if is_wit and wit_ver >= 1:
391391
# TODO: Deal with script path signatures
392392
# For now, assume key path signature
393-
psbt_in.tap_key_sig = sig
393+
psbt_in.tap_key_sig = yielded.signature
394394
else:
395-
psbt_in.partial_sigs[pubkey] = sig
395+
psbt_in.partial_sigs[yielded.pubkey] = yielded.signature
396396

397397
# Extract the sigs from psbt2 and put them into tx
398398
for sig_in, psbt_in in zip(psbt2.inputs, psbt.inputs):

hwilib/devices/ledger_bitcoin/client.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from .command_builder import BitcoinCommandBuilder, BitcoinInsType
66
from ...common import Chain
77
from .client_command import ClientCommandInterpreter
8-
from .client_base import Client, TransportClient
8+
from .client_base import Client, PartialSignature, SignPsbtYieldedObject, TransportClient
99
from .client_legacy import LegacyClient
10+
from .errors import UnknownDeviceError
1011
from .exception import DeviceException, NotSupportedError
1112
from .merkle import get_merkleized_map_commitment
1213
from .wallet import WalletPolicy, WalletType
@@ -31,6 +32,37 @@ def parse_stream_to_map(f: BufferedReader) -> Mapping[bytes, bytes]:
3132
result[key] = value
3233
return result
3334

35+
def _make_partial_signature(pubkey_augm: bytes, signature: bytes) -> PartialSignature:
36+
if len(pubkey_augm) == 64:
37+
# tapscript spend: pubkey_augm is the concatenation of:
38+
# - a 32-byte x-only pubkey
39+
# - the 32-byte tapleaf_hash
40+
return PartialSignature(signature=signature, pubkey=pubkey_augm[0:32], tapleaf_hash=pubkey_augm[32:])
41+
42+
else:
43+
# either legacy, segwit or taproot keypath spend
44+
# pubkey must be 32 (taproot x-only pubkey) or 33 bytes (compressed pubkey)
45+
46+
if len(pubkey_augm) not in [32, 33]:
47+
raise UnknownDeviceError(f"Invalid pubkey length returned: {len(pubkey_augm)}")
48+
49+
return PartialSignature(signature=signature, pubkey=pubkey_augm)
50+
51+
def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObject]:
52+
res_buffer = BytesIO(res)
53+
input_index_or_tag = read_varint(res_buffer)
54+
55+
# values follow an encoding without an explicit tag, where the
56+
# first element is the input index. All the signature types are implemented
57+
# by the PartialSignature type (not to be confused with the musig Partial Signature).
58+
input_index = input_index_or_tag
59+
60+
pubkey_augm_len = read_uint(res_buffer, 8)
61+
pubkey_augm = res_buffer.read(pubkey_augm_len)
62+
63+
signature = res_buffer.read()
64+
65+
return((input_index, _make_partial_signature(pubkey_augm, signature)))
3466

3567
def read_uint(buf: BytesIO,
3668
bit_len: int,
@@ -156,7 +188,7 @@ def get_wallet_address(
156188

157189
return response.decode()
158190

159-
def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, bytes, bytes]]:
191+
def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, SignPsbtYieldedObject]]:
160192
"""Signs a PSBT using a registered wallet (or a standard wallet that does not need registration).
161193
162194
Signature requires explicit approval from the user.
@@ -240,17 +272,10 @@ def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[byte
240272
if any(len(x) <= 1 for x in results):
241273
raise RuntimeError("Invalid response")
242274

243-
results_list: List[Tuple[int, bytes, bytes]] = []
275+
results_list: List[Tuple[int, SignPsbtYieldedObject]] = []
244276
for res in results:
245-
res_buffer = BytesIO(res)
246-
input_index = read_varint(res_buffer)
247-
248-
pubkey_len = read_uint(res_buffer, 8)
249-
pubkey = res_buffer.read(pubkey_len)
250-
251-
signature = res_buffer.read()
252-
253-
results_list.append((input_index, pubkey, signature))
277+
input_index, obj = _decode_signpsbt_yielded_value(res)
278+
results_list.append((input_index, obj))
254279

255280
return results_list
256281

hwilib/devices/ledger_bitcoin/client_base.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from dataclasses import dataclass
2+
13
from typing import Tuple, Optional, Union, List
24
from io import BytesIO
35

@@ -45,6 +47,25 @@ def apdu_exchange_nowait(
4547
def stop(self) -> None:
4648
self.transport.close()
4749

50+
@dataclass(frozen=True)
51+
class PartialSignature:
52+
"""Represents a partial signature returned by sign_psbt. Such objects can be added to the PSBT.
53+
54+
It always contains a pubkey and a signature.
55+
The pubkey is a compressed 33-byte for legacy and segwit Scripts, or 32-byte x-only key for taproot.
56+
The signature is in the format it would be pushed on the scriptSig or the witness stack, therefore of
57+
variable length, and possibly concatenated with the SIGHASH flag byte if appropriate.
58+
59+
The tapleaf_hash is also filled if signing for a tapscript.
60+
61+
Note: not to be confused with 'partial signature' of protocols like MuSig2;
62+
"""
63+
pubkey: bytes
64+
signature: bytes
65+
tapleaf_hash: Optional[bytes] = None
66+
67+
68+
SignPsbtYieldedObject = Union[PartialSignature]
4869

4970
class Client:
5071
def __init__(self, transport_client: TransportClient, chain: Chain = Chain.MAIN) -> None:
@@ -183,18 +204,19 @@ def get_wallet_address(
183204

184205
raise NotImplementedError
185206

186-
def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, bytes, bytes]]:
207+
def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, SignPsbtYieldedObject]]:
187208
"""Signs a PSBT using a registered wallet (or a standard wallet that does not need registration).
188209
189210
Signature requires explicit approval from the user.
190211
191212
Parameters
192213
----------
193-
psbt : PSBT
214+
psbt : PSBT | bytes | str
194215
A PSBT of version 0 or 2, with all the necessary information to sign the inputs already filled in; what the
195216
required fields changes depending on the type of input.
196217
The non-witness UTXO must be present for both legacy and SegWit inputs, or the hardware wallet will reject
197218
signing (this will change for Taproot inputs).
219+
The argument can be either a `PSBT` object, or `bytes`, or a base64-encoded `str`.
198220
199221
wallet : WalletPolicy
200222
The registered wallet policy, or a standard wallet policy.
@@ -204,11 +226,10 @@ def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[byte
204226
205227
Returns
206228
-------
207-
List[Tuple[int, bytes, bytes]]
229+
List[Tuple[int, PartialSignature]]
208230
A list of tuples returned by the hardware wallets, where each element is a tuple of:
209231
- an integer, the index of the input being signed;
210-
- a `bytes` array of length 33 (compressed ecdsa pubkey) or 32 (x-only BIP-0340 pubkey), the corresponding pubkey for this signature;
211-
- a `bytes` array with the signature.
232+
- an instance of `PartialSignature`.
212233
"""
213234

214235
raise NotImplementedError

hwilib/devices/ledger_bitcoin/client_legacy.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import re
1111
import base64
1212

13-
from .client import Client, TransportClient
13+
from .client import Client, PartialSignature, SignPsbtYieldedObject, TransportClient
1414

1515
from typing import List, Tuple, Optional, Union
1616

@@ -137,7 +137,7 @@ def get_wallet_address(
137137
return output['address'][12:-2] # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'<address>')". This extracts the actual address to work around this.
138138

139139
# NOTE: This is different from the new API, but we need it for multisig support.
140-
def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, bytes, bytes]]:
140+
def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, SignPsbtYieldedObject]]:
141141
if wallet_hmac is not None or wallet.n_keys != 1:
142142
raise NotImplementedError("Policy wallets are only supported from version 2.0.0. Please update your Ledger hardware wallet")
143143

@@ -259,7 +259,7 @@ def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[byte
259259

260260
all_signature_attempts[i_num] = signature_attempts
261261

262-
result: List[int, bytes, bytes] = []
262+
result: List[int, SignPsbtYieldedObject] = []
263263

264264
# Sign any segwit inputs
265265
if has_segwit:
@@ -276,7 +276,7 @@ def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[byte
276276
for signature_attempt in all_signature_attempts[i]:
277277
self.app.startUntrustedTransaction(False, 0, [segwit_inputs[i]], script_codes[i], c_tx.nVersion)
278278

279-
result.append((i, signature_attempt[1], self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01)))
279+
result.append((i, PartialSignature(pubkey=signature_attempt[1], signature=self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01))))
280280

281281
elif has_legacy:
282282
first_input = True
@@ -287,7 +287,7 @@ def sign_psbt(self, psbt: PSBT, wallet: WalletPolicy, wallet_hmac: Optional[byte
287287
self.app.startUntrustedTransaction(first_input, i, legacy_inputs, script_codes[i], c_tx.nVersion)
288288
self.app.finalizeInput(b"DUMMY", -1, -1, change_path, tx_bytes)
289289

290-
result.append((i, signature_attempt[1], self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01)))
290+
result.append((i, PartialSignature(pubkey=signature_attempt[1], signature=self.app.untrustedHashSign(signature_attempt[0], "", c_tx.nLockTime, 0x01))))
291291

292292
first_input = False
293293

0 commit comments

Comments
 (0)