Compare commits

...

69 Commits

Author SHA1 Message Date
Roman Zeyde
88ff57187f Bump version: 0.14.0 → 0.14.1 2020-05-02 17:43:44 +03:00
Roman Zeyde
52d840cbbb Initialize passphrase cache at UI c-tor 2020-04-29 22:01:06 +03:00
Roman Zeyde
8c22e5030b Bump 'trezor_agent' version: 0.10.0 → 0.11.0 2020-04-17 14:42:15 +03:00
Roman Zeyde
18c80b4cca Bump version: 0.13.1 → 0.14.0 2020-04-17 14:31:41 +03:00
Roman Zeyde
7eab4933ed Add more Python version to Travis 2020-04-17 14:30:36 +03:00
Roman Zeyde
d103ebee6f Fix pylint warning 2020-04-17 14:28:50 +03:00
matejcik
d8bcca3ccb support trezorlib 0.12 2020-04-09 14:41:56 +02:00
Roman Zeyde
67ef11419a Merge pull request #320 from eli-b/patch-5
docs: Install libagent from source too
2020-04-06 23:30:46 +03:00
Eli Boyarski
d4d168c746 docs: Install libagent from source too
Installing the trezor/ledger agent from source installs the libagent module from PyPI unless libagent is already installed from source beforehand.
2020-04-06 20:44:25 +03:00
Roman Zeyde
61cfcef35c Merge branch 'NTICompass/keepkey-webusb' 2020-03-16 23:21:01 +02:00
Eric Siegel
0f627e8322 Clean up code... 2020-03-16 15:26:15 -04:00
Eric Siegel (Rocket Hazmat)
7bdfa7609d Upgrade KeepKey for new libagent code
Add get_public_node for KeepKey
2020-03-13 13:50:09 -04:00
Eric Siegel (Rocket Hazmat)
53b08f4968 Fix detecting KeepKey USB device
The new KeepKey firmware uses WebUSB instead of HID
2020-03-13 13:05:08 -04:00
Roman Zeyde
15b0218bf2 Default GPG key creation time to 0 (i.e. Jan 1 1970) 2019-10-29 09:14:26 +02:00
Roman Zeyde
f52e959639 Merge branch 'patch-2' of https://github.com/zack-shoylev/trezor-agent 2019-10-29 09:12:18 +02:00
Roman Zeyde
d98f49445e Merge branch 'patch-1' of https://github.com/korzq/trezor-agent 2019-10-26 13:52:02 +03:00
Roman Zeyde
ab6892f42f Fix pylint warnings 2019-10-26 13:47:29 +03:00
Eric Zhu
f03312d61f Update README-SSH.md 2019-10-02 17:41:51 -04:00
Roman Zeyde
b75cf74976 Merge pull request #301 from hkjn/20190925-describe-versioning
Add components section
2019-09-25 18:44:47 +03:00
Henrik Jonsson
363b4d633f Add components section 2019-09-25 12:19:33 +02:00
Zack Shoylev
b7d0ef0f94 Update README-SSH.md
Fix typo
2019-08-27 15:33:02 -05:00
Zack Shoylev
8c3744c30c Update README-SSH.md
Small systemd doc improvements.
2019-06-25 13:24:32 -05:00
Roman Zeyde
513b1259c4 Bump version: 0.13.0 → 0.13.1 2019-03-10 18:41:14 +02:00
Roman Zeyde
5984a58f65 Update .bumpversion.cfg 2019-03-10 18:41:07 +02:00
Roman Zeyde
e437591dd5 Fix prompt for symmetric encryption passphrase 2019-03-03 22:51:15 +02:00
André Vitor de Lima Matos
94ad9648f8 Fix passphrase cache
Broken since 2cb64991c3
Fix #284
2019-02-23 17:42:08 -03:00
Roman Zeyde
ed64f94bd3 Merge pull request #281 from eli-b/patch-4
Fix header numbering
2018-12-20 07:46:33 +02:00
Eli Boyarski
bf9f2593b5 Fix header numbering 2018-12-19 13:06:24 +02:00
Roman Zeyde
995fba3e93 Drop compatibility with <0.11 trezorlib 2018-12-13 00:05:47 +02:00
Roman Zeyde
34b269be1e Bump library and TREZOR-related agent versions 2018-12-12 23:59:15 +02:00
matejcik
5cfdc7734b fix style complaints 2018-12-10 16:30:56 +01:00
matejcik
2cb64991c3 Trezor: restructure code to support python-trezor 0.11 2018-12-10 16:10:55 +01:00
matejcik
a30cab1156 Trezor: bump version requirement to 0.10.1
because 0.9 doesn't work anyway due to the hidapi extra,
and there's no point of supporting 0.10.0 that doesn't have state
handling
2018-12-10 16:10:55 +01:00
matejcik
b30e6a8408 Allow devices to override connection closing 2018-12-10 16:10:55 +01:00
Roman Zeyde
8041ed883f Ignore cyclic imports pylint warning 2018-11-30 11:29:40 +02:00
walkjivefly
a71fa8de9e Add missing pre-reqs
Attempting to install 0.10.2 from PyPI failed because docutils and wheel were not installed.
2018-11-30 12:31:54 +07:00
Roman Zeyde
ddd823d976 Bump version: 0.12.0 → 0.12.1 2018-11-17 23:48:23 +02:00
Roman Zeyde
fec84288be gpg: --homedir should come before --list-secret-keys 2018-10-27 18:15:29 +03:00
Roman Zeyde
71f357c1bf Add 'hidapi' dependency 2018-08-18 12:55:46 +03:00
Eli Boyarski
8f1d008eb2 fixed typo + missing word 2018-08-06 23:19:32 +03:00
Roman Zeyde
7a351acf15 Merge remote-tracking branch 'matejcik/master' 2018-08-02 22:01:31 +03:00
Roman Zeyde
7f9aa2b147 Bump version: 0.11.3 → 0.12.0 2018-07-25 13:47:48 +03:00
Roman Zeyde
eed168341c Don't inheric from 'object' (after deprecating Python 2.x support) 2018-07-25 13:44:55 +03:00
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.
2018-07-17 16:50:53 +02:00
matejcik
8708b1e16d trezor: use TREZOR_PATH environment variable to specify device path 2018-07-17 16:45:09 +02:00
Roman Zeyde
03e7fc48e9 Improve Git-related documentation 2018-07-12 12:10:48 +03:00
Roman Zeyde
4968ca7ff3 Merge branch 'master' into neopg-wip 2018-07-01 13:52:37 +03:00
Roman Zeyde
6b6d9f5d20 Add a link to neopg-trezor wrapper at documentation 2018-07-01 13:17:13 +03:00
Roman Zeyde
c22109df24 Document argv[0] hack for NeoPG 2018-07-01 13:15:04 +03:00
Roman Zeyde
47ce035e79 Remove unused import 2018-07-01 12:52:08 +03:00
Roman Zeyde
36cbba6c57 Fix a few lint issues 2018-07-01 12:49:39 +03:00
Roman Zeyde
6afe20350b Simplify GPG command generation 2018-07-01 12:12:16 +03:00
Roman Zeyde
fa171e8923 Add short example for NeoPG usage 2018-07-01 12:08:46 +03:00
Roman Zeyde
f0bda9a3e6 Allow using $PATH when looking for GPG binary
It's needed for running neopg (instead of gnupg).
2018-07-01 12:05:25 +03:00
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].
2018-07-01 12:04:32 +03:00
Roman Zeyde
3b9c00e02a Default to $GNUPGHOME when not specified on commandline 2018-07-01 11:46:16 +03:00
Roman Zeyde
dcee59a19e Assume NeoPG binary runs GnuPG functionality 2018-07-01 11:32:02 +03:00
Roman Zeyde
a274de30b8 Parse NeoPG development versions
e.g. v0.0.5-37-g1fe5046-dirty
2018-06-30 13:05:21 +03:00
Roman Zeyde
4fe9e437ad Simplify GPG homedir setting 2018-06-30 13:03:30 +03:00
Roman Zeyde
d04527a8ed Replace GPG version assertion by an error log
since NeoPG uses different versioning
2018-06-30 13:02:50 +03:00
Roman Zeyde
3329c29cb4 Use gpg_command() for identity generation 2018-06-30 12:50:55 +03:00
Roman Zeyde
df2cb52f8d fixup! Reply with an ERR to SCD SERIALNO openpgp ASSUAN command 2018-06-30 12:49:59 +03:00
Roman Zeyde
f36ef4ffe0 Allow running NeoPG binary (instead of GnuPG) 2018-06-30 12:44:17 +03:00
Roman Zeyde
f74de828fc Reply with an ERR to SCD SERIALNO openpgp ASSUAN command
(for NeoPG)
2018-06-30 12:10:37 +03:00
Roman Zeyde
912b1cde7a Add support for file-descriptor-based socket server
(for NeoPG)
2018-06-30 12:10:03 +03:00
Roman Zeyde
b7a8c42893 Merge pull request #153 from romanz/drop-py2
setup: deprecate Python2 support
2018-06-30 11:24:52 +03:00
Roman Zeyde
1e6c4e6930 Add links to SSH/GPG usage examples 2018-06-30 11:21:47 +03:00
Roman Zeyde
a8f19e4150 Comment about SSH argument separation 2018-06-30 11:12:43 +03:00
Roman Zeyde
2e688ccac9 setup: deprecate Python2 support 2018-03-08 09:18:37 +02:00
31 changed files with 339 additions and 185 deletions

