Compare commits

...

79 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
0cf6bb9998
commands.py: use fingerprint if already captured by client 2019-04-29 10:54:19 -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
c628206d31
Merge #154: Make versioned compressed packages for releases
cb2ed16 Build Windows binary in travis as well (Andrew Chow)
a86323d Make versioned compressed packages for releases (Andrew Chow)

Pull request description:

  The current build scripts produce binary files named `hwi` and `hwi.exe` which is both unversioned and conflicting for the Linux and MacOS binaries. For the 1.0.0 binaries, I had compressed these and named them myself before uploading. This PR changes the build scripts to compress and name the packages too. Additionally the names will include the version number which I forgot to do for 1.0.0.

ACKs for commit cb2ed1:

Tree-SHA512: 07a167f45ce3652c347350574c2dd0d2d7f39724665aa6b4ebb2b41212bf2526ff21fe36b732cb01890ff051e35d57dc91fa4220ec22f5395a02f3c9ed6bf266
2019-04-21 19:16:22 -04:00
Andrew Chow
cb2ed1654c Build Windows binary in travis as well 2019-04-21 18:18:50 -04:00
Andrew Chow
a86323d419 Make versioned compressed packages for releases 2019-04-21 18:18:50 -04:00
Andrew Chow
b5d74ec24c
Merge #153: Include Windows UCRT in Windows build
f109365 Install Windoew Universal C++ Runtime from Windows 10 SDK in Windows build (Andrew Chow)

Pull request description:

  Older versions of Windows needs to have the Windows Universal C++ Runtime packaged with the binary to work. This PR adds to the `build_wine.sh` build script to download the DLLs for the UCRT and package them with the Windows build.

  Fixes #150

ACKs for commit f10936:

Tree-SHA512: 7b9b98cdc4e329cf228dc42a09aa434f984c4db90b09b2ca6cb3dd7d8c1957cd717f761e8a1eeadbd8662f74452141a4df583ab40d939c57e20f0b0f83625b47
2019-04-21 18:08:02 -04:00
Andrew Chow
30146a26b8
Merge #156: Add Trezor Model T
95bd88e Add Trezor Model T (nopara73)

Pull request description:

  # Notes

  - I marked with `?` the functions I did not test myself.
  - I can make a PR in a way that I add Trezor Model T to the last row so the diff would be easier to see if you prefer.

ACKs for commit 95bd88:
  achow101:
    ACK 95bd88e41d

Tree-SHA512: fb2934f3a0cbc0ce4f473945be1f457f0e0a6fea17bdd6d89b8d375a249d251a68188de3d20a00a4954daa819734ebb3aad871e2b0aeb3ac231fc200cdba5eff
2019-04-21 18:05:45 -04:00
Andrew Chow
bfc29c26a5
Merge #155: Fix last command format at examples.md
9e82690 Fix last command format at `examples.md` (Roman Zeyde)

Pull request description:

ACKs for commit 9e8269:
  achow101:
    ACK 9e826909cf

Tree-SHA512: 55b039d8f4ad5205b83e92e6a2c36c9688055ed7083c6de75479a3e712b77087a82a1f8f6d5ee1d299fdd1c5d012ef368f4d741e52eca18a6119ea1d99b78bff
2019-04-21 18:04:35 -04:00
nopara73
95bd88e41d
Add Trezor Model T 2019-04-21 22:20:19 +02:00
Roman Zeyde
9e826909cf
Fix last command format at examples.md 2019-04-20 20:38:16 +03:00
Andrew Chow
f109365047 Install Windoew Universal C++ Runtime from Windows 10 SDK in Windows build
Download and extract the Universal C++ Runtime from the Windows 10 SDK
and copy the DLLs to C:\Windows\System32 when doing Windows build.
This will make sure that the proper DLLs are included in the Windows
binary so that it works on older Windows.
2019-04-19 22:37:49 -04: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
Andrew Chow
6e92b7d40f
Merge #147: Add udev rules for all supported devices
397993b [ci skip] Document udev rules (Andrew Chow)
1222d9a Add udev rules for all supported devices (Andrew Chow)

