Browse Source

jade: update Jade api to 1.0.29

Updates cbor dependency to cbor2, required for modern linux/python
versions.
master
Jamie C. Driver 2 years ago
parent
commit
17508086aa
No known key found for this signature in database
GPG Key ID: FBB9327247889D30
  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
btchip-python==0.1.32 \
--hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55
cbor==1.0.0 \
--hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6
cbor2==5.4.6 \
--hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7
certifi==2024.2.2 \
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1

2
contrib/requirements/requirements-hw.txt

@ -24,7 +24,7 @@ ckcc-protocol>=0.7.7
bitbox02>=6.2.0
# device plugin: jade
cbor>=1.0.0,<2.0.0
cbor2>=5.4.6,<6.0.0
pyserial>=3.5.0,<4.0.0
# 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 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.

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

@ -1,4 +1,4 @@
import cbor
import cbor2 as cbor
import hashlib
import json
import time
@ -17,20 +17,19 @@ from .jade_serial import JadeSerialImpl
from .jade_tcp import JadeTCPImpl
# 'jade' logger
logger = logging.getLogger('jade')
device_logger = logging.getLogger('jade-device')
logger = logging.getLogger(__name__)
device_logger = logging.getLogger(f'{__name__}-device')
# BLE comms backend is optional
# It relies on the BLE dependencies being available
try:
from .jade_ble import JadeBleImpl
except ImportError as e:
logger.warn(e)
logger.warn('BLE scanning/connectivity will not be available')
logger.warning(e)
logger.warning('BLE scanning/connectivity will not be available')
# Default serial connection
DEFAULT_SERIAL_DEVICE = '/dev/ttyUSB0'
DEFAULT_BAUD_RATE = 115200
DEFAULT_SERIAL_TIMEOUT = 120
@ -77,7 +76,7 @@ def _hexlify(data):
# The default implementation used in JadeAPI._jadeRpc() below.
# 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
# ----------
@ -94,13 +93,18 @@ def _hexlify(data):
#
# # Use the first non-onion url
# url = [url for url in params['urls'] if not url.endswith('.onion')][0]
#
# if params['method'] == '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':
# 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))
#
# if f.status_code != 200:
@ -109,13 +113,15 @@ def _hexlify(data):
#
# assert params['accept'] == 'json'
# f = f.json()
# except Exception as e:
# logging.error(e)
# f = None
#
# return {'body': f}
#
# except ImportError as e:
# logger.warn(e)
# logger.warn('Default _http_requests() function will not be available')
#
# logger.info(e)
# logger.info('Default _http_requests() function will not be available')
class JadeAPI:
"""
@ -144,9 +150,9 @@ class JadeAPI:
def __exit__(self, exc_type, exc, tb):
if (exc_type):
logger.error("Exception causing JadeAPI context exit.")
logger.error(exc_type)
logger.error(exc)
logger.info("Exception causing JadeAPI context exit.")
logger.info(exc_type)
logger.info(exc)
traceback.print_tb(tb)
self.disconnect(exc_type is not None)
@ -340,16 +346,41 @@ class JadeAPI:
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.
Parameters
----------
nonblocking : bool
If True message will be handled immediately (see also ping()) *experimental feature*
Returns
-------
dict
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):
"""
@ -387,7 +418,20 @@ class JadeAPI:
params = {'epoch': epoch if epoch is not None else int(time.time())}
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.
@ -403,6 +447,12 @@ class JadeAPI:
and ack'd by the hw unit.
The maximum supported chunk size is given in the version info data, under the key
'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
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.
@ -434,6 +484,9 @@ class JadeAPI:
'cmpsize': cmplen,
'cmphash': cmphash}
if fwhash is not None:
params['fwhash'] = fwhash
if patchlen is not None:
ota_method = 'ota_delta'
params['patchsize'] = patchlen
@ -464,11 +517,53 @@ class JadeAPI:
Returns
-------
bool
True on success.
int
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)
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):
"""
RPC call to clean/reset memory and storage, as much as is practical.
@ -529,6 +624,38 @@ class JadeAPI:
params = {'seed': seed}
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):
"""
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')
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,
master_blinding_key=None):
"""
@ -748,8 +924,59 @@ class JadeAPI:
'master_blinding_key': master_blinding_key}}
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,
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.
The call has three forms.
@ -795,6 +1022,16 @@ class JadeAPI:
multisig_name : str
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
-------
str
@ -805,6 +1042,10 @@ class JadeAPI:
assert len(args) == 2
keys = ['network', 'paths', '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:
assert len(args) == 2
keys = ['network', 'path', 'variant']
@ -865,6 +1106,26 @@ class JadeAPI:
params = {'path': path, 'message': message}
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):
"""
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}
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.
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
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
-------
32-bytes
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):
"""
@ -1034,26 +1305,22 @@ class JadeAPI:
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.
Normally the blinding factors are generated and returned in the `get_commitments` call,
but for the last output the vbf must be generated on the host, so this call allows the
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.
RPC call to get deterministic blinding factors to blind an output.
Predicated on the host calculating the 'hash_prevouts' value correctly.
Can fetch abf, vbf, or both together.
Parameters
----------
hash_prevouts : 32-bytes
This value is computed as specified in bip143.
It is verified immediately since at this point Jade doesn't have the tx in question.
It will be checked later during `sign_liquid_tx()`.
This value should be computed by the host as specified in bip143.
It is not verified by Jade, since at this point Jade does not have the tx in question.
output_index : int
The index of the output we are trying to blind
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
The name of any registered multisig wallet for which to fetch the blinding factor.
@ -1061,8 +1328,9 @@ class JadeAPI:
Returns
-------
32-bytes
The requested blinding factor
32-bytes or 64-bytes
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,
'output_index': output_index,
@ -1109,7 +1377,7 @@ class JadeAPI:
Returns
-------
dict
Containing the following the blinding factors and output commitments.
Containing the blinding factors and output commitments.
"""
params = {'asset_id': asset_id,
'value': value,
@ -1161,7 +1429,7 @@ class JadeAPI:
host_ae_entropy_values = []
for txinput in inputs:
# 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))
base_id += 1
@ -1193,6 +1461,9 @@ class JadeAPI:
# Send all n inputs
requests = []
for txinput in inputs:
if txinput is None:
txinput = {}
base_id += 1
msg_id = str(base_id)
request = self.jade.build_request(msg_id, 'tx_input', txinput)
@ -1212,31 +1483,44 @@ class JadeAPI:
return signatures
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.
Parameters
----------
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
The transaction to sign
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
value_commitment, 33-bytes - The value commitment of ths input
These are only required if signing this input:
script, bytes- the redeem script
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:
ae_host_commitment, 32-bytes - The host-commitment 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]
An array sized for the number of outputs.
Unblinded outputs should have a 'null' placeholder element.
@ -1246,16 +1530,18 @@ class JadeAPI:
change : [dict]
An array sized for the number of outputs.
Outputs which are not change should have a 'null' placeholder element.
Change elements with data will be automatically verified by Jade, and not by the user.
Populated elements should contain sufficient data to generate the change address.
Outputs which are not to this wallet should have a 'null' placeholder element.
The output scripts for the elements with data will be verified by Jade.
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()`
use_ae_signatures : bool, optional
Whether to use the anti-exfil protocol to generate the signatures.
Defaults to False.
asset_info : [dict]
asset_info : [dict], optional
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.
At the very least must contain 'asset_id', 'contract' and 'issuance_prevout' items,
@ -1263,6 +1549,17 @@ class JadeAPI:
not required.
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
-------
1. if use_ae_signatures is False
@ -1286,7 +1583,8 @@ class JadeAPI:
'trusted_commitments': commitments,
'use_ae_signatures': use_ae_signatures,
'change': change,
'asset_info': asset_info}
'asset_info': asset_info,
'additional_info': additional_info}
reply = self._jadeRpc('sign_liquid_tx', params, str(base_id))
assert reply
@ -1301,23 +1599,25 @@ class JadeAPI:
Parameters
----------
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
The transaction to sign
inputs : [dict]
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:
is_witness, bool - whether this is a segwit input
script, bytes- the redeem script
path, [int] - the bip32 path to sign with
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
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:
ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures
@ -1325,9 +1625,11 @@ class JadeAPI:
change : [dict]
An array sized for the number of outputs.
Outputs which are not change should have a 'null' placeholder element.
Change elements with data will be automatically verified by Jade, and not by the user.
Populated elements should contain sufficient data to generate the change address.
Outputs which are not to this wallet should have a 'null' placeholder element.
The output scripts for the elements with data will be verified by Jade.
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()`
use_ae_signatures : bool
@ -1362,6 +1664,50 @@ class JadeAPI:
# Send inputs and receive 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:
"""
@ -1394,9 +1740,9 @@ class JadeInterface:
def __exit__(self, exc_type, exc, tb):
if (exc_type):
logger.error("Exception causing JadeInterface context exit.")
logger.error(exc_type)
logger.error(exc)
logger.info("Exception causing JadeInterface context exit.")
logger.info(exc_type)
logger.info(exc)
traceback.print_tb(tb)
self.disconnect(exc_type is not None)
@ -1422,14 +1768,14 @@ class JadeInterface:
Returns
-------
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
- caller must call 'connect()' before trying to use the Jade.
"""
if device and JadeTCPImpl.isSupportedDevice(device):
impl = JadeTCPImpl(device)
impl = JadeTCPImpl(device, timeout or DEFAULT_SERIAL_TIMEOUT)
else:
impl = JadeSerialImpl(device or DEFAULT_SERIAL_DEVICE,
impl = JadeSerialImpl(device,
baud or DEFAULT_BAUD_RATE,
timeout or DEFAULT_SERIAL_TIMEOUT)
return JadeInterface(impl)
@ -1464,7 +1810,7 @@ class JadeInterface:
Returns
-------
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
- caller must call 'connect()' before trying to use the Jade.
@ -1510,7 +1856,7 @@ class JadeInterface:
Log any/all outstanding messages/data.
NOTE: can run indefinitely if data is arriving constantly.
"""
logger.warn("Draining interface...")
logger.warning("Draining interface...")
drained = bytearray()
finished = False
@ -1521,14 +1867,14 @@ class JadeInterface:
if finished or byte_ == b'\n' or len(drained) > 256:
try:
device_logger.warn(drained.decode('utf-8'))
device_logger.warning(drained.decode('utf-8'))
except Exception as e:
# Dump the bytes raw and as hex if decoding as utf-8 failed
device_logger.warn("Raw:")
device_logger.warn(drained)
device_logger.warn("----")
device_logger.warn("Hex dump:")
device_logger.warn(drained.hex())
device_logger.warning("Raw:")
device_logger.warning(drained)
device_logger.warning("----")
device_logger.warning("Hex dump:")
device_logger.warning(drained.hex())
# Clear and loop to continue collecting
drained.clear()
@ -1662,7 +2008,7 @@ class JadeInterface:
response = message['log'].decode("utf-8")
log_methods = {
'E': device_logger.error,
'W': device_logger.warn,
'W': device_logger.warning,
'I': device_logger.info,
'D': device_logger.debug,
'V': device_logger.debug,
@ -1718,7 +2064,7 @@ class JadeInterface:
def make_rpc_call(self, request, long_timeout=False):
"""
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.
Any read-timeout is respected unless 'long_timeout' is passed, in which case the call
blocks indefinitely awaiting a response.

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

@ -1,8 +1,9 @@
import serial
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)
#
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):
self.device = device
self.device = device or self._get_first_compatible_device()
self.baud = baud
self.timeout = timeout
self.ser = None

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

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