Skip to content

Commit db17fed

Browse files
committed
Add tests for Jade hww using qemu emulator
1 parent 44d556e commit db17fed

File tree

7 files changed

+404
-31
lines changed

7 files changed

+404
-31
lines changed

.cirrus.yml

+23
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ device_matrix_template: &DEVICE_MATRIX_TEMPLATE
5454
- tar -xvf "mcu.tar.gz"
5555
- wget -nv "https://api.cirrus-ci.com/v1/artifact/build/$CIRRUS_BUILD_ID/bitcoind_builder/bitcoin/bitcoin.tar.gz"
5656
- tar -xvf "bitcoin.tar.gz"
57+
- env:
58+
DEVICE: --jade
59+
depends_on:
60+
- Jade Sim Builder
61+
- dist_builder
62+
- bitcoind_builder
63+
fetch_sim_script:
64+
- wget -nv "https://api.cirrus-ci.com/v1/artifact/build/$CIRRUS_BUILD_ID/Jade Sim Builder/sim/jade.tar.gz"
65+
- tar -xvf "jade.tar.gz"
66+
- wget -nv "https://api.cirrus-ci.com/v1/artifact/build/$CIRRUS_BUILD_ID/bitcoind_builder/bitcoin/bitcoin.tar.gz"
67+
- tar -xvf "bitcoin.tar.gz"
5768
- env:
5869
DEVICE: --ledger
5970
depends_on:
@@ -186,6 +197,18 @@ task:
186197
sim_artifacts:
187198
path: "mcu.tar.gz"
188199

200+
task:
201+
env:
202+
DEVICE: --jade
203+
name: Jade Sim Builder
204+
sim_work_cache:
205+
folder: test/work/jade
206+
build_script:
207+
- cd test; ./setup_environment.sh $DEVICE; cd ..
208+
- tar -czf jade.tar.gz test/work/jade/simulator
209+
sim_artifacts:
210+
path: "jade.tar.gz"
211+
189212
task:
190213
env:
191214
DEVICE: --ledger

ci/cirrus.Dockerfile

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ RUN apt-get install -y \
2424
libdb-dev \
2525
libdb++-dev \
2626
libevent-dev \
27+
libgcrypt20-dev \
2728
libnewlib-arm-none-eabi \
2829
libsdl2-dev \
2930
libsdl2-image-dev \
3031
libssl-dev \
3132
libtool \
3233
libudev-dev \
3334
libusb-1.0-0-dev \
35+
ninja-build \
3436
pkg-config \
3537
protobuf-compiler \
3638
qemu-user-static
@@ -48,7 +50,7 @@ ENV PATH="/root/.cargo/bin:$PATH"
4850
# as needed.
4951
# e.g.,
5052
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
51-
# docker run -it --entrypoint /bin/bash hwi_tst
53+
# docker run -it --entrypoint /bin/bash hwi_test
5254
# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only
5355
####################
5456

@@ -68,6 +70,7 @@ ENV PATH="/root/.cargo/bin:$PATH"
6870
#RUN cd test; ./setup_environment.sh --bitbox01
6971
#RUN cd test; ./setup_environment.sh --ledger
7072
#RUN cd test; ./setup_environment.sh --keepkey
73+
#RUN cd test; ./setup_environment.sh --jade
7174
#RUN cd test; ./setup_environment.sh --bitcoind
7275
#
7376
## Once everything has been built, put rest of files in place

hwilib/devices/jade.py

+51-24
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@
5454
parse_multisig
5555
)
5656

57+
import logging
5758
import os
5859

60+
# The test emulator port
61+
SIMULATOR_PATH = 'tcp:127.0.0.1:2222'
62+
5963
JADE_DEVICE_IDS = [(0x10c4, 0xea60)]
6064
HAS_NETWORKING = hasattr(jade, '_http_request')
6165