View File

@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 0.11.3
current_version = 0.14.1
[bumpversion:file:setup.py]

View File

@@ -1,5 +1,5 @@
[MESSAGES CONTROL]
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme,duplicate-code
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme,duplicate-code,cyclic-import,import-outside-toplevel
[SIMILARITIES]
min-similarity-lines=5

View File

@@ -1,10 +1,10 @@
sudo: false
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "3.7"
- "3.8"
cache:
directories:

View File

@@ -16,6 +16,20 @@ See the following blog posts about this tool:
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.
## Components
This repository contains source code for one library as well as
agents to interact with several different hardware devices:
* [`libagent`](https://pypi.org/project/libagent/): shared library
* [`trezor-agent`](https://pypi.org/project/trezor-agent/): Using Trezor as hardware-based SSH/PGP agent
* [`ledger_agent`](https://pypi.org/project/ledger_agent/): Using Ledger as hardware-based SSH/PGP agent
* [`keepkey_agent`](https://pypi.org/project/keepkey_agent/): Using KeepKey as hardware-based SSH/PGP agent
The [/releases](/releases) page on Github contains the `libagent`
releases.
## Documentation
* **Installation** instructions are [here](doc/INSTALL.md)
@@ -24,4 +38,4 @@ Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/)
Note: If you're using Windows, see [trezor-ssh-agent](https://github.com/martin-lizner/trezor-ssh-agent) by Martin Lízner.
* **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)
* Instructions to configure a Trezor-style **PIN entry** program are [here](doc/README-PINENTRY.md)

View File

@@ -3,15 +3,15 @@ from setuptools import setup
setup(
name='trezor_agent',
version='0.9.3',
version='0.11.0',
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.11.2',
'trezor[hidapi]>=0.9.0'
'libagent>=0.14.0',
'trezor[hidapi]>=0.12.0,<0.13'
],
platforms=['POSIX'],
classifiers=[
@@ -22,10 +22,10 @@ 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.7',
'Programming Language :: Python :: 3.8',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',

15
contrib/neopg-trezor Executable file
View File

@@ -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:])

View File

@@ -36,7 +36,7 @@ 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.

View File

@@ -66,7 +66,7 @@ gpg (GnuPG) 2.1.15
3. Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agent) package:
```
$ pip3 install Cython
$ pip3 install Cython hidapi
$ pip3 install trezor_agent
```
@@ -74,6 +74,7 @@ gpg (GnuPG) 2.1.15
```
$ git clone https://github.com/romanz/trezor-agent
$ pip3 install --user -e trezor-agent
$ pip3 install --user -e trezor-agent/agents/trezor
```
@@ -126,6 +127,7 @@ Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_ag
```
$ git clone https://github.com/romanz/trezor-agent
$ pip3 install --user -e trezor-agent
$ pip3 install --user -e trezor-agent/agents/ledger
```

