Compare commits

...

35 Commits

Author SHA1 Message Date
Andrew Chow
9b722055c4
Merge #183: Officially support Trezor T and tests for it
84315dd Build and test with Trezor T emulator (Andrew Chow)
ef81662 Switch to Trezor monorepo (Andrew Chow)
6952edd Introduce full_type in tests for distinguishing between device models (Andrew Chow)
9484803 Update Trezor T support (Andrew Chow)

Pull request description:

  Fills out the table in the README with the rest of the Trezor T support.

  Changes the Trezor emulator stuff to use their new monorepo. Also adds Trezor T tests.

  Built on #157

ACKs for commit 84315d:

Tree-SHA512: f4f19574d4f7b177dc98e5770017c7801fec35f481d1b50cc5ec8bf95ce483327c47626d824cea372c28ebd807c19adb412aec58d0a016643ab5366bb0eb528e
2019-06-16 13:42:09 -04:00
Andrew Chow
84315dd5de Build and test with Trezor T emulator 2019-06-16 13:02:32 -04:00
Andrew Chow
ef816628e1 Switch to Trezor monorepo 2019-06-16 10:45:08 -04:00
Andrew Chow
6952edd879 Introduce full_type in tests for distinguishing between device models 2019-06-16 10:45:08 -04:00
Andrew Chow
9484803b71 Update Trezor T support 2019-06-16 10:45:08 -04:00
Andrew Chow
d8cf9f120b
Merge #157: Indicate in enumerate whether a device needs a passphrase or a pin
4d21f29 ignore other devices when checking that the coldcard has started (Andrew Chow)
54e14ba Change the order devices are tested in (Andrew Chow)
1c33312 Test need_passphrase_sent and need_pin_sent (Andrew Chow)
bca8535 Add needs_pin_sent to indicate whether promptpin needs to be used (Andrew Chow)
d2696cf Add needs_passphrase_sent to enumerate output (Andrew Chow)

Pull request description:

  This adds two fields to `numerate`: `needs_passphrase_sent` and `needs_pin_sent` which indicate whether a command with a `-p` option is needed or `promptpin` and `sendpin` need to be done. These fields will be true in the instances that a passphrase needs to be sent and have not been cached by the device. To avoid a chicken and egg problem with fingerprints on Trezors, if the device needs a passphrase but one was not specified in `enumerate`, the fingerprint won't be retrieved in order to not contaminate the passphrase cache on device.

  Built on #152 because it has some interactions with it that needed to be addressed.

ACKs for commit 4d21f2:

Tree-SHA512: 4164a8e541a9441b615205b84561966dd2f199f5f5e738cc55f12c60866483e458f3a5c05ac22c69baf862c37f6beeb3ca78f01c7b63770b9440f47a56811173
2019-06-16 10:44:09 -04:00
Andrew Chow
4d21f29127 ignore other devices when checking that the coldcard has started 2019-05-30 18:44:18 -04:00
Andrew Chow
54e14ba301 Change the order devices are tested in
Test the digitalbitbox first because it will self wipe if the wrong
password is given too many times (by other tests) which causes test
to fail.

Then, group up tests by simulator, those with simulators started at
the beginning (dbb and coldcard) go first. Then those with simulators
started for each test (trezor and keepkey). Lastly the test that
requires a physical device.
2019-05-30 18:44:18 -04:00
Andrew Chow
1c33312bb1 Test need_passphrase_sent and need_pin_sent 2019-05-30 18:44:18 -04:00
Andrew Chow
bca85354f9 Add needs_pin_sent to indicate whether promptpin needs to be used 2019-05-30 18:44:18 -04:00
Andrew Chow
d2696cfadf Add needs_passphrase_sent to enumerate output 2019-05-30 18:44:17 -04:00
Andrew Chow
a4a3aff89b
Merge #169: Add installudevrules command for linux
cada7f5 Add installudevrules command for linux (lontivero)

Pull request description:

  This PR is for discussing the concept and the implementation. The idea is to make it easier for Linux users to install the udev rules in their systems by running:

  ```
  $ hwi installudevrules
  ```

ACKs for commit cada7f:
  achow101:
    ACK cada7f5ce0

Tree-SHA512: 54a11b9bbbf8f4258a32a8503694026fda6adfe301bc3f44ed85c155426b894d512698046f14d9b62600d80614f2a6bd28d1d77ff0cfd2859c0f0786bc4013d6
2019-05-28 15:31:52 -04:00
lontivero
cada7f5ce0 Add installudevrules command for linux 2019-05-28 15:43:54 -03:00
Andrew Chow
64153a8a65
Merge #182: Fix displayaddress with descriptors and its tests
54bd668 Actually test that displayaddress is returning correct output (Andrew Chow)
0974f69 Ensure the client's fingerprint is available for descriptor displayaddress (Andrew Chow)
cea784e tests: Replace remaining process_commands with self.do_command in tests (Andrew Chow)
70e31ca tests: Properly escape arguments for cli interface (Andrew Chow)

Pull request description:

  `displayaddress` with descriptors wasn't working correctly nor was it being tested correctly. Fixed it and the tests.

ACKs for commit 54bd66:

Tree-SHA512: 84d27925a0bdb26e6ab844ded23e08d7b5f3e2aa78964d324c9223c04aaa26252a1e60e556696937b90518072fd62765fb38a15bc5f54600f837e4867cf1b148
2019-05-28 14:29:04 -04:00
Andrew Chow
1fb5eb42c7
Merge #181: Provide error codes in enumerate output
129e91d Provide error codes in enumerate output (Andrew Chow)

Pull request description:

  Adds error codes to `enumerate`'s output as suggested here: https://github.com/bitcoin-core/HWI/pull/175#discussion_r286126395

ACKs for commit 129e91:

Tree-SHA512: ba82b5a9764dd56c2872dd4b71f9bef60b7eb41437fc02099191f8dd92dfbd078a8d73aa279ae42ff5f351ac3c168bd0a429d4737695aec35f2c9f6a30102b5c
2019-05-28 14:27:37 -04:00
Andrew Chow
54bd6687cc Actually test that displayaddress is returning correct output 2019-05-21 19:34:15 -04:00
Andrew Chow
0974f6944c Ensure the client's fingerprint is available for descriptor displayaddress 2019-05-21 19:33:45 -04:00
Andrew Chow
cea784ef60 tests: Replace remaining process_commands with self.do_command in tests 2019-05-21 19:09:18 -04:00
Andrew Chow
70e31cad0c tests: Properly escape arguments for cli interface 2019-05-21 19:09:10 -04:00
Andrew Chow
129e91d9f0 Provide error codes in enumerate output 2019-05-21 17:19:56 -04:00
Andrew Chow
c14896c6eb
Merge #152: Keep Trezor sessions open unless state becomes inconsistent
d00ae56 Don't send Initialize when first connecting to a Trezor (Andrew Chow)

Pull request description:

  When the client is opened, we would send an `Initialize` message. However because a client may end up being opened many times in the process of executing one command, the saved state (in particular cached passphrases) would be lost each time the client is opened due to `Initialize` being sent. In order to have it keep that state, we don't want to send `Initialize` every time. Instead we will send `GetFeatures` which does the other part of what `Initialize` does (get the features for the device). `GetFeatures` failing would indicate that the state is inconsistent (which can be caused by command abort, exceptions, etc.), so in that case, `Initialize` is sent to clear the inconsistent state.

  I tested this using the Trezor T emulator and the password prompt does not appear multiple times per command. Additionally the password prompt does not appear for every command.

  Fixes #151

ACKs for commit d00ae5:

Tree-SHA512: 76bde488bf9a486267cc0c84668db79e9f1706f10f4d12d093829bcf1a5f713a7aec9da3eae674b152071d3d5c7219c163fe01465dea9e02f1e56298d318058a
2019-05-03 16:45:39 -04:00
Andrew Chow
6275418c90
Merge #166: Have setup_environment.sh remove emulator.img files for trezor and keepkey
6735983 Have setup_environment.sh remove emulator.img files for trezor and keepkey (Andrew Chow)

