Compare commits

...

23 Commits

Author SHA1 Message Date
Roman Zeyde
85274d8374 bump version 2017-05-13 13:18:25 +03:00
Roman Zeyde
f358ca29d4 Allow loading previously exported SSH public keys from a file 2017-05-13 12:47:48 +03:00
Roman Zeyde
53d43cba29 gpg: update README after re-packaging 2017-05-06 21:13:07 +03:00
Roman Zeyde
214b556f83 ssh: simplify argparse creation 2017-05-06 20:55:38 +03:00
Roman Zeyde
051a3fd4ab ssh: remove unused git-related code 2017-05-06 20:40:23 +03:00
Roman Zeyde
91050ee64a ssh: remove unused git wrapper 2017-05-06 15:02:46 +03:00
Roman Zeyde
257992d04c ssh: move related code to a separate subdirectory 2017-05-05 11:22:00 +03:00
Roman Zeyde
6c2273387d util: extend logging.basicConfig() 2017-05-03 21:30:33 +03:00
Roman Zeyde
b6ad8207ba setup: add Python 3.6 support 2017-05-01 21:37:32 +03:00
Roman Zeyde
3a93fc859d setup: fix indentation for lists 2017-05-01 21:35:05 +03:00
Roman Zeyde
7d9b3ff1d0 README: update device-related info 2017-04-29 20:51:18 +03:00
Roman Zeyde
4af881b3cb Split the package into a shared library and separate per-device packages 2017-04-29 18:34:46 +03:00
Roman Zeyde
eb525e1b62 gpg: simplify Python entry point and refactor Bash scripts a bit
Now there is a single 'trezor-gpg' tool, with various subcommands.
2017-04-26 23:12:09 +03:00
Roman Zeyde
02c8e729b7 ssh: retrieve all keys using a single device session 2017-04-25 20:43:19 +03:00
Roman Zeyde
12359938ad keepkey: fix transport import 2017-04-23 21:27:02 +03:00
Roman Zeyde
93cd3e688b travis: add Python 3.6 2017-04-22 20:19:06 +03:00
Tomás Rojas
26d7dd3124 Cache public keys for the duration of the agent
This saves a lot of time when connecting to multiple hosts
simultaneously (e.g., during a deploy) as every time we are asked to sign a
challenge, all public keys are iterated to find the correct one. This
can become especially slow when using the Bridge transport and/or many
identities are defined.
2017-04-22 14:47:30 +03:00
Tomás Rojas
0d5c3a9ca7 Allow using TREZOR bridge (instead of HID transport) 2017-04-22 11:16:30 +03:00
Roman Zeyde
97ec6b2719 travis: fix dependency 2017-04-22 10:53:39 +03:00
Roman Zeyde
8ba9be1780 fix pylint warnings 2017-04-21 22:56:50 +03:00
Roman Zeyde
b2bc87c0c7 fix pydocstyle warnings 2017-04-21 22:51:11 +03:00
Timothy Hobbs
d522d148ef Connection error is confusing (#105)
Hi,

I ran into this connection error:

````
> trezor-agent  timothy@localhost
2017-04-10 00:22:01,818 ERROR        Connection error: open failed                                                                        [__main__.py:130]
````

I didn't know what was going on, whether there was a problem connecting to localhost or what. I eventually logged into trezor wallet and found that there too, my device was not recognized (probably because I did not unplug it/replug it after updating the HID settings.) Unplugging it and replugging it fixed everything.
2017-04-10 10:40:27 +03:00
Roman Zeyde
c796a3b01d README: password manager usage example 2017-03-28 21:21:35 +03:00
57 changed files with 445 additions and 289 deletions

View File

@@ -1,2 +1,2 @@
[MESSAGES CONTROL]
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return

View File

@@ -4,29 +4,23 @@ python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
cache:
directories:
- $HOME/.cache/pip
addons:
apt:
packages:
- libudev-dev
- libusb-1.0-0-dev
before_install:
- pip install -U setuptools pylint coverage pep8 pydocstyle "pip>=7.0" wheel
- pip install -e git+https://github.com/keepkey/python-keepkey@6e8baa8b935e830d05f87b6dfd9bc7c927a96dc3#egg=keepkey
install:
- pip install -e .
script:
- pep8 trezor_agent
- pylint --reports=no --rcfile .pylintrc trezor_agent
- pydocstyle trezor_agent
- coverage run --source trezor_agent/ -m py.test -v
- pep8 libagent
- pylint --reports=no --rcfile .pylintrc libagent
- pydocstyle libagent
- coverage run --source libagent/ -m py.test -v
after_success:
- coverage report

View File

@@ -16,11 +16,10 @@ gpg (GnuPG) 2.1.15
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).
Update you TREZOR firmware to the latest version (at least v1.4.0).
Update you device firmware to the latest version and install your specific `agent` package:
Install latest `trezor-agent` package from GitHub:
```
$ pip install --user git+https://github.com/romanz/trezor-agent.git
$ pip install --user (trezor|keepkey|ledger)_agent
```
# Quickstart
@@ -49,3 +48,33 @@ $ git log --show-signature -1 # verify commit signature
$ git tag v1.2.3 --sign # create GPG-signed tag
$ git tag v1.2.3 --verify # verify tag signature
```
## Password manager
First install `pass` from [passwordstore.org](https://www.passwordstore.org/) and initialize it to use your TREZOR-based GPG identity:
```
$ ./scripts/gpg-shell
$ pass init "Roman Zeyde <roman.zeyde@gmail.com>"
Password store initialized for Roman Zeyde <roman.zeyde@gmail.com>
```
Then, you can generate truly random passwords and save them encrypted using your public key (as separate `.gpg` files under `~/.password-store/`):
```
$ pass generate Dev/github 32
$ pass generate Social/hackernews 32
$ pass generate Social/twitter 32
$ pass generate VPS/linode 32
$ pass
Password Store
├── Dev
│   └── github
├── Social
│   ├── hackernews
│   └── twitter
└── VPS
└── linode
```
In order to paste them into the browser, you'd need to decrypt the password using your hardware device:
```
$ pass --clip VPS/linode
Copied VPS/linode to clipboard. Will clear in 45 seconds.
```

View File

@@ -14,34 +14,56 @@ See SatoshiLabs' blog posts about this feature:
## Installation
First, make sure that the latest [trezorlib](https://pypi.python.org/pypi/trezor) Python package
is installed correctly (at least v0.6.6):
Install the following packages:
$ apt-get install python-dev libusb-1.0-0-dev libudev-dev
$ pip install -U setuptools pip
$ pip install Cython trezor
Make sure you are running the latest firmware version on your hardware device.
Currently the following firmware versions are supported:
* [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+`
### TREZOR
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:
$ pip install trezor_agent
Or, directly from the latest source code (if `pip` doesn't work for you):
Or, directly from the latest source code:
$ git clone https://github.com/romanz/trezor-agent && cd trezor-agent
$ python setup.py build && python setup.py install
Finally, verify that you are running the latest [TREZOR firmware](https://wallet.mytrezor.com/data/firmware/releases.json) version (at least v1.4.0):
$ trezorctl get_features | head
vendor: "bitcointrezor.com"
major_version: 1
minor_version: 4
patch_version: 0
...
$ git clone https://github.com/romanz/trezor-agent
$ pip install --user -e trezor-agent/agents/trezor
If you have an error regarding `protobuf` imports (after installing it), please see [this issue](https://github.com/romanz/trezor-agent/issues/28).
### KeepKey
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
Or, directly from the latest source code:
$ git clone https://github.com/romanz/trezor-agent
$ pip install --user -e trezor-agent/agents/keepkey
### Ledger Nano S
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:
$ pip install ledger_agent
Or, directly from the latest source code:
$ git clone https://github.com/romanz/trezor-agent
$ pip install --user -e trezor-agent/agents/ledger
## Usage
For SSH, see the [following instructions](README-SSH.md) (for Windows support,

View File

@@ -0,0 +1,5 @@
import libagent.gpg
import libagent.ssh
from libagent.device import keepkey
ssh_agent = lambda: libagent.ssh.main(keepkey.KeepKey)

38
agents/keepkey/setup.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='keepkey_agent',
version='0.9.0',
description='Using KeepKey as hardware SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
url='http://github.com/romanz/trezor-agent',
scripts=['keepkey_agent.py'],
install_requires=[
'libagent>=0.9.0',
'keepkey>=0.7.3'
],
platforms=['POSIX'],
classifiers=[
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'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',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'keepkey-agent = keepkey_agent:ssh_agent',
]},
)

View File

@@ -0,0 +1,7 @@
import libagent.gpg
import libagent.ssh
from libagent.device.ledger import LedgerNanoS as DeviceType
ssh_agent = lambda: libagent.ssh.main(DeviceType)
gpg_tool = lambda: libagent.gpg.main(DeviceType)
gpg_agent = lambda: libagent.gpg.run_agent(DeviceType)

40
agents/ledger/setup.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='ledger_agent',
version='0.9.0',
description='Using Ledger as hardware SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
url='http://github.com/romanz/trezor-agent',
scripts=['ledger_agent.py'],
install_requires=[
'libagent>=0.9.0',
'ledgerblue>=0.1.8'
],
platforms=['POSIX'],
classifiers=[
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'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',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'ledger-agent = ledger_agent:ssh_agent',
'ledger-gpg = ledger_agent:gpg_tool',
'ledger-gpg-agent = ledger_agent:gpg_agent',
]},
)

