Skip to content

Commit abc4ac8

Browse files
committed
bitbox02: add bitbox02 simulator and tests
Signed-off-by: asi345 <inanata15@gmail.com>
1 parent 000e5c5 commit abc4ac8

16 files changed

+337
-60
lines changed

.github/actions/install-sim/action.yml

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ runs:
3838
apt-get install -y libusb-1.0-0
3939
tar -xvf mcu.tar.gz
4040
41+
- if: inputs.device == 'bitbox02'
42+
shell: bash
43+
run: |
44+
apt-get update
45+
apt-get install -y libusb-1.0-0 docker.io
46+
tar -xvf bitbox02.tar.gz
47+
4148
- if: inputs.device == 'jade'
4249
shell: bash
4350
run: |

.github/workflows/ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ jobs:
153153
- { name: 'jade', archive: 'jade', paths: 'test/work/jade/simulator' }
154154
- { name: 'ledger', archive: 'speculos', paths: 'test/work/speculos' }
155155
- { name: 'keepkey', archive: 'keepkey-firmware', paths: 'test/work/keepkey-firmware/bin' }
156+
- { name: 'bitbox02', archive: 'bitbox02', paths: 'test/work/bitbox02-firmware/build-build/bin/simulator' }
156157

157158
steps:
158159
- uses: actions/checkout@v4
@@ -219,6 +220,7 @@ jobs:
219220
- 'ledger'
220221
- 'ledger-legacy'
221222
- 'keepkey'
223+
- 'bitbox02'
222224
script:
223225
- name: 'Wheel'
224226
install: 'pip install dist/*.whl'
@@ -289,6 +291,7 @@ jobs:
289291
- 'ledger'
290292
- 'ledger-legacy'
291293
- 'keepkey'
294+
- 'bitbox02'
292295
interface: [ 'library', 'cli', 'stdin' ]
293296

294297
container: python:${{ matrix.python-version }}

ci/build_bitbox02.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
docker volume rm bitbox02_volume || true
2+
docker volume create bitbox02_volume
3+
CONTAINER_VERSION=$(curl https://raw.githubusercontent.com/BitBoxSwiss/bitbox02-firmware/master/.containerversion)
4+
docker pull shiftcrypto/firmware_v2:$CONTAINER_VERSION
5+
docker run -i --rm -v bitbox02_volume:/bitbox02-firmware shiftcrypto/firmware_v2:$CONTAINER_VERSION bash -c \
6+
"cd /bitbox02-firmware && \
7+
git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git . && \
8+
git config --global --add safe.directory ./ && \
9+
make -j simulator"

ci/cirrus.Dockerfile

+5
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ RUN protoc --version
5959
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
6060
# docker run -it --entrypoint /bin/bash hwi_test
6161
# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only
62+
# For BitBox02:
63+
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
64+
# ./ci/build_bitbox02.sh
65+
# docker run -it -v bitbox02_volume:/test/work/bitbox02-firmware --name hwi --entrypoint /bin/bash hwi_test
66+
# cd test; poetry run ./run_tests.py --bitbox02 --interface=cli --device-only
6267
####################
6368

6469
####################

hwilib/devices/bitbox02.py

+89-43
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import base64
2020
import builtins
2121
import sys
22+
import socket
2223
from functools import wraps
2324

2425
from .._base58 import decode_check, encode_check
@@ -79,6 +80,8 @@
7980
BitBoxNoiseConfig,
8081
)
8182

83+
SIMULATOR_PATH = "127.0.0.1:15423"
84+
8285
class BitBox02Error(UnavailableActionError):
8386
def __init__(self, msg: str):
8487
"""
@@ -178,10 +181,15 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
178181
Enumerate all BitBox02 devices. Bootloaders excluded.
179182
"""
180183
result = []
181-
for device_info in devices.get_any_bitbox02s():
182-
path = device_info["path"].decode()
183-
client = Bitbox02Client(path)
184-
client.set_noise_config(SilentNoiseConfig())
184+
devs = [device_info["path"].decode() for device_info in devices.get_any_bitbox02s()]
185+
if allow_emulators:
186+
devs.append(SIMULATOR_PATH)
187+
for path in devs:
188+
client = Bitbox02Client(path=path)
189+
if allow_emulators and client.simulator and not client.simulator.connected:
190+
continue
191+
if path != SIMULATOR_PATH:
192+
client.set_noise_config(SilentNoiseConfig())
185193
d_data: Dict[str, object] = {}
186194
bb02 = None
187195
with handle_errors(common_err_msgs["enumerate"], d_data):
@@ -252,9 +260,31 @@ def func(*args, **kwargs): # type: ignore
252260
raise exc
253261
except FirmwareVersionOutdatedException as exc:
254262
raise DeviceNotReadyError(str(exc))
263+
except ValueError as e:
264+
raise BadArgumentError(str(e))
255265

