Compare commits

...

145 Commits

Author SHA1 Message Date
Roman Zeyde 71f357c1bf
Add 'hidapi' dependency 6 years ago
Eli Boyarski 8f1d008eb2
fixed typo + missing word 6 years ago
Roman Zeyde 7a351acf15
Merge remote-tracking branch 'matejcik/master' 6 years ago
Roman Zeyde 7f9aa2b147
Bump version: 0.11.3 → 0.12.0 6 years ago
Roman Zeyde eed168341c
Don't inheric from 'object' (after deprecating Python 2.x support) 6 years ago
matejcik 8b85090fba trezor: usage for TREZOR_PATH variable
This is not a great place, as the variable will work anywhere,
but I couldn't find a better place to put it.

Also fixes a typo in the service definition.
6 years ago
matejcik 8708b1e16d trezor: use TREZOR_PATH environment variable to specify device path 6 years ago
Roman Zeyde 03e7fc48e9
Improve Git-related documentation 6 years ago
Roman Zeyde 4968ca7ff3
Merge branch 'master' into neopg-wip 6 years ago
Roman Zeyde 6b6d9f5d20
Add a link to neopg-trezor wrapper at documentation 6 years ago
Roman Zeyde c22109df24
Document argv[0] hack for NeoPG 6 years ago
Roman Zeyde 47ce035e79
Remove unused import 6 years ago
Roman Zeyde 36cbba6c57
Fix a few lint issues 6 years ago
Roman Zeyde 6afe20350b
Simplify GPG command generation 6 years ago
Roman Zeyde fa171e8923
Add short example for NeoPG usage 6 years ago
Roman Zeyde f0bda9a3e6
Allow using $PATH when looking for GPG binary
It's needed for running neopg (instead of gnupg).
6 years ago
Roman Zeyde 71b56e15d7
Add NeoPG commandline wrapper for TREZOR-based agent
It invokes `trezor-gpg-agent` instead of `neopg agent`, by putting
its own path at argv[0].
6 years ago
Roman Zeyde 3b9c00e02a
Default to $GNUPGHOME when not specified on commandline 6 years ago
Roman Zeyde dcee59a19e
Assume NeoPG binary runs GnuPG functionality 6 years ago
Roman Zeyde a274de30b8
Parse NeoPG development versions
e.g. v0.0.5-37-g1fe5046-dirty
6 years ago
Roman Zeyde 4fe9e437ad
Simplify GPG homedir setting 6 years ago
Roman Zeyde d04527a8ed
Replace GPG version assertion by an error log
since NeoPG uses different versioning
6 years ago
Roman Zeyde 3329c29cb4
Use gpg_command() for identity generation 6 years ago
Roman Zeyde df2cb52f8d
fixup! Reply with an ERR to `SCD SERIALNO openpgp` ASSUAN command 6 years ago
Roman Zeyde f36ef4ffe0
Allow running NeoPG binary (instead of GnuPG) 6 years ago
Roman Zeyde f74de828fc
Reply with an ERR to `SCD SERIALNO openpgp` ASSUAN command
(for NeoPG)
6 years ago
Roman Zeyde 912b1cde7a
Add support for file-descriptor-based socket server
(for NeoPG)
6 years ago
Roman Zeyde b7a8c42893
Merge pull request #153 from romanz/drop-py2
setup: deprecate Python2 support
6 years ago
Roman Zeyde 1e6c4e6930
Add links to SSH/GPG usage examples 6 years ago
Roman Zeyde a8f19e4150
Comment about SSH argument separation 6 years ago
Roman Zeyde 6a9fdf75e2
Bump version: 0.11.2 → 0.11.3 6 years ago
Roman Zeyde 6bc5b6af5e
Add small example for IdentityOnly use-case 6 years ago
Roman Zeyde 8672a6901a
Document IdentitiesOnly support 6 years ago
Roman Zeyde 672af98ad7
Explicitly use IdentityFile option when connecting to specific host 6 years ago
Roman Zeyde ed531cfff8
Remove trailing whitespace
git ls-files | xargs -n1 sed -e's/[[:space:]]*$//' -i
6 years ago
Bram bd1ae0f091
Update INSTALL.md
I've sorted out the Formula for Homebrew and it's been merged.
6 years ago
Roman Zeyde 0c762e8998
Use `pinentry` homebrew formula on macOS 6 years ago
Roman Zeyde bd0df4f801
trezor: update setup.py for latest libagent and trezorlib 6 years ago
Roman Zeyde 3d1639d271
gpg: require symmetric passphrase re-entry 6 years ago
Roman Zeyde bea899d1ef
gpg: allow symmetric encryption with a passphrase 6 years ago
Roman Zeyde ccc2174775
gpg: allow more verbose output during GnuPG pubkey import 6 years ago
Roman Zeyde afa3fdb89c
gpg: allow setting passphrase cache expriration duration 6 years ago
Roman Zeyde 2ca3941cfa
ssh: allow setting passphrase cache expriration duration 6 years ago
Roman Zeyde b1bd6cb690
gpg: refactor GETINFO handling into a separate method 6 years ago
Roman Zeyde 766536d2c4
trezor: allow expiring cached passphrase 6 years ago
Roman Zeyde 91f70e7a96
Merge pull request #238 from pruflyos/patch-1
Update INSTALL.md
6 years ago
Roman Zeyde cf5bfd960a
Merge pull request #237 from menteb/patch-2
Update to Install.md reflecting Homebrew formula
6 years ago
pruflyos 4bd769f138
Update INSTALL.md
On Fedora `python3-tk` is called `python3-tkinter`
6 years ago
Bram 91b850f184
Update to Install.md reflecting Homebrew formula 6 years ago
Roman Zeyde c6bb090dfc
Merge pull request #235 from timthelion/git-email-readme
Document the configuration of the git email setting and errors
6 years ago
Timothy Hobbs fef4fd06c9 Document the configuration of the git email setting and errors
Signed-off-by: Timothy Hobbs <timothyhobbs@seznam.cz>
6 years ago
Roman Zeyde bc691ae795
gpg: fix method's caching 6 years ago
Roman Zeyde 61e516e200
Add link to Ledger Nano S guide 6 years ago
Roman Zeyde 543ff7021d
doc: explain how to reset cached passphrase 6 years ago
Roman Zeyde 2e0cfc8088
gpg: fail if new identity is missing 6 years ago
Roman Zeyde 18f33f8a08
README: document PIN entry depedencies 6 years ago
Roman Zeyde 2973413995
Merge pull request #227 from kvbik/patch-1
mention brew install libusb on macOS
6 years ago
Jakub Vysoký 2360693dc5
mention brew install libusb on macOS 6 years ago
Roman Zeyde 7443fc6512
Pass 'state' during TREZOR initialization 6 years ago
Roman Zeyde 5efb752979
doc: update Fedora installation instructions 6 years ago
Roman Zeyde 4546cd674b
Bump version: 0.11.1 → 0.11.2 6 years ago
Roman Zeyde 5dba12f144
gpg: don't clear options on RESET assuan command 6 years ago
Roman Zeyde 887561de9f
pylint: skip 'fixme' warnings 6 years ago
Roman Zeyde 6d730e0a5b
ui: subprocess.Popen doesn't have 'args' attribute in Python 2 6 years ago
Roman Zeyde d0732d16e8
ui: don't log passphrases (since the log may be persisted) 6 years ago
Roman Zeyde dafb80ad7a
trezor: don't retry on PIN/passphrase entry cancellation 6 years ago
Roman Zeyde df6249b071
Merge remote-tracking branch 'rendaw/pinentry-docs' 6 years ago
rendaw 942f01418b Also set DISPLAY in SSH unit 6 years ago
rendaw 93b548b737 Add docs to show using the gpg agent with systemd; set PATH for ssh unit 6 years ago
rendaw 329f07249a Small reword 6 years ago
rendaw a1f7088d33 Remove pin entry instructions from INSTALL, didn't seem that relevant 6 years ago
rendaw 25f066e113 Document --pin-entry-binary with usage guide 6 years ago
Roman Zeyde 0699273d49
util: move ASSUAN serialization to break circular import 6 years ago
Roman Zeyde 92c352e860
Bump version: 0.11.0 → 0.11.1 6 years ago
Roman Zeyde 34c03a462c
ui: merge into a single module 6 years ago
Roman Zeyde 51dbecd4c2
Bump version: 0.10.0 → 0.11.0 6 years ago
Roman Zeyde ceae65aa5a
ui: use {} as default config 6 years ago
Roman Zeyde d0497b0137
pinentry: specify device name at PIN/passphrase entry UI 6 years ago
Roman Zeyde 870152a7af
gpg: allow specifying custom homedir during init 6 years ago
Roman Zeyde cbdc52c0a4
trezor: handle passphrase on-device entry (for Model T) 6 years ago
Roman Zeyde 0c9fc33757
gpg: replace gpg-agent.conf by run-agent.sh 6 years ago
Roman Zeyde 17ea941add
gpg: use pinentry UI for initialization and agent 6 years ago
Roman Zeyde 64064b5ecc
ssh: use pinentry UI 6 years ago
Roman Zeyde 601a2b1336
device: refactor PIN/passphrase UI into a separate class
This would allow easier customization.
6 years ago
Roman Zeyde 2e688ccac9
setup: deprecate Python2 support 6 years ago
Roman Zeyde b6181bb5b5
trezor: replace tk-based pinentry with GnuPG pinentry 6 years ago
Roman Zeyde b6da299cb0
pinentry: add simple wrapper for PIN/passphrase entry 6 years ago
Roman Zeyde 04627f0899
gpg: collect OPTIONs from agent 6 years ago
Roman Zeyde 54ce6f2cec
trezor: limit passphrase length 6 years ago
Roman Zeyde a1047ba7b1
Bump version: 0.9.8 → 0.10.0 6 years ago
Roman Zeyde e90bd0cd81
trezor: refactor transport enumeration a bit 6 years ago
slush 66e3e60370 trezor: Use composite transport for device detection. 6 years ago
slush 3f1604d609 Use Python3 by default 6 years ago
slush d0f4cccfd2 trezor: Both Trezor One and Model T are supported. 6 years ago
Roman Zeyde 08d81c992c
trezor: split pinentry tool into a separate file 6 years ago
Roman Zeyde 55a899f929
trezor: initialize cached_passphrase_ack with None (instead of 0) 6 years ago
Roman Zeyde e7604dff68
ssh: fix small commandline documentation nits 6 years ago
rendaw 8849545700 Clarify a couple sentences 6 years ago
rendaw d109cd73b5 Adjust ssh systemd instructions; cleanup 6 years ago
rendaw 95e98d6eda Merge remote-tracking branch 'upstream/master' into doc-enhancements2 6 years ago
rendaw 9e78d52721 SSH clarification 6 years ago
Roman Zeyde 2a76ef6819
gpg: notice encryption of gpg-agent logs (for privacy) 6 years ago
Roman Zeyde 654a3c465a
Merge remote-tracking branch 'rendaw/systemd-instructions' 6 years ago
Roman Zeyde 2168115b06
ssh: fixup small refactoring bug 6 years ago
Roman Zeyde 4a9140c42d
Merge branch 'serge' 6 years ago
Roman Zeyde b20d98bf57
gpg: move socket path generation into a helper function 6 years ago
Roman Zeyde 199fb299c3
gpg: use 'sys.exit' and log homedir 6 years ago
rendaw 06e169f141 Add instructions for using SSH agent as systemd unit 6 years ago
rendaw 131111bc0e Add additional information to docs; collect and reorganize sections 6 years ago
Roman Zeyde f4208009e0
trezor: init_device before failing PIN/passphrase entry 6 years ago
Roman Zeyde 73d60dbec0
gpg: cache the passphrase for better UX 6 years ago
Roman Zeyde 34ea224290
gpg: the scripts should be only user-readable 6 years ago
Roman Zeyde 7803026f61
gpg: allow setting passphrase from environment variable
as done by TREZOR's client library
6 years ago
Roman Zeyde 34ce1005fd
build: add simple script for PyPI release 6 years ago
Roman Zeyde 8677c8ebaa
trezor_agent: fix broken PyPI package 6 years ago
Serge Pokhodyaev 6363eb0d4a add -f/--foreground option to run as systemd service 6 years ago
Serge Pokhodyaev a32bfc749b don't overwrite homedir 6 years ago
Roman Zeyde 75d117ad0d
Bump version: 0.9.7 → 0.9.8 6 years ago
Roman Zeyde cefc5f180a
ssh: add --sock-path flag to explicitly specify SSH agent's UNIX socket 6 years ago
Roman Zeyde 0f5c71b748
ssh: add --log-file flag 6 years ago
Roman Zeyde d5f97b7efa
Update README title 6 years ago
Roman Zeyde 4a12bfa0b7
Allow SSH agent to daemonize when invoked with `-d` flag
This change adds the support for "eval `trezor-agent -d`" invocation.
6 years ago
Roman Zeyde cac889ff7d
Update trezorlib dependency for trezor_agent 6 years ago
Roman Zeyde 92c6e680ed
doc: add python-tk dependency
Following #194
6 years ago
Roman Zeyde bf294beb56
gpg: decode stdout as UTF-8 6 years ago
Roman Zeyde 713345918e
ssh: document sub-shell mode 6 years ago
Roman Zeyde eb60c2f475
fix more pylint issues 6 years ago
Roman Zeyde 6d8d43db9b
fix pylint issues 6 years ago
Roman Zeyde 3e67bc9f0e
gpg: log GnuPG commands' output 6 years ago
Roman Zeyde 38b50485de
ssh: remove old demo from README 6 years ago
Roman Zeyde 9cba27b31a
Merge pull request #188 from eli-b/patch-2
README-SSH.md: spelling
6 years ago
Eli Boyarski 00a65a9820
README-SSH.md: spelling 6 years ago
Roman Zeyde 52ad601e66
Merge pull request #187 from eli-b/patch-1
INSTALL.md: update the Ledger Nano S udev link
6 years ago
Eli Boyarski d96a2820ff
INSTALL.md: update the Ledger Nano S udev link 6 years ago
Roman Zeyde 29aaf777ad
Bump version: 0.9.6 → 0.9.7 6 years ago
Roman Zeyde 385fc9457b
Support multiple devices 6 years ago
Jonathan Roelofs 9cf73f677a Show libagent version too 6 years ago
Jonathan Roelofs ec97cd0c44 Implement #182 6 years ago
Jonathan Roelofs 4cd7dc02eb Fix an argparse nuance
https://bugs.python.org/issue16308
6 years ago
Roman Zeyde 8fe9460ed6
trezor: allow UDP connection (for emulator) 6 years ago
Roman Zeyde db16aa3d1c
trezor: update to latest trezorlib version 6 years ago
Roman Zeyde 41ccd2f332
fix new pylint warning 6 years ago
Roman Zeyde cb14d1e00b
Bump version: 0.9.5 → 0.9.6 7 years ago
Roman Zeyde cc6ee31deb
add .bumpversion.cfg 7 years ago
Roman Zeyde b1f302151b
tests: fix test_server.py 7 years ago

