Skip to content

Commit 6b2271b

Browse files
jgriffithsLawrence Nahum
andcommitted
libjade: enable building the Jade firmware as a native library
Implements an in-process instance of the Jade firmware. This can be thought of as an emulated or virtual Jade device that runs in the address space of the application that links to it. This initial implementation is highly experimental and incomplete, and should under no circumstances be used beyond development and testing. Co-authored-by: Lawrence Nahum <[email protected]>
1 parent b529891 commit 6b2271b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2261
-32
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pins
22
pinsdir
33
build
4+
build_linux
45
sdkconfig
56
sdkconfig.defaults
67
sdkconfig.old

.gitlab-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ include:
2020
- gitlab/apidocs.yml
2121
- gitlab/test_fw.yml
2222
- gitlab/dev_fw.yml
23+
- gitlab/test_libjade.yml
2324
- gitlab/test.yml
2425
- gitlab/flash.yml
2526
- gitlab/release.yml

CMakeLists.txt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
cmake_minimum_required(VERSION 3.16)
2-
set(EXTRA_COMPONENT_DIRS bootloader_components/bootloader_support)
3-
4-
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
5-
idf_build_set_property(DEPENDENCIES_LOCK dependencies.lock.${IDF_TARGET})
6-
project(jade)
2+
if(DEFINED ESP_PLATFORM AND ESP_PLATFORM EQUAL 1)
3+
set(EXTRA_COMPONENT_DIRS bootloader_components/bootloader_support)
4+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
5+
idf_build_set_property(DEPENDENCIES_LOCK dependencies.lock.${IDF_TARGET})
6+
project(jade)
7+
else()
8+
add_subdirectory(libjade)
9+
endif()

format.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
set -eo pipefail
33

