1
0

plugins: ledger: rm support for hw.1

This removes support for Ledger HW.1 and "Nano" (non-S) devices.
These were manufactured/sold around 2015-2016, and are long unsupported by the upstream vendor.

We previously added a deprecation warning to the GUI [0] released in 4.3.3 (2023-01-02), to warn owners of these devices.
This PR now fully removes support.

As a consequence, the unmaintained btchip-python dependency can now be removed, which solves [1].

[0]: 9b82eb6d06
[1]: https://github.com/spesmilo/electrum/issues/9370#issuecomment-2593675364
This commit is contained in:
SomberNight
2025-03-18 16:18:49 +00:00
parent 7737dbf795
commit 154adf0081
7 changed files with 21 additions and 359 deletions

View File

@@ -41,7 +41,6 @@ datas = [
datas += collect_data_files(f"{PYPKG}.plugins")
datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs
datas += collect_data_files('safetlib')
datas += collect_data_files('btchip')
datas += collect_data_files('ckcc')
datas += collect_data_files('bitbox02')

View File

@@ -4,8 +4,6 @@ base58==2.1.1 \
bitbox02==6.2.0 \
--hash=sha256:5a8290bd270468ccdf2e6ff7174d25ea2b2f191e19734a79aa573c2b982c266f \
--hash=sha256:cede06e399c98ed536fed6d8a421208daa00f97b697bd8363a941ac5f33309bf
btchip-python==0.1.32 \
--hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55
cbor2==5.4.6 \
--hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7
certifi==2024.2.2 \

View File

@@ -44,7 +44,6 @@ datas = [
datas += collect_data_files(f"{PYPKG}.plugins")
datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs
datas += collect_data_files('safetlib')
datas += collect_data_files('btchip')
datas += collect_data_files('ckcc')
datas += collect_data_files('bitbox02')

View File

@@ -14,8 +14,6 @@ hidapi>=0.7.99.post15
libusb1>=1.6
# device plugin: ledger
# note: btchip-python only needed for "legacy" protocol and HW.1 support
btchip-python>=0.1.32
ledger-bitcoin>=0.2.0
hidapi

View File

@@ -1,168 +0,0 @@
import copy
from typing import TYPE_CHECKING
from PyQt6.QtWidgets import (QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel,
QWidget, QHBoxLayout, QComboBox)
from btchip.btchip import BTChipException
from electrum.gui.qt.util import PasswordLineEdit
from electrum.i18n import _
from electrum import constants, bitcoin
from electrum.logging import get_logger
if TYPE_CHECKING:
from .ledger import Ledger_Client
_logger = get_logger(__name__)
DEBUG = False
helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.<br><br>" \
"For best security you should unplug your device, open a text editor on another computer, " \
"put your cursor into it, and plug your device into that computer. " \
"It will output a summary of the transaction being signed and a one-time PIN.<br><br>" \
"Verify the transaction summary and type the PIN code here.<br><br>" \
"Before pressing enter, plug the device back into this computer.<br>"),
_("Verify the address below.<br>Type the character from your security card corresponding to the <u><b>BOLD</b></u> character."),
]
class LedgerAuthDialog(QDialog):
def __init__(self, handler, data, *, client: 'Ledger_Client'):
'''Ask user for 2nd factor authentication. Support text and security card methods.
Use last method from settings, but support downgrade.
'''
QDialog.__init__(self, handler.top_level_window())
self.handler = handler
self.txdata = data
self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else ''
self.setMinimumWidth(650)
self.setWindowTitle(_("Ledger Wallet Authentication"))
self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg)
self.dongle = client.dongleObject.dongle
self.pin = ''
self.devmode = self.getDevice2FAMode()
if self.devmode == 0x11 or self.txdata['confirmationType'] == 1:
self.cfg['mode'] = 0
vbox = QVBoxLayout()
self.setLayout(vbox)
def on_change_mode(idx):
self.cfg['mode'] = 0 if self.devmode == 0x11 else idx if idx > 0 else 1
if self.cfg['mode'] > 0:
self.handler.win.wallet.get_keystore().cfg = self.cfg
self.handler.win.wallet.save_keystore()
self.update_dlg()
def return_pin():
self.pin = self.pintxt.text() if self.txdata['confirmationType'] == 1 else self.cardtxt.text()
if self.cfg['mode'] == 1:
self.pin = ''.join(chr(int(str(i),16)) for i in self.pin)
self.accept()
self.modebox = QWidget()
modelayout = QHBoxLayout()
self.modebox.setLayout(modelayout)
modelayout.addWidget(QLabel(_("Method:")))
self.modes = QComboBox()
modelayout.addWidget(self.modes, 2)
modelayout.addStretch(1)
self.modebox.setMaximumHeight(50)
vbox.addWidget(self.modebox)
self.populate_modes()
self.modes.currentIndexChanged.connect(on_change_mode)
self.helpmsg = QTextEdit()
self.helpmsg.setStyleSheet("QTextEdit { color:black; background-color: lightgray; }")
self.helpmsg.setReadOnly(True)
vbox.addWidget(self.helpmsg)
self.pinbox = QWidget()
pinlayout = QHBoxLayout()
self.pinbox.setLayout(pinlayout)
self.pintxt = PasswordLineEdit()
self.pintxt.setMaxLength(4)
self.pintxt.returnPressed.connect(return_pin)
pinlayout.addWidget(QLabel(_("Enter PIN:")))
pinlayout.addWidget(self.pintxt)
pinlayout.addWidget(QLabel(_("NOT DEVICE PIN - see above")))
pinlayout.addStretch(1)
self.pinbox.setVisible(self.cfg['mode'] == 0)
vbox.addWidget(self.pinbox)
self.cardbox = QWidget()
card = QVBoxLayout()
self.cardbox.setLayout(card)
self.addrtext = QTextEdit()
self.addrtext.setStyleSheet('''
QTextEdit {
color:blue; background-color:lightgray; padding:15px 10px; border:none;
font-size:20pt; font-family: "Courier New", monospace; }
''')
self.addrtext.setReadOnly(True)
self.addrtext.setMaximumHeight(130)
card.addWidget(self.addrtext)
def pin_changed(s):
if len(s) < len(self.idxs):
i = self.idxs[len(s)]
addr = self.txdata['address']
if not constants.net.TESTNET:
text = addr[:i] + '<u><b>' + addr[i:i+1] + '</u></b>' + addr[i+1:]
else:
# pin needs to be created from mainnet address
addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), net=constants.BitcoinMainnet)
addr_mainnet = addr_mainnet[:i] + '<u><b>' + addr_mainnet[i:i+1] + '</u></b>' + addr_mainnet[i+1:]
text = str(addr) + '\n' + str(addr_mainnet)
self.addrtext.setHtml(str(text))
else:
self.addrtext.setHtml(_("Press Enter"))
pin_changed('')
cardpin = QHBoxLayout()
cardpin.addWidget(QLabel(_("Enter PIN:")))
self.cardtxt = PasswordLineEdit()
self.cardtxt.setMaxLength(len(self.idxs))
self.cardtxt.textChanged.connect(pin_changed)
self.cardtxt.returnPressed.connect(return_pin)
cardpin.addWidget(self.cardtxt)
cardpin.addWidget(QLabel(_("NOT DEVICE PIN - see above")))
cardpin.addStretch(1)
card.addLayout(cardpin)
self.cardbox.setVisible(self.cfg['mode'] == 1)
vbox.addWidget(self.cardbox)
self.update_dlg()
def populate_modes(self):
self.modes.blockSignals(True)
self.modes.clear()
self.modes.addItem(_("Summary Text PIN (requires dongle replugging)") if self.txdata['confirmationType'] == 1 else _("Summary Text PIN is Disabled"))
if self.txdata['confirmationType'] > 1:
self.modes.addItem(_("Security Card Challenge"))
self.modes.blockSignals(False)
def update_dlg(self):
self.modes.setCurrentIndex(self.cfg['mode'])
self.modebox.setVisible(True)
self.helpmsg.setText(helpTxt[self.cfg['mode']])
self.helpmsg.setMinimumHeight(180 if self.txdata['confirmationType'] == 1 else 100)
self.helpmsg.setVisible(True)
self.pinbox.setVisible(self.cfg['mode'] == 0)
self.cardbox.setVisible(self.cfg['mode'] == 1)
self.pintxt.setFocus() if self.cfg['mode'] == 0 else self.cardtxt.setFocus()
self.setMaximumHeight(400)
def getDevice2FAMode(self):
apdu = [0xe0, 0x24, 0x01, 0x00, 0x00, 0x01] # get 2fa mode
try:
mode = self.dongle.exchange(bytearray(apdu))
return mode
except BTChipException as e:
_logger.debug('Device getMode Failed')
return 0x11

