Skip to content

Commit 7a1d0c1

Browse files
committed
bitbox02: add bitbox02 simulator and tests
Signed-off-by: asi345 <inanata15@gmail.com>
1 parent 25d390b commit 7a1d0c1

16 files changed

+317
-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
@@ -218,6 +219,7 @@ jobs:
218219
- 'ledger'
219220
- 'ledger-legacy'
220221
- 'keepkey'
222+
- 'bitbox02'
221223
script:
222224
- name: 'Wheel'
223225
install: 'pip install dist/*.whl'
@@ -287,6 +289,7 @@ jobs:
287289
- 'ledger'
288290
- 'ledger-legacy'
289291
- 'keepkey'
292+
- 'bitbox02'
290293
interface: [ 'library', 'cli', 'stdin' ]
291294

292295
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_VERSIONs 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

+70-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,13 @@ 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 path != SIMULATOR_PATH:
190+
client.set_noise_config(SilentNoiseConfig())
185191
d_data: Dict[str, object] = {}
186192
bb02 = None
187193
with handle_errors(common_err_msgs["enumerate"], d_data):
@@ -252,9 +258,27 @@ def func(*args, **kwargs): # type: ignore
252258
raise exc
253259
except FirmwareVersionOutdatedException as exc:
254260
raise DeviceNotReadyError(str(exc))
261+
except ValueError as e:
262+
raise BadArgumentError(str(e))
255263

256264
return cast(T, func)
257265

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

259283
# This class extends the HardwareWalletClient for BitBox02 specific things
260284
class Bitbox02Client(HardwareWalletClient):
@@ -267,56 +291,55 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal
267291
"The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock."
268292
)
269293
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)
294+
simulator = None
295+
self.noise_config: BitBoxNoiseConfig = BitBoxNoiseConfig()
296+
297+
if path != SIMULATOR_PATH:
298+
hid_device = hid.device()
299+
hid_device.open_path(path.encode())
300+
self.transport = u2fhid.U2FHid(hid_device)
301+
self.noise_config = CLINoiseConfig()
302+
else:
303+
simulator = BitBox02Simulator()
304+
self.transport = u2fhid.U2FHid(simulator)
274305
self.device_path = path
275306

276307
# use self.init() to access self.bb02.
277308
self.bb02: Optional[bitbox02.BitBox02] = None
278309

279-
self.noise_config: BitBoxNoiseConfig = CLINoiseConfig()
280-
281310
def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None:
282311
self.noise_config = noise_config
283312

284313
def init(self, expect_initialized: Optional[bool] = True) -> bitbox02.BitBox02:
285314
if self.bb02 is not None:
286315
return self.bb02
287316

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."
317+
bb02 = bitbox02.BitBox02(
318+
transport=self.transport,
319+
# Passing None as device_info means the device will be queried for the relevant device info.
320+
device_info=None,
321+
noise_config=self.noise_config,
322+
)
323+
try:
324+
bb02.check_min_version()
325+
except FirmwareVersionOutdatedException as exc:
326+
sys.stderr.write("WARNING: {}\n".format(exc))
327+
raise
328+
self.bb02 = bb02
329+
is_initialized = bb02.device_info()["initialized"]
330+
if expect_initialized is not None:
331+
if expect_initialized:
332+
if not is_initialized:
333+
raise HWWError(
334+
"The BitBox02 must be initialized first.",
335+
DEVICE_NOT_INITIALIZED,
314336
)
337+
elif is_initialized:
338+
raise UnavailableActionError(
339+
"The BitBox02 must be wiped before setup."
340+
)
315341

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

321344
def close(self) -> None:
322345
self.transport.close()
@@ -883,9 +906,13 @@ def setup_device(
883906

884907
if label:
885908
bb02.set_device_name(label)
886-
if not bb02.set_password():
887-
return False
888-
return bb02.create_backup()
909+
if self.device_path != SIMULATOR_PATH:
910+
if not bb02.set_password():
911+
return False
912+
return bb02.create_backup()
913+
else:
914+
bb02.restore_from_mnemonic()
915+
return True
889916

890917
@bitbox02_exception
891918
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+
docker pull shiftcrypto/firmware_v2:latest
357+
```
358+
359+
Build the simulator:
360+
361+
```
362+
cd bitbox02-firmware
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 Coldcar simulator', default='work/firmware/unix/headless.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/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)