256266
return cast(T, func)
257267

268+
class BitBox02Simulator():
269+
def __init__(self) -> None:
270+
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
271+
ip, port = SIMULATOR_PATH.split(":")
272+
self.connected = True
273+
try:
274+
self.client_socket.connect((ip, int(port)))
275+
except:
276+
self.connected = False
277+
278+
def write(self, data: bytes) -> None:
279+
# Messages from client are always prefixed with HID report ID(0x00), which is not expected by the simulator.
280+
self.client_socket.send(data[1:])
281+
282+
def read(self, size: int, timeout_ms: int) -> bytes:
283+
res = self.client_socket.recv(64)
284+
return res
285+
286+
def close(self) -> None:
287+
self.client_socket.close()
258288

259289
# This class extends the HardwareWalletClient for BitBox02 specific things
260290
class Bitbox02Client(HardwareWalletClient):
@@ -267,56 +297,68 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal
267297
"The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock."
268298
)
269299
super().__init__(path, password=password, expert=expert, chain=chain)
270-
271-
hid_device = hid.device()
272-
hid_device.open_path(path.encode())
273-
self.transport = u2fhid.U2FHid(hid_device)
300+
<<<<<<< HEAD
301+
self.simulator = None
302+
=======
303+
simulator = None
304+
>>>>>>> 8d5339f (bitbox02: add bitbox02 simulator and tests)
305+
self.noise_config: BitBoxNoiseConfig = BitBoxNoiseConfig()
306+
307+
if path != SIMULATOR_PATH:
308+
hid_device = hid.device()
309+
hid_device.open_path(path.encode())
310+
self.transport = u2fhid.U2FHid(hid_device)
311+
self.noise_config = CLINoiseConfig()
312+
else:
313+
<<<<<<< HEAD
314+
self.simulator = BitBox02Simulator()
315+
if self.simulator.connected:
316+
self.transport = u2fhid.U2FHid(self.simulator)
317+
=======
318+
simulator = BitBox02Simulator()
319+
if simulator.connected:
320+
self.transport = u2fhid.U2FHid(simulator)
321+
else:
322+
self.transport = None
323+
>>>>>>> 8d5339f (bitbox02: add bitbox02 simulator and tests)
274324
self.device_path = path
275325

276326
# use self.init() to access self.bb02.
277327
self.bb02: Optional[bitbox02.BitBox02] = None
278328

279-
self.noise_config: BitBoxNoiseConfig = CLINoiseConfig()
280-
281329
def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None:
282330
self.noise_config = noise_config
283331

284332
def init(self, expect_initialized: Optional[bool] = True) -> bitbox02.BitBox02:
285333
if self.bb02 is not None:
286334
return self.bb02
287335