40
agents/trezor/setup.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='trezor_agent',
version='0.9.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.9.0',
'trezor>=0.7.6'
],
platforms=['POSIX'],
classifiers=[
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'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',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'trezor-agent = trezor_agent:ssh_agent',
'trezor-gpg = trezor_agent:gpg_tool',
'trezor-gpg-agent = trezor_agent:gpg_agent',
]},
)

View File

@@ -0,0 +1,7 @@
import libagent.gpg
import libagent.ssh
from libagent.device.trezor import Trezor as DeviceType
ssh_agent = lambda: libagent.ssh.main(DeviceType)
gpg_tool = lambda: libagent.gpg.main(DeviceType)
gpg_agent = lambda: libagent.gpg.run_agent(DeviceType)

View File

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

View File

@@ -54,7 +54,7 @@ class NotFoundError(Error):
class DeviceError(Error):
""""Error during device operation."""
"""Error during device operation."""
class Identity(object):
@@ -82,7 +82,7 @@ class Identity(object):
s = io.BytesIO(bytearray(digest))
hardened = 0x80000000
addr_0 = [13, 17][bool(ecdh)]
addr_0 = 17 if bool(ecdh) else 13
address_n = [addr_0] + list(util.recv(s, '<LLLL'))
return [(hardened | value) for value in address_n]