View File

@@ -39,13 +39,12 @@ try:
from ledgercomm.interfaces.hid_device import HID
# legacy imports
# note: we could replace "btchip" with "ledger_bitcoin.btchip" but the latter does not support HW.1
import hid
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
from ledger_bitcoin.btchip.btchipComm import HIDDongleHIDAPI
from ledger_bitcoin.btchip.btchip import btchip
from ledger_bitcoin.btchip.btchipUtils import compress_public_key
from ledger_bitcoin.btchip.bitcoinTransaction import bitcoinTransaction
from ledger_bitcoin.btchip.btchipException import BTChipException
LEDGER_BITCOIN = True
except ImportError as e:
@@ -310,7 +309,7 @@ class Ledger_Client(HardwareClientBase, ABC):
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)
raise Exception("ledger hw.1 devices are no longer supported")
# 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
@@ -328,8 +327,10 @@ class Ledger_Client(HardwareClientBase, ABC):
_logger.info(f"ledger_bitcoin.createClient() got exc: {e}. falling back to old plugin.")
cl = None
if isinstance(cl, ledger_bitcoin.client.NewClient):
_logger.debug(f"Ledger_Client.construct_new(). creating NewClient for {device=}.")
return Ledger_Client_New(hid_device, *args, **kwargs)
else:
_logger.debug(f"Ledger_Client.construct_new(). creating LegacyClient for {device=}.")
return Ledger_Client_Legacy(hid_device, *args, **kwargs)
def __init__(self, *, plugin: HW_PluginBase):
@@ -552,7 +553,6 @@ class Ledger_Client_Legacy(Ledger_Client):
chipInputs = []
redeemScripts = []
changePath = ""
output = None
p2shTransaction = False
segwitTransaction = False
pin = ""
@@ -606,30 +606,16 @@ class Ledger_Client_Legacy(Ledger_Client):
if not is_txin_legacy_multisig(txin):
self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen
txOutput = bytearray()
txOutput += var_int(len(tx.outputs()))
for o in tx.outputs():
txOutput += int.to_bytes(o.value, length=8, byteorder="little", signed=False)
script = o.scriptpubkey
txOutput += var_int(len(script))
txOutput += script
txOutput = bytes(txOutput)
if not self.supports_multi_output():
if len(tx.outputs()) > 2:
self.give_error("Transaction with more than 2 outputs not supported")
for txout in tx.outputs():
if self.is_hw1() and txout.address and not is_b58_address(txout.address):
self.give_error(_("This {} device can only send to base58 addresses.").format(keystore.device))
if not txout.address:
if self.is_hw1():
self.give_error(_("Only address outputs are supported by {}").format(keystore.device))
# 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
# - only one output and one change is authorized (for hw.1 and nano)
# - at most one output can bypass confirmation (~change) (for all)
# - at most one output can bypass confirmation (~change)
if not p2shTransaction:
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
@@ -643,10 +629,6 @@ class Ledger_Client_Legacy(Ledger_Client):
assert changePath
changePath = convert_bip32_intpath_to_strpath(changePath)[2:]
has_change = True
else:
output = txout.address
else:
output = txout.address
try:
# Get trusted inputs from the original transactions
@@ -681,22 +663,11 @@ class Ledger_Client_Legacy(Ledger_Client):
firstTransaction = True
inputIndex = 0
rawTx = tx.serialize_to_network(include_sigs=False)
if self.is_hw1():
self.dongleObject.enableAlternate2fa(False)
if segwitTransaction:
self.dongleObject.startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version)
# we don't set meaningful outputAddress, amount and fees
# as we only care about the alternateEncoding==True branch
outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
outputData['outputData'] = txOutput
if outputData['confirmationNeeded']:
outputData['address'] = output
self.handler.finished()
# do the authenticate dialog and get pin:
pin = self.handler.get_auth(outputData, client=self)
if not pin:
raise UserWarning()
self.handler.show_message(_("Confirmed. Signing Transaction..."))
while inputIndex < len(inputs):
self.handler.show_message(_("Signing transaction...") + f" (phase2, {inputIndex}/{len(inputs)})")
singleInput = [chipInputs[inputIndex]]
@@ -716,24 +687,14 @@ class Ledger_Client_Legacy(Ledger_Client):
# we don't set meaningful outputAddress, amount and fees
# as we only care about the alternateEncoding==True branch
outputData = self.dongleObject.finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
outputData['outputData'] = txOutput
if outputData['confirmationNeeded']:
outputData['address'] = output
self.handler.finished()
# do the authenticate dialog and get pin:
pin = self.handler.get_auth(outputData, client=self)
if not pin:
raise UserWarning()
self.handler.show_message(_("Confirmed. Signing Transaction..."))
else:
# Sign input with the provided PIN
inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
inputSignature[0] = 0x30 # force for 1.4.9+
my_pubkey = inputs[inputIndex][4]
tx.add_signature_to_txin(txin_idx=inputIndex,
signing_pubkey=my_pubkey,
sig=inputSignature)
inputIndex = inputIndex + 1
# Sign input with the provided PIN
inputSignature = self.dongleObject.untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
inputSignature[0] = 0x30 # force for 1.4.9+
my_pubkey = inputs[inputIndex][4]
tx.add_signature_to_txin(txin_idx=inputIndex,
signing_pubkey=my_pubkey,
sig=inputSignature)
inputIndex = inputIndex + 1
firstTransaction = False
except UserWarning:
self.handler.show_error(_('Cancelled by user'))
@@ -770,12 +731,6 @@ class Ledger_Client_Legacy(Ledger_Client):
try:
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)
except BTChipException as e:
if e.sw == 0x6a80:
@@ -813,88 +768,6 @@ class Ledger_Client_Legacy(Ledger_Client):
return bytes([27 + 4 + (signature[0] & 0x01)]) + r_padded + s_padded
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"\nInstalled: {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.\nPlease 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\nIt 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
class Ledger_Client_New(Ledger_Client):
"""Client based on the ledger_bitcoin library, targeting versions 2.1.* and above."""
@@ -1333,10 +1206,10 @@ class LedgerPlugin(HW_PluginBase):
keystore_class = Ledger_KeyStore
minimum_library = (0, 2, 0)
maximum_library = (0, 4, 0)
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
DEVICE_IDS = [(0x2581, 0x1807), # HW.1 legacy btchip # not supported anymore (but we log an exception)
(0x2581, 0x2b7c), # HW.1 transitional production # not supported anymore
(0x2581, 0x3b7c), # HW.1 ledger production # not supported anymore
(0x2581, 0x4b7c), # HW.1 ledger test # not supported anymore
(0x2c97, 0x0000), # Blue
(0x2c97, 0x0001), # Nano-S
(0x2c97, 0x0004), # Nano-X

View File

@@ -52,46 +52,9 @@ class Plugin(LedgerPlugin, QtPluginBase):
class Ledger_Handler(QtHandlerBase):
setup_signal = pyqtSignal()
auth_signal = pyqtSignal(object, object)
MESSAGE_DIALOG_TITLE = _("Ledger Status")
def __init__(self, win):
super(Ledger_Handler, self).__init__(win, 'Ledger')
self.setup_signal.connect(self.setup_dialog)
self.auth_signal.connect(self.auth_dialog)
def word_dialog(self, msg):
response = QInputDialog.getText(self.top_level_window(), "Ledger Wallet Authentication", msg, QLineEdit.EchoMode.Password)
if not response[1]:
self.word = None
else:
self.word = str(response[0])
self.done.set()
def auth_dialog(self, data, client: 'Ledger_Client'):
try:
from .auth2fa import LedgerAuthDialog
except ImportError as e:
self.message_dialog(repr(e))
return
dialog = LedgerAuthDialog(self, data, client=client)
dialog.exec()
self.word = dialog.pin
self.done.set()
def get_auth(self, data, *, client: 'Ledger_Client'):
self.done.clear()
self.auth_signal.emit(data, client)
self.done.wait()
return self.word
def get_setup(self):
self.done.clear()
self.setup_signal.emit()
self.done.wait()
return
def setup_dialog(self):
self.show_error(_('Initialization of Ledger HW devices is currently disabled.'))