mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a9fdf75e2 | ||
|
|
6bc5b6af5e | ||
|
|
8672a6901a | ||
|
|
672af98ad7 | ||
|
|
ed531cfff8 | ||
|
|
bd1ae0f091 | ||
|
|
0c762e8998 | ||
|
|
bd0df4f801 | ||
|
|
3d1639d271 | ||
|
|
bea899d1ef | ||
|
|
ccc2174775 | ||
|
|
afa3fdb89c | ||
|
|
2ca3941cfa | ||
|
|
b1bd6cb690 | ||
|
|
766536d2c4 | ||
|
|
91f70e7a96 | ||
|
|
cf5bfd960a | ||
|
|
4bd769f138 | ||
|
|
91b850f184 | ||
|
|
c6bb090dfc | ||
|
|
fef4fd06c9 | ||
|
|
bc691ae795 | ||
|
|
61e516e200 | ||
|
|
543ff7021d | ||
|
|
2e0cfc8088 | ||
|
|
18f33f8a08 | ||
|
|
2973413995 | ||
|
|
2360693dc5 | ||
|
|
7443fc6512 | ||
|
|
5efb752979 | ||
|
|
4546cd674b | ||
|
|
5dba12f144 | ||
|
|
887561de9f | ||
|
|
6d730e0a5b | ||
|
|
d0732d16e8 | ||
|
|
dafb80ad7a | ||
|
|
df6249b071 | ||
|
|
942f01418b | ||
|
|
93b548b737 | ||
|
|
329f07249a | ||
|
|
a1f7088d33 | ||
|
|
25f066e113 | ||
|
|
0699273d49 | ||
|
|
92c352e860 | ||
|
|
34c03a462c | ||
|
|
51dbecd4c2 | ||
|
|
ceae65aa5a | ||
|
|
d0497b0137 | ||
|
|
870152a7af | ||
|
|
cbdc52c0a4 | ||
|
|
0c9fc33757 | ||
|
|
17ea941add | ||
|
|
64064b5ecc | ||
|
|
601a2b1336 | ||
|
|
b6181bb5b5 | ||
|
|
b6da299cb0 | ||
|
|
04627f0899 | ||
|
|
54ce6f2cec | ||
|
|
a1047ba7b1 | ||
|
|
e90bd0cd81 | ||
|
|
66e3e60370 | ||
|
|
3f1604d609 | ||
|
|
d0f4cccfd2 | ||
|
|
08d81c992c | ||
|
|
55a899f929 | ||
|
|
e7604dff68 | ||
|
|
8849545700 | ||
|
|
d109cd73b5 | ||
|
|
95e98d6eda | ||
|
|
9e78d52721 | ||
|
|
2a76ef6819 | ||
|
|
654a3c465a | ||
|
|
2168115b06 | ||
|
|
4a9140c42d | ||
|
|
b20d98bf57 | ||
|
|
199fb299c3 | ||
|
|
06e169f141 | ||
|
|
131111bc0e | ||
|
|
f4208009e0 | ||
|
|
73d60dbec0 | ||
|
|
34ea224290 | ||
|
|
7803026f61 | ||
|
|
34ce1005fd | ||
|
|
8677c8ebaa | ||
|
|
6363eb0d4a | ||
|
|
a32bfc749b | ||
|
|
75d117ad0d | ||
|
|
cefc5f180a | ||
|
|
0f5c71b748 | ||
|
|
d5f97b7efa | ||
|
|
4a12bfa0b7 | ||
|
|
cac889ff7d | ||
|
|
92c6e680ed | ||
|
|
bf294beb56 | ||
|
|
713345918e | ||
|
|
eb60c2f475 | ||
|
|
6d8d43db9b | ||
|
|
3e67bc9f0e | ||
|
|
38b50485de | ||
|
|
9cba27b31a | ||
|
|
00a65a9820 | ||
|
|
52ad601e66 | ||
|
|
d96a2820ff |
@@ -1,7 +1,7 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 0.9.7
|
||||
current_version = 0.11.3
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return
|
||||
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme,duplicate-code
|
||||
|
||||
[SIMILARITIES]
|
||||
min-similarity-lines=5
|
||||
|
||||
26
README.md
26
README.md
@@ -1,25 +1,27 @@
|
||||
# Using TREZOR as a hardware SSH/GPG agent
|
||||
# Hardware-based SSH/GPG agent
|
||||
|
||||
[](https://travis-ci.org/romanz/trezor-agent)
|
||||
[](https://gitter.im/romanz/trezor-agent)
|
||||
|
||||
See SatoshiLabs' blog posts about this feature:
|
||||
This project allows you to use various hardware security devices to operate GPG and SSH. Instead of keeping your key on your computer and decrypting it with a passphrase when you want to use it, the key is generated and stored on the device and never reaches your computer. Read more about the design [here](doc/DESIGN.md).
|
||||
|
||||
You can do things like sign your emails, git commits, and software packages, manage your passwords (with [pass](https://www.passwordstore.org/) and [gopass](https://www.justwatch.com/gopass/), among others), authenticate web tunnels and file transfers, and more.
|
||||
|
||||
See the following blog posts about this tool:
|
||||
|
||||
- [TREZOR Firmware 1.3.4 enables SSH login](https://medium.com/@satoshilabs/trezor-firmware-1-3-4-enables-ssh-login-86a622d7e609)
|
||||
- [TREZOR Firmware 1.3.6 — GPG Signing, SSH Login Updates and Advanced Transaction Features for Segwit](https://medium.com/@satoshilabs/trezor-firmware-1-3-6-20a7df6e692)
|
||||
- [TREZOR Firmware 1.4.0 — GPG decryption support](https://www.reddit.com/r/TREZOR/comments/50h8r9/new_trezor_firmware_fidou2f_and_initial_ethereum/d7420q7/)
|
||||
- [A Step by Step Guide to Securing your SSH Keys with the Ledger Nano S](https://thoughts.t37.net/a-step-by-step-guide-to-securing-your-ssh-keys-with-the-ledger-nano-s-92e58c64a005)
|
||||
|
||||
## Installation
|
||||
Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Keepkey](https://www.keepkey.com/), and [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s) are supported.
|
||||
|
||||
See the [following instructions](doc/INSTALL.md) for the
|
||||
[TREZOR](https://trezor.io/), [Keepkey](https://www.keepkey.com/) and
|
||||
[Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s) devices.
|
||||
## Documentation
|
||||
|
||||
## Usage
|
||||
* **Installation** instructions are [here](doc/INSTALL.md)
|
||||
* **SSH** instructions and common use cases are [here](doc/README-SSH.md)
|
||||
|
||||
For SSH, see the [following instructions](doc/README-SSH.md) (for Windows support,
|
||||
see [trezor-ssh-agent](https://github.com/martin-lizner/trezor-ssh-agent) project by Martin Lízner).
|
||||
Note: If you're using Windows, see [trezor-ssh-agent](https://github.com/martin-lizner/trezor-ssh-agent) by Martin Lízner.
|
||||
|
||||
For GPG, see the [following instructions](doc/README-GPG.md).
|
||||
|
||||
See [here](https://github.com/romanz/python-trezor#pin-entering) for PIN entering instructions.
|
||||
* **GPG** instructions and common use cases are [here](doc/README-GPG.md)
|
||||
* Instructions to configure a Trezor-style **PIN entry** program are [here](doc/README-PINENTRY.md)
|
||||
@@ -3,15 +3,15 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='trezor_agent',
|
||||
version='0.9.0',
|
||||
version='0.9.3',
|
||||
description='Using Trezor as hardware SSH/GPG agent',
|
||||
author='Roman Zeyde',
|
||||
author_email='roman.zeyde@gmail.com',
|
||||
url='http://github.com/romanz/trezor-agent',
|
||||
scripts=['trezor_agent.py'],
|
||||
install_requires=[
|
||||
'libagent>=0.9.0',
|
||||
'trezor>=0.7.6'
|
||||
'libagent>=0.11.2',
|
||||
'trezor[hidapi]>=0.9.0'
|
||||
],
|
||||
platforms=['POSIX'],
|
||||
classifiers=[
|
||||
|
||||
@@ -12,11 +12,11 @@ So when you `ssh` to a machine - rather than consult the normal ssh-agent (which
|
||||
|
||||
## Key Naming
|
||||
|
||||
`trezor-agent` goes to great length to avoid using the valuable parent key.
|
||||
`trezor-agent` goes to great length to avoid using the valuable parent key.
|
||||
|
||||
The rationale behind this is that `trezor-agent` is to some extent condemned to *blindly* signing any NONCE given to it (e.g. as part of a challenge respone, or as the hash/hmac of someting to sign).
|
||||
The rationale behind this is that `trezor-agent` is to some extent condemned to *blindly* signing any NONCE given to it (e.g. as part of a challenge respone, or as the hash/hmac of someting to sign).
|
||||
|
||||
And doing so with the master private key is risky - as rogue (ssh) server could possibly provide a doctored NONCE that happens to be tied to a transaction or something else.
|
||||
And doing so with the master private key is risky - as rogue (ssh) server could possibly provide a doctored NONCE that happens to be tied to a transaction or something else.
|
||||
|
||||
It therefore uses only derived child keys pairs instead (according to the [BIP-0032: Hierarchical Deterministic Wallets][1] system) - and ones on different leafs. So the parent key is only used within the device for creating the child keys - and not exposed in any way to `trezor-agent`.
|
||||
|
||||
@@ -26,7 +26,7 @@ It is common for SSH users to use one (or a few) private keys with SSH on all se
|
||||
|
||||
So taking a commmand such as:
|
||||
|
||||
$ trezor-agent -c user@fqdn.com
|
||||
$ trezor-agent -c user@fqdn.com
|
||||
|
||||
The `trezor-agent` will take the `user`@`fqdn.com`; canonicalise it (e.g. to add the ssh default port number if none was specified) and then apply some simple hashing (See [SLIP-0013 : Authentication using deterministic hierarchy][2]). The resulting 128bit hash is then used to construct a lead 'HD node' that contains an extened public private *child* key.
|
||||
|
||||
@@ -42,10 +42,10 @@ Note: Keepkey does not support en-/de-cryption at this time.
|
||||
|
||||
### Index
|
||||
|
||||
The canonicalisation process ([SLIP-0013][2] and [SLIP-0017][3]) of an email address or ssh address allows for the mixing in of an extra 'index' - a unsigned 32 bit number. This allows one to have multiple, different keys, for the same address.
|
||||
The canonicalisation process ([SLIP-0013][2] and [SLIP-0017][3]) of an email address or ssh address allows for the mixing in of an extra 'index' - a unsigned 32 bit number. This allows one to have multiple, different keys, for the same address.
|
||||
|
||||
This feature is currently not used -- it is set to '0'. This may change in the future.
|
||||
|
||||
[1]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||
[1]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||
[2]: https://github.com/satoshilabs/slips/blob/master/slip-0013.md
|
||||
[3]: https://github.com/satoshilabs/slips/blob/master/slip-0017.md
|
||||
|
||||
127
doc/INSTALL.md
127
doc/INSTALL.md
@@ -1,82 +1,135 @@
|
||||
# Installation
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Install the following packages (depending on your distribution):
|
||||
|
||||
## Install dependencies
|
||||
### OS dependencies
|
||||
|
||||
### Debian
|
||||
This software needs Python, libusb, and libudev along with development files.
|
||||
|
||||
$ apt update && apt upgrade
|
||||
$ apt install python-pip python-dev libusb-1.0-0-dev libudev-dev
|
||||
You can install them on these distributions as follows:
|
||||
|
||||
### Fedora/RedHat
|
||||
##### Debian
|
||||
|
||||
$ yum update
|
||||
$ yum install python-pip python-devel libusb-devel libudev-devel \
|
||||
$ apt-get install python3-pip python3-dev python3-tk libusb-1.0-0-dev libudev-dev
|
||||
|
||||
##### RedHat
|
||||
|
||||
$ yum install python3-pip python3-devel python3-tk libusb-devel libudev-devel \
|
||||
gcc redhat-rpm-config
|
||||
|
||||
### OpenSUSE
|
||||
##### Fedora
|
||||
|
||||
$ zypper install python-pip python-devel libusb-1_0-devel libudev-devel
|
||||
$ dnf install python3-pip python3-devel python3-tkinter libusb-devel libudev-devel \
|
||||
gcc redhat-rpm-config
|
||||
|
||||
##### OpenSUSE
|
||||
|
||||
$ zypper install python-pip python-devel python-tk libusb-1_0-devel libudev-devel
|
||||
|
||||
If you are using python3 or your system `pip` command points to `pip3.x`
|
||||
(`/etc/alternatives/pip -> /usr/bin/pip3.6`) you will need to install these
|
||||
dependencies instead:
|
||||
|
||||
$ zypper install python3-pip python3-devel libusb-1_0-devel libudev-devel
|
||||
$ zypper install python3-pip python3-devel python3-tk libusb-1_0-devel libudev-devel
|
||||
|
||||
## Update setuptools and pip
|
||||
##### macOS
|
||||
|
||||
Also, update Python packages before starting the installation:
|
||||
There are many different options to install python environment on macOS ([official](https://www.python.org/downloads/mac-osx/), [anaconda](https://conda.io/docs/user-guide/install/macos.html), ..). Most importantly you need `libusb`. Probably the easiest way is via [homebrew](https://brew.sh/)
|
||||
|
||||
$ pip install -U setuptools pip
|
||||
$ brew install libusb
|
||||
|
||||
## Check device's firmware version
|
||||
### GPG
|
||||
|
||||
Make sure you are running the latest firmware version on your hardware device.
|
||||
Currently the following firmware versions are supported:
|
||||
If you intend to use GPG make sure you have GPG installed and up to date. This software requires a GPG version >= 2.1.11.
|
||||
|
||||
* [TREZOR](https://wallet.trezor.io/data/firmware/releases.json): `1.4.2+`
|
||||
* [KeepKey](https://github.com/keepkey/keepkey-firmware/releases): `3.0.17+`
|
||||
* [Ledger Nano S](https://github.com/LedgerHQ/blue-app-ssh-agent): `0.0.3+` (install [SSH/PGP Agent](https://www.ledgerwallet.com/images/apps/chrome-mngr-apps.png) app)
|
||||
You can verify your installed version by running:
|
||||
```
|
||||
$ gpg2 --version | head -n1
|
||||
gpg (GnuPG) 2.1.15
|
||||
```
|
||||
|
||||
## TREZOR
|
||||
* Follow this installation guide for [Debian](https://gist.github.com/vt0r/a2f8c0bcb1400131ff51)
|
||||
* Install GPG for [macOS](https://sourceforge.net/p/gpgosx/docu/Download/)
|
||||
* Install packages for Ubuntu 16.04 [here](https://launchpad.net/ubuntu/+source/gnupg2)
|
||||
* Install packages for Linux Mint 18 [here](https://community.linuxmint.com/software/view/gnupg2)
|
||||
|
||||
Make sure that your `udev` rules are configured [correctly](https://doc.satoshilabs.com/trezor-user/settingupchromeonlinux.html#manual-configuration-of-udev-rules).
|
||||
Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agent) package:
|
||||
# 2. Install the TREZOR agent
|
||||
|
||||
$ pip install trezor_agent
|
||||
1. Make sure you are running the latest firmware version on your Trezor:
|
||||
|
||||
Or, directly from the latest source code:
|
||||
* [TREZOR firmware releases](https://wallet.trezor.io/data/firmware/releases.json): `1.4.2+`
|
||||
|
||||
2. Make sure that your `udev` rules are configured [correctly](https://doc.satoshilabs.com/trezor-user/settingupchromeonlinux.html#manual-configuration-of-udev-rules).
|
||||
|
||||
3. Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agent) package:
|
||||
|
||||
```
|
||||
$ pip3 install Cython
|
||||
$ pip3 install trezor_agent
|
||||
```
|
||||
|
||||
Or, directly from the latest source code:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/romanz/trezor-agent
|
||||
$ pip install --user -e trezor-agent/agents/trezor
|
||||
$ pip3 install --user -e trezor-agent/agents/trezor
|
||||
```
|
||||
|
||||
## KeepKey
|
||||
Or, through Homebrew on macOS:
|
||||
|
||||
Make sure that your `udev` rules are configured [correctly](https://support.keepkey.com/support/solutions/articles/6000037796-keepkey-wallet-is-not-being-recognized-by-linux).
|
||||
```
|
||||
$ brew install trezor-agent
|
||||
```
|
||||
|
||||
# 3. Install the KeepKey agent
|
||||
|
||||
1. Make sure you are running the latest firmware version on your KeepKey:
|
||||
|
||||
* [KeepKey firmware releases](https://github.com/keepkey/keepkey-firmware/releases): `3.0.17+`
|
||||
|
||||
2. Make sure that your `udev` rules are configured [correctly](https://support.keepkey.com/support/solutions/articles/6000037796-keepkey-wallet-is-not-being-recognized-by-linux).
|
||||
Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_agent) package:
|
||||
|
||||
$ pip install keepkey_agent
|
||||
```
|
||||
$ pip3 install keepkey_agent
|
||||
```
|
||||
|
||||
Or, directly from the latest source code:
|
||||
Or, on Mac using Homebrew:
|
||||
|
||||
```
|
||||
$ homebrew install keepkey-agent
|
||||
```
|
||||
|
||||
Or, directly from the latest source code:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/romanz/trezor-agent
|
||||
$ pip install --user -e trezor-agent/agents/keepkey
|
||||
$ pip3 install --user -e trezor-agent/agents/keepkey
|
||||
```
|
||||
|
||||
## Ledger Nano S
|
||||
# 4. Install the Ledger Nano S agent
|
||||
|
||||
Make sure that your `udev` rules are configured [correctly](http://support.ledgerwallet.com/knowledge_base/topics/ledger-wallet-is-not-recognized-on-linux).
|
||||
Then, install the latest [ledger_agent](https://pypi.python.org/pypi/ledger_agent) package:
|
||||
1. Make sure you are running the latest firmware version on your Ledger Nano S:
|
||||
|
||||
$ pip install ledger_agent
|
||||
* [Ledger Nano S firmware releases](https://github.com/LedgerHQ/blue-app-ssh-agent): `0.0.3+` (install [SSH/PGP Agent](https://www.ledgerwallet.com/images/apps/chrome-mngr-apps.png) app)
|
||||
|
||||
Or, directly from the latest source code:
|
||||
2. Make sure that your `udev` rules are configured [correctly](https://ledger.zendesk.com/hc/en-us/articles/115005165269-What-if-Ledger-Wallet-is-not-recognized-on-Linux-).
|
||||
3. Then, install the latest [ledger_agent](https://pypi.python.org/pypi/ledger_agent) package:
|
||||
|
||||
```
|
||||
$ pip3 install ledger_agent
|
||||
```
|
||||
|
||||
Or, directly from the latest source code:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/romanz/trezor-agent
|
||||
$ pip install --user -e trezor-agent/agents/ledger
|
||||
$ pip3 install --user -e trezor-agent/agents/ledger
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
# 5. Installation Troubleshooting
|
||||
|
||||
If there is an import problem with the installed `protobuf` package,
|
||||
see [this issue](https://github.com/romanz/trezor-agent/issues/28) for fixing it.
|
||||
|
||||
@@ -1,62 +1,66 @@
|
||||
# GPG Agent
|
||||
|
||||
Note: the GPG-related code is still under development, so please try the current implementation
|
||||
and please let me [know](https://github.com/romanz/trezor-agent/issues/new) if something doesn't
|
||||
work well for you. If possible:
|
||||
|
||||
* record the session (e.g. using [asciinema](https://asciinema.org))
|
||||
* attach the GPG agent log from `~/.gnupg/{trezor,ledger}/gpg-agent.log`
|
||||
* attach the GPG agent log from `~/.gnupg/{trezor,ledger}/gpg-agent.log` (can be [encrypted](https://keybase.io/romanz))
|
||||
|
||||
Thanks!
|
||||
|
||||
# Installation
|
||||
## 1. Configuration
|
||||
|
||||
First, verify that you have GPG 2.1.11+ installed
|
||||
([Debian](https://gist.github.com/vt0r/a2f8c0bcb1400131ff51),
|
||||
[macOS](https://sourceforge.net/p/gpgosx/docu/Download/)):
|
||||
1. Initialize the agent GPG directory.
|
||||
|
||||
```
|
||||
$ gpg2 --version | head -n1
|
||||
gpg (GnuPG) 2.1.15
|
||||
```
|
||||
[](https://asciinema.org/a/3iNw2L9QWB8R3EVdYdAxMOLK8)
|
||||
|
||||
This GPG version is included in [Ubuntu 16.04](https://launchpad.net/ubuntu/+source/gnupg2)
|
||||
and [Linux Mint 18](https://community.linuxmint.com/software/view/gnupg2).
|
||||
Run
|
||||
|
||||
Update you device firmware to the latest version and install your specific `agent` package:
|
||||
```
|
||||
$ (trezor|keepkey|ledger)-gpg init "Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
```
|
||||
|
||||
```
|
||||
$ pip install --user (trezor|keepkey|ledger)_agent
|
||||
```
|
||||
Follow the instructions provided to complete the setup. Keep note of the timestamp value which you'll need if you want to regenerate the key later.
|
||||
|
||||
# Quickstart
|
||||
If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md).
|
||||
|
||||
## Identity creation
|
||||
[](https://asciinema.org/a/3iNw2L9QWB8R3EVdYdAxMOLK8)
|
||||
2. Add `export GNUPGHOME=~/.gnupg/(trezor|keepkey|ledger)` to your `.bashrc` or other environment file.
|
||||
|
||||
In order to use specific device type for GPG indentity creation, use either command:
|
||||
```
|
||||
$ trezor-gpg init "Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
$ ledger-gpg init "Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
```
|
||||
This `GNUPGHOME` contains your hardware keyring and agent settings. This agent software assumes all keys are backed by hardware devices so you can't use standard GPG keys in `GNUPGHOME` (if you do mix keys you'll receive an error when you attempt to use them).
|
||||
|
||||
If you wish to switch back to your software keys unset `GNUPGHOME`.
|
||||
|
||||
3. Log out and back into your session to ensure your environment is updated everywhere.
|
||||
|
||||
## 2. Usage
|
||||
|
||||
You can use any GPG commands or software that uses GPG as usual and will be prompted to interact with your hardware device as necessary. The agent is automatically started if it isn't running when you run any `gpg` command.
|
||||
|
||||
##### Restarting the agent
|
||||
|
||||
If you change settings or need to restart the agent for some other reason, simply kill it. It will restart the next time GPG is invoked.
|
||||
|
||||
## 3. Common Use Cases
|
||||
|
||||
### Sign and decrypt files
|
||||
|
||||
## Sample usage (signature and decryption)
|
||||
[](https://asciinema.org/a/120441)
|
||||
|
||||
In order to use specific device type for GPG operations, set the following environment variable to either:
|
||||
```
|
||||
$ export GNUPGHOME=~/.gnupg/{trezor,ledger}
|
||||
```
|
||||
|
||||
You can use GNU Privacy Assistant (GPA) in order to inspect the created keys
|
||||
and perform signature and decryption operations using:
|
||||
### Inspect GPG keys
|
||||
You can use GNU Privacy Assistant (GPA) in order to inspect the created keys and perform signature and decryption operations as usual:
|
||||
|
||||
```
|
||||
$ sudo apt install gpa
|
||||
$ GNUPGHOME=~/.gnupg/trezor gpa
|
||||
$ gpa
|
||||
```
|
||||
|
||||
[](https://www.gnupg.org/related_software/swlist.html#gpa)
|
||||
|
||||
## Git commit & tag signatures:
|
||||
### Sign Git commits and tags
|
||||
|
||||
Git can use GPG to sign and verify commits and tags (see [here](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work)):
|
||||
|
||||
```
|
||||
$ git config --local commit.gpgsign 1
|
||||
$ git config --local gpg.program $(which gpg2)
|
||||
@@ -66,11 +70,29 @@ $ git tag v1.2.3 --sign # create GPG-signed tag
|
||||
$ git tag v1.2.3 --verify # verify tag signature
|
||||
```
|
||||
|
||||
## Password manager
|
||||
Note that your git email has to correlate to your gpg key email. If you use a different email for git, you'll need to either generate a new gpg key for that email or set your git email using the command:
|
||||
|
||||
First install `pass` from [passwordstore.org](https://www.passwordstore.org/) and initialize it to use your TREZOR-based GPG identity:
|
||||
````
|
||||
$ git config user.email foo@example.com
|
||||
````
|
||||
|
||||
If your git email is configured incorrectly, you will receive the error:
|
||||
|
||||
````
|
||||
error: gpg failed to sign the data
|
||||
fatal: failed to write commit object
|
||||
````
|
||||
|
||||
when committing to git.
|
||||
|
||||
### Manage passwords
|
||||
|
||||
Password managers such as [pass](https://www.passwordstore.org/) and [gopass](https://www.justwatch.com/gopass/) rely on GPG for encryption so you can use your device with them too.
|
||||
|
||||
##### With `pass`:
|
||||
|
||||
First install `pass` from [passwordstore.org] and initialize it to use your TREZOR-based GPG identity:
|
||||
```
|
||||
$ export GNUPGHOME=~/.gnupg/trezor
|
||||
$ pass init "Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
Password store initialized for Roman Zeyde <roman.zeyde@gmail.com>
|
||||
```
|
||||
@@ -99,10 +121,9 @@ Copied VPS/linode to clipboard. Will clear in 45 seconds.
|
||||
You can also use the following [Qt-based UI](https://qtpass.org/) for `pass`:
|
||||
```
|
||||
$ sudo apt install qtpass
|
||||
$ GNUPGHOME=~/.gnupg/trezor qtpass
|
||||
```
|
||||
|
||||
## Re-generation of an existing GPG identity
|
||||
### Re-generate a GPG identity
|
||||
[](https://asciinema.org/a/5tIQa5qt5bV134oeOqFyKEU29)
|
||||
|
||||
If you've forgotten the timestamp value, but still have access to the public key, then you can
|
||||
@@ -112,7 +133,7 @@ retrieve the timestamp with the following command (substitute "john@doe.bit" for
|
||||
$ gpg2 --export 'john@doe.bit' | gpg2 --list-packets | grep created | head -n1
|
||||
```
|
||||
|
||||
## Adding new user IDs
|
||||
### Add new UIDs to your identity
|
||||
|
||||
After your main identity is created, you can add new user IDs using the regular GnuPG commands:
|
||||
```
|
||||
@@ -144,7 +165,7 @@ uid [ultimate] Foobar
|
||||
ssb nistp256/35F58F26 2017-12-05 [E]
|
||||
```
|
||||
|
||||
## GnuPG subkey generation
|
||||
### Generate GnuPG subkeys
|
||||
In order to add TREZOR-based subkey to an existing GnuPG identity, use the `--subkey` flag:
|
||||
```
|
||||
$ gpg2 -k foobar
|
||||
@@ -173,3 +194,58 @@ There are 4 choices for the alternative pinentry (providing /usr/bin/pinentry).
|
||||
|
||||
Press <enter> to keep the current choice[*], or type selection number: 0
|
||||
```
|
||||
|
||||
### Sign and decrypt email
|
||||
|
||||
Follow [these instructions](enigmail.md) to set up Enigmail in Thunderbird.
|
||||
|
||||
### Start the agent as a systemd unit
|
||||
|
||||
##### 1. Create these files in `~/.config/systemd/user`
|
||||
|
||||
Replace `trezor` with `keepkey` or `ledger` as required.
|
||||
|
||||
###### `trezor-gpg-agent.service`
|
||||
|
||||
````
|
||||
[Unit]
|
||||
Description=trezor-gpg-agent
|
||||
Requires=trezor-gpg-agent.socket
|
||||
|
||||
[Service]
|
||||
Type=Simple
|
||||
Environment="GNUPGHOME=%h/.gnupg/trezor"
|
||||
Environment="PATH=/bin:/usr/bin:/usr/local/bin:%h/.local/bin"
|
||||
ExecStart=/usr/bin/trezor-gpg-agent -vv
|
||||
````
|
||||
|
||||
If you've installed `trezor-agent` locally you may have to change the path in `ExecStart=`.
|
||||
|
||||
###### `trezor-gpg-agent.socket`
|
||||
|
||||
````
|
||||
[Unit]
|
||||
Description=trezor-gpg-agent socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=%t/gnupg/S.gpg-agent
|
||||
FileDescriptorName=std
|
||||
SocketMode=0600
|
||||
DirectoryMode=0700
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
````
|
||||
|
||||
##### 2. Stop trezor-gpg-agent if it's already running
|
||||
|
||||
```
|
||||
killall trezor-gpg-agent
|
||||
```
|
||||
|
||||
##### 3. Run
|
||||
|
||||
```
|
||||
systemctl --user start trezor-gpg-agent.service trezor-gpg-agent.socket
|
||||
systemctl --user enable trezor-gpg-agent.socket
|
||||
```
|
||||
|
||||
69
doc/README-PINENTRY.md
Normal file
69
doc/README-PINENTRY.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Custom PIN entry
|
||||
|
||||
In order to use the default GPG pinentry program, install one of the following Linux packages:
|
||||
|
||||
```
|
||||
$ apt install pinentry-{curses,gnome3,qt}
|
||||
```
|
||||
|
||||
or (on macOS):
|
||||
|
||||
```
|
||||
$ brew install pinentry
|
||||
```
|
||||
|
||||
By default a standard GPG PIN entry program is used when entering your Trezor PIN, but it's difficult to use if you don't have a numeric keypad or want to use your mouse.
|
||||
|
||||
You can specify a custom PIN entry program such as [trezor-gpg-pinentry-tk](https://github.com/rendaw/trezor-gpg-pinentry-tk) (and separately, a passphrase entry program) to match your workflow.
|
||||
|
||||
The below examples use `trezor-gpg-pinentry-tk` but any GPG compatible PIN entry can be used.
|
||||
|
||||
##### 1. Install the PIN entry
|
||||
|
||||
Run
|
||||
|
||||
```
|
||||
pip install trezor-gpg-pinentry-tk
|
||||
```
|
||||
|
||||
##### 2. SSH
|
||||
|
||||
Add the flag `--pin-entry-binary trezor-gpg-pinentry-tk` to all calls to `trezor-agent`.
|
||||
|
||||
To automatically use this flag, add the line `pinentry=trezor-gpg-pinentry-tk` to `~/.ssh/agent.config`. **Note** this is currently broken due to [this dependency issue](https://github.com/bw2/ConfigArgParse/issues/114).
|
||||
|
||||
If you run the SSH agent with Systemd you'll need to add `--pin-entry-binary` to the `ExecStart` command. You may also need to add this line:
|
||||
|
||||
```
|
||||
Environment="DISPLAY=:0"
|
||||
```
|
||||
|
||||
to the `[Service]` section to tell the PIN entry program how to connect to the X11 server.
|
||||
|
||||
##### 3. GPG
|
||||
|
||||
If you haven't completed initialization yet, run:
|
||||
|
||||
```
|
||||
$ (trezor|keepkey|ledger)-gpg init --pin-entry-binary trezor-gpg-pinentry-tk "Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
```
|
||||
|
||||
to configure the PIN entry at the same time.
|
||||
|
||||
Otherwise, open `$GNUPGHOME/trezor/run-agent.sh` and change the `--pin-entry-binary` option to `trezor-gpg-pinentry-tk` and run:
|
||||
|
||||
```
|
||||
killall trezor-gpg-agent
|
||||
```
|
||||
|
||||
##### 4. Troubleshooting
|
||||
|
||||
Any problems running the PIN entry program with GPG should appear in `$HOME/.gnupg/trezor/gpg-agent.log`.
|
||||
|
||||
You can get similar logs for SSH by specifying `--log-file` in the SSH command line.
|
||||
|
||||
The passphrase is cached by the agent (after its first entry), which needs to be restarted in order to reset the passphrase:
|
||||
```
|
||||
$ killall trezor-agent # (for SSH)
|
||||
$ killall trezor-gpg-agent # (for GPG)
|
||||
```
|
||||
@@ -1,18 +1,74 @@
|
||||
# Screencast demo usage
|
||||
# SSH Agent
|
||||
|
||||
## Simple usage (single SSH session)
|
||||
## 1. Configuration
|
||||
|
||||
SSH requires no configuration, but you may put common command line options in `~/.ssh/agent.conf` to avoid repeating them in every invocation.
|
||||
|
||||
See `(trezor|keepkey|ledger)-agent -h` for details on supported options and the configuration file format.
|
||||
|
||||
If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md).
|
||||
|
||||
## 2. Usage
|
||||
|
||||
Use the `(trezor|keepkey|ledger)-agent` program to work with SSH. It has three main modes of operation:
|
||||
|
||||
##### 1. Export public keys
|
||||
|
||||
To get your public key so you can add it to `authorized_hosts` or allow
|
||||
ssh access to a service that supports it, run:
|
||||
|
||||
```
|
||||
(trezor|keepkey|ledger)-agent identity@myhost
|
||||
```
|
||||
|
||||
The identity (ex: `identity@myhost`) is used to derive the public key and is added as a comment to the exported key string.
|
||||
|
||||
##### 2. Run a command with the agent's environment
|
||||
|
||||
Run
|
||||
|
||||
```
|
||||
$ (trezor|keepkey|ledger)-agent identity@myhost -- COMMAND --WITH --ARGUMENTS
|
||||
```
|
||||
|
||||
to start the agent in the background and execute the command with environment variables set up to use the SSH agent. The specified identity is used for all SSH connections. The agent will exit after the command completes.
|
||||
|
||||
As a shortcut you can run
|
||||
|
||||
```
|
||||
$ (trezor|keepkey|ledger)-agent identity@myhost -s
|
||||
```
|
||||
|
||||
to start a shell with the proper environment.
|
||||
|
||||
##### 2. Connect to a server directly via `(trezor|keepkey|ledger)-agent`
|
||||
|
||||
If you just want to connect to a server this is the simplest way to do it:
|
||||
|
||||
```
|
||||
$ (trezor|keepkey|ledger)-agent user@remotehost -c
|
||||
```
|
||||
|
||||
The identity `user@remotehost` is used as both the destination user and host as well as for key derivation, so you must generate a separate key for each host you connect to.
|
||||
|
||||
## 3. Common Use Cases
|
||||
|
||||
### Start a single SSH session
|
||||
[](https://asciinema.org/a/22959)
|
||||
|
||||
## Advanced usage (multiple SSH sessions from a sub-shell)
|
||||
### Start multiple SSH sessions from a sub-shell
|
||||
|
||||
This feature allows using regular SSH-related commands within a subprocess running user's shell.
|
||||
`SSH_AUTH_SOCK` environment variable is defined for the subprocess (pointing to the SSH agent, running as a parent process).
|
||||
This way the user can use SSH-related commands (e.g. `ssh`, `ssh-add`, `sshfs`, `git`, `hg`), while authenticating via the hardware device.
|
||||
|
||||
[](https://asciinema.org/a/33240)
|
||||
|
||||
## Using for GitHub SSH authentication (via `trezor-git` utility)
|
||||
[](https://asciinema.org/a/38337)
|
||||
### Load different SSH identities from configuration file
|
||||
|
||||
## Loading multiple SSH identities from configuration file
|
||||
[](https://asciinema.org/a/bdxxtgctk5syu56yfz8lcp7ny)
|
||||
|
||||
# Public key generation
|
||||
### Implement passwordless login
|
||||
|
||||
Run:
|
||||
|
||||
@@ -26,34 +82,13 @@ Append `hostname.pub` contents to `/home/user/.ssh/authorized_keys`
|
||||
configuration file at `ssh.hostname.com`, so the remote server
|
||||
would allow you to login using the corresponding private key signature.
|
||||
|
||||
# Usage
|
||||
### Access remote Git/Mercurial repositories
|
||||
|
||||
Run:
|
||||
|
||||
/tmp $ trezor-agent user@ssh.hostname.com -v -c
|
||||
2015-09-02 15:09:39,782 INFO getting "ssh://user@ssh.hostname.com" public key from Trezor...
|
||||
2015-09-02 15:09:44,430 INFO please confirm user "roman" login to "ssh://user@ssh.hostname.com" using Trezor...
|
||||
2015-09-02 15:09:46,152 INFO signature status: OK
|
||||
Linux lmde 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1+deb8u3 (2015-08-04) x86_64
|
||||
|
||||
The programs included with the Debian GNU/Linux system are free software;
|
||||
the exact distribution terms for each program are described in the
|
||||
individual files in /usr/share/doc/*/copyright.
|
||||
|
||||
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
|
||||
permitted by applicable law.
|
||||
Last login: Tue Sep 1 15:57:05 2015 from localhost
|
||||
~ $
|
||||
|
||||
Make sure to confirm SSH signature on the Trezor device when requested.
|
||||
|
||||
## Accessing remote Git/Mercurial repositories
|
||||
|
||||
Use your SSH public key to access your remote repository (e.g. [GitHub](https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/)):
|
||||
Copy your public key and register it in your repository web interface (e.g. [GitHub](https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/)):
|
||||
|
||||
$ trezor-agent -v -e ed25519 git@github.com | xclip
|
||||
|
||||
Use the following Bash alias for convinient Git operations:
|
||||
Use the following Bash alias for convenient Git operations:
|
||||
|
||||
$ alias git_hub='trezor-agent -v -e ed25519 git@github.com -- git'
|
||||
|
||||
@@ -65,13 +100,68 @@ The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.c
|
||||
|
||||
$ trezor-agent -v -e ed25519 git@bitbucket.org -- hg push
|
||||
|
||||
### Start the agent as a systemd unit
|
||||
|
||||
# Troubleshooting
|
||||
##### 1. Create these files in `~/.config/systemd/user`
|
||||
|
||||
Replace `trezor` with `keepkey` or `ledger` as required.
|
||||
|
||||
###### `trezor-ssh-agent.service`
|
||||
|
||||
````
|
||||
[Unit]
|
||||
Description=trezor-agent SSH agent
|
||||
Requires=trezor-ssh-agent.socket
|
||||
|
||||
[Service]
|
||||
Type=Simple
|
||||
Environment="DISPLAY=:0"
|
||||
Environment="PATH=/bin:/usr/bin:/usr/local/bin:%h/.local/bin"
|
||||
ExecStart=/usr/bin/trezor-agent --foreground --sock-path %t/trezor-agent/S.ssh IDENTITY
|
||||
````
|
||||
|
||||
If you've installed `trezor-agent` locally you may have to change the path in `ExecStart=`.
|
||||
|
||||
Replace `IDENTITY` with the identity you used when exporting the public key.
|
||||
|
||||
###### `trezor-ssh-agent.socket`
|
||||
|
||||
````
|
||||
[Unit]
|
||||
Description=trezor-agent SSH agent socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=%t/trezor-agent/S.ssh
|
||||
FileDescriptorName=ssh
|
||||
Service=trezor-ssh-agent.service
|
||||
SocketMode=0600
|
||||
DirectoryMode=0700
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
````
|
||||
|
||||
##### 2. Run
|
||||
|
||||
```
|
||||
systemctl --user start trezor-ssh-agent.service trezor-ssh-agent.socket
|
||||
systemctl --user enable trezor-ssh-agent.socket
|
||||
```
|
||||
|
||||
##### 3. Add this line to your `.bashrc` or equivalent file:
|
||||
|
||||
```bash
|
||||
export SSH_AUTH_SOCK=$(systemctl show --user --property=Listen trezor-ssh-agent.socket | grep -o "/run.*")
|
||||
```
|
||||
|
||||
##### 4. SSH will now automatically use your device key in all terminals.
|
||||
|
||||
## 4. Troubleshooting
|
||||
|
||||
If SSH connection fails to work, please open an [issue](https://github.com/romanz/trezor-agent/issues)
|
||||
with a verbose log attached (by running `trezor-agent -vv`) .
|
||||
|
||||
## Incompatible SSH options
|
||||
##### `IdentitiesOnly` SSH option
|
||||
|
||||
Note that your local SSH configuration may ignore `trezor-agent`, if it has `IdentitiesOnly` option set to `yes`.
|
||||
|
||||
@@ -82,6 +172,21 @@ Note that your local SSH configuration may ignore `trezor-agent`, if it has `Ide
|
||||
This option is intended for situations where ssh-agent offers many different identities.
|
||||
The default is “no”.
|
||||
|
||||
If you are failing to connect, try running:
|
||||
If you are failing to connect, save your public key using:
|
||||
|
||||
$ trezor-agent -vv user@host -- ssh -vv -oIdentitiesOnly=no user@host
|
||||
$ trezor-agent -vv foobar@hostname.com > ~/.ssh/hostname.pub
|
||||
|
||||
And add the following lines to `~/.ssh/config` (providing the public key explicitly to SSH):
|
||||
|
||||
Host hostname.com
|
||||
User foobar
|
||||
IdentityFile ~/.ssh/hostname.pub
|
||||
|
||||
Then, the following commands should successfully command to the remote host:
|
||||
|
||||
$ trezor-agent -v foobar@hostname.com -s
|
||||
$ ssh foobar@hostname.com
|
||||
|
||||
or,
|
||||
|
||||
$ trezor-agent -v foobar@hostname.com -c
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Cryptographic hardware device management."""
|
||||
|
||||
from . import interface
|
||||
from . import interface, ui
|
||||
|
||||
@@ -32,6 +32,9 @@ class KeepKey(trezor.Trezor):
|
||||
|
||||
required_version = '>=1.0.4'
|
||||
|
||||
def _override_state_handler(self, _):
|
||||
"""No support for `state` handling on Keepkey."""
|
||||
|
||||
def pubkey(self, identity, ecdh=False):
|
||||
"""Return public key."""
|
||||
_verify_support(identity, ecdh)
|
||||
|
||||
@@ -5,5 +5,10 @@
|
||||
from keepkeylib.client import CallException, PinException
|
||||
from keepkeylib.client import KeepKeyClient as Client
|
||||
from keepkeylib.messages_pb2 import PassphraseAck, PinMatrixAck
|
||||
from keepkeylib.transport_hid import HidTransport as Transport
|
||||
from keepkeylib.transport_hid import HidTransport
|
||||
from keepkeylib.types_pb2 import IdentityType
|
||||
|
||||
|
||||
def enumerate_transports():
|
||||
"""Returns USB HID transports."""
|
||||
return [HidTransport(p) for p in HidTransport.enumerate()]
|
||||
|
||||
@@ -2,35 +2,16 @@
|
||||
|
||||
import binascii
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import mnemonic
|
||||
import semver
|
||||
|
||||
from . import interface
|
||||
from .. import util
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _message_box(label, sp=subprocess):
|
||||
"""Launch an external process for PIN/passphrase entry GUI."""
|
||||
cmd = ('import sys, pymsgbox; '
|
||||
'sys.stdout.write(pymsgbox.password(sys.stdin.read()))')
|
||||
args = [sys.executable, '-c', cmd]
|
||||
p = sp.Popen(args=args, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||
out, err = p.communicate(label.encode('ascii'))
|
||||
exitcode = p.wait()
|
||||
if exitcode != 0:
|
||||
log.error('UI failed: %r', err)
|
||||
raise sp.CalledProcessError(exitcode, args)
|
||||
return out.decode('ascii')
|
||||
|
||||
|
||||
def _is_open_tty(stream):
|
||||
return not stream.closed and os.isatty(stream.fileno())
|
||||
|
||||
|
||||
class Trezor(interface.Device):
|
||||
"""Connection to TREZOR device."""
|
||||
|
||||
@@ -42,50 +23,71 @@ class Trezor(interface.Device):
|
||||
@property
|
||||
def _defs(self):
|
||||
from . import trezor_defs
|
||||
# Allow using TREZOR bridge transport (instead of the HID default)
|
||||
trezor_defs.Transport = {
|
||||
'bridge': trezor_defs.BridgeTransport,
|
||||
'udp': trezor_defs.UdpTransport,
|
||||
'hid': trezor_defs.HidTransport,
|
||||
}[os.environ.get('TREZOR_TRANSPORT', 'hid')]
|
||||
return trezor_defs
|
||||
|
||||
required_version = '>=1.4.0'
|
||||
|
||||
def _override_pin_handler(self, conn):
|
||||
cli_handler = conn.callback_PinMatrixRequest
|
||||
ui = None # can be overridden by device's users
|
||||
|
||||
def new_handler(msg):
|
||||
if _is_open_tty(sys.stdin):
|
||||
result = cli_handler(msg) # CLI-based PIN handler
|
||||
else:
|
||||
scrambled_pin = _message_box(
|
||||
'Use the numeric keypad to describe number positions.\n'
|
||||
'The layout is:\n'
|
||||
' 7 8 9\n'
|
||||
' 4 5 6\n'
|
||||
' 1 2 3\n'
|
||||
'Please enter PIN:')
|
||||
def _override_pin_handler(self, conn):
|
||||
if self.ui is None:
|
||||
return
|
||||
|
||||
def new_handler(_):
|
||||
try:
|
||||
scrambled_pin = self.ui.get_pin()
|
||||
result = self._defs.PinMatrixAck(pin=scrambled_pin)
|
||||
if not set(result.pin).issubset('123456789'):
|
||||
raise self._defs.PinException(
|
||||
None, 'Invalid scrambled PIN: {!r}'.format(result.pin))
|
||||
return result
|
||||
if not set(scrambled_pin).issubset('123456789'):
|
||||
raise self._defs.PinException(
|
||||
None, 'Invalid scrambled PIN: {!r}'.format(result.pin))
|
||||
return result
|
||||
except: # noqa
|
||||
conn.init_device()
|
||||
raise
|
||||
|
||||
conn.callback_PinMatrixRequest = new_handler
|
||||
|
||||
cached_passphrase_ack = util.ExpiringCache(seconds=float('inf'))
|
||||
cached_state = None
|
||||
|
||||
def _override_passphrase_handler(self, conn):
|
||||
cli_handler = conn.callback_PassphraseRequest
|
||||
if self.ui is None:
|
||||
return
|
||||
|
||||
def new_handler(msg):
|
||||
if _is_open_tty(sys.stdin):
|
||||
return cli_handler(msg) # CLI-based PIN handler
|
||||
try:
|
||||
if msg.on_device is True:
|
||||
return self._defs.PassphraseAck()
|
||||
ack = self.__class__.cached_passphrase_ack.get()
|
||||
if ack:
|
||||
log.debug('re-using cached %s passphrase', self)
|
||||
return ack
|
||||
|
||||
passphrase = _message_box('Please enter passphrase:')
|
||||
return self._defs.PassphraseAck(passphrase=passphrase)
|
||||
passphrase = self.ui.get_passphrase()
|
||||
passphrase = mnemonic.Mnemonic.normalize_string(passphrase)
|
||||
ack = self._defs.PassphraseAck(passphrase=passphrase)
|
||||
|
||||
length = len(ack.passphrase)
|
||||
if length > 50:
|
||||
msg = 'Too long passphrase ({} chars)'.format(length)
|
||||
raise ValueError(msg)
|
||||
|
||||
self.__class__.cached_passphrase_ack.set(ack)
|
||||
return ack
|
||||
except: # noqa
|
||||
conn.init_device()
|
||||
raise
|
||||
|
||||
conn.callback_PassphraseRequest = new_handler
|
||||
|
||||
def _override_state_handler(self, conn):
|
||||
def callback_PassphraseStateRequest(msg):
|
||||
log.debug('caching state from %r', msg)
|
||||
self.__class__.cached_state = msg.state
|
||||
return self._defs.PassphraseStateAck()
|
||||
|
||||
conn.callback_PassphraseStateRequest = callback_PassphraseStateRequest
|
||||
|
||||
def _verify_version(self, connection):
|
||||
f = connection.features
|
||||
log.debug('connected to %s %s', self, f.device_id)
|
||||
@@ -103,27 +105,30 @@ class Trezor(interface.Device):
|
||||
current_version))
|
||||
|
||||
def connect(self):
|
||||
"""Enumerate and connect to the first USB HID interface."""
|
||||
for transport in self._defs.Transport.enumerate():
|
||||
log.debug('transport: %s', transport)
|
||||
for _ in range(5):
|
||||
connection = self._defs.Client(transport)
|
||||
self._override_pin_handler(connection)
|
||||
self._override_passphrase_handler(connection)
|
||||
self._verify_version(connection)
|
||||
"""Enumerate and connect to the first available interface."""
|
||||
transports = self._defs.enumerate_transports()
|
||||
if not transports:
|
||||
raise interface.NotFoundError('{} not connected'.format(self))
|
||||
|
||||
try:
|
||||
connection.ping(msg='', pin_protection=True) # unlock PIN
|
||||
return connection
|
||||
except (self._defs.PinException, ValueError) as e:
|
||||
log.error('Invalid PIN: %s, retrying...', e)
|
||||
continue
|
||||
except Exception as e:
|
||||
log.exception('ping failed: %s', e)
|
||||
connection.close() # so the next HID open() will succeed
|
||||
raise
|
||||
log.debug('transports: %s', transports)
|
||||
for _ in range(5): # Retry a few times in case of PIN failures
|
||||
connection = self._defs.Client(transport=transports[0],
|
||||
state=self.__class__.cached_state)
|
||||
self._override_pin_handler(connection)
|
||||
self._override_passphrase_handler(connection)
|
||||
self._override_state_handler(connection)
|
||||
self._verify_version(connection)
|
||||
|
||||
raise interface.NotFoundError('{} not connected'.format(self))
|
||||
try:
|
||||
connection.ping(msg='', pin_protection=True) # unlock PIN
|
||||
return connection
|
||||
except (self._defs.PinException, ValueError) as e:
|
||||
log.error('Invalid PIN: %s, retrying...', e)
|
||||
continue
|
||||
except Exception as e:
|
||||
log.exception('ping failed: %s', e)
|
||||
connection.close() # so the next HID open() will succeed
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
"""Close connection."""
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
from trezorlib.client import CallException, PinException
|
||||
from trezorlib.client import TrezorClient as Client
|
||||
from trezorlib.messages import IdentityType, PassphraseAck, PinMatrixAck
|
||||
from trezorlib.transport_bridge import BridgeTransport
|
||||
from trezorlib.transport_hid import HidTransport
|
||||
from trezorlib.transport_udp import UdpTransport
|
||||
from trezorlib.messages import IdentityType, PassphraseAck, PinMatrixAck, PassphraseStateAck
|
||||
from trezorlib.device import TrezorDevice
|
||||
|
||||
|
||||
def enumerate_transports():
|
||||
"""Returns all available transports."""
|
||||
return TrezorDevice.enumerate()
|
||||
|
||||
129
libagent/device/ui.py
Normal file
129
libagent/device/ui.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""UIs for PIN/passphrase entry."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from .. import util
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UI(object):
|
||||
"""UI for PIN/passphrase entry (for TREZOR devices)."""
|
||||
|
||||
def __init__(self, device_type, config=None):
|
||||
"""C-tor."""
|
||||
default_pinentry = 'pinentry' # by default, use GnuPG pinentry tool
|
||||
if config is None:
|
||||
config = {}
|
||||
self.pin_entry_binary = config.get('pin_entry_binary',
|
||||
default_pinentry)
|
||||
self.passphrase_entry_binary = config.get('passphrase_entry_binary',
|
||||
default_pinentry)
|
||||
self.options_getter = create_default_options_getter()
|
||||
self.device_name = device_type.__name__
|
||||
|
||||
def get_pin(self, name=None):
|
||||
"""Ask the user for (scrambled) PIN."""
|
||||
description = (
|
||||
'Use the numeric keypad to describe number positions.\n'
|
||||
'The layout is:\n'
|
||||
' 7 8 9\n'
|
||||
' 4 5 6\n'
|
||||
' 1 2 3')
|
||||
return interact(
|
||||
title='{} PIN'.format(name or self.device_name),
|
||||
prompt='PIN:',
|
||||
description=description,
|
||||
binary=self.pin_entry_binary,
|
||||
options=self.options_getter())
|
||||
|
||||
def get_passphrase(self, name=None):
|
||||
"""Ask the user for passphrase."""
|
||||
return interact(
|
||||
title='{} passphrase'.format(name or self.device_name),
|
||||
prompt='Passphrase:',
|
||||
description=None,
|
||||
binary=self.passphrase_entry_binary,
|
||||
options=self.options_getter())
|
||||
|
||||
|
||||
def create_default_options_getter():
|
||||
"""Return current TTY and DISPLAY settings for GnuPG pinentry."""
|
||||
options = []
|
||||
try:
|
||||
ttyname = subprocess.check_output(args=['tty']).strip()
|
||||
options.append(b'ttyname=' + ttyname)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log.warning('no TTY found: %s', e)
|
||||
|
||||
display = os.environ.get('DISPLAY')
|
||||
if display is not None:
|
||||
options.append('display={}'.format(display).encode('ascii'))
|
||||
else:
|
||||
log.warning('DISPLAY not defined')
|
||||
|
||||
log.info('using %s for pinentry options', options)
|
||||
return lambda: options
|
||||
|
||||
|
||||
def write(p, line):
|
||||
"""Send and flush a single line to the subprocess' stdin."""
|
||||
log.debug('%s <- %r', p.args, line)
|
||||
p.stdin.write(line)
|
||||
p.stdin.flush()
|
||||
|
||||
|
||||
class UnexpectedError(Exception):
|
||||
"""Unexpected response."""
|
||||
|
||||
|
||||
def expect(p, prefixes, confidential=False):
|
||||
"""Read a line and return it without required prefix."""
|
||||
resp = p.stdout.readline()
|
||||
log.debug('%s -> %r', p.args, resp if not confidential else '********')
|
||||
for prefix in prefixes:
|
||||
if resp.startswith(prefix):
|
||||
return resp[len(prefix):]
|
||||
raise UnexpectedError(resp)
|
||||
|
||||
|
||||
def interact(title, description, prompt, binary, options):
|
||||
"""Use GPG pinentry program to interact with the user."""
|
||||
args = [binary]
|
||||
p = subprocess.Popen(args=args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
env=os.environ)
|
||||
p.args = args # TODO: remove after Python 2 deprecation.
|
||||
expect(p, [b'OK'])
|
||||
|
||||
title = util.assuan_serialize(title.encode('ascii'))
|
||||
write(p, b'SETTITLE ' + title + b'\n')
|
||||
expect(p, [b'OK'])
|
||||
|
||||
if description:
|
||||
description = util.assuan_serialize(description.encode('ascii'))
|
||||
write(p, b'SETDESC ' + description + b'\n')
|
||||
expect(p, [b'OK'])
|
||||
|
||||
if prompt:
|
||||
prompt = util.assuan_serialize(prompt.encode('ascii'))
|
||||
write(p, b'SETPROMPT ' + prompt + b'\n')
|
||||
expect(p, [b'OK'])
|
||||
|
||||
log.debug('setting %d options', len(options))
|
||||
for opt in options:
|
||||
write(p, b'OPTION ' + opt + b'\n')
|
||||
expect(p, [b'OK', b'ERR'])
|
||||
|
||||
write(p, b'GETPIN\n')
|
||||
pin = expect(p, [b'OK', b'D '], confidential=True)
|
||||
|
||||
p.communicate() # close stdin and wait for the process to exit
|
||||
exit_code = p.wait()
|
||||
if exit_code:
|
||||
raise subprocess.CalledProcessError(exit_code, binary)
|
||||
|
||||
return pin.decode('ascii').strip()
|
||||
@@ -123,10 +123,17 @@ def run_init(device_type, args):
|
||||
# Prepare new GPG home directory for hardware-based identity
|
||||
device_name = os.path.basename(sys.argv[0]).rsplit('-', 1)[0]
|
||||
log.info('device name: %s', device_name)
|
||||
homedir = os.path.expanduser('~/.gnupg/{}'.format(device_name))
|
||||
homedir = args.homedir
|
||||
if not homedir:
|
||||
homedir = os.path.expanduser('~/.gnupg/{}'.format(device_name))
|
||||
|
||||
log.info('GPG home directory: %s', homedir)
|
||||
|
||||
check_call(['rm', '-rf', homedir])
|
||||
if os.path.exists(homedir):
|
||||
log.error('GPG home directory %s exists, '
|
||||
'remove it manually if required', homedir)
|
||||
sys.exit(1)
|
||||
|
||||
check_call(['mkdir', '-p', homedir])
|
||||
check_call(['chmod', '700', homedir])
|
||||
|
||||
@@ -134,11 +141,16 @@ def run_init(device_type, args):
|
||||
|
||||
# Prepare GPG agent invocation script (to pass the PATH from environment).
|
||||
with open(os.path.join(homedir, 'run-agent.sh'), 'w') as f:
|
||||
f.write("""#!/bin/sh
|
||||
f.write(r"""#!/bin/sh
|
||||
export PATH={0}
|
||||
{1} $*
|
||||
""".format(os.environ['PATH'], agent_path))
|
||||
check_call(['chmod', 'u+x', f.name])
|
||||
{1} \
|
||||
-vv \
|
||||
--pin-entry-binary={pin_entry_binary} \
|
||||
--passphrase-entry-binary={passphrase_entry_binary} \
|
||||
--cache-expiry-seconds={cache_expiry_seconds} \
|
||||
$*
|
||||
""".format(os.environ['PATH'], agent_path, **vars(args)))
|
||||
check_call(['chmod', '700', f.name])
|
||||
run_agent_script = f.name
|
||||
|
||||
# Prepare GPG configuration file
|
||||
@@ -149,13 +161,6 @@ personal-digest-preferences SHA512
|
||||
default-key \"{1}\"
|
||||
""".format(run_agent_script, args.user_id))
|
||||
|
||||
# Prepare GPG agent configuration file
|
||||
with open(os.path.join(homedir, 'gpg-agent.conf'), 'w') as f:
|
||||
f.write("""# Hardware-based GPG agent emulator
|
||||
log-file {0}/gpg-agent.log
|
||||
verbosity 2
|
||||
""".format(homedir))
|
||||
|
||||
# Prepare a helper script for setting up the new identity
|
||||
with open(os.path.join(homedir, 'env'), 'w') as f:
|
||||
f.write("""#!/bin/bash
|
||||
@@ -169,13 +174,14 @@ else
|
||||
${{COMMAND}}
|
||||
fi
|
||||
""".format(homedir))
|
||||
check_call(['chmod', 'u+x', f.name])
|
||||
check_call(['chmod', '700', f.name])
|
||||
|
||||
# Generate new GPG identity and import into GPG keyring
|
||||
pubkey = write_file(os.path.join(homedir, 'pubkey.asc'),
|
||||
export_public_key(device_type, args))
|
||||
gpg_binary = keyring.get_gnupg_binary()
|
||||
check_call([gpg_binary, '--homedir', homedir, '--quiet',
|
||||
verbosity = ('-' + ('v' * args.verbose)) if args.verbose else '--quiet'
|
||||
check_call([gpg_binary, '--homedir', homedir, verbosity,
|
||||
'--import', pubkey.name])
|
||||
|
||||
# Make new GPG identity with "ultimate" trust (via its fingerprint)
|
||||
@@ -187,7 +193,8 @@ fi
|
||||
'--import-ownertrust', f.name])
|
||||
|
||||
# Load agent and make sure it responds with the new identity
|
||||
check_call([gpg_binary, '--list-secret-keys'], env={'GNUPGHOME': homedir})
|
||||
check_call([gpg_binary, '--list-secret-keys', args.user_id],
|
||||
env={'GNUPGHOME': homedir})
|
||||
|
||||
|
||||
def run_unlock(device_type, args):
|
||||
@@ -199,19 +206,24 @@ def run_unlock(device_type, args):
|
||||
|
||||
def run_agent(device_type):
|
||||
"""Run a simple GPG-agent server."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--homedir', default=os.environ.get('GNUPGHOME'))
|
||||
args, _ = parser.parse_known_args()
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--homedir', default=os.environ.get('GNUPGHOME'))
|
||||
p.add_argument('-v', '--verbose', default=0, action='count')
|
||||
|
||||
p.add_argument('--pin-entry-binary', type=str, default='pinentry',
|
||||
help='Path to PIN entry UI helper.')
|
||||
p.add_argument('--passphrase-entry-binary', type=str, default='pinentry',
|
||||
help='Path to passphrase entry UI helper.')
|
||||
p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'),
|
||||
help='Expire passphrase from cache after this duration.')
|
||||
|
||||
args, _ = p.parse_known_args()
|
||||
|
||||
assert args.homedir
|
||||
config_file = os.path.join(args.homedir, 'gpg-agent.conf')
|
||||
|
||||
lines = (line.strip() for line in open(config_file))
|
||||
lines = (line for line in lines if line and not line.startswith('#'))
|
||||
config = dict(line.split(' ', 1) for line in lines)
|
||||
log_file = os.path.join(args.homedir, 'gpg-agent.log')
|
||||
util.setup_logging(verbosity=args.verbose, filename=log_file)
|
||||
|
||||
util.setup_logging(verbosity=int(config['verbosity']),
|
||||
filename=config['log-file'])
|
||||
log.debug('sys.argv: %s', sys.argv)
|
||||
log.debug('os.environ: %s', os.environ)
|
||||
log.debug('pid: %d, parent pid: %d', os.getpid(), os.getppid())
|
||||
@@ -219,9 +231,14 @@ def run_agent(device_type):
|
||||
env = {'GNUPGHOME': args.homedir}
|
||||
sock_path = keyring.get_agent_sock_path(env=env)
|
||||
pubkey_bytes = keyring.export_public_keys(env=env)
|
||||
handler = agent.Handler(device=device_type(), pubkey_bytes=pubkey_bytes)
|
||||
device_type.ui = device.ui.UI(device_type=device_type,
|
||||
config=vars(args))
|
||||
device_type.cached_passphrase_ack = util.ExpiringCache(
|
||||
seconds=float(args.cache_expiry_seconds))
|
||||
with server.unix_domain_socket_server(sock_path) as sock:
|
||||
for conn in agent.yield_connections(sock):
|
||||
handler = agent.Handler(device=device_type(),
|
||||
pubkey_bytes=pubkey_bytes)
|
||||
with contextlib.closing(conn):
|
||||
try:
|
||||
handler.handle(conn)
|
||||
@@ -255,6 +272,17 @@ def main(device_type):
|
||||
p.add_argument('-t', '--time', type=int, default=int(time.time()))
|
||||
p.add_argument('-v', '--verbose', default=0, action='count')
|
||||
p.add_argument('-s', '--subkey', default=False, action='store_true')
|
||||
|
||||
p.add_argument('--homedir', type=str,
|
||||
help='Customize GnuPG home directory for the new identity.')
|
||||
|
||||
p.add_argument('--pin-entry-binary', type=str, default='pinentry',
|
||||
help='Path to PIN entry UI helper.')
|
||||
p.add_argument('--passphrase-entry-binary', type=str, default='pinentry',
|
||||
help='Path to passphrase entry UI helper.')
|
||||
p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'),
|
||||
help='Expire passphrase from cache after this duration.')
|
||||
|
||||
p.set_defaults(func=run_init)
|
||||
|
||||
p = subparsers.add_parser('unlock', help='unlock the hardware device')
|
||||
@@ -262,4 +290,8 @@ def main(device_type):
|
||||
p.set_defaults(func=run_unlock)
|
||||
|
||||
args = parser.parse_args()
|
||||
device_type.ui = device.ui.UI(device_type=device_type, config=vars(args))
|
||||
device_type.cached_passphrase_ack = util.ExpiringCache(
|
||||
seconds=float(args.cache_expiry_seconds))
|
||||
|
||||
return args.func(device_type=device_type, args=args)
|
||||
|
||||
@@ -21,25 +21,17 @@ def yield_connections(sock):
|
||||
yield conn
|
||||
|
||||
|
||||
def serialize(data):
|
||||
"""Serialize data according to ASSUAN protocol."""
|
||||
for c in [b'%', b'\n', b'\r']:
|
||||
escaped = '%{:02X}'.format(ord(c)).encode('ascii')
|
||||
data = data.replace(c, escaped)
|
||||
return data
|
||||
|
||||
|
||||
def sig_encode(r, s):
|
||||
"""Serialize ECDSA signature data into GPG S-expression."""
|
||||
r = serialize(util.num2bytes(r, 32))
|
||||
s = serialize(util.num2bytes(s, 32))
|
||||
r = util.assuan_serialize(util.num2bytes(r, 32))
|
||||
s = util.assuan_serialize(util.num2bytes(s, 32))
|
||||
return b'(7:sig-val(5:ecdsa(1:r32:' + r + b')(1:s32:' + s + b')))'
|
||||
|
||||
|
||||
def _serialize_point(data):
|
||||
prefix = '{}:'.format(len(data)).encode('ascii')
|
||||
# https://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
|
||||
return b'(5:value' + serialize(prefix + data) + b')'
|
||||
return b'(5:value' + util.assuan_serialize(prefix + data) + b')'
|
||||
|
||||
|
||||
def parse_ecdh(line):
|
||||
@@ -77,27 +69,30 @@ class AgentStop(Exception):
|
||||
"""Raised to close the agent."""
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class Handler(object):
|
||||
"""GPG agent requests' handler."""
|
||||
|
||||
def _get_options(self):
|
||||
return self.options
|
||||
|
||||
def __init__(self, device, pubkey_bytes):
|
||||
"""C-tor."""
|
||||
self.reset()
|
||||
self.options = []
|
||||
device.ui.options_getter = self._get_options
|
||||
self.client = client.Client(device=device)
|
||||
# Cache ASSUAN commands' arguments between commands
|
||||
self.keygrip = None
|
||||
self.digest = None
|
||||
self.algo = None
|
||||
# Cache public keys from GnuPG
|
||||
self.pubkey_bytes = pubkey_bytes
|
||||
# "Clone" existing GPG version
|
||||
self.version = keyring.gpg_version()
|
||||
|
||||
self.handlers = {
|
||||
b'RESET': None,
|
||||
b'OPTION': None,
|
||||
b'RESET': lambda *_: self.reset(),
|
||||
b'OPTION': lambda _, args: self.handle_option(*args),
|
||||
b'SETKEYDESC': None,
|
||||
b'NOP': None,
|
||||
b'GETINFO': lambda conn, _: keyring.sendline(conn, b'D ' + self.version),
|
||||
b'GETINFO': self.handle_getinfo,
|
||||
b'AGENT_ID': lambda conn, _: keyring.sendline(conn, b'D TREZOR'), # "Fake" agent ID
|
||||
b'SIGKEY': lambda _, args: self.set_key(*args),
|
||||
b'SETKEY': lambda _, args: self.set_key(*args),
|
||||
@@ -107,8 +102,46 @@ class Handler(object):
|
||||
b'HAVEKEY': lambda _, args: self.have_key(*args),
|
||||
b'KEYINFO': _key_info,
|
||||
b'SCD': self.handle_scd,
|
||||
b'GET_PASSPHRASE': self.handle_get_passphrase,
|
||||
}
|
||||
|
||||
def reset(self):
|
||||
"""Reset agent's state variables."""
|
||||
self.keygrip = None
|
||||
self.digest = None
|
||||
self.algo = None
|
||||
|
||||
def handle_option(self, opt):
|
||||
"""Store GPG agent-related options (e.g. for pinentry)."""
|
||||
self.options.append(opt)
|
||||
log.debug('options: %s', self.options)
|
||||
|
||||
def handle_get_passphrase(self, conn, _):
|
||||
"""Allow simple GPG symmetric encryption (using a passphrase)."""
|
||||
p1 = self.client.device.ui.get_passphrase('Symmetric encryption')
|
||||
p2 = self.client.device.ui.get_passphrase('Re-enter encryption')
|
||||
if p1 == p2:
|
||||
result = b'D ' + util.assuan_serialize(p1.encode('ascii'))
|
||||
keyring.sendline(conn, result, confidential=True)
|
||||
else:
|
||||
log.warning('Passphrase does not match!')
|
||||
|
||||
def handle_getinfo(self, conn, args):
|
||||
"""Handle some of the GETINFO messages."""
|
||||
result = None
|
||||
if args[0] == b'version':
|
||||
result = self.version
|
||||
elif args[0] == b's2k_count':
|
||||
# Use highest number of S2K iterations.
|
||||
# https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Options.html
|
||||
# https://tools.ietf.org/html/rfc4880#section-3.7.1.3
|
||||
result = '{}'.format(64 << 20).encode('ascii')
|
||||
else:
|
||||
log.warning('Unknown GETINFO command: %s', args)
|
||||
|
||||
if result:
|
||||
keyring.sendline(conn, b'D ' + result)
|
||||
|
||||
def handle_scd(self, conn, args):
|
||||
"""No support for smart-card device protocol."""
|
||||
reply = {
|
||||
@@ -118,7 +151,7 @@ class Handler(object):
|
||||
raise AgentError(b'ERR 100696144 No such device <SCD>')
|
||||
keyring.sendline(conn, b'D ' + reply)
|
||||
|
||||
@util.memoize
|
||||
@util.memoize_method # global cache for key grips
|
||||
def get_identity(self, keygrip):
|
||||
"""
|
||||
Returns device.interface.Identity that matches specified keygrip.
|
||||
@@ -165,7 +198,6 @@ class Handler(object):
|
||||
ec_point = self.client.ecdh(identity=identity, pubkey=remote_pubkey)
|
||||
keyring.sendline(conn, b'D ' + _serialize_point(ec_point))
|
||||
|
||||
@util.memoize
|
||||
def have_key(self, *keygrips):
|
||||
"""Check if any keygrip corresponds to a TREZOR-based key."""
|
||||
for keygrip in keygrips:
|
||||
|
||||
@@ -14,10 +14,18 @@ from .. import util
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_output(args, env=None, sp=subprocess):
|
||||
"""Call an external binary and return its stdout."""
|
||||
log.debug('calling %s with env %s', args, env)
|
||||
output = sp.check_output(args=args, env=env)
|
||||
log.debug('output: %r', output)
|
||||
return output
|
||||
|
||||
|
||||
def get_agent_sock_path(env=None, sp=subprocess):
|
||||
"""Parse gpgconf output to find out GPG agent UNIX socket path."""
|
||||
args = [util.which('gpgconf'), '--list-dirs']
|
||||
output = sp.check_output(args=args, env=env)
|
||||
output = check_output(args=args, env=env, sp=sp)
|
||||
lines = output.strip().split(b'\n')
|
||||
dirs = dict(line.split(b':', 1) for line in lines)
|
||||
log.debug('%s: %s', args, dirs)
|
||||
@@ -27,7 +35,8 @@ def get_agent_sock_path(env=None, sp=subprocess):
|
||||
def connect_to_agent(env=None, sp=subprocess):
|
||||
"""Connect to GPG agent's UNIX socket."""
|
||||
sock_path = get_agent_sock_path(sp=sp, env=env)
|
||||
sp.check_call(['gpg-connect-agent', '/bye']) # Make sure it's running
|
||||
# Make sure the original gpg-agent is running.
|
||||
check_output(args=['gpg-connect-agent', '/bye'], sp=sp)
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(sock_path)
|
||||
return sock
|
||||
@@ -39,9 +48,9 @@ def communicate(sock, msg):
|
||||
return recvline(sock)
|
||||
|
||||
|
||||
def sendline(sock, msg):
|
||||
def sendline(sock, msg, confidential=False):
|
||||
"""Send a binary message, followed by EOL."""
|
||||
log.debug('<- %r', msg)
|
||||
log.debug('<- %r', ('<snip>' if confidential else msg))
|
||||
sock.sendall(msg + b'\n')
|
||||
|
||||
|
||||
@@ -102,8 +111,8 @@ def parse(s):
|
||||
value, s = parse(s)
|
||||
values.append(value)
|
||||
return values, s[1:]
|
||||
else:
|
||||
return parse_term(s)
|
||||
|
||||
return parse_term(s)
|
||||
|
||||
|
||||
def _parse_ecdsa_sig(args):
|
||||
@@ -144,7 +153,7 @@ def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None):
|
||||
|
||||
assert communicate(sock, 'RESET').startswith(b'OK')
|
||||
|
||||
ttyname = sp.check_output(['tty']).strip()
|
||||
ttyname = check_output(args=['tty'], sp=sp).strip()
|
||||
options = ['ttyname={}'.format(ttyname)] # set TTY for passphrase entry
|
||||
|
||||
display = (environ or os.environ).get('DISPLAY')
|
||||
@@ -181,8 +190,9 @@ def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None):
|
||||
|
||||
def get_gnupg_components(sp=subprocess):
|
||||
"""Parse GnuPG components' paths."""
|
||||
output = sp.check_output([util.which('gpgconf'), '--list-components'])
|
||||
components = dict(re.findall('(.*):.*:(.*)', output.decode('ascii')))
|
||||
args = [util.which('gpgconf'), '--list-components']
|
||||
output = check_output(args=args, sp=sp)
|
||||
components = dict(re.findall('(.*):.*:(.*)', output.decode('utf-8')))
|
||||
log.debug('gpgconf --list-components: %s', components)
|
||||
return components
|
||||
|
||||
@@ -207,14 +217,14 @@ def gpg_command(args, env=None):
|
||||
def get_keygrip(user_id, sp=subprocess):
|
||||
"""Get a keygrip of the primary GPG key of the specified user."""
|
||||
args = gpg_command(['--list-keys', '--with-keygrip', user_id])
|
||||
output = sp.check_output(args).decode('ascii')
|
||||
output = check_output(args=args, sp=sp).decode('utf-8')
|
||||
return re.findall(r'Keygrip = (\w+)', output)[0]
|
||||
|
||||
|
||||
def gpg_version(sp=subprocess):
|
||||
"""Get a keygrip of the primary GPG key of the specified user."""
|
||||
args = gpg_command(['--version'])
|
||||
output = sp.check_output(args)
|
||||
output = check_output(args=args, sp=sp)
|
||||
line = output.split(b'\n')[0] # b'gpg (GnuPG) 2.1.11'
|
||||
return line.split(b' ')[-1] # b'2.1.11'
|
||||
|
||||
@@ -222,7 +232,7 @@ def gpg_version(sp=subprocess):
|
||||
def export_public_key(user_id, env=None, sp=subprocess):
|
||||
"""Export GPG public key for specified `user_id`."""
|
||||
args = gpg_command(['--export', user_id])
|
||||
result = sp.check_output(args=args, env=env)
|
||||
result = check_output(args=args, env=env, sp=sp)
|
||||
if not result:
|
||||
log.error('could not find public key %r in local GPG keyring', user_id)
|
||||
raise KeyError(user_id)
|
||||
@@ -232,7 +242,7 @@ def export_public_key(user_id, env=None, sp=subprocess):
|
||||
def export_public_keys(env=None, sp=subprocess):
|
||||
"""Export all GPG public keys."""
|
||||
args = gpg_command(['--export'])
|
||||
result = sp.check_output(args=args, env=env)
|
||||
result = check_output(args=args, env=env, sp=sp)
|
||||
if not result:
|
||||
raise KeyError('No GPG public keys found at env: {!r}'.format(env))
|
||||
return result
|
||||
|
||||
11
libagent/gpg/tests/test_agent.py
Normal file
11
libagent/gpg/tests/test_agent.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .. import agent
|
||||
|
||||
|
||||
def test_sig_encode():
|
||||
SIG = (
|
||||
b'(7:sig-val(5:ecdsa(1:r32:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x0c)(1:s32:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00")))')
|
||||
assert agent.sig_encode(12, 34) == SIG
|
||||
@@ -5,13 +5,15 @@ import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
import configargparse
|
||||
import pkg_resources
|
||||
import configargparse
|
||||
import daemon
|
||||
|
||||
from .. import device, formats, server, util
|
||||
from . import client, protocol
|
||||
@@ -21,9 +23,11 @@ log = logging.getLogger(__name__)
|
||||
UNIX_SOCKET_TIMEOUT = 0.1
|
||||
|
||||
|
||||
def ssh_args(label):
|
||||
def ssh_args(conn):
|
||||
"""Create SSH command for connecting specified server."""
|
||||
identity = device.interface.string_to_identity(label)
|
||||
I, = conn.identities
|
||||
identity = I.identity_dict
|
||||
pubkey_tempfile, = conn.public_keys_as_files()
|
||||
|
||||
args = []
|
||||
if 'port' in identity:
|
||||
@@ -31,12 +35,15 @@ def ssh_args(label):
|
||||
if 'user' in identity:
|
||||
args += ['-l', identity['user']]
|
||||
|
||||
args += ['-o', 'IdentityFile={}'.format(pubkey_tempfile.name)]
|
||||
args += ['-o', 'IdentitiesOnly=true']
|
||||
return args + [identity['host']]
|
||||
|
||||
|
||||
def mosh_args(label):
|
||||
def mosh_args(conn):
|
||||
"""Create SSH command for connecting specified server."""
|
||||
identity = device.interface.string_to_identity(label)
|
||||
I, = conn.identities
|
||||
identity = I.identity_dict
|
||||
|
||||
args = []
|
||||
if 'port' in identity:
|
||||
@@ -78,10 +85,26 @@ def create_agent_parser(device_type):
|
||||
help='timeout for accepting SSH client connections')
|
||||
p.add_argument('--debug', default=False, action='store_true',
|
||||
help='log SSH protocol messages for debugging.')
|
||||
p.add_argument('--log-file', type=str,
|
||||
help='Path to the log file (to be written by the agent).')
|
||||
p.add_argument('--sock-path', type=str,
|
||||
help='Path to the UNIX domain socket of the agent.')
|
||||
|
||||
p.add_argument('--pin-entry-binary', type=str, default='pinentry',
|
||||
help='Path to PIN entry UI helper.')
|
||||
p.add_argument('--passphrase-entry-binary', type=str, default='pinentry',
|
||||
help='Path to passphrase entry UI helper.')
|
||||
p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'),
|
||||
help='Expire passphrase from cache after this duration.')
|
||||
|
||||
g = p.add_mutually_exclusive_group()
|
||||
g.add_argument('-d', '--daemonize', default=False, action='store_true',
|
||||
help='Daemonize the agent and print its UNIX socket path')
|
||||
g.add_argument('-f', '--foreground', default=False, action='store_true',
|
||||
help='Run agent in foreground with specified UNIX socket path')
|
||||
g.add_argument('-s', '--shell', default=False, action='store_true',
|
||||
help='run ${SHELL} as subprocess under SSH agent')
|
||||
help=('run ${SHELL} as subprocess under SSH agent, allowing '
|
||||
'regular SSH-based tools to be used in the shell'))
|
||||
g.add_argument('-c', '--connect', default=False, action='store_true',
|
||||
help='connect to specified host via SSH')
|
||||
g.add_argument('--mosh', default=False, action='store_true',
|
||||
@@ -95,7 +118,7 @@ def create_agent_parser(device_type):
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def serve(handler, sock_path=None, timeout=UNIX_SOCKET_TIMEOUT):
|
||||
def serve(handler, sock_path, timeout=UNIX_SOCKET_TIMEOUT):
|
||||
"""
|
||||
Start the ssh-agent server on a UNIX-domain socket.
|
||||
|
||||
@@ -105,9 +128,6 @@ def serve(handler, sock_path=None, timeout=UNIX_SOCKET_TIMEOUT):
|
||||
ssh_version = subprocess.check_output(['ssh', '-V'],
|
||||
stderr=subprocess.STDOUT)
|
||||
log.debug('local SSH version: %r', ssh_version)
|
||||
if sock_path is None:
|
||||
sock_path = tempfile.mktemp(prefix='trezor-ssh-agent-')
|
||||
|
||||
environ = {'SSH_AUTH_SOCK': sock_path, 'SSH_AGENT_PID': str(os.getpid())}
|
||||
device_mutex = threading.Lock()
|
||||
with server.unix_domain_socket_server(sock_path) as sock:
|
||||
@@ -127,14 +147,20 @@ def serve(handler, sock_path=None, timeout=UNIX_SOCKET_TIMEOUT):
|
||||
quit_event.set()
|
||||
|
||||
|
||||
def run_server(conn, command, debug, timeout):
|
||||
def run_server(conn, command, sock_path, debug, timeout):
|
||||
"""Common code for run_agent and run_git below."""
|
||||
ret = 0
|
||||
try:
|
||||
handler = protocol.Handler(conn=conn, debug=debug)
|
||||
with serve(handler=handler, timeout=timeout) as env:
|
||||
return server.run_process(command=command, environ=env)
|
||||
with serve(handler=handler, sock_path=sock_path,
|
||||
timeout=timeout) as env:
|
||||
if command:
|
||||
ret = server.run_process(command=command, environ=env)
|
||||
else:
|
||||
signal.pause() # wait for signal (e.g. SIGINT)
|
||||
except KeyboardInterrupt:
|
||||
log.info('server stopped')
|
||||
return ret
|
||||
|
||||
|
||||
def handle_connection_error(func):
|
||||
@@ -172,6 +198,7 @@ class JustInTimeConnection(object):
|
||||
self.conn_factory = conn_factory
|
||||
self.identities = identities
|
||||
self.public_keys_cache = public_keys
|
||||
self.public_keys_tempfiles = []
|
||||
|
||||
def public_keys(self):
|
||||
"""Return a list of SSH public keys (in textual format)."""
|
||||
@@ -188,19 +215,47 @@ class JustInTimeConnection(object):
|
||||
pk['identity'] = identity
|
||||
return public_keys
|
||||
|
||||
def public_keys_as_files(self):
|
||||
"""Store public keys as temporary SSH identity files."""
|
||||
if not self.public_keys_tempfiles:
|
||||
for pk in self.public_keys():
|
||||
f = tempfile.NamedTemporaryFile(prefix='trezor-ssh-pubkey-', mode='w')
|
||||
f.write(pk)
|
||||
f.flush()
|
||||
self.public_keys_tempfiles.append(f)
|
||||
|
||||
return self.public_keys_tempfiles
|
||||
|
||||
def sign(self, blob, identity):
|
||||
"""Sign a given blob using the specified identity on the device."""
|
||||
conn = self.conn_factory()
|
||||
return conn.sign_ssh_challenge(blob=blob, identity=identity)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _dummy_context():
|
||||
yield
|
||||
|
||||
|
||||
def _get_sock_path(args):
|
||||
sock_path = args.sock_path
|
||||
if not sock_path:
|
||||
if args.foreground:
|
||||
log.error('running in foreground mode requires specifying UNIX socket path')
|
||||
sys.exit(1)
|
||||
else:
|
||||
sock_path = tempfile.mktemp(prefix='trezor-ssh-agent-')
|
||||
return sock_path
|
||||
|
||||
|
||||
@handle_connection_error
|
||||
def main(device_type):
|
||||
"""Run ssh-agent using given hardware client factory."""
|
||||
args = create_agent_parser(device_type=device_type).parse_args()
|
||||
util.setup_logging(verbosity=args.verbose)
|
||||
util.setup_logging(verbosity=args.verbose, filename=args.log_file)
|
||||
|
||||
public_keys = None
|
||||
filename = None
|
||||
if args.identity.startswith('/'):
|
||||
filename = args.identity
|
||||
contents = open(filename, 'rb').read().decode('utf-8')
|
||||
@@ -215,24 +270,40 @@ def main(device_type):
|
||||
identity.identity_dict['proto'] = u'ssh'
|
||||
log.info('identity #%d: %s', index, identity.to_string())
|
||||
|
||||
# override default PIN/passphrase entry tools (relevant for TREZOR/Keepkey):
|
||||
device_type.ui = device.ui.UI(device_type=device_type, config=vars(args))
|
||||
device_type.cached_passphrase_ack = util.ExpiringCache(
|
||||
args.cache_expiry_seconds)
|
||||
|
||||
conn = JustInTimeConnection(
|
||||
conn_factory=lambda: client.Client(device_type()),
|
||||
identities=identities, public_keys=public_keys)
|
||||
|
||||
sock_path = _get_sock_path(args)
|
||||
command = args.command
|
||||
context = _dummy_context()
|
||||
if args.connect:
|
||||
command = ['ssh'] + ssh_args(args.identity) + args.command
|
||||
command = ['ssh'] + ssh_args(conn) + args.command
|
||||
elif args.mosh:
|
||||
command = ['mosh'] + mosh_args(args.identity) + args.command
|
||||
else:
|
||||
command = args.command
|
||||
command = ['mosh'] + mosh_args(conn) + args.command
|
||||
elif args.daemonize:
|
||||
out = 'SSH_AUTH_SOCK={0}; export SSH_AUTH_SOCK;\n'.format(sock_path)
|
||||
sys.stdout.write(out)
|
||||
sys.stdout.flush()
|
||||
context = daemon.DaemonContext()
|
||||
log.info('running the agent as a daemon on %s', sock_path)
|
||||
elif args.foreground:
|
||||
log.info('running the agent on %s', sock_path)
|
||||
|
||||
use_shell = bool(args.shell)
|
||||
if use_shell:
|
||||
command = os.environ['SHELL']
|
||||
sys.stdin.close()
|
||||
|
||||
conn = JustInTimeConnection(
|
||||
conn_factory=lambda: client.Client(device_type()),
|
||||
identities=identities, public_keys=public_keys)
|
||||
if command:
|
||||
return run_server(conn=conn, command=command, debug=args.debug,
|
||||
timeout=args.timeout)
|
||||
if command or args.daemonize or args.foreground:
|
||||
with context:
|
||||
return run_server(conn=conn, command=command, sock_path=sock_path,
|
||||
debug=args.debug, timeout=args.timeout)
|
||||
else:
|
||||
for pk in conn.public_keys():
|
||||
sys.stdout.write(pk)
|
||||
|
||||
@@ -115,3 +115,32 @@ def test_memoize():
|
||||
assert g(1) == g(1)
|
||||
assert g(1) != g(2)
|
||||
assert f.mock_calls == [mock.call(1), mock.call(2)]
|
||||
|
||||
|
||||
def test_assuan_serialize():
|
||||
assert util.assuan_serialize(b'') == b''
|
||||
assert util.assuan_serialize(b'123\n456') == b'123%0A456'
|
||||
assert util.assuan_serialize(b'\r\n') == b'%0D%0A'
|
||||
|
||||
|
||||
def test_cache():
|
||||
timer = mock.Mock(side_effect=range(7))
|
||||
c = util.ExpiringCache(seconds=2, timer=timer) # t=0
|
||||
assert c.get() is None # t=1
|
||||
obj = 'foo'
|
||||
c.set(obj) # t=2
|
||||
assert c.get() is obj # t=3
|
||||
assert c.get() is obj # t=4
|
||||
assert c.get() is None # t=5
|
||||
assert c.get() is None # t=6
|
||||
|
||||
|
||||
def test_cache_inf():
|
||||
timer = mock.Mock(side_effect=range(6))
|
||||
c = util.ExpiringCache(seconds=float('inf'), timer=timer)
|
||||
obj = 'foo'
|
||||
c.set(obj)
|
||||
assert c.get() is obj
|
||||
assert c.get() is obj
|
||||
assert c.get() is obj
|
||||
assert c.get() is obj
|
||||
|
||||
@@ -5,6 +5,7 @@ import functools
|
||||
import io
|
||||
import logging
|
||||
import struct
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -215,6 +216,24 @@ def memoize(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
def memoize_method(method):
|
||||
"""Simple caching decorator."""
|
||||
cache = {}
|
||||
|
||||
@functools.wraps(method)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
"""Caching wrapper."""
|
||||
key = (args, tuple(sorted(kwargs.items())))
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
else:
|
||||
result = method(self, *args, **kwargs)
|
||||
cache[key] = result
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@memoize
|
||||
def which(cmd):
|
||||
"""Return full path to specified command, or raise OSError if missing."""
|
||||
@@ -229,3 +248,33 @@ def which(cmd):
|
||||
raise OSError('Cannot find {!r} in $PATH'.format(cmd))
|
||||
log.debug('which %r => %r', cmd, full_path)
|
||||
return full_path
|
||||
|
||||
|
||||
def assuan_serialize(data):
|
||||
"""Serialize data according to ASSUAN protocol (for GPG daemon communication)."""
|
||||
for c in [b'%', b'\n', b'\r']:
|
||||
escaped = '%{:02X}'.format(ord(c)).encode('ascii')
|
||||
data = data.replace(c, escaped)
|
||||
return data
|
||||
|
||||
|
||||
class ExpiringCache(object):
|
||||
"""Simple cache with a deadline."""
|
||||
|
||||
def __init__(self, seconds, timer=time.time):
|
||||
"""C-tor."""
|
||||
self.duration = seconds
|
||||
self.timer = timer
|
||||
self.value = None
|
||||
self.set(None)
|
||||
|
||||
def get(self):
|
||||
"""Returns existing value, or None if deadline has expired."""
|
||||
if self.timer() > self.deadline:
|
||||
self.value = None
|
||||
return self.value
|
||||
|
||||
def set(self, value):
|
||||
"""Set new value and reset the deadline for expiration."""
|
||||
self.deadline = self.timer() + self.duration
|
||||
self.value = value
|
||||
|
||||
6
release.sh
Executable file
6
release.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
rm -rv dist/*
|
||||
python3 setup.py sdist
|
||||
gpg2 -v --detach-sign -a dist/*.tar.gz
|
||||
twine upload dist/*
|
||||
4
setup.py
4
setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='libagent',
|
||||
version='0.9.7',
|
||||
version='0.11.3',
|
||||
description='Using hardware wallets as SSH/GPG agent',
|
||||
author='Roman Zeyde',
|
||||
author_email='roman.zeyde@gmail.com',
|
||||
@@ -17,8 +17,10 @@ setup(
|
||||
install_requires=[
|
||||
'backports.shutil_which>=3.5.1',
|
||||
'ConfigArgParse>=0.12.0',
|
||||
'python-daemon>=2.1.2',
|
||||
'ecdsa>=0.13',
|
||||
'ed25519>=1.4',
|
||||
'mnemonic>=0.18',
|
||||
'pymsgbox>=1.0.6',
|
||||
'semver>=2.2',
|
||||
'unidecode>=0.4.20',
|
||||
|
||||
Reference in New Issue
Block a user