Browse Source

bip32: fix hardened char "h" vs "'" compatibility for some hw wallets

in particular, ledger: fix sign_message for some wallets

```
156.02 | E | plugins.ledger.ledger |
Traceback (most recent call last):
  File "...\electrum\electrum\plugins\ledger\ledger.py", line 1265, in sign_message
    result = base64.b64decode(self.client.sign_message(message, address_path))
  File "...\Python310\site-packages\ledger_bitcoin\client.py", line 230, in sign_message
    sw, response = self._make_request(self.builder.sign_message(message_bytes, bip32_path), client_intepreter)
  File "...\Python310\site-packages\ledger_bitcoin\command_builder.py", line 176, in sign_message
    bip32_path: List[bytes] = bip32_path_from_string(bip32_path)
  File "...\Python310\site-packages\ledger_bitcoin\common.py", line 68, in bip32_path_from_string
    return [int(p).to_bytes(4, byteorder="big") if "'" not in p
  File "...\Python310\site-packages\ledger_bitcoin\common.py", line 68, in <listcomp>
    return [int(p).to_bytes(4, byteorder="big") if "'" not in p
ValueError: invalid literal for int() with base 10: '84h'
```

Regression from df2bd61de6, where the
default hardened char was changed from "'" to "h". Note that there was
no corresponding wallet db upgrade, so some files use one char and
others use the other.
master
SomberNight 3 years ago
parent
commit
499f51535f
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 10
      electrum/bip32.py
  2. 4
      electrum/keystore.py
  3. 5
      electrum/plugins/digitalbitbox/digitalbitbox.py
  4. 16
      electrum/plugins/ledger/ledger.py
  5. 4
      electrum/tests/test_bitcoin.py

10
electrum/bip32.py

@ -352,7 +352,9 @@ def convert_bip32_strpath_to_intpath(n: str) -> List[int]:
return path
def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str:
def convert_bip32_intpath_to_strpath(path: Sequence[int], *, hardened_char=BIP32_HARDENED_CHAR) -> str:
assert isinstance(hardened_char, str), hardened_char
assert len(hardened_char) == 1, hardened_char
s = "m/"
for child_index in path:
if not isinstance(child_index, int):
@ -361,7 +363,7 @@ def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str:
raise ValueError(f"bip32 path child index out of range: {child_index}")
prime = ""
if child_index & BIP32_PRIME:
prime = BIP32_HARDENED_CHAR
prime = hardened_char
child_index = child_index ^ BIP32_PRIME
s += str(child_index) + prime + '/'
# cut trailing "/"
@ -380,13 +382,13 @@ def is_bip32_derivation(s: str) -> bool:
return True
def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]:
def normalize_bip32_derivation(s: Optional[str], *, hardened_char=BIP32_HARDENED_CHAR) -> Optional[str]:
if s is None:
return None
if not is_bip32_derivation(s):
raise ValueError(f"invalid bip32 derivation: {s}")
ints = convert_bip32_strpath_to_intpath(s)
return convert_bip32_intpath_to_strpath(ints)
return convert_bip32_intpath_to_strpath(ints, hardened_char=hardened_char)
def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool:

4
electrum/keystore.py

@ -508,7 +508,9 @@ class Xpub(MasterPublicKeyMixin):
return self._xpub_bip32_node
def get_derivation_prefix(self) -> Optional[str]:
return self._derivation_prefix
if self._derivation_prefix is None:
return None
return normalize_bip32_derivation(self._derivation_prefix)
def get_root_fingerprint(self) -> Optional[str]:
return self._root_fingerprint

5
electrum/plugins/digitalbitbox/digitalbitbox.py

@ -19,6 +19,7 @@ import copy
from electrum.crypto import sha256d, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot
from electrum.bitcoin import public_key_to_p2pkh
from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation
from electrum.bip32 import normalize_bip32_derivation
from electrum import descriptor
from electrum import ecc
from electrum.ecc import msg_magic
@ -104,7 +105,8 @@ class DigitalBitbox_Client(HardwareClientBase):
return False
return True
def _get_xpub(self, bip32_path):
def _get_xpub(self, bip32_path: str):
bip32_path = normalize_bip32_derivation(bip32_path, hardened_char="'")
if self.check_device_dialog():
return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8'))
@ -458,6 +460,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
try:
message = message.encode('utf8')
inputPath = self.get_derivation_prefix() + "/%d/%d" % sequence
inputPath = normalize_bip32_derivation(inputPath, hardened_char="'")
msg_hash = sha256d(msg_magic(message))
inputHash = to_hexstr(msg_hash)
hasharray = []

16
electrum/plugins/ledger/ledger.py

@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Sequence, Tuple
from electrum import bip32, constants, ecc
from electrum import descriptor
from electrum.base_wizard import ScriptTypeNotSupported
from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath
from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, normalize_bip32_derivation
from electrum.bitcoin import EncodeBase58Check, int_to_hex, is_b58_address, is_segwit_script_type, var_int
from electrum.crypto import hash_160
from electrum.i18n import _
@ -430,7 +430,7 @@ class Ledger_Client_Legacy(Ledger_Client):
raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)
if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit():
raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)
bip32_path = bip32.normalize_bip32_derivation(bip32_path)
bip32_path = bip32.normalize_bip32_derivation(bip32_path, hardened_char="'")
bip32_intpath = bip32.convert_bip32_strpath_to_intpath(bip32_path)
bip32_path = bip32_path[2:] # cut off "m/"
if len(bip32_intpath) >= 1:
@ -931,10 +931,10 @@ class Ledger_Client_New(Ledger_Client):
@runs_in_hwd_thread
@test_pin_unlocked
def get_xpub(self, bip32_path, xtype):
def get_xpub(self, bip32_path: str, xtype):
# try silently first; if not a standard path, repeat with on-screen display
bip32_path = bip32_path.replace('h', '\'')
bip32_path = normalize_bip32_derivation(bip32_path, hardened_char="'")
# cache known path/xpubs combinations in order to avoid requesting them many times
if bip32_path in self._known_xpubs:
@ -1300,14 +1300,18 @@ class Ledger_KeyStore(Hardware_KeyStore):
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
address_path = self.get_derivation_prefix() + "/%d/%d" % sequence
address_path = normalize_bip32_derivation(address_path, hardened_char="'")
address_path = address_path[2:] # cut m/
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
address_path = self.get_derivation_prefix() + "/%d/%d" % sequence
address_path = normalize_bip32_derivation(address_path, hardened_char="'")
address_path = address_path[2:] # cut m/
return self.get_client_dongle_object().show_address(address_path, *args, **kwargs)

4
electrum/tests/test_bitcoin.py

@ -862,6 +862,10 @@ class Test_xprv_xpub(ElectrumTestCase):
self.assertEqual("m", convert_bip32_intpath_to_strpath([]))
self.assertEqual("m/44h/5241h/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221]))
self.assertEqual("m/0/1'/1'", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001], hardened_char="'"))
self.assertEqual("m", convert_bip32_intpath_to_strpath([], hardened_char="'"))
self.assertEqual("m/44'/5241'/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221], hardened_char="'"))
def test_normalize_bip32_derivation(self):
self.assertEqual("m/0/1h/1h", normalize_bip32_derivation("m/0/1h/1'"))
self.assertEqual("m", normalize_bip32_derivation("m////"))

Loading…
Cancel
Save