31
doc/README-NeoPG.md Normal file
View File

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

View File

@@ -32,6 +32,12 @@ $ (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.
Example:
```
(trezor|keepkey|ledger)-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob
```
As a shortcut you can run
@@ -41,7 +47,7 @@ $ (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`
##### 3. 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:
@@ -84,21 +90,29 @@ would allow you to login using the corresponding private key signature.
### Access remote Git/Mercurial repositories
Copy your public key and register it in your repository web interface (e.g. [GitHub](https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/)):
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 | xclip
$ trezor-agent -v -e ed25519 git@github.com > ~/.ssh/github.pub
Add the following configuration to your `~/.ssh/config` file:
Host github.com
IdentityFile ~/.ssh/github.pub
Use the following Bash alias for convenient Git operations:
$ alias git_hub='trezor-agent -v -e ed25519 git@github.com -- git'
$ alias ssh-shell='trezor-agent ~/.ssh/github.pub -v --shell'
Replace `git` with `git_hub` for remote operations:
Now, you can use regular Git commands under the "SSH-enabled" sub-shell:
$ git_hub push origin master
$ ssh-shell
$ git push origin master
The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html)):
$ trezor-agent -v -e ed25519 git@bitbucket.org -- hg push
$ ssh-shell
$ hg push
### Start the agent as a systemd unit
@@ -114,7 +128,8 @@ Description=trezor-agent SSH agent
Requires=trezor-ssh-agent.socket
[Service]
Type=Simple
Type=simple
Restart=always
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
@@ -124,6 +139,21 @@ If you've installed `trezor-agent` locally you may have to change the path in `E
Replace `IDENTITY` with the identity you used when exporting the public key.
`IDENTITY` can be a path (starting with `/`) to a file containing a list of public keys
generated by Trezor. I.e. `/home/myUser/.ssh/trezor.conf` with one public key per line.
This is a more convenient way to have a systemd setup that has to handle multiple
keys/hosts.
When updating the file, make sure to restart trezor-agent.
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`
````
@@ -151,9 +181,13 @@ 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.*")
export SSH_AUTH_SOCK=$(systemctl show --user --property=Listen trezor-ssh-agent.socket | grep -o "/run.*" | cut -d " " -f 1)
```
Make sure the SSH_AUTH_SOCK variable matches the location of the socket that trezor-agent
is listening on: `ps -x | grep trezor-agent`. In this setup trezor-agent should start
automatically when the socket is opened.
##### 4. SSH will now automatically use your device key in all terminals.
## 4. Troubleshooting

View File

@@ -39,10 +39,6 @@ class FakeDevice(interface.Device):
self.vk = self.sk.get_verifying_key()
return self
def close(self):
"""Close connection."""
self.conn = None
def pubkey(self, identity, ecdh=False):
"""Return public key."""
_verify_support(identity)

View File

@@ -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):
@@ -113,6 +113,14 @@ class Device(object):
"""Connect to device, otherwise raise NotFoundError."""
raise NotImplementedError()
def close(self):
"""Close connection to device.
By default, close the underlying connection. Overriding classes
can perform their own cleanup.
"""
self.conn.close()
def __enter__(self):
"""Allow usage as context manager."""
self.conn = self.connect()
@@ -121,7 +129,7 @@ class Device(object):
def __exit__(self, *args):
"""Close and mark as disconnected."""
try:
self.conn.close()
self.close()
except Exception as e: # pylint: disable=broad-except
log.exception('close failed: %s', e)
self.conn = None

View File

@@ -6,9 +6,18 @@ 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
from keepkeylib.transport_webusb import WebUsbTransport
from keepkeylib.types_pb2 import IdentityType
get_public_node = Client.get_public_node
sign_identity = Client.sign_identity
Client.state = None
def enumerate_transports():
"""Returns USB HID transports."""
return [HidTransport(p) for p in HidTransport.enumerate()]
def find_device():
"""Returns first WebUSB or HID transport."""
for d in WebUsbTransport.enumerate():
return WebUsbTransport(d)
for d in HidTransport.enumerate():
return HidTransport(d)

View File

@@ -3,11 +3,9 @@
import binascii
import logging
import mnemonic
import semver
from . import interface
from .. import util
log = logging.getLogger(__name__)
@@ -28,65 +26,7 @@ class Trezor(interface.Device):
required_version = '>=1.4.0'
ui = None # can be overridden by device's users
def _override_pin_handler(self, conn):
if self.ui is None:
return
def new_handler(_):
try:
scrambled_pin = self.ui.get_pin()
result = self._defs.PinMatrixAck(pin=scrambled_pin)
if not set(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):
if self.ui is None:
return
def new_handler(msg):
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
cached_session_id = None
def _verify_version(self, connection):
f = connection.features
@@ -106,21 +46,22 @@ class Trezor(interface.Device):
def connect(self):
"""Enumerate and connect to the first available interface."""
transports = self._defs.enumerate_transports()
if not transports:
transport = self._defs.find_device()
if not transport:
raise interface.NotFoundError('{} not connected'.format(self))
log.debug('transports: %s', transports)
log.debug('using transport: %s', transport)
for _ in range(5): # Retry a few times in case of PIN failures
connection = self._defs.Client(transport=transports[0],
state=self.__class__.cached_state)
self._override_pin_handler(connection)
self._override_passphrase_handler(connection)
self._override_state_handler(connection)
connection = self._defs.Client(transport=transport,
ui=self.ui,
session_id=self.__class__.cached_session_id)
self._verify_version(connection)
try:
connection.ping(msg='', pin_protection=True) # unlock PIN
# unlock PIN and passphrase
self._defs.get_address(connection,
"Testnet",
self._defs.PASSPHRASE_TEST_PATH)
return connection
except (self._defs.PinException, ValueError) as e:
log.error('Invalid PIN: %s, retrying...', e)
@@ -132,7 +73,8 @@ class Trezor(interface.Device):
def close(self):
"""Close connection."""
self.conn.close()
self.__class__.cached_session_id = self.conn.session_id
super().close()
def pubkey(self, identity, ecdh=False):
"""Return public key."""
@@ -140,8 +82,10 @@ class Trezor(interface.Device):
log.debug('"%s" getting public key (%s) from %s',
identity.to_string(), curve_name, self)
addr = identity.get_bip32_address(ecdh=ecdh)
result = self.conn.get_public_node(
n=addr, ecdsa_curve_name=curve_name)
result = self._defs.get_public_node(
self.conn,
n=addr,
ecdsa_curve_name=curve_name)
log.debug('result: %s', result)
return bytes(result.node.public_key)
@@ -157,7 +101,8 @@ class Trezor(interface.Device):
log.debug('"%s" signing %r (%s) on %s',
identity.to_string(), blob, curve_name, self)
try:
result = self.conn.sign_identity(
result = self._defs.sign_identity(
self.conn,
identity=self._identity_proto(identity),
challenge_hidden=blob,
challenge_visual='',
@@ -166,7 +111,7 @@ class Trezor(interface.Device):
assert len(result.signature) == 65
assert result.signature[:1] == b'\x00'
return bytes(result.signature[1:])
except self._defs.CallException as e:
except self._defs.TrezorFailure as e:
msg = '{} error: {}'.format(self, e)
log.debug(msg, exc_info=True)
raise interface.DeviceError(msg)
@@ -177,7 +122,8 @@ class Trezor(interface.Device):
log.debug('"%s" shared session key (%s) for %r from %s',
identity.to_string(), curve_name, pubkey, self)
try:
result = self.conn.get_ecdh_session_key(
result = self._defs.get_ecdh_session_key(
self.conn,
identity=self._identity_proto(identity),
peer_public_key=pubkey,
ecdsa_curve_name=curve_name)
@@ -185,7 +131,7 @@ class Trezor(interface.Device):
assert len(result.session_key) in {65, 33} # NIST256 or Curve25519
assert result.session_key[:1] == b'\x04'
return bytes(result.session_key)
except self._defs.CallException as e:
except self._defs.TrezorFailure as e:
msg = '{} error: {}'.format(self, e)
log.debug(msg, exc_info=True)
raise interface.DeviceError(msg)

View File

@@ -1,13 +1,30 @@
"""TREZOR-related definitions."""
# pylint: disable=unused-import,import-error
# pylint: disable=unused-import,import-error,no-name-in-module,no-member
import os
import logging
from trezorlib.client import CallException, PinException
from trezorlib.client import TrezorClient as Client
from trezorlib.messages import IdentityType, PassphraseAck, PinMatrixAck, PassphraseStateAck
from trezorlib.device import TrezorDevice
import mnemonic
import semver
import trezorlib
from trezorlib.client import TrezorClient as Client, PASSPHRASE_TEST_PATH
from trezorlib.exceptions import TrezorFailure, PinException
from trezorlib.transport import get_transport
from trezorlib.messages import IdentityType
from trezorlib.btc import get_address, get_public_node
from trezorlib.misc import sign_identity, get_ecdh_session_key
log = logging.getLogger(__name__)
def enumerate_transports():
"""Returns all available transports."""
return TrezorDevice.enumerate()
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)

View File

@@ -4,12 +4,17 @@ import logging
import os
import subprocess
try:
from trezorlib.client import PASSPHRASE_ON_DEVICE
except ImportError:
PASSPHRASE_ON_DEVICE = object()
from .. import util
log = logging.getLogger(__name__)
class UI(object):
class UI:
"""UI for PIN/passphrase entry (for TREZOR devices)."""
def __init__(self, device_type, config=None):
@@ -23,8 +28,10 @@ class UI(object):
default_pinentry)
self.options_getter = create_default_options_getter()
self.device_name = device_type.__name__
self.cached_passphrase_ack = util.ExpiringCache(
seconds=float(config.get('cache_expiry_seconds', 'inf')))
def get_pin(self, name=None):
def get_pin(self, _code=None):
"""Ask the user for (scrambled) PIN."""
description = (
'Use the numeric keypad to describe number positions.\n'
@@ -33,20 +40,37 @@ class UI(object):
' 4 5 6\n'
' 1 2 3')
return interact(
title='{} PIN'.format(name or self.device_name),
title='{} PIN'.format(self.device_name),
prompt='PIN:',
description=description,
binary=self.pin_entry_binary,
options=self.options_getter())
def get_passphrase(self, name=None):
def get_passphrase(self, prompt='Passphrase:', available_on_device=False):
"""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())
passphrase = None
if self.cached_passphrase_ack:
passphrase = self.cached_passphrase_ack.get()
if passphrase is None:
env_passphrase = os.environ.get("TREZOR_PASSPHRASE")
if env_passphrase is not None:
passphrase = env_passphrase
elif available_on_device:
passphrase = PASSPHRASE_ON_DEVICE
else:
passphrase = interact(
title='{} passphrase'.format(self.device_name),
prompt=prompt,
description=None,
binary=self.passphrase_entry_binary,
options=self.options_getter())
if self.cached_passphrase_ack:
self.cached_passphrase_ack.set(passphrase)
return passphrase
def button_request(self, _code=None):
"""Called by TrezorClient when device interaction is required."""
# XXX: show notification to the user?
def create_default_options_getter():

View File

@@ -86,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):
@@ -179,22 +180,23 @@ fi
# 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()
verbosity = ('-' + ('v' * args.verbose)) if args.verbose else '--quiet'
check_call([gpg_binary, '--homedir', homedir, verbosity,
'--import', pubkey.name])
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', args.user_id],
env={'GNUPGHOME': homedir})
check_call(keyring.gpg_command(['--homedir', homedir,
'--list-secret-keys', args.user_id]))
def run_unlock(device_type, args):
@@ -204,11 +206,26 @@ 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."""
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.')
@@ -228,32 +245,40 @@ def run_agent(device_type):
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)
device_type.ui = device.ui.UI(device_type=device_type,
config=vars(args))
device_type.cached_passphrase_ack = util.ExpiringCache(
seconds=float(args.cache_expiry_seconds))
with server.unix_domain_socket_server(sock_path) as sock:
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):
handler = agent.Handler(device=device_type(),
pubkey_bytes=pubkey_bytes)
with contextlib.closing(conn):
try:
handler.handle(conn)
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()
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)}
@@ -269,11 +294,11 @@ def main(device_type):
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('-t', '--time', type=int, default=0)
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,
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',
@@ -291,7 +316,5 @@ def main(device_type):
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)