@@ -127,20 +131,27 @@ def __init__(self, path: str, password: str = '', expert: bool = False, timeout:
127131
self.jade = JadeAPI.create_serial(path, timeout=timeout)
128132
self.jade.connect()
129133

130-
# Push some host entropy into jade
131-
self.jade.add_entropy(os.urandom(32))
134+
verinfo = self.jade.get_version_info()
135+
uninitialized = verinfo['JADE_STATE'] not in ['READY', 'TEMP']
132136

133-
# Ensure Jade unlocked
134-
authenticated = False
135-
while not authenticated:
136-
if not HAS_NETWORKING and self.jade.get_version_info()['JADE_STATE'] not in ['READY', 'TEMP']:
137-
# Wallet not initialised nor do we have networking dependencies
137+
if path == SIMULATOR_PATH:
138+
if uninitialized:
139+
# Connected to simulator but it appears to have no wallet set
140+
raise DeviceNotReadyError('Use JadeAPI.set_[seed|mnemonic] to set simulator wallet')
141+
else:
142+
if uninitialized and not HAS_NETWORKING:
143+
# Wallet not initialised/unlocked nor do we have networking dependencies
138144
# User must use 'Emergency Restore' feature to enter mnemonic on Jade hw
139145
raise DeviceNotReadyError('Use "Emergency Restore" feature on Jade hw to enter wallet mnemonic')
140146

147+
# Push some host entropy into jade
148+
self.jade.add_entropy(os.urandom(32))
149+
141150
# Authenticate the user - this may require a PIN and pinserver interaction
142151
# (if we have required networking dependencies)
143-
authenticated = self.jade.auth_user(self._network())
152+
authenticated = False
153+
while not authenticated:
154+
authenticated = self.jade.auth_user(self._network())
144155

145156
# Retrieves the public key at the specified BIP 32 derivation path
146157
@jade_exception
@@ -477,26 +488,42 @@ def toggle_passphrase(self) -> bool:
477488
def enumerate(password: str = '') -> List[Dict[str, Any]]:
478489
results = []
479490

491+
def _get_device_entry(device_model: str, device_path: str) -> Dict[str, Any]:
492+
d_data: Dict[str, Any] = {}
493+
d_data['type'] = 'jade'
494+
d_data['model'] = device_model
495+
d_data['path'] = device_path
496+
d_data['needs_pin_sent'] = False
497+
d_data['needs_passphrase_sent'] = False
498+
499+
client = None
500+
with handle_errors(common_err_msgs['enumerate'], d_data):
501+
client = JadeClient(device_path, password, timeout=1)
502+
d_data['fingerprint'] = client.get_master_fingerprint().hex()
503+
504+
if client:
505+
client.close()
506+
507+
return d_data
508+
480509
# Jade is not really an HID device, it shows as a serial/com port device.
481510
# Scan com ports looking for the relevant vid and pid, and use 'path' to
482511
# hold the path to the serial port device, eg. /dev/ttyUSB0
483512
for devinfo in list_ports.comports():
484513
if (devinfo.vid, devinfo.pid) in JADE_DEVICE_IDS:
485-
d_data: Dict[str, Any] = {}
486-
d_data['type'] = 'jade'
487-
d_data['model'] = 'jade'
488-
d_data['path'] = devinfo.device
489-
d_data['needs_pin_sent'] = False
490-
d_data['needs_passphrase_sent'] = False
491-
492-
client = None
493-
with handle_errors(common_err_msgs['enumerate'], d_data):
494-
client = JadeClient(devinfo.device, password, timeout=1)
495-
d_data['fingerprint'] = client.get_master_fingerprint().hex()
496-
497-
if client:
498-
client.close()
499-
500-
results.append(d_data)
514+
results.append(_get_device_entry('jade', devinfo.device))
515+
516+
# If we can connect to the simulator, add it too
517+
try:
518+
with JadeAPI.create_serial(SIMULATOR_PATH, timeout=1) as jade:
519+
verinfo = jade.get_version_info()
520+
521+
if verinfo is not None:
522+
results.append(_get_device_entry('jade_simulator', SIMULATOR_PATH))
523+
524+
except Exception as e:
525+
# If we get any sort of error do not add the simulator
526+
logging.debug(f'Failed to connect to Jade simulator at {SIMULATOR_PATH}')
527+
logging.debug(e)
501528

502529
return results

test/run_tests.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from test_ledger import ledger_test_suite
1616
from test_digitalbitbox import digitalbitbox_test_suite
1717
from test_keepkey import keepkey_test_suite
18+
from test_jade import jade_test_suite
1819
from test_udevrules import TestUdevRulesInstaller
1920

2021
parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests')
@@ -38,6 +39,10 @@
3839
keepkey_group.add_argument('--no-keepkey', dest='keepkey', help='Do not run Keepkey test with emulator', action='store_false')
3940
keepkey_group.add_argument('--keepkey', dest='keepkey', help='Run Keepkey test with emulator', action='store_true')
4041

42+
jade_group = parser.add_mutually_exclusive_group()
43+
jade_group.add_argument('--no-jade', dest='jade', help='Do not run Jade test with emulator', action='store_false')
44+
jade_group.add_argument('--jade', dest='jade', help='Run Jade test with emulator', action='store_true')
45+
4146
dbb_group = parser.add_mutually_exclusive_group()
4247
dbb_group.add_argument('--no_bitbox01', dest='bitbox01', help='Do not run Digital Bitbox test with simulator', action='store_false')
4348
dbb_group.add_argument('--bitbox01', dest='bitbox01', help='Run Digital Bitbox test with simulator', action='store_true')
@@ -48,14 +53,15 @@
4853
parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu')
4954
parser.add_argument('--bitbox01-path', dest='bitbox01_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator')
5055
parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py')
56+
parser.add_argument('--jade-path', dest='jade_path', help='Path to Jade qemu emulator', default='work/jade/simulator')
5157

5258
parser.add_argument('--all', help='Run tests on all existing simulators', default=False, action='store_true')
5359
parser.add_argument('--bitcoind', help='Path to bitcoind', default='work/bitcoin/src/bitcoind')
5460
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist', 'stdin'], default='library')
5561

5662
parser.add_argument("--device-only", help="Only run device tests", action="store_true")
5763

58-
parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None)
64+
parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, jade=None)
5965

