mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ff57187f | ||
|
|
52d840cbbb | ||
|
|
8c22e5030b | ||
|
|
18c80b4cca | ||
|
|
7eab4933ed | ||
|
|
d103ebee6f | ||
|
|
d8bcca3ccb | ||
|
|
67ef11419a | ||
|
|
d4d168c746 | ||
|
|
61cfcef35c | ||
|
|
0f627e8322 | ||
|
|
7bdfa7609d | ||
|
|
53b08f4968 | ||
|
|
15b0218bf2 | ||
|
|
f52e959639 | ||
|
|
d98f49445e | ||
|
|
ab6892f42f | ||
|
|
f03312d61f | ||
|
|
b75cf74976 | ||
|
|
363b4d633f | ||
|
|
b7d0ef0f94 | ||
|
|
8c3744c30c | ||
|
|
513b1259c4 | ||
|
|
5984a58f65 | ||
|
|
e437591dd5 | ||
|
|
94ad9648f8 | ||
|
|
ed64f94bd3 | ||
|
|
bf9f2593b5 | ||
|
|
995fba3e93 | ||
|
|
34b269be1e | ||
|
|
5cfdc7734b | ||
|
|
2cb64991c3 | ||
|
|
a30cab1156 | ||
|
|
b30e6a8408 | ||
|
|
8041ed883f | ||
|
|
a71fa8de9e | ||
|
|
ddd823d976 | ||
|
|
fec84288be | ||
|
|
71f357c1bf | ||
|
|
8f1d008eb2 | ||
|
|
7a351acf15 | ||
|
|
7f9aa2b147 | ||
|
|
eed168341c | ||
|
|
8b85090fba | ||
|
|
8708b1e16d | ||
|
|
03e7fc48e9 | ||
|
|
4968ca7ff3 | ||
|
|
6b6d9f5d20 | ||
|
|
c22109df24 | ||
|
|
47ce035e79 | ||
|
|
36cbba6c57 | ||
|
|
6afe20350b | ||
|
|
fa171e8923 | ||
|
|
f0bda9a3e6 | ||
|
|
71b56e15d7 | ||
|
|
3b9c00e02a | ||
|
|
dcee59a19e | ||
|
|
a274de30b8 | ||
|
|
4fe9e437ad | ||
|
|
d04527a8ed | ||
|
|
3329c29cb4 | ||
|
|
df2cb52f8d | ||
|
|
f36ef4ffe0 | ||
|
|
f74de828fc | ||
|
|
912b1cde7a | ||
|
|
b7a8c42893 | ||
|
|
1e6c4e6930 | ||
|
|
a8f19e4150 | ||
|
|
2e688ccac9 |
@@ -1,7 +1,7 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 0.11.3
|
||||
current_version = 0.14.1
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
||||
16
README.md
16
README.md
@@ -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)
|
||||
|
||||
@@ -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
15
contrib/neopg-trezor
Executable 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:])
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
31
doc/README-NeoPG.md
Normal 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]
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
11
setup.py
11
setup.py
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user