288-
for device_info in devices.get_any_bitbox02s():
289-
if device_info["path"].decode() != self.device_path:
290-
continue
291-
292-
bb02 = bitbox02.BitBox02(
293-
transport=self.transport,
294-
device_info=device_info,
295-
noise_config=self.noise_config,
296-
)
297-
try:
298-
bb02.check_min_version()
299-
except FirmwareVersionOutdatedException as exc:
300-
sys.stderr.write("WARNING: {}\n".format(exc))
301-
raise
302-
self.bb02 = bb02
303-
is_initialized = bb02.device_info()["initialized"]
304-
if expect_initialized is not None:
305-
if expect_initialized:
306-
if not is_initialized:
307-
raise HWWError(
308-
"The BitBox02 must be initialized first.",
309-
DEVICE_NOT_INITIALIZED,
310-
)
311-
elif is_initialized:
312-
raise UnavailableActionError(
313-
"The BitBox02 must be wiped before setup."
336+
bb02 = bitbox02.BitBox02(
337+
transport=self.transport,
338+
# Passing None as device_info means the device will be queried for the relevant device info.
339+
device_info=None,
340+
noise_config=self.noise_config,
341+
)
342+
try:
343+
bb02.check_min_version()
344+
except FirmwareVersionOutdatedException as exc:
345+
sys.stderr.write("WARNING: {}\n".format(exc))
346+
raise
347+
self.bb02 = bb02
348+
is_initialized = bb02.device_info()["initialized"]
349+
if expect_initialized is not None:
350+
if expect_initialized:
351+
if not is_initialized:
352+
raise HWWError(
353+
"The BitBox02 must be initialized first.",
354+
DEVICE_NOT_INITIALIZED,
314355
)
356+
elif is_initialized:
357+
raise UnavailableActionError(
358+
"The BitBox02 must be wiped before setup."
359+
)
315360

316-
return bb02
317-
raise Exception(
318-
"Could not find the hid device info for path {}".format(self.device_path)
319-
)
361+
return bb02
320362

321363
def close(self) -> None:
322364
self.transport.close()
@@ -883,9 +925,13 @@ def setup_device(
883925

884926
if label:
885927
bb02.set_device_name(label)
886-
if not bb02.set_password():
887-
return False
888-
return bb02.create_backup()
928+
if self.device_path != SIMULATOR_PATH:
929+
if not bb02.set_password():
930+
return False
931+
return bb02.create_backup()
932+
else:
933+
bb02.restore_from_mnemonic()
934+
return True
889935

890936
@bitbox02_exception
891937
def wipe_device(self) -> bool:

test/README.md

+40-5
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,19 @@ It also tests usage with `bitcoind`.
2020
- `test_jade.py` tests the command line interface and Blockstream Jade implementation.
2121
It uses the [Espressif fork of the Qemu emulator](https://github.com/espressif/qemu.git).
2222
It also tests usage with `bitcoind`.
23+
- `test_bitbox02.py` tests the command line interface and the BitBox02 implementation.
24+
It uses the [BitBox02 simulator](https://github.com/BitBoxSwiss/bitbox02-firmware/tree/master/test/simulator).
25+
It also tests usage with `bitcoind`.
2326

24-
`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, and `bitcoind`.
25-
if run in the `test/` directory, these will be built in `work/test/trezor-firmware`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, and `work/test/bitcoin` respectively.
27+
`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, the BitBox02 simulator and `bitcoind`.
28+
if run in the `test/` directory, these will be built in `work/test/trezor-firmware`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, `work/test/bitbox02-firmware` and `work/test/bitcoin` respectively.
2629
In order to build each simulator/emulator, you will need to use command line arguments.
27-
These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, and `--bitcoind`.
30+
These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, `--bitbox02` and `--bitcoind`.
2831
If an environment variable is not present or not set, then the simulator/emulator or bitcoind that it guards will not be built.
2932

30-
`run_tests.py` runs the tests. If run from the `test/` directory, it will be able to find the Trezor emulator, Coldcard simulator, Keepkey emulator, Digital Bitbox simulator, Jade emulator, and bitcoind.
33+
`run_tests.py` runs the tests. If run from the `test/` directory, it will be able to find the Trezor emulator, Coldcard simulator, Keepkey emulator, Digital Bitbox simulator, Jade emulator, BitBox02 simulator and bitcoind.
3134
Otherwise the paths to those will need to be specified on the command line.
32-
`test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.py`, and `test/test_digitalbitbox.py` can be disabled.
35+
`test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.py`, `test_bitbox02.py` and `test/test_digitalbitbox.py` can be disabled.
3336

3437
If you are building the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Jade emulator, the Digital Bitbox simulator, and `bitcoind` without `setup_environment.sh`, then you will need to make `work/` inside of `test/`.
3538

@@ -329,6 +332,38 @@ You also have to install its python dependencies
329332
pip install -r requirements.txt
330333
```
331334

335+
## BitBox02 Simulator
336+
337+
### Dependencies
338+
339+
In order to build the BitBox02 simulator, the following packages will need to be installed:
340+
341+
```
342+
apt install docker.io
343+
```
344+
345+
### Building
346+
347+
Clone the repository:
348+
349+
```
350+
git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git
351+
```
352+
353+
Pull the BitBox02 firmware Docker image:
354+
355+
```
356+
cd bitbox02-firmware
357+
make dockerpull
358+
```
359+
360+
Build the simulator:
361+
362+
```
363+
make dockerdev
364+
make simulator
365+
```
366+
332367

333368
## Bitcoin Core
334369

test/run_tests.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from test_digitalbitbox import digitalbitbox_test_suite
1717
from test_keepkey import keepkey_test_suite
1818
from test_jade import jade_test_suite
19+
from test_bitbox02 import bitbox02_test_suite
1920
from test_udevrules import TestUdevRulesInstaller
2021

2122
parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests')
@@ -51,21 +52,26 @@
5152
dbb_group.add_argument('--no_bitbox01', dest='bitbox01', help='Do not run Digital Bitbox test with simulator', action='store_false')
5253
dbb_group.add_argument('--bitbox01', dest='bitbox01', help='Run Digital Bitbox test with simulator', action='store_true')
5354

55+
dbb_group = parser.add_mutually_exclusive_group()
56+
dbb_group.add_argument('--no_bitbox02', dest='bitbox02', help='Do not run BitBox02 test with simulator', action='store_false')
57+
dbb_group.add_argument('--bitbox02', dest='bitbox02', help='Run BitBox02 test with simulator', action='store_true')
58+
5459
parser.add_argument('--trezor-1-path', dest='trezor_1_path', help='Path to Trezor 1 emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf')
5560
parser.add_argument('--trezor-t-path', dest='trezor_t_path', help='Path to Trezor T emulator', default='work/trezor-firmware/core/emu.sh')
5661
parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcard simulator', default='work/firmware/unix/simulator.py')
5762
parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu')
5863
parser.add_argument('--bitbox01-path', dest='bitbox01_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator')
5964
parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py')
6065
parser.add_argument('--jade-path', dest='jade_path', help='Path to Jade qemu emulator', default='work/jade/simulator')
66+
parser.add_argument('--bitbox02-path', dest='bitbox02_path', help='Path to BitBox02 simulator', default='work/bitbox02-firmware/build-build/bin/simulator')
6167

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

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

68-
parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None)
74+
parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None)
6975

7076
args = parser.parse_args()
7177

@@ -92,6 +98,7 @@
9298
args.ledger = True if args.ledger is None else args.ledger
9399
args.ledger_legacy = True if args.ledger_legacy is None else args.ledger_legacy
94100
args.jade = True if args.jade is None else args.jade
101+
args.bitbox02 = True if args.bitbox02 is None else args.bitbox02
95102
else:
96103
# Default all false unless overridden
97104
args.trezor_1 = False if args.trezor_1 is None else args.trezor_1
@@ -102,8 +109,9 @@
102109
args.ledger = False if args.ledger is None else args.ledger
103110
args.ledger_legacy = False if args.ledger_legacy is None else args.ledger_legacy
104111
args.jade = False if args.jade is None else args.jade
112+
args.bitbox02 = False if args.bitbox02 is None else args.bitbox02
105113

106-
if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade:
114+
if args.trezor_1 or args.trezor_t or args.coldcard or args.ledger or args.ledger_legacy or args.keepkey or args.bitbox01 or args.jade or args.bitbox02:
107115
# Start bitcoind
108116
bitcoind = Bitcoind.create(args.bitcoind)
109117

@@ -123,5 +131,7 @@
123131
success &= ledger_test_suite(args.ledger_path, bitcoind, args.interface, True)
124132
if success and args.jade:
125133
success &= jade_test_suite(args.jade_path, bitcoind, args.interface)
134+
if success and args.bitbox02:
135+
success &= bitbox02_test_suite(args.bitbox02_path, bitcoind, args.interface)
126136

127137
sys.exit(not success)

0 commit comments

Comments
 (0)