6066
args = parser.parse_args()
6167

@@ -80,6 +86,7 @@
8086
args.keepkey = True if args.keepkey is None else args.keepkey
8187
args.bitbox01 = True if args.bitbox01 is None else args.bitbox01
8288
args.ledger = True if args.ledger is None else args.ledger
89+
args.jade = True if args.jade is None else args.jade
8390
else:
8491
# Default all false unless overridden
8592
args.trezor_1 = False if args.trezor_1 is None else args.trezor_1
@@ -88,8 +95,9 @@
8895
args.keepkey = False if args.keepkey is None else args.keepkey
8996
args.bitbox01 = False if args.bitbox01 is None else args.bitbox01
9097
args.ledger = False if args.ledger is None else args.ledger
98+
args.jade = False if args.jade is None else args.jade
9199

92-
if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox01:
100+
if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.keepkey or args.bitbox01 or args.jade:
93101
# Start bitcoind
94102
rpc, userpass = start_bitcoind(args.bitcoind)
95103

@@ -105,5 +113,7 @@
105113
success &= keepkey_test_suite(args.keepkey_path, rpc, userpass, args.interface)
106114
if success and args.ledger:
107115
success &= ledger_test_suite(args.ledger_path, rpc, userpass, args.interface)
116+
if success and args.jade:
117+
success &= jade_test_suite(args.jade_path, rpc, userpass, args.interface)
108118

109119
sys.exit(not success)

test/setup_environment.sh