View File

@@ -70,7 +70,7 @@ class AgentStop(Exception):
# pylint: disable=too-many-instance-attributes
class Handler(object):
class Handler:
"""GPG agent requests' handler."""
def _get_options(self):
@@ -118,8 +118,8 @@ class Handler(object):
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')
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)

View File

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

View File

@@ -173,9 +173,7 @@ def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None):
assert communicate(sock, 'PKSIGN') == b'OK'
while True:
line = recvline(sock).strip()
if line.startswith(b'S PROGRESS'):
continue
else:
if not line.startswith(b'S PROGRESS'):
break
line = unescape(line)
log.debug('unescaped: %r', line)
@@ -198,8 +196,10 @@ def get_gnupg_components(sp=subprocess):
@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']
@@ -207,11 +207,8 @@ 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):
@@ -226,7 +223,9 @@ def gpg_version(sp=subprocess):
args = gpg_command(['--version'])
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):

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,10 @@ def _to_unicode(s):
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()
@@ -75,8 +78,7 @@ def create_agent_parser(device_type):
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))
curve_names = ', '.join(sorted(formats.SUPPORTED_CURVES))
p.add_argument('-e', '--ecdsa-curve-name', metavar='CURVE',
default=formats.CURVE_NIST256,
help='specify ECDSA curve name: ' + curve_names)
@@ -190,7 +192,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):
@@ -272,8 +274,6 @@ def main(device_type):
# 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()),

