Skip to content

Commit

Permalink
bitbox02: add bitbox02 simulator and tests
Browse files Browse the repository at this point in the history
Signed-off-by: asi345 <[email protected]>
  • Loading branch information
asi345 committed Sep 5, 2024
1 parent 8195c62 commit 8b76ed6
Show file tree
Hide file tree
Showing 16 changed files with 318 additions and 60 deletions.
7 changes: 7 additions & 0 deletions .github/actions/install-sim/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ runs:
apt-get install -y libusb-1.0-0
tar -xvf mcu.tar.gz
- if: inputs.device == 'bitbox02'
shell: bash
run: |
apt-get update
apt-get install -y libusb-1.0-0 docker.io
tar -xvf bitbox02.tar.gz
- if: inputs.device == 'jade'
shell: bash
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ jobs:
- { name: 'jade', archive: 'jade', paths: 'test/work/jade/simulator' }
- { name: 'ledger', archive: 'speculos', paths: 'test/work/speculos' }
- { name: 'keepkey', archive: 'keepkey-firmware', paths: 'test/work/keepkey-firmware/bin' }
- { name: 'bitbox02', archive: 'bitbox02', paths: 'test/work/bitbox02-firmware/build-build/bin/simulator' }

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -219,6 +220,7 @@ jobs:
- 'ledger'
- 'ledger-legacy'
- 'keepkey'
- 'bitbox02'
script:
- name: 'Wheel'
install: 'pip install dist/*.whl'
Expand Down Expand Up @@ -289,6 +291,7 @@ jobs:
- 'ledger'
- 'ledger-legacy'
- 'keepkey'
- 'bitbox02'
interface: [ 'library', 'cli', 'stdin' ]

container: python:${{ matrix.python-version }}
Expand Down
9 changes: 9 additions & 0 deletions ci/build_bitbox02.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
docker volume rm bitbox02_volume || true
docker volume create bitbox02_volume
CONTAINER_VERSION=$(curl https://raw.githubusercontent.com/BitBoxSwiss/bitbox02-firmware/master/.containerversion)
docker pull shiftcrypto/firmware_v2:$CONTAINER_VERSION
docker run -i --rm -v bitbox02_volume:/bitbox02-firmware shiftcrypto/firmware_v2:$CONTAINER_VERSION bash -c \
"cd /bitbox02-firmware && \
git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git . && \
git config --global --add safe.directory ./ && \
make -j simulator"
5 changes: 5 additions & 0 deletions ci/cirrus.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ RUN protoc --version
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
# docker run -it --entrypoint /bin/bash hwi_test
# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only
# For BitBox02:
# docker build -f ci/cirrus.Dockerfile -t hwi_test .
# ./ci/build_bitbox02.sh
# docker run -it -v bitbox02_volume:/test/work/bitbox02-firmware --name hwi --entrypoint /bin/bash hwi_test
# cd test; poetry run ./run_tests.py --bitbox02 --interface=cli --device-only
####################

####################
Expand Down
113 changes: 70 additions & 43 deletions hwilib/devices/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import base64
import builtins
import sys
import socket
from functools import wraps

from .._base58 import decode_check, encode_check
Expand Down Expand Up @@ -79,6 +80,8 @@
BitBoxNoiseConfig,
)

SIMULATOR_PATH = "127.0.0.1:15423"

class BitBox02Error(UnavailableActionError):
def __init__(self, msg: str):
"""
Expand Down Expand Up @@ -178,10 +181,13 @@ def enumerate(password: Optional[str] = None, expert: bool = False, chain: Chain
Enumerate all BitBox02 devices. Bootloaders excluded.
"""
result = []
for device_info in devices.get_any_bitbox02s():
path = device_info["path"].decode()
client = Bitbox02Client(path)
client.set_noise_config(SilentNoiseConfig())
devs = [device_info["path"].decode() for device_info in devices.get_any_bitbox02s()]
if allow_emulators:
devs.append(SIMULATOR_PATH)
for path in devs:
client = Bitbox02Client(path=path)
if path != SIMULATOR_PATH:
client.set_noise_config(SilentNoiseConfig())
d_data: Dict[str, object] = {}
bb02 = None
with handle_errors(common_err_msgs["enumerate"], d_data):
Expand Down Expand Up @@ -252,9 +258,27 @@ def func(*args, **kwargs): # type: ignore
raise exc
except FirmwareVersionOutdatedException as exc:
raise DeviceNotReadyError(str(exc))
except ValueError as e:
raise BadArgumentError(str(e))