Pull request description:

  It's useful to have all of the udev rules needed for all devices in one location instead of having to go to every vendor's webpage to find these.

  Original sources:
  `20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules
  `51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules
  `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://shiftcrypto.ch/start_linux
  `51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules
  `51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules

ACKs for commit 397993:

Tree-SHA512: c2dbe4dcff6e54ecb71724faeacc1aaca06b5a90dbc22289694ff40e0af30c756f42af8e52208c155d9fef54b099ac51679d9f9cbc0f0de1384ce47e39c00b3c
2019-04-16 14:32:43 -04:00
Andrew Chow
397993bfdc [ci skip] Document udev rules 2019-04-16 14:30:36 -04:00
Andrew Chow
f94913739e
Merge #146: Import HID in Ledger driver
d065307 Import HID in Ledger driver (Andrew Chow)

Pull request description:

  Apparently this is needed, especially for newly setup environments.

  Fixes #143

ACKs for commit d06530:

Tree-SHA512: 4434f947a1cf1191445ffb5c51ebb00d221fd05f64b48b6cb403feeca378bfcd59f81e414b90b16552cf1af1df452603e54aa2e90f01c741ac3c376d96d7a47e
2019-04-16 14:18:36 -04:00
Andrew Chow
4814a293e8
Merge #142: Document how to handle binary format files from command line with bas…
f096c18 Document how to handle binary format files from command line with bash example (Gregory Sanders)

Pull request description:

  …h example

  Since these are quite standard utilities, we may want to take the unix philosophy and let other utils take care of these common conversions.

ACKs for commit f096c1:
  achow101:
    ACK f096c18478

Tree-SHA512: ee04a1e4e7f4106f7afcb5130cd8bb8fb1446a79b8babaf14c7aed4b753699b29ed8dfca057928289848f562b24663850377eccbbef27ba9c60f011a901cdc5a
2019-04-15 20:29:24 -04:00
Andrew Chow
1222d9a492 Add udev rules for all supported devices 2019-04-15 19:50:26 -04:00
Andrew Chow
d065307317 Import HID in Ledger driver 2019-04-15 19:45:39 -04:00
Andrew Chow
685faa2e5c bump version number to 1.0.0 2019-03-15 16:43:53 -04:00
Gregory Sanders
f096c18478 Document how to handle binary format files from command line with bash example 2019-03-15 09:50:02 -04:00
Andrew Chow
7b0e84e26c
Merge #140: Add Keepkey Webusb device ids
6f67659 Add Keepkey Webusb device ids (Andrew Chow)

Pull request description:

  Keepkey's latest firmware uses webusb but has different device ids. Add those ids so that that interface can be used.

Tree-SHA512: 082f1eb1fd827af2e4d8380101477e879ed026910cc57413c69d941fe4bc75483ac10318f887a53796dbd44e0ce1206cba1dbc3d50ada2e0314f78748ab48faf
2019-03-14 23:44:44 -04:00
Andrew Chow
6f67659e07 Add Keepkey Webusb device ids 2019-03-14 23:09:44 -04:00
Andrew Chow
a83fdc6be3
Merge #124: Add a switch for interactivity and make setup and restore interactive only commands
4453555 Add interactivity to Trezor setup and restore (Andrew Chow)
18857d9 Add interactive option and move setup and restore to interactive only (Andrew Chow)

Pull request description:

  Currently using setup and restore on the Trezor and Keepkey (which are basically the only devices that support setup and restore) are broken. They require user interaction. Instead of removing them entirely, move those commands behind a switch, `--interactive`.

Tree-SHA512: 4c76a94d7dc3b876f57eb417f22b14cb0a4ecbb97dc79ba675dc49670369e5682edbaf846946e246a028532f92c32f3e0f67b66d000d341368937c856dea5347
2019-03-14 20:32:10 -04:00
Andrew Chow
4453555c21 Add interactivity to Trezor setup and restore 2019-03-14 16:39:12 -04:00
Andrew Chow
18857d9cd0 Add interactive option and move setup and restore to interactive only 2019-03-14 16:39:12 -04:00
Andrew Chow
0dd2e86394
Merge #126: Add option to enter commands over stdin
49bc7fa Add tests for stdin interface and travis job (Andrew Chow)
0185391 Add --stdin to enter commands and arguments over stdin (Andrew Chow)

Pull request description:

  This PR adds a `--stdin` option which allows arguments to be entered over stdin in a way similar to bitcoin-cli's `--stdin` option.

  Built on #125 and to test the `stdin` interface

Tree-SHA512: 48e04298b3537ac59523e40fab3e1709d2ec4ca50284b0bcbbe9062f284bcfbf0cbc4492bd5b8fa673be35375fe8d9436d4aaec0a304b574ba411ee0fb284c0d
2019-03-14 15:00:24 -04:00
Andrew Chow
d0ac6b9398
Merge #138: Update documentation and change name to Hardware Wallet Interface
376245a Update tests documentation with new tests (Andrew Chow)
0235baf List all command support (Andrew Chow)
7929692 Update Bitcoin Core usage docs to match current software behavior (Andrew Chow)
b7873ac Describe the other files in contrib/ (Andrew Chow)
fc33fc5 List additional dependencies (Andrew Chow)
5e42d3d Rename to Hardware Wallet Interface and describe it a bit more (Andrew Chow)

Pull request description:

  Updates the docs to reflect current behavior and describe the stuff that is currently in the repo.

  Also renames the project from Hardware Wallet Interaction scripts to Hardware Wallet Interface.

Tree-SHA512: b5bd81110ac6181011629c39dad3bbca1bfb2ddc05f5d190a9d91685c30de0d535c3d0e893b510638eb963adffe63db388a84a19a377fbb9ec820ce4aa2fc55f
2019-03-11 10:24:50 -04:00
Andrew Chow
49bc7fa5da Add tests for stdin interface and travis job 2019-03-11 10:23:45 -04:00
Andrew Chow
0185391c72 Add --stdin to enter commands and arguments over stdin 2019-03-11 10:23:45 -04:00
Andrew Chow
2bf3d418e1
Merge #139: Add --version
d924f39 Add --version option and show version in help text (Andrew Chow)
b99385c Add version number to __init__.py and docs to update that (Andrew Chow)

Pull request description:

  Adds version info and a `--version` option.

Tree-SHA512: 56562da645b555dabb40ba0508b35fc6a2bde0f510668f51f1be402a743796ca1be27afce4980589718768260c7469b373a6b8c8cc3a29e637ed7af071bd5bd4
2019-03-09 14:28:12 -05:00
Andrew Chow
d924f39b6b Add --version option and show version in help text 2019-03-09 12:39:09 -05:00
Andrew Chow
376245ae0f Update tests documentation with new tests 2019-03-09 01:17:05 -05:00
Andrew Chow
0235baf377 List all command support 2019-03-09 01:17:05 -05:00
Andrew Chow
7929692c9d Update Bitcoin Core usage docs to match current software behavior 2019-03-09 01:17:05 -05:00
Andrew Chow
b99385c8e2 Add version number to __init__.py and docs to update that 2019-03-08 18:05:10 -05:00
Andrew Chow
b7873acddc Describe the other files in contrib/ 2019-03-08 16:55:13 -05:00
Andrew Chow
fc33fc5bbe List additional dependencies 2019-03-08 16:55:12 -05:00
Andrew Chow
5e42d3d696 Rename to Hardware Wallet Interface and describe it a bit more 2019-03-08 15:20:18 -05:00
Andrew Chow
07ece90769
Merge #121: Tools for deterministic builds of standalone binaries and distribution archives
8931ae0 Run tests using the binary distribution (Andrew Chow)
39a6fc9 Fixes for Windows (Andrew Chow)
a229de0 Update .travis.yml to use poetry for build (Andrew Chow)
77257a1 Add build scripts and documentation for building releases (Andrew Chow)
9e04d1a Add a hwi.spec file for pyinstaller to build standalone binaries (Andrew Chow)
d6b24b8 Add pyproject.toml and poetry.lock for poetry dependency manager (Andrew Chow)

Pull request description:

  This PR adds several scripts and tools for making standalone binaries of the `hwi.py` script and for creating distribution archives that will go on pypi.org.

  To achieve deterministic builds, the dependencies used must be locked to specific versions and hashes. To do this, I have added configuration files for using the [Poetry dependency manager](https://github.com/sdispater/poetry). Because Poetry uses a `pyproject.toml` file instead of `setup.py`, I have created a helper script which will automatically generate the proper `setup.py` file from `pyproject.toml`. The reason I chose Poetry instead of Pipenv for this task is because it has the ability to do deterministic builds of the distribution archives (python wheel and source tar for pypi.org) which Pipenv does not have.

  Additionally scripts have been added to the newly created `contrib/` folder which will perform the deterministic builds of the binaries and distribution archives. The builds of the binaries are done using [pyinstaller](http://www.pyinstaller.org/). In order to build for different platforms, the `contrib/build_bin.sh` script needs to be run on each of the platforms we wish to release for. It can also be run in wine to do windows builds (see `contrib/build_wine.sh`). The configuration file that pyinstaller needs has also been added.

  Lastly the pyenv version for this project has been bumped to 3.6.8 since using python 3.5.x produced standalone binaries that did not work.

  This PR is built on #120 as reducing the number of dependencies fixed several issues with the standalone binary builds.

Tree-SHA512: abc1a6ac06d663b1316cde254980b0b1e8c392a6ffe478710df7c8e48a344cd57105e83555ec8fdcdc30e2b7d6d9cd6464367afda80653cb3cbc3acaf6119f48
2019-03-08 15:04:01 -05:00
Andrew Chow
8931ae0e90 Run tests using the binary distribution 2019-03-08 00:42:43 -05:00
Andrew Chow
39a6fc9654 Fixes for Windows 2019-03-08 00:17:33 -05:00
Andrew Chow
a229de0780 Update .travis.yml to use poetry for build 2019-03-08 00:17:33 -05:00
Andrew Chow
77257a18da Add build scripts and documentation for building releases
Adds builds scripts that are used to build releases deterministically.
Also adds documentation that explains the release process and what
the build scripts do.
2019-03-08 00:17:33 -05:00
Andrew Chow
9e04d1a16a Add a hwi.spec file for pyinstaller to build standalone binaries 2019-03-08 00:17:33 -05:00
Andrew Chow
d6b24b853d Add pyproject.toml and poetry.lock for poetry dependency manager
pyproject.toml contains everything that was in the setup.py. The setup.py
file is replaced with the one that poetry automatically generates.
2019-03-01 19:47:07 -05:00
Andrew Chow
6e4ffcdd1b
Merge #134: Use new importmulti array range format
2dc818c Use new importmulti array range format (Andrew Chow)

Pull request description:

  The range format changed again.

Tree-SHA512: fb81bfdceb5e0451ac0d72537a788f3c9d1aba0381afb129b1f1784d1635c154ec3b0ac93a0c3ea096d140833f18a6b47af1f495c8860d0401a05770c5305c23
2019-03-01 17:45:41 -05:00
53 changed files with 1395 additions and 227 deletions

View File

@ -1 +1 @@
3.5.6
3.6.8

View File

@ -2,7 +2,7 @@ language: python
os: linux
dist: xenial
python:
- '3.5'
- '3.6.8'
cache:
pip: true
ccache: true
@ -40,7 +40,7 @@ addons:
- cython3
- ccache
install:
- pip install pipenv pysdl2 python-bitcoinrpc protobuf
- 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
@ -48,10 +48,35 @@ install:
# Build emulators/simulators and bitcoind
- cd test; ./setup_environment.sh; cd ..
- pip uninstall -y trezor # Hack to get rid of master branch version of trezor that is installed for trezor-mcu build
- python setup.py install
- poetry install
jobs:
include:
- name: With process_commands interface
script: cd test; ./run_tests.py --interface=library
script: cd test; poetry run ./run_tests.py --interface=library
- name: With command line interface
script: cd test; ./run_tests.py --interface=cli
script: cd test; poetry run ./run_tests.py --interface=cli
- name: With stdin interface
script: cd test; poetry run ./run_tests.py --interface=stdin
- name: With linux binary distribution command line interface
services: docker
before_script:
- docker build -t hwi-builder -f contrib/build.Dockerfile .
script:
- docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_wine.sh && contrib/build_dist.sh"
- sudo chown -R `whoami`:`whoami` dist/
- cd test; poetry run ./run_tests.py --interface=bindist
- cd ..; sha256sum dist/*
- name: macOS binary distribution (no tests)
os: osx
osx_image: xcode7.3
language: generic
addons:
artifacts:
working_dir: dist
install:
- brew update && brew upgrade pyenv
- brew install libusb
- cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.8
script:
- contrib/build_bin.sh
- shasum -a 256 dist/*

View File

@ -1,14 +1,33 @@
# Bitcoin Hardware Wallet Interaction scripts
# Bitcoin Hardware Wallet Interface
[![Build Status](https://travis-ci.org/bitcoin-core/HWI.svg?branch=master)](https://travis-ci.org/bitcoin-core/HWI)
This project contains several scripts for interacting with Bitcoin hardware wallets.
The Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.
It provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.
Python software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.
## Prerequisites
Python 3 is required. The libraries and udev rules for each device must also be installed.
Python 3 is required. The libraries and udev rules for each device must also be installed. Some libraries will need to be installed
Install all of the libraries using `pip` (in virtualenv or system):
For Ubuntu/Debian:
```
sudo apt install libusb-1.0-0-dev libudev-dev
```
For macOS:
```
brew install libusb
```
This project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager.
Once 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:
```
poetry install
```
Pip can also be used to install all of the dependencies (in virtualenv or system):
```
pip3 install hidapi # HID API needed in general
@ -45,29 +64,29 @@ The below table lists what devices and features are supported for each device.
Please also see [docs](docs/) for additional information about each device.
| Feature \ Device | Ledger Nano S | Trezor One | Digital BitBox | KeepKey | Coldcard |
|:---:|:---:|:---:|:---:|:---:|:---:|
| Support Planned | Yes | Yes | Yes | Yes | Yes |
| Implemented | Yes | Yes | Yes | Yes | Yes |
| xpub retrieval | 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 |
| P2WPKH Inputs | 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 |
| Display on device screen | Yes | Yes | N/A | Yes | Yes |
| Feature \ Device | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 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 | 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 | 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