View File

@@ -1,9 +1,9 @@
"""KeepKey-related definitions."""
# pylint: disable=unused-import
# pylint: disable=unused-import,import-error
from keepkeylib.client import CallException as Error
from keepkeylib.client import KeepKeyClient as Client
from keepkeylib.messages_pb2 import PassphraseAck
from keepkeylib.transport_hid import HidTransport
from keepkeylib.transport_hid import HidTransport as Transport
from keepkeylib.types_pb2 import IdentityType

View File

@@ -4,7 +4,7 @@ import binascii
import logging
import struct
from ledgerblue import comm
from ledgerblue import comm # pylint: disable=import-error
from . import interface

View File

@@ -17,6 +17,10 @@ class Trezor(interface.Device):
@property
def _defs(self):
from . import trezor_defs
# Allow using TREZOR bridge transport (instead of the HID default)
trezor_defs.Transport = {
'bridge': trezor_defs.BridgeTransport,
}.get(os.environ.get('TREZOR_TRANSPORT'), trezor_defs.HidTransport)
return trezor_defs
required_version = '>=1.4.0'
@@ -29,9 +33,9 @@ class Trezor(interface.Device):
'non-empty' if self.passphrase else 'empty', self)
return self._defs.PassphraseAck(passphrase=self.passphrase)
for d in self._defs.HidTransport.enumerate():
for d in self._defs.Transport.enumerate():
log.debug('endpoint: %s', d)
transport = self._defs.HidTransport(d)
transport = self._defs.Transport(d)
connection = self._defs.Client(transport)
connection.callback_PassphraseRequest = passphrase_handler
f = connection.features

View File

@@ -1,9 +1,10 @@
"""TREZOR-related definitions."""
# pylint: disable=unused-import
# pylint: disable=unused-import,import-error
from trezorlib.client import CallException as Error
from trezorlib.client import TrezorClient as Client
from trezorlib.messages_pb2 import PassphraseAck
from trezorlib.transport_bridge import BridgeTransport
from trezorlib.transport_hid import HidTransport
from trezorlib.types_pb2 import IdentityType

View File

