Browse Source

Merge pull request #9007 from JamieDriver/update_jade_api

jade: update Jade api to 1.0.29
master
ghost43 2 years ago committed by GitHub
parent
commit
137f280690
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      contrib/deterministic-build/requirements-hw.txt
  2. 2
      contrib/requirements/requirements-hw.txt
  3. 2
      electrum/plugins/jade/jadepy/README.md
  4. 492
      electrum/plugins/jade/jadepy/jade.py
  5. 22
      electrum/plugins/jade/jadepy/jade_serial.py
  6. 11
      electrum/plugins/jade/jadepy/jade_tcp.py

4
contrib/deterministic-build/requirements-hw.txt

@ -6,8 +6,8 @@ bitbox02==6.2.0 \
--hash=sha256:cede06e399c98ed536fed6d8a421208daa00f97b697bd8363a941ac5f33309bf --hash=sha256:cede06e399c98ed536fed6d8a421208daa00f97b697bd8363a941ac5f33309bf
btchip-python==0.1.32 \ btchip-python==0.1.32 \
--hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55 --hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55
cbor==1.0.0 \ cbor2==5.4.6 \
--hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6 --hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7
certifi==2024.2.2 \ certifi==2024.2.2 \
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1

2
contrib/requirements/requirements-hw.txt

@ -24,7 +24,7 @@ ckcc-protocol>=0.7.7
bitbox02>=6.2.0 bitbox02>=6.2.0
# device plugin: jade # device plugin: jade
cbor>=1.0.0,<2.0.0 cbor2>=5.4.6,<6.0.0
pyserial>=3.5.0,<4.0.0 pyserial>=3.5.0,<4.0.0
# prefer older protobuf (see #7922) # prefer older protobuf (see #7922)

2
electrum/plugins/jade/jadepy/README.md

