From 2e688ccac9eed4957e1cdee0dd98977446a4870d Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 15 Oct 2017 21:56:30 +0300 Subject: [PATCH 01/21] setup: deprecate Python2 support --- .travis.yml | 2 -- setup.py | 5 +---- tox.ini | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6458abd..fb61b07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ sudo: false language: python python: - - "2.7" - - "3.4" - "3.5" - "3.6" diff --git a/setup.py b/setup.py index 1ff7293..047222e 100755 --- a/setup.py +++ b/setup.py @@ -34,10 +34,7 @@ setup( 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 'Operating System :: POSIX', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Networking', 'Topic :: Communications', diff --git a/tox.ini b/tox.ini index 1165b3f..f25a59b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py3 +envlist = py3 [pycodestyle] max-line-length = 100 [pep257] From fef4fd06c986cd6ff2b2bee3566ada0c19071bfa Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Wed, 18 Apr 2018 12:41:38 +0200 Subject: [PATCH 02/21] Document the configuration of the git email setting and errors Signed-off-by: Timothy Hobbs --- doc/README-GPG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/README-GPG.md b/doc/README-GPG.md index bed4d34..9129dcf 100644 --- a/doc/README-GPG.md +++ b/doc/README-GPG.md @@ -70,6 +70,21 @@ $ git tag v1.2.3 --sign # create GPG-signed tag $ git tag v1.2.3 --verify # verify tag signature ``` +Note that your git email has to correlate to your gpg key email. If you use a different email for git, you'll need to either generate a new gpg key for that email or set your git email using the command: + +```` +$ git config user.email foo@example.com +```` + +If your git email is configured incorrectly, you will receive the error: + +```` +error: gpg failed to sign the data +fatal: failed to write commit object +```` + +when committing to git. + ### Manage passwords Password managers such as [pass](https://www.passwordstore.org/) and [gopass](https://www.justwatch.com/gopass/) rely on GPG for encryption so you can use your device with them too. From 91b850f1844c3ee3696a15286d10361a6b324aa9 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 21 Apr 2018 13:20:22 +0300 Subject: [PATCH 03/21] Update to Install.md reflecting Homebrew formula --- doc/INSTALL.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/INSTALL.md b/doc/INSTALL.md index ccb63b6..5d27c7f 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -76,6 +76,12 @@ gpg (GnuPG) 2.1.15 $ git clone https://github.com/romanz/trezor-agent $ pip3 install --user -e trezor-agent/agents/trezor ``` + + Or, through Homebrew on macOS: + + ``` + $ brew install trezor-agent + ``` # 3. Install the KeepKey agent From 4bd769f138bff6aafd7a00b1168fe5e6d0854428 Mon Sep 17 00:00:00 2001 From: pruflyos <30443750+pruflyos@users.noreply.github.com> Date: Sat, 21 Apr 2018 16:15:44 -0400 Subject: [PATCH 04/21] Update INSTALL.md On Fedora `python3-tk` is called `python3-tkinter` --- doc/INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/INSTALL.md b/doc/INSTALL.md index ccb63b6..0df5691 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -21,7 +21,7 @@ You can install them on these distributions as follows: ##### Fedora - $ dnf install python3-pip python3-devel python3-tk libusb-devel libudev-devel \ + $ dnf install python3-pip python3-devel python3-tkinter libusb-devel libudev-devel \ gcc redhat-rpm-config ##### OpenSUSE From 766536d2c4694f2d6596b1c8b6a0a085d235b06c Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 23 Apr 2018 22:27:59 +0300 Subject: [PATCH 05/21] trezor: allow expiring cached passphrase --- libagent/device/trezor.py | 11 +++++++---- libagent/tests/test_util.py | 23 +++++++++++++++++++++++ libagent/util.py | 23 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/libagent/device/trezor.py b/libagent/device/trezor.py index 0fbfc2b..19efccb 100644 --- a/libagent/device/trezor.py +++ b/libagent/device/trezor.py @@ -7,6 +7,7 @@ import mnemonic import semver from . import interface +from .. import util log = logging.getLogger(__name__) @@ -46,7 +47,8 @@ class Trezor(interface.Device): conn.callback_PinMatrixRequest = new_handler - cached_passphrase_ack = None + # Remembers the passphrase for an hour. + cached_passphrase_ack = util.ExpiringCache(seconds=60*60) cached_state = None def _override_passphrase_handler(self, conn): @@ -57,9 +59,10 @@ class Trezor(interface.Device): try: if msg.on_device is True: return self._defs.PassphraseAck() - if self.__class__.cached_passphrase_ack: + ack = self.__class__.cached_passphrase_ack.get() + if ack: log.debug('re-using cached %s passphrase', self) - return self.__class__.cached_passphrase_ack + return ack passphrase = self.ui.get_passphrase() passphrase = mnemonic.Mnemonic.normalize_string(passphrase) @@ -70,7 +73,7 @@ class Trezor(interface.Device): msg = 'Too long passphrase ({} chars)'.format(length) raise ValueError(msg) - self.__class__.cached_passphrase_ack = ack + self.__class__.cached_passphrase_ack.set(ack) return ack except: # noqa conn.init_device() diff --git a/libagent/tests/test_util.py b/libagent/tests/test_util.py index 2cef300..e85bc2f 100644 --- a/libagent/tests/test_util.py +++ b/libagent/tests/test_util.py @@ -121,3 +121,26 @@ def test_assuan_serialize(): assert util.assuan_serialize(b'') == b'' assert util.assuan_serialize(b'123\n456') == b'123%0A456' assert util.assuan_serialize(b'\r\n') == b'%0D%0A' + + +def test_cache(): + timer = mock.Mock(side_effect=range(7)) + c = util.ExpiringCache(seconds=2, timer=timer) # t=0 + assert c.get() is None # t=1 + obj = 'foo' + c.set(obj) # t=2 + assert c.get() is obj # t=3 + assert c.get() is obj # t=4 + assert c.get() is None # t=5 + assert c.get() is None # t=6 + + +def test_cache_inf(): + timer = mock.Mock(side_effect=range(6)) + c = util.ExpiringCache(seconds=float('inf'), timer=timer) + obj = 'foo' + c.set(obj) + assert c.get() is obj + assert c.get() is obj + assert c.get() is obj + assert c.get() is obj diff --git a/libagent/util.py b/libagent/util.py index c98891f..7df843b 100644 --- a/libagent/util.py +++ b/libagent/util.py @@ -5,6 +5,7 @@ import functools import io import logging import struct +import time log = logging.getLogger(__name__) @@ -255,3 +256,25 @@ def assuan_serialize(data): escaped = '%{:02X}'.format(ord(c)).encode('ascii') data = data.replace(c, escaped) return data + + +class ExpiringCache(object): + """Simple cache with a deadline.""" + + def __init__(self, seconds, timer=time.time): + """C-tor.""" + self.duration = seconds + self.timer = timer + self.value = None + self.set(None) + + def get(self): + """Returns existing value, or None if deadline has expired.""" + if self.timer() > self.deadline: + self.value = None + return self.value + + def set(self, value): + """Set new value and reset the deadline for expiration.""" + self.deadline = self.timer() + self.duration + self.value = value From b1bd6cb6908e636385097f43ae193eecf789f70b Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 23 Apr 2018 22:59:11 +0300 Subject: [PATCH 06/21] gpg: refactor GETINFO handling into a separate method --- libagent/gpg/agent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libagent/gpg/agent.py b/libagent/gpg/agent.py index 92bb2d7..0079fe4 100644 --- a/libagent/gpg/agent.py +++ b/libagent/gpg/agent.py @@ -92,7 +92,7 @@ class Handler(object): b'OPTION': lambda _, args: self.handle_option(*args), b'SETKEYDESC': None, b'NOP': None, - b'GETINFO': lambda conn, _: keyring.sendline(conn, b'D ' + self.version), + b'GETINFO': self.handle_getinfo, b'AGENT_ID': lambda conn, _: keyring.sendline(conn, b'D TREZOR'), # "Fake" agent ID b'SIGKEY': lambda _, args: self.set_key(*args), b'SETKEY': lambda _, args: self.set_key(*args), @@ -115,6 +115,10 @@ class Handler(object): self.options.append(opt) log.debug('options: %s', self.options) + def handle_getinfo(self, conn, _args): + """Handle some of the GETINFO messages.""" + keyring.sendline(conn, b'D ' + self.version) + def handle_scd(self, conn, args): """No support for smart-card device protocol.""" reply = { From 2ca3941cfa07f494b11f1376e294251804b12a8f Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 25 Apr 2018 00:01:55 +0300 Subject: [PATCH 07/21] ssh: allow setting passphrase cache expriration duration --- libagent/device/trezor.py | 3 +-- libagent/ssh/__init__.py | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libagent/device/trezor.py b/libagent/device/trezor.py index 19efccb..9b1f7e2 100644 --- a/libagent/device/trezor.py +++ b/libagent/device/trezor.py @@ -47,8 +47,7 @@ class Trezor(interface.Device): conn.callback_PinMatrixRequest = new_handler - # Remembers the passphrase for an hour. - cached_passphrase_ack = util.ExpiringCache(seconds=60*60) + cached_passphrase_ack = util.ExpiringCache(seconds=float('inf')) cached_state = None def _override_passphrase_handler(self, conn): diff --git a/libagent/ssh/__init__.py b/libagent/ssh/__init__.py index 8c5c894..cf4f41f 100644 --- a/libagent/ssh/__init__.py +++ b/libagent/ssh/__init__.py @@ -89,6 +89,8 @@ def create_agent_parser(device_type): help='Path to PIN entry UI helper.') p.add_argument('--passphrase-entry-binary', type=str, default='pinentry', help='Path to passphrase entry UI helper.') + p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'), + help='Expire passphrase from cache after this duration.') g = p.add_mutually_exclusive_group() g.add_argument('-d', '--daemonize', default=False, action='store_true', @@ -274,6 +276,8 @@ def main(device_type): # override default PIN/passphrase entry tools (relevant for TREZOR/Keepkey): device_type.ui = device.ui.UI(device_type=device_type, config=vars(args)) + device_type.cached_passphrase_ack = util.ExpiringCache( + args.cache_expiry_seconds) conn = JustInTimeConnection( conn_factory=lambda: client.Client(device_type()), From afa3fdb89c1043b8cd44df42ffe17f5e57691420 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 25 Apr 2018 00:02:48 +0300 Subject: [PATCH 08/21] gpg: allow setting passphrase cache expriration duration --- .pylintrc | 2 +- libagent/gpg/__init__.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 031341d..ed703f3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,5 @@ [MESSAGES CONTROL] -disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme +disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme,duplicate-code [SIMILARITIES] min-similarity-lines=5 diff --git a/libagent/gpg/__init__.py b/libagent/gpg/__init__.py index 2a37df8..8e3ed90 100644 --- a/libagent/gpg/__init__.py +++ b/libagent/gpg/__init__.py @@ -147,6 +147,7 @@ export PATH={0} -vv \ --pin-entry-binary={pin_entry_binary} \ --passphrase-entry-binary={passphrase_entry_binary} \ +--cache-expiry-seconds={cache_expiry_seconds} \ $* """.format(os.environ['PATH'], agent_path, **vars(args))) check_call(['chmod', '700', f.name]) @@ -212,6 +213,8 @@ def run_agent(device_type): help='Path to PIN entry UI helper.') p.add_argument('--passphrase-entry-binary', type=str, default='pinentry', help='Path to passphrase entry UI helper.') + p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'), + help='Expire passphrase from cache after this duration.') args, _ = p.parse_known_args() @@ -229,6 +232,8 @@ def run_agent(device_type): pubkey_bytes = keyring.export_public_keys(env=env) device_type.ui = device.ui.UI(device_type=device_type, config=vars(args)) + device_type.cached_passphrase_ack = util.ExpiringCache( + seconds=float(args.cache_expiry_seconds)) with server.unix_domain_socket_server(sock_path) as sock: for conn in agent.yield_connections(sock): handler = agent.Handler(device=device_type(), @@ -274,6 +279,8 @@ def main(device_type): help='Path to PIN entry UI helper.') p.add_argument('--passphrase-entry-binary', type=str, default='pinentry', help='Path to passphrase entry UI helper.') + p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'), + help='Expire passphrase from cache after this duration.') p.set_defaults(func=run_init) @@ -283,5 +290,7 @@ def main(device_type): args = parser.parse_args() device_type.ui = device.ui.UI(device_type=device_type, config=vars(args)) + device_type.cached_passphrase_ack = util.ExpiringCache( + seconds=float(args.cache_expiry_seconds)) return args.func(device_type=device_type, args=args) From ccc2174775cea8e830a3b7d9fb9eb412ddae0d6c Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 25 Apr 2018 00:16:27 +0300 Subject: [PATCH 09/21] gpg: allow more verbose output during GnuPG pubkey import --- libagent/gpg/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libagent/gpg/__init__.py b/libagent/gpg/__init__.py index 8e3ed90..b881b58 100644 --- a/libagent/gpg/__init__.py +++ b/libagent/gpg/__init__.py @@ -180,7 +180,8 @@ fi pubkey = write_file(os.path.join(homedir, 'pubkey.asc'), export_public_key(device_type, args)) gpg_binary = keyring.get_gnupg_binary() - check_call([gpg_binary, '--homedir', homedir, '--quiet', + verbosity = ('-' + ('v' * args.verbose)) if args.verbose else '--quiet' + check_call([gpg_binary, '--homedir', homedir, verbosity, '--import', pubkey.name]) # Make new GPG identity with "ultimate" trust (via its fingerprint) From bea899d1efc0c572f25875faaa4e25d6a7b5e687 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 25 Apr 2018 11:09:58 +0300 Subject: [PATCH 10/21] gpg: allow symmetric encryption with a passphrase --- libagent/device/ui.py | 8 ++++---- libagent/gpg/agent.py | 22 ++++++++++++++++++++-- libagent/gpg/keyring.py | 4 ++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/libagent/device/ui.py b/libagent/device/ui.py index cdeaa91..1b9b985 100644 --- a/libagent/device/ui.py +++ b/libagent/device/ui.py @@ -24,7 +24,7 @@ class UI(object): self.options_getter = create_default_options_getter() self.device_name = device_type.__name__ - def get_pin(self): + def get_pin(self, name=None): """Ask the user for (scrambled) PIN.""" description = ( 'Use the numeric keypad to describe number positions.\n' @@ -33,16 +33,16 @@ class UI(object): ' 4 5 6\n' ' 1 2 3') return interact( - title='{} PIN'.format(self.device_name), + title='{} PIN'.format(name or self.device_name), prompt='PIN:', description=description, binary=self.pin_entry_binary, options=self.options_getter()) - def get_passphrase(self): + def get_passphrase(self, name=None): """Ask the user for passphrase.""" return interact( - title='{} passphrase'.format(self.device_name), + title='{} passphrase'.format(name or self.device_name), prompt='Passphrase:', description=None, binary=self.passphrase_entry_binary, diff --git a/libagent/gpg/agent.py b/libagent/gpg/agent.py index 0079fe4..516be83 100644 --- a/libagent/gpg/agent.py +++ b/libagent/gpg/agent.py @@ -102,6 +102,7 @@ class Handler(object): b'HAVEKEY': lambda _, args: self.have_key(*args), b'KEYINFO': _key_info, b'SCD': self.handle_scd, + b'GET_PASSPHRASE': self.handle_get_passphrase, } def reset(self): @@ -115,9 +116,26 @@ class Handler(object): self.options.append(opt) log.debug('options: %s', self.options) - def handle_getinfo(self, conn, _args): + def handle_get_passphrase(self, conn, args): + passphrase = self.client.device.ui.get_passphrase('Symmetric encryption') + result = b'D ' + util.assuan_serialize(passphrase.encode('ascii')) + keyring.sendline(conn, result, confidential=True) + + def handle_getinfo(self, conn, args): """Handle some of the GETINFO messages.""" - keyring.sendline(conn, b'D ' + self.version) + result = None + if args[0] == b'version': + result = self.version + elif args[0] == b's2k_count': + # Use highest number of S2K iterations. + # https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Options.html + # https://tools.ietf.org/html/rfc4880#section-3.7.1.3 + result = '{}'.format(64 << 20).encode('ascii') + else: + log.warning('Unknown GETINFO command: %s', args) + + if result: + keyring.sendline(conn, b'D ' + result) def handle_scd(self, conn, args): """No support for smart-card device protocol.""" diff --git a/libagent/gpg/keyring.py b/libagent/gpg/keyring.py index 85adc55..e001ffb 100644 --- a/libagent/gpg/keyring.py +++ b/libagent/gpg/keyring.py @@ -48,9 +48,9 @@ def communicate(sock, msg): return recvline(sock) -def sendline(sock, msg): +def sendline(sock, msg, confidential=False): """Send a binary message, followed by EOL.""" - log.debug('<- %r', msg) + log.debug('<- %r', ('' if confidential else msg)) sock.sendall(msg + b'\n') From 3d1639d271a14170dd203c041096461a314e74ca Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 25 Apr 2018 11:13:28 +0300 Subject: [PATCH 11/21] gpg: require symmetric passphrase re-entry --- libagent/gpg/agent.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libagent/gpg/agent.py b/libagent/gpg/agent.py index 516be83..1898a77 100644 --- a/libagent/gpg/agent.py +++ b/libagent/gpg/agent.py @@ -116,10 +116,15 @@ class Handler(object): self.options.append(opt) log.debug('options: %s', self.options) - def handle_get_passphrase(self, conn, args): - passphrase = self.client.device.ui.get_passphrase('Symmetric encryption') - result = b'D ' + util.assuan_serialize(passphrase.encode('ascii')) - keyring.sendline(conn, result, confidential=True) + def handle_get_passphrase(self, conn, _): + """Allow simple GPG symmetric encryption (using a passphrase).""" + p1 = self.client.device.ui.get_passphrase('Symmetric encryption') + p2 = self.client.device.ui.get_passphrase('Re-enter encryption') + if p1 == p2: + result = b'D ' + util.assuan_serialize(p1.encode('ascii')) + keyring.sendline(conn, result, confidential=True) + else: + log.warning('Passphrase does not match!') def handle_getinfo(self, conn, args): """Handle some of the GETINFO messages.""" From bd0df4f801f9ff0e5077f8de71f6d3b5440e087d Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 5 May 2018 21:05:02 +0300 Subject: [PATCH 12/21] trezor: update setup.py for latest libagent and trezorlib --- agents/trezor/setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agents/trezor/setup.py b/agents/trezor/setup.py index 6671d9d..44d1d2e 100644 --- a/agents/trezor/setup.py +++ b/agents/trezor/setup.py @@ -3,15 +3,15 @@ from setuptools import setup setup( name='trezor_agent', - version='0.9.2', + version='0.9.3', 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.9.0' + 'libagent>=0.11.2', + 'trezor[hidapi]>=0.9.0' ], platforms=['POSIX'], classifiers=[ From 0c762e8998969f24b30b1c3f7e96ae92660ffeb2 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 23 May 2018 08:35:34 +0300 Subject: [PATCH 13/21] Use `pinentry` homebrew formula on macOS --- doc/README-PINENTRY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README-PINENTRY.md b/doc/README-PINENTRY.md index 6c90705..17aa5c8 100644 --- a/doc/README-PINENTRY.md +++ b/doc/README-PINENTRY.md @@ -9,7 +9,7 @@ $ apt install pinentry-{curses,gnome3,qt} or (on macOS): ``` -$ brew install pinentry-mac +$ brew install pinentry ``` By default a standard GPG PIN entry program is used when entering your Trezor PIN, but it's difficult to use if you don't have a numeric keypad or want to use your mouse. From bd1ae0f091b08cc287790b8e20ae350a44114c35 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 24 May 2018 14:01:40 +0300 Subject: [PATCH 14/21] Update INSTALL.md I've sorted out the Formula for Homebrew and it's been merged. --- doc/INSTALL.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 808be10..db1addf 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -95,6 +95,12 @@ Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_ag ``` $ pip3 install keepkey_agent ``` + + Or, on Mac using Homebrew: + + ``` + $ homebrew install keepkey-agent + ``` Or, directly from the latest source code: From ed531cfff80d76dad39ad2c322a1e552c0782e2a Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 25 May 2018 08:43:22 +0300 Subject: [PATCH 15/21] Remove trailing whitespace git ls-files | xargs -n1 sed -e's/[[:space:]]*$//' -i --- doc/DESIGN.md | 12 ++++++------ doc/INSTALL.md | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/DESIGN.md b/doc/DESIGN.md index 28bc064..598899c 100644 --- a/doc/DESIGN.md +++ b/doc/DESIGN.md @@ -12,11 +12,11 @@ So when you `ssh` to a machine - rather than consult the normal ssh-agent (which ## Key Naming -`trezor-agent` goes to great length to avoid using the valuable parent key. +`trezor-agent` goes to great length to avoid using the valuable parent key. -The rationale behind this is that `trezor-agent` is to some extent condemned to *blindly* signing any NONCE given to it (e.g. as part of a challenge respone, or as the hash/hmac of someting to sign). +The rationale behind this is that `trezor-agent` is to some extent condemned to *blindly* signing any NONCE given to it (e.g. as part of a challenge respone, or as the hash/hmac of someting to sign). -And doing so with the master private key is risky - as rogue (ssh) server could possibly provide a doctored NONCE that happens to be tied to a transaction or something else. +And doing so with the master private key is risky - as rogue (ssh) server could possibly provide a doctored NONCE that happens to be tied to a transaction or something else. It therefore uses only derived child keys pairs instead (according to the [BIP-0032: Hierarchical Deterministic Wallets][1] system) - and ones on different leafs. So the parent key is only used within the device for creating the child keys - and not exposed in any way to `trezor-agent`. @@ -26,7 +26,7 @@ It is common for SSH users to use one (or a few) private keys with SSH on all se So taking a commmand such as: - $ trezor-agent -c user@fqdn.com + $ trezor-agent -c user@fqdn.com The `trezor-agent` will take the `user`@`fqdn.com`; canonicalise it (e.g. to add the ssh default port number if none was specified) and then apply some simple hashing (See [SLIP-0013 : Authentication using deterministic hierarchy][2]). The resulting 128bit hash is then used to construct a lead 'HD node' that contains an extened public private *child* key. @@ -42,10 +42,10 @@ Note: Keepkey does not support en-/de-cryption at this time. ### Index -The canonicalisation process ([SLIP-0013][2] and [SLIP-0017][3]) of an email address or ssh address allows for the mixing in of an extra 'index' - a unsigned 32 bit number. This allows one to have multiple, different keys, for the same address. +The canonicalisation process ([SLIP-0013][2] and [SLIP-0017][3]) of an email address or ssh address allows for the mixing in of an extra 'index' - a unsigned 32 bit number. This allows one to have multiple, different keys, for the same address. This feature is currently not used -- it is set to '0'. This may change in the future. -[1]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +[1]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki [2]: https://github.com/satoshilabs/slips/blob/master/slip-0013.md [3]: https://github.com/satoshilabs/slips/blob/master/slip-0017.md diff --git a/doc/INSTALL.md b/doc/INSTALL.md index db1addf..521714f 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -33,7 +33,7 @@ If you are using python3 or your system `pip` command points to `pip3.x` dependencies instead: $ zypper install python3-pip python3-devel python3-tk libusb-1_0-devel libudev-devel - + ##### macOS There are many different options to install python environment on macOS ([official](https://www.python.org/downloads/mac-osx/), [anaconda](https://conda.io/docs/user-guide/install/macos.html), ..). Most importantly you need `libusb`. Probably the easiest way is via [homebrew](https://brew.sh/) @@ -76,7 +76,7 @@ gpg (GnuPG) 2.1.15 $ git clone https://github.com/romanz/trezor-agent $ pip3 install --user -e trezor-agent/agents/trezor ``` - + Or, through Homebrew on macOS: ``` @@ -95,9 +95,9 @@ Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_ag ``` $ pip3 install keepkey_agent ``` - + Or, on Mac using Homebrew: - + ``` $ homebrew install keepkey-agent ``` From 672af98ad7814c3841b80d920665baa652f9b81d Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 19 Jun 2018 18:34:25 +0300 Subject: [PATCH 16/21] Explicitly use IdentityFile option when connecting to specific host --- libagent/ssh/__init__.py | 49 +++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/libagent/ssh/__init__.py b/libagent/ssh/__init__.py index cf4f41f..da055f4 100644 --- a/libagent/ssh/__init__.py +++ b/libagent/ssh/__init__.py @@ -23,9 +23,11 @@ log = logging.getLogger(__name__) UNIX_SOCKET_TIMEOUT = 0.1 -def ssh_args(label): +def ssh_args(conn): """Create SSH command for connecting specified server.""" - identity = device.interface.string_to_identity(label) + I, = conn.identities + identity = I.identity_dict + pubkey_tempfile, = conn.public_keys_as_files() args = [] if 'port' in identity: @@ -33,12 +35,15 @@ def ssh_args(label): if 'user' in identity: args += ['-l', identity['user']] + args += ['-o', 'IdentityFile={}'.format(pubkey_tempfile.name)] + args += ['-o', 'IdentitiesOnly=true'] return args + [identity['host']] -def mosh_args(label): +def mosh_args(conn): """Create SSH command for connecting specified server.""" - identity = device.interface.string_to_identity(label) + I, = conn.identities + identity = I.identity_dict args = [] if 'port' in identity: @@ -193,6 +198,7 @@ class JustInTimeConnection(object): self.conn_factory = conn_factory self.identities = identities self.public_keys_cache = public_keys + self.public_keys_tempfiles = [] def public_keys(self): """Return a list of SSH public keys (in textual format).""" @@ -209,6 +215,17 @@ class JustInTimeConnection(object): pk['identity'] = identity return public_keys + def public_keys_as_files(self): + """Store public keys as temporary SSH identity files.""" + if not self.public_keys_tempfiles: + for pk in self.public_keys(): + f = tempfile.NamedTemporaryFile(prefix='trezor-ssh-pubkey-', mode='w') + f.write(pk) + f.flush() + self.public_keys_tempfiles.append(f) + + return self.public_keys_tempfiles + def sign(self, blob, identity): """Sign a given blob using the specified identity on the device.""" conn = self.conn_factory() @@ -238,6 +255,7 @@ def main(device_type): util.setup_logging(verbosity=args.verbose, filename=args.log_file) public_keys = None + filename = None if args.identity.startswith('/'): filename = args.identity contents = open(filename, 'rb').read().decode('utf-8') @@ -252,14 +270,22 @@ def main(device_type): identity.identity_dict['proto'] = u'ssh' log.info('identity #%d: %s', index, identity.to_string()) - sock_path = _get_sock_path(args) + # override default PIN/passphrase entry tools (relevant for TREZOR/Keepkey): + device_type.ui = device.ui.UI(device_type=device_type, config=vars(args)) + device_type.cached_passphrase_ack = util.ExpiringCache( + args.cache_expiry_seconds) + + conn = JustInTimeConnection( + conn_factory=lambda: client.Client(device_type()), + identities=identities, public_keys=public_keys) + sock_path = _get_sock_path(args) command = args.command context = _dummy_context() if args.connect: - command = ['ssh'] + ssh_args(args.identity) + args.command + command = ['ssh'] + ssh_args(conn) + args.command elif args.mosh: - command = ['mosh'] + mosh_args(args.identity) + args.command + command = ['mosh'] + mosh_args(conn) + args.command elif args.daemonize: out = 'SSH_AUTH_SOCK={0}; export SSH_AUTH_SOCK;\n'.format(sock_path) sys.stdout.write(out) @@ -274,15 +300,6 @@ def main(device_type): command = os.environ['SHELL'] sys.stdin.close() - # override default PIN/passphrase entry tools (relevant for TREZOR/Keepkey): - device_type.ui = device.ui.UI(device_type=device_type, config=vars(args)) - device_type.cached_passphrase_ack = util.ExpiringCache( - args.cache_expiry_seconds) - - conn = JustInTimeConnection( - conn_factory=lambda: client.Client(device_type()), - identities=identities, public_keys=public_keys) - if command or args.daemonize or args.foreground: with context: return run_server(conn=conn, command=command, sock_path=sock_path, From 8672a6901a636bd0232a5eb6bd2d06051a44914a Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 19 Jun 2018 18:49:36 +0300 Subject: [PATCH 17/21] Document IdentitiesOnly support --- doc/README-SSH.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/README-SSH.md b/doc/README-SSH.md index 008c8a8..2f72a39 100644 --- a/doc/README-SSH.md +++ b/doc/README-SSH.md @@ -161,7 +161,7 @@ export SSH_AUTH_SOCK=$(systemctl show --user --property=Listen trezor-ssh-agent. If SSH connection fails to work, please open an [issue](https://github.com/romanz/trezor-agent/issues) with a verbose log attached (by running `trezor-agent -vv`) . -##### Incompatible SSH options +##### `IdentitiesOnly` SSH option Note that your local SSH configuration may ignore `trezor-agent`, if it has `IdentitiesOnly` option set to `yes`. @@ -172,6 +172,12 @@ Note that your local SSH configuration may ignore `trezor-agent`, if it has `Ide This option is intended for situations where ssh-agent offers many different identities. The default is “no”. -If you are failing to connect, try running: +If you are failing to connect, save your public key using: - $ trezor-agent -vv user@host -- ssh -vv -oIdentitiesOnly=no user@host + $ trezor-agent -vv foobar@hostname.com > ~/.ssh/hostname.pub + +And add the following lines to `~/.ssh/config` (providing the public key explicitly to SSH): + + Host hostname.com + User foobar + IdentityFile ~/.ssh/hostname.pub From 6bc5b6af5eea7e23766a7829d5ad91e00771fb4e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 19 Jun 2018 19:04:05 +0300 Subject: [PATCH 18/21] Add small example for IdentityOnly use-case --- doc/README-SSH.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/README-SSH.md b/doc/README-SSH.md index 2f72a39..9480cc2 100644 --- a/doc/README-SSH.md +++ b/doc/README-SSH.md @@ -181,3 +181,12 @@ And add the following lines to `~/.ssh/config` (providing the public key explici Host hostname.com User foobar IdentityFile ~/.ssh/hostname.pub + +Then, the following commands should successfully command to the remote host: + + $ trezor-agent -v foobar@hostname.com -s + $ ssh foobar@hostname.com + +or, + + $ trezor-agent -v foobar@hostname.com -c From 6a9fdf75e273edb05fd29f621d38a2cb0a0ce63e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 19 Jun 2018 21:15:14 +0300 Subject: [PATCH 19/21] =?UTF-8?q?Bump=20version:=200.11.2=20=E2=86=92=200.?= =?UTF-8?q?11.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8ddf8fb..86c2178 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 0.11.2 +current_version = 0.11.3 [bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index 8520bb1..39bec67 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup( name='libagent', - version='0.11.2', + version='0.11.3', description='Using hardware wallets as SSH/GPG agent', author='Roman Zeyde', author_email='roman.zeyde@gmail.com', From a8f19e415059b96cd5dd1a892b9924b7f0fd8676 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 30 Jun 2018 11:12:43 +0300 Subject: [PATCH 20/21] Comment about SSH argument separation --- doc/README-SSH.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/README-SSH.md b/doc/README-SSH.md index 9480cc2..3101352 100644 --- a/doc/README-SSH.md +++ b/doc/README-SSH.md @@ -32,6 +32,7 @@ $ (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 From 1e6c4e69307d00c48044dfb1713947921c7eeae3 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 30 Jun 2018 11:21:47 +0300 Subject: [PATCH 21/21] Add links to SSH/GPG usage examples --- libagent/gpg/__init__.py | 4 +++- libagent/ssh/__init__.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/libagent/gpg/__init__.py b/libagent/gpg/__init__.py index b881b58..ab918c6 100644 --- a/libagent/gpg/__init__.py +++ b/libagent/gpg/__init__.py @@ -253,7 +253,9 @@ def run_agent(device_type): def main(device_type): """Parse command-line arguments.""" - parser = argparse.ArgumentParser() + epilog = ('See https://github.com/romanz/trezor-agent/blob/master/' + 'doc/README-GPG.md for usage examples.') + parser = argparse.ArgumentParser(epilog=epilog) agent_package = device_type.package_name() resources_map = {r.key: r for r in pkg_resources.require(agent_package)} diff --git a/libagent/ssh/__init__.py b/libagent/ssh/__init__.py index da055f4..dc756da 100644 --- a/libagent/ssh/__init__.py +++ b/libagent/ssh/__init__.py @@ -65,7 +65,10 @@ def _to_unicode(s): def create_agent_parser(device_type): """Create an ArgumentParser for this tool.""" - p = configargparse.ArgParser(default_config_files=['~/.ssh/agent.config']) + epilog = ('See https://github.com/romanz/trezor-agent/blob/master/' + 'doc/README-SSH.md for usage examples.') + p = configargparse.ArgParser(default_config_files=['~/.ssh/agent.config'], + epilog=epilog) p.add_argument('-v', '--verbose', default=0, action='count') agent_package = device_type.package_name()