+100
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ while [[ $# -gt 0 ]]; do
2626
build_keepkey=1
2727
shift
2828
;;
29+
--jade)
30+
build_jade=1
31+
shift
32+
;;
2933
--bitcoind)
3034
build_bitcoind=1
3135
shift
@@ -37,6 +41,7 @@ while [[ $# -gt 0 ]]; do
3741
build_bitbox01=1
3842
build_ledger=1
3943
build_keepkey=1
44+
build_jade=1
4045
build_bitcoind=1
4146
shift
4247
;;
@@ -258,6 +263,101 @@ if [[ -n ${build_ledger} ]]; then
258263
cd ..
259264
fi
260265

266+
if [[ -n ${build_jade} ]]; then
267+
mkdir -p jade
268+
cd jade
269+
270+
# Clone Blockstream Jade firmware if it doesn't exist, or update it if it does
271+
if [ ! -d "jade" ]; then
272+
git clone --recursive --branch master https://github.com/Blockstream/Jade.git ./jade
273+
cd jade
274+
else
275+
cd jade
276+
git fetch
277+
278+
# Determine if we need to pull. From https://stackoverflow.com/a/3278427
279+
UPSTREAM=${1:-'@{u}'}
280+
LOCAL=$(git rev-parse @)
281+
REMOTE=$(git rev-parse "$UPSTREAM")
282+
BASE=$(git merge-base @ "$UPSTREAM")
283+
284+
if [ $LOCAL = $REMOTE ]; then
285+
echo "Up-to-date"
286+
elif [ $LOCAL = $BASE ]; then
287+
git pull
288+
fi
289+
git submodule update --recursive --init
290+
fi
291+
292+
# Deduce the relevant versions of esp-idf and qemu to use
293+
ESP_IDF_BRANCH=$(grep "ARG ESP_IDF_BRANCH=" Dockerfile | cut -d\= -f2)
294+
ESP_IDF_COMMIT=$(grep "ARG ESP_IDF_COMMIT=" Dockerfile | cut -d\= -f2)
295+
ESP_QEMU_BRANCH=$(grep "ARG ESP_QEMU_BRANCH=" Dockerfile | cut -d\= -f2)
296+
ESP_QEMU_COMMIT=$(grep "ARG ESP_QEMU_COMMIT=" Dockerfile | cut -d\= -f2)
297+
cd ..
298+
299+
# Build the qemu emulator
300+
if [ ! -d "qemu" ]; then
301+
git clone --depth 1 --branch ${ESP_QEMU_BRANCH} --single-branch --recursive https://github.com/espressif/qemu.git ./qemu
302+
cd qemu
303+
./configure --target-list=xtensa-softmmu \
304+
--enable-gcrypt \
305+
--enable-debug --enable-sanitizers \
306+
--disable-strip --disable-user \
307+
--disable-capstone --disable-vnc \
308+
--disable-sdl --disable-gtk
309+
else
310+
cd qemu
311+
git fetch
312+
fi
313+
git checkout ${ESP_QEMU_COMMIT}
314+
git submodule update --recursive --init
315+
ninja -C build
316+
cd ..
317+
318+
# Build the esp-idf toolchain
319+
if [ ! -d "esp-idf" ]; then
320+
git clone --depth=1 --branch ${ESP_IDF_BRANCH} --single-branch --recursive https://github.com/espressif/esp-idf.git ./esp-idf
321+
cd esp-idf
322+
else
323+
cd esp-idf
324+
git fetch
325+
fi
326+
git checkout ${ESP_IDF_COMMIT}
327+
git submodule update --recursive --init
328+
329+
# Install the isp-idf tools in a given location (otherwise defauts to user home dir)
330+
IDF_TOOLS_PATH=$(pwd)/tools
331+
./install.sh
332+
. ./export.sh
333+
cd ..
334+
335+
# Build Blockstream Jade firmware configured for the emulator
336+
cd jade
337+
rm -fr sdkconfig
338+
cp configs/sdkconfig_qemu.defaults sdkconfig.defaults
339+
idf.py all
340+
341+
# Make the qemu flash image
342+
esptool.py --chip esp32 merge_bin --fill-flash-size 4MB -o main/qemu/flash_image.bin \
343+
--flash_mode dio --flash_freq 40m --flash_size 4MB \
344+
0x9000 build/partition_table/partition-table.bin \
345+
0xe000 build/ota_data_initial.bin \
346+
0x1000 build/bootloader/bootloader.bin \
347+
0x10000 build/jade.bin
348+
cd ..
349+
350+
# Extract the minimal artifacts required to run the emulator
351+
rm -fr simulator
352+
mkdir simulator
353+
cp qemu/build/qemu-system-xtensa simulator/
354+
cp -R qemu/pc-bios simulator/
355+
cp jade/main/qemu/flash_image.bin simulator/
356+
cp jade/main/qemu/qemu_efuse.bin simulator/
357+
358+
cd ..
359+
fi
360+
261361
if [[ -n ${build_bitcoind} ]]; then
262362
# Clone bitcoind if it doesn't exist, or update it if it does
263363
bitcoind_setup_needed=false

test/test_device.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
SUPPORTS_MS_DISPLAY = {'trezor_1', 'keepkey', 'coldcard', 'trezor_t'}
2121
SUPPORTS_XPUB_MS_DISPLAY = {'trezor_1', 'trezor_t'}
2222
SUPPORTS_UNSORTED_MS = {"trezor_1", "trezor_t"}
23-
SUPPORTS_MIXED = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t'}
24-
SUPPORTS_MULTISIG = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'}
25-
SUPPORTS_EXTERNAL = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t'}
26-
SUPPORTS_OP_RETURN = {'ledger', 'digitalbitbox', 'trezor_1', 'trezor_t', 'keepkey'}
23+
SUPPORTS_MIXED = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey', 'trezor_t', 'jade'}
24+
SUPPORTS_MULTISIG = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t', 'jade'}
25+
SUPPORTS_EXTERNAL = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey', 'coldcard', 'trezor_t', 'jade'}
26+
SUPPORTS_OP_RETURN = {'ledger', 'digitalbitbox', 'trezor_1', 'trezor_t', 'keepkey', 'jade'}
2727

2828
# Class for emulator control
2929
class DeviceEmulator():

0 commit comments

Comments
 (0)