Pull request description:

  This makes travis stop hanging when it gets to the keepkey tests.

ACKs for commit 673598:

Tree-SHA512: 1eb37e4114e35a5636c69d9d0ba57a808d471f8675f1f02823bcb25aa1a6775d52bf4e5ab5fa12934fc79db8a82c4bfe589ccb0dddddf7b7761ee4a0e454c86a
2019-04-30 21:39:50 -04:00
Andrew Chow
67359835eb Have setup_environment.sh remove emulator.img files for trezor and keepkey 2019-04-30 19:29:15 -04:00
Andrew Chow
57a06836b9
Merge #163: commands.py: use fingerprint if already captured by client
0cf6bb9 commands.py: use fingerprint if already captured by client (Peter D. Gray)

Pull request description:

  For me with a `signmessage` to the Coldcard simulator, this saves 6-7 seconds. But this should speed up all devices, since they are already capturing the fingerprint value into the enumeration information.

ACKs for commit 0cf6bb:
  achow101:
    ACK 0cf6bb9998

Tree-SHA512: db2bb8eed855b70e8f1ee68c4eaf59eae191732246525669f9e600c10bee0dca1108685f1f508c24232151a792b8178d11bf80eee8cf880f0442e5f69dd81f56
2019-04-30 19:20:55 -04:00
Andrew Chow
c3f2521855
Merge #162: devices/coldcard.py: accelerate enumeration process
0c831e4 devices/coldcard.py: accelerate enum process by using fingerprint value revealed during connection setup (Peter D. Gray)

Pull request description:

  ... by using fingerprint value revealed during connection setup.

  For my system, with one Coldcard and CC simulator connected, this change saves six seconds.

ACKs for commit 0c831e:
  achow101:
    ACK 0c831e4f75

Tree-SHA512: d309dcc4730c54a47fc32329fed6adb9e6abe93e9068765874d32f6155d4e946359537a945afdc11f4fbcab692e0b8c29cd1e78d73c52d47ce21521d781970f4
2019-04-30 19:20:10 -04:00
Andrew Chow
d0c561e516
Merge #164: pin poetry version to 0.12.12 as 0.12.14 is broken
85eaf70 pin poetry version to 0.12.12 as 0.12.14 is broken (Andrew Chow)

Pull request description:

  The latest version of poetry (0.12.14) is broken which is causing travis to fail. Pin the version to 0.12.13 for now.

ACKs for commit 85eaf7:

Tree-SHA512: 9d024ddc52f688c8ec24018201113273a089ee36afb2f2b2c959cada5b982db61365222afc9f934657bdbc862401ab000ae59e4bf4f354121a7d8f22a0001a0c
2019-04-30 15:50:32 -04:00
Andrew Chow
85eaf70f36 pin poetry version to 0.12.12 as 0.12.14 is broken 2019-04-30 14:09:23 -04:00
Andrew Chow
4dd56fda3c
Merge #165: Add packages to pyproject.toml to allow generating a working setup.py
ae9bca3 Add packages to pyproject.toml to allow generating a working setup.py (Jonas Nick)

Pull request description:

  Fix of #159. Now setup.py was generated with `contrib/generate_setup.sh` after adding `hwi.py` and `hwilib` explicitly to `pyproject.toml`. This allows installing with `setup.py`.

ACKs for commit ae9bca:
  achow101:
    ACK ae9bca3ae3

Tree-SHA512: f31d2b81292d7fa324806ab4ff588f678e6d58dea37d209e423a20522c97a1a9dfeb18db8d02840f46aafc18b2533b8eef3913fd86dee5ef31db7e1b02e936ee
2019-04-29 15:58:06 -04:00
Jonas Nick
ae9bca3ae3 Add packages to pyproject.toml to allow generating a working setup.py 2019-04-29 19:47:46 +00:00
Andrew Chow
7201300e92
Merge #161: Mention in Ledger doc that Bitcoin App needs to be running
90f5e32 Mention in Ledger doc that Bitcoin App needs to be running (Jonas Nick)

Pull request description:

ACKs for commit 90f5e3:
  achow101:
    ACK 90f5e32cc3

Tree-SHA512: 051d3e29f3e0d4e2e0ae4273d77fcd20e4adab66da5227438c6fd9972b6f1f0288d49c23dbf4d2efd51155c8bfdfa180dd8fee6fe3d04883ec39e4d09224a345
2019-04-29 13:15:44 -04:00
Andrew Chow
892b552bb3
Merge #160: Fix keypool refill instructions in docs
483b589 Fix keypool refill instructions in docs (Jonas Nick)

Pull request description:

  The current instructions both give the same results, so I presume the first should be non-internal.

ACKs for commit 483b58:
  achow101:
    ACK 483b589eeb

Tree-SHA512: 0c01de587c86e7b1df3e8781f83dab8f906ed0f89864f33f83992e0f724ceb3216e4aeee21a1d00c1ef74021c4975b823b0f20fe13b6ef7c0c6c2b61da5f9d8d
2019-04-29 13:15:07 -04:00
Peter D. Gray
0c831e4f75
devices/coldcard.py: accelerate enum process by using fingerprint value revealed during connection setup 2019-04-29 10:32:58 -04:00
Jonas Nick
90f5e32cc3 Mention in Ledger doc that Bitcoin App needs to be running 2019-04-29 09:21:59 +00:00
Jonas Nick
483b589eeb Fix keypool refill instructions in docs 2019-04-29 09:02:50 +00:00
Andrew Chow
d00ae56eef Don't send Initialize when first connecting to a Trezor
Instead of sending Initialize, don't. This lets it stay in the
same "session" as previous commands so some state such as passphrases
are still cached by the device.

If GetFeatures fails, then try sending Initialize so that any
inconsistent state on the device is cleared.
2019-04-18 17:30:51 -04:00
29 changed files with 513 additions and 120 deletions

View File

@ -40,7 +40,7 @@ addons:
- cython3
- ccache
install:
- pip install pipenv pysdl2 python-bitcoinrpc protobuf poetry
- pip install pipenv pysdl2 python-bitcoinrpc protobuf poetry==0.12.12
# From trezor-mcu to get the correct protobuf version
- curl -LO "https://github.com/google/protobuf/releases/download/v3.4.0/protoc-3.4.0-linux-x86_64.zip"
- unzip "protoc-3.4.0-linux-x86_64.zip" -d protoc
@ -56,7 +56,7 @@ jobs:
- name: With command line interface
script: cd test; poetry run ./run_tests.py --interface=cli
- name: With stdin interface
script: cd test; ./run_tests.py --interface=stdin
script: cd test; poetry run ./run_tests.py --interface=stdin
- name: With linux binary distribution command line interface
services: docker
before_script:

View File