44
(cd main && clang-format -i *.c *.h */*.{c,h,inc})
5+
(cd libjade && clang-format -i *.c *.h */*.h */*/*.h)
56

67
clang-format -i tools/bip85_rsa_key_gen/main.c
78

gitlab/test_libjade.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
test_libjade:
2+
tags: [ ga ]
3+
stage: test
4+
script:
5+
- . $HOME/esp/esp-idf/export.sh
6+
- cp configs/sdkconfig_jade.defaults sdkconfig.defaults
7+
- idf.py reconfigure
8+
- source /venv/bin/activate
9+
- pip install -r requirements.txt
10+
- pip install -r pinserver/requirements.txt
11+
- pip install .
12+
- echo "----------------------- BUILD ---------------------------------"
13+
- python ./components/assets/gen_assets.py ./components/assets/asset_data.json ./build/esp-idf/assets/asset_data.inc
14+
- python ./components/assets/gen_assets.py ./components/assets/asset_data_testnet.json ./build/esp-idf/assets/asset_data_testnet.inc
15+
- ./make_libjade.sh Sanitize
16+
- export ASAN_OPTIONS=symbolize=1,detect_leaks=0
17+
- export UBSAN_OPTIONS=print_stacktrace=1
18+
- export ASAN_SO=/usr/lib/gcc/x86_64-linux-gnu/12/libasan.so
19+
- export LD_LIBRARY_PATH=$PWD/build_linux/libjade
20+
- export SOCKET_LINK=$PWD/tmp_socket
21+
- echo "----------------------- STANDARD TESTS ------------------------"
22+
- LD_PRELOAD=$ASAN_SO python ./test_jade.py --log CRITICAL --libjade
23+
- echo "----------------------- NON-LEGACY TESTS ----------------------"
24+
- LD_PRELOAD=$ASAN_SO python ./test_jade.py --log CRITICAL --libjade --nolegacyflow
25+
- echo "----------------------- SERIAL TESTS ----------------------"
26+
- LD_PRELOAD=$ASAN_SO setsid $PWD/build_linux/libjade/libjade_daemon --serialport $SOCKET_LINK >daemon.log 2>&1 &
27+
- DAEMON_PID=$!
28+
- LD_PRELOAD=$ASAN_SO python ./test_jade.py --log CRITICAL --nolegacyflow --serialport $SOCKET_LINK --serialtimeout 30
29+
- kill -- -$DAEMON_PID
30+
artifacts:
31+
expire_in: 2 days
32+
name: libjade_serial_log
33+
paths:
34+
- daemon.log

jadepy/jade.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
logger.warning(e)
3030
logger.warning('BLE scanning/connectivity will not be available')
3131

32+
# libjade in-process software emulation is optional.
33+
# It relies on the libjade.so shared library being in LD_LIBRARY_PATH.
34+
try:
35+
from .jade_sw import JadeSoftwareImpl
36+
except (ImportError, FileNotFoundError) as e:
37+
logger.debug(e)
38+
logger.debug('libjade Software Jade emulation will not be available')
39+
3240

3341
# Default serial connection
3442
DEFAULT_BAUD_RATE = 115200
@@ -39,6 +47,9 @@
3947
DEFAULT_BLE_SERIAL_NUMBER = None
4048
DEFAULT_BLE_SCAN_TIMEOUT = 60
4149

50+
# Default libjade connection
51+
DEFAULT_LIBJADE_TIMEOUT = 5
52+
4253

4354
def _hexlify(data):
4455
"""
@@ -255,6 +266,31 @@ def create_ble(device_name=None, serial_number=None,
255266
scan_timeout, loop)
256267
return JadeAPI(impl)
257268

269+
@staticmethod
270+
def create_libjade(timeout=None):
271+
"""
272+
Create a JadeAPI object using libjade (in-process software emulation).
273+
WARNING: libjade is BETA and should not be used with real funds.
274+
NOTE: raises JadeError if libjade dependencies not installed.
275+
276+
Parameters
277+
----------
278+
timeout : int, optional
279+
The read timeout when awaiting messages (Uses 5s if not given).
280+
281+
Returns
282+
-------
283+
JadeAPI
284+
API object configured to use libjade.
285+
NOTE: The caller must call 'connect()' before using the instance.
286+
287+
Raises
288+
------
289+
JadeError if libjade is not available (libjade.so not installed)
290+
"""
291+
impl = JadeInterface.create_libjade(timeout)
292+
return JadeAPI(impl)
293+
258294
def connect(self):
259295
"""
260296
Try to connect the underlying transport interface (eg. serial, ble, etc.)
@@ -1993,18 +2029,18 @@ def sign_psbt(self, network, psbt, additional_info=None):
19932029

19942030
class JadeInterface:
19952031
"""
1996-
Mid-level interface to Jade
1997-
Wraps either a serial or a ble connection
2032+
Mid-level interface to Jade.
2033+
Wraps either a serial or ble connection, or an in-process libjade instance.
19982034
Calls to send and receive bytes and cbor messages over the interface.
19992035
20002036
Either:
20012037
a) use wrapped with JadeAPI
20022038
(recommended)
20032039
or:
2004-
b) use with JadeInterface.create_[serial|ble]() as jade:
2040+
b) use with JadeInterface.create_[serial|ble|libjade]() as jade:
20052041
...
20062042
or:
2007-
c) use JadeInterface.create_[serial|ble], then call connect() before
2043+
c) use JadeInterface.create_[serial|ble|libjade], then call connect() before
20082044
using, and disconnect() when finished
20092045
(caveat cranium)
20102046
or:
@@ -2110,6 +2146,31 @@ def create_ble(device_name=None, serial_number=None,
21102146
loop=loop)
21112147
return JadeInterface(impl)
21122148

2149+
@staticmethod
2150+
def create_libjade(timeout=None):
2151+
"""
2152+
Create a JadeInterface object object using libjade (in-process software emulation).
2153+
WARNING: libjade is BETA and should not be used with real funds.
2154+
NOTE: raises JadeError if libjade dependencies not installed.
2155+
2156+
Parameters
2157+
----------
2158+
timeout : int, optional
2159+
The read timeout when awaiting messages (Uses 5s if not given).
2160+
2161+
Returns
2162+
-------
2163+
JadeInterface
2164+
Interface object configured to use the libjade.
2165+
NOTE: The caller must call 'connect()' before using the instance.
2166+
"""
2167+
this_module = sys.modules[__name__]
2168+
if not hasattr(this_module, "JadeSoftwareImpl"):
2169+
raise JadeError(1, "libjade support not installed", None)
2170+
2171+
impl = JadeSoftwareImpl(timeout or DEFAULT_LIBJADE_TIMEOUT)
2172+
return JadeInterface(impl)
2173+
21132174
def connect(self):
21142175
"""
21152176
Try to connect the underlying transport interface (eg. serial, ble, etc.)

jadepy/jade_serial.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ def connect(self):
6060
raise JadeError(1, 'Unable to open port', self.device)
6161

6262
# Ensure RTS and DTR are not set (as this can cause the hw to reboot)
63-
self.ser.setRTS(False)
64-
self.ser.setDTR(False)
63+
if self.device.startswith('/dev/tty'):
64+
self.ser.setRTS(False)
65+
self.ser.setDTR(False)
6566

6667
logger.info('Connected')
6768

@@ -70,8 +71,9 @@ def disconnect(self):
7071

7172
# Ensure RTS and DTR are not set (as this can cause the hw to reboot)
7273
# and then close the connection
73-
self.ser.setRTS(False)
74-
self.ser.setDTR(False)
74+
if self.device.startswith('/dev/tty'):
75+
self.ser.setRTS(False)
76+
self.ser.setDTR(False)
7577
self.ser.close()
7678

7779
# Reset state

jadepy/jade_sw.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from ctypes import CDLL, POINTER, c_ubyte, c_size_t, byref
2+
import logging
3+
from .jade_error import JadeError
4+
5+
6+
logger = logging.getLogger(__name__)
7+
8+
try:
9+
_libjade = CDLL('libjade.so')
10+
_libjade.libjade_receive.restype = POINTER(c_ubyte)
11+
except Exception as _:
12+
raise ImportError # libjade.so not available
13+
14+
15+
#
16+
# Experimental, internal, in-process interface to Jade
17+
# Intended for use via JadeInterface wrapper.
18+
#
19+
# Use via JadeInterface.create_libjade() (see JadeInterface)
20+
#
21+
class JadeSoftwareImpl:
22+
23+
_log_levels = {
24+
logging.DEBUG: 1,
25+
logging.INFO: 2,
26+
logging.WARNING: 3,
27+
logging.ERROR: 4,
28+
logging.CRITICAL: 5, # Note we have no critical logs
29+
logging.NOTSET: 5 # Default to critical (i.e. no logging)
30+
}
31+
32+
def __init__(self, timeout):
33+
self.timeout = timeout
34+
self.libjade = None
35+
self.msg = None # Bytes of the current message being read, if any
36+
37+
def connect(self):
38+
assert self.libjade is None
39+
self.libjade = _libjade
40+
# Respect the python log level for Jade logging
41+
log_level = self._log_levels[logger.getEffectiveLevel()]
42+
self.libjade.libjade_set_log_level(log_level)
43+
# Starts the firmware in a separate thread
44+
self.libjade.libjade_start()
45+
logger.info('Connected to in-process software Jade')
46+
47+
def disconnect(self):
48+
assert self.libjade is not None
49+
self.libjade.libjade_stop()
50+
self.libjade = None
51+
52+
def write(self, bytes_):
53+
assert self.libjade is not None
54+
if logger.isEnabledFor(logging.DEBUG):
55+
logger.debug(f'Pushing {bytes_.hex()}\n')
56+
# The software interface takes the whole message in one go
57+
num_bytes = len(bytes_)
58+
if not self.libjade.libjade_send(bytes_, num_bytes):
59+
raise JadeError(1, f'Failed to send {num_bytes} bytes', None)
60+
return num_bytes # Let the caller know we wrote all bytes
61+
62+
def read(self, n):
63+
assert self.libjade is not None
64+
if n == 0:
65+
# Sometimes read is called with 0 bytes.
66+
# Treat this as a no-op.
67+
logger.debug('Read of 0 bytes requested')
68+
return bytes()
69+
70+
if not self.msg:
71+
# The software interface reads the whole message in one go.
72+
# Fetch it here, then return it in chunks below
73+
logger.debug(f'Calling libjade_receive() with timeout {self.timeout}\n')
74+
bytes_len = c_size_t()
75+
buff = self.libjade.libjade_receive(self.timeout, byref(bytes_len))
76+
if not buff:
77+
logger.debug('Timeout calling libjade_receive()\n')
78+
return bytes()
79+
self.msg = bytes([buff[i] for i in range(bytes_len.value)])
80+
self.libjade.libjade_release(buff)
81+
if logger.isEnabledFor(logging.DEBUG):
82+
logger.debug(f'Received message {self.msg.hex()}\n')
83+
84+
# Return as much of the message as the caller asked for
85+
ret = self.msg[:n]
86+
self.msg = self.msg[n:]
87+
if False and logger.isEnabledFor(logging.DEBUG):
88+
# Not generally useful unless debugging serialization failures
89+
logger.debug(f'Returning {ret.hex()} leaving {self.msg.hex()}')
90+
elif not self.msg:
91+
logger.debug('Message consumed')
92+
return ret

jadepy/jade_tcp.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import socket
22
import logging
3+
import os
34

45

56
logger = logging.getLogger(__name__)
@@ -34,21 +35,35 @@ def __init__(self, device, timeout):
3435
def connect(self):
3536
assert self.isSupportedDevice(self.device)
3637
assert self.tcp_sock is None
37-
3838
logger.info(f'Connecting to {self.device}')
39-
self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
40-
self.tcp_sock.settimeout(self.timeout)
39+
conn_path = self.device[len(self.PROTOCOL_PREFIX):]
4140

42-
url = self.device[len(self.PROTOCOL_PREFIX):].split(':')
43-
self.tcp_sock.connect((url[0], int(url[1])))
44-
assert self.tcp_sock is not None
41+
is_unix_socket = False
42+
if conn_path.startswith('/') or os.path.exists(conn_path):
43+
is_unix_socket = True
44+
elif '/' in conn_path and ':' not in conn_path:
45+
is_unix_socket = True
4546

47+
if is_unix_socket:
48+
self.tcp_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
49+
self.tcp_sock.settimeout(self.timeout)
50+
self.tcp_sock.connect(conn_path)
51+
else:
52+
self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
53+
self.tcp_sock.settimeout(self.timeout)
54+
if ':' in conn_path:
55+
url = conn_path.split(':')
56+
self.tcp_sock.connect((url[0], int(url[1])))
57+
else:
58+
self.tcp_sock.connect((conn_path, 80))
59+
60+
assert self.tcp_sock is not None
4661
self.tcp_sock.__enter__()
4762
logger.info('Connected')
4863

4964
def disconnect(self):
5065
assert self.tcp_sock is not None
51-
self.tcp_sock.__exit__()
66+
self.tcp_sock.__exit__(None, None, None)
5267

5368
# Reset state
5469
self.tcp_sock = None

0 commit comments

Comments
 (0)