Compare commits

..

No commits in common. 'master' and 'v0.11.3' have entirely different histories.

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

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

@ -1,15 +0,0 @@
#!/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, 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.
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.
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 hidapi
$ pip3 install Cython
$ pip3 install trezor_agent
```

@ -1,31 +0,0 @@
# 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,7 +32,6 @@ $ (trezor|keepkey|ledger)-agent identity@myhost -- COMMAND --WITH --ARGUMENTS
```
to start the agent in the background and execute the command with environment variables set up to use the SSH agent. The specified identity is used for all SSH connections. The agent will exit after the command completes.
Note the `--` separator, which is used to separate `trezor-agent`'s arguments from the SSH command arguments.
As a shortcut you can run
@ -85,29 +84,21 @@ would allow you to login using the corresponding private key signature.
### Access remote Git/Mercurial repositories
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/)):
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/)):
$ 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
$ trezor-agent -v -e ed25519 git@github.com | xclip
Use the following Bash alias for convenient Git operations:
$ alias ssh-shell='trezor-agent ~/.ssh/github.pub -v --shell'
$ alias git_hub='trezor-agent -v -e ed25519 git@github.com -- git'
Now, you can use regular Git commands under the "SSH-enabled" sub-shell:
Replace `git` with `git_hub` for remote operations:
$ ssh-shell
$ git push origin master
$ git_hub push origin master
The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html)):
$ ssh-shell
$ hg push
$ trezor-agent -v -e ed25519 git@bitbucket.org -- hg push
### Start the agent as a systemd unit
@ -123,7 +114,7 @@ Description=trezor-agent SSH agent
Requires=trezor-ssh-agent.socket
[Service]
Type=simple
Type=Simple
Environment="DISPLAY=:0"
Environment="PATH=/bin:/usr/bin:/usr/local/bin:%h/.local/bin"
ExecStart=/usr/bin/trezor-agent --foreground --sock-path %t/trezor-agent/S.ssh IDENTITY
@ -133,14 +124,6 @@ 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.
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`
````

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

@ -9,6 +9,6 @@ from keepkeylib.transport_hid import HidTransport
from keepkeylib.types_pb2 import IdentityType
def find_device():
"""Returns first USB HID transport."""
return next(HidTransport(p) for p in HidTransport.enumerate())
def enumerate_transports():
"""Returns USB HID transports."""
return [HidTransport(p) for p in HidTransport.enumerate()]

@ -106,13 +106,13 @@ class Trezor(interface.Device):
def connect(self):
"""Enumerate and connect to the first available interface."""
transport = self._defs.find_device()
if not transport:
transports = self._defs.enumerate_transports()
if not transports:
raise interface.NotFoundError('{} not connected'.format(self))
log.debug('using transport: %s', transport)
log.debug('transports: %s', transports)
for _ in range(5): # Retry a few times in case of PIN failures
connection = self._defs.Client(transport=transport,
connection = self._defs.Client(transport=transports[0],
state=self.__class__.cached_state)
self._override_pin_handler(connection)
self._override_passphrase_handler(connection)

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

@ -9,7 +9,7 @@ from .. import util
log = logging.getLogger(__name__)
class UI:
class UI(object):
"""UI for PIN/passphrase entry (for TREZOR devices)."""
def __init__(self, device_type, config=None):

@ -86,8 +86,7 @@ def verify_gpg_version():
required_gpg = '>=2.1.11'
msg = 'Existing GnuPG has version "{}" ({} required)'.format(existing_gpg,
required_gpg)
if not semver.match(existing_gpg, required_gpg):
log.error(msg)
assert semver.match(existing_gpg, required_gpg), msg
def check_output(args):
@ -180,23 +179,22 @@ 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(keyring.gpg_command(['--homedir', homedir, verbosity,
'--import', pubkey.name]))
check_call([gpg_binary, '--homedir', homedir, verbosity,
'--import', pubkey.name])
# Make new GPG identity with "ultimate" trust (via its fingerprint)
out = check_output(keyring.gpg_command(['--homedir', homedir,
'--list-public-keys',
'--with-fingerprint',
'--with-colons']))
out = check_output([gpg_binary, '--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(keyring.gpg_command(['--homedir', homedir,
'--import-ownertrust', f.name]))
check_call([gpg_binary, '--homedir', homedir,
'--import-ownertrust', f.name])
# Load agent and make sure it responds with the new identity
check_call(keyring.gpg_command(['--list-secret-keys', args.user_id,
'--homedir', homedir]))
check_call([gpg_binary, '--list-secret-keys', args.user_id],
env={'GNUPGHOME': homedir})
def run_unlock(device_type, args):
@ -206,26 +204,11 @@ 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.')
@ -245,42 +228,32 @@ 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, 'PATH': os.environ['PATH']}
env = {'GNUPGHOME': args.homedir}
sock_path = keyring.get_agent_sock_path(env=env)
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))
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:
with server.unix_domain_socket_server(sock_path) 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."""
epilog = ('See https://github.com/romanz/trezor-agent/blob/master/'
'doc/README-GPG.md for usage examples.')
parser = argparse.ArgumentParser(epilog=epilog)
parser = argparse.ArgumentParser()
agent_package = device_type.package_name()
resources_map = {r.key: r for r in pkg_resources.require(agent_package)}
@ -300,7 +273,7 @@ def main(device_type):
p.add_argument('-v', '--verbose', default=0, action='count')
p.add_argument('-s', '--subkey', default=False, action='store_true')
p.add_argument('--homedir', type=str, default=os.environ.get('GNUPGHOME'),
p.add_argument('--homedir', type=str,
help='Customize GnuPG home directory for the new identity.')
p.add_argument('--pin-entry-binary', type=str, default='pinentry',

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

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

@ -198,10 +198,8 @@ def get_gnupg_components(sp=subprocess):
@util.memoize
def get_gnupg_binary(sp=subprocess, neopg_binary=None):
def get_gnupg_binary(sp=subprocess):
"""Starting GnuPG 2.2.x, the default installation uses `gpg`."""
if neopg_binary:
return neopg_binary
return get_gnupg_components(sp=sp)['gpg']
@ -209,8 +207,11 @@ def gpg_command(args, env=None):
"""Prepare common GPG command line arguments."""
if env is None:
env = os.environ
cmd = get_gnupg_binary(neopg_binary=env.get('NEOPG_BINARY'))
return [cmd] + args
cmd = [get_gnupg_binary()]
homedir = env.get('GNUPGHOME')
if homedir:
cmd.extend(['--homedir', homedir])
return cmd + args
def get_keygrip(user_id, sp=subprocess):
@ -225,9 +226,7 @@ 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'
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
return line.split(b' ')[-1] # b'2.1.11'
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:
class PublicKey(object):
"""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:
class FakeSocket(object):
def __init__(self):
self.rx = io.BytesIO()
self.tx = io.BytesIO()

@ -39,43 +39,6 @@ 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,10 +65,7 @@ def _to_unicode(s):
def create_agent_parser(device_type):
"""Create an ArgumentParser for this tool."""
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 = configargparse.ArgParser(default_config_files=['~/.ssh/agent.config'])
p.add_argument('-v', '--verbose', default=0, action='count')
agent_package = device_type.package_name()
@ -193,7 +190,7 @@ def import_public_keys(contents):
yield line
class JustInTimeConnection:
class JustInTimeConnection(object):
"""Connect to the device just before the needed operation."""
def __init__(self, conn_factory, identities, public_keys=None):

@ -11,7 +11,7 @@ from . import formats, util
log = logging.getLogger(__name__)
class Client:
class Client(object):
"""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:
class Handler(object):
"""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:
class FakeSocket(object):
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:
class FakeServer(object):
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:
class FakeSocket(object):
def __init__(self):
self.buf = io.BytesIO()

@ -146,7 +146,7 @@ def hexlify(blob):
return binascii.hexlify(blob).decode('ascii').upper()
class Reader:
class Reader(object):
"""Read basic type objects out of given stream."""
def __init__(self, stream):
@ -258,7 +258,7 @@ def assuan_serialize(data):
return data
class ExpiringCache:
class ExpiringCache(object):
"""Simple cache with a deadline."""
def __init__(self, seconds, timer=time.time):

@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='libagent',
version='0.12.0',
version='0.11.3',
description='Using hardware wallets as SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
@ -34,7 +34,10 @@ setup(
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Operating System :: POSIX',
'Programming Language :: Python :: 3 :: Only',
'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',

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

Loading…
Cancel
Save