@ -69,23 +69,23 @@ Please also see [docs](docs/) for additional information about each device.
| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes |
| Implemented | Yes | Yes | Yes | Yes | Yes | Yes |
| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes |
| Message Signing | Yes | Yes | ? | Yes | Yes | Yes |
| Device Setup | N/A | Yes | ? | Yes | Yes | N/A |
| Device Wipe | N/A | Yes | ? | Yes | Yes | N/A |
| Device Recovery | N/A | Yes | ? | N/A | Yes | N/A |
| Device Backup | N/A | N/A | ? | Yes | N/A | Yes |
| P2PKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |
| P2SH-P2WPKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |
| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes |
| Device Setup | N/A | Yes | Yes | Yes | Yes | N/A |
| Device Wipe | N/A | Yes | Yes | Yes | Yes | N/A |
| Device Recovery | N/A | Yes | Yes | N/A | Yes | N/A |
| Device Backup | N/A | N/A | N/A | Yes | N/A | Yes |
| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |
| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |
| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |
| P2SH Multisig Inputs | Yes | Yes | ? | Yes | Yes | N/A |
| P2SH-P2WSH Multisig Inputs | Yes | No | ? | Yes | No | N/A |
| P2WSH Multisig Inputs | Yes | No | ? | Yes | Yes | N/A |
| Bare Multisig Inputs | Yes | N/A | ? | Yes | N/A | N/A |
| Aribtrary scriptPubKey Inputs | Yes | N/A | ? | Yes | N/A | N/A |
| Aribtrary redeemScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |
| Arbitrary witnessScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |
| Non-wallet inputs | Yes | Yes | ? | Yes | Yes | Yes |
| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | ? | Yes | Yes | Yes |
| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A |
| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | No | N/A |
| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A |
| Bare Multisig Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
| Aribtrary scriptPubKey Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
| Aribtrary redeemScript Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
| Arbitrary witnessScript Inputs | Yes | N/A | N/A | Yes | N/A | N/A |
| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes |
| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | N/A | Yes | Yes | Yes |
| Display on device screen | Yes | Yes | Yes | N/A | Yes | Yes |
## Using with Bitcoin Core

View File

@ -4,7 +4,7 @@
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
pip install -U pip
pip install poetry
pip install poetry==0.12.12
# Setup poetry and install the dependencies
poetry install

View File

@ -4,7 +4,7 @@
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
pip install -U pip
pip install poetry
pip install poetry==0.12.12
# Setup poetry and install the dependencies
poetry install

View File

@ -53,7 +53,7 @@ popd
$PYTHON -m pip install -U pip
# Install Poetry and things needed for pyinstaller
$PYTHON -m pip install poetry
$PYTHON -m pip install poetry==0.12.12
# We also need to change the timestamps of all of the base library files
lib_dir=~/.wine/drive_c/python3/Lib

View File

@ -286,7 +286,7 @@ e51392c82e13bbfe714c73361aff14ac1a1637abf37587a562844ae5a4265adf
When the keypools run out, they can be refilled by using the `getkeypool` commands as done in the beginning, but with different starting and ending indexes. For example, to refill my keypools, I would use the following `getkeypool` commands:
```
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool --internal 1000 2000
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool 1000 2000
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool --internal 1000 2000
```
The output can be imported with `importmulti` as shown in the Setup steps.

View File

@ -1,6 +1,7 @@
# Ledger Nano S
The Ledger Nano S is supported by HWI.
Note that the Bitcoin App must be installed and running on the device.
Currently implemented commands:

View File