@@ -1,5 +1,13 @@
#!/usr/bin/env python
"""Create signatures and export public keys for GPG using TREZOR."""
"""
TREZOR support for ECDSA GPG signatures.
See these links for more details:
- https://www.gnupg.org/faq/whats-new-in-2.1.html
- https://tools.ietf.org/html/rfc4880
- https://tools.ietf.org/html/rfc6637
- https://tools.ietf.org/html/draft-irtf-cfrg-eddsa-05
"""
import argparse
import contextlib
import logging
@@ -15,14 +23,15 @@ from .. import device, formats, server, util
log = logging.getLogger(__name__)
def run_create(args):
def export_public_key(device_type, args):
"""Generate a new pubkey for a new/existing GPG identity."""
log.warning('NOTE: in order to re-generate the exact same GPG key later, '
'run this command with "--time=%d" commandline flag (to set '
'the timestamp of the GPG key manually).', args.time)
d = client.Client(user_id=args.user_id, curve_name=args.ecdsa_curve)
verifying_key = d.pubkey(ecdh=False)
decryption_key = d.pubkey(ecdh=True)
c = client.Client(user_id=args.user_id, curve_name=args.ecdsa_curve,
device_type=device_type)
verifying_key = c.pubkey(ecdh=False)
decryption_key = c.pubkey(ecdh=True)
if args.subkey: # add as subkey
log.info('adding %s GPG subkey for "%s" to existing key',
@@ -38,10 +47,10 @@ def run_create(args):
primary_bytes = keyring.export_public_key(args.user_id)
result = encode.create_subkey(primary_bytes=primary_bytes,
subkey=signing_key,
signer_func=d.sign)
signer_func=c.sign)
result = encode.create_subkey(primary_bytes=result,
subkey=encryption_key,
signer_func=d.sign)
signer_func=c.sign)
else: # add as primary
log.info('creating new %s GPG primary key for "%s"',
args.ecdsa_curve, args.user_id)
@@ -56,24 +65,16 @@ def run_create(args):
result = encode.create_primary(user_id=args.user_id,
pubkey=primary,
signer_func=d.sign)
signer_func=c.sign)
result = encode.create_subkey(primary_bytes=result,
subkey=subkey,
signer_func=d.sign)
signer_func=c.sign)
sys.stdout.write(protocol.armor(result, 'PUBLIC KEY BLOCK'))
def main_create():
"""Main function for GPG identity creation."""
p = argparse.ArgumentParser()
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')
args = p.parse_args()
def run_create(device_type, args):
"""Export public GPG key."""
util.setup_logging(verbosity=args.verbose)
log.warning('This GPG tool is still in EXPERIMENTAL mode, '
'so please note that the API and features may '
@@ -82,19 +83,27 @@ def main_create():
existing_gpg = keyring.gpg_version().decode('ascii')
required_gpg = '>=2.1.11'
if semver.match(existing_gpg, required_gpg):
run_create(args)
export_public_key(device_type, args)
else:
log.error('Existing gpg2 has version "%s" (%s required)',
existing_gpg, required_gpg)
def main_agent():
def run_unlock(device_type, args):
"""Unlock hardware device (for future interaction)."""
util.setup_logging(verbosity=args.verbose)
with device_type() as d:
log.info('unlocked %s device', d)
def run_agent(device_type):
"""Run a simple GPG-agent server."""
home_dir = os.environ.get('GNUPGHOME', os.path.expanduser('~/.gnupg/trezor'))
config_file = os.path.join(home_dir, 'gpg-agent.conf')
if not os.path.exists(config_file):
msg = 'No configuration file found: {}'.format(config_file)
raise IOError(msg)
parser = argparse.ArgumentParser()
parser.add_argument('--homedir', default=os.environ.get('GNUPGHOME'))
args, _ = parser.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('#'))
@@ -107,7 +116,7 @@ def main_agent():
for conn in agent.yield_connections(sock):
with contextlib.closing(conn):
try:
agent.handle_connection(conn)
agent.handle_connection(conn=conn, device_type=device_type)
except StopIteration:
log.info('stopping gpg-agent')
return
@@ -115,12 +124,22 @@ def main_agent():
log.exception('gpg-agent failed: %s', e)
def auto_unlock():
"""Automatically unlock first found device (used for `gpg-shell`)."""
p = argparse.ArgumentParser()
p.add_argument('-v', '--verbose', default=0, action='count')
def main(device_type):
"""Parse command-line arguments."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
args = p.parse_args()
util.setup_logging(verbosity=args.verbose)
d = device.detect()
log.info('unlocked %s device', d)
p = subparsers.add_parser('create', help='Export public GPG key')
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.set_defaults(func=run_create)
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()
return args.func(device_type=device_type, args=args)

View File

@@ -36,7 +36,7 @@ def sig_encode(r, s):
return b'(7:sig-val(5:ecdsa(1:r32:' + r + b')(1:s32:' + s + b')))'
def open_connection(keygrip_bytes):
def open_connection(keygrip_bytes, device_type):
"""
Connect to the device for the specified keygrip.
@@ -51,7 +51,7 @@ def open_connection(keygrip_bytes):
curve_name = protocol.get_curve_name_by_oid(pubkey_dict['curve_oid'])
ecdh = (pubkey_dict['algo'] == protocol.ECDH_ALGO_ID)
conn = client.Client(user_id, curve_name=curve_name)
conn = client.Client(user_id, curve_name=curve_name, device_type=device_type)
pubkey = protocol.PublicKey(
curve_name=curve_name, created=pubkey_dict['created'],
verifying_key=conn.pubkey(ecdh=ecdh), ecdh=ecdh)
@@ -60,11 +60,11 @@ def open_connection(keygrip_bytes):
return conn
def pksign(keygrip, digest, algo):
def pksign(keygrip, digest, algo, device_type):
"""Sign a message digest using a private EC key."""
log.debug('signing %r digest (algo #%s)', digest, algo)
keygrip_bytes = binascii.unhexlify(keygrip)
conn = open_connection(keygrip_bytes)
conn = open_connection(keygrip_bytes, device_type=device_type)
r, s = conn.sign(binascii.unhexlify(digest))
result = sig_encode(r, s)
log.debug('result: %r', result)
@@ -92,7 +92,7 @@ def parse_ecdh(line):
return dict(items)[b'e']
def pkdecrypt(keygrip, conn):
def pkdecrypt(keygrip, conn, device_type):
"""Handle decryption using ECDH."""
for msg in [b'S INQUIRE_MAXLEN 4096', b'INQUIRE CIPHERTEXT']:
keyring.sendline(conn, msg)
@@ -102,15 +102,16 @@ def pkdecrypt(keygrip, conn):
remote_pubkey = parse_ecdh(line)
keygrip_bytes = binascii.unhexlify(keygrip)
conn = open_connection(keygrip_bytes)
conn = open_connection(keygrip_bytes, device_type=device_type)
return _serialize_point(conn.ecdh(remote_pubkey))
@util.memoize
def have_key(keygrip):
def have_key(keygrip, device_type):
"""Check if current keygrip correspond to a TREZOR-based key."""
try:
open_connection(keygrip_bytes=binascii.unhexlify(keygrip))
open_connection(keygrip_bytes=binascii.unhexlify(keygrip),
device_type=device_type)
return True
except KeyError as e:
log.warning('HAVEKEY(%s) failed: %s', keygrip, e)
@@ -118,7 +119,7 @@ def have_key(keygrip):
# pylint: disable=too-many-branches
def handle_connection(conn):
def handle_connection(conn, device_type):
"""Handle connection from GPG binary using the ASSUAN protocol."""
keygrip = None
digest = None
@@ -141,13 +142,13 @@ def handle_connection(conn):
elif command == b'SETHASH':
algo, digest = args
elif command == b'PKSIGN':
sig = pksign(keygrip, digest, algo)
sig = pksign(keygrip, digest, algo, device_type=device_type)
keyring.sendline(conn, b'D ' + sig)
elif command == b'PKDECRYPT':
sec = pkdecrypt(keygrip, conn)
sec = pkdecrypt(keygrip, conn, device_type=device_type)
keyring.sendline(conn, b'D ' + sec)
elif command == b'HAVEKEY':
if not have_key(keygrip=args[0]):
if not have_key(keygrip=args[0], device_type=device_type):
keyring.sendline(conn,
b'ERR 67108881 No secret key <GPG Agent>')
return