@ -0,0 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 0.12.0
[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

@ -1,8 +1,6 @@
sudo: false
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"

@ -1,25 +1,27 @@
# Using TREZOR as a hardware SSH/GPG agent
# Hardware-based SSH/GPG agent
[![Build Status](https://travis-ci.org/romanz/trezor-agent.svg?branch=master)](https://travis-ci.org/romanz/trezor-agent)
[![Chat](https://badges.gitter.im/romanz/trezor-agent.svg)](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.6GPG Signing, SSH Login Updates and Advanced Transaction Features for Segwit](https://medium.com/@satoshilabs/trezor-firmware-1-3-6-20a7df6e692)
- [TREZOR Firmware 1.4.0GPG 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
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.
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.
## Usage
## Documentation
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).
* **Installation** instructions are [here](doc/INSTALL.md)
* **SSH** instructions and common use cases are [here](doc/README-SSH.md)
For GPG, see the [following instructions](doc/README-GPG.md).
Note: If you're using Windows, see [trezor-ssh-agent](https://github.com/martin-lizner/trezor-ssh-agent) by Martin Lízner.
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=[

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import os
import sys
agent = 'trezor-gpg-agent'
binary = 'neopg'
if sys.argv[1:2] == ['agent']:
os.execvp(agent, [agent, '-vv'] + sys.argv[2:])
else:
# HACK: pass this script's path as argv[0], so it will be invoked again
# when NeoPG tries to run its own agent:
# https://github.com/das-labor/neopg/blob/1fe50460abe01febb118641e37aa50bc429a1786/src/neopg.cpp#L114
# https://github.com/das-labor/neopg/blob/1fe50460abe01febb118641e37aa50bc429a1786/legacy/gnupg/common/asshelp.cpp#L217
os.execvp(binary, [__file__, 'gpg2'] + sys.argv[1:])

@ -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.
@ -36,16 +36,16 @@ The `trezor-agent` then instructs SSH to connect to the server. It will then eng
### GPG
GPG uses much the same approach as SSH, expect in this it relies on [SLIP-0017 : ECDH using deterministic hierarchy][3] for the mapping to an ECDH key and it maps these to the normal GPG child key infrastructure.
GPG uses much the same approach as SSH, except in this case it relies on [SLIP-0017 : ECDH using deterministic hierarchy][3] for the mapping to an ECDH key and it maps these to the normal GPG child key infrastructure.
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

@ -1,82 +1,135 @@
# Installation
## 1. Prerequisites
Install the following packages (depending on your distribution):
## Install dependencies
### OS dependencies
This software needs Python, libusb, and libudev along with development files.
You can install them on these distributions as follows:
### Debian
##### Debian
$ apt update && apt upgrade
$ apt install python-pip python-dev libusb-1.0-0-dev libudev-dev
$ apt-get install python3-pip python3-dev python3-tk libusb-1.0-0-dev libudev-dev
### Fedora/RedHat
##### RedHat
$ yum update
$ yum install python-pip python-devel libusb-devel libudev-devel \
$ yum install python3-pip python3-devel python3-tk libusb-devel libudev-devel \
gcc redhat-rpm-config
### OpenSUSE
##### Fedora
$ dnf install python3-pip python3-devel python3-tkinter libusb-devel libudev-devel \
gcc redhat-rpm-config
$ zypper install python-pip python-devel libusb-1_0-devel libudev-devel
##### 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
##### macOS
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/)
$ brew install libusb
## Update setuptools and pip
### GPG
Also, update Python packages before starting the installation:
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.
$ pip install -U setuptools pip
You can verify your installed version by running:
```
$ gpg2 --version | head -n1
gpg (GnuPG) 2.1.15
```
## Check device's firmware version
* 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 you are running the latest firmware version on your hardware device.
Currently the following firmware versions are supported:
# 2. Install the TREZOR agent
* [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)
1. Make sure you are running the latest firmware version on your Trezor:
## TREZOR
* [TREZOR firmware releases](https://wallet.trezor.io/data/firmware/releases.json): `1.4.2+`
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. Make sure that your `udev` rules are configured [correctly](https://doc.satoshilabs.com/trezor-user/settingupchromeonlinux.html#manual-configuration-of-udev-rules).
$ pip install trezor_agent
3. Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agent) package:
Or, directly from the latest source code:
```
$ pip3 install Cython hidapi
$ 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
```
Or, through Homebrew on macOS:
```
$ brew install trezor-agent
```
# 3. Install the KeepKey agent
1. Make sure you are running the latest firmware version on your KeepKey:
## KeepKey
* [KeepKey firmware releases](https://github.com/keepkey/keepkey-firmware/releases): `3.0.17+`
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).
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
```
# 4. Install the Ledger Nano S agent
1. Make sure you are running the latest firmware version on your Ledger Nano S:
## Ledger Nano S
* [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)
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:
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:
$ pip install ledger_agent
```
$ pip3 install ledger_agent
```
Or, directly from the latest source code:
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
```
[![asciicast](https://asciinema.org/a/3iNw2L9QWB8R3EVdYdAxMOLK8.png)](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
[![asciicast](https://asciinema.org/a/3iNw2L9QWB8R3EVdYdAxMOLK8.png)](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).
## Sample usage (signature and decryption)
[![asciicast](https://asciinema.org/a/120441.png)](https://asciinema.org/a/120441)
If you wish to switch back to your software keys unset `GNUPGHOME`.
In order to use specific device type for GPG operations, set the following environment variable to either:
```
$ export GNUPGHOME=~/.gnupg/{trezor,ledger}
```
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
You can use GNU Privacy Assistant (GPA) in order to inspect the created keys
and perform signature and decryption operations using:
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
[![asciicast](https://asciinema.org/a/120441.png)](https://asciinema.org/a/120441)
### 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
```
[![GPA](https://cloud.githubusercontent.com/assets/9900/20224804/053d7474-a849-11e6-87f3-ab07dc536158.png)](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:
````
$ 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.
First install `pass` from [passwordstore.org](https://www.passwordstore.org/) and initialize it to use your TREZOR-based GPG identity:
### 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
[![asciicast](https://asciinema.org/a/5tIQa5qt5bV134oeOqFyKEU29.png)](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
```

@ -0,0 +1,31 @@
# NeoPG experimental support
1. Download build and install NeoPG from [source code](https://github.com/das-labor/neopg#installation).
2. Generate Ed25519-based identity (using a [special wrapper](https://github.com/romanz/trezor-agent/blob/c22109df24c6eb8263aa40183a016be3437b1a0c/contrib/neopg-trezor) to invoke TREZOR-based agent):
```bash
$ export NEOPG_BINARY=$PWD/contrib/neopg-trezor
$ $NEOPG_BINARY --help
$ export GNUPGHOME=/tmp/homedir
$ trezor-gpg init "FooBar" -e ed25519
sec ed25519 2018-07-01 [SC]
802AF7E2DCF4491FFBB2F032341E95EF57CD7D5E
uid [ultimate] FooBar
ssb cv25519 2018-07-01 [E]
```
3. Sign and verify signatures:
```
$ $NEOPG_BINARY -v --detach-sign FILE
neopg: starting agent '/home/roman/Code/trezor/trezor-agent/contrib/neopg-trezor'
neopg: using pgp trust model
neopg: writing to 'FILE.sig'
neopg: EDDSA/SHA256 signature from: "341E95EF57CD7D5E FooBar"
$ $NEOPG_BINARY --verify FILE.sig FILE
neopg: Signature made Sun Jul 1 11:52:51 2018 IDT
neopg: using EDDSA key 802AF7E2DCF4491FFBB2F032341E95EF57CD7D5E
neopg: Good signature from "FooBar" [ultimate]
```

@ -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,75 @@
# 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.
Note the `--` separator, which is used to separate `trezor-agent`'s arguments from the SSH command arguments.
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
[![Demo](https://asciinema.org/a/22959.png)](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.
[![Subshell](https://asciinema.org/a/33240.png)](https://asciinema.org/a/33240)
## Using for GitHub SSH authentication (via `trezor-git` utility)
[![GitHub](https://asciinema.org/a/38337.png)](https://asciinema.org/a/38337)
### Load different SSH identities from configuration file
## Loading multiple SSH identities from configuration file
[![Config](https://asciinema.org/a/bdxxtgctk5syu56yfz8lcp7ny.png)](https://asciinema.org/a/bdxxtgctk5syu56yfz8lcp7ny)
# Public key generation
### Implement passwordless login
Run:
@ -26,52 +83,102 @@ 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:
Export 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 > ~/.ssh/github.pub
/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
Add the following configuration to your `~/.ssh/config` file:
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.
Host github.com
IdentityFile ~/.ssh/github.pub
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
~ $
Use the following Bash alias for convenient Git operations:
Make sure to confirm SSH signature on the Trezor device when requested.
$ alias ssh-shell='trezor-agent ~/.ssh/github.pub -v --shell'
## Accessing remote Git/Mercurial repositories
Now, you can use regular Git commands under the "SSH-enabled" sub-shell:
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/)):
$ ssh-shell
$ git push origin master
$ trezor-agent -v -e ed25519 git@github.com | xclip
The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html)):
Use the following Bash alias for convinient Git operations:
$ ssh-shell
$ hg push
$ alias git_hub='trezor-agent -v -e ed25519 git@github.com -- git'
### Start the agent as a systemd unit
Replace `git` with `git_hub` for remote operations:
##### 1. Create these files in `~/.config/systemd/user`
$ git_hub push origin master
Replace `trezor` with `keepkey` or `ledger` as required.
The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html)):
###### `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.
If you have multiple Trezors connected, you can select which one to use via a `TREZOR_PATH`
environment variable. Use `trezorctl list` to find the correct path. Then add it
to the agent with the following line:
````
Environment="TREZOR_PATH=<your path here>"
````
Note that USB paths depend on the _USB port_ which you use.
###### `trezor-ssh-agent.socket`
````
[Unit]
Description=trezor-agent SSH agent socket
$ trezor-agent -v -e ed25519 git@bitbucket.org -- hg push
[Socket]
ListenStream=%t/trezor-agent/S.ssh
FileDescriptorName=ssh
Service=trezor-ssh-agent.service
SocketMode=0600
DirectoryMode=0700
[Install]
WantedBy=sockets.target
````
# Troubleshooting
##### 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 +189,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 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 -vv user@host -- ssh -vv -oIdentitiesOnly=no user@host
$ trezor-agent -v foobar@hostname.com -c

@ -1,3 +1,3 @@
"""Cryptographic hardware device management."""
from . import interface
from . import interface, ui

@ -21,6 +21,11 @@ def _verify_support(identity):
class FakeDevice(interface.Device):
"""Connection to TREZOR device."""
@classmethod
def package_name(cls):
"""Python package name."""
return 'fake-device-agent'
def connect(self):
"""Return "dummy" connection."""
log.critical('NEVER USE THIS CODE FOR REAL-LIFE USE-CASES!!!')

@ -59,7 +59,7 @@ class DeviceError(Error):
"""Error during device operation."""
class Identity(object):
class Identity:
"""Represent SLIP-0013 identity, together with a elliptic curve choice."""
def __init__(self, identity_str, curve_name):
@ -102,7 +102,7 @@ class Identity(object):
return self.curve_name
class Device(object):
class Device:
"""Abstract cryptographic hardware device interface."""
def __init__(self):

@ -20,6 +20,11 @@ def _verify_support(identity, ecdh):
class KeepKey(trezor.Trezor):
"""Connection to KeepKey device."""
@classmethod
def package_name(cls):
"""Python package name (at PyPI)."""
return 'keepkey-agent'
@property
def _defs(self):
from . import keepkey_defs
@ -27,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 find_device():
"""Returns first USB HID transport."""
return next(HidTransport(p) for p in HidTransport.enumerate())

@ -36,6 +36,11 @@ def _convert_public_key(ecdsa_curve_name, result):
class LedgerNanoS(interface.Device):
"""Connection to Ledger Nano S device."""
@classmethod
def package_name(cls):
"""Python package name (at PyPI)."""
return 'ledger-agent'
def connect(self):
"""Enumerate and connect to the first USB HID interface."""
try:

@ -2,84 +2,92 @@
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:
return out.decode('ascii')
else:
log.error('UI failed: %r', err)
raise sp.CalledProcessError(exitcode, args)
def _is_open_tty(stream):
return not stream.closed and os.isatty(stream.fileno())
class Trezor(interface.Device):
"""Connection to TREZOR device."""
@classmethod
def package_name(cls):
"""Python package name (at PyPI)."""
return 'trezor-agent'
@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,
}.get(os.environ.get('TREZOR_TRANSPORT'), trezor_defs.HidTransport)
return trezor_defs
required_version = '>=1.4.0'
ui = None # can be overridden by device's users
def _override_pin_handler(self, conn):
cli_handler = conn.callback_PinMatrixRequest
if self.ui is None:
return
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 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
passphrase = _message_box('Please enter passphrase:')
return self._defs.PassphraseAck(passphrase=passphrase)
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 = 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)
@ -97,28 +105,30 @@ class Trezor(interface.Device):
current_version))
def connect(self):
"""Enumerate and connect to the first USB HID interface."""
for d in self._defs.Transport.enumerate():
log.debug('endpoint: %s', d)
transport = self._defs.Transport(d)
for _ in range(5):
connection = self._defs.Client(transport)
self._override_pin_handler(connection)
self._override_passphrase_handler(connection)
self._verify_version(connection)
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
raise interface.NotFoundError('{} not connected'.format(self))
"""Enumerate and connect to the first available interface."""
transport = self._defs.find_device()
if not transport:
raise interface.NotFoundError('{} not connected'.format(self))
log.debug('using transport: %s', transport)
for _ in range(5): # Retry a few times in case of PIN failures
connection = self._defs.Client(transport=transport,
state=self.__class__.cached_state)
self._override_pin_handler(connection)
self._override_passphrase_handler(connection)
self._override_state_handler(connection)
self._verify_version(connection)
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."""
@ -133,7 +143,7 @@ class Trezor(interface.Device):
result = self.conn.get_public_node(
n=addr, ecdsa_curve_name=curve_name)
log.debug('result: %s', result)
return result.node.public_key
return bytes(result.node.public_key)
def _identity_proto(self, identity):
result = self._defs.IdentityType()
@ -155,7 +165,7 @@ class Trezor(interface.Device):
log.debug('result: %s', result)
assert len(result.signature) == 65
assert result.signature[:1] == b'\x00'
return result.signature[1:]
return bytes(result.signature[1:])
except self._defs.CallException as e:
msg = '{} error: {}'.format(self, e)
log.debug(msg, exc_info=True)
@ -174,7 +184,7 @@ class Trezor(interface.Device):
log.debug('result: %s', result)
assert len(result.session_key) in {65, 33} # NIST256 or Curve25519
assert result.session_key[:1] == b'\x04'
return result.session_key
return bytes(result.session_key)
except self._defs.CallException as e:
msg = '{} error: {}'.format(self, e)
log.debug(msg, exc_info=True)

@ -1,10 +1,28 @@
"""TREZOR-related definitions."""
# pylint: disable=unused-import,import-error
import os
import logging
from trezorlib.client import CallException, PinException
from trezorlib.client import TrezorClient as Client
from trezorlib.messages_pb2 import PassphraseAck, PinMatrixAck
from trezorlib.transport_bridge import BridgeTransport
from trezorlib.transport_hid import HidTransport
from trezorlib.types_pb2 import IdentityType
from trezorlib.messages import IdentityType, PassphraseAck, PinMatrixAck, PassphraseStateAck
try:
from trezorlib.transport import get_transport
except ImportError:
from trezorlib.device import TrezorDevice
get_transport = TrezorDevice.find_by_path
log = logging.getLogger(__name__)
def find_device():
"""Selects a transport based on `TREZOR_PATH` environment variable.
If unset, picks first connected device.
"""
try:
return get_transport(os.environ.get("TREZOR_PATH"))
except Exception as e: # pylint: disable=broad-except
log.debug("Failed to find a Trezor device: %s", e)

@ -0,0 +1,129 @@
"""UIs for PIN/passphrase entry."""
import logging
import os
import subprocess
from .. import util
log = logging.getLogger(__name__)
class UI:
"""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()

@ -102,6 +102,8 @@ def _decompress_ed25519(pubkey):
if pubkey[:1] == b'\x00':
# set by Trezor fsm_msgSignIdentity() and fsm_msgGetPublicKey()
return ed25519.VerifyingKey(pubkey[1:])
else:
return None
def _decompress_nist256(pubkey):
@ -126,6 +128,8 @@ def _decompress_nist256(pubkey):
point = ecdsa.ellipticcurve.Point(curve.curve, x, y)
return ecdsa.VerifyingKey.from_public_point(point, curve=curve,
hashfunc=hashfunc)
else:
return None
def decompress_pubkey(pubkey, curve_name):

@ -18,6 +18,7 @@ import subprocess
import sys
import time
import pkg_resources
import semver
@ -85,7 +86,8 @@ def verify_gpg_version():
required_gpg = '>=2.1.11'
msg = 'Existing GnuPG has version "{}" ({} required)'.format(existing_gpg,
required_gpg)
assert semver.match(existing_gpg, required_gpg), msg
if not semver.match(existing_gpg, required_gpg):
log.error(msg)
def check_output(args):
@ -122,10 +124,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])
@ -133,11 +142,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
@ -148,13 +162,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
@ -168,25 +175,28 @@ 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',
'--import', pubkey.name])
verbosity = ('-' + ('v' * args.verbose)) if args.verbose else '--quiet'
check_call(keyring.gpg_command(['--homedir', homedir, verbosity,
'--import', pubkey.name]))
# Make new GPG identity with "ultimate" trust (via its fingerprint)
out = check_output([gpg_binary, '--homedir', homedir, '--list-public-keys',
'--with-fingerprint', '--with-colons'])
out = check_output(keyring.gpg_command(['--homedir', homedir,
'--list-public-keys',
'--with-fingerprint',
'--with-colons']))
fpr = re.findall('fpr:::::::::([0-9A-F]+):', out)[0]
f = write_file(os.path.join(homedir, 'ownertrust.txt'), fpr + ':6\n')
check_call([gpg_binary, '--homedir', homedir,
'--import-ownertrust', f.name])
check_call(keyring.gpg_command(['--homedir', homedir,
'--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(keyring.gpg_command(['--list-secret-keys', args.user_id,
'--homedir', homedir]))
def run_unlock(device_type, args):
@ -196,30 +206,59 @@ def run_unlock(device_type, args):
log.info('unlocked %s device', d)
def _server_from_assuan_fd(env):
fd = env.get('_assuan_connection_fd')
if fd is None:
return None
log.info('using fd=%r for UNIX socket server', fd)
return server.unix_domain_socket_server_from_fd(int(fd))
def _server_from_sock_path(env):
sock_path = keyring.get_agent_sock_path(env=env)
return server.unix_domain_socket_server(sock_path)
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('--server', default=False, action='store_true',
help='Use stdin/stdout for communication with GPG.')
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())
try:
env = {'GNUPGHOME': args.homedir}
sock_path = keyring.get_agent_sock_path(env=env)
env = {'GNUPGHOME': args.homedir, 'PATH': os.environ['PATH']}
pubkey_bytes = keyring.export_public_keys(env=env)
handler = agent.Handler(device=device_type(), pubkey_bytes=pubkey_bytes)
with server.unix_domain_socket_server(sock_path) as sock:
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))
handler = agent.Handler(device=device_type(),
pubkey_bytes=pubkey_bytes)
sock_server = _server_from_assuan_fd(os.environ)
if sock_server is None:
sock_server = _server_from_sock_path(env)
with sock_server as sock:
for conn in agent.yield_connections(sock):
with contextlib.closing(conn):
try:
@ -227,29 +266,59 @@ def run_agent(device_type):
except agent.AgentStop:
log.info('stopping gpg-agent')
return
except IOError as e:
log.info('connection closed: %s', e)
return
except Exception as e: # pylint: disable=broad-except
log.exception('handler failed: %s', e)
except Exception as e: # pylint: disable=broad-except
log.exception('gpg-agent failed: %s', e)
def main(device_type):
"""Parse command-line arguments."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
epilog = ('See https://github.com/romanz/trezor-agent/blob/master/'
'doc/README-GPG.md for usage examples.')
parser = argparse.ArgumentParser(epilog=epilog)
agent_package = device_type.package_name()
resources_map = {r.key: r for r in pkg_resources.require(agent_package)}
resources = [resources_map[agent_package], resources_map['libagent']]
versions = '\n'.join('{}={}'.format(r.key, r.version) for r in resources)
parser.add_argument('--version', help='print the version info',
action='version', version=versions)
subparsers = parser.add_subparsers(title='Action', dest='action')
subparsers.required = True
p = subparsers.add_parser('init',
help='Initialize hardware-based GnuPG identity')
help='initialize hardware-based GnuPG identity')
p.add_argument('user_id')
p.add_argument('-e', '--ecdsa-curve', default='nist256p1')
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, default=os.environ.get('GNUPGHOME'),
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')
p = subparsers.add_parser('unlock', help='unlock the hardware device')
p.add_argument('-v', '--verbose', default=0, action='count')
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."""
class Handler(object):
# pylint: disable=too-many-instance-attributes
class Handler:
"""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:

@ -15,7 +15,7 @@ def create_identity(user_id, curve_name):
return result
class Client(object):
class Client:
"""Sign messages and get public keys from a hardware device."""
def __init__(self, device):

@ -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,15 +190,18 @@ 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
@util.memoize
def get_gnupg_binary(sp=subprocess):
def get_gnupg_binary(sp=subprocess, neopg_binary=None):
"""Starting GnuPG 2.2.x, the default installation uses `gpg`."""
if neopg_binary:
return neopg_binary
return get_gnupg_components(sp=sp)['gpg']
@ -197,32 +209,31 @@ def gpg_command(args, env=None):
"""Prepare common GPG command line arguments."""
if env is None:
env = os.environ
cmd = [get_gnupg_binary()]
homedir = env.get('GNUPGHOME')
if homedir:
cmd.extend(['--homedir', homedir])
return cmd + args
cmd = get_gnupg_binary(neopg_binary=env.get('NEOPG_BINARY'))
return [cmd] + args
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'
line = line.split(b' ')[-1] # b'2.1.11'
line = line.split(b'-')[0] # remove trailing version parts
return line.split(b'v')[-1] # remove 'v' prefix
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 +243,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

@ -185,7 +185,7 @@ def get_curve_name_by_oid(oid):
raise KeyError('Unknown OID: {!r}'.format(oid))
class PublicKey(object):
class PublicKey:
"""GPG representation for public key packets."""
def __init__(self, curve_name, created, verifying_key, ecdh=False):

@ -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

@ -41,7 +41,7 @@ def test_parse_rsa():
assert keyring.parse_sig(sig) == (0x1020304,)
class FakeSocket(object):
class FakeSocket:
def __init__(self):
self.rx = io.BytesIO()
self.tx = io.BytesIO()

@ -39,6 +39,43 @@ def unix_domain_socket_server(sock_path):
remove_file(sock_path)
class FDServer:
"""File-descriptor based server (for NeoPG)."""
def __init__(self, fd):
"""C-tor."""
self.fd = fd
self.sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
def accept(self):
"""Use the same socket for I/O."""
return self, None
def recv(self, n):
"""Forward to underlying socket."""
return self.sock.recv(n)
def sendall(self, data):
"""Forward to underlying socket."""
return self.sock.sendall(data)
def close(self):
"""Not needed."""
def settimeout(self, _):
"""Not needed."""
def getsockname(self):
"""Simple representation."""
return '<fd: {}>'.format(self.fd)
@contextlib.contextmanager
def unix_domain_socket_server_from_fd(fd):
"""Build UDS-based socket server from a file descriptor."""
yield FDServer(fd)
def handle_connection(conn, handler, mutex):
"""
Handle a single connection using the specified protocol handler in a loop.

@ -5,12 +5,15 @@ import io
import logging
import os
import re
import signal
import subprocess
import sys
import tempfile
import threading
import pkg_resources
import configargparse
import daemon
from .. import device, formats, server, util
from . import client, protocol
@ -20,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:
@ -30,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:
@ -55,11 +63,21 @@ def _to_unicode(s):
return s
def create_agent_parser():
def create_agent_parser(device_type):
"""Create an ArgumentParser for this tool."""
p = configargparse.ArgParser(default_config_files=['~/.ssh/agent.config'])
epilog = ('See https://github.com/romanz/trezor-agent/blob/master/'
'doc/README-SSH.md for usage examples.')
p = configargparse.ArgParser(default_config_files=['~/.ssh/agent.config'],
epilog=epilog)
p.add_argument('-v', '--verbose', default=0, action='count')
agent_package = device_type.package_name()
resources_map = {r.key: r for r in pkg_resources.require(agent_package)}
resources = [resources_map[agent_package], resources_map['libagent']]
versions = '\n'.join('{}={}'.format(r.key, r.version) for r in resources)
p.add_argument('--version', help='print the version info',
action='version', version=versions)
curve_names = [name for name in formats.SUPPORTED_CURVES]
curve_names = ', '.join(sorted(curve_names))
p.add_argument('-e', '--ecdsa-curve-name', metavar='CURVE',
@ -67,13 +85,29 @@ def create_agent_parser():
help='specify ECDSA curve name: ' + curve_names)
p.add_argument('--timeout',
default=UNIX_SOCKET_TIMEOUT, type=float,
help='Timeout for accepting SSH client connections')
help='timeout for accepting SSH client connections')
p.add_argument('--debug', default=False, action='store_true',
help='Log SSH protocol messages for debugging.')
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',
@ -87,7 +121,7 @@ def create_agent_parser():
@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.
@ -97,9 +131,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:
@ -119,14 +150,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):
@ -156,7 +193,7 @@ def import_public_keys(contents):
yield line
class JustInTimeConnection(object):
class JustInTimeConnection:
"""Connect to the device just before the needed operation."""
def __init__(self, conn_factory, identities, public_keys=None):
@ -164,6 +201,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)."""
@ -180,19 +218,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().parse_args()
util.setup_logging(verbosity=args.verbose)
args = create_agent_parser(device_type=device_type).parse_args()
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')
@ -207,24 +273,41 @@ 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)
return 0 # success exit code

@ -11,7 +11,7 @@ from . import formats, util
log = logging.getLogger(__name__)
class Client(object):
class Client:
"""Client wrapper for SSH authentication device."""
def __init__(self, device):

@ -70,7 +70,7 @@ def _legacy_pubs(buf):
return util.frame(code, num)
class Handler(object):
class Handler:
"""ssh-agent protocol handler."""
def __init__(self, conn, debug=False):

@ -1,4 +1,3 @@
import functools
import io
import os
import socket
@ -19,7 +18,7 @@ def test_socket():
assert not os.path.isfile(path)
class FakeSocket(object):
class FakeSocket:
def __init__(self, data=b''):
self.rx = io.BytesIO(data)
@ -74,26 +73,27 @@ def test_handle():
def test_server_thread():
connections = [FakeSocket()]
sock = FakeSocket()
connections = [sock]
quit_event = threading.Event()
class FakeServer(object):
class FakeServer:
def accept(self): # pylint: disable=no-self-use
if connections:
return connections.pop(), 'address'
quit_event.set()
raise socket.timeout()
if not connections:
raise socket.timeout()
return connections.pop(), 'address'
def getsockname(self): # pylint: disable=no-self-use
return 'fake_server'
handler = protocol.Handler(conn=empty_device()),
handle_conn = functools.partial(server.handle_connection,
handler=handler,
mutex=None)
def handle_conn(conn):
assert conn is sock
quit_event.set()
server.server_thread(sock=FakeServer(),
handle_conn=handle_conn,
quit_event=quit_event)
quit_event.wait()
def test_spawn():

@ -25,7 +25,7 @@ def test_frames():
assert util.read_frame(io.BytesIO(f)) == b''.join(msgs)
class FakeSocket(object):
class FakeSocket:
def __init__(self):
self.buf = io.BytesIO()
@ -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__)
@ -145,7 +146,7 @@ def hexlify(blob):
return binascii.hexlify(blob).decode('ascii').upper()
class Reader(object):
class Reader:
"""Read basic type objects out of given stream."""
def __init__(self, stream):
@ -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:
"""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

@ -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/*

@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='libagent',
version='0.9.5',
version='0.12.0',
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',
@ -32,10 +34,7 @@ setup(
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',

@ -1,5 +1,5 @@
[tox]
envlist = py27,py3
envlist = py3
[pycodestyle]
max-line-length = 100
[pep257]

Loading…
Cancel
Save