35
contrib/README.md Normal file
View File

@ -0,0 +1,35 @@
# Assorted tools
## `build_bin.sh`
Creates a virtualenv with the locked dependencies using Poetry. Then uses pyinstaller to create a standalone binary for the OS type currently running.
## `build_dist.sh`
Creates a virtualenv with the locked dependencies using Poetry. Then uses Poetry to produce deterministic builds of the wheel and sdist for upload to PyPi
`faketime` needs to be installed
## `build_wine.sh`
Sets up Wine with Python and everything needed to build Windows binaries. Creates a virtualenv with the locked dependencies using Poetry. Then uses pyinstaller to create a standalone Windows binary.
`wine` needs to be installed
## `generate_setup.sh`
Builds the source distribution and extracts the setup.py from it.
## `build.Dockerfile`
A Dockerfile for setting up the deterministic build environment.
# Other files
## `reproducible-python.diff`
A path for python in order to do a deterministic build of Python for the deterministically built binaries.
## `pyinstaller-hooks/hook-hwilib.devices.py`
Pyinstaller hook so that the device drivers are actually included. Due to how the imports work, we need this hook.

50
contrib/build.Dockerfile Normal file
View File

@ -0,0 +1,50 @@
FROM debian:stretch-slim
SHELL ["/bin/bash", "-c"]
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y \
apt-transport-https \
git \
make \
build-essential \
libssl-dev \
zlib1g-dev \
libbz2-dev \
libreadline-dev \
libsqlite3-dev \
wget \
curl \
llvm \
libncurses5-dev \
xz-utils \
libxml2-dev \
libxmlsec1-dev \
libffi-dev \
liblzma-dev \
libusb-1.0-0-dev \
libudev-dev \
faketime \
zip \
dos2unix
RUN curl https://pyenv.run | bash
ENV PATH="/root/.pyenv/bin:$PATH"
COPY contrib/reproducible-python.diff /opt/reproducible-python.diff
ENV PYTHON_CONFIGURE_OPTS="--enable-shared"
ENV BUILD_DATE="Jan 1 2019"
ENV BUILD_TIME="00:00:00"
RUN eval "$(pyenv init -)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.6.8
RUN dpkg --add-architecture i386
RUN wget -nc https://dl.winehq.org/wine-builds/winehq.key
RUN apt-key add winehq.key
RUN echo "deb https://dl.winehq.org/wine-builds/debian/ stretch main" >> /etc/apt/sources.list
RUN apt-get update
RUN apt-get install --install-recommends -y \
wine-stable-amd64 \
wine-stable-i386 \
wine-stable \
winehq-stable \
p7zip-full

36
contrib/build_bin.sh Executable file
View File

@ -0,0 +1,36 @@
#! /bin/bash
# Script for building standalone binary releases deterministically
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
pip install -U pip
pip install poetry==0.12.12
# Setup poetry and install the dependencies
poetry install
# We now need to remove debugging symbols and build id from the hidapi SO file
so_dir=`dirname $(dirname $(poetry run which python))`/lib/python3.6/site-packages
find ${so_dir} -name '*.so' -type f -execdir strip '{}' \;
if [[ $OSTYPE != *"darwin"* ]]; then
find ${so_dir} -name '*.so' -type f -execdir strip -R .note.gnu.build-id '{}' \;
fi
# We also need to change the timestamps of all of the base library files
lib_dir=`pyenv root`/versions/3.6.8/lib/python3.6
TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \;
# Make the standalone binary
export PYTHONHASHSEED=42
poetry run pyinstaller hwi.spec
unset PYTHONHASHSEED
# Make the final compressed package
pushd dist
VERSION=`poetry run hwi --version | cut -d " " -f 2`
OS=`uname | tr '[:upper:]' '[:lower:]'`
if [[ $OS == "darwin" ]]; then
OS="mac"
fi
tar -czf "hwi-${VERSION}-${OS}-amd64.tar.gz" hwi
popd

15
contrib/build_dist.sh Executable file
View File

@ -0,0 +1,15 @@
#! /bin/bash
# Script for building pypi distribution archives deterministically
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
pip install -U pip
pip install poetry==0.12.12
# Setup poetry and install the dependencies
poetry install
# Make the distribution archives for pypi
poetry build -f wheel
# faketime is needed to make sdist detereministic
TZ=UTC faketime -f "2019-01-01 00:00:00" poetry build -f sdist

76
contrib/build_wine.sh Executable file
View File