View File

@@ -10,9 +10,9 @@ log = logging.getLogger(__name__)
class Client(object):
"""Sign messages and get public keys from a hardware device."""
def __init__(self, user_id, curve_name):
def __init__(self, user_id, curve_name, device_type):
"""Connect to the device and retrieve required public key."""
self.device = device.detect()
self.device = device_type()
self.user_id = user_id
self.identity = device.interface.Identity(
identity_str='gpg://', curve_name=curve_name)

View File

@@ -41,8 +41,7 @@ def public_key_path(request):
def test_gpg_files(public_key_path): # pylint: disable=redefined-outer-name
with open(public_key_path, 'rb') as f:
packets = list(decode.parse_packets(f))
assert len(packets) > 0
assert list(decode.parse_packets(f))
def test_has_custom_subpacket():

View File

@@ -1,19 +1,15 @@
"""UNIX-domain socket server for ssh-agent implementation."""
import contextlib
import functools
import logging
import os
import socket
import subprocess
import tempfile
import threading
from . import util
log = logging.getLogger(__name__)
UNIX_SOCKET_TIMEOUT = 0.1
def remove_file(path, remove=os.remove, exists=os.path.exists):
"""Remove file, and raise OSError if still exists."""
@@ -114,39 +110,6 @@ def spawn(func, kwargs):
t.join()
@contextlib.contextmanager
def serve(handler, sock_path=None, timeout=UNIX_SOCKET_TIMEOUT):
"""
Start the ssh-agent server on a UNIX-domain socket.
If no connection is made during the specified timeout,
retry until the context is over.
"""
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 unix_domain_socket_server(sock_path) as sock:
sock.settimeout(timeout)
quit_event = threading.Event()
handle_conn = functools.partial(handle_connection,
handler=handler,
mutex=device_mutex)
kwargs = dict(sock=sock,
handle_conn=handle_conn,
quit_event=quit_event)
with spawn(server_thread, kwargs):
try:
yield environ
finally:
log.debug('closing server')
quit_event.set()
def run_process(command, environ):
"""
Run the specified process and wait until it finishes.

View File

@@ -1,16 +1,24 @@
"""SSH-agent implementation using hardware authentication devices."""
import argparse
import contextlib
import functools
import io
import logging
import os
import re
import subprocess
import sys
import tempfile
import threading
from . import client, device, formats, protocol, server, util
from .. import device, formats, server, util
from . import client, protocol
log = logging.getLogger(__name__)
UNIX_SOCKET_TIMEOUT = 0.1
def ssh_args(label):
"""Create SSH command for connecting specified server."""
@@ -40,8 +48,8 @@ def mosh_args(label):
return args
def create_parser():
"""Create argparse.ArgumentParser for this tool."""
def create_agent_parser():
"""Create an ArgumentParser for this tool."""
p = argparse.ArgumentParser()
p.add_argument('-v', '--verbose', default=0, action='count')
@@ -51,16 +59,10 @@ def create_parser():
default=formats.CURVE_NIST256,
help='specify ECDSA curve name: ' + curve_names)
p.add_argument('--timeout',
default=server.UNIX_SOCKET_TIMEOUT, type=float,
default=UNIX_SOCKET_TIMEOUT, type=float,
help='Timeout for accepting SSH client connections')
p.add_argument('--debug', default=False, action='store_true',
help='Log SSH protocol messages for debugging.')
return p
def create_agent_parser():
"""Specific parser for SSH connection."""
p = create_parser()
g = p.add_mutually_exclusive_group()
g.add_argument('-s', '--shell', default=False, action='store_true',
@@ -77,44 +79,44 @@ def create_agent_parser():
return p
def create_git_parser():
"""Specific parser for git commands."""
p = create_parser()
@contextlib.contextmanager
def serve(handler, sock_path=None, timeout=UNIX_SOCKET_TIMEOUT):
"""
Start the ssh-agent server on a UNIX-domain socket.
p.add_argument('-r', '--remote', default='origin',
help='use this git remote URL to generate SSH identity')
p.add_argument('-t', '--test', action='store_true',
help='test connection using `ssh -T user@host` command')
p.add_argument('command', type=str, nargs='*', metavar='ARGUMENT',
help='Git command to run under the SSH agent')
return p
If no connection is made during the specified timeout,
retry until the context is over.
"""
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-')
def git_host(remote_name, attributes):
"""Extract git SSH host for specified remote name."""
try:
output = subprocess.check_output('git config --local --list'.split())
except subprocess.CalledProcessError:
return
for attribute in attributes:
name = r'remote.{0}.{1}'.format(remote_name, attribute)
matches = re.findall(re.escape(name) + '=(.*)', output)
log.debug('%r: %r', name, matches)
if not matches:
continue
url = matches[0].strip()
match = re.match('(?P<user>.*?)@(?P<host>.*?):(?P<path>.*)', url)
if match:
return '{user}@{host}'.format(**match.groupdict())
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:
sock.settimeout(timeout)
quit_event = threading.Event()
handle_conn = functools.partial(server.handle_connection,
handler=handler,
mutex=device_mutex)
kwargs = dict(sock=sock,
handle_conn=handle_conn,
quit_event=quit_event)
with server.spawn(server.server_thread, kwargs):
try:
yield environ
finally:
log.debug('closing server')
quit_event.set()
def run_server(conn, command, debug, timeout):
"""Common code for run_agent and run_git below."""
try:
handler = protocol.Handler(conn=conn, debug=debug)
with server.serve(handler=handler, timeout=timeout) as env:
with serve(handler=handler, timeout=timeout) as env:
return server.run_process(command=command, environ=env)
except KeyboardInterrupt:
log.info('server stopped')
@@ -127,31 +129,41 @@ def handle_connection_error(func):
try:
return func(*args, **kwargs)
except IOError as e:
log.error('Connection error: %s', e)
log.error('Connection error (try unplugging and replugging your device): %s', e)
return 1
return wrapper
def parse_config(fname):
def parse_config(contents):
"""Parse config file into a list of Identity objects."""
contents = open(fname).read()
for identity_str, curve_name in re.findall(r'\<(.*?)\|(.*?)\>', contents):
yield device.interface.Identity(identity_str=identity_str,
curve_name=curve_name)
def import_public_keys(contents):
"""Load (previously exported) SSH public keys from a file's contents."""
for line in io.StringIO(contents):
# Verify this line represents valid SSH public key
formats.import_public_key(line)
yield line
class JustInTimeConnection(object):
"""Connect to the device just before the needed operation."""
def __init__(self, conn_factory, identities):
def __init__(self, conn_factory, identities, public_keys=None):
"""Create a JIT connection object."""
self.conn_factory = conn_factory
self.identities = identities
self.public_keys_cache = public_keys
def public_keys(self):
"""Return a list of SSH public keys (in textual format)."""
conn = self.conn_factory()
return [conn.get_public_key(i) for i in self.identities]
if not self.public_keys_cache:
conn = self.conn_factory()
self.public_keys_cache = conn.export_public_keys(self.identities)
return self.public_keys_cache
def parse_public_keys(self):
"""Parse SSH public keys into dictionaries."""
@@ -168,13 +180,19 @@ class JustInTimeConnection(object):
@handle_connection_error
def run_agent(client_factory=client.Client):
def main(device_type):
"""Run ssh-agent using given hardware client factory."""
args = create_agent_parser().parse_args()
util.setup_logging(verbosity=args.verbose)
public_keys = None
if args.identity.startswith('/'):
identities = list(parse_config(fname=args.identity))
filename = args.identity
contents = open(filename, 'rb').read().decode('ascii')
# Allow loading previously exported SSH public keys
if filename.endswith('.pub'):
public_keys = list(import_public_keys(contents))
identities = list(parse_config(contents))
else:
identities = [device.interface.Identity(
identity_str=args.identity, curve_name=args.ecdsa_curve_name)]
@@ -194,8 +212,8 @@ def run_agent(client_factory=client.Client):
command = os.environ['SHELL']
conn = JustInTimeConnection(
conn_factory=lambda: client_factory(device.detect()),
identities=identities)
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)