@ -25,6 +25,10 @@ a = Analysis(['hwi.py'],
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
if platform.system() == 'Linux':
a.datas += Tree('udev', prefix='udev')
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,

View File

@ -2,7 +2,7 @@
from .commands import backup_device, displayaddress, enumerate, find_device, \
get_client, getmasterxpub, getxpub, getkeypool, prompt_pin, restore_device, send_pin, setup_device, \
signmessage, signtx, wipe_device
signmessage, signtx, wipe_device, install_udev_rules
from .errors import (
HWWError,
NO_DEVICE_PATH,
@ -63,6 +63,9 @@ def prompt_pin_handler(args, client):
def send_pin_handler(args, client):
return send_pin(client, pin=args.pin)
def install_udev_rules_handler(args):
return install_udev_rules(args.source, args.location)
def process_commands(cli_args):
parser = argparse.ArgumentParser(description='Hardware Wallet Interface, version {}.\nAccess and send commands to a hardware wallet device. Responses are in JSON format.'.format(__version__), formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--device-path', '-d', help='Specify the device path of the device to connect to')
@ -142,6 +145,13 @@ def process_commands(cli_args):
sendpin_parser.add_argument('pin', help='The numeric positions of the PIN')
sendpin_parser.set_defaults(func=send_pin_handler)
if sys.platform.startswith("linux"):
udevrules_parser = subparsers.add_parser('installudevrules', help='Install and load the udev rule files for the hardware wallet devices')
udevrules_parser.add_argument('--source', help=argparse.SUPPRESS, default='udev')
udevrules_parser.add_argument('--location', help='The path where the udev rules files will be copied', default='/etc/udev/rules.d/')
udevrules_parser.set_defaults(func=install_udev_rules_handler)
if any(arg == '--stdin' for arg in cli_args):
blank_count = 0
while True:
@ -177,6 +187,17 @@ def process_commands(cli_args):
if command == 'enumerate':
return args.func(args)
# Install the devices udev rules for Linux
if command == 'installudevrules':
try:
result = args.func(args)
except Exception as e:
if args.debug:
import traceback
traceback.print_exc()
result = {'error': str(e), 'code': UNKNOWN_ERROR}
return result
# Auto detect if we are using fingerprint or type to identify device
if args.fingerprint or (args.device_type and not args.device_path):
client = find_device(args.device_path, args.password, args.device_type, args.fingerprint)

View File

@ -9,6 +9,8 @@ from .base58 import get_xpub_fingerprint_as_id, get_xpub_fingerprint_hex, xpub_t
from .errors import NoPasswordError, UnavailableActionError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, UnknownDeviceError, BAD_ARGUMENT, NOT_IMPLEMENTED
from .descriptor import Descriptor
from .devices import __all__ as all_devs
from .udevinstaller import UDevInstaller
# Get the client for the device
def get_client(device_type, device_path, password=''):
@ -155,6 +157,10 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False):
return {'error':'Both `--wpkh` and `--sh_wpkh` can not be selected at the same time.','code':BAD_ARGUMENT}
return client.display_address(path, sh_wpkh, wpkh)
elif desc is not None:
if client.fingerprint is None:
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
client.fingerprint = get_xpub_fingerprint_hex(master_xpub)
if sh_wpkh == True or wpkh == True:
return {'error':' `--wpkh` and `--sh_wpkh` can not be combined with --desc','code':BAD_ARGUMENT}
descriptor = Descriptor.parse(desc, client.is_testnet)
@ -186,3 +192,6 @@ def prompt_pin(client):
def send_pin(client, pin):
return client.send_pin(pin)
def install_udev_rules(source, location):
return UDevInstaller.install(source, location);

View File

@ -1,7 +1,7 @@
# Coldcard interaction script
from ..hwwclient import HardwareWalletClient
from ..errors import ActionCanceledError, BadArgumentError, DeviceBusyError, UnavailableActionError, DeviceFailureError
from ..errors import ActionCanceledError, BadArgumentError, DeviceBusyError, DeviceFailureError, HWWError, UnavailableActionError, UNKNOWN_ERROR
from .ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID
from .ckcc.protocol import CCProtocolPacker, CCBusyError, CCProtoError, CCUserRefused
from .ckcc.constants import MAX_BLK_LEN, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH
@ -14,6 +14,8 @@ import hid
import io
import sys
import time
import struct
from binascii import hexlify
CC_SIMULATOR_SOCK = '/tmp/ckcc-simulator.sock'
# Using the simulator: https://github.com/Coldcard/firmware/blob/master/unix/README.md
@ -56,6 +58,10 @@ class ColdcardClient(HardwareWalletClient):
else:
return {'xpub':xpub}
def _get_fingerprint_hex(self):
# quick method to get fingerprint of wallet
return hexlify(struct.pack('<I', self.device.master_fingerprint)).decode()
# Must return a hex string with the signed transaction
# The tx must be in the combined unsigned transaction format
@coldcard_exception
@ -219,29 +225,35 @@ def enumerate(password=''):
path = d['path'].decode()
d_data['type'] = 'coldcard'
d_data['path'] = path
d_data['needs_passphrase'] = False
client = None
try:
client = ColdcardClient(path)
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
d_data['fingerprint'] = client._get_fingerprint_hex()
except HWWError as e:
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
d_data['code'] = e.get_code()
except Exception as e:
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
d_data['code'] = UNKNOWN_ERROR
if client:
client.close()
results.append(d_data)
# Check if the simulator is there
client = None
try:
client = ColdcardClient(CC_SIMULATOR_SOCK)
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
d_data = {}
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
d_data['fingerprint'] = client._get_fingerprint_hex()
d_data['type'] = 'coldcard'
d_data['path'] = CC_SIMULATOR_SOCK
d_data['needs_pin_sent'] = False
d_data['needs_passphrase_sent'] = False
results.append(d_data)
except RuntimeError as e:
if str(e) == 'Cannot connect to simulator. Is it running?':
@ -250,4 +262,5 @@ def enumerate(password=''):
raise e
if client:
client.close()
return results

View File

@ -15,7 +15,7 @@ import sys
import time
from ..hwwclient import HardwareWalletClient
from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DeviceNotReadyError, NoPasswordError, UnavailableActionError, DeviceFailureError
from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DeviceNotReadyError, HWWError, NoPasswordError, UnavailableActionError, UNKNOWN_ERROR
from ..serializations import CTransaction, PSBT, hash256, hash160, ser_sig_der, ser_sig_compact, ser_compact_size
from ..base58 import get_xpub_fingerprint, decode, to_address, xpub_main_2_test, get_xpub_fingerprint_hex
@ -603,8 +603,14 @@ def enumerate(password=''):
else:
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
d_data['needs_pin_sent'] = False
d_data['needs_passphrase_sent'] = True
except HWWError as e:
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
d_data['code'] = e.get_code()
except Exception as e:
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
d_data['code'] = UNKNOWN_ERROR
if client:
client.close()

View File

@ -1,5 +1,6 @@
# KeepKey interaction script
from ..errors import HWWError, UNKNOWN_ERROR
from .trezorlib.transport import enumerate_devices
from .trezor import TrezorClient
from ..base58 import get_xpub_fingerprint_hex
@ -25,13 +26,24 @@ def enumerate(password=''):
client.client.init_device()
if not 'keepkey' in client.client.features.vendor:
continue
d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.pin_cached
d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection and not client.client.features.passphrase_cached
if d_data['needs_pin_sent']:
raise DeviceNotReadyError('Keepkey is locked. Unlock by using \'promptpin\' and then \'sendpin\'.')
if d_data['needs_passphrase_sent'] and not password:
raise DeviceNotReadyError("Passphrase needs to be specified before the fingerprint information can be retrieved")
if client.client.features.initialized:
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
d_data['needs_passphrase_sent'] = False # Passphrase is always needed for the above to have worked, so it's already sent
else:
d_data['error'] = 'Not initialized'
except HWWError as e:
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
d_data['code'] = e.get_code()
except Exception as e:
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
d_data['code'] = UNKNOWN_ERROR
if client:
client.close()

View File

@ -1,7 +1,7 @@
# Ledger interaction script
from ..hwwclient import HardwareWalletClient
from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, UnavailableActionError
from ..errors import ActionCanceledError, BadArgumentError, DeviceConnectionError, DeviceFailureError, HWWError, UnavailableActionError, UNKNOWN_ERROR
from .btchip.btchip import *
from .btchip.btchipUtils import *
import base64
@ -350,8 +350,11 @@ def enumerate(password=''):
client = LedgerClient(path, password)
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
d_data['needs_pin_sent'] = False
d_data['needs_passphrase_sent'] = False
except Exception as e:
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
d_data['code'] = UNKNOWN_ERROR
if client:
client.close()

View File

@ -1,7 +1,7 @@
# Trezor interaction script
from ..hwwclient import HardwareWalletClient
from ..errors import ActionCanceledError, BadArgumentError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, DeviceConnectionError, UnavailableActionError, DeviceNotReadyError
from ..errors import ActionCanceledError, BadArgumentError, DeviceAlreadyInitError, DeviceAlreadyUnlockedError, DeviceConnectionError, DeviceNotReadyError, HWWError, UnavailableActionError, UNKNOWN_ERROR
from .trezorlib.client import TrezorClient as Trezor
from .trezorlib.debuglink import TrezorClientDebugLink, DebugUI
from .trezorlib.exceptions import Cancelled
@ -100,6 +100,7 @@ class TrezorClient(HardwareWalletClient):
transport = get_transport(path)
self.client = TrezorClientDebugLink(transport=transport)
self.simulator = True
self.client.set_passphrase(password)
else:
self.client = Trezor(transport=get_transport(path), ui=PassphraseUI(password))
@ -421,13 +422,24 @@ def enumerate(password=''):
client.client.init_device()
if not 'trezor' in client.client.features.vendor:
continue
d_data['needs_pin_sent'] = client.client.features.pin_protection and not client.client.features.pin_cached
d_data['needs_passphrase_sent'] = client.client.features.passphrase_protection and not client.client.features.passphrase_cached
if d_data['needs_pin_sent']:
raise DeviceNotReadyError('Trezor is locked. Unlock by using \'promptpin\' and then \'sendpin\'.')
if d_data['needs_passphrase_sent'] and not password:
raise DeviceNotReadyError("Passphrase needs to be specified before the fingerprint information can be retrieved")
if client.client.features.initialized:
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
d_data['fingerprint'] = get_xpub_fingerprint_hex(master_xpub)
d_data['needs_passphrase_sent'] = False # Passphrase is always needed for the above to have worked, so it's already sent
else:
d_data['error'] = 'Not initialized'
except HWWError as e:
d_data['error'] = "Could not open client or get fingerprint information: " + e.get_msg()
d_data['code'] = e.get_code()
except Exception as e:
d_data['error'] = "Could not open client or get fingerprint information: " + str(e)
d_data['code'] = UNKNOWN_ERROR
if client:
client.close()

View File

@ -178,7 +178,10 @@ class TrezorClient:
@tools.session
def init_device(self):
resp = self.call_raw(messages.Initialize(state=self.state))
resp = self.call_raw(messages.GetFeatures())
# If GetFeatures fails, try initializing and clearing inconsistent state on the device
if isinstance(resp, messages.Failure):
resp = self.call_raw(messages.Initialize())
if not isinstance(resp, messages.Features):
raise exceptions.TrezorException("Unexpected initial response")
else:

View File

@ -16,6 +16,7 @@ DEVICE_NOT_READY = -12
UNKNOWN_ERROR = -13
ACTION_CANCELED = -14
DEVICE_BUSY = -15
NEED_TO_BE_ROOT = -16
# Exceptions
class HWWError(Exception):

60
hwilib/udevinstaller.py Normal file
View File

@ -0,0 +1,60 @@
import sys
from subprocess import check_call, CalledProcessError, DEVNULL
from .errors import NEED_TO_BE_ROOT
from shutil import copy
from os import path, environ, listdir, getlogin, geteuid
class UDevInstaller(object):
@staticmethod
def install(source, location):
try:
udev_installer = UDevInstaller()
udev_installer.copy_udev_rule_files(source, location)
udev_installer.trigger()
udev_installer.reload_rules()
udev_installer.add_user_plugdev_group()
except CalledProcessError as e:
if geteuid() != 0:
return {'error':'Need to be root.','code':NEED_TO_BE_ROOT}
raise
return {"success": True}
def __init__(self):
self._udevadm = '/sbin/udevadm'
self._groupadd = '/usr/sbin/groupadd'
self._usermod = '/usr/sbin/usermod'
def _execute(self, command, *args):
command = [command] + list(args)
check_call(command, stderr=DEVNULL, stdout=DEVNULL)
def trigger(self):
self._execute(self._udevadm, 'trigger')
def reload_rules(self):
self._execute(self._udevadm, 'control', '--reload-rules')
def add_user_plugdev_group(self):
self._create_group('plugdev')
self._add_user_to_group(getlogin(), 'plugdev')
def _create_group(self, name):
try:
self._execute(self._groupadd, name)
except CalledProcessError as e:
if e.returncode != 9: # group already exists
raise
def _add_user_to_group(self, user, group):
self._execute(self._usermod, '-aG', group, user)
def copy_udev_rule_files(self, source, location):
src_dir_path = source
for rules_file_name in listdir(src_dir_path):
rules_file_path = _resource_path(path.join(src_dir_path, rules_file_name))
copy(rules_file_path, location)
def _resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
return path.join(sys._MEIPASS, relative_path)
return path.join(relative_path)

View File

@ -9,6 +9,10 @@ repository = "https://github.com/bitcoin-core/HWI"
homepage = "https://github.com/bitcoin-core/HWI"
exclude = ["docs/", "test/"]
include = ["hwilib/**/*.py"]
packages = [
{ include = "hwi.py" },
{ include = "hwilib" },
]
[tool.poetry.dependencies]
python = ">=3.5.6"

View File

@ -1,6 +1,18 @@
# -*- coding: utf-8 -*-
from distutils.core import setup
packages = \
['hwilib',
'hwilib.devices',
'hwilib.devices.btchip',
'hwilib.devices.ckcc',
'hwilib.devices.trezorlib',
'hwilib.devices.trezorlib.messages',
'hwilib.devices.trezorlib.transport']
package_data = \
{'': ['*']}
modules = \
['hwi']
install_requires = \
@ -21,10 +33,12 @@ setup_kwargs = {
'name': 'hwi',
'version': '1.0.0',
'description': 'A library for working with Bitcoin hardware wallets',
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and udev rules for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager.\nOnce HWI's source has been downloaded with git clone, it and its dependencies can be installed via poetry by execting the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to install all of the dependencies (in virtualenv or system):\n\n```\npip3 install hidapi # HID API needed in general\npip3 install ecdsa\npip3 install pyaes\npip3 install typing_extensions\npip3 install mnemonic\npip3 install libusb1\n```\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\n```\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano S | Trezor One | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | Yes | Yes | Yes |\n| Device Setup | N/A | Yes | Yes | Yes | N/A |\n| Device Wipe | N/A | Yes | Yes | Yes | N/A |\n| Device Recovery | N/A | Yes | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | N/A |\n| P2SH-P2WSH Multisig Inputs | Yes | No | Yes | No | N/A |\n| P2WSH Multisig Inputs | Yes | No | Yes | Yes | N/A |\n| Bare Multisig Inputs | Yes | N/A | Yes | N/A | N/A |\n| Aribtrary scriptPubKey Inputs | Yes | N/A | Yes | N/A | N/A |\n| Aribtrary redeemScript Inputs | Yes | N/A | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | N/A | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\n## Prerequisites\n\nPython 3 is required. The libraries and udev rules for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager.\nOnce HWI's source has been downloaded with git clone, it and its dependencies can be installed via poetry by execting the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to install all of the dependencies (in virtualenv or system):\n\n```\npip3 install hidapi # HID API needed in general\npip3 install ecdsa\npip3 install pyaes\npip3 install typing_extensions\npip3 install mnemonic\npip3 install libusb1\n```\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\n```\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path is known, issue commands to it like so:\n\n```\n./hwi.py -t <type> -d <path> <command> <command args>\n```\n\n## Device Support\n\nThe below table lists what devices and features are supported for each device.\n\nPlease also see [docs](docs/) for additional information about each device.\n\n| Feature \\ Device | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |\n|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes |\n| Implemented | Yes | Yes | Yes | Yes | Yes | Yes |\n| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes |\n| Message Signing | Yes | Yes | ? | Yes | Yes | Yes |\n| Device Setup | N/A | Yes | ? | Yes | Yes | N/A |\n| Device Wipe | N/A | Yes | ? | Yes | Yes | N/A |\n| Device Recovery | N/A | Yes | ? | N/A | Yes | N/A |\n| Device Backup | N/A | N/A | ? | Yes | N/A | Yes |\n| P2PKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |\n| P2SH-P2WPKH Inputs | Yes | Yes | ? | Yes | Yes | Yes |\n| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes |\n| P2SH Multisig Inputs | Yes | Yes | ? | Yes | Yes | N/A |\n| P2SH-P2WSH Multisig Inputs | Yes | No | ? | Yes | No | N/A |\n| P2WSH Multisig Inputs | Yes | No | ? | Yes | Yes | N/A |\n| Bare Multisig Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Aribtrary scriptPubKey Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Aribtrary redeemScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Arbitrary witnessScript Inputs | Yes | N/A | ? | Yes | N/A | N/A |\n| Non-wallet inputs | Yes | Yes | ? | Yes | Yes | Yes |\n| Mixed Segwit and Non-Segwit Inputs | N/A | Yes | ? | Yes | Yes | Yes |\n| Display on device screen | Yes | Yes | Yes | N/A | Yes | Yes |\n\n## Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](docs/bitcoin-core-usage.md).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n",
'author': 'Andrew Chow',
'author_email': 'andrew@achow101.com',
'url': 'https://github.com/bitcoin-core/HWI',
'packages': packages,
'package_data': package_data,
'py_modules': modules,
'install_requires': install_requires,
'extras_require': extras_require,

View File

@ -14,11 +14,15 @@ from test_trezor import trezor_test_suite
from test_ledger import ledger_test_suite
from test_digitalbitbox import digitalbitbox_test_suite
from test_keepkey import keepkey_test_suite
from test_udevrules import TestUdevRulesInstaller
parser = argparse.ArgumentParser(description='Setup the testing environment and run automated tests')
trezor_group = parser.add_mutually_exclusive_group()
trezor_group.add_argument('--no_trezor', help='Do not run Trezor test with emulator', action='store_true')
trezor_group.add_argument('--trezor', help='Path to Trezor emulator.', default='work/trezor-mcu/firmware/trezor.elf')
trezor_group.add_argument('--trezor', help='Path to Trezor emulator.', default='work/trezor-firmware/legacy/firmware/trezor.elf')
trezor_t_group = parser.add_mutually_exclusive_group()
trezor_t_group.add_argument('--no_trezor_t', help='Do not run Trezor T test with emulator', action='store_true')
trezor_t_group.add_argument('--trezor_t', help='Path to Trezor T emulator.', default='work/trezor-firmware/core/emu.sh')
coldcard_group = parser.add_mutually_exclusive_group()
coldcard_group.add_argument('--no_coldcard', help='Do not run Coldcard test with simulator', action='store_true')
coldcard_group.add_argument('--coldcard', help='Path to Coldcard simulator.', default='work/firmware/unix/headless.py')
@ -40,20 +44,25 @@ suite = unittest.TestSuite()
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestDescriptor))
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestSegwitAddress))
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestPSBT))
if sys.platform.startswith("linux"):
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestUdevRulesInstaller))
if not args.no_trezor or not args.no_coldcard or args.ledger or not args.no_bitbox or not args.no_keepkey:
if not args.no_trezor or not args.no_coldcard or args.ledger or not args.no_bitbox or not args.no_keepkey or not args.no_trezor_t:
# Start bitcoind
rpc, userpass = start_bitcoind(args.bitcoind)
if not args.no_trezor:
suite.addTest(trezor_test_suite(args.trezor, rpc, userpass, args.interface))
if not args.no_coldcard:
suite.addTest(coldcard_test_suite(args.coldcard, rpc, userpass, args.interface))
if args.ledger:
suite.addTest(ledger_test_suite(rpc, userpass, args.interface))
if not args.no_bitbox:
suite.addTest(digitalbitbox_test_suite(rpc, userpass, args.bitbox, args.interface))
if not args.no_coldcard:
suite.addTest(coldcard_test_suite(args.coldcard, rpc, userpass, args.interface))
if not args.no_trezor:
suite.addTest(trezor_test_suite(args.trezor, rpc, userpass, args.interface))
if not args.no_trezor_t:
suite.addTest(trezor_test_suite(args.trezor_t, rpc, userpass, args.interface, True))
if not args.no_keepkey:
suite.addTest(keepkey_test_suite(args.keepkey, rpc, userpass, args.interface))
if args.ledger:
suite.addTest(ledger_test_suite(rpc, userpass, args.interface))
result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
sys.exit(not result.wasSuccessful())

View File

@ -9,12 +9,12 @@ cd work
# Clone trezor-mcu if it doesn't exist, or update it if it does
trezor_setup_needed=false
if [ ! -d "trezor-mcu" ]; then
git clone --recursive https://github.com/trezor/trezor-mcu.git
cd trezor-mcu
if [ ! -d "trezor-firmware" ]; then
git clone --recursive https://github.com/trezor/trezor-firmware.git
cd trezor-firmware
trezor_setup_needed=true
else
cd trezor-mcu
cd trezor-firmware
git fetch
# Determine if we need to pull. From https://stackoverflow.com/a/3278427
@ -31,16 +31,30 @@ else
fi
fi
# Build emulator. This is pretty fast, so rebuilding every time is ok
# Build trezor one emulator. This is pretty fast, so rebuilding every time is ok
# But there should be some caching that makes this faster
cd legacy
export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1
if [ "$trezor_setup_needed" == true ] ; then
script/setup
pipenv install
fi
pipenv run script/cibuild
# Delete any emulator.img file
find . -name "emulator.img" -exec rm {} \;
cd ..
# Build trezor t emulator. This is pretty fast, so rebuilding every time is ok
# But there should be some caching that makes this faster
cd core
if [ "$trezor_setup_needed" == true ] ; then
make vendor
fi
make build_unix
# Delete any emulator.img file
rm /var/tmp/trezor.flash
cd ../..
# Clone coldcard firmware if it doesn't exist, or update it if it does
coldcard_setup_needed=false
if [ ! -d "firmware" ]; then
@ -144,6 +158,8 @@ cd ../../../
export PATH=$PATH:`pwd`/nanopb/generator
pipenv run cmake -C cmake/caches/emulator.cmake . -DNANOPB_DIR=nanopb/ -DKK_HAVE_STRLCAT=OFF -DKK_HAVE_STRLCPY=OFF
pipenv run make -j$(nproc) kkemu
# Delete any emulator.img file
find . -name "emulator.img" -exec rm {} \;
cd ..
# Clone bitcoind if it doesn't exist, or update it if it does

View File

@ -18,7 +18,12 @@ def coldcard_test_suite(simulator, rpc, userpass, interface):
# Wait for simulator to be up
while True:
enum_res = process_commands(['enumerate'])
if len(enum_res) > 0 and 'error' not in enum_res[0]:
found = False
for dev in enum_res:
if dev['type'] == 'coldcard' and 'error' not in dev:
found = True
break
if found:
break
time.sleep(0.5)
# Cleanup
@ -70,12 +75,12 @@ def coldcard_test_suite(simulator, rpc, userpass, interface):
# Generic device tests
suite = unittest.TestSuite()
suite.addTest(DeviceTestCase.parameterize(TestColdcardManCommands, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', '', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestColdcardManCommands, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', '', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'coldcard', 'coldcard', '/tmp/ckcc-simulator.sock', '0f056943', 'tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd', interface=interface))
return suite
if __name__ == '__main__':

View File

@ -3,6 +3,7 @@
import atexit
import json
import os
import shlex
import shutil
import subprocess
import tempfile
@ -51,11 +52,12 @@ def start_bitcoind(bitcoind_path):
return (rpc, userpass)
class DeviceTestCase(unittest.TestCase):
def __init__(self, rpc, rpc_userpass, type, path, fingerprint, master_xpub, password = '', emulator=None, interface='library', methodName='runTest'):
def __init__(self, rpc, rpc_userpass, type, full_type, path, fingerprint, master_xpub, password = '', emulator=None, interface='library', methodName='runTest'):
super(DeviceTestCase, self).__init__(methodName)
self.rpc = rpc
self.rpc_userpass = rpc_userpass
self.type = type
self.full_type = full_type
self.path = path
self.fingerprint = fingerprint
self.master_xpub = master_xpub
@ -70,21 +72,24 @@ class DeviceTestCase(unittest.TestCase):
self.interface = interface
@staticmethod
def parameterize(testclass, rpc, rpc_userpass, type, path, fingerprint, master_xpub, password = '', interface='library', emulator=None):
def parameterize(testclass, rpc, rpc_userpass, type, full_type, path, fingerprint, master_xpub, password = '', interface='library', emulator=None):
testloader = unittest.TestLoader()
testnames = testloader.getTestCaseNames(testclass)
suite = unittest.TestSuite()
for name in testnames:
suite.addTest(testclass(rpc, rpc_userpass, type, path, fingerprint, master_xpub, password, emulator, interface, name))
suite.addTest(testclass(rpc, rpc_userpass, type, full_type, path, fingerprint, master_xpub, password, emulator, interface, name))
return suite
def do_command(self, args):
cli_args = []
for arg in args:
cli_args.append(shlex.quote(arg))
if self.interface == 'cli':
proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'bindist':
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'stdin':
@ -101,10 +106,10 @@ class DeviceTestCase(unittest.TestCase):
return []
def __str__(self):
return '{}: {}'.format(self.type, super().__str__())
return '{}: {}'.format(self.full_type, super().__str__())
def __repr__(self):
return '{}: {}'.format(self.type, super().__repr__())
return '{}: {}'.format(self.full_type, super().__repr__())
class TestDeviceConnect(DeviceTestCase):
def setUp(self):
@ -154,9 +159,9 @@ class TestDeviceConnect(DeviceTestCase):
class TestGetKeypool(DeviceTestCase):
def setUp(self):
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
if '{}_test'.format(self.type) not in self.rpc.listwallets():
self.rpc.createwallet('{}_test'.format(self.type), True)
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.type))
if '{}_test'.format(self.full_type) not in self.rpc.listwallets():
self.rpc.createwallet('{}_test'.format(self.full_type), True)
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type))
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
if '--testnet' not in self.dev_args:
self.dev_args.append('--testnet')
@ -261,9 +266,9 @@ class TestGetKeypool(DeviceTestCase):
class TestSignTx(DeviceTestCase):
def setUp(self):
self.rpc = AuthServiceProxy('http://{}@127.0.0.1:18443'.format(self.rpc_userpass))
if '{}_test'.format(self.type) not in self.rpc.listwallets():
self.rpc.createwallet('{}_test'.format(self.type), True)
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.type))
if '{}_test'.format(self.full_type) not in self.rpc.listwallets():
self.rpc.createwallet('{}_test'.format(self.full_type), True)
self.wrpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/{}_test'.format(self.rpc_userpass, self.full_type))
self.wpk_rpc = AuthServiceProxy('http://{}@127.0.0.1:18443/wallet/'.format(self.rpc_userpass))
if '--testnet' not in self.dev_args:
self.dev_args.append('--testnet')
@ -405,13 +410,13 @@ class TestSignTx(DeviceTestCase):
# Test wrapper to avoid mixed-inputs signing for Ledger
def test_signtx(self):
supports_mixed = {'coldcard', 'trezor', 'digitalbitbox', 'keepkey'}
supports_multisig = {'ledger', 'trezor', 'digitalbitbox', 'keepkey'}
if self.type not in supports_mixed:
self._test_signtx("legacy", self.type in supports_multisig)
self._test_signtx("segwit", self.type in supports_multisig)
supports_mixed = {'coldcard', 'trezor_1', 'digitalbitbox', 'keepkey'}
supports_multisig = {'ledger', 'trezor_1', 'digitalbitbox', 'keepkey'}
if self.full_type not in supports_mixed:
self._test_signtx("legacy", self.full_type in supports_multisig)
self._test_signtx("segwit", self.full_type in supports_multisig)
else:
self._test_signtx("all", self.type in supports_multisig)
self._test_signtx("all", self.full_type in supports_multisig)
# Make a huge transaction which might cause some problems with different interfaces
def test_big_tx(self):
@ -456,45 +461,68 @@ class TestDisplayAddress(DeviceTestCase):
self.assertEqual(result['code'], -7)
def test_display_address_path(self):
self.do_command(self.dev_args + ['displayaddress', '--path', 'm/44h/1h/0h/0/0'])
self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', 'm/49h/1h/0h/0/0'])
self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', 'm/84h/1h/0h/0/0'])
result = self.do_command(self.dev_args + ['displayaddress', '--path', 'm/44h/1h/0h/0/0'])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
result = self.do_command(self.dev_args + ['displayaddress', '--sh_wpkh', '--path', 'm/49h/1h/0h/0/0'])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
result = self.do_command(self.dev_args + ['displayaddress', '--wpkh', '--path', 'm/84h/1h/0h/0/0'])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
def test_display_address_bad_path(self):
result = self.do_command(self.dev_args + ['displayaddress', '--path', 'f'])
self.assertEquals(result['code'], -7)
def test_display_address_descriptor(self):
account_xpub = process_commands(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub']
p2sh_segwit_account_xpub = process_commands(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub']
legacy_account_xpub = process_commands(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub']
account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub']
p2sh_segwit_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub']
legacy_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub']
# Native SegWit address using xpub:
process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + account_xpub + '/0/0)'])
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + account_xpub + '/0/0)'])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
# Native SegWit address using hex encoded pubkey:
process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + xpub_to_pub_hex(account_xpub) + '/0/0)'])
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + xpub_to_pub_hex(account_xpub) + '/0/0)'])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
# P2SH wrapped SegWit address using xpub:
process_commands(self.dev_args + ['displayaddress', '--desc', 'sh(wpkh([' + self.fingerprint + '/49h/1h/0h)]' + p2sh_segwit_account_xpub + '/0/0))'])
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'sh(wpkh([' + self.fingerprint + '/49h/1h/0h]' + p2sh_segwit_account_xpub + '/0/0))'])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
# Legacy address
process_commands(self.dev_args + ['displayaddress', '--desc', 'pkh([' + self.fingerprint + '/44h/1h/0h)]' + legacy_account_xpub + '/0/0)'])
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'pkh([' + self.fingerprint + '/44h/1h/0h]' + legacy_account_xpub + '/0/0)'])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
# Should check xpub
result = process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + "not_and_xpub" + '/0/0)'])
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + "not_and_xpub" + '/0/0)'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['code'], -7)
# Should check hex pub
result = process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h)]' + "not_and_xpub" + '/0/0)'])
result = self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([' + self.fingerprint + '/84h/1h/0h]' + "not_and_xpub" + '/0/0)'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['code'], -7)
# Should check fingerprint
process_commands(self.dev_args + ['displayaddress', '--desc', 'wpkh([00000000/84h/1h/0h)]' + account_xpub + '/0/0)'])
self.do_command(self.dev_args + ['displayaddress', '--desc', 'wpkh([00000000/84h/1h/0h]' + account_xpub + '/0/0)'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['code'], -7)

View File

@ -37,6 +37,7 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
# params
type = 'digitalbitbox'
full_type = 'digitalbitbox'
path = 'udp:127.0.0.1:35345'
fingerprint = 'a31b978a'
master_xpub = 'xpub6BsWJiRvbzQJg3J6tgUKmHWYbHJSj41EjAAje6LuDwnYLqLiNSWK4N7rCXwiUmNJTBrKL8AEH3LBzhJdgdxoy4T9aMPLCWAa6eWKGCFjQhq'
@ -93,7 +94,7 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
self.assertTrue(result['success'])
# Reset back to original
result = process_commands(self.dev_args + ['wipe'])
result = self.do_command(self.dev_args + ['wipe'])
self.assertTrue(result['success'])
send_plain(b'{"password":"0000"}', dev)
send_encrypt(json.dumps({"seed":{"source":"backup","filename":"test_backup.pdf","key":"key"}}), '0000', dev)
@ -126,11 +127,11 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
# Generic Device tests
suite = unittest.TestSuite()
suite.addTest(DeviceTestCase.parameterize(TestDBBManCommands, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDBBManCommands, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, '0000', interface=interface))
return suite
if __name__ == '__main__':

View File

@ -4,6 +4,7 @@ import argparse
import atexit
import json
import os
import shlex
import socket
import subprocess
import sys
@ -82,12 +83,15 @@ class KeepkeyTestCase(unittest.TestCase):
return suite
def do_command(self, args):
cli_args = []
for arg in args:
cli_args.append(shlex.quote(arg))
if self.interface == 'cli':
proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'bindist':
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'stdin':
@ -176,11 +180,19 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
result = self.do_command(self.dev_args + ['sendpin', '1234'])
self.assertEqual(result['error'], 'This device does not need a PIN')
self.assertEqual(result['code'], -11)
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_pin_sent'])
# Set a PIN
device.wipe(self.client)
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test')
self.client.call(messages.ClearSession())
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertTrue(dev['needs_pin_sent'])
result = self.do_command(self.dev_args + ['promptpin'])
self.assertTrue(result['success'])
@ -208,6 +220,11 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
result = self.do_command(self.dev_args + ['sendpin', pin])
self.assertTrue(result['success'])
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_pin_sent'])
# Sending PIN after unlock
result = self.do_command(self.dev_args + ['promptpin'])
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
@ -216,12 +233,58 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
self.assertEqual(result['code'], -11)
def test_passphrase(self):
# There's no passphrase
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertEquals(dev['fingerprint'], '95d8f670')
# Setting a passphrase won't change the fingerprint
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
for dev in result:
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertEquals(dev['fingerprint'], '95d8f670')
# Set a passphrase
device.wipe(self.client)
self.client.set_passphrase('pass')
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=True, label='test')
self.client.call(messages.ClearSession())
# A passphrase will need to be sent
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertTrue(dev['needs_passphrase_sent'])
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
for dev in result:
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
fpr = dev['fingerprint']
# A different passphrase would not change the fingerprint
result = self.do_command(self.dev_args + ['-p', 'pass2', 'enumerate'])
for dev in result:
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertEqual(dev['fingerprint'], fpr)
# Clearing the session and starting a new one with a new passphrase should change the passphrase
self.client.call(messages.ClearSession())
result = self.do_command(self.dev_args + ['-p', 'pass3', 'enumerate'])
for dev in result:
if dev['type'] == 'keepkey' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertNotEqual(dev['fingerprint'], fpr)
def keepkey_test_suite(emulator, rpc, userpass, interface):
# Redirect stderr to /dev/null as it's super spammy
sys.stderr = open(os.devnull, 'w')
# Device info for tests
type = 'keepkey'
full_type = 'keepkey'
path = 'udp:127.0.0.1:21324'
fingerprint = '95d8f670'
master_xpub = 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH'
@ -229,11 +292,11 @@ def keepkey_test_suite(emulator, rpc, userpass, interface):
# Generic Device tests
suite = unittest.TestSuite()
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyGetxpub, emulator=dev_emulator, interface=interface))
suite.addTest(KeepkeyTestCase.parameterize(TestKeepkeyManCommands, emulator=dev_emulator, interface=interface))
return suite

View File

@ -73,12 +73,12 @@ def ledger_test_suite(rpc, userpass, interface):
# Generic Device tests
suite = unittest.TestSuite()
suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestLedgerDisabledCommands, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, 'ledger', 'ledger', path, fingerprint, master_xpub, interface=interface))
return suite
if __name__ == '__main__':

View File

@ -4,6 +4,8 @@ import argparse
import atexit
import json
import os
import shlex
import signal
import socket
import subprocess
import sys
@ -35,7 +37,7 @@ class TrezorEmulator(DeviceEmulator):
def start(self):
# Start the Trezor emulator
self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path))
self.emulator_proc = subprocess.Popen(['./' + os.path.basename(self.emulator_path)], cwd=os.path.dirname(self.emulator_path), stdout=subprocess.DEVNULL, env={'SDL_VIDEODRIVER': 'dummy', 'PYOPT': '0'}, shell=True, preexec_fn=os.setsid)
# Wait for emulator to be up
# From https://github.com/trezor/trezor-mcu/blob/master/script/wait_for_emulator.py
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@ -63,8 +65,8 @@ class TrezorEmulator(DeviceEmulator):
return client
def stop(self):
self.emulator_proc.kill()
self.emulator_proc.wait()
os.killpg(os.getpgid(self.emulator_proc.pid), signal.SIGINT)
os.waitpid(self.emulator_proc.pid, 0)
class TrezorTestCase(unittest.TestCase):
def __init__(self, emulator, interface='library', methodName='runTest'):
@ -82,12 +84,15 @@ class TrezorTestCase(unittest.TestCase):
return suite
def do_command(self, args):
cli_args = []
for arg in args:
cli_args.append(shlex.quote(arg))
if self.interface == 'cli':
proc = subprocess.Popen(['hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
proc = subprocess.Popen(['hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'bindist':
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
proc = subprocess.Popen(['../dist/hwi ' + ' '.join(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'stdin':
@ -99,10 +104,10 @@ class TrezorTestCase(unittest.TestCase):
return process_commands(args)
def __str__(self):
return 'trezor: {}'.format(super().__str__())
return 'trezor 1: {}'.format(super().__str__())
def __repr__(self):
return 'trezor: {}'.format(super().__repr__())
return 'trezor 1: {}'.format(super().__repr__())
# Trezor specific getxpub test because this requires device specific thing to set xprvs
class TestTrezorGetxpub(TrezorTestCase):
@ -176,11 +181,19 @@ class TestTrezorManCommands(TrezorTestCase):
result = self.do_command(self.dev_args + ['sendpin', '1234'])
self.assertEqual(result['error'], 'This device does not need a PIN')
self.assertEqual(result['code'], -11)
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_pin_sent'])
# Set a PIN
device.wipe(self.client)
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='1234', passphrase_protection=False, label='test')
self.client.call(messages.ClearSession())
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertTrue(dev['needs_pin_sent'])
result = self.do_command(self.dev_args + ['promptpin'])
self.assertTrue(result['success'])
@ -208,6 +221,11 @@ class TestTrezorManCommands(TrezorTestCase):
result = self.do_command(self.dev_args + ['sendpin', pin])
self.assertTrue(result['success'])
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_pin_sent'])
# Sending PIN after unlock
result = self.do_command(self.dev_args + ['promptpin'])
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
@ -216,7 +234,51 @@ class TestTrezorManCommands(TrezorTestCase):
self.assertEqual(result['error'], 'The PIN has already been sent to this device')
self.assertEqual(result['code'], -11)
def trezor_test_suite(emulator, rpc, userpass, interface):
def test_passphrase(self):
# There's no passphrase
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertEquals(dev['fingerprint'], '95d8f670')
# Setting a passphrase won't change the fingerprint
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertEquals(dev['fingerprint'], '95d8f670')
# Set a passphrase
device.wipe(self.client)
load_device_by_mnemonic(client=self.client, mnemonic='alcohol woman abuse must during monitor noble actual mixed trade anger aisle', pin='', passphrase_protection=True, label='test')
self.client.call(messages.ClearSession())
# A passphrase will need to be sent
result = self.do_command(self.dev_args + ['enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertTrue(dev['needs_passphrase_sent'])
result = self.do_command(self.dev_args + ['-p', 'pass', 'enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
fpr = dev['fingerprint']
# A different passphrase would not change the fingerprint
result = self.do_command(self.dev_args + ['-p', 'pass2', 'enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertEqual(dev['fingerprint'], fpr)
# Clearing the session and starting a new one with a new passphrase should change the passphrase
self.client.call(messages.Initialize())
result = self.do_command(self.dev_args + ['-p', 'pass3', 'enumerate'])
for dev in result:
if dev['type'] == 'trezor' and dev['path'] == 'udp:127.0.0.1:21324':
self.assertFalse(dev['needs_passphrase_sent'])
self.assertNotEqual(dev['fingerprint'], fpr)
def trezor_test_suite(emulator, rpc, userpass, interface, model_t=False):
# Redirect stderr to /dev/null as it's super spammy
sys.stderr = open(os.devnull, 'w')
@ -227,15 +289,21 @@ def trezor_test_suite(emulator, rpc, userpass, interface):
master_xpub = 'xpub6D1weXBcFAo8CqBbpP4TbH5sxQH8ZkqC5pDEvJ95rNNBZC9zrKmZP2fXMuve7ZRBe18pWQQsGg68jkq24mZchHwYENd8cCiSb71u3KD4AFH'
dev_emulator = TrezorEmulator(emulator)
if model_t:
full_type = 'trezor_t'
else:
full_type = 'trezor_1'
# Generic Device tests
suite = unittest.TestSuite()
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(TrezorTestCase.parameterize(TestTrezorGetxpub, emulator=dev_emulator, interface=interface))
suite.addTest(TrezorTestCase.parameterize(TestTrezorManCommands, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDeviceConnect, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestGetKeypool, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, rpc, userpass, type, full_type, path, fingerprint, master_xpub, emulator=dev_emulator, interface=interface))
if not model_t:
suite.addTest(TrezorTestCase.parameterize(TestTrezorGetxpub, emulator=dev_emulator, interface=interface))
suite.addTest(TrezorTestCase.parameterize(TestTrezorManCommands, emulator=dev_emulator, interface=interface))
return suite
if __name__ == '__main__':
@ -243,10 +311,11 @@ if __name__ == '__main__':
parser.add_argument('emulator', help='Path to the Trezor emulator')
parser.add_argument('bitcoind', help='Path to bitcoind binary')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library')
parser.add_argument('--model_t', help='The emulator is for the Trezor T', action='store_true')
args = parser.parse_args()
# Start bitcoind
rpc, userpass = start_bitcoind(args.bitcoind)
suite = trezor_test_suite(args.emulator, rpc, userpass, args.interface)
suite = trezor_test_suite(args.emulator, rpc, userpass, args.interface, args.model_t)
unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)

39
test/test_udevrules.py Executable file
View File

@ -0,0 +1,39 @@
#! /usr/bin/env python3
import unittest
import json
import filecmp
from os import makedirs, remove, removedirs, walk, path
from hwilib.cli import process_commands
class TestUdevRulesInstaller(unittest.TestCase):
INSTALLATION_FOLDER = 'rules.d'
SOURCE_FOLDER = '../udev'
@classmethod
def setUpClass(cls):
# Create directory where copy the udev rules to.
makedirs(cls.INSTALLATION_FOLDER, exist_ok=True)
@classmethod
def tearDownClass(self):
for root, dirs, files in walk(self.INSTALLATION_FOLDER, topdown=False):
for name in files:
remove(path.join(root, name))
removedirs(self.INSTALLATION_FOLDER)
def test_rules_file_are_copied(self):
result = process_commands( ['installudevrules', '--source', self.SOURCE_FOLDER, '--location', self.INSTALLATION_FOLDER])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['error'], 'Need to be root.')
self.assertEqual(result['code'], -16)
# Assert files wre copied
for root, dirs, files in walk(self.INSTALLATION_FOLDER, topdown=False):
for file_name in files:
src = path.join(self.SOURCE_FOLDER, file_name)
tgt = path.join(self.INSTALLATION_FOLDER, file_name)
self.assertTrue(filecmp.cmp(src, tgt))
if __name__ == "__main__":
unittest.main()