From 11f5ba31cea402677a99956d60e780f0c6503c84 Mon Sep 17 00:00:00 2001 From: "Jamie C. Driver" Date: Wed, 4 Sep 2024 11:21:12 +0100 Subject: [PATCH 1/2] jade: update Jade api to 1.0.31 This extends the serial api to recognise recently supported hardware. --- electrum/plugins/jade/jade.py | 12 +- electrum/plugins/jade/jadepy/README.md | 5 +- electrum/plugins/jade/jadepy/__init__.py | 2 +- electrum/plugins/jade/jadepy/jade.py | 118 +++++++++++++++++++- electrum/plugins/jade/jadepy/jade_serial.py | 6 +- 5 files changed, 128 insertions(+), 15 deletions(-) diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index dd55659fa..aa86b3e1a 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -26,14 +26,15 @@ _logger = get_logger(__name__) #import logging #LOGGING = logging.INFO #if LOGGING: -# logger = logging.getLogger('jade') +# logger = logging.getLogger('electrum.plugins.jade.jadepy.jade') # logger.setLevel(LOGGING) -# device_logger = logging.getLogger('jade-device') +# device_logger = logging.getLogger('electrum.plugins.jade.jadepy.jade-device') # device_logger.setLevel(LOGGING) try: # Do imports from .jadepy.jade import JadeAPI + from .jadepy.jade_serial import JadeSerialImpl from serial.tools import list_ports except ImportError as e: _logger.exception('error importing Jade plugin deps') @@ -353,12 +354,9 @@ class Jade_KeyStore(Hardware_KeyStore): class JadePlugin(HW_PluginBase): keystore_class = Jade_KeyStore minimum_library = (0, 0, 1) - DEVICE_IDS = [(0x10c4, 0xea60), # Development Jade device - (0x1a86, 0x55d4), # Retail Blockstream Jade (And some DIY devices) - (0x0403, 0x6001), # DIY FTDI Based Devices (Eg: M5StickC-Plus) - (0x1a86, 0x7523)] # DIY CH340 Based devices (Eg: ESP32-Wrover) + DEVICE_IDS = JadeSerialImpl.JADE_DEVICE_IDS SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') - MIN_SUPPORTED_FW_VERSION = (0, 1, 32) + MIN_SUPPORTED_FW_VERSION = (0, 1, 47) # For testing with qemu simulator (experimental) SIMULATOR_PATH = None # 'tcp:127.0.0.1:2222' diff --git a/electrum/plugins/jade/jadepy/README.md b/electrum/plugins/jade/jadepy/README.md index 125ccb13a..6ea4c069b 100644 --- a/electrum/plugins/jade/jadepy/README.md +++ b/electrum/plugins/jade/jadepy/README.md @@ -2,10 +2,9 @@ This is a slightly modified version of the official [Jade](https://github.com/Blockstream/Jade) python library. -This modified version was made from tag [1.0.29](https://github.com/Blockstream/Jade/releases/tag/1.0.29). - -Intention is to fold these modifications back into Jade repo, for future api release. +This modified version was made from tag [1.0.31](https://github.com/Blockstream/Jade/releases/tag/1.0.31). ## Changes + - Removed BLE module, reducing transitive dependencies - _http_request() function removed, so cannot be used as unintentional fallback diff --git a/electrum/plugins/jade/jadepy/__init__.py b/electrum/plugins/jade/jadepy/__init__.py index 64e2ceb7e..ce732cbcd 100644 --- a/electrum/plugins/jade/jadepy/__init__.py +++ b/electrum/plugins/jade/jadepy/__init__.py @@ -1,4 +1,4 @@ from .jade import JadeAPI from .jade_error import JadeError -__version__ = "0.2.0" +__version__ = "1.0.31" diff --git a/electrum/plugins/jade/jadepy/jade.py b/electrum/plugins/jade/jadepy/jade.py index be6ad5028..cfb982666 100644 --- a/electrum/plugins/jade/jadepy/jade.py +++ b/electrum/plugins/jade/jadepy/jade.py @@ -7,6 +7,7 @@ import collections import collections.abc import traceback import random +import socket import sys # JadeError @@ -24,7 +25,7 @@ device_logger = logging.getLogger(f'{__name__}-device') # It relies on the BLE dependencies being available try: from .jade_ble import JadeBleImpl -except ImportError as e: +except (ImportError, FileNotFoundError) as e: logger.warning(e) logger.warning('BLE scanning/connectivity will not be available') @@ -123,6 +124,32 @@ def _hexlify(data): # logger.info(e) # logger.info('Default _http_requests() function will not be available') +def generate_dump(): + while True: + try: + with socket.create_connection(("localhost", 4444)) as s: + output = b"" + while b"Open On-Chip Debugger" not in output: + data = s.recv(1024) + if not data: + continue + output += data + + s.sendall(b"esp gcov dump\n") + + output = b"" + while b"Targets disconnected." not in output: + data = s.recv(1024) + if not data: + continue + output += data + s.sendall(b"resume\n") + time.sleep(1) + return + except ConnectionRefusedError: + pass + + class JadeAPI: """ High-Level Jade Client API @@ -431,7 +458,8 @@ class JadeAPI: """ return self._jadeRpc('logout') - def ota_update(self, fwcmp, fwlen, chunksize, fwhash=None, patchlen=None, cb=None): + def ota_update(self, fwcmp, fwlen, chunksize, fwhash=None, patchlen=None, cb=None, + gcov_dump=False): """ RPC call to attempt to update the unit's firmware. @@ -507,6 +535,9 @@ class JadeAPI: if (cb): cb(written, cmplen) + if gcov_dump: + self.run_remote_gcov_dump() + # All binary data uploaded return self._jadeRpc('ota_complete') @@ -523,6 +554,22 @@ class JadeAPI: """ return self._jadeRpc('debug_selfcheck', long_timeout=True) + def run_remote_gcov_dump(self): + """ + RPC call to run in-built gcov-dump. + NOTE: Only available in a DEBUG build of the firmware. + + Returns + ------- + bool + Always True. + """ + result = self._jadeRpc('debug_gcov_dump', long_timeout=True) + time.sleep(0.5) + generate_dump() + time.sleep(2) + return result + def capture_image_data(self, check_qr=False): """ RPC call to capture raw image data from the camera. @@ -951,6 +998,42 @@ class JadeAPI: params = {'multisig_file': multisig_file} return self._jadeRpc('register_multisig', params) + def get_registered_descriptors(self): + """ + RPC call to fetch brief summaries of any descriptor wallets registered to this signer. + + Returns + ------- + dict + Brief description of registered descriptor, keyed by registration name. + Each entry contains keys: + descriptor_len - int, length of descriptor output script + num_datavalues - int, total number of substitution placeholders passed with script + master_blinding_key - 32-bytes, any liquid master blinding key for this wallet + """ + return self._jadeRpc('get_registered_descriptors') + + def get_registered_descriptor(self, descriptor_name): + """ + RPC call to fetch details of a named descriptor wallet registered to this signer. + + Parameters + ---------- + descriptor_name : string + Name of descriptor registration record to return. + + Returns + ------- + dict + Description of registered descriptor wallet identified by registration name. + Contains keys: + descriptor_name - str, name of descritpor registration + descriptor - str, descriptor output script, may contain substitution placeholders + datavalues - dict containing placeholders for substitution into script + """ + params = {'descriptor_name': descriptor_name} + return self._jadeRpc('get_registered_descriptor', params) + def register_descriptor(self, network, descriptor_name, descriptor_script, datavalues=None): """ RPC call to register a new descriptor wallet, which must contain the hw signer. @@ -959,7 +1042,7 @@ class JadeAPI: Parameters ---------- network : string - Network to which the multisig should apply - eg. 'mainnet', 'liquid', 'testnet', etc. + Network to which the descriptor should apply - eg. 'mainnet', 'liquid', 'testnet', etc. descriptor_name : string Name to use to identify this descriptor wallet registration record. @@ -1221,6 +1304,35 @@ class JadeAPI: params = {'identity': identity, 'curve': curve, 'index': index, 'challenge': challenge} return self._jadeRpc('sign_identity', params) + def sign_attestation(self, challenge): + """ + RPC call to sign passed challenge with embedded hw RSA-4096 key, such that the caller + can check the authenticity of the hardware unit. eg. whether it is a genuine + Blockstream production Jade unit. + Caller must have the public key of the external verifying authority they wish to validate + against (eg. Blockstream's Jade verification public key). + NOTE: only supported by ESP32S3-based hardware units. + + Parameters + ---------- + challenge : bytes + Challenge bytes to sign + + Returns + ------- + dict + Contains keys: + signature - 512-bytes, hardware RSA signature of the SHA256 hash of the passed + challenge bytes. + pubkey_pem - str, PEM export of RSA pubkey of the hardware unit, to verify the returned + RSA signature. + ext_signature - bytes, RSA signature of the verifying authority over the returned + pubkey_pem data. + (Caller can verify this signature with the public key of the verifying authority.) + """ + params = {'challenge': challenge} + return self._jadeRpc('sign_attestation', params) + def get_master_blinding_key(self, only_if_silent=False): """ RPC call to fetch the master (SLIP-077) blinding key for the hw signer. diff --git a/electrum/plugins/jade/jadepy/jade_serial.py b/electrum/plugins/jade/jadepy/jade_serial.py index 37706fe67..4b17c59dc 100644 --- a/electrum/plugins/jade/jadepy/jade_serial.py +++ b/electrum/plugins/jade/jadepy/jade_serial.py @@ -2,6 +2,7 @@ import serial import logging from serial.tools import list_ports +from .jade_error import JadeError logger = logging.getLogger(__name__) @@ -53,7 +54,10 @@ class JadeSerialImpl: assert self.ser is not None if not self.ser.is_open: - self.ser.open() + try: + self.ser.open() + except serial.serialutil.SerialException: + raise JadeError(1, "Unable to open port", self.device) # Ensure RTS and DTR are not set (as this can cause the hw to reboot) self.ser.setRTS(False) From 2cba0708d540b6ca51f194e6fba0c926ba80bfdd Mon Sep 17 00:00:00 2001 From: "Jamie C. Driver" Date: Wed, 4 Sep 2024 14:44:20 +0100 Subject: [PATCH 2/2] jade: use Jade's native PSBT signing and remove massaging into legacy format --- electrum/plugins/jade/jade.py | 92 ++++++++--------------------------- 1 file changed, 19 insertions(+), 73 deletions(-) diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index aa86b3e1a..1a40db1e3 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -7,7 +7,7 @@ from electrum import bip32, constants from electrum.crypto import sha256 from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore -from electrum.transaction import Transaction +from electrum.transaction import PartialTransaction, Transaction from electrum.wallet import Multisig_Wallet from electrum.util import UserFacingException from electrum.logging import get_logger @@ -194,25 +194,11 @@ class Jade_Client(HardwareClientBase): return base64.b64decode(sig) @runs_in_hwd_thread - def sign_tx(self, txn_bytes, inputs, change): + def sign_psbt(self, psbt_bytes): self.authenticate() - # Add some host entropy for AE sigs (although we won't verify) - for input in inputs: - if input['path'] is not None: - input['ae_host_entropy'] = os.urandom(32) - input['ae_host_commitment'] = os.urandom(32) - - # Map change script type - for output in change: - if output and output.get('variant') is not None: - output['variant'] = self._convertAddrType(output['variant'], False) - - # Pass to Jade to generate signatures - sig_data = self.jade.sign_tx(self._network(), txn_bytes, inputs, change, use_ae_signatures=True) - - # Extract signatures from returned data (sig[0] is the AE signer-commitment) - return [sig[1] for sig in sig_data] + # Pass as PSBT to Jade for signing. As of fw v0.1.47 Jade should handle PSBT natively. + return self.jade.sign_psbt(self._network(), psbt_bytes) @runs_in_hwd_thread def show_address(self, bip32_path_prefix, sequence, txin_type): @@ -261,64 +247,24 @@ class Jade_KeyStore(Hardware_KeyStore): self.handler.show_message(_("Preparing to sign transaction ...")) try: wallet = self.handler.get_wallet() - is_multisig = _is_multisig(wallet) - - # Fetch inputs of the transaction to sign - jade_inputs = [] - for txin in tx.inputs(): - pubkey, path = self.find_my_pubkey_in_txinout(txin) - witness_input = txin.is_segwit() - redeem_script = Transaction.get_preimage_script(txin) - input_tx = txin.utxo - input_tx = bytes.fromhex(input_tx.serialize()) if input_tx is not None else None - - # Build the input and add to the list - include some host entropy for AE sigs (although we won't verify) - jade_inputs.append({'is_witness': witness_input, - 'input_tx': input_tx, - 'script': redeem_script, - 'path': path}) - - # Change detection - change = [None] * len(tx.outputs()) - for index, txout in enumerate(tx.outputs()): - if txout.is_mine and txout.is_change: - desc = txout.script_descriptor - assert desc - if is_multisig: + if _is_multisig(wallet): + # Register multisig on Jade using any change addresses + for txout in tx.outputs(): + if txout.is_mine and txout.is_change: # Multisig - wallet details must be registered on Jade hw - multisig_name = _register_multisig_wallet(wallet, self, txout.address) - - # Jade only needs the path suffix(es) and the multisig registration - # name to generate the address, as the fixed derivation part is - # embedded in the multisig wallet registration record - # NOTE: all cosigners have same path suffix - path_suffix = wallet.get_address_index(txout.address) - paths = [path_suffix] * wallet.n - change[index] = {'multisig_name': multisig_name, 'paths': paths} - else: - # Pass entire path - pubkey, path = self.find_my_pubkey_in_txinout(txout) - change[index] = {'path':path, 'variant': desc.to_legacy_electrum_script_type()} - - # The txn itself - txn_bytes = bytes.fromhex(tx.serialize_to_network(include_sigs=False)) - - # Request Jade generate the signatures for our inputs. - # Change details are passed to be validated on the hw (user does not confirm) + _register_multisig_wallet(wallet, self, txout.address) + + # NOTE: sign_psbt() does not use AE signatures, so sticks with default (rfc6979) self.handler.show_message(_("Please confirm the transaction details on your Jade device...")) client = self.get_client() - signatures = client.sign_tx(txn_bytes, jade_inputs, change) - assert len(signatures) == len(tx.inputs()) - - # Inject signatures into tx - for index, (txin, signature) in enumerate(zip(tx.inputs(), signatures)): - pubkey, path = self.find_my_pubkey_in_txinout(txin) - if pubkey is not None and signature is not None: - tx.add_signature_to_txin( - txin_idx=index, - signing_pubkey=pubkey, - sig=signature, - ) + + psbt_bytes = tx.serialize_as_bytes() + psbt_bytes = client.sign_psbt(psbt_bytes) + signed_tx = PartialTransaction.from_raw_psbt(psbt_bytes) + + # Copy signatures into original tx + tx.combine_with_other_psbt(signed_tx) + finally: self.handler.finished()