View File

@@ -18,15 +18,17 @@ class Client(object):
"""Connect to hardware device."""
self.device = device
def get_public_key(self, identity):
"""Get SSH public key from the device."""
def export_public_keys(self, identities):
"""Export SSH public keys from the device."""
public_keys = []
with self.device:
pubkey = self.device.pubkey(identity)
vk = formats.decompress_pubkey(pubkey=pubkey,
curve_name=identity.curve_name)
return formats.export_public_key(vk=vk,
label=str(identity))
for i in identities:
pubkey = self.device.pubkey(identity=i)
vk = formats.decompress_pubkey(pubkey=pubkey,
curve_name=i.curve_name)
public_keys.append(formats.export_public_key(vk=vk,
label=str(i)))
return public_keys
def sign_ssh_challenge(self, blob, identity):
"""Sign given blob using a private key on the device."""

View File

@@ -49,7 +49,7 @@ def test_ssh_agent():
identity = device.interface.Identity(identity_str='localhost:22',
curve_name=CURVE)
c = client.Client(device=MockDevice())
assert c.get_public_key(identity) == PUBKEY_TEXT
assert c.export_public_keys([identity]) == [PUBKEY_TEXT]
signature = c.sign_ssh_challenge(blob=BLOB, identity=identity)
key = formats.import_public_key(PUBKEY_TEXT)

