2022-10-18 15:29:03 +02:00
# Some parts of this code are adapted from bitcoin-core/HWI:
# https://github.com/bitcoin-core/HWI/blob/e731395bde13362950e9f13e01689c475545e4dc/hwilib/devices/ledger.py
from abc import ABC , abstractmethod
import base64
2016-01-30 18:00:51 +09:00
import hashlib
2022-10-18 15:29:03 +02:00
from typing import Dict , List , Optional , Sequence , Tuple
2014-08-24 19:44:26 +02:00
2022-10-18 15:29:03 +02:00
from electrum import bip32 , constants , ecc
2023-02-26 13:28:31 +00:00
from electrum import descriptor
2022-10-18 15:29:03 +02:00
from electrum . base_wizard import ScriptTypeNotSupported
2019-10-23 17:09:41 +02:00
from electrum . bip32 import BIP32Node , convert_bip32_intpath_to_strpath
2022-10-18 15:29:03 +02:00
from electrum . bitcoin import EncodeBase58Check , int_to_hex , is_b58_address , is_segwit_script_type , var_int
from electrum . crypto import hash_160
2014-08-24 19:44:26 +02:00
from electrum . i18n import _
2022-11-09 10:59:12 +01:00
from electrum . keystore import Hardware_KeyStore
2019-04-26 18:52:26 +02:00
from electrum . logging import get_logger
2022-10-18 15:29:03 +02:00
from electrum . plugin import Device , runs_in_hwd_thread
2023-02-26 13:28:31 +00:00
from electrum . transaction import PartialTransaction , Transaction , PartialTxInput
2022-10-18 15:29:03 +02:00
from electrum . util import bfh , UserFacingException , versiontuple
from electrum . wallet import Standard_Wallet
2018-10-25 22:20:33 +02:00
2022-10-18 15:29:03 +02:00
from . . hw_wallet import HardwareClientBase , HW_PluginBase
2022-11-07 11:30:41 +01:00
from . . hw_wallet . plugin import is_any_tx_output_on_change_branch , validate_op_return_output , LibraryFoundButUnusable
2019-04-26 18:52:26 +02:00
_logger = get_logger ( __name__ )
2022-11-07 11:30:41 +01:00
2014-08-24 19:44:26 +02:00
try :
2022-11-07 11:30:41 +01:00
import ledger_bitcoin
from ledger_bitcoin import WalletPolicy , MultisigWallet , AddressType , Chain
from ledger_bitcoin . exception . errors import DenyError , NotSupportedError , SecurityStatusNotSatisfiedError
from ledger_bitcoin . key import KeyOriginInfo
from ledgercomm . interfaces . hid_device import HID
# legacy imports
2022-11-09 20:42:55 +00:00
# note: we could replace "btchip" with "ledger_bitcoin.btchip" but the latter does not support HW.1
2016-08-28 16:38:30 +02:00
import hid
2022-11-09 20:42:55 +00:00
from btchip . btchipComm import HIDDongleHIDAPI
from btchip . btchip import btchip
from btchip . btchipUtils import compress_public_key
from btchip . bitcoinTransaction import bitcoinTransaction
from btchip . btchipException import BTChipException
2022-11-07 11:30:41 +01:00
LEDGER_BITCOIN = True
2021-01-11 00:05:23 +01:00
except ImportError as e :
2022-11-07 11:30:41 +01:00
if not ( isinstance ( e , ModuleNotFoundError ) and e . name == ' ledger_bitcoin ' ) :
2021-01-11 00:05:23 +01:00
_logger . exception ( ' error importing ledger plugin deps ' )
2022-11-07 11:30:41 +01:00
LEDGER_BITCOIN = False
2014-08-24 19:44:26 +02:00
2017-12-16 19:46:45 +01:00
MSG_NEEDS_FW_UPDATE_GENERIC = _ ( ' Firmware version too old. Please update at ' ) + \
2022-11-07 14:29:02 +01:00
' https://www.ledger.com '
2017-12-16 19:46:45 +01:00
MSG_NEEDS_FW_UPDATE_SEGWIT = _ ( ' Firmware version (or " Bitcoin " app) too old for Segwit support. Please update at ' ) + \
2022-11-07 14:29:02 +01:00
' https://www.ledger.com '
2018-01-08 23:41:20 -05:00
MULTI_OUTPUT_SUPPORT = ' 1.1.4 '
SEGWIT_SUPPORT = ' 1.1.10 '
SEGWIT_SUPPORT_SPECIAL = ' 1.0.4 '
2020-06-27 18:26:54 +02:00
SEGWIT_TRUSTEDINPUTS = ' 1.4.0 '
2017-12-16 19:46:45 +01:00
2022-10-18 15:29:03 +02:00
def is_policy_standard ( wp : ' WalletPolicy ' , fpr : bytes , exp_coin_type : int ) - > bool :
""" Returns True if the wallet policy can be used without registration. """
if wp . name != " " or wp . n_keys != 1 :
return False
key_info = wp . keys_info [ 0 ]
if key_info [ 0 ] != ' [ ' :
# no key origin info
return False
try :
key_orig_end = key_info . index ( ' ] ' )
except ValueError :
# invalid key_info
return False
key_fpr , key_path = key_info [ 1 : key_orig_end ] . split ( ' / ' , maxsplit = 1 )
if key_fpr != fpr . hex ( ) :
# not an internal key
return False
key_path_parts = key_path . split ( ' / ' )
# Account key should be exactly 3 hardened derivation steps
if len ( key_path_parts ) != 3 or any ( part [ - 1 ] != " ' " for part in key_path_parts ) :
return False
purpose , coin_type , account_index = key_path_parts
if coin_type != f " { exp_coin_type } ' " or int ( account_index [ : - 1 ] ) > 100 :
return False
if wp . descriptor_template == " pkh(@0/**) " :
# BIP-44
return purpose == " 44 ' "
elif wp . descriptor_template == " sh(wpkh(@0/**)) " :
# BIP-49, nested SegWit
return purpose == " 49 ' "
elif wp . descriptor_template == " wpkh(@0/**) " :
# BIP-84, native SegWit
return purpose == " 84 ' "
elif wp . descriptor_template == " tr(@0/**) " :
# BIP-86, taproot single key
return purpose == " 86 ' "
else :
# unknown
return False
def convert_xpub ( xpub : str , xtype = ' standard ' ) - > str :
bip32node = BIP32Node . from_xkey ( xpub )
return BIP32Node (
xtype = xtype ,
eckey = bip32node . eckey ,
chaincode = bip32node . chaincode ,
depth = bip32node . depth ,
fingerprint = bip32node . fingerprint ,
child_number = bip32node . child_number ) . to_xpub ( )
2018-04-22 02:15:08 +02:00
def test_pin_unlocked ( func ) :
""" Function decorator to test the Ledger for being unlocked, and if not,
raise a human - readable exception .
"""
def catch_exception ( self , * args , * * kwargs ) :
try :
return func ( self , * args , * * kwargs )
2022-10-18 15:29:03 +02:00
except SecurityStatusNotSatisfiedError :
raise UserFacingException ( _ ( ' Your Ledger is locked. Please unlock it. ' ) )
2018-04-22 02:15:08 +02:00
return catch_exception
2022-10-18 15:29:03 +02:00
# from HWI
def is_witness ( script : bytes ) - > Tuple [ bool , int , bytes ] :
"""
Determine whether a script is a segwit output script .
If so , also returns the witness version and witness program .
: param script : The script
: returns : A tuple of a bool indicating whether the script is a segwit output script ,
an int representing the witness version ,
and the bytes of the witness program .
"""
if len ( script ) < 4 or len ( script ) > 42 :
return ( False , 0 , b " " )
if script [ 0 ] != 0 and ( script [ 0 ] < 81 or script [ 0 ] > 96 ) :
return ( False , 0 , b " " )
if script [ 1 ] + 2 == len ( script ) :
return ( True , script [ 0 ] - 0x50 if script [ 0 ] else 0 , script [ 2 : ] )
return ( False , 0 , b " " )
# from HWI
# Only handles up to 15 of 15. Returns None if this script is not a
# multisig script. Returns (m, pubkeys) otherwise.
def parse_multisig ( script : bytes ) - > Optional [ Tuple [ int , Sequence [ bytes ] ] ] :
"""
Determine whether a script is a multisig script . If so , determine the parameters of that multisig .
: param script : The script
: returns : ` ` None ` ` if the script is not multisig .
If multisig , returns a tuple of the number of signers required ,
and a sequence of public key bytes .
"""
# Get m
m = script [ 0 ] - 80
if m < 1 or m > 15 :
return None
# Get pubkeys
pubkeys = [ ]
offset = 1
while True :
pubkey_len = script [ offset ]
if pubkey_len != 33 :
break
offset + = 1
pubkeys . append ( script [ offset : offset + 33 ] )
offset + = 33
# Check things at the end
n = script [ offset ] - 80
if n != len ( pubkeys ) :
return None
offset + = 1
op_cms = script [ offset ]
if op_cms != 174 :
return None
return ( m , pubkeys )
HARDENED_FLAG = 1 << 31
def H_ ( x : int ) - > int :
"""
Shortcut function that " hardens " a number in a BIP44 path .
"""
return x | HARDENED_FLAG
def is_hardened ( i : int ) - > bool :
"""
Returns whether an index is hardened
"""
return i & HARDENED_FLAG != 0
def get_bip44_purpose ( addrtype : ' AddressType ' ) - > int :
"""
Determine the BIP 44 purpose based on the given : class : ` ~ hwilib . common . AddressType ` .
: param addrtype : The address type
"""
if addrtype == AddressType . LEGACY :
return 44
elif addrtype == AddressType . SH_WIT :
return 49
elif addrtype == AddressType . WIT :
return 84
elif addrtype == AddressType . TAP :
return 86
else :
raise ValueError ( " Unknown address type " )
def get_bip44_chain ( chain : ' Chain ' ) - > int :
"""
Determine the BIP 44 coin type based on the Bitcoin chain type .
For the Bitcoin mainnet chain , this returns 0. For the other chains , this returns 1.
: param chain : The chain
"""
if chain == Chain . MAIN :
return 0
else :
return 1
def get_addrtype_from_bip44_purpose ( index : int ) - > Optional [ ' AddressType ' ] :
purpose = index & ~ HARDENED_FLAG
if purpose == 44 :
return AddressType . LEGACY
elif purpose == 49 :
return AddressType . SH_WIT
elif purpose == 84 :
return AddressType . WIT
elif purpose == 86 :
return AddressType . TAP
else :
return None
def is_standard_path (
path : Sequence [ int ] ,
addrtype : ' AddressType ' ,
chain : ' Chain ' ,
) - > bool :
if len ( path ) != 5 :
return False
if not is_hardened ( path [ 0 ] ) or not is_hardened ( path [ 1 ] ) or not is_hardened ( path [ 2 ] ) :
return False
if is_hardened ( path [ 3 ] ) or is_hardened ( path [ 4 ] ) :
return False
computed_addrtype = get_addrtype_from_bip44_purpose ( path [ 0 ] )
if computed_addrtype is None :
return False
if computed_addrtype != addrtype :
return False
if path [ 1 ] != H_ ( get_bip44_chain ( chain ) ) :
return False
if path [ 3 ] not in [ 0 , 1 ] :
return False
return True
def get_chain ( ) - > ' Chain ' :
if constants . net . NET_NAME == " mainnet " :
return Chain . MAIN
elif constants . net . NET_NAME == " testnet " :
return Chain . TEST
elif constants . net . NET_NAME == " signet " :
return Chain . SIGNET
elif constants . net . NET_NAME == " regtest " :
return Chain . REGTEST
else :
raise ValueError ( " Unsupported network " )
class Ledger_Client ( HardwareClientBase , ABC ) :
is_legacy : bool
2022-11-09 20:42:55 +00:00
@staticmethod
def construct_new ( * args , device : Device , * * kwargs ) - > ' Ledger_Client ' :
""" The ' real ' constructor, that automatically decides which subclass to use. """
if LedgerPlugin . is_hw1 ( device . product_key ) :
return Ledger_Client_Legacy_HW1 ( * args , * * kwargs , device = device )
# for nano S or newer hw, decide which client impl to use based on software/firmware version:
hid_device = HID ( )
hid_device . path = device . path
hid_device . open ( )
transport = ledger_bitcoin . TransportClient ( ' hid ' , hid = hid_device )
2022-10-18 15:29:03 +02:00
cl = ledger_bitcoin . createClient ( transport , chain = get_chain ( ) )
if isinstance ( cl , ledger_bitcoin . client . NewClient ) :
2022-11-09 20:42:55 +00:00
return Ledger_Client_New ( hid_device , * args , * * kwargs )
2022-10-18 15:29:03 +02:00
else :
2022-11-09 20:42:55 +00:00
return Ledger_Client_Legacy ( hid_device , * args , * * kwargs )
2022-10-18 15:29:03 +02:00
2022-11-09 20:42:55 +00:00
def __init__ ( self , * , plugin : HW_PluginBase ) :
2020-04-17 19:25:18 +02:00
HardwareClientBase . __init__ ( self , plugin = plugin )
2022-10-18 15:29:03 +02:00
def get_master_fingerprint ( self ) - > bytes :
return self . request_root_fingerprint_from_device ( )
@abstractmethod
def show_address ( self , address_path : str , txin_type : str ) :
pass
@abstractmethod
def sign_transaction ( self , keystore : Hardware_KeyStore , tx : PartialTransaction , password : str ) :
pass
@abstractmethod
def sign_message (
self ,
address_path : str ,
message : str ,
password ,
* ,
script_type : Optional [ str ] = None ,
) - > bytes :
pass
class Ledger_Client_Legacy ( Ledger_Client ) :
""" Client based on the bitchip library, targeting versions 2.0.* and below. """
is_legacy = True
2022-11-09 20:42:55 +00:00
def __init__ ( self , hidDevice : ' HID ' , * , product_key : Tuple [ int , int ] ,
2022-10-18 15:29:03 +02:00
plugin : HW_PluginBase ) :
2022-11-09 20:42:55 +00:00
Ledger_Client . __init__ ( self , plugin = plugin )
2022-10-18 15:29:03 +02:00
# Hack, we close the old object and instantiate a new one
hidDevice . close ( )
dev = hid . device ( )
dev . open_path ( hidDevice . path )
dev . set_nonblocking ( True )
self . dongleObject = btchip ( HIDDongleHIDAPI ( dev , True , False ) )
self . signing = False
2020-04-08 17:25:18 +02:00
self . _product_key = product_key
2020-04-08 14:43:01 +02:00
self . _soft_device_id = None
2016-08-28 16:38:30 +02:00
def is_pairable ( self ) :
return True
2022-10-18 15:29:03 +02:00
def set_and_unset_signing ( func ) :
""" Function decorator to set and unset self.signing. """
def wrapper ( self , * args , * * kwargs ) :
try :
self . signing = True
return func ( self , * args , * * kwargs )
finally :
self . signing = False
return wrapper
def give_error ( self , message ) :
_logger . info ( message )
if not self . signing :
self . handler . show_error ( message )
else :
self . signing = False
raise UserFacingException ( message )
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2016-08-28 16:38:30 +02:00
def close ( self ) :
2020-09-08 15:52:53 +00:00
self . dongleObject . dongle . close ( )
2016-08-28 16:38:30 +02:00
def is_initialized ( self ) :
return True
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2020-04-08 14:43:01 +02:00
def get_soft_device_id ( self ) :
if self . _soft_device_id is None :
# modern ledger can provide xpub without user interaction
# (hw1 would prompt for PIN)
if not self . is_hw1 ( ) :
self . _soft_device_id = self . request_root_fingerprint_from_device ( )
return self . _soft_device_id
2019-12-17 21:10:14 +01:00
def is_hw1 ( self ) - > bool :
2022-11-09 20:42:55 +00:00
return LedgerPlugin . is_hw1 ( self . _product_key )
2020-04-08 17:25:18 +02:00
def device_model_name ( self ) :
2020-11-18 15:14:55 +01:00
return LedgerPlugin . device_name_from_product_key ( self . _product_key )
2019-12-17 21:10:14 +01:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2018-03-18 03:54:28 +01:00
def has_usable_connection_with_device ( self ) :
try :
self . dongleObject . getFirmwareVersion ( )
except BaseException :
return False
return True
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2018-01-12 04:03:43 +01:00
@test_pin_unlocked
2017-10-31 11:45:25 +01:00
def get_xpub ( self , bip32_path , xtype ) :
2016-08-28 16:38:30 +02:00
self . checkDevice ( )
# bip32_path is of the form 44'/0'/1'
# S-L-O-W - we don't handle the fingerprint directly, so compute
# it manually from the previous node
# This only happens once so it's bearable
2022-10-18 15:29:03 +02:00
# self.get_client() # prompt for the PIN before displaying the dialog if necessary
# self.handler.show_message("Computing master public key")
2017-11-03 10:32:16 +01:00
if xtype in [ ' p2wpkh ' , ' p2wsh ' ] and not self . supports_native_segwit ( ) :
2018-11-08 19:46:15 +01:00
raise UserFacingException ( MSG_NEEDS_FW_UPDATE_SEGWIT )
2017-11-03 10:32:16 +01:00
if xtype in [ ' p2wpkh-p2sh ' , ' p2wsh-p2sh ' ] and not self . supports_segwit ( ) :
2018-11-08 19:46:15 +01:00
raise UserFacingException ( MSG_NEEDS_FW_UPDATE_SEGWIT )
2019-11-05 21:34:54 +01:00
bip32_path = bip32 . normalize_bip32_derivation ( bip32_path )
2023-03-10 14:23:17 +00:00
bip32_intpath = bip32 . convert_bip32_strpath_to_intpath ( bip32_path )
2019-11-05 21:34:54 +01:00
bip32_path = bip32_path [ 2 : ] # cut off "m/"
if len ( bip32_intpath ) > = 1 :
prevPath = bip32 . convert_bip32_intpath_to_strpath ( bip32_intpath [ : - 1 ] ) [ 2 : ]
2017-11-03 10:32:16 +01:00
nodeData = self . dongleObject . getWalletPublicKey ( prevPath )
2018-01-12 04:03:43 +01:00
publicKey = compress_public_key ( nodeData [ ' publicKey ' ] )
2019-11-05 21:34:54 +01:00
fingerprint_bytes = hash_160 ( publicKey ) [ 0 : 4 ]
childnum_bytes = bip32_intpath [ - 1 ] . to_bytes ( length = 4 , byteorder = " big " )
else :
fingerprint_bytes = bytes ( 4 )
childnum_bytes = bytes ( 4 )
2017-11-03 10:32:16 +01:00
nodeData = self . dongleObject . getWalletPublicKey ( bip32_path )
publicKey = compress_public_key ( nodeData [ ' publicKey ' ] )
2019-11-05 21:34:54 +01:00
depth = len ( bip32_intpath )
2019-02-21 22:17:06 +01:00
return BIP32Node ( xtype = xtype ,
ecc.ECPubkey: also accept bytearray in __init__
regression since #5947
Traceback (most recent call last):
File "...\electrum\electrum\base_wizard.py", line 339, in on_device
self.plugin.setup_device(device_info, self, purpose)
File "...\electrum\electrum\plugins\ledger\ledger.py", line 598, in setup_device
client.get_xpub("m/44'/0'", 'standard') # TODO replace by direct derivation once Nano S > 1.1
File "...\electrum\electrum\plugins\ledger\ledger.py", line 55, in catch_exception
return func(self, *args, **kwargs)
File "...\electrum\electrum\plugins\ledger\ledger.py", line 124, in get_xpub
eckey=ecc.ECPubkey(publicKey),
File "...\electrum\electrum\ecc.py", line 145, in __init__
self._x, self._y = _x_and_y_from_pubkey_bytes(b)
File "...\electrum\electrum\ecc.py", line 119, in _x_and_y_from_pubkey_bytes
ret = _libsecp256k1.secp256k1_ec_pubkey_parse(
ctypes.ArgumentError: argument 3: <class 'TypeError'>: wrong type
2020-02-19 00:40:33 +01:00
eckey = ecc . ECPubkey ( bytes ( publicKey ) ) ,
2019-02-21 22:17:06 +01:00
chaincode = nodeData [ ' chainCode ' ] ,
depth = depth ,
2019-11-05 21:34:54 +01:00
fingerprint = fingerprint_bytes ,
child_number = childnum_bytes ) . to_xpub ( )
2016-08-28 16:38:30 +02:00
2022-06-03 15:29:25 +02:00
def has_detached_pin_support ( self , client : ' btchip ' ) :
2016-08-28 16:38:30 +02:00
try :
client . getVerifyPinRemainingAttempts ( )
return True
2017-02-16 10:54:24 +01:00
except BTChipException as e :
2016-08-28 16:38:30 +02:00
if e . sw == 0x6d00 :
return False
raise e
2022-06-03 15:29:25 +02:00
def is_pin_validated ( self , client : ' btchip ' ) :
2016-08-28 16:38:30 +02:00
try :
# Invalid SET OPERATION MODE to verify the PIN status
client . dongle . exchange ( bytearray ( [ 0xe0 , 0x26 , 0x00 , 0x00 , 0x01 , 0xAB ] ) )
2017-02-16 10:54:24 +01:00
except BTChipException as e :
2016-08-28 16:38:30 +02:00
if ( e . sw == 0x6982 ) :
return False
if ( e . sw == 0x6A80 ) :
return True
raise e
2017-01-25 01:34:35 +01:00
def supports_multi_output ( self ) :
return self . multiOutputSupported
2017-09-17 18:34:38 +02:00
def supports_segwit ( self ) :
return self . segwitSupported
def supports_native_segwit ( self ) :
return self . nativeSegwitSupported
2020-06-27 18:26:54 +02:00
def supports_segwit_trustedInputs ( self ) :
return self . segwitTrustedInputs
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2016-08-28 16:38:30 +02:00
def checkDevice ( self ) :
2022-11-09 10:59:12 +01:00
firmwareInfo = self . dongleObject . getFirmwareVersion ( )
firmware = firmwareInfo [ ' version ' ]
self . multiOutputSupported = versiontuple ( firmware ) > = versiontuple ( MULTI_OUTPUT_SUPPORT )
self . nativeSegwitSupported = versiontuple ( firmware ) > = versiontuple ( SEGWIT_SUPPORT )
self . segwitSupported = self . nativeSegwitSupported or ( firmwareInfo [ ' specialVersion ' ] == 0x20 and versiontuple ( firmware ) > = versiontuple ( SEGWIT_SUPPORT_SPECIAL ) )
self . segwitTrustedInputs = versiontuple ( firmware ) > = versiontuple ( SEGWIT_TRUSTEDINPUTS )
2016-08-28 16:38:30 +02:00
def password_dialog ( self , msg = None ) :
response = self . handler . get_word ( msg )
if response is None :
return False , None , None
return True , response , response
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2018-04-22 02:15:08 +02:00
@test_pin_unlocked
2018-03-14 15:18:26 +01:00
@set_and_unset_signing
2022-10-18 15:29:03 +02:00
def show_address ( self , address_path : str , txin_type : str ) :
self . handler . show_message ( _ ( " Showing address ... " ) )
segwit = is_segwit_script_type ( txin_type )
segwitNative = txin_type == ' p2wpkh '
2014-08-24 19:44:26 +02:00
try :
2022-10-18 15:29:03 +02:00
self . dongleObject . getWalletPublicKey ( address_path , showOnScreen = True , segwit = segwit , segwitNative = segwitNative )
2017-02-16 10:54:24 +01:00
except BTChipException as e :
2022-10-18 15:29:03 +02:00
if e . sw == 0x6985 : # cancelled by user
pass
2018-04-22 02:15:08 +02:00
elif e . sw == 0x6982 :
raise # pin lock. decorator will catch it
2022-10-18 15:29:03 +02:00
elif e . sw == 0x6b00 : # hw.1 raises this
self . handler . show_error ( ' {} \n {} \n {} ' . format (
_ ( ' Error showing address ' ) + ' : ' ,
e ,
_ ( ' Your device might not have support for this functionality. ' ) ) )
2015-07-04 16:45:08 +09:00
else :
2022-10-18 15:29:03 +02:00
_logger . exception ( ' ' )
self . handler . show_error ( e )
except BaseException as e :
_logger . exception ( ' ' )
self . handler . show_error ( e )
2014-08-24 19:44:26 +02:00
finally :
2017-11-18 04:09:15 +01:00
self . handler . finished ( )
2017-11-05 17:45:55 +01:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2018-04-22 02:15:08 +02:00
@test_pin_unlocked
2018-03-14 15:18:26 +01:00
@set_and_unset_signing
2022-10-18 15:29:03 +02:00
def sign_transaction ( self , keystore : Hardware_KeyStore , tx : PartialTransaction , password : str ) :
2014-10-24 10:53:09 +02:00
if tx . is_complete ( ) :
2014-09-19 13:39:12 +02:00
return
2022-10-18 15:29:03 +02:00
2014-08-24 19:44:26 +02:00
inputs = [ ]
inputsPaths = [ ]
2016-08-28 16:38:30 +02:00
chipInputs = [ ]
2015-07-04 16:45:08 +09:00
redeemScripts = [ ]
changePath = " "
2014-08-24 19:44:26 +02:00
output = None
2016-08-28 16:38:30 +02:00
p2shTransaction = False
2017-04-13 11:58:42 +02:00
segwitTransaction = False
2014-08-27 23:19:14 +02:00
pin = " "
2022-06-03 17:07:36 +02:00
# prompt for the PIN before displaying the dialog if necessary
2016-09-30 14:56:53 +02:00
2023-02-26 13:28:31 +00:00
def is_txin_legacy_multisig ( txin : PartialTxInput ) - > bool :
desc = txin . script_descriptor
return ( isinstance ( desc , descriptor . SHDescriptor )
and isinstance ( desc . subdescriptors [ 0 ] , descriptor . MultisigDescriptor ) )
2014-08-24 19:44:26 +02:00
# Fetch inputs of the transaction to sign
2016-09-22 10:25:03 +02:00
for txin in tx . inputs ( ) :
2020-01-02 00:43:49 +01:00
if txin . is_coinbase_input ( ) :
2014-09-19 13:39:12 +02:00
self . give_error ( " Coinbase not supported " ) # should never happen
2016-09-22 10:25:03 +02:00
2023-02-26 13:28:31 +00:00
if is_txin_legacy_multisig ( txin ) :
2016-09-22 10:25:03 +02:00
p2shTransaction = True
2016-09-30 14:56:53 +02:00
2023-02-26 13:28:31 +00:00
if txin . is_p2sh_segwit ( ) :
2022-10-18 15:29:03 +02:00
if not self . supports_segwit ( ) :
2017-12-16 19:46:45 +01:00
self . give_error ( MSG_NEEDS_FW_UPDATE_SEGWIT )
2017-09-17 18:34:38 +02:00
segwitTransaction = True
2023-02-26 13:28:31 +00:00
if txin . is_native_segwit ( ) :
2022-10-18 15:29:03 +02:00
if not self . supports_native_segwit ( ) :
2017-12-16 19:46:45 +01:00
self . give_error ( MSG_NEEDS_FW_UPDATE_SEGWIT )
2017-04-12 19:15:43 +02:00
segwitTransaction = True
2022-10-18 15:29:03 +02:00
my_pubkey , full_path = keystore . find_my_pubkey_in_txinout ( txin )
2019-10-23 17:09:41 +02:00
if not full_path :
self . give_error ( " No matching pubkey for sign_transaction " ) # should never happen
2019-11-05 21:34:54 +01:00
full_path = convert_bip32_intpath_to_strpath ( full_path ) [ 2 : ]
2016-08-28 16:38:30 +02:00
2017-04-18 11:12:46 +02:00
redeemScript = Transaction . get_preimage_script ( txin )
2019-10-23 17:09:41 +02:00
txin_prev_tx = txin . utxo
2020-10-24 08:03:13 +02:00
if txin_prev_tx is None and not txin . is_segwit ( ) :
2019-10-23 17:09:41 +02:00
raise UserFacingException ( _ ( ' Missing previous tx for legacy input. ' ) )
txin_prev_tx_raw = txin_prev_tx . serialize ( ) if txin_prev_tx else None
2018-04-28 22:56:53 +02:00
inputs . append ( [ txin_prev_tx_raw ,
2019-10-23 17:09:41 +02:00
txin . prevout . out_idx ,
2018-04-28 22:56:53 +02:00
redeemScript ,
2019-10-23 17:09:41 +02:00
txin . prevout . txid . hex ( ) ,
my_pubkey ,
txin . nsequence ,
txin . value_sats ( ) ] )
inputsPaths . append ( full_path )
2016-09-22 10:25:03 +02:00
2016-08-28 16:38:30 +02:00
# Sanity check
if p2shTransaction :
2017-03-02 08:38:06 +01:00
for txin in tx . inputs ( ) :
2023-02-26 13:28:31 +00:00
if not is_txin_legacy_multisig ( txin ) :
2022-10-18 15:29:03 +02:00
self . give_error ( " P2SH / regular input mixed in same transaction not supported " ) # should never happen
2016-09-22 10:25:03 +02:00
txOutput = var_int ( len ( tx . outputs ( ) ) )
2018-10-02 15:52:24 +02:00
for o in tx . outputs ( ) :
2019-10-23 17:09:41 +02:00
txOutput + = int_to_hex ( o . value , 8 )
script = o . scriptpubkey . hex ( )
2022-10-18 15:29:03 +02:00
txOutput + = var_int ( len ( script ) / / 2 )
2016-09-22 10:25:03 +02:00
txOutput + = script
2017-08-13 12:00:33 +02:00
txOutput = bfh ( txOutput )
2014-08-24 19:44:26 +02:00
2022-10-18 15:29:03 +02:00
if not self . supports_multi_output ( ) :
2019-12-17 21:33:07 +01:00
if len ( tx . outputs ( ) ) > 2 :
self . give_error ( " Transaction with more than 2 outputs not supported " )
for txout in tx . outputs ( ) :
2022-10-18 15:29:03 +02:00
if self . is_hw1 ( ) and txout . address and not is_b58_address ( txout . address ) :
2022-11-09 20:11:08 +00:00
self . give_error ( _ ( " This {} device can only send to base58 addresses. " ) . format ( keystore . device ) )
2019-12-17 21:33:07 +01:00
if not txout . address :
2022-10-18 15:29:03 +02:00
if self . is_hw1 ( ) :
2022-11-09 20:11:08 +00:00
self . give_error ( _ ( " Only address outputs are supported by {} " ) . format ( keystore . device ) )
2019-12-17 21:33:07 +01:00
# note: max_size based on https://github.com/LedgerHQ/ledger-app-btc/commit/3a78dee9c0484821df58975803e40d58fbfc2c38#diff-c61ccd96a6d8b54d48f54a3bc4dfa7e2R26
validate_op_return_output ( txout , max_size = 190 )
# Output "change" detection
2018-06-15 20:48:57 +02:00
# - only one output and one change is authorized (for hw.1 and nano)
# - at most one output can bypass confirmation (~change) (for all)
2016-08-28 16:38:30 +02:00
if not p2shTransaction :
2018-06-15 20:48:57 +02:00
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch ( tx )
2019-10-23 17:09:41 +02:00
for txout in tx . outputs ( ) :
if txout . is_mine and len ( tx . outputs ( ) ) > 1 \
2018-06-15 20:48:57 +02:00
and not has_change :
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
2019-10-23 17:09:41 +02:00
if txout . is_change == any_output_on_change_branch :
2022-10-18 15:29:03 +02:00
my_pubkey , changePath = keystore . find_my_pubkey_in_txinout ( txout )
2019-10-23 17:09:41 +02:00
assert changePath
2019-11-05 21:34:54 +01:00
changePath = convert_bip32_intpath_to_strpath ( changePath ) [ 2 : ]
2018-06-15 20:48:57 +02:00
has_change = True
else :
2019-10-23 17:09:41 +02:00
output = txout . address
2016-08-28 16:38:30 +02:00
else :
2019-10-23 17:09:41 +02:00
output = txout . address
2016-09-22 10:25:03 +02:00
2014-08-24 19:44:26 +02:00
try :
# Get trusted inputs from the original transactions
2022-03-26 15:26:13 +01:00
for input_idx , utxo in enumerate ( inputs ) :
2022-10-18 15:29:03 +02:00
self . handler . show_message ( _ ( " Preparing transaction inputs... " ) + f " (phase1, { input_idx } / { len ( inputs ) } ) " )
2017-04-12 19:35:00 +02:00
sequence = int_to_hex ( utxo [ 5 ] , 4 )
2022-10-18 15:29:03 +02:00
if segwitTransaction and not self . supports_segwit_trustedInputs ( ) :
2017-08-13 12:00:33 +02:00
tmp = bfh ( utxo [ 3 ] ) [ : : - 1 ]
tmp + = bfh ( int_to_hex ( utxo [ 1 ] , 4 ) )
2018-04-28 22:56:53 +02:00
tmp + = bfh ( int_to_hex ( utxo [ 6 ] , 8 ) ) # txin['value']
2022-10-18 15:29:03 +02:00
chipInputs . append ( { ' value ' : tmp , ' witness ' : True , ' sequence ' : sequence } )
2017-08-13 12:00:33 +02:00
redeemScripts . append ( bfh ( utxo [ 2 ] ) )
2022-10-18 15:29:03 +02:00
elif ( not p2shTransaction ) or self . supports_multi_output ( ) :
2017-11-08 15:01:25 +01:00
txtmp = bitcoinTransaction ( bfh ( utxo [ 0 ] ) )
2022-10-18 15:29:03 +02:00
trustedInput = self . dongleObject . getTrustedInput ( txtmp , utxo [ 1 ] )
2017-11-08 15:01:25 +01:00
trustedInput [ ' sequence ' ] = sequence
2020-06-27 18:26:54 +02:00
if segwitTransaction :
trustedInput [ ' witness ' ] = True
2017-11-08 15:01:25 +01:00
chipInputs . append ( trustedInput )
2020-06-27 18:26:54 +02:00
if p2shTransaction or segwitTransaction :
2020-07-01 16:15:13 +02:00
redeemScripts . append ( bfh ( utxo [ 2 ] ) )
2020-06-27 18:26:54 +02:00
else :
redeemScripts . append ( txtmp . outputs [ utxo [ 1 ] ] . script )
2016-08-28 16:38:30 +02:00
else :
2017-08-13 12:00:33 +02:00
tmp = bfh ( utxo [ 3 ] ) [ : : - 1 ]
tmp + = bfh ( int_to_hex ( utxo [ 1 ] , 4 ) )
2022-10-18 15:29:03 +02:00
chipInputs . append ( { ' value ' : tmp , ' sequence ' : sequence } )
2017-08-13 12:00:33 +02:00
redeemScripts . append ( bfh ( utxo [ 2 ] ) )
2016-08-28 16:38:30 +02:00
2022-03-26 15:26:13 +01:00
self . handler . show_message ( _ ( " Confirm Transaction on your Ledger device... " ) )
2014-08-24 19:44:26 +02:00
# Sign all inputs
firstTransaction = True
inputIndex = 0
2018-06-15 20:21:29 +02:00
rawTx = tx . serialize_to_network ( )
2022-11-09 20:42:55 +00:00
if self . is_hw1 ( ) :
self . dongleObject . enableAlternate2fa ( False )
2017-04-12 19:15:43 +02:00
if segwitTransaction :
2022-10-18 15:29:03 +02:00
self . dongleObject . startUntrustedTransaction ( True , inputIndex , chipInputs , redeemScripts [ inputIndex ] , version = tx . version )
2018-10-02 15:52:24 +02:00
# we don't set meaningful outputAddress, amount and fees
# as we only care about the alternateEncoding==True branch
2022-10-18 15:29:03 +02:00
outputData = self . dongleObject . finalizeInput ( b ' ' , 0 , 0 , changePath , bfh ( rawTx ) )
2017-04-12 19:15:43 +02:00
outputData [ ' outputData ' ] = txOutput
2015-09-04 09:07:18 +09:00
if outputData [ ' confirmationNeeded ' ] :
2016-12-21 12:52:54 +07:00
outputData [ ' address ' ] = output
2017-11-18 04:09:15 +01:00
self . handler . finished ( )
2020-10-24 23:32:18 +02:00
# do the authenticate dialog and get pin:
2022-10-18 15:29:03 +02:00
pin = self . handler . get_auth ( outputData , client = self )
2016-12-21 12:52:54 +07:00
if not pin :
raise UserWarning ( )
2019-08-09 19:54:09 +02:00
self . handler . show_message ( _ ( " Confirmed. Signing Transaction... " ) )
2017-11-18 04:09:15 +01:00
while inputIndex < len ( inputs ) :
2022-10-18 15:29:03 +02:00
self . handler . show_message ( _ ( " Signing transaction... " ) + f " (phase2, { inputIndex } / { len ( inputs ) } ) " )
2021-03-21 00:34:25 -04:00
singleInput = [ chipInputs [ inputIndex ] ]
2022-10-18 15:29:03 +02:00
self . dongleObject . startUntrustedTransaction ( False , 0 ,
singleInput , redeemScripts [ inputIndex ] , version = tx . version )
inputSignature = self . dongleObject . untrustedHashSign ( inputsPaths [ inputIndex ] , pin , lockTime = tx . locktime )
inputSignature [ 0 ] = 0x30 # force for 1.4.9+
2019-10-23 17:09:41 +02:00
my_pubkey = inputs [ inputIndex ] [ 4 ]
tx . add_signature_to_txin ( txin_idx = inputIndex ,
signing_pubkey = my_pubkey . hex ( ) ,
sig = inputSignature . hex ( ) )
2014-08-24 19:44:26 +02:00
inputIndex = inputIndex + 1
2017-04-12 19:15:43 +02:00
else :
while inputIndex < len ( inputs ) :
2022-10-18 15:29:03 +02:00
self . handler . show_message ( _ ( " Signing transaction... " ) + f " (phase2, { inputIndex } / { len ( inputs ) } ) " )
self . dongleObject . startUntrustedTransaction ( firstTransaction , inputIndex , chipInputs , redeemScripts [ inputIndex ] , version = tx . version )
2018-10-02 15:52:24 +02:00
# we don't set meaningful outputAddress, amount and fees
# as we only care about the alternateEncoding==True branch
2022-10-18 15:29:03 +02:00
outputData = self . dongleObject . finalizeInput ( b ' ' , 0 , 0 , changePath , bfh ( rawTx ) )
2017-08-31 09:55:44 +02:00
outputData [ ' outputData ' ] = txOutput
2017-04-12 19:15:43 +02:00
if outputData [ ' confirmationNeeded ' ] :
outputData [ ' address ' ] = output
2017-11-18 04:09:15 +01:00
self . handler . finished ( )
2020-10-24 23:32:18 +02:00
# do the authenticate dialog and get pin:
2022-10-18 15:29:03 +02:00
pin = self . handler . get_auth ( outputData , client = self )
2017-04-12 19:15:43 +02:00
if not pin :
raise UserWarning ( )
2019-08-09 19:54:09 +02:00
self . handler . show_message ( _ ( " Confirmed. Signing Transaction... " ) )
2017-04-12 19:15:43 +02:00
else :
# Sign input with the provided PIN
2022-10-18 15:29:03 +02:00
inputSignature = self . dongleObject . untrustedHashSign ( inputsPaths [ inputIndex ] , pin , lockTime = tx . locktime )
inputSignature [ 0 ] = 0x30 # force for 1.4.9+
2019-10-23 17:09:41 +02:00
my_pubkey = inputs [ inputIndex ] [ 4 ]
tx . add_signature_to_txin ( txin_idx = inputIndex ,
signing_pubkey = my_pubkey . hex ( ) ,
sig = inputSignature . hex ( ) )
2017-04-12 19:15:43 +02:00
inputIndex = inputIndex + 1
2019-08-09 19:54:09 +02:00
firstTransaction = False
2016-12-21 12:52:54 +07:00
except UserWarning :
self . handler . show_error ( _ ( ' Cancelled by user ' ) )
return
2018-03-14 15:18:26 +01:00
except BTChipException as e :
2019-02-28 17:56:08 +01:00
if e . sw in ( 0x6985 , 0x6d00 ) : # cancelled by user
2018-03-14 15:18:26 +01:00
return
2018-04-22 02:15:08 +02:00
elif e . sw == 0x6982 :
raise # pin lock. decorator will catch it
2018-03-14 15:18:26 +01:00
else :
2022-10-18 15:29:03 +02:00
_logger . exception ( ' ' )
2022-05-11 19:30:14 +02:00
self . give_error ( e )
2016-09-22 10:25:03 +02:00
except BaseException as e :
2022-10-18 15:29:03 +02:00
_logger . exception ( ' ' )
2022-05-11 19:30:14 +02:00
self . give_error ( e )
2014-08-24 19:44:26 +02:00
finally :
2017-11-18 04:09:15 +01:00
self . handler . finished ( )
2014-08-24 19:44:26 +02:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2018-04-22 02:15:08 +02:00
@test_pin_unlocked
2018-03-14 15:18:26 +01:00
@set_and_unset_signing
2022-10-18 15:29:03 +02:00
def sign_message (
self ,
address_path : str ,
message : str ,
password ,
* ,
script_type : Optional [ str ] = None ,
) - > bytes :
message = message . encode ( ' utf8 ' )
message_hash = hashlib . sha256 ( message ) . hexdigest ( ) . upper ( )
self . handler . show_message ( " Signing message ... \r \n Message hash: " + message_hash )
2018-01-11 04:37:41 +11:00
try :
2022-10-18 15:29:03 +02:00
info = self . dongleObject . signMessagePrepare ( address_path , message )
pin = " "
if info [ ' confirmationNeeded ' ] :
# do the authenticate dialog and get pin:
pin = self . handler . get_auth ( info , client = self )
if not pin :
raise UserWarning ( _ ( ' Cancelled by user ' ) )
pin = str ( pin ) . encode ( )
signature = self . dongleObject . signMessageSign ( pin )
2018-01-10 18:39:25 +01:00
except BTChipException as e :
2022-10-18 15:29:03 +02:00
if e . sw == 0x6a80 :
self . give_error ( " Unfortunately, this message cannot be signed by the Ledger wallet. "
" Only alphanumerical messages shorter than 140 characters are supported. "
" Please remove any extra characters (tab, carriage return) and retry. " )
elif e . sw == 0x6985 : # cancelled by user
return b ' '
2018-04-22 02:15:08 +02:00
elif e . sw == 0x6982 :
raise # pin lock. decorator will catch it
2018-01-10 18:39:25 +01:00
else :
2022-10-18 15:29:03 +02:00
self . give_error ( e )
except UserWarning :
self . handler . show_error ( _ ( ' Cancelled by user ' ) )
return b ' '
except Exception as e :
self . give_error ( e )
finally :
self . handler . finished ( )
# Parse the ASN.1 signature
rLength = signature [ 3 ]
r = signature [ 4 : 4 + rLength ]
sLength = signature [ 4 + rLength + 1 ]
s = signature [ 4 + rLength + 2 : ]
if rLength == 33 :
r = r [ 1 : ]
if sLength == 33 :
s = s [ 1 : ]
# And convert it
# Pad r and s points with 0x00 bytes when the point is small to get valid signature.
r_padded = bytes ( [ 0x00 ] ) * ( 32 - len ( r ) ) + r
s_padded = bytes ( [ 0x00 ] ) * ( 32 - len ( s ) ) + s
return bytes ( [ 27 + 4 + ( signature [ 0 ] & 0x01 ) ] ) + r_padded + s_padded
2022-11-09 20:42:55 +00:00
class Ledger_Client_Legacy_HW1 ( Ledger_Client_Legacy ) :
""" Even " legacy-er " client for deprecated HW.1 support. """
MIN_SUPPORTED_HW1_FW_VERSION = " 1.0.2 "
def __init__ ( self , product_key : Tuple [ int , int ] ,
plugin : HW_PluginBase , device : ' Device ' ) :
# note: Ledger_Client_Legacy.__init__ is *not* called
Ledger_Client . __init__ ( self , plugin = plugin )
self . _product_key = product_key
assert self . is_hw1 ( )
ledger = device . product_key [ 1 ] in ( 0x3b7c , 0x4b7c )
dev = hid . device ( )
dev . open_path ( device . path )
dev . set_nonblocking ( True )
hid_device = HIDDongleHIDAPI ( dev , ledger , debug = False )
self . dongleObject = btchip ( hid_device )
self . _preflightDone = False
self . signing = False
self . _soft_device_id = None
@runs_in_hwd_thread
def checkDevice ( self ) :
super ( ) . checkDevice ( )
self . _perform_hw1_preflight ( )
def _perform_hw1_preflight ( self ) :
assert self . is_hw1 ( )
if self . _preflightDone :
return
try :
firmwareInfo = self . dongleObject . getFirmwareVersion ( )
firmware = firmwareInfo [ ' version ' ]
if versiontuple ( firmware ) < versiontuple ( self . MIN_SUPPORTED_HW1_FW_VERSION ) :
self . close ( )
raise UserFacingException (
_ ( " Unsupported device firmware (too old). " ) + f " \n Installed: { firmware } . Needed: >= { self . MIN_SUPPORTED_HW1_FW_VERSION } " )
try :
self . dongleObject . getOperationMode ( )
except BTChipException as e :
if ( e . sw == 0x6985 ) :
self . close ( )
self . handler . get_setup ( )
# Acquire the new client on the next run
else :
raise e
if self . has_detached_pin_support ( self . dongleObject ) and not self . is_pin_validated ( self . dongleObject ) :
assert self . handler , " no handler for client "
remaining_attempts = self . dongleObject . getVerifyPinRemainingAttempts ( )
if remaining_attempts != 1 :
msg = " Enter your Ledger PIN - remaining attempts : " + str ( remaining_attempts )
else :
msg = " Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped. "
confirmed , p , pin = self . password_dialog ( msg )
if not confirmed :
raise UserFacingException ( ' Aborted by user - please unplug the dongle and plug it again before retrying ' )
pin = pin . encode ( )
self . dongleObject . verifyPin ( pin )
except BTChipException as e :
if ( e . sw == 0x6faa ) :
raise UserFacingException ( " Dongle is temporarily locked - please unplug it and replug it again " )
if ( ( e . sw & 0xFFF0 ) == 0x63c0 ) :
raise UserFacingException ( " Invalid PIN - please unplug the dongle and plug it again before retrying " )
if e . sw == 0x6f00 and e . message == ' Invalid channel ' :
# based on docs 0x6f00 might be a more general error, hence we also compare message to be sure
raise UserFacingException ( " Invalid channel. \n "
" Please make sure that ' Browser support ' is disabled on your device. " )
if e . sw == 0x6d00 or e . sw == 0x6700 :
raise UserFacingException ( _ ( " Device not in Bitcoin mode " ) ) from e
raise e
else :
deprecation_warning = (
" This Ledger device (HW.1) is being deprecated. \n \n It is no longer supported by Ledger. \n "
" Future versions of Electrum will no longer be compatible with it. \n \n "
" You should move your coins and migrate to a modern hardware device. " )
_logger . warning ( deprecation_warning . replace ( " \n " , " " ) )
if self . handler :
self . handler . show_message ( deprecation_warning )
self . _preflightDone = True
2022-10-18 15:29:03 +02:00
class Ledger_Client_New ( Ledger_Client ) :
""" Client based on the ledger_bitcoin library, targeting versions 2.1.* and above. """
is_legacy = False
2022-11-09 20:42:55 +00:00
def __init__ ( self , hidDevice : ' HID ' , * , product_key : Tuple [ int , int ] ,
2022-10-18 15:29:03 +02:00
plugin : HW_PluginBase ) :
2022-11-09 20:42:55 +00:00
Ledger_Client . __init__ ( self , plugin = plugin )
2022-10-18 15:29:03 +02:00
transport = ledger_bitcoin . TransportClient ( ' hid ' , hid = hidDevice )
self . client = ledger_bitcoin . client . NewClient ( transport , get_chain ( ) )
self . _product_key = product_key
self . _soft_device_id = None
self . master_fingerprint = None
self . _known_xpubs : Dict [ str , str ] = { } # path ==> xpub
self . _registered_policies : Dict [ bytes , bytes ] = { } # wallet id => wallet hmac
def is_pairable ( self ) :
return True
@runs_in_hwd_thread
def close ( self ) :
self . client . stop ( )
def is_initialized ( self ) :
return True
@runs_in_hwd_thread
def get_soft_device_id ( self ) :
if self . _soft_device_id is None :
self . _soft_device_id = self . request_root_fingerprint_from_device ( )
return self . _soft_device_id
def device_model_name ( self ) :
return LedgerPlugin . device_name_from_product_key ( self . _product_key )
@runs_in_hwd_thread
def has_usable_connection_with_device ( self ) :
try :
self . client . get_version ( )
except BaseException :
return False
return True
@runs_in_hwd_thread
@test_pin_unlocked
def get_xpub ( self , bip32_path , xtype ) :
# try silently first; if not a standard path, repeat with on-screen display
bip32_path = bip32_path . replace ( ' h ' , ' \' ' )
# cache known path/xpubs combinations in order to avoid requesting them many times
if bip32_path in self . _known_xpubs :
xpub = self . _known_xpubs [ bip32_path ]
else :
try :
xpub = self . client . get_extended_pubkey ( bip32_path )
except NotSupportedError :
xpub = self . client . get_extended_pubkey ( bip32_path , True )
self . _known_xpubs [ bip32_path ] = xpub
# Ledger always returns 'standard' xpubs; convert to the right xtype
return convert_xpub ( xpub , xtype )
@runs_in_hwd_thread
def request_root_fingerprint_from_device ( self ) - > str :
return self . client . get_master_fingerprint ( ) . hex ( )
@runs_in_hwd_thread
@test_pin_unlocked
def get_master_fingerprint ( self ) - > bytes :
if self . master_fingerprint is None :
self . master_fingerprint = self . client . get_master_fingerprint ( )
return self . master_fingerprint
@runs_in_hwd_thread
@test_pin_unlocked
def get_singlesig_default_wallet_policy ( self , addr_type : ' AddressType ' , account : int ) - > ' WalletPolicy ' :
assert account > = HARDENED_FLAG
if addr_type == AddressType . LEGACY :
template = " pkh(@0/**) "
elif addr_type == AddressType . WIT :
template = " wpkh(@0/**) "
elif addr_type == AddressType . SH_WIT :
template = " sh(wpkh(@0/**)) "
elif addr_type == AddressType . TAP :
template = " tr(@0/**) "
else :
raise ValueError ( " Unknown address type " )
fpr = self . get_master_fingerprint ( )
key_origin_steps = f " { get_bip44_purpose ( addr_type ) } ' / { get_bip44_chain ( self . client . chain ) } ' / { account & ~ HARDENED_FLAG } ' "
xpub = self . get_xpub ( f " m/ { key_origin_steps } " , ' standard ' )
key_str = f " [ { fpr . hex ( ) } / { key_origin_steps } ] { xpub } "
# Make the Wallet object
return WalletPolicy ( name = " " , descriptor_template = template , keys_info = [ key_str ] )
@runs_in_hwd_thread
@test_pin_unlocked
def get_singlesig_policy_for_path ( self , path : str , xtype : str , master_fp : bytes ) - > Optional [ ' WalletPolicy ' ] :
path = path . replace ( " h " , " ' " )
path_parts = path . split ( " / " )
if not 5 < = len ( path_parts ) < = 6 :
raise UserFacingException ( f " Unsupported path: { path } " )
path_root = " / " . join ( path_parts [ : - 2 ] )
fpr = self . get_master_fingerprint ( )
# Ledger always uses standard xpubs in wallet policies
xpub = self . get_xpub ( f " m/ { path_root } " , ' standard ' )
key_info = f " [ { fpr . hex ( ) } / { path_root } ] { xpub } "
if xtype == ' p2pkh ' :
name = " Legacy P2PKH "
descriptor_template = " pkh(@0/**) "
elif xtype == ' p2wpkh-p2sh ' :
name = " Nested SegWit "
descriptor_template = " sh(wpkh(@0/**)) "
elif xtype == ' p2wpkh ' :
name = " SegWit "
descriptor_template = " wpkh(@0/**) "
elif xtype == ' p2tr ' :
name = " Taproot "
descriptor_template = " tr(@0/**) "
else :
return None
policy = WalletPolicy ( " " , descriptor_template , [ key_info ] )
if is_policy_standard ( policy , master_fp , constants . net . BIP44_COIN_TYPE ) :
return policy
# Non standard policy, so give it a name
return WalletPolicy ( name , descriptor_template , [ key_info ] )
def password_dialog ( self , msg = None ) :
response = self . handler . get_word ( msg )
if response is None :
return False , None , None
return True , response , response
def _register_policy_if_needed ( self , wallet_policy : ' WalletPolicy ' ) - > Tuple [ bytes , bytes ] :
# If the policy is not register, registers it and saves its hmac on success
# Returns the pair of wallet id and wallet hmac
if wallet_policy . id not in self . _registered_policies :
wallet_id , wallet_hmac = self . client . register_wallet ( wallet_policy )
assert wallet_id == wallet_policy . id
self . _registered_policies [ wallet_id ] = wallet_hmac
return wallet_policy . id , self . _registered_policies [ wallet_policy . id ]
@runs_in_hwd_thread
@test_pin_unlocked
def show_address ( self , address_path : str , txin_type : str ) :
client_ledger = self . client
self . handler . show_message ( _ ( " Showing address ... " ) )
# TODO: generalize for multisignature
try :
master_fp = client_ledger . get_master_fingerprint ( )
wallet_policy = self . get_singlesig_policy_for_path ( address_path , txin_type , master_fp )
change , addr_index = [ int ( i ) for i in address_path . split ( " / " ) [ - 2 : ] ]
wallet_hmac = None
if not is_policy_standard ( wallet_policy , master_fp , constants . net . BIP44_COIN_TYPE ) :
wallet_id , wallet_hmac = self . _register_policy_if_needed ( wallet_policy )
self . client . get_wallet_address ( wallet_policy , wallet_hmac , change , addr_index , True )
except DenyError :
pass # cancelled by user
except BaseException as e :
_logger . exception ( ' Error while showing an address ' )
self . handler . show_error ( e )
finally :
self . handler . finished ( )
@runs_in_hwd_thread
@test_pin_unlocked
def sign_transaction ( self , keystore : Hardware_KeyStore , tx : PartialTransaction , password : str ) :
if tx . is_complete ( ) :
return
# mostly adapted from HWI
psbt_bytes = tx . serialize_as_bytes ( )
psbt = ledger_bitcoin . client . PSBT ( )
psbt . deserialize ( base64 . b64encode ( psbt_bytes ) . decode ( ' ascii ' ) )
try :
master_fp = self . client . get_master_fingerprint ( )
# Figure out which wallets are signing
wallets : Dict [ bytes , Tuple [ AddressType , WalletPolicy , Optional [ bytes ] ] ] = { }
for input_num , ( electrum_txin , psbt_in ) in enumerate ( zip ( tx . inputs ( ) , psbt . inputs ) ) :
if electrum_txin . is_coinbase_input ( ) :
raise UserFacingException ( " Coinbase not supported " ) # should never happen
utxo = None
if psbt_in . witness_utxo :
utxo = psbt_in . witness_utxo
if psbt_in . non_witness_utxo :
if psbt_in . prev_txid != psbt_in . non_witness_utxo . hash :
raise UserFacingException ( f " Input { input_num } has a non_witness_utxo with the wrong hash " )
assert psbt_in . prev_out is not None
utxo = psbt_in . non_witness_utxo . vout [ psbt_in . prev_out ]
if utxo is None :
continue
2023-02-26 13:28:31 +00:00
if ( desc := electrum_txin . script_descriptor ) is None :
raise Exception ( " script_descriptor missing for txin " )
scriptcode = desc . expand ( ) . scriptcode_for_sighash
2022-10-18 15:29:03 +02:00
2023-02-22 14:02:24 +00:00
is_wit , wit_ver , __ = is_witness ( psbt_in . redeem_script or utxo . scriptPubKey )
2022-10-18 15:29:03 +02:00
script_addrtype = AddressType . LEGACY
if is_wit :
# if it's a segwit spend (any version), make sure the witness_utxo is also present
psbt_in . witness_utxo = utxo
2023-02-26 13:28:31 +00:00
if electrum_txin . is_p2sh_segwit ( ) :
2022-10-18 15:29:03 +02:00
if wit_ver == 0 :
script_addrtype = AddressType . SH_WIT
else :
raise UserFacingException ( " Cannot have witness v1+ in p2sh " )
else :
if wit_ver == 0 :
script_addrtype = AddressType . WIT
elif wit_ver == 1 :
script_addrtype = AddressType . TAP
else :
continue
multisig = parse_multisig ( scriptcode )
if multisig is not None :
k , ms_pubkeys = multisig
# Figure out the parent xpubs
key_exprs : List [ str ] = [ ]
ok = True
our_keys = 0
for pub in ms_pubkeys :
if pub in psbt_in . hd_keypaths :
pk_origin = psbt_in . hd_keypaths [ pub ]
if pk_origin . fingerprint == master_fp :
our_keys + = 1
for xpub_bytes , xpub_origin in psbt . xpub . items ( ) :
xpub_str = EncodeBase58Check ( xpub_bytes )
if ( xpub_origin . fingerprint == pk_origin . fingerprint ) and ( xpub_origin . path == pk_origin . path [ : len ( xpub_origin . path ) ] ) :
key_origin_full = pk_origin . to_string ( ) . replace ( ' h ' , ' \' ' )
# strip last two steps of derivation
key_origin_parts = key_origin_full . split ( ' / ' )
if len ( key_origin_parts ) < 3 :
raise UserFacingException ( _ ( ' Unable to sign this transaction ' ) )
key_origin = ' / ' . join ( key_origin_parts [ : - 2 ] )
key_exprs . append ( f " [ { key_origin } ] { xpub_str } " )
break
else :
# No xpub, Ledger will not accept this multisig
ok = False
if not ok :
continue
# Electrum uses sortedmulti; we make sure that the array of key information is normalized in a consistent order
key_exprs = list ( sorted ( key_exprs ) )
# Make and register the MultisigWallet
msw = MultisigWallet ( f " { k } of { len ( key_exprs ) } Multisig " , script_addrtype , k , key_exprs )
msw_id = msw . id
if msw_id not in wallets :
__ , registered_hmac = self . _register_policy_if_needed ( msw )
wallets [ msw_id ] = (
script_addrtype ,
msw ,
registered_hmac ,
)
else :
def process_origin ( origin : KeyOriginInfo ) - > None :
if is_standard_path ( origin . path , script_addrtype , get_chain ( ) ) :
# these policies do not need to be registered
policy = self . get_singlesig_default_wallet_policy ( script_addrtype , origin . path [ 2 ] )
wallets [ policy . id ] = (
script_addrtype ,
self . get_singlesig_default_wallet_policy ( script_addrtype , origin . path [ 2 ] ) ,
None , # Wallet hmac
)
else :
# register the policy
if script_addrtype == AddressType . LEGACY :
name = " Legacy "
template = " pkh(@0/**) "
elif script_addrtype == AddressType . WIT :
name = " Native SegWit "
template = " wpkh(@0/**) "
elif script_addrtype == AddressType . SH_WIT :
name = " Nested SegWit "
template = " sh(wpkh(@0/**)) "
elif script_addrtype == AddressType . TAP :
name = " Taproot "
template = " tr(@0/**) "
else :
raise ValueError ( " Unknown address type " )
key_origin_info = origin . to_string ( )
key_origin_steps = key_origin_info . replace ( ' h ' , ' \' ' ) . split ( ' / ' ) [ 1 : ]
if len ( key_origin_steps ) < 3 :
# Skip this input, not able to sign
return
# remove the last two steps
account_key_origin = " / " . join ( key_origin_steps [ : - 2 ] )
# get the account-level xpub
xpub = self . get_xpub ( f " m/ { account_key_origin } " , ' standard ' )
key_str = f " [ { master_fp . hex ( ) } / { account_key_origin } ] { xpub } "
policy = WalletPolicy ( name , template , [ key_str ] )
__ , registered_hmac = self . client . register_wallet ( policy )
wallets [ policy . id ] = (
script_addrtype ,
policy ,
registered_hmac ,
)
for key , origin in psbt_in . hd_keypaths . items ( ) :
if origin . fingerprint == master_fp :
process_origin ( origin )
for key , ( __ , origin ) in psbt_in . tap_bip32_paths . items ( ) :
# TODO: Support script path signing
if key == psbt_in . tap_internal_key and origin . fingerprint == master_fp :
process_origin ( origin )
2022-11-09 13:21:14 +01:00
self . handler . show_message ( _ ( " Confirm Transaction on your Ledger device... " ) )
2022-10-18 15:29:03 +02:00
2022-11-09 13:21:14 +01:00
if len ( wallets ) == 0 :
# Could not find a WalletPolicy to sign with
raise UserFacingException ( _ ( ' Unable to sign this transaction ' ) )
2022-10-18 15:29:03 +02:00
2022-11-09 13:21:14 +01:00
# For each wallet, sign
for __ , ( __ , wallet , wallet_hmac ) in wallets . items ( ) :
input_sigs = self . client . sign_psbt ( psbt , wallet , wallet_hmac )
for idx , pubkey , sig in input_sigs :
tx . add_signature_to_txin ( txin_idx = idx , signing_pubkey = pubkey . hex ( ) , sig = sig . hex ( ) )
2022-10-18 15:29:03 +02:00
except DenyError :
pass # cancelled by user
except BaseException as e :
_logger . exception ( ' Error while signing ' )
self . handler . show_error ( e )
finally :
self . handler . finished ( )
@runs_in_hwd_thread
@test_pin_unlocked
def sign_message (
self ,
address_path : str ,
message : str ,
password ,
* ,
script_type : Optional [ str ] = None ,
) - > bytes :
message = message . encode ( ' utf8 ' )
message_hash = hashlib . sha256 ( message ) . hexdigest ( ) . upper ( )
# prompt for the PIN before displaying the dialog if necessary
self . handler . show_message ( " Signing message ... \r \n Message hash: " + message_hash )
result = b ' '
try :
result = base64 . b64decode ( self . client . sign_message ( message , address_path ) )
except DenyError :
pass # cancelled by user
2018-01-10 18:39:25 +01:00
except BaseException as e :
2022-10-18 15:29:03 +02:00
_logger . exception ( ' ' )
2018-01-10 18:39:25 +01:00
self . handler . show_error ( e )
2018-01-11 04:37:41 +11:00
finally :
self . handler . finished ( )
2016-08-21 22:44:42 +02:00
2022-10-18 15:29:03 +02:00
return result
class Ledger_KeyStore ( Hardware_KeyStore ) :
""" Ledger keystore. Targets all versions, will have different behavior with different clients. """
hw_type = ' ledger '
device = ' Ledger '
plugin : ' LedgerPlugin '
def __init__ ( self , d ) :
Hardware_KeyStore . __init__ ( self , d )
self . cfg = d . get ( ' cfg ' , { ' mode ' : 0 } )
def dump ( self ) :
obj = Hardware_KeyStore . dump ( self )
obj [ ' cfg ' ] = self . cfg
return obj
def get_client_dongle_object ( self , * , client : Optional [ Ledger_Client ] = None ) - > Ledger_Client :
if client is None :
client = self . get_client ( )
return client
def decrypt_message ( self , pubkey , message , password ) :
raise UserFacingException ( _ ( ' Encryption and decryption are currently not supported for {} ' ) . format ( self . device ) )
def sign_message ( self , sequence , * args , * * kwargs ) :
address_path = self . get_derivation_prefix ( ) [ 2 : ] + " / %d / %d " % sequence
return self . get_client_dongle_object ( ) . sign_message ( address_path , * args , * * kwargs )
def sign_transaction ( self , * args , * * kwargs ) :
return self . get_client_dongle_object ( ) . sign_transaction ( self , * args , * * kwargs )
def show_address ( self , sequence , * args , * * kwargs ) :
address_path = self . get_derivation_prefix ( ) [ 2 : ] + " / %d / %d " % sequence
return self . get_client_dongle_object ( ) . show_address ( address_path , * args , * * kwargs )
2016-08-21 22:44:42 +02:00
class LedgerPlugin ( HW_PluginBase ) :
keystore_class = Ledger_KeyStore
2022-10-18 15:29:03 +02:00
minimum_library = ( 0 , 1 , 1 )
DEVICE_IDS = [ ( 0x2581 , 0x1807 ) , # HW.1 legacy btchip
( 0x2581 , 0x2b7c ) , # HW.1 transitional production
( 0x2581 , 0x3b7c ) , # HW.1 ledger production
( 0x2581 , 0x4b7c ) , # HW.1 ledger test
( 0x2c97 , 0x0000 ) , # Blue
( 0x2c97 , 0x0001 ) , # Nano-S
( 0x2c97 , 0x0004 ) , # Nano-X
( 0x2c97 , 0x0005 ) , # Nano-S Plus
( 0x2c97 , 0x0006 ) , # RFU
( 0x2c97 , 0x0007 ) , # RFU
( 0x2c97 , 0x0008 ) , # RFU
( 0x2c97 , 0x0009 ) , # RFU
( 0x2c97 , 0x000a ) ] # RFU
2021-03-21 00:34:25 -04:00
VENDOR_IDS = ( 0x2c97 , )
2020-11-18 15:14:55 +01:00
LEDGER_MODEL_IDS = {
0x10 : " Ledger Nano S " ,
0x40 : " Ledger Nano X " ,
2022-03-02 13:59:51 +01:00
0x50 : " Ledger Nano S Plus " ,
2020-11-18 15:14:55 +01:00
}
2022-10-18 15:29:03 +02:00
2018-05-09 18:17:49 +02:00
SUPPORTED_XTYPES = ( ' standard ' , ' p2wpkh-p2sh ' , ' p2wpkh ' , ' p2wsh-p2sh ' , ' p2wsh ' )
2016-08-28 16:38:30 +02:00
def __init__ ( self , parent , config , name ) :
2017-04-12 19:15:43 +02:00
self . segwit = config . get ( " segwit " )
2016-08-28 16:38:30 +02:00
HW_PluginBase . __init__ ( self , parent , config , name )
2020-07-01 16:13:38 +02:00
self . libraries_available = self . check_libraries_available ( )
if not self . libraries_available :
2022-11-09 11:21:21 +01:00
_logger . info ( " Library unavailable " )
2020-07-01 16:13:38 +02:00
return
2020-11-18 15:14:55 +01:00
# to support legacy devices and legacy firmwares
2020-07-01 16:13:38 +02:00
self . device_manager ( ) . register_devices ( self . DEVICE_IDS , plugin = self )
2020-11-18 15:14:55 +01:00
# to support modern firmware
self . device_manager ( ) . register_vendor_ids ( self . VENDOR_IDS , plugin = self )
2020-07-01 16:13:38 +02:00
def get_library_version ( self ) :
2022-11-07 11:30:41 +01:00
try :
import ledger_bitcoin
version = ledger_bitcoin . __version__
except ImportError :
raise
except :
version = " unknown "
if LEDGER_BITCOIN :
return version
else :
raise LibraryFoundButUnusable ( library_version = version )
2016-08-21 22:44:42 +02:00
2022-11-09 20:42:55 +00:00
@classmethod
def is_hw1 ( cls , product_key ) - > bool :
return product_key [ 0 ] == 0x2581
2020-11-18 15:14:55 +01:00
@classmethod
def _recognize_device ( cls , product_key ) - > Tuple [ bool , Optional [ str ] ] :
""" Returns (can_recognize, model_name) tuple. """
# legacy product_keys
if product_key in cls . DEVICE_IDS :
2022-11-09 20:42:55 +00:00
if cls . is_hw1 ( product_key ) :
2020-11-18 15:14:55 +01:00
return True , " Ledger HW.1 "
if product_key == ( 0x2c97 , 0x0000 ) :
return True , " Ledger Blue "
if product_key == ( 0x2c97 , 0x0001 ) :
return True , " Ledger Nano S "
if product_key == ( 0x2c97 , 0x0004 ) :
return True , " Ledger Nano X "
2022-03-02 13:59:51 +01:00
if product_key == ( 0x2c97 , 0x0005 ) :
return True , " Ledger Nano S Plus "
2020-11-18 15:14:55 +01:00
return True , None
# modern product_keys
if product_key [ 0 ] == 0x2c97 :
product_id = product_key [ 1 ]
model_id = product_id >> 8
if model_id in cls . LEDGER_MODEL_IDS :
model_name = cls . LEDGER_MODEL_IDS [ model_id ]
return True , model_name
# give up
return False , None
def can_recognize_device ( self , device : Device ) - > bool :
return self . _recognize_device ( device . product_key ) [ 0 ]
@classmethod
def device_name_from_product_key ( cls , product_key ) - > Optional [ str ] :
return cls . _recognize_device ( product_key ) [ 1 ]
def create_device_from_hid_enumeration ( self , d , * , product_key ) :
device = super ( ) . create_device_from_hid_enumeration ( d , product_key = product_key )
if not self . can_recognize_device ( device ) :
return None
return device
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2022-11-09 11:34:41 +01:00
def create_client ( self , device , handler ) - > Optional [ Ledger_Client ] :
2022-11-09 20:42:55 +00:00
try :
return Ledger_Client . construct_new ( device = device , product_key = device . product_key , plugin = self )
except Exception as e :
self . logger . info ( f " cannot connect at { device . path } { e } " )
2022-11-09 11:34:41 +01:00
return None
2016-08-28 16:38:30 +02:00
2017-12-07 11:35:10 +01:00
def setup_device ( self , device_info , wizard , purpose ) :
2016-08-28 16:38:30 +02:00
device_id = device_info . device . id_
2022-10-18 15:29:03 +02:00
client : Ledger_Client = self . scan_and_create_client_for_device ( device_id = device_id , wizard = wizard )
2020-04-01 21:05:19 +02:00
wizard . run_task_without_blocking_gui (
2022-10-18 15:29:03 +02:00
task = lambda : client . get_master_fingerprint ( ) )
2020-04-09 18:00:35 +02:00
return client
2016-08-28 16:38:30 +02:00
2017-10-31 11:45:25 +01:00
def get_xpub ( self , device_id , derivation , xtype , wizard ) :
2018-05-09 18:17:49 +02:00
if xtype not in self . SUPPORTED_XTYPES :
raise ScriptTypeNotSupported ( _ ( ' This type of script is not supported with {} . ' ) . format ( self . device ) )
2020-04-01 20:22:39 +02:00
client = self . scan_and_create_client_for_device ( device_id = device_id , wizard = wizard )
2022-10-18 15:29:03 +02:00
return client . get_xpub ( derivation , xtype )
2018-01-11 04:37:41 +11:00
2020-09-08 15:52:53 +00:00
@runs_in_hwd_thread
2018-04-27 03:21:27 +02:00
def show_address ( self , wallet , address , keystore = None ) :
if keystore is None :
keystore = wallet . get_keystore ( )
if not self . show_address_helper ( wallet , address , keystore ) :
return
if type ( wallet ) is not Standard_Wallet :
keystore . handler . show_error ( _ ( ' This function is only available for standard wallets when using {} . ' ) . format ( self . device ) )
return
2018-01-11 04:37:41 +11:00
sequence = wallet . get_address_index ( address )
txin_type = wallet . get_txin_type ( address )
2022-10-18 15:29:03 +02:00
2018-04-27 03:21:27 +02:00
keystore . show_address ( sequence , txin_type )