@ -0,0 +1,76 @@
#!/bin/bash
# Script which sets up Wine and builds the Windows standalone binary
set -e
PYTHON_VERSION=3.6.8
PYTHON_FOLDER="python3"
PYHOME="c:/$PYTHON_FOLDER"
PYTHON="wine $PYHOME/python.exe -OO -B"
LIBUSB_URL=https://github.com/libusb/libusb/releases/download/v1.0.22/libusb-1.0.22.7z
LIBUSB_HASH="671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b"
WINDOWS_SDK_URL=http://go.microsoft.com/fwlink/p/?LinkID=2033686
WINDOWS_SDK_HASH="016981259708e1afcab666c7c1ff44d1c4d63b5e778af8bc41b4f6db3d27961a"
WINDOWS_SDK_VERSION=10.0.17763.0
wine 'wineboot'
# Install Python
# Get the PGP keys
wget -N -c "https://www.python.org/static/files/pubkeys.txt"
gpg --import pubkeys.txt
rm pubkeys.txt
# Install python components
for msifile in core dev exe lib pip tools; do
wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi"
wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi.asc"
gpg --verify "${msifile}.msi.asc" "${msifile}.msi"
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=$PYHOME
rm $msifile.msi*
done
# Get libusb
wget -N -c -O libusb.7z "$LIBUSB_URL"
echo "$LIBUSB_HASH libusb.7z" | sha256sum -c
7za x -olibusb libusb.7z -aoa
cp libusb/MS64/dll/libusb-1.0.dll ~/.wine/drive_c/python3/
rm -r libusb*
# Get the Windows SDK
pushd `mktemp -d`
wget -O sdk.iso "$WINDOWS_SDK_URL"
echo "$WINDOWS_SDK_HASH sdk.iso" | sha256sum -c
7z e sdk.iso
wine msiexec /i "Universal CRT Redistributable-x86_en-us.msi"
cp ~/.wine/drive_c/Program\ Files\ \(x86\)/Windows\ Kits/10/Redist/${WINDOWS_SDK_VERSION}/ucrt/DLLs/x64/*.dll ~/.wine/drive_c/windows/system32/
popd
# Update pip
$PYTHON -m pip install -U pip
# Install Poetry and things needed for pyinstaller
$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
TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \;
# Install python dependencies
POETRY="wine $PYHOME/Scripts/poetry.exe"
sleep 5 # For some reason, pausing for a few seconds makes the next step work
$POETRY install -E windist
# Do the build
export PYTHONHASHSEED=42
$POETRY run pyinstaller hwi.spec
unset PYTHONHASHSEED
# Make the final compressed package
pushd dist
VERSION=`$POETRY run hwi --version | cut -d " " -f 2 | dos2unix`
zip "hwi-${VERSION}-windows-amd64.zip" hwi.exe
popd

32
contrib/generate_setup.sh Executable file
View File

@ -0,0 +1,32 @@
#! /bin/bash
# Generates the setup.py file
set -e
# Setup poetry and install the dependencies
poetry install
# Build the source distribution
poetry build -f sdist
# Extract setup.py from the distribution
unset -v tarball
for file in dist/*
do
if [[ $file -nt $tarball && $file == *".tar.gz" ]]
then
tarball=$file
fi
done
unset -v toextract
for file in `tar -tf $tarball`
do
if [[ $file == *"setup.py" ]]
then
toextract=$file
fi
done
tar -xf $tarball $toextract
mv $toextract .
dir=`echo $toextract | cut -f1 -d"/"`
rm -r $dir

View File

@ -0,0 +1,4 @@
from hwilib.devices import __all__
hiddenimports = []
for d in __all__:
hiddenimports.append('hwilib.devices.' + d)

View File

@ -0,0 +1,13 @@
# DP: Build getbuildinfo.o with DATE/TIME values when defined
--- Makefile.pre.in
+++ Makefile.pre.in
@@ -741,6 +741,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \
-DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \
-DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \
-DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \
+ $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \
+ $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \
-o $@ $(srcdir)/Modules/getbuildinfo.c
Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile

View File

@ -1,6 +1,6 @@
# Using Bitcoin Core with Hardware Wallets
This approach is fairly manual, requires the command line, and requires a patched version of Bitcoin Core.
This approach is fairly manual, requires the command line, and Bitcoin Core >=0.18.0.
Note: For this guide, code lines prefixed with `$` means that the command is typed in the terminal. Lines without `$` are output of the commands.
@ -14,14 +14,14 @@ We are not liable for any coins that may be lost through this method. The softwa
This method of using hardware wallets uses Bitcoin Core as the wallet for monitoring the blockchain. It allows a user to use their own full node instead of relying on an SPV wallet or vendor provided software.
HWI works with Bitcoin Core as of commit [c576979b78b541bf3b4a7cbeee989b55d268e3e1](https://github.com/bitcoin/bitcoin/commit/c576979b78b541bf3b4a7cbeee989b55d268e3e1).
HWI works with Bitcoin Core as of commit [c576979b78b541bf3b4a7cbeee989b55d268e3e1](https://github.com/bitcoin/bitcoin/commit/c576979b78b541bf3b4a7cbeee989b55d268e3e1). It is usable with Bitcoin Core >=0.18.0.
## Setup
Clone the modified Bitcoin Core and build it. Clone HWI.
Clone Bitcoin Core and build it. Clone HWI.
```
$ git clone https://github.com/achow101/bitcoin.git -b hww
$ git clone https://github.com/bitcoin/bitcoin.git
$ cd bitcoin
$ ./autogen.sh
$ ./configure
@ -47,7 +47,7 @@ We will be fetching keys at the BIP 84 default.
```
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool 0 1000
[{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)", "internal": false, "range": {"start": 0, "end": 1000}, "timestamp": "now", "keypool": true, "watchonly": true}]
[{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}]
```
We now create a new Bitcoin Core wallet and import the keys into Bitcoin Core. The output is formatted properly for Bitcoin Core so it can be copy and pasted.
@ -58,7 +58,7 @@ $ ../bitcoin/src/bitcoin-cli createwallet "coldcard" true
"name": "coldcard",
"warning": ""
}
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)", "internal": false, "range": {"start": 0, "end": 1000}, "timestamp": "now", "keypool": true, "watchonly": true}]'
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"desc": "wpkh([8038ecd9/84'/0'/0']xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/0/*)#36sal9a4", "internal": false, "range": [0, 1000], "timestamp": "now", "keypool": true, "watchonly": true}]'
[
{
@ -71,8 +71,8 @@ Now we repeat the `getkeypool` and `importmulti` steps but set a `--internal` fl
```
$ ./hwi.py -f 8038ecd9 getkeypool --wpkh --keypool --internal 0 1000
[{"internal": true, "timestamp": "now", "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)", "keypool": true, "range": {"start": 0, "end": 1000}}]
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": "now", "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)", "keypool": true, "range": {"start": 0, "end": 1000}, "watchonly": true}]'
[{"internal": true, "timestamp": "now", "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": {"start": 0, "end": 1000}}]
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": "now", "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)", "keypool": true, "range": [0, 1000], "watchonly": true}]'
[
{
@ -90,8 +90,8 @@ Here are some examples (`<blockheight>` refers to a block height before the wall
$ ../bitcoin/src/bitcoin-cli rescanblockchain <blockheight>
$ ../bitcoin/src/bitcoin-cli rescanblockchain 500000 # Rescan from block 500000
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": <blockheight>, "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)", "keypool": true, "range": {"start": 0, "end": 1000}, "watchonly": true}]'
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": 500000, "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)", "keypool": true, "range": {"start": 0, "end": 1000}, "watchonly": true}]' # Imports and rescans from block 500000
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": <blockheight>, "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]'
$ ../bitcoin/src/bitcoin-cli -rpcwallet=coldcard importmulti '[{"internal": true, "timestamp": 500000, "desc": "wpkh([8038ecd9/84h/0h/0h]xpub6DR4rqx16YnCcfwFqgwvJdKiWrjDRzqxYTY44aoyHwZDSeSB5n2tqt42aYr9qPKhSKUdftPdTjhHrKKD6WGKVbuyhMvGH76VyKKZubg8o4P/1/*)#qw4uzsdd", "keypool": true, "range": [0, 1000], "watchonly": true}]' # Imports and rescans from block 500000
```
## Usage
@ -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

@ -12,6 +12,7 @@ Current implemented commands are:
- `restore`
- `backup`
- `displayaddress`
- `signmessage`
## Usage Notes

View File

@ -135,3 +135,11 @@ bitcoin-cli scantxoutset start '[{"desc":"wpkh(xpub6DP9afdc7qsz7s7mwAvciAR2dV6vP
"total_amount": 0.00000000
}
```
### Binary format handling
The input and output format supported by HWI is base64, which is prescribed by BIP174 as the string format. Note that the PSBT standard also allows for binary formatting when stored as a file. There is no direct support within HWI, but this can be easily accomplished using common utilities. A bash command-line example is detailed below, where the PSBT binary file is stored in `example.psbt` and only the common utilities `base64` and `jq` are required:
```
cat example.psbt | base64 --wrap=0 | ./hwi.py -t ledger --stdin signtx | jq .[] --raw-output | base64 -d > example_result.psbt
```

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:

51
docs/release-process.md Normal file
View File

@ -0,0 +1,51 @@
# Release Process
1. Bump version number in `pyproject.toml` and `hwilib/__init__.py`, generate the setup.py file, and git tag release
2. Build distribution archives for PyPi with `contrib/build_dist.sh`
3. For MacOS and Linux, use `contrib/build_bin.sh`. This needs to be run on a MacOS machine for the MacOS binary and on a Linux machine for the linux one.
4. For Windows, use `contrib/build_wine.sh` to build the Windows binary using wine
5. Upload distribution archives to PyPi
6. Upload distribution archives and standalone binaries to Github
## Deterministic builds with Docker
Create the docker image:
```
docker build --no-cache -t hwi-builder -f contrib/build.Dockerfile .
```
Build everything
```
docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_dist.sh && contrib/build_wine.sh"
```
## Building macOS binary
Note that the macOS build is non-deterministic.
First install [pyenv](https://github.com/pyenv/pyenv) using whichever method you prefer.
Then a deterministic build of Python 3.6.8 needs to be installed. This can be done with the patch in `contrib/reproducible-python.diff`. First `cd` into HWI's source tree. Then use:
```
cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.6.8
```
Make sure that python 3.6.8 is active
```
$ python --version
Python 3.6.8
```
Now install [Poetry](https://github.com/sdispater/poetry) with `pip install poetry`
Additional dependencies can be installed with:
```
brew install libusb
```
Build the binaries by using `contrib/build_bin.sh`.

46
hwi.spec Normal file
View File

@ -0,0 +1,46 @@
# -*- mode: python -*-
import platform
import subprocess
block_cipher = None
binaries = []
if platform.system() == 'Windows':
binaries = [("c:/python3/libusb-1.0.dll", ".")]
elif platform.system() == 'Linux':
binaries = [("/lib/x86_64-linux-gnu/libusb-1.0.so.0", ".")]
elif platform.system() == 'Darwin':
find_brew_libusb_proc = subprocess.Popen(['brew', '--prefix', 'libusb'], stdout=subprocess.PIPE)
libusb_path = find_brew_libusb_proc.communicate()[0]
binaries = [(libusb_path.rstrip().decode() + "/lib/libusb-1.0.dylib", ".")]
a = Analysis(['hwi.py'],
binaries=binaries,
datas=[],
hiddenimports=[],
hookspath=['contrib/pyinstaller-hooks/'],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
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,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='hwi',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=True )

View File

@ -0,0 +1 @@
__version__ = '1.0.0'

View File

@ -2,15 +2,17 @@
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,
DEVICE_CONN_ERROR,
NO_PASSWORD,
UNKNWON_DEVICE_TYPE,
UNKNOWN_ERROR
UNKNOWN_ERROR,
UNAVAILABLE_ACTION
)
from . import __version__
import argparse
import getpass
@ -37,10 +39,14 @@ def getkeypool_handler(args, client):
return getkeypool(client, path=args.path, start=args.start, end=args.end, internal=args.internal, keypool=args.keypool, account=args.account, sh_wpkh=args.sh_wpkh, wpkh=args.wpkh)
def restore_device_handler(args, client):
return restore_device(client, label=args.label)
if args.interactive:
return restore_device(client, label=args.label)
return {'error': 'restore requires interactive mode', 'code': UNAVAILABLE_ACTION}
def setup_device_handler(args, client):
return setup_device(client, label=args.label, backup_passphrase=args.backup_passphrase)
if args.interactive:
return setup_device(client, label=args.label, backup_passphrase=args.backup_passphrase)
return {'error': 'setup requires interactive mode', 'code': UNAVAILABLE_ACTION}
def signmessage_handler(args, client):
return signmessage(client, message=args.message, path=args.path)
@ -57,8 +63,11 @@ def prompt_pin_handler(args, client):
def send_pin_handler(args, client):
return send_pin(client, pin=args.pin)
def process_commands(args):
parser = argparse.ArgumentParser(description='Access and send commands to a hardware wallet device. Responses are in JSON format')
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')
parser.add_argument('--device-type', '-t', help='Specify the type of device that will be connected. If `--device-path` not given, the first device of this type enumerated is used.')
parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='')
@ -66,6 +75,9 @@ def process_commands(args):
parser.add_argument('--testnet', help='Use testnet prefixes', action='store_true')
parser.add_argument('--debug', help='Print debug statements', action='store_true')
parser.add_argument('--fingerprint', '-f', help='Specify the device to connect to using the first 4 bytes of the hash160 of the master public key. It will connect to the first device that matches this fingerprint.')
parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__))
parser.add_argument('--stdin', help='Enter commands and arguments via stdin', action='store_true')
parser.add_argument('--interactive', '-i', help='Use some commands interactively. Currently required for all device configuration commands', action='store_true')
subparsers = parser.add_subparsers(description='Commands', dest='command')
# work-around to make subparser required
@ -109,7 +121,7 @@ def process_commands(args):
displayaddr_parser.add_argument('--wpkh', action='store_true', help='Display the bech32 version of the address associated with this key path')
displayaddr_parser.set_defaults(func=displayaddress_handler)
setupdev_parser = subparsers.add_parser('setup', help='Setup a device. Passphrase protection uses the password given by -p')
setupdev_parser = subparsers.add_parser('setup', help='Setup a device. Passphrase protection uses the password given by -p. Requires interactive mode')
setupdev_parser.add_argument('--label', '-l', help='The name to give to the device', default='')
setupdev_parser.add_argument('--backup_passphrase', '-b', help='The passphrase to use for the backup, if applicable', default='')
setupdev_parser.set_defaults(func=setup_device_handler)
@ -117,7 +129,7 @@ def process_commands(args):
wipedev_parser = subparsers.add_parser('wipe', help='Wipe a device')
wipedev_parser.set_defaults(func=wipe_device_handler)
restore_parser = subparsers.add_parser('restore', help='Initiate the device restoring process')
restore_parser = subparsers.add_parser('restore', help='Initiate the device restoring process. Requires interactive mode')
restore_parser.add_argument('--label', '-l', help='The name to give to the device', default='')
restore_parser.set_defaults(func=restore_device_handler)
@ -133,7 +145,30 @@ def process_commands(args):
sendpin_parser.add_argument('pin', help='The numeric positions of the PIN')
sendpin_parser.set_defaults(func=send_pin_handler)
args = parser.parse_args(args)
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:
try:
line = input()
# Exit loop when we see 2 consecutive newlines (i.e. an empty line)
if line == '':
break
# Split the line and append it to the cli args
import shlex
cli_args.extend(shlex.split(line))
except EOFError:
# If we see EOF, stop taking input
break
# Parse arguments again for anything entered over stdin
args = parser.parse_args(cli_args)
device_path = args.device_path
device_type = args.device_type
@ -152,6 +187,17 @@ def process_commands(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

@ -2,14 +2,15 @@
# Hardware wallet interaction script
import glob
import importlib
from .serializations import PSBT, Base64ToHex, HexToBase64, hash160
from .base58 import get_xpub_fingerprint_as_id, get_xpub_fingerprint_hex, xpub_to_pub_hex
from os.path import dirname, basename, isfile
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=''):
@ -32,11 +33,7 @@ def get_client(device_type, device_path, password=''):
def enumerate(password=''):
result = []
# Gets the module names of all the files in devices/
files = glob.glob(dirname(__file__)+"/devices/*.py")
modules = [ basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')]
for module in modules:
for module in all_devs:
try:
imported_dev = importlib.import_module('.devices.' + module, __package__)
result.extend(imported_dev.enumerate(password))
@ -53,8 +50,12 @@ def find_device(device_path, password='', device_type=None, fingerprint=None):
client = None
try:
client = get_client(d['type'], d['path'], password)
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
master_fpr = get_xpub_fingerprint_hex(master_xpub)
master_fpr = d.get('fingerprint', None)
if master_fpr is None:
master_xpub = client.get_pubkey_at_path('m/0h')['xpub']
master_fpr = get_xpub_fingerprint_hex(master_xpub)
if fingerprint and master_fpr != fingerprint:
client.close()
continue
@ -156,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)
@ -187,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

@ -0,0 +1,7 @@
__all__ = [
'trezor',
'ledger',
'keepkey',
'digitalbitbox',
'coldcard'
]

View File

@ -8,7 +8,7 @@
#
# - ec_mult, ec_setup, aes_setup, mitm_verify
#
import hid, sys, os
import hid, sys, os, platform
from binascii import b2a_hex, a2b_hex
from hashlib import sha256
from .protocol import CCProtocolPacker, CCProtocolUnpacker, CCProtoError, MAX_MSG_LEN, MAX_BLK_LEN
@ -27,6 +27,8 @@ class ColdcardDevice:
self.is_simulator = False
if not dev and sn and '/' in sn:
if platform.system() == 'Windows':
raise RuntimeError("Cannot connect to simulator. Is it running?")
dev = UnixSimulatorPipe(sn)
found = 'simulator'
self.is_simulator = True

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,10 +1,11 @@
# 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
import hid
import json
import struct
from .. import base58
@ -349,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,18 +1,19 @@
# 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
from .trezorlib.debuglink import TrezorClientDebugLink, DebugUI
from .trezorlib.exceptions import Cancelled
from .trezorlib.transport import enumerate_devices, get_transport
from .trezorlib.ui import PassphraseUI, mnemonic_words, PIN_MATRIX_DESCRIPTION
from .trezorlib.ui import echo, PassphraseUI, mnemonic_words, PIN_CURRENT, PIN_NEW, PIN_CONFIRM, PIN_MATRIX_DESCRIPTION, prompt
from .trezorlib import protobuf, tools, btc, device
from .trezorlib import messages as proto
from ..base58 import get_xpub_fingerprint, decode, to_address, xpub_main_2_test, get_xpub_fingerprint_hex
from ..serializations import ser_uint256, uint256_from_str
from .. import bech32
from usb1 import USBErrorNoDevice
from types import MethodType
import base64
import binascii
@ -69,15 +70,37 @@ def trezor_exception(f):
raise DeviceConnectionError('Device disconnected')
return func
def interactive_get_pin(self, code=None):
if code == PIN_CURRENT:
desc = "current PIN"
elif code == PIN_NEW:
desc = "new PIN"
elif code == PIN_CONFIRM:
desc = "new PIN again"
else:
desc = "PIN"
echo(PIN_MATRIX_DESCRIPTION)
while True:
pin = prompt("Please enter {}".format(desc), hide_input=True)
if not pin.isdigit():
echo("Non-numerical PIN provided, please try again")
else:
return pin
# This class extends the HardwareWalletClient for Trezor specific things
class TrezorClient(HardwareWalletClient):
def __init__(self, path, password=''):
super(TrezorClient, self).__init__(path, password)
self.simulator = False
if path.startswith('udp'):
logging.debug('Simulator found, using DebugLink')
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))
@ -314,6 +337,10 @@ class TrezorClient(HardwareWalletClient):
@trezor_exception
def setup_device(self, label='', passphrase=''):
self.client.init_device()
if not self.simulator:
# Use interactive_get_pin
self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui)
if self.client.features.initialized:
raise DeviceAlreadyInitError('Device is already initialized. Use wipe first and try again')
passphrase_enabled = False
@ -332,8 +359,13 @@ class TrezorClient(HardwareWalletClient):
# Restore device from mnemonic or xprv
@trezor_exception
def restore_device(self, label=''):
self.client.init_device()
if not self.simulator:
# Use interactive_get_pin
self.client.ui.get_pin = MethodType(interactive_get_pin, self.client.ui)
passphrase_enabled = False
device.recover(self.client, label=label, input_callback=mnemonic_words, passphrase_protection=bool(self.password))
device.recover(self.client, label=label, input_callback=mnemonic_words(), passphrase_protection=bool(self.password))
return {'success': True}
# Begin backup process
@ -390,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

@ -27,8 +27,9 @@ DEV_TREZOR1 = (0x534C, 0x0001)
DEV_TREZOR2 = (0x1209, 0x53C1)
DEV_TREZOR2_BL = (0x1209, 0x53C0)
DEV_KEEPKEY = (0x2B24, 0x0001)
DEV_KEEPKEY_WEBUSB = (0x2B24, 0x0002)
TREZORS = {DEV_TREZOR1, DEV_TREZOR2, DEV_TREZOR2_BL, DEV_KEEPKEY}
TREZORS = {DEV_TREZOR1, DEV_TREZOR2, DEV_TREZOR2_BL, DEV_KEEPKEY, DEV_KEEPKEY_WEBUSB}
UDEV_RULES_STR = """
Do you have udev rules installed?

View File

@ -126,7 +126,7 @@ class WebUsbTransport(ProtocolBasedTransport):
# non-functional.
dev.getProduct()
devices.append(WebUsbTransport(dev))
except usb1.USBErrorNotSupported:
except:
pass
return devices

View File

@ -50,8 +50,12 @@ PIN_CONFIRM = PinMatrixRequestType.NewSecond
def echo(msg):
print(msg, file=sys.stderr)
def prompt(msg):
return input(msg)
def prompt(msg, hide_input=False):
if hide_input:
import getpass
return getpass.getpass(msg + ' :\n')
else:
return input(msg + ':\n')
class PassphraseUI:
def __init__(self, passphrase):

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)

164
poetry.lock generated Normal file
View File

@ -0,0 +1,164 @@
[[package]]
category = "dev"
description = "Python graph (network) package"
name = "altgraph"
optional = false
python-versions = "*"
version = "0.16.1"
[[package]]
category = "main"
description = "ECDSA cryptographic signature library (pure python)"
name = "ecdsa"
optional = false
python-versions = "*"
version = "0.13"
[[package]]
category = "dev"
description = "Clean single-source support for Python 3 and 2"
name = "future"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.17.1"
[[package]]
category = "main"
description = "A Cython interface to the hidapi from https://github.com/signal11/hidapi"
name = "hidapi"
optional = false
python-versions = "*"
version = "0.7.99.post21"
[package.dependencies]
setuptools = ">=19.0"
[[package]]
category = "main"
description = "Pure-python wrapper for libusb-1.0"
name = "libusb1"
optional = false
python-versions = "*"
version = "1.7"
[[package]]
category = "dev"
description = "Mach-O header analysis and editing"
name = "macholib"
optional = false
python-versions = "*"
version = "1.11"
[package.dependencies]
altgraph = ">=0.15"
[[package]]
category = "main"
description = "Implementation of Bitcoin BIP-0039"
name = "mnemonic"
optional = false
python-versions = "*"
version = "0.18"
[package.dependencies]
pbkdf2 = "*"
[[package]]
category = "main"
description = "PKCS#5 v2.0 PBKDF2 Module"
name = "pbkdf2"
optional = false
python-versions = "*"
version = "1.3"
[[package]]
category = "dev"
description = "Python PE parsing module"
name = "pefile"
optional = false
python-versions = "*"
version = "2018.8.8"
[package.dependencies]
future = "*"
[[package]]
category = "main"
description = "Pure-Python Implementation of the AES block-cipher and common modes of operation"
name = "pyaes"
optional = false
python-versions = "*"
version = "1.6.1"
[[package]]
category = "dev"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
name = "pyinstaller"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.4"
[package.dependencies]
altgraph = "*"
macholib = ">=1.8"
pefile = ">=2017.8.1"
setuptools = "*"
[[package]]
category = "dev"
description = "Enhanced version of python-jsonrpc for use with Bitcoin"
name = "python-bitcoinrpc"
optional = false
python-versions = "*"
version = "1.0"
[[package]]
category = "main"
description = ""
name = "pywin32-ctypes"
optional = true
python-versions = "*"
version = "0.2.0"
[[package]]
category = "main"
description = "Type Hints for Python"
name = "typing"
optional = false
python-versions = "*"
version = "3.6.6"
[[package]]
category = "main"
description = "Backported and Experimental Type Hints for Python 3.5+"
name = "typing-extensions"
optional = false
python-versions = "*"
version = "3.7.2"
[package.dependencies]
typing = ">=3.6.2"
[extras]
windist = ["pywin32-ctypes"]
[metadata]
content-hash = "f668b6352b31d2aa7cf5b0cf19b77d04b92821df3383c9105f75699bbe42aa2e"
python-versions = ">=3.5.6"
[metadata.hashes]
altgraph = ["d6814989f242b2b43025cba7161fc1b8fb487a62cd49c49245d6fd01c18ac997", "ddf5320017147ba7b810198e0b6619bd7b5563aa034da388cea8546b877f9b0c"]
ecdsa = ["40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c", "64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa"]
future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"]
hidapi = ["1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24", "6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3", "8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946", "92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7", "b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87", "bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660", "c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7", "d4ad1e46aef98783a9e6274d523b8b1e766acfc3d72828cd44a337564d984cfa", "d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b", "e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97", "edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922"]
libusb1 = ["9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f"]
macholib = ["ac02d29898cf66f27510d8f39e9112ae00590adb4a48ec57b25028d6962b1ae1", "c4180ffc6f909bf8db6cd81cff4b6f601d575568f4d5dee148c830e9851eb9db"]
mnemonic = ["02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d"]
pbkdf2 = ["ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979"]
pefile = ["4c5b7e2de0c8cb6c504592167acf83115cbbde01fe4a507c16a1422850e86cd6"]
pyaes = ["02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"]
pyinstaller = ["a5a6e04a66abfcf8761e89a2ebad937919c6be33a7b8963e1a961b55cb35986b"]
python-bitcoinrpc = ["a6a6f35672635163bc491c25fe29520bdd063dedbeda3b37bf5be97aa038c6e7"]
pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"]
typing = ["4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", "57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", "a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a"]
typing-extensions = ["07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64", "f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c", "fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71"]

39
pyproject.toml Normal file
View File

@ -0,0 +1,39 @@
[tool.poetry]
name = "hwi"
version = "1.0.0"
description = "A library for working with Bitcoin hardware wallets"
authors = ["Andrew Chow <andrew@achow101.com>"]
license = "MIT"
readme = "README.md"
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"
hidapi = "^0.7.99"
ecdsa = "^0.13.0"
pyaes = "^1.6"
pywin32-ctypes = {version = "^0.2.0", optional = true}
mnemonic = "^0.18.0"
typing-extensions = "^3.7"
libusb1 = "^1.7"
[tool.poetry.dev-dependencies]
pyinstaller = "^3.4"
python-bitcoinrpc = "^1.0"
[tool.poetry.extras]
windist = ["pywin32-ctypes"]
[tool.poetry.scripts]
hwi = 'hwilib.cli:main'
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

View File

@ -1,38 +1,50 @@
import setuptools
# -*- coding: utf-8 -*-
from distutils.core import setup
with open("README.md", "r") as fh:
long_description = fh.read()
packages = \
['hwilib',
'hwilib.devices',
'hwilib.devices.btchip',
'hwilib.devices.ckcc',
'hwilib.devices.trezorlib',
'hwilib.devices.trezorlib.messages',
'hwilib.devices.trezorlib.transport']
setuptools.setup(
name="hwi",
version="0.0.5",
author="Andrew Chow",
author_email="andrew@achow101.com",
description="A library for working with Bitcoin hardware wallets",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/bitcoin-core/hwi",
packages=setuptools.find_packages(exclude=['docs', 'test']),
install_requires=[
'hidapi', # HID API needed in general
'pyaes',
'ecdsa', # Needed for Ledger but their library does not install it
'typing_extensions>=3.7',
'mnemonic>=0.18.0',
'libusb1'
],
python_requires='>=3',
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
extras_require={
'tests': ['python-bitcoinrpc']
},
entry_points={
'console_scripts': [
'hwi = hwilib.cli:main'
]
}
)
package_data = \
{'': ['*']}
modules = \
['hwi']
install_requires = \
['ecdsa>=0.13.0,<0.14.0',
'hidapi>=0.7.99,<0.8.0',
'libusb1>=1.7,<2.0',
'mnemonic>=0.18.0,<0.19.0',
'pyaes>=1.6,<2.0',
'typing-extensions>=3.7,<4.0']
extras_require = \
{'windist': ['pywin32-ctypes>=0.2.0,<0.3.0']}
entry_points = \
{'console_scripts': ['hwi = hwilib.cli:main']}
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 | 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,
'entry_points': entry_points,
'python_requires': '>=3.5.6',
}
setup(**setup_kwargs)

View File

@ -10,19 +10,22 @@ This is taken directly from the [python reference implementation](https://github
It implements all of the [BIP 174 serialization test vectors](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Test_Vectors).
- `test_trezor.py` tests the command line interface and the Trezor implementation.
It uses the [Trezor One firmware emulator](https://github.com/trezor/trezor-mcu/#building-for-development).
It also tests usage with `bitcoind`, so the [patched Bitcoin Core](../docs/bitcoin-core-usage.md#bitcoin-core) is required.
It also tests usage with `bitcoind`.
- `test_keepkey.py` tests the command line interface and the Keepkey implementation.
It uses the [Keepkey firmware emulator](https://github.com/keepkey/keepkey-firmware/blob/master/docs/Build.md).
It also tests usage with `bitcoind`.
- `test_coldcard.py` tests the command line interface and Coldcard implementation.
It uses the [Coldcard simulator](https://github.com/Coldcard/firmware/tree/master/unix#coldcard-desktop-simulator).
It also tests usage with `bitcoind`, so the [patched Bitcoin Core](../docs/bitcoin-core-usage.md#bitcoin-core) is required.
It also tests usage with `bitcoind`.
`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, and the patched `bitcoind`.
if run in the `test/` directory, these will be built in `work/test/trezor-mcu`, `work/test/firmware`, and `work/test/bitcoin` respectively.
`setup_environment.sh` will build the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, and `bitcoind`.
if run in the `test/` directory, these will be built in `work/test/trezor-mcu`, `work/test/firmware`, `work/test/keepkey-firmware`, `work/test/mcu`, and `work/test/bitcoin` respectively.
`run_tests.py` runs the tests. If run from the `test/` directory, it will be able to find the Trezor emulator, Coldcard simulator, 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, and bitcoind.
Otherwise the paths to those will need to be specified on the command line.
test_trezor.py` and `test_coldcard.py` can be disabled.
test_trezor.py`, `test_coldcard.py`, `test_keepkey.py`, and `test/test_digitalbitbox.py` can be disabled.
If you are building the Trezor emulator, the Coldcard simulator, and `bitcoind` without `setup_environment.sh`, then you will need to make `work/` inside of `test/`.
If you are building the Trezor emulator, the Coldcard simulator, the Keepkey emulator, the Digital Bitbox simulator, and `bitcoind` without `setup_environment.sh`, then you will need to make `work/` inside of `test/`.
```
$ cd test

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')
@ -32,7 +36,7 @@ dbb_group.add_argument('--no_bitbox', help='Do not run Digital Bitbox test with
dbb_group.add_argument('--bitbox', help='Path to Digital bitbox simulator.', default='work/mcu/build/bin/simulator')
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'], default='library')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist', 'stdin'], default='library')
args = parser.parse_args()
# Run tests
@ -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
@ -30,7 +35,7 @@ def coldcard_test_suite(simulator, rpc, userpass, interface):
# Coldcard specific management command tests
class TestColdcardManCommands(DeviceTestCase):
def test_setup(self):
result = self.do_command(self.dev_args + ['setup'])
result = self.do_command(self.dev_args + ['-i', 'setup'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['error'], 'The Coldcard does not support software setup')
@ -44,7 +49,7 @@ def coldcard_test_suite(simulator, rpc, userpass, interface):
self.assertEqual(result['code'], -9)
def test_restore(self):
result = self.do_command(self.dev_args + ['restore'])
result = self.do_command(self.dev_args + ['-i', 'restore'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['error'], 'The Coldcard does not support restoring via software')
@ -70,19 +75,19 @@ 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__':
parser = argparse.ArgumentParser(description='Test Coldcard implementation')
parser.add_argument('simulator', help='Path to the Coldcard simulator')
parser.add_argument('bitcoind', help='Path to bitcoind binary')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library')
args = parser.parse_args()
# Start bitcoind

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,19 +72,31 @@ 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(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'stdin':
input_str = '\n'.join(args) + '\n'
proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
result = proc.communicate(input_str.encode())
return json.loads(result[0].decode())
else:
return process_commands(args)
@ -92,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):
@ -145,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')
@ -252,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')
@ -396,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):
@ -447,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'
@ -44,7 +45,7 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
# DigitalBitbox specific management command tests
class TestDBBManCommands(DeviceTestCase):
def test_restore(self):
result = self.do_command(self.dev_args + ['restore'])
result = self.do_command(self.dev_args + ['-i', 'restore'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['error'], 'The Digital Bitbox does not support restoring via software')
@ -72,7 +73,7 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
def test_setup_wipe(self):
# Device is init, setup should fail
result = self.do_command(self.dev_args + ['setup', '--label', 'setup_test', '--backup_passphrase', 'testpass'])
result = self.do_command(self.dev_args + ['-i', 'setup', '--label', 'setup_test', '--backup_passphrase', 'testpass'])
self.assertEquals(result['code'], -10)
self.assertEquals(result['error'], 'Device is already initialized. Use wipe first and try again')
@ -81,25 +82,25 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
self.assertTrue(result['success'])
# Check arguments
result = self.do_command(self.dev_args + ['setup', '--label', 'setup_test'])
result = self.do_command(self.dev_args + ['-i', 'setup', '--label', 'setup_test'])
self.assertEquals(result['code'], -7)
self.assertEquals(result['error'], 'The label and backup passphrase for a new Digital Bitbox wallet must be specified and cannot be empty')
result = self.do_command(self.dev_args + ['setup', '--backup_passphrase', 'testpass'])
result = self.do_command(self.dev_args + ['-i', 'setup', '--backup_passphrase', 'testpass'])
self.assertEquals(result['code'], -7)
self.assertEquals(result['error'], 'The label and backup passphrase for a new Digital Bitbox wallet must be specified and cannot be empty')
# Setup
result = self.do_command(self.dev_args + ['setup', '--label', 'setup_test', '--backup_passphrase', 'testpass'])
result = self.do_command(self.dev_args + ['-i', 'setup', '--label', 'setup_test', '--backup_passphrase', 'testpass'])
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)
# Make sure device is init, setup should fail
result = self.do_command(self.dev_args + ['setup', '--label', 'setup_test', '--backup_passphrase', 'testpass'])
result = self.do_command(self.dev_args + ['-i', 'setup', '--label', 'setup_test', '--backup_passphrase', 'testpass'])
self.assertEquals(result['code'], -10)
self.assertEquals(result['error'], 'Device is already initialized. Use wipe first and try again')
@ -117,7 +118,7 @@ def digitalbitbox_test_suite(rpc, userpass, simulator, interface):
self.assertTrue(result['success'])
# Setup
result = self.do_command(self.dev_args + ['setup', '--label', 'backup_test', '--backup_passphrase', 'testpass'])
result = self.do_command(self.dev_args + ['-i', 'setup', '--label', 'backup_test', '--backup_passphrase', 'testpass'])
self.assertTrue(result['success'])
# make the backup
@ -126,18 +127,18 @@ 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__':
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('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library')
args = parser.parse_args()
# Start bitcoind

View File

@ -4,6 +4,7 @@ import argparse
import atexit
import json
import os
import shlex
import socket
import subprocess
import sys
@ -82,10 +83,22 @@ 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(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'stdin':
input_str = '\n'.join(args) + '\n'
proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
result = proc.communicate(input_str.encode())
return json.loads(result[0].decode())
else:
return process_commands(args)
@ -132,7 +145,7 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
def test_setup_wipe(self):
# Device is init, setup should fail
result = self.do_command(self.dev_args + ['setup'])
result = self.do_command(self.dev_args + ['-i', 'setup'])
self.assertEquals(result['code'], -10)
self.assertEquals(result['error'], 'Device is already initialized. Use wipe first and try again')
@ -148,7 +161,7 @@ class TestKeepkeyManCommands(KeepkeyTestCase):
self.assertTrue(result['success'])
# Make sure device is init, setup should fail
result = self.do_command(self.dev_args + ['setup'])
result = self.do_command(self.dev_args + ['-i', 'setup'])
self.assertEquals(result['code'], -10)
self.assertEquals(result['error'], 'Device is already initialized. Use wipe first and try again')
@ -167,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'])
@ -199,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')
@ -207,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'
@ -220,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
@ -233,7 +305,7 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Test Keepkey implementation')
parser.add_argument('emulator', help='Path to the Keepkey emulator')
parser.add_argument('bitcoind', help='Path to bitcoind binary')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library')
args = parser.parse_args()
# Start bitcoind

View File

@ -44,7 +44,7 @@ def ledger_test_suite(rpc, userpass, interface):
self.assertEqual(result['code'], -9)
def test_setup(self):
result = self.do_command(self.dev_args + ['setup'])
result = self.do_command(self.dev_args + ['-i', 'setup'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['error'], 'The Ledger Nano S does not support software setup')
@ -58,7 +58,7 @@ def ledger_test_suite(rpc, userpass, interface):
self.assertEqual(result['code'], -9)
def test_restore(self):
result = self.do_command(self.dev_args + ['restore'])
result = self.do_command(self.dev_args + ['-i', 'restore'])
self.assertIn('error', result)
self.assertIn('code', result)
self.assertEqual(result['error'], 'The Ledger Nano S does not support restoring via software')
@ -73,18 +73,18 @@ 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__':
parser = argparse.ArgumentParser(description='Test Ledger implementation')
parser.add_argument('bitcoind', help='Path to bitcoind binary')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli'], default='library')
parser.add_argument('--interface', help='Which interface to send commands over', choices=['library', 'cli', 'bindist'], default='library')
args = parser.parse_args()
# Start bitcoind

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,18 +84,30 @@ 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(cli_args)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)
result = proc.communicate()
return json.loads(result[0].decode())
elif self.interface == 'stdin':
input_str = '\n'.join(args) + '\n'
proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
result = proc.communicate(input_str.encode())
return json.loads(result[0].decode())
else:
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):
@ -132,7 +146,7 @@ class TestTrezorManCommands(TrezorTestCase):
def test_setup_wipe(self):
# Device is init, setup should fail
result = self.do_command(self.dev_args + ['setup'])
result = self.do_command(self.dev_args + ['-i', 'setup'])
self.assertEquals(result['code'], -10)
self.assertEquals(result['error'], 'Device is already initialized. Use wipe first and try again')
@ -148,7 +162,7 @@ class TestTrezorManCommands(TrezorTestCase):
self.assertTrue(result['success'])
# Make sure device is init, setup should fail
result = self.do_command(self.dev_args + ['setup'])
result = self.do_command(self.dev_args + ['-i', 'setup'])
self.assertEquals(result['code'], -10)
self.assertEquals(result['error'], 'Device is already initialized. Use wipe first and try again')
@ -167,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'])
@ -199,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')
@ -207,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')
@ -218,26 +289,33 @@ 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__':
parser = argparse.ArgumentParser(description='Test Trezor implementation')
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'], default='library')
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()

9
udev/20-hw1.rules Normal file
View File

@ -0,0 +1,9 @@
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="2b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="3b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="4b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1807", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1808", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004", MODE="0660", GROUP="plugdev"

8
udev/51-coinkite.rules Normal file
View File

@ -0,0 +1,8 @@
# probably not needed:
SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666"
# required:
# from <https://github.com/signal11/hidapi/blob/master/udev/99-hid.rules>
KERNEL=="hidraw*", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666"

View File

@ -0,0 +1 @@
SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbb%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402"

17
udev/51-trezor.rules Normal file
View File

@ -0,0 +1,17 @@
# TREZOR: The Original Hardware Wallet
# https://trezor.io/
#
# Put this file into /etc/udev/rules.d
#
# If you are creating a distribution package,
# put this into /usr/lib/udev/rules.d or /lib/udev/rules.d
# depending on your distribution
# TREZOR
SUBSYSTEM=="usb", ATTR{idVendor}=="534c", ATTR{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n"
KERNEL=="hidraw*", ATTRS{idVendor}=="534c", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
# TREZOR v2
SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c0", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n"
SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n"
KERNEL=="hidraw*", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"

11
udev/51-usb-keepkey.rules Normal file
View File

@ -0,0 +1,11 @@
# KeepKey: Your Private Bitcoin Vault
# http://www.keepkey.com/
# Put this file into /usr/lib/udev/rules.d or /etc/udev/rules.d
# KeepKey HID Firmware/Bootloader
SUBSYSTEM=="usb", ATTR{idVendor}=="2b24", ATTR{idProduct}=="0001", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="keepkey%n"
KERNEL=="hidraw*", ATTRS{idVendor}=="2b24", ATTRS{idProduct}=="0001", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
# KeepKey WebUSB Firmware/Bootloader
SUBSYSTEM=="usb", ATTR{idVendor}=="2b24", ATTR{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="keepkey%n"
KERNEL=="hidraw*", ATTRS{idVendor}=="2b24", ATTRS{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"

View File

@ -0,0 +1 @@
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbbf%n"

23
udev/README.md Normal file
View File

@ -0,0 +1,23 @@
# udev rules
This directory contains all of the udev rules for the supported devices as retrieved from vendor websites and repositories.
These are necessary for the devices to be reachable on linux environments.
`20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules
`51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules
`51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://shiftcrypto.ch/start_linux
`51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules
`51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules
# Usage
Apply these rules by copying them to `/etc/udev/rules.d/` and notifying `udevadm`.
Your user will need to be added to the `plugdev` group, which needs to be created if it does not already exist.
```
$ sudo cp udev/*.rules /etc/udev/rules.d/
$ sudo udevadm trigger
$ sudo udevadm control --reload-rules
$ sudo groupadd plugdev
$ sudo usermod -aG plugdev `whoami`
```