View File

@@ -0,0 +1 @@
"""Unit-tests for this package."""

View File

@@ -8,7 +8,8 @@ import threading
import mock
import pytest
from .. import protocol, server, util
from .. import server, util
from ..ssh import protocol
def test_socket():
@@ -117,12 +118,6 @@ def test_run():
server.run_process([''], environ={})
def test_serve_main():
handler = protocol.Handler(conn=empty_device())
with server.serve(handler=handler, sock_path=None):
pass
def test_remove():
path = 'foo.bar'

View File

@@ -179,13 +179,22 @@ class Reader(object):
self._captured = None
def setup_logging(verbosity, **kwargs):
def setup_logging(verbosity, filename=None):
"""Configure logging for this tool."""
fmt = ('%(asctime)s %(levelname)-12s %(message)-100s '
'[%(filename)s:%(lineno)d]')
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
level = levels[min(verbosity, len(levels) - 1)]
logging.basicConfig(format=fmt, level=level, **kwargs)
logging.root.setLevel(level)
fmt = logging.Formatter('%(asctime)s %(levelname)-12s %(message)-100s '
'[%(filename)s:%(lineno)d]')
hdlr = logging.StreamHandler() # stderr
hdlr.setFormatter(fmt)
logging.root.addHandler(hdlr)
if filename:
hdlr = logging.FileHandler(filename, 'a')
hdlr.setFormatter(fmt)
logging.root.addHandler(hdlr)
def memoize(func):

View File