return cast(T, func)

class BitBox02Simulator():
def __init__(self) -> None:
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ip, port = SIMULATOR_PATH.split(":")
self.client_socket.connect((ip, int(port)))

def write(self, data: bytes) -> None:
# Messages from client are always prefixed with HID report ID(0x00), which is not expected by the simulator.
self.client_socket.send(data[1:])

def read(self, size: int, timeout_ms: int) -> bytes:
res = self.client_socket.recv(64)
return res

def close(self) -> None:
self.client_socket.close()

# This class extends the HardwareWalletClient for BitBox02 specific things
class Bitbox02Client(HardwareWalletClient):
Expand All @@ -267,56 +291,55 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal
"The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock."
)
super().__init__(path, password=password, expert=expert, chain=chain)

hid_device = hid.device()
hid_device.open_path(path.encode())
self.transport = u2fhid.U2FHid(hid_device)
simulator = None
self.noise_config: BitBoxNoiseConfig = BitBoxNoiseConfig()

if path != SIMULATOR_PATH:
hid_device = hid.device()
hid_device.open_path(path.encode())
self.transport = u2fhid.U2FHid(hid_device)
self.noise_config = CLINoiseConfig()
else:
simulator = BitBox02Simulator()
self.transport = u2fhid.U2FHid(simulator)
self.device_path = path

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

self.noise_config: BitBoxNoiseConfig = CLINoiseConfig()

def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None:
self.noise_config = noise_config

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

for device_info in devices.get_any_bitbox02s():
if device_info["path"].decode() != self.device_path:
continue

