Skip to content

Commit

Permalink
Merge #105: Automated tests for Digital Bitbox
Browse files Browse the repository at this point in the history
0b870d8 Document how to build the simulator (Andrew Chow)
8f1dff2 Change Digital Bitbox test to use simulator (Andrew Chow)
07c9771 Build digital bitbox simulator in setup_environment.sh (Andrew Chow)
814d02d Implement support for communicating with a dbb simulator (Andrew Chow)

Pull request description:

  This PR changes the current manual Digital Bitbox tests into an automated test. This uses the [simulator that I wrote](https://github.com/achow101/mcu/blob/simulator/src/simulator.c) (which is also [PR'd](BitBoxSwiss/mcu#253) to Digital Bitbox).

  Built on #104

Tree-SHA512: 582afa5e045c1e222064958b1b9dda17db7025dd5ff3f8a64a826abe92631b0dd808fcded501bedec7fc1d8ef4c386c99212ff1b182d149e182ea472f65d92ea
  • Loading branch information
achow101 committed Jan 14, 2019
2 parents 7e71527 + 0b870d8 commit 486d2fa
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 40 deletions.
62 changes: 50 additions & 12 deletions hwilib/devices/digitalbitbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import binascii
import logging
import socket
import time

from ..hwwclient import HardwareWalletClient, NoPasswordError, UnavailableActionError
Expand Down Expand Up @@ -75,6 +76,25 @@ def to_string(x, enc):
else:
raise TypeError("Not a string or bytes like object")

class BitboxSimulator():
def __init__(self, ip, port):
self.ip = ip
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.connect((self.ip, self.port))
self.socket.settimeout(1)

def send_recv(self, msg):
self.socket.sendall(msg)
data = self.socket.recv(3584)
return data

def close(self):
self.socket.close()

def get_serial_number_string(self):
return 'dbb_fw:v5.0.0'

def send_frame(data, device):
data = bytearray(data)
data_len = len(data)
Expand Down Expand Up @@ -121,16 +141,19 @@ def get_firmware_version(device):
def send_plain(msg, device):
reply = ""
try:
firm_ver = get_firmware_version(device)
if (firm_ver[0] == 2 and firm_ver[1] == 0) or (firm_ver[0] == 1):
hidBufSize = 4096
device.write('\0' + msg + '\0' * (hidBufSize - len(msg)))
r = bytearray()
while len(r) < hidBufSize:
r += bytearray(self.dbb_hid.read(hidBufSize))
if isinstance(device, BitboxSimulator):
r = device.send_recv(msg)
else:
send_frame(msg, device)
r = read_frame(device)
firm_ver = get_firmware_version(device)
if (firm_ver[0] == 2 and firm_ver[1] == 0) or (firm_ver[0] == 1):
hidBufSize = 4096
device.write('\0' + msg + '\0' * (hidBufSize - len(msg)))
r = bytearray()
while len(r) < hidBufSize:
r += bytearray(self.dbb_hid.read(hidBufSize))
else:
send_frame(msg, device)
r = read_frame(device)
r = r.rstrip(b' \t\r\n\0')
r = r.replace(b"\0", b'')
r = to_string(r, 'utf8')
Expand Down Expand Up @@ -184,8 +207,14 @@ def __init__(self, path, password):
super(DigitalbitboxClient, self).__init__(path, password)
if not password:
raise NoPasswordError('Password must be supplied for digital BitBox')
self.device = hid.device()
self.device.open_path(path.encode())
if path.startswith('udp:'):
split_path = path.split(':')
ip = split_path[1]
port = int(split_path[2])
self.device = BitboxSimulator(ip, port)
else:
self.device = hid.device()
self.device.open_path(path.encode())
self.password = password

# Must return a dict with the xpub
Expand Down Expand Up @@ -441,7 +470,16 @@ def close(self):

def enumerate(password=''):
results = []
for d in hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID):
devices = hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID)
# Try connecting to simulator
try:
dev = BitboxSimulator('127.0.0.1', 35345)
res = dev.send_recv(b'{"device" : "info"}')
devices.append({'path': b'udp:127.0.0.1:35345', 'interface_number': 0})
dev.close()
except:
pass
for d in devices:
if ('interface_number' in d and d['interface_number'] == 0 \
or ('usage_page' in d and d['usage_page'] == 0xffff)):
d_data = {}
Expand Down
27 changes: 27 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,33 @@ $ make setup
$ make
```

## Bitbox Simulator

### Dependencies

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

```
build-essential git cmake
```

### Building

Clone the repository:

```
$ git clone https://github.com/achow101/mcu -b simulator
```

Build the simulator:

```
$ cd mcu
$ mkdir -p build && cd build
$ cmake .. -DBUILD_TYPE=simulator
$ make
```

## Bitcoin Core

In order to build `bitcoind`, see [Bitcoin Core's build documentation](https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md#linux-distribution-specific-instructions) to get all of the dependencies installed and for instructions on how to build.
14 changes: 6 additions & 8 deletions test/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,19 @@
keepkey_group = parser.add_mutually_exclusive_group()
keepkey_group.add_argument('--no_keepkey', help='Do not run Keepkey test with emulator', action='store_true')
keepkey_group.add_argument('--keepkey', help='Path to Keepkey emulator.', default='work/keepkey-firmware/bin/kkemu')
dbb_group = parser.add_mutually_exclusive_group()
dbb_group.add_argument('--no_bitbox', help='Do not run Digital Bitbox test with simulator', action='store_true')
dbb_group.add_argument('--bitbox', help='Path to Digital bitbox simulator.', default='work/mcu/build/bin/simulator')

parser.add_argument('--digitalbitbox', help='Run physical Digital Bitbox tests.', action='store_true')
parser.add_argument('--bitcoind', help='Path to bitcoind.', default='work/bitcoin/src/bitcoind')
parser.add_argument('--password', '-p', help='Device password')
args = parser.parse_args()

# Run tests
suite = unittest.TestSuite()
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress))
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT))

if not args.no_trezor or not args.no_coldcard or args.ledger or args.digitalbitbox:
if not args.no_trezor or not args.no_coldcard or args.ledger or not args.no_bitbox:
# Start bitcoind
rpc, userpass = start_bitcoind(args.bitcoind)

Expand All @@ -47,11 +48,8 @@
suite.addTest(coldcard_test_suite(args.coldcard, rpc, userpass))
if args.ledger:
suite.addTest(ledger_test_suite(rpc, userpass))
if args.digitalbitbox:
if args.password:
suite.addTest(digitalbitbox_test_suite(rpc, userpass, args.password))
else:
print('Cannot run Digital Bitbox test without --password set')
if not args.no_bitbox:
suite.addTest(digitalbitbox_test_suite(rpc, userpass, args.bitbox))
if not args.no_keepkey:
suite.addTest(keepkey_test_suite(args.keepkey, rpc, userpass))
result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
Expand Down
30 changes: 30 additions & 0 deletions test/setup_environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@ fi
make -j$(nproc)
cd ../..

# Clone digital bitbox firmware if it doesn't exist, or update it if it does
dbb_setup_needed=false
if [ ! -d "mcu" ]; then
git clone --recursive https://github.com/achow101/mcu.git -b simulator
cd mcu
dbb_setup_needed=true
else
cd mcu
git fetch

# Determine if we need to pull. From https://stackoverflow.com/a/3278427
UPSTREAM=${1:-'@{u}'}
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse "$UPSTREAM")
BASE=$(git merge-base @ "$UPSTREAM")

if [ $LOCAL = $REMOTE ]; then
echo "Up-to-date"
elif [ $LOCAL = $BASE ]; then
git pull
coldcard_setup_needed=true
fi
fi

# Build the simulator. This is cached, but it is also fast
mkdir -p build && cd build
cmake .. -DBUILD_TYPE=simulator
make -j$(nproc)
cd ../..

# Clone keepkey firmware if it doesn't exist, or update it if it does
keepkey_setup_needed=false
if [ ! -d "keepkey-firmware" ]; then
Expand Down
60 changes: 40 additions & 20 deletions test/test_digitalbitbox.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,62 @@
#! /usr/bin/env python3

import argparse
import atexit
import json
import os
import subprocess
import time
import unittest

from test_device import DeviceTestCase, start_bitcoind, TestDeviceConnect, TestGetKeypool, TestSignTx, TestSignMessage

from hwilib.commands import process_commands

def digitalbitbox_test_suite(rpc, userpass, password):
# Look for real Digital BitBox using HWI API(self-referential, but no other way)
enum_res = process_commands(['-p', password, 'enumerate'])
path = None
master_xpub = None
fingerprint = None
for device in enum_res:
if device['type'] == 'digitalbitbox':
fingerprint = device['fingerprint']
path = device['path']
master_xpub = process_commands(['-f', fingerprint, '-p', password, 'getmasterxpub'])['xpub']
break
assert(path is not None and master_xpub is not None and fingerprint is not None)
from hwilib.devices.digitalbitbox import BitboxSimulator, send_plain, send_encrypt

def digitalbitbox_test_suite(rpc, userpass, simulator):
# Start the Digital bitbox simulator
simulator_proc = subprocess.Popen(['./' + os.path.basename(simulator), '../../tests/sd_files/'], cwd=os.path.dirname(simulator), stderr=subprocess.DEVNULL)
# Wait for simulator to be up
while True:
try:
dev = BitboxSimulator('127.0.0.1', 35345)
reply = send_plain(b'{"password":"0000"}', dev)
if 'error' not in reply:
break
except:
pass
time.sleep(0.5)
# Cleanup
def cleanup_simulator():
simulator_proc.kill()
simulator_proc.wait()
atexit.register(cleanup_simulator)

# Set password and load from backup
send_encrypt(json.dumps({"seed":{"source":"backup","filename":"test_backup.pdf","key":"key"}}), '0000', dev)

# params
type = 'digitalbitbox'
path = 'udp:127.0.0.1:35345'
fingerprint = 'a31b978a'
master_xpub = 'xpub6BsWJiRvbzQJg3J6tgUKmHWYbHJSj41EjAAje6LuDwnYLqLiNSWK4N7rCXwiUmNJTBrKL8AEH3LBzhJdgdxoy4T9aMPLCWAa6eWKGCFjQhq'

# Generic Device tests
suite = unittest.TestSuite()
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'digitalbitbox', path, fingerprint, master_xpub, password))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'digitalbitbox', path, fingerprint, master_xpub, password))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'digitalbitbox', path, fingerprint, master_xpub, password))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'digitalbitbox', path, fingerprint, master_xpub, password))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, path, fingerprint, master_xpub, '0000'))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, path, fingerprint, master_xpub, '0000'))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, path, fingerprint, master_xpub, '0000'))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, path, fingerprint, master_xpub, '0000'))
return suite

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Test Digital Bitbox implementation')
parser.add_argument('simulator', help='Path to simulator binary')
parser.add_argument('bitcoind', help='Path to bitcoind binary')
parser.add_argument('password', help='Device password')
args = parser.parse_args()

# Start bitcoind
rpc, userpass = start_bitcoind(args.bitcoind)

suite = digitalbitbox_test_suite(rpc, userpass, args.password)
suite = digitalbitbox_test_suite(rpc, userpass, args.simulator)
unittest.TextTestRunner(verbosity=2).run(suite)

0 comments on commit 486d2fa

Please sign in to comment.