@@ -4,31 +4,52 @@ set -eu
gpg2 --version >/dev/null # verify that GnuPG 2 is installed
USER_ID="${1}"
HOMEDIR=~/.gnupg/trezor
DEVICE=${DEVICE:="trezor"} # or "ledger"
CURVE=${CURVE:="nist256p1"} # or "ed25519"
TIMESTAMP=${TIMESTAMP:=`date +%s`} # key creation timestamp
HOMEDIR=~/.gnupg/${DEVICE}
# Prepare new GPG home directory for TREZOR-based identity
# Prepare new GPG home directory for hardware-based identity
rm -rf "${HOMEDIR}"
mkdir -p "${HOMEDIR}"
chmod 700 "${HOMEDIR}"
# Generate new GPG identity and import into GPG keyring
trezor-gpg-create -v "${USER_ID}" -t "${TIMESTAMP}" -e "${CURVE}" > "${HOMEDIR}/pubkey.asc"
gpg2 --homedir "${HOMEDIR}" --import < "${HOMEDIR}/pubkey.asc"
$DEVICE-gpg create -v "${USER_ID}" -t "${TIMESTAMP}" -e "${CURVE}" > "${HOMEDIR}/pubkey.asc"
gpg2 --homedir "${HOMEDIR}" --import < "${HOMEDIR}/pubkey.asc" 2> /dev/null
rm -f "${HOMEDIR}/S.gpg-agent" # (otherwise, our agent won't be started automatically)
# Make new GPG identity with "ultimate" trust (via its fingerprint)
FINGERPRINT=$(gpg2 --homedir "${HOMEDIR}" --list-public-keys --with-fingerprint --with-colons | sed -n -E 's/^fpr:::::::::([0-9A-F]+):$/\1/p' | head -n1)
echo "${FINGERPRINT}:6" | gpg2 --homedir "${HOMEDIR}" --import-ownertrust
echo "${FINGERPRINT}:6" | gpg2 --homedir "${HOMEDIR}" --import-ownertrust 2> /dev/null
AGENT_PATH="$(which ${DEVICE}-gpg-agent)"
# Prepare GPG configuration file
echo "# TREZOR-based GPG configuration
agent-program $(which trezor-gpg-agent)
echo "# Hardware-based GPG configuration
agent-program ${AGENT_PATH}
personal-digest-preferences SHA512
" | tee "${HOMEDIR}/gpg.conf"
" > "${HOMEDIR}/gpg.conf"
echo "# TREZOR-based GPG agent emulator
# Prepare GPG agent configuration file
echo "# Hardware-based GPG agent emulator
log-file ${HOMEDIR}/gpg-agent.log
verbosity 2
" | tee "${HOMEDIR}/gpg-agent.conf"
" > "${HOMEDIR}/gpg-agent.conf"
# Prepare a helper script for setting up the new identity
echo "#!/bin/bash
set -eu
export GNUPGHOME=${HOMEDIR}
COMMAND=\$*
if [ -z \"\${COMMAND}\" ]
then
\${SHELL}
else
\${COMMAND}
fi
" > "${HOMEDIR}/env"
chmod u+x "${HOMEDIR}/env"
# Load agent and make sure it responds with the new identity
GNUPGHOME="$HOMEDIR" gpg2 -K 2> /dev/null

View File

@@ -1,28 +0,0 @@
#!/bin/bash
set -eu
gpg2 --version >/dev/null # verify that GnuPG 2 is installed
export GNUPGHOME=~/.gnupg/trezor
CONFIG_PATH="${GNUPGHOME}/gpg-agent.conf"
if [ ! -f ${CONFIG_PATH} ]
then
echo "No configuration found: ${CONFIG_PATH}"
exit 1
fi
# Make sure that the device is unlocked before starting the shell
trezor-gpg-unlock
# Make sure TREZOR-based gpg-agent is running
gpg-connect-agent --agent-program "$(which trezor-gpg-agent)" </dev/null
COMMAND=$*
if [ -z "${COMMAND}" ]
then
gpg2 --list-public-keys
${SHELL}
else
${COMMAND}
fi

26
setup.py Normal file → Executable file
View File

@@ -2,17 +2,22 @@
from setuptools import setup
setup(
name='trezor_agent',
version='0.8.3',
description='Using Trezor as hardware SSH agent',
name='libagent',
version='0.9.1',
description='Using hardware wallets as SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
url='http://github.com/romanz/trezor-agent',
packages=['trezor_agent', 'trezor_agent.device', 'trezor_agent.gpg'],
packages=[
'libagent',
'libagent.device',
'libagent.gpg',
'libagent.ssh'
],
install_requires=[
'ecdsa>=0.13', 'ed25519>=1.4', 'Cython>=0.23.4', 'protobuf>=3.0.0', 'semver>=2.2',
'trezor>=0.7.6', 'keepkey>=0.7.3', 'ledgerblue>=0.1.8',
'hidapi==0.7.99.post15' # until https://github.com/keepkey/python-keepkey/pull/8 is merged
'ecdsa>=0.13',
'ed25519>=1.4',
'semver>=2.2'
],
platforms=['POSIX'],
classifiers=[
@@ -26,16 +31,11 @@ setup(
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'trezor-agent = trezor_agent.__main__:run_agent',
'trezor-gpg-create = trezor_agent.gpg.__main__:main_create',
'trezor-gpg-agent = trezor_agent.gpg.__main__:main_agent',
'trezor-gpg-unlock = trezor_agent.gpg.__main__:auto_unlock',
]},
)

12
tox.ini
View File

@@ -2,6 +2,8 @@
envlist = py27,py3
[pep8]
max-line-length = 100
[pep257]
add-ignore = D401
[testenv]
deps=
pytest
@@ -13,10 +15,10 @@ deps=
pydocstyle
isort
commands=
pep8 trezor_agent
isort --skip-glob .tox -c -r trezor_agent
pylint --reports=no --rcfile .pylintrc trezor_agent
pydocstyle trezor_agent
coverage run --omit='trezor_agent/__main__.py' --source trezor_agent -m py.test -v trezor_agent
pep8 libagent
isort --skip-glob .tox -c -r libagent
pylint --reports=no --rcfile .pylintrc libagent
pydocstyle libagent
coverage run --source libagent -m py.test -v libagent
coverage report
coverage html

View File

@@ -1,27 +0,0 @@
"""Cryptographic hardware device management."""
import logging
from . import trezor
from . import keepkey
from . import ledger
from . import interface
log = logging.getLogger(__name__)
DEVICE_TYPES = [
trezor.Trezor,
keepkey.KeepKey,
ledger.LedgerNanoS,
]
def detect():
"""Detect the first available device and return it to the user."""
for device_type in DEVICE_TYPES:
try:
with device_type() as d:
return d
except interface.NotFoundError as e:
log.debug('device not found: %s', e)
raise IOError('No device found!')

View File

@@ -1,9 +0,0 @@
"""
TREZOR support for ECDSA GPG signatures.
See these links for more details:
- https://www.gnupg.org/faq/whats-new-in-2.1.html
- https://tools.ietf.org/html/rfc4880
- https://tools.ietf.org/html/rfc6637
- https://tools.ietf.org/html/draft-irtf-cfrg-eddsa-05
"""