@ -2,7 +2,7 @@
This is a slightly modified version of the official [Jade](https://github.com/Blockstream/Jade) python library. This is a slightly modified version of the official [Jade](https://github.com/Blockstream/Jade) python library.
This modified version was made from tag [0.1.37](https://github.com/Blockstream/Jade/releases/tag/0.1.37). 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. Intention is to fold these modifications back into Jade repo, for future api release.

492
electrum/plugins/jade/jadepy/jade.py

@ -1,4 +1,4 @@
import cbor import cbor2 as cbor
import hashlib import hashlib
import json import json
import time import time
@ -17,20 +17,19 @@ from .jade_serial import JadeSerialImpl
from .jade_tcp import JadeTCPImpl from .jade_tcp import JadeTCPImpl
# 'jade' logger # 'jade' logger
logger = logging.getLogger('jade') logger = logging.getLogger(__name__)
device_logger = logging.getLogger('jade-device') device_logger = logging.getLogger(f'{__name__}-device')
# BLE comms backend is optional # BLE comms backend is optional
# It relies on the BLE dependencies being available # It relies on the BLE dependencies being available
try: try:
from .jade_ble import JadeBleImpl from .jade_ble import JadeBleImpl
except ImportError as e: except ImportError as e:
logger.warn(e) logger.warning(e)
logger.warn('BLE scanning/connectivity will not be available') logger.warning('BLE scanning/connectivity will not be available')
# Default serial connection # Default serial connection
DEFAULT_SERIAL_DEVICE = '/dev/ttyUSB0'
DEFAULT_BAUD_RATE = 115200 DEFAULT_BAUD_RATE = 115200
DEFAULT_SERIAL_TIMEOUT = 120 DEFAULT_SERIAL_TIMEOUT = 120
@ -77,7 +76,7 @@ def _hexlify(data):
# The default implementation used in JadeAPI._jadeRpc() below. # The default implementation used in JadeAPI._jadeRpc() below.
# NOTE: Only available if the 'requests' dependency is available. # NOTE: Only available if the 'requests' dependency is available.
# #
# Callers can supply their own implementation of this call where it is required. # Callers can supply their own implmentation of this call where it is required.
# #
# Parameters # Parameters
# ---------- # ----------
@ -94,13 +93,18 @@ def _hexlify(data):
# #
# # Use the first non-onion url # # Use the first non-onion url
# url = [url for url in params['urls'] if not url.endswith('.onion')][0] # url = [url for url in params['urls'] if not url.endswith('.onion')][0]
#
# if params['method'] == 'GET': # if params['method'] == 'GET':
# assert 'data' not in params, 'Cannot pass body to requests.get' # assert 'data' not in params, 'Cannot pass body to requests.get'
# f = requests.get(url) # def http_call_fn(): return requests.get(url)
# elif params['method'] == 'POST': # elif params['method'] == 'POST':
# data = json.dumps(params['data']) # data = json.dumps(params['data'])
# f = requests.post(url, data) # def http_call_fn(): return requests.post(url, data)
# else:
# raise JadeError(1, "Only GET and POST methods supported", params['method'])
# #
# try:
# f = http_call_fn()
# logger.debug("http_request received reply: {}".format(f.text)) # logger.debug("http_request received reply: {}".format(f.text))
# #
# if f.status_code != 200: # if f.status_code != 200:
@ -109,13 +113,15 @@ def _hexlify(data):
# #
# assert params['accept'] == 'json' # assert params['accept'] == 'json'
# f = f.json() # f = f.json()
# except Exception as e:
# logging.error(e)
# f = None
# #
# return {'body': f} # return {'body': f}
# #
# except ImportError as e: # except ImportError as e:
# logger.warn(e) # logger.info(e)
# logger.warn('Default _http_requests() function will not be available') # logger.info('Default _http_requests() function will not be available')
#
class JadeAPI: class JadeAPI:
""" """
@ -144,9 +150,9 @@ class JadeAPI:
def __exit__(self, exc_type, exc, tb): def __exit__(self, exc_type, exc, tb):
if (exc_type): if (exc_type):
logger.error("Exception causing JadeAPI context exit.") logger.info("Exception causing JadeAPI context exit.")
logger.error(exc_type) logger.info(exc_type)
logger.error(exc) logger.info(exc)
traceback.print_tb(tb) traceback.print_tb(tb)
self.disconnect(exc_type is not None) self.disconnect(exc_type is not None)
@ -340,16 +346,41 @@ class JadeAPI:
return result return result
def get_version_info(self): def ping(self):
"""
RPC call to test the connection to Jade and that Jade is powered on and receiving data, and
return whether the main task is currently handling a message, handling user menu navigation
or is idle.
NOTE: unlike all other calls this is not queued and handled in fifo order - this message is
handled immediately and the response sent as quickly as possible. This call does not block.
If this call is made in parallel with Jade processing other messages, the replies may be
out of order (although the message 'id' should still be correct). Use with caution.
Returns
-------
0 if the main task is currently idle
1 if the main task is handling a client message
2 if the main task is handling user ui menu navigation
"""
return self._jadeRpc('ping')
def get_version_info(self, nonblocking=False):
""" """
RPC call to fetch summary details pertaining to the hardware unit and running firmware. RPC call to fetch summary details pertaining to the hardware unit and running firmware.
Parameters
----------
nonblocking : bool
If True message will be handled immediately (see also ping()) *experimental feature*
Returns Returns
------- -------
dict dict
Contains keys for various info describing the hw and running fw Contains keys for various info describing the hw and running fw
""" """
return self._jadeRpc('get_version_info') params = {'nonblocking': True} if nonblocking else None
return self._jadeRpc('get_version_info', params)
def add_entropy(self, entropy): def add_entropy(self, entropy):
""" """
@ -387,7 +418,20 @@ class JadeAPI:
params = {'epoch': epoch if epoch is not None else int(time.time())} params = {'epoch': epoch if epoch is not None else int(time.time())}
return self._jadeRpc('set_epoch', params) return self._jadeRpc('set_epoch', params)
def ota_update(self, fwcmp, fwlen, chunksize, patchlen=None, cb=None): def logout(self):
"""
RPC call to logout of any wallet loaded on the Jade unit.
Any key material is freed and zero'd.
Call always returns true.
Returns
-------
bool
True
"""
return self._jadeRpc('logout')
def ota_update(self, fwcmp, fwlen, chunksize, fwhash=None, patchlen=None, cb=None):
""" """
RPC call to attempt to update the unit's firmware. RPC call to attempt to update the unit's firmware.
@ -403,6 +447,12 @@ class JadeAPI:
and ack'd by the hw unit. and ack'd by the hw unit.
The maximum supported chunk size is given in the version info data, under the key The maximum supported chunk size is given in the version info data, under the key
'JADE_OTA_MAX_CHUNK'. 'JADE_OTA_MAX_CHUNK'.
fwhash: 32-bytes, optional
The sha256 hash of the full uncompressed final firmware image. In the case of a full
firmware upload this should be the hash of the uncompressed file. In the case of a
delta update this is the hash of the expected final image - ie. the existing firmware
with the uploaded delta applied. ie. it is a verification of the fw image Jade will try
to boot. Optional for backward-compatibility - may become mandatory in a future release.
patchlen: int, optional patchlen: int, optional
If the compressed firmware bytes are an incremental diff to be applied to the running If the compressed firmware bytes are an incremental diff to be applied to the running
firmware image, this is the size of that patch when uncompressed. firmware image, this is the size of that patch when uncompressed.
@ -434,6 +484,9 @@ class JadeAPI:
'cmpsize': cmplen, 'cmpsize': cmplen,
'cmphash': cmphash} 'cmphash': cmphash}
if fwhash is not None:
params['fwhash'] = fwhash
if patchlen is not None: if patchlen is not None:
ota_method = 'ota_delta' ota_method = 'ota_delta'
params['patchsize'] = patchlen params['patchsize'] = patchlen
@ -464,11 +517,53 @@ class JadeAPI:
Returns Returns
------- -------
bool int
True on success. Time in ms for the internal tests to run, as measured on the hw.
ie. excluding any messaging overhead
""" """
return self._jadeRpc('debug_selfcheck', long_timeout=True) return self._jadeRpc('debug_selfcheck', long_timeout=True)
def capture_image_data(self, check_qr=False):
"""
RPC call to capture raw image data from the camera.
See also scan_qr() below.
NOTE: Only available in a DEBUG build of the firmware.
Parameters
----------
check_qr : bool, optional
If True only images which contain a valid qr code are captured and returned.
If False, any image is considered valid and is returned.
Defaults to False
Returns
-------
bytes
Raw image data from the camera framebuffer
"""
params = {'check_qr': check_qr}
return self._jadeRpc('debug_capture_image_data', params)
def scan_qr(self, image):
"""
RPC call to scan a passed image and return any data extracted from any qr image.
Exercises the camera image capture, but ignores result and uses passed image instead.
See also capture_image_data() above.
NOTE: Only available in a DEBUG build of the firmware.
Parameters
----------
image : bytes
The image data (as obtained from capture_image_data() above).
Returns
-------
bytes
String or byte data obtained from the image (via qr code)
"""
params = {'image': image}
return self._jadeRpc('debug_scan_qr', params)
def clean_reset(self): def clean_reset(self):
""" """
RPC call to clean/reset memory and storage, as much as is practical. RPC call to clean/reset memory and storage, as much as is practical.
@ -529,6 +624,38 @@ class JadeAPI:
params = {'seed': seed} params = {'seed': seed}
return self._jadeRpc('debug_set_mnemonic', params) return self._jadeRpc('debug_set_mnemonic', params)
def get_bip85_bip39_entropy(self, num_words, index, pubkey):
"""
RPC call to fetch encrypted bip85-bip39 entropy.
NOTE: Only available in a DEBUG build of the firmware.
Parameters
----------
num_words : int
The number of words the entropy is required to produce.
index : int
The index to use in the bip32 path to calcuate the entropy.
pubkey: 33-bytes
The host ephemeral pubkey to use to generate a shared ecdh secret to use as an AES key
to encrypt the returned entropy.
Returns
-------
dict
pubkey - 33-bytes, Jade's ephemeral pubkey used to generate a shared ecdh secret used as
an AES key to encrypt the returned entropy
encrypted - bytes, the requested bip85 bip39 entropy, AES encrypted with the first key
derived from the ecdh shared secret, prefixed with the iv
hmac - 32-bytes, the hmac of the encrypted buffer, using the second key derived from the
ecdh shared secret
"""
params = {'num_words': num_words,
'index': index,
'pubkey': pubkey}
return self._jadeRpc('get_bip85_bip39_entropy', params)
def set_pinserver(self, urlA=None, urlB=None, pubkey=None, cert=None): def set_pinserver(self, urlA=None, urlB=None, pubkey=None, cert=None):
""" """
RPC call to explicitly set (override) the details of the blind pinserver used to RPC call to explicitly set (override) the details of the blind pinserver used to
@ -698,6 +825,55 @@ class JadeAPI:
""" """
return self._jadeRpc('get_registered_multisigs') return self._jadeRpc('get_registered_multisigs')
def get_registered_multisig(self, multisig_name, as_file=False):
"""
RPC call to fetch details of a named multisig wallet registered to this signer.
NOTE: the multisig wallet must have been registered with firmware v1.0.23 or later
for the full signer details to be persisted and available.
Parameters
----------
multisig_name : string
Name of multsig registration record to return.
as_file : string, optional
If true the flat file format is returned, otherwise structured json is returned.
Defaults to false.
Returns
-------
dict
Description of registered multisig wallet identified by registration name.
Contains keys:
is_file is true:
multisig_file - str, the multisig file as produced by several wallet apps.
eg:
Name: MainWallet
Policy: 2 of 3
Format: P2WSH
Derivation: m/48'/0'/0'/2'
B237FE9D: xpub6E8C7BX4c7qfTsX7urnXggcAyFuhDmYLQhwRwZGLD9maUGWPinuc9k96ej...
249192D2: xpub6EbXynW6xjYR3crcztum6KzSWqDJoAJQoovwamwVnLaCSHA6syXKPnJo6U...
67F90FFC: xpub6EHuWWrYd8bp5FS1XAZsMPkmCqLSjpULmygWqAqWRCCjSWQwz6ntq5KnuQ...
is_file is false:
multisig_name - str, name of multisig registration
variant - str, script type, eg. 'sh(wsh(multi(k)))'
sorted - boolean, whether bip67 key sorting is applied
threshold - int, number of signers required,N
master_blinding_key - 32-bytes, any liquid master blinding key for this wallet
signers - dict containing keys:
fingerprint - 4 bytes, origin fingerprint
derivation - [int], bip32 path from origin to signer xpub provided
xpub - str, base58 xpub of signer
path - [int], any fixed path to always apply after the xpub - usually empty.
"""
params = {'multisig_name': multisig_name,
'as_file': as_file}
return self._jadeRpc('get_registered_multisig', params)
def register_multisig(self, network, multisig_name, variant, sorted_keys, threshold, signers, def register_multisig(self, network, multisig_name, variant, sorted_keys, threshold, signers,
master_blinding_key=None): master_blinding_key=None):
""" """
@ -748,8 +924,59 @@ class JadeAPI:
'master_blinding_key': master_blinding_key}} 'master_blinding_key': master_blinding_key}}
return self._jadeRpc('register_multisig', params) return self._jadeRpc('register_multisig', params)
def register_multisig_file(self, multisig_file):
"""
RPC call to register a new multisig wallet, which must contain the hw signer.
A registration file is provided - as produced my several wallet apps.
Parameters
----------
multisig_file : string
The multisig file as produced by several wallet apps.
eg:
Name: MainWallet
Policy: 2 of 3
Format: P2WSH
Derivation: m/48'/0'/0'/2'
B237FE9D: xpub6E8C7BX4c7qfTsX7urnXggcAyFuhDmYLQhwRwZGLD9maUGWPinuc9k96ejhEQ1DCk...
249192D2: xpub6EbXynW6xjYR3crcztum6KzSWqDJoAJQoovwamwVnLaCSHA6syXKPnJo6U3bVeGde...
67F90FFC: xpub6EHuWWrYd8bp5FS1XAZsMPkmCqLSjpULmygWqAqWRCCjSWQwz6ntq5KnuQnL23No2...
Returns
-------
bool
True on success, implying the mutisig wallet can now be used.
"""
params = {'multisig_file': multisig_file}
return self._jadeRpc('register_multisig', 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.
A registration name is provided - if it already exists that record is overwritten.
Parameters
----------
network : string
Network to which the multisig should apply - eg. 'mainnet', 'liquid', 'testnet', etc.
descriptor_name : string
Name to use to identify this descriptor wallet registration record.
If a registration record exists with the name given, that record is overwritten.
Returns
-------
bool
True on success, implying the descriptor wallet can now be used.
"""
params = {'network': network, 'descriptor_name': descriptor_name,
'descriptor': descriptor_script, 'datavalues': datavalues}
return self._jadeRpc('register_descriptor', params)
def get_receive_address(self, *args, recovery_xpub=None, csv_blocks=0, def get_receive_address(self, *args, recovery_xpub=None, csv_blocks=0,
variant=None, multisig_name=None, confidential=None): variant=None, multisig_name=None, descriptor_name=None,
confidential=None):
""" """
RPC call to generate, show, and return an address for the given path. RPC call to generate, show, and return an address for the given path.
The call has three forms. The call has three forms.
@ -795,6 +1022,16 @@ class JadeAPI:
multisig_name : str multisig_name : str
The name of the registered multisig wallet record used to generate the address. The name of the registered multisig wallet record used to generate the address.
4. Descriptor wallet addresses
branch : int
Multi-path derivation branch, usually 0.
pointer : int
Path index to descriptor
descriptor_name : str
The name of the registered descriptor wallet record used to generate the address.
Returns Returns
------- -------
str str
@ -805,6 +1042,10 @@ class JadeAPI:
assert len(args) == 2 assert len(args) == 2
keys = ['network', 'paths', 'multisig_name'] keys = ['network', 'paths', 'multisig_name']
args += (multisig_name,) args += (multisig_name,)
elif descriptor_name is not None:
assert len(args) == 3
keys = ['network', 'branch', 'pointer', 'descriptor_name']
args += (descriptor_name,)
elif variant is not None: elif variant is not None:
assert len(args) == 2 assert len(args) == 2
keys = ['network', 'path', 'variant'] keys = ['network', 'path', 'variant']
@ -865,6 +1106,26 @@ class JadeAPI:
params = {'path': path, 'message': message} params = {'path': path, 'message': message}
return self._jadeRpc('sign_message', params) return self._jadeRpc('sign_message', params)
def sign_message_file(self, message_file):
"""
RPC call to format and sign the given message, using the given bip32 path.
A message file is provided - as produced by eg. Specter wallet.
Supports RFC6979 only.
Parameters
----------
message_file : str
Message file to parse and produce signature for.
eg: 'signmessage m/84h/0h/0h/0/0 ascii:this is a test message'
Returns
-------
str
base64-encoded RFC6979 signature
"""
params = {'message_file': message_file}
return self._jadeRpc('sign_message', params)
def get_identity_pubkey(self, identity, curve, key_type, index=0): def get_identity_pubkey(self, identity, curve, key_type, index=0):
""" """
RPC call to fetch a pubkey for the given identity (slip13/slip17). RPC call to fetch a pubkey for the given identity (slip13/slip17).
@ -960,18 +1221,28 @@ class JadeAPI:
params = {'identity': identity, 'curve': curve, 'index': index, 'challenge': challenge} params = {'identity': identity, 'curve': curve, 'index': index, 'challenge': challenge}
return self._jadeRpc('sign_identity', params) return self._jadeRpc('sign_identity', params)
def get_master_blinding_key(self): def get_master_blinding_key(self, only_if_silent=False):
""" """
RPC call to fetch the master (SLIP-077) blinding key for the hw signer. RPC call to fetch the master (SLIP-077) blinding key for the hw signer.
May block temporarily to request the user's permission to export. Passing 'only_if_silent'
causes the call to return the 'denied' error if it would normally ask the user.
NOTE: the master blinding key of any registered multisig wallets can be obtained from NOTE: the master blinding key of any registered multisig wallets can be obtained from
the result of `get_registered_multisigs()`. the result of `get_registered_multisigs()`.
Parameters
----------
only_if_silent : boolean, optional
If True Jade will return the denied error if it would normally ask the user's permission
to export the master blinding key. Passing False (or letting default) may block while
asking the user to confirm the export on Jade.
Returns Returns
------- -------
32-bytes 32-bytes
SLIP-077 master blinding key SLIP-077 master blinding key
""" """
return self._jadeRpc('get_master_blinding_key') params = {'only_if_silent': only_if_silent}
return self._jadeRpc('get_master_blinding_key', params)
def get_blinding_key(self, script, multisig_name=None): def get_blinding_key(self, script, multisig_name=None):
""" """
@ -1034,26 +1305,22 @@ class JadeAPI:
def get_blinding_factor(self, hash_prevouts, output_index, bftype, multisig_name=None): def get_blinding_factor(self, hash_prevouts, output_index, bftype, multisig_name=None):
""" """
RPC call to get a deterministic "trusted" blinding factor to blind an output. RPC call to get deterministic blinding factors to blind an output.
Normally the blinding factors are generated and returned in the `get_commitments` call, Predicated on the host calculating the 'hash_prevouts' value correctly.
but for the last output the vbf must be generated on the host, so this call allows the Can fetch abf, vbf, or both together.
host to get a valid abf to compute the generator and then the "final" vbf.
Nonetheless, this call is kept generic, and can also generate vbfs, hence the "bftype"
parameter.
Parameters Parameters
---------- ----------
hash_prevouts : 32-bytes hash_prevouts : 32-bytes
This value is computed as specified in bip143. This value should be computed by the host as specified in bip143.
It is verified immediately since at this point Jade doesn't have the tx in question. It is not verified by Jade, since at this point Jade does not have the tx in question.
It will be checked later during `sign_liquid_tx()`.
output_index : int output_index : int
The index of the output we are trying to blind The index of the output we are trying to blind
bftype : str bftype : str
Can be eitehr "ASSET" or "VALUE", to generate abfs or vbfs. Can be "ASSET", "VALUE", or "ASSET_AND_VALUE", to generate abf, vbf, or both.
multisig_name : str, optional multisig_name : str, optional
The name of any registered multisig wallet for which to fetch the blinding factor. The name of any registered multisig wallet for which to fetch the blinding factor.
@ -1061,8 +1328,9 @@ class JadeAPI:
Returns Returns
------- -------
32-bytes 32-bytes or 64-bytes
The requested blinding factor The blinding factor for "ASSET" and "VALUE" requests, or both concatenated abf|vbf
ie. the first 32 bytes being abf, the second 32 bytes being vbf.
""" """
params = {'hash_prevouts': hash_prevouts, params = {'hash_prevouts': hash_prevouts,
'output_index': output_index, 'output_index': output_index,
@ -1109,7 +1377,7 @@ class JadeAPI:
Returns Returns
------- -------
dict dict
Containing the following the blinding factors and output commitments. Containing the blinding factors and output commitments.
""" """
params = {'asset_id': asset_id, params = {'asset_id': asset_id,
'value': value, 'value': value,
@ -1161,7 +1429,7 @@ class JadeAPI:
host_ae_entropy_values = [] host_ae_entropy_values = []
for txinput in inputs: for txinput in inputs:
# ae-protocol - do not send the host entropy immediately # ae-protocol - do not send the host entropy immediately
txinput = txinput.copy() # shallow copy txinput = txinput.copy() if txinput else {} # shallow copy
host_ae_entropy_values.append(txinput.pop('ae_host_entropy', None)) host_ae_entropy_values.append(txinput.pop('ae_host_entropy', None))
base_id += 1 base_id += 1
@ -1193,6 +1461,9 @@ class JadeAPI:
# Send all n inputs # Send all n inputs
requests = [] requests = []
for txinput in inputs: for txinput in inputs:
if txinput is None:
txinput = {}
base_id += 1 base_id += 1
msg_id = str(base_id) msg_id = str(base_id)
request = self.jade.build_request(msg_id, 'tx_input', txinput) request = self.jade.build_request(msg_id, 'tx_input', txinput)
@ -1212,31 +1483,44 @@ class JadeAPI:
return signatures return signatures
def sign_liquid_tx(self, network, txn, inputs, commitments, change, use_ae_signatures=False, def sign_liquid_tx(self, network, txn, inputs, commitments, change, use_ae_signatures=False,
asset_info=None): asset_info=None, additional_info=None):
""" """
RPC call to sign a liquid transaction. RPC call to sign a liquid transaction.
Parameters Parameters
---------- ----------
network : str network : str
Network to which the address should apply - eg. 'liquid', 'liquid-testnet', etc. Network to which the txn should apply - eg. 'liquid', 'liquid-testnet', etc.
txn : bytes txn : bytes
The transaction to sign The transaction to sign
inputs : [dict] inputs : [dict]
The tx inputs. Should contain keys: The tx inputs.
If signing this input, should contain keys:
is_witness, bool - whether this is a segwit input is_witness, bool - whether this is a segwit input
value_commitment, 33-bytes - The value commitment of ths input
These are only required if signing this input:
script, bytes- the redeem script script, bytes- the redeem script
path, [int] - the bip32 path to sign with path, [int] - the bip32 path to sign with
value_commitment, 33-bytes - The value commitment of ths input
This is optional if signing this input:
sighash, int - The sighash to use, defaults to 0x01 (SIGHASH_ALL)
These are only required for Anti-Exfil signatures: These are only required for Anti-Exfil signatures:
ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures
ae_host_entropy, 32-bytes - The host-entropy for Anti-Exfil signatures ae_host_entropy, 32-bytes - The host-entropy for Anti-Exfil signatures
These are only required for advanced transactions, eg. swaps, and only when the
inputs need unblinding.
Not needed for vanilla send-payment/redeposit etc:
abf, 32-bytes - asset blinding factor
asset_id, 32-bytes - the unblinded asset-id
asset_generator, 33-bytes - the (blinded) asset-generator
vbf, 32-bytes - the value blinding factor
value, int - the unblinded sats value of the input
If not signing this input a null or an empty dict can be passed.
commitments : [dict] commitments : [dict]
An array sized for the number of outputs. An array sized for the number of outputs.
Unblinded outputs should have a 'null' placeholder element. Unblinded outputs should have a 'null' placeholder element.
@ -1246,16 +1530,18 @@ class JadeAPI:
change : [dict] change : [dict]
An array sized for the number of outputs. An array sized for the number of outputs.
Outputs which are not change should have a 'null' placeholder element. Outputs which are not to this wallet should have a 'null' placeholder element.
Change elements with data will be automatically verified by Jade, and not by the user. The output scripts for the elements with data will be verified by Jade.
Populated elements should contain sufficient data to generate the change address. Unless the element also contains 'is_change': False, these outputs will automatically
be approved and not be verified by the user.
Populated elements should contain sufficient data to generate the wallet address.
See `get_receive_address()` See `get_receive_address()`
use_ae_signatures : bool, optional use_ae_signatures : bool, optional
Whether to use the anti-exfil protocol to generate the signatures. Whether to use the anti-exfil protocol to generate the signatures.
Defaults to False. Defaults to False.
asset_info : [dict] asset_info : [dict], optional
Any asset-registry data relevant to the assets being transacted, such that Jade can Any asset-registry data relevant to the assets being transacted, such that Jade can
display a meaningful name, issuer, ticker etc. rather than just asset-id. display a meaningful name, issuer, ticker etc. rather than just asset-id.
At the very least must contain 'asset_id', 'contract' and 'issuance_prevout' items, At the very least must contain 'asset_id', 'contract' and 'issuance_prevout' items,
@ -1263,6 +1549,17 @@ class JadeAPI:
not required. not required.
Defaults to None. Defaults to None.
additional_info: dict, optional
Extra data about the transaction. Only required for advanced transactions, eg. swaps.
Not needed for vanilla send-payment/redeposit etc:
tx_type, str: 'swap' indicates the tx represents an asset-swap proposal or transaction.
wallet_input_summary, dict: a list of entries containing 'asset_id' (32-bytes) and
'satoshi' (int) showing net movement of assets out of the wallet (ie. sum of wallet
inputs per asset, minus any change outputs).
wallet_output_summary, dict: a list of entries containing 'asset_id' (32-bytes) and
'satoshi' (int) showing net movement of assets into the wallet (ie. sum of wallet
outputs per asset, excluding any change outputs).
Returns Returns
------- -------
1. if use_ae_signatures is False 1. if use_ae_signatures is False
@ -1286,7 +1583,8 @@ class JadeAPI:
'trusted_commitments': commitments, 'trusted_commitments': commitments,
'use_ae_signatures': use_ae_signatures, 'use_ae_signatures': use_ae_signatures,
'change': change, 'change': change,
'asset_info': asset_info} 'asset_info': asset_info,
'additional_info': additional_info}
reply = self._jadeRpc('sign_liquid_tx', params, str(base_id)) reply = self._jadeRpc('sign_liquid_tx', params, str(base_id))
assert reply assert reply
@ -1301,23 +1599,25 @@ class JadeAPI:
Parameters Parameters
---------- ----------
network : str network : str
Network to which the address should apply - eg. 'mainnet', 'testnet', etc. Network to which the txn should apply - eg. 'mainnet', 'testnet', etc.
txn : bytes txn : bytes
The transaction to sign The transaction to sign
inputs : [dict] inputs : [dict]
The tx inputs. Should contain keys: The tx inputs. Should contain keys:
is_witness, bool - whether this is a segwit input One of these is required:
input_tx, bytes - The prior transaction which created the utxo of this input
satoshi, int - The satoshi amount of this input - can be used in place of
'input_tx' for a tx with a single segwit input
These are only required if signing this input: These are only required if signing this input:
is_witness, bool - whether this is a segwit input
script, bytes- the redeem script script, bytes- the redeem script
path, [int] - the bip32 path to sign with path, [int] - the bip32 path to sign with
One of these is required: This is optional if signing this input:
input_tx, bytes - The prior transaction which created the utxo of this input sighash, int - The sighash to use, defaults to 0x01 (SIGHASH_ALL)
satoshi, int - The satoshi amount of this input - can be used in place of
'input_tx' for a tx with a single segwit input
These are only required for Anti-Exfil signatures: These are only required for Anti-Exfil signatures:
ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures
@ -1325,9 +1625,11 @@ class JadeAPI:
change : [dict] change : [dict]
An array sized for the number of outputs. An array sized for the number of outputs.
Outputs which are not change should have a 'null' placeholder element. Outputs which are not to this wallet should have a 'null' placeholder element.
Change elements with data will be automatically verified by Jade, and not by the user. The output scripts for the elements with data will be verified by Jade.
Populated elements should contain sufficient data to generate the change address. Unless the element also contains 'is_change': False, these outputs will automatically
be approved and not be verified by the user.
Populated elements should contain sufficient data to generate the wallet address.
See `get_receive_address()` See `get_receive_address()`
use_ae_signatures : bool use_ae_signatures : bool
@ -1362,6 +1664,50 @@ class JadeAPI:
# Send inputs and receive signatures # Send inputs and receive signatures
return self._send_tx_inputs(base_id, inputs, use_ae_signatures) return self._send_tx_inputs(base_id, inputs, use_ae_signatures)
def sign_psbt(self, network, psbt):
"""
RPC call to sign a passed psbt as required
Parameters
----------
network : str
Network to which the txn should apply - eg. 'mainnet', 'testnet', etc.
psbt : bytes
The psbt formatted as bytes
Returns
-------
bytes
The psbt, updated with any signatures required from the hw signer
"""
# Send PSBT message
params = {'network': network, 'psbt': psbt}
msgid = str(random.randint(100000, 999999))
request = self.jade.build_request(msgid, 'sign_psbt', params)
self.jade.write_request(request)
# Read replies until we have them all, collate data and return.
# NOTE: we send 'get_extended_data' messages to request more 'chunks' of the reply data.
psbt_out = bytearray()
while True:
reply = self.jade.read_response()
self.jade.validate_reply(request, reply)
psbt_out.extend(self._get_result_or_raise_error(reply))
if 'seqnum' not in reply or reply['seqnum'] == reply['seqlen']:
break
newid = str(random.randint(100000, 999999))
params = {'origid': msgid,
'orig': 'sign_psbt',
'seqnum': reply['seqnum'] + 1,
'seqlen': reply['seqlen']}
request = self.jade.build_request(newid, 'get_extended_data', params)
self.jade.write_request(request)
return psbt_out
class JadeInterface: class JadeInterface:
""" """
@ -1394,9 +1740,9 @@ class JadeInterface:
def __exit__(self, exc_type, exc, tb): def __exit__(self, exc_type, exc, tb):
if (exc_type): if (exc_type):
logger.error("Exception causing JadeInterface context exit.") logger.info("Exception causing JadeInterface context exit.")
logger.error(exc_type) logger.info(exc_type)
logger.error(exc) logger.info(exc)
traceback.print_tb(tb) traceback.print_tb(tb)
self.disconnect(exc_type is not None) self.disconnect(exc_type is not None)
@ -1422,14 +1768,14 @@ class JadeInterface:
Returns Returns
------- -------
JadeInterface JadeInterface
Interface object configured to use given serial parameters. Inerface object configured to use given serial parameters.
NOTE: the instance has not yet tried to contact the hw NOTE: the instance has not yet tried to contact the hw
- caller must call 'connect()' before trying to use the Jade. - caller must call 'connect()' before trying to use the Jade.
""" """
if device and JadeTCPImpl.isSupportedDevice(device): if device and JadeTCPImpl.isSupportedDevice(device):
impl = JadeTCPImpl(device) impl = JadeTCPImpl(device, timeout or DEFAULT_SERIAL_TIMEOUT)
else: else:
impl = JadeSerialImpl(device or DEFAULT_SERIAL_DEVICE, impl = JadeSerialImpl(device,
baud or DEFAULT_BAUD_RATE, baud or DEFAULT_BAUD_RATE,
timeout or DEFAULT_SERIAL_TIMEOUT) timeout or DEFAULT_SERIAL_TIMEOUT)
return JadeInterface(impl) return JadeInterface(impl)
@ -1464,7 +1810,7 @@ class JadeInterface:
Returns Returns
------- -------
JadeInterface JadeInterface
Interface object configured to use given BLE parameters. Inerface object configured to use given BLE parameters.
NOTE: the instance has not yet tried to contact the hw NOTE: the instance has not yet tried to contact the hw
- caller must call 'connect()' before trying to use the Jade. - caller must call 'connect()' before trying to use the Jade.
@ -1510,7 +1856,7 @@ class JadeInterface:
Log any/all outstanding messages/data. Log any/all outstanding messages/data.
NOTE: can run indefinitely if data is arriving constantly. NOTE: can run indefinitely if data is arriving constantly.
""" """
logger.warn("Draining interface...") logger.warning("Draining interface...")
drained = bytearray() drained = bytearray()
finished = False finished = False
@ -1521,14 +1867,14 @@ class JadeInterface:
if finished or byte_ == b'\n' or len(drained) > 256: if finished or byte_ == b'\n' or len(drained) > 256:
try: try:
device_logger.warn(drained.decode('utf-8')) device_logger.warning(drained.decode('utf-8'))
except Exception as e: except Exception as e:
# Dump the bytes raw and as hex if decoding as utf-8 failed # Dump the bytes raw and as hex if decoding as utf-8 failed
device_logger.warn("Raw:") device_logger.warning("Raw:")
device_logger.warn(drained) device_logger.warning(drained)
device_logger.warn("----") device_logger.warning("----")
device_logger.warn("Hex dump:") device_logger.warning("Hex dump:")
device_logger.warn(drained.hex()) device_logger.warning(drained.hex())
# Clear and loop to continue collecting # Clear and loop to continue collecting
drained.clear() drained.clear()
@ -1662,7 +2008,7 @@ class JadeInterface:
response = message['log'].decode("utf-8") response = message['log'].decode("utf-8")
log_methods = { log_methods = {
'E': device_logger.error, 'E': device_logger.error,
'W': device_logger.warn, 'W': device_logger.warning,
'I': device_logger.info, 'I': device_logger.info,
'D': device_logger.debug, 'D': device_logger.debug,
'V': device_logger.debug, 'V': device_logger.debug,
@ -1718,7 +2064,7 @@ class JadeInterface:
def make_rpc_call(self, request, long_timeout=False): def make_rpc_call(self, request, long_timeout=False):
""" """
Method to send a request over the underlying interface, and await a response. Method to send a request over the underlying interface, and await a response.
The request is minimally validated before it is sent, and the response is similarly The request is minimally validated before it is sent, and the response is simialrly
validated before being returned. validated before being returned.
Any read-timeout is respected unless 'long_timeout' is passed, in which case the call Any read-timeout is respected unless 'long_timeout' is passed, in which case the call
blocks indefinitely awaiting a response. blocks indefinitely awaiting a response.

22
electrum/plugins/jade/jadepy/jade_serial.py

@ -1,8 +1,9 @@
import serial import serial
import logging import logging
from serial.tools import list_ports
logger = logging.getLogger('jade.serial') logger = logging.getLogger(__name__)
# #
@ -19,8 +20,25 @@ logger = logging.getLogger('jade.serial')
# (caveat cranium) # (caveat cranium)
# #
class JadeSerialImpl: class JadeSerialImpl:
# Used when searching for devices that might be a Jade/compatible hw
JADE_DEVICE_IDS = [
(0x10c4, 0xea60), (0x1a86, 0x55d4), (0x0403, 0x6001),
(0x1a86, 0x7523), (0x303a, 0x4001), (0x303a, 0x1001)]
@classmethod
def _get_first_compatible_device(cls):
jades = []
for devinfo in list_ports.comports():
if (devinfo.vid, devinfo.pid) in cls.JADE_DEVICE_IDS:
jades.append(devinfo.device)
if len(jades) > 1:
logger.warning(f'Multiple potential jade devices detected: {jades}')
return jades[0] if jades else None
def __init__(self, device, baud, timeout): def __init__(self, device, baud, timeout):
self.device = device self.device = device or self._get_first_compatible_device()
self.baud = baud self.baud = baud
self.timeout = timeout self.timeout = timeout
self.ser = None self.ser = None

11
electrum/plugins/jade/jadepy/jade_tcp.py

@ -2,7 +2,7 @@ import socket
import logging import logging
logger = logging.getLogger('jade.tcp') logger = logging.getLogger(__name__)
# #
@ -25,9 +25,10 @@ class JadeTCPImpl:
def isSupportedDevice(cls, device): def isSupportedDevice(cls, device):
return device is not None and device.startswith(cls.PROTOCOL_PREFIX) return device is not None and device.startswith(cls.PROTOCOL_PREFIX)
def __init__(self, device): def __init__(self, device, timeout):
assert self.isSupportedDevice(device) assert self.isSupportedDevice(device)
self.device = device self.device = device
self.timeout = timeout
self.tcp_sock = None self.tcp_sock = None
def connect(self): def connect(self):
@ -36,6 +37,7 @@ class JadeTCPImpl:
logger.info('Connecting to {}'.format(self.device)) logger.info('Connecting to {}'.format(self.device))
self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_sock.settimeout(self.timeout)
url = self.device[len(self.PROTOCOL_PREFIX):].split(':') url = self.device[len(self.PROTOCOL_PREFIX):].split(':')
self.tcp_sock.connect((url[0], int(url[1]))) self.tcp_sock.connect((url[0], int(url[1])))
@ -57,4 +59,7 @@ class JadeTCPImpl:
def read(self, n): def read(self, n):
assert self.tcp_sock is not None assert self.tcp_sock is not None
return self.tcp_sock.recv(n) buf = self.tcp_sock.recv(n)
while len(buf) < n:
buf += self.tcp_sock.recv(n - len(buf))
return buf

Loading…
Cancel
Save