bb02 = bitbox02.BitBox02(
transport=self.transport,
device_info=device_info,
noise_config=self.noise_config,
)
try:
bb02.check_min_version()
except FirmwareVersionOutdatedException as exc:
sys.stderr.write("WARNING: {}\n".format(exc))
raise
self.bb02 = bb02
is_initialized = bb02.device_info()["initialized"]
if expect_initialized is not None:
if expect_initialized:
if not is_initialized:
raise HWWError(
"The BitBox02 must be initialized first.",
DEVICE_NOT_INITIALIZED,
)
elif is_initialized:
raise UnavailableActionError(
"The BitBox02 must be wiped before setup."
bb02 = bitbox02.BitBox02(
transport=self.transport,
# Passing None as device_info means the device will be queried for the relevant device info.
device_info=None,
noise_config=self.noise_config,
)
try:
bb02.check_min_version()
except FirmwareVersionOutdatedException as exc:
sys.stderr.write("WARNING: {}\n".format(exc))
raise
self.bb02 = bb02
is_initialized = bb02.device_info()["initialized"]
if expect_initialized is not None:
if expect_initialized:
if not is_initialized:
raise HWWError(
"The BitBox02 must be initialized first.",
DEVICE_NOT_INITIALIZED,
)
elif is_initialized:
raise UnavailableActionError(
"The BitBox02 must be wiped before setup."
)

return bb02
raise Exception(
"Could not find the hid device info for path {}".format(self.device_path)
)
return bb02

def close(self) -> None:
self.transport.close()
Expand Down Expand Up @@ -883,9 +906,13 @@ def setup_device(

if label:
bb02.set_device_name(label)
if not bb02.set_password():
return False
return bb02.create_backup()
if self.device_path != SIMULATOR_PATH:
if not bb02.set_password():
return False
return bb02.create_backup()
else:
bb02.restore_from_mnemonic()
return True

@bitbox02_exception
def wipe_device(self) -> bool:
Expand Down
45 changes: 40 additions & 5 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ It also tests usage with `bitcoind`.
- `test_jade.py` tests the command line interface and Blockstream Jade implementation.
It uses the [Espressif fork of the Qemu emulator](https://github.com/espressif/qemu.git).
It also tests usage with `bitcoind`.
- `test_bitbox02.py` tests the command line interface and the BitBox02 implementation.
It uses the [BitBox02 simulator](https://github.com/BitBoxSwiss/bitbox02-firmware/tree/master/test/simulator).
It also tests usage with `bitcoind`.

`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, the Jade emulator, and `bitcoind`.
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.
`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`.
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.
In order to build each simulator/emulator, you will need to use command line arguments.
These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, and `--bitcoind`.
These are `--trezor-1`, `--trezor-t`, `--coldcard`, `--keepkey`, `--bitbox01`, `--jade`, `--bitbox02` and `--bitcoind`.
If an environment variable is not present or not set, then the simulator/emulator or bitcoind that it guards will not be built.

`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.
`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.
Otherwise the paths to those will need to be specified on the command line.
`test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.py`, and `test/test_digitalbitbox.py` can be disabled.
`test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, `test_jade.py`, `test_bitbox02.py` and `test/test_digitalbitbox.py` can be disabled.

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/`.

Expand Down Expand Up @@ -329,6 +332,38 @@ You also have to install its python dependencies
pip install -r requirements.txt
```

## BitBox02 Simulator

### Dependencies

In order to build the BitBox02 simulator, the following packages will need to be installed:

```
apt install docker.io
```

### Building

Clone the repository:

```
git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git
```

Pull the BitBox02 firmware Docker image:

```
cd bitbox02-firmware
make dockerpull
```

Build the simulator:

```
make dockerdev
make simulator
```


## Bitcoin Core

Expand Down
14 changes: 12 additions & 2 deletions test/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from test_digitalbitbox import digitalbitbox_test_suite
from test_keepkey import keepkey_test_suite
from test_jade import jade_test_suite
from test_bitbox02 import bitbox02_test_suite
from test_udevrules import TestUdevRulesInstaller

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

dbb_group = parser.add_mutually_exclusive_group()
dbb_group.add_argument('--no_bitbox02', dest='bitbox02', help='Do not run BitBox02 test with simulator', action='store_false')
dbb_group.add_argument('--bitbox02', dest='bitbox02', help='Run BitBox02 test with simulator', action='store_true')

parser.add_argument('--trezor-1-path', dest='trezor_1_path', help='Path to Trezor 1 emulator', default='work/trezor-firmware/legacy/firmware/trezor.elf')
parser.add_argument('--trezor-t-path', dest='trezor_t_path', help='Path to Trezor T emulator', default='work/trezor-firmware/core/emu.sh')
parser.add_argument('--coldcard-path', dest='coldcard_path', help='Path to Coldcar simulator', default='work/firmware/unix/headless.py')
parser.add_argument('--keepkey-path', dest='keepkey_path', help='Path to Keepkey emulator', default='work/keepkey-firmware/bin/kkemu')
parser.add_argument('--bitbox01-path', dest='bitbox01_path', help='Path to Digital Bitbox simulator', default='work/mcu/build/bin/simulator')
parser.add_argument('--ledger-path', dest='ledger_path', help='Path to Ledger emulator', default='work/speculos/speculos.py')
parser.add_argument('--jade-path', dest='jade_path', help='Path to Jade qemu emulator', default='work/jade/simulator')
parser.add_argument('--bitbox02-path', dest='bitbox02_path', help='Path to BitBox02 simulator', default='work/bitbox02-firmware/build-build/bin/simulator')

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

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

parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None)
parser.set_defaults(trezor_1=None, trezor_t=None, coldcard=None, keepkey=None, bitbox01=None, ledger=None, ledger_legacy=None, jade=None, bitbox02=None)

args = parser.parse_args()

Expand All @@ -92,6 +98,7 @@
args.ledger = True if args.ledger is None else args.ledger
args.ledger_legacy = True if args.ledger_legacy is None else args.ledger_legacy
args.jade = True if args.jade is None else args.jade
args.bitbox02 = True if args.bitbox02 is None else args.bitbox02
else:
# Default all false unless overridden
args.trezor_1 = False if args.trezor_1 is None else args.trezor_1
Expand All @@ -102,8 +109,9 @@
args.ledger = False if args.ledger is None else args.ledger
args.ledger_legacy = False if args.ledger_legacy is None else args.ledger_legacy
args.jade = False if args.jade is None else args.jade
args.bitbox02 = False if args.bitbox02 is None else args.bitbox02

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:
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:
# Start bitcoind
bitcoind = Bitcoind.create(args.bitcoind)

Expand All @@ -123,5 +131,7 @@
success &= ledger_test_suite(args.ledger_path, bitcoind, args.interface, True)
if success and args.jade:
success &= jade_test_suite(args.jade_path, bitcoind, args.interface)
if success and args.bitbox02:
success &= bitbox02_test_suite(args.bitbox02_path, bitcoind, args.interface)

sys.exit(not success)
Loading

0 comments on commit 8b76ed6

Please sign in to comment.