mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85274d8374 | ||
|
|
f358ca29d4 | ||
|
|
53d43cba29 | ||
|
|
214b556f83 | ||
|
|
051a3fd4ab | ||
|
|
91050ee64a | ||
|
|
257992d04c | ||
|
|
6c2273387d | ||
|
|
b6ad8207ba | ||
|
|
3a93fc859d | ||
|
|
7d9b3ff1d0 | ||
|
|
4af881b3cb | ||
|
|
eb525e1b62 | ||
|
|
02c8e729b7 | ||
|
|
12359938ad | ||
|
|
93cd3e688b | ||
|
|
26d7dd3124 | ||
|
|
0d5c3a9ca7 | ||
|
|
97ec6b2719 | ||
|
|
8ba9be1780 | ||
|
|
b2bc87c0c7 | ||
|
|
d522d148ef | ||
|
|
c796a3b01d |
@@ -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
|
||||
|
||||
16
.travis.yml
16
.travis.yml
@@ -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
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
52
README.md
52
README.md
@@ -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,
|
||||
|
||||
5
agents/keepkey/keepkey_agent.py
Normal file
5
agents/keepkey/keepkey_agent.py
Normal 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
38
agents/keepkey/setup.py
Normal 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',
|
||||
]},
|
||||
)
|
||||
7
agents/ledger/ledger_agent.py
Normal file
7
agents/ledger/ledger_agent.py
Normal 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
40
agents/ledger/setup.py
Normal 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
40
agents/trezor/setup.py
Normal 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',
|
||||
]},
|
||||
)
|
||||
7
agents/trezor/trezor_agent.py
Normal file
7
agents/trezor/trezor_agent.py
Normal 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)
|
||||
3
libagent/device/__init__.py
Normal file
3
libagent/device/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Cryptographic hardware device management."""
|
||||
|
||||
from . import interface
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
91
trezor_agent/gpg/__main__.py → libagent/gpg/__init__.py
Executable file → Normal file
91
trezor_agent/gpg/__main__.py → libagent/gpg/__init__.py
Executable file → Normal 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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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():
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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."""
|
||||
@@ -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)
|
||||
1
libagent/tests/__init__.py
Normal file
1
libagent/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Unit-tests for this package."""
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
@@ -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
|
||||
|
||||
@@ -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
26
setup.py
Normal file → Executable 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
12
tox.ini
@@ -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
|
||||
|
||||
@@ -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!')
|
||||
@@ -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
|
||||
"""
|
||||
Reference in New Issue
Block a user