Compare commits
79 Commits
fix-ranges
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b722055c4 | ||
|
|
84315dd5de | ||
|
|
ef816628e1 | ||
|
|
6952edd879 | ||
|
|
9484803b71 | ||
|
|
d8cf9f120b | ||
|
|
4d21f29127 | ||
|
|
54e14ba301 | ||
|
|
1c33312bb1 | ||
|
|
bca85354f9 | ||
|
|
d2696cfadf | ||
|
|
a4a3aff89b | ||
|
|
cada7f5ce0 | ||
|
|
64153a8a65 | ||
|
|
1fb5eb42c7 | ||
|
|
54bd6687cc | ||
|
|
0974f6944c | ||
|
|
cea784ef60 | ||
|
|
70e31cad0c | ||
|
|
129e91d9f0 | ||
|
|
c14896c6eb | ||
|
|
6275418c90 | ||
|
|
67359835eb | ||
|
|
57a06836b9 | ||
|
|
c3f2521855 | ||
|
|
d0c561e516 | ||
|
|
85eaf70f36 | ||
|
|
4dd56fda3c | ||
|
|
ae9bca3ae3 | ||
|
|
7201300e92 | ||
|
|
892b552bb3 | ||
|
|
0cf6bb9998 | ||
|
|
0c831e4f75 | ||
|
|
90f5e32cc3 | ||
|
|
483b589eeb | ||
|
|
c628206d31 | ||
|
|
cb2ed1654c | ||
|
|
a86323d419 | ||
|
|
b5d74ec24c | ||
|
|
30146a26b8 | ||
|
|
bfc29c26a5 | ||
|
|
95bd88e41d | ||
|
|
9e826909cf | ||
|
|
f109365047 | ||
|
|
d00ae56eef | ||
|
|
6e92b7d40f | ||
|
|
397993bfdc | ||
|
|
f94913739e | ||
|
|
4814a293e8 | ||
|
|
1222d9a492 | ||
|
|
d065307317 | ||
|
|
685faa2e5c | ||
|
|
f096c18478 | ||
|
|
7b0e84e26c | ||
|
|
6f67659e07 | ||
|
|
a83fdc6be3 | ||
|
|
4453555c21 | ||
|
|
18857d9cd0 | ||
|
|
0dd2e86394 | ||
|
|
d0ac6b9398 | ||
|
|
49bc7fa5da | ||
|
|
0185391c72 | ||
|
|
2bf3d418e1 | ||
|
|
d924f39b6b | ||
|
|
376245ae0f | ||
|
|
0235baf377 | ||
|
|
7929692c9d | ||
|
|
b99385c8e2 | ||
|
|
b7873acddc | ||
|
|
fc33fc5bbe | ||
|
|
5e42d3d696 | ||
|
|
07ece90769 | ||
|
|
8931ae0e90 | ||
|
|
39a6fc9654 | ||
|
|
a229de0780 | ||
|
|
77257a18da | ||
|
|
9e04d1a16a | ||
|
|
d6b24b853d | ||
|
|
6e4ffcdd1b |
@ -1 +1 @@
|
||||
3.5.6
|
||||
3.6.8
|
||||
|
||||
35
.travis.yml
35
.travis.yml
@ -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/*
|
||||
|
||||
73
README.md
73
README.md
@ -1,14 +1,33 @@
|
||||
# Bitcoin Hardware Wallet Interaction scripts
|
||||
# Bitcoin Hardware Wallet Interface
|
||||
|
||||
[](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
35
contrib/README.md
Normal 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
50
contrib/build.Dockerfile
Normal 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
36
contrib/build_bin.sh
Executable 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
15
contrib/build_dist.sh
Executable 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
76
contrib/build_wine.sh
Executable 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
32
contrib/generate_setup.sh
Executable 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
|
||||
4
contrib/pyinstaller-hooks/hook-hwilib.devices.py
Normal file
4
contrib/pyinstaller-hooks/hook-hwilib.devices.py
Normal file
@ -0,0 +1,4 @@
|
||||
from hwilib.devices import __all__
|
||||
hiddenimports = []
|
||||
for d in __all__:
|
||||
hiddenimports.append('hwilib.devices.' + d)
|
||||
13
contrib/reproducible-python.diff
Normal file
13
contrib/reproducible-python.diff
Normal 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
|
||||
@ -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.
|
||||
|
||||
@ -12,6 +12,7 @@ Current implemented commands are:
|
||||
- `restore`
|
||||
- `backup`
|
||||
- `displayaddress`
|
||||
- `signmessage`
|
||||
|
||||
## Usage Notes
|
||||
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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
51
docs/release-process.md
Normal 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
46
hwi.spec
Normal 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 )
|
||||
@ -0,0 +1 @@
|
||||
__version__ = '1.0.0'
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
@ -0,0 +1,7 @@
|
||||
__all__ = [
|
||||
'trezor',
|
||||
'ledger',
|
||||
'keepkey',
|
||||
'digitalbitbox',
|
||||
'coldcard'
|
||||
]
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -126,7 +126,7 @@ class WebUsbTransport(ProtocolBasedTransport):
|
||||
# non-functional.
|
||||
dev.getProduct()
|
||||
devices.append(WebUsbTransport(dev))
|
||||
except usb1.USBErrorNotSupported:
|
||||
except:
|
||||
pass
|
||||
return devices
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
60
hwilib/udevinstaller.py
Normal 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
164
poetry.lock
generated
Normal 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
39
pyproject.toml
Normal 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"
|
||||
84
setup.py
84
setup.py
@ -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[](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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
39
test/test_udevrules.py
Executable 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
9
udev/20-hw1.rules
Normal 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
8
udev/51-coinkite.rules
Normal 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"
|
||||
|
||||
1
udev/51-hid-digitalbitbox.rules
Normal file
1
udev/51-hid-digitalbitbox.rules
Normal 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
17
udev/51-trezor.rules
Normal 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
11
udev/51-usb-keepkey.rules
Normal 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"
|
||||
1
udev/52-hid-digitalbitbox.rules
Normal file
1
udev/52-hid-digitalbitbox.rules
Normal 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
23
udev/README.md
Normal 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`
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user