Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
gruve-p committed Sep 10, 2024
2 parents f3e80e3 + 2ebf8fd commit 42d8eb0
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 88 deletions.
1 change: 1 addition & 0 deletions electrum_grs/gui/qt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugin
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
if hasattr(QGuiApplication, 'setDesktopFileName'):
QGuiApplication.setDesktopFileName('electrum-grs.desktop')
QGuiApplication.setApplicationName("Electrum-GRS")
self.gui_thread = threading.current_thread()
self.windows = [] # type: List[ElectrumWindow]
self.efilter = OpenFileEventFilter(self.windows)
Expand Down
104 changes: 24 additions & 80 deletions electrum_grs/plugins/jade/jade.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from electrum_grs.crypto import sha256
from electrum_grs.i18n import _
from electrum_grs.keystore import Hardware_KeyStore
from electrum_grs.transaction import Transaction
from electrum_grs.transaction import PartialTransaction, Transaction
from electrum_grs.wallet import Multisig_Wallet
from electrum_grs.util import UserFacingException
from electrum_grs.logging import get_logger
Expand All @@ -26,14 +26,15 @@
#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')
Expand Down Expand Up @@ -193,25 +194,11 @@ def sign_message(self, bip32_path_prefix, sequence, message):
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):
Expand Down Expand Up @@ -260,64 +247,24 @@ def sign_transaction(self, tx, password):
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()

Expand Down Expand Up @@ -353,12 +300,9 @@ def show_address_multi(self, multisig_name, paths):
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'
Expand Down
5 changes: 2 additions & 3 deletions electrum_grs/plugins/jade/jadepy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion electrum_grs/plugins/jade/jadepy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .jade import JadeAPI
from .jade_error import JadeError

__version__ = "0.2.0"
__version__ = "1.0.31"
118 changes: 115 additions & 3 deletions electrum_grs/plugins/jade/jadepy/jade.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import collections.abc
import traceback
import random
import socket
import sys

# JadeError
Expand All @@ -24,7 +25,7 @@
# 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')

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -431,7 +458,8 @@ def logout(self):
"""
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.
Expand Down Expand Up @@ -507,6 +535,9 @@ def ota_update(self, fwcmp, fwlen, chunksize, fwhash=None, patchlen=None, cb=Non
if (cb):
cb(written, cmplen)

if gcov_dump:
self.run_remote_gcov_dump()

# All binary data uploaded
return self._jadeRpc('ota_complete')

Expand All @@ -523,6 +554,22 @@ def run_remote_selfcheck(self):
"""
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.
Expand Down Expand Up @@ -951,6 +998,42 @@ def register_multisig_file(self, multisig_file):
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.
Expand All @@ -959,7 +1042,7 @@ def register_descriptor(self, network, descriptor_name, descriptor_script, datav
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.
Expand Down Expand Up @@ -1221,6 +1304,35 @@ def sign_identity(self, identity, curve, challenge, index=0):
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.
Expand Down
6 changes: 5 additions & 1 deletion electrum_grs/plugins/jade/jadepy/jade_serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging

from serial.tools import list_ports
from .jade_error import JadeError

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,7 +54,10 @@ def connect(self):
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)
Expand Down

0 comments on commit 42d8eb0

Please sign in to comment.