View File

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

View File

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

View File

@@ -18,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)
@@ -77,7 +77,7 @@ def test_server_thread():
connections = [sock]
quit_event = threading.Event()
class FakeServer(object):
class FakeServer:
def accept(self): # pylint: disable=no-self-use
if not connections:
raise socket.timeout()

View File

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

View File

@@ -146,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):
@@ -242,7 +242,7 @@ def which(cmd):
from shutil import which as _which
except ImportError:
# For Python 2
from backports.shutil_which import which as _which # pylint: disable=relative-import
from backports.shutil_which import which as _which
full_path = _which(cmd)
if full_path is None:
raise OSError('Cannot find {!r} in $PATH'.format(cmd))
@@ -258,7 +258,7 @@ def assuan_serialize(data):
return data
class ExpiringCache(object):
class ExpiringCache:
"""Simple cache with a deadline."""
def __init__(self, seconds, timer=time.time):

View File

@@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='libagent',
version='0.11.3',
version='0.14.1',
description='Using hardware wallets as SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
@@ -15,8 +15,10 @@ setup(
'libagent.ssh'
],
install_requires=[
'docutils>=0.14',
'wheel>=0.32.3',
'backports.shutil_which>=3.5.1',
'ConfigArgParse>=0.12.0',
'ConfigArgParse>=0.12.1',
'python-daemon>=2.1.2',
'ecdsa>=0.13',
'ed25519>=1.4',
@@ -34,10 +36,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',

View File

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