From 498093f2f6b404903578277224a912e131b7313b Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Sat, 18 Sep 2021 14:00:20 -0500 Subject: [PATCH 01/38] Add Windows native SSH support --- libagent/device/ui.py | 6 +- libagent/ssh/__init__.py | 110 +++++++++++++++------- libagent/win_server.py | 191 +++++++++++++++++++++++++++++++++++++++ setup.py | 1 + tox.ini | 1 + 5 files changed, 275 insertions(+), 34 deletions(-) create mode 100644 libagent/win_server.py diff --git a/libagent/device/ui.py b/libagent/device/ui.py index 00486262..ebc88995 100644 --- a/libagent/device/ui.py +++ b/libagent/device/ui.py @@ -78,7 +78,8 @@ def button_request(self, _code=None): def create_default_options_getter(): """Return current TTY and DISPLAY settings for GnuPG pinentry.""" options = [] - if sys.stdin.isatty(): # short-circuit calling `tty` + # Windows reports that it has a TTY but throws FileNotFoundError + if sys.platform != 'win32' and sys.stdin.isatty(): # short-circuit calling `tty` try: ttyname = subprocess.check_output(args=['tty']).strip() options.append(b'ttyname=' + ttyname) @@ -88,7 +89,8 @@ def create_default_options_getter(): display = os.environ.get('DISPLAY') if display is not None: options.append('display={}'.format(display).encode('ascii')) - else: + # Windows likely doesn't support this anyway + elif sys.platform != 'win32': log.warning('DISPLAY not defined') log.info('using %s for pinentry options', options) diff --git a/libagent/ssh/__init__.py b/libagent/ssh/__init__.py index 614404d1..3f265abe 100644 --- a/libagent/ssh/__init__.py +++ b/libagent/ssh/__init__.py @@ -4,24 +4,32 @@ import io import logging import os +import random import re import signal +import string import subprocess import sys import tempfile import threading import configargparse -import daemon +try: + # TODO: Not supported on Windows. Use daemoniker instead? + import daemon +except ImportError: + daemon = None import pkg_resources -from .. import device, formats, server, util +from .. import device, formats, server, util, win_server from . import client, protocol log = logging.getLogger(__name__) UNIX_SOCKET_TIMEOUT = 0.1 - +WIN_PIPE_TIMEOUT = 0.1 +DEFAULT_TIMEOUT = WIN_PIPE_TIMEOUT if sys.platform == 'win32' else UNIX_SOCKET_TIMEOUT +SOCK_TYPE = 'Windows named pipe' if sys.platform == 'win32' else 'UNIX domain socket' def ssh_args(conn): """Create SSH command for connecting specified server.""" @@ -35,7 +43,7 @@ def ssh_args(conn): if 'user' in identity: args += ['-l', identity['user']] - args += ['-o', 'IdentityFile={}'.format(pubkey_tempfile.name)] + args += ['-o', 'IdentityFile={}'.format(pubkey_tempfile)] args += ['-o', 'IdentitiesOnly=true'] return args + [identity['host']] @@ -83,14 +91,14 @@ def create_agent_parser(device_type): default=formats.CURVE_NIST256, help='specify ECDSA curve name: ' + curve_names) p.add_argument('--timeout', - default=UNIX_SOCKET_TIMEOUT, type=float, + default=DEFAULT_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.') p.add_argument('--log-file', type=str, help='Path to the log file (to be written by the agent).') p.add_argument('--sock-path', type=str, - help='Path to the UNIX domain socket of the agent.') + help='Path to the ' + SOCK_TYPE + ' of the agent.') p.add_argument('--pin-entry-binary', type=str, default='pinentry', help='Path to PIN entry UI helper.') @@ -100,17 +108,20 @@ def create_agent_parser(device_type): help='Expire passphrase from cache after this duration.') g = p.add_mutually_exclusive_group() - g.add_argument('-d', '--daemonize', default=False, action='store_true', - help='Daemonize the agent and print its UNIX socket path') + if daemon: + g.add_argument('-d', '--daemonize', default=False, action='store_true', + help='Daemonize the agent and print its ' + SOCK_TYPE) g.add_argument('-f', '--foreground', default=False, action='store_true', - help='Run agent in foreground with specified UNIX socket path') + help='Run agent in foreground with specified ' + SOCK_TYPE) g.add_argument('-s', '--shell', default=False, action='store_true', help=('run ${SHELL} as subprocess under SSH agent, allowing ' 'regular SSH-based tools to be used in the shell')) g.add_argument('-c', '--connect', default=False, action='store_true', help='connect to specified host via SSH') - g.add_argument('--mosh', default=False, action='store_true', - help='connect to specified host via using Mosh') + # Windows doesn't have native mosh + if sys.platform != 'win32': + g.add_argument('--mosh', default=False, action='store_true', + help='connect to specified host via using Mosh') p.add_argument('identity', type=_to_unicode, default=None, help='proto://[user@]host[:port][/path]') @@ -119,18 +130,48 @@ def create_agent_parser(device_type): return p +def get_ssh_env(sock_path): + ssh_version = subprocess.check_output(['ssh', '-V'], + stderr=subprocess.STDOUT) + log.debug('local SSH version: %r', ssh_version) + return {'SSH_AUTH_SOCK': sock_path, 'SSH_AGENT_PID': str(os.getpid())} + + +# Windows doesn't support AF_UNIX yet +# https://bugs.python.org/issue33408 +@contextlib.contextmanager +def serve_win(handler, sock_path, timeout=WIN_PIPE_TIMEOUT): + """ + Start the ssh-agent server on a Windows named pipe. + """ + environ = get_ssh_env(sock_path) + device_mutex = threading.Lock() + quit_event = threading.Event() + handle_conn = functools.partial(win_server.handle_connection, + handler=handler, + mutex=device_mutex, + quit_event=quit_event) + kwargs = dict(pipe_name=sock_path, + handle_conn=handle_conn, + quit_event=quit_event, + timeout=timeout) + with server.spawn(win_server.server_thread, kwargs): + try: + yield environ + finally: + log.debug('closing server') + quit_event.set() + + @contextlib.contextmanager -def serve(handler, sock_path, timeout=UNIX_SOCKET_TIMEOUT): +def serve_unix(handler, sock_path, 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) - environ = {'SSH_AUTH_SOCK': sock_path, 'SSH_AGENT_PID': str(os.getpid())} + environ = get_ssh_env(sock_path) device_mutex = threading.Lock() with server.unix_domain_socket_server(sock_path) as sock: sock.settimeout(timeout) @@ -154,12 +195,15 @@ def run_server(conn, command, sock_path, debug, timeout): ret = 0 try: handler = protocol.Handler(conn=conn, debug=debug) - with serve(handler=handler, sock_path=sock_path, - timeout=timeout) as env: + serve_platform = serve_win if sys.platform == 'win32' else serve_unix + with serve_platform(handler=handler, sock_path=sock_path, timeout=timeout) as env: if command: ret = server.run_process(command=command, environ=env) else: - signal.pause() # wait for signal (e.g. SIGINT) + try: + signal.pause() # wait for signal (e.g. SIGINT) + except AttributeError: + sys.stdin.read() # Windows doesn't support signal.pause except KeyboardInterrupt: log.info('server stopped') return ret @@ -221,10 +265,9 @@ 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) + with tempfile.NamedTemporaryFile(prefix='trezor-ssh-pubkey-', mode='w', delete=False, newline='') as f: + f.write(pk) + self.public_keys_tempfiles.append(f.name) return self.public_keys_tempfiles @@ -241,13 +284,16 @@ def _dummy_context(): def _get_sock_path(args): sock_path = args.sock_path - if not sock_path: - if args.foreground: - log.error('running in foreground mode requires specifying UNIX socket path') - sys.exit(1) - else: - sock_path = tempfile.mktemp(prefix='trezor-ssh-agent-') - return sock_path + if sock_path: + return sock_path + elif args.foreground: + log.error('running in foreground mode requires specifying ' + SOCK_TYPE) + sys.exit(1) + elif sys.platform == 'win32': + suffix = random.choices(string.ascii_letters, k=10) + return '\\\\.\pipe\\trezor-ssh-agent-' + ''.join(suffix) + else: + return tempfile.mktemp(prefix='trezor-ssh-agent-') @handle_connection_error @@ -286,7 +332,7 @@ def main(device_type): command = ['ssh'] + ssh_args(conn) + args.command elif args.mosh: command = ['mosh'] + mosh_args(conn) + args.command - elif args.daemonize: + elif daemon and args.daemonize: out = 'SSH_AUTH_SOCK={0}; export SSH_AUTH_SOCK;\n'.format(sock_path) sys.stdout.write(out) sys.stdout.flush() @@ -300,7 +346,7 @@ def main(device_type): command = os.environ['SHELL'] sys.stdin.close() - if command or args.daemonize or args.foreground: + if command or (daemon and args.daemonize) or args.foreground: with context: return run_server(conn=conn, command=command, sock_path=sock_path, debug=args.debug, timeout=args.timeout) diff --git a/libagent/win_server.py b/libagent/win_server.py new file mode 100644 index 00000000..2bccffdd --- /dev/null +++ b/libagent/win_server.py @@ -0,0 +1,191 @@ +"""Windows named pipe server for ssh-agent implementation.""" +import logging +import pywintypes +import struct +import threading +import win32api +import win32event +import win32pipe +import win32file +import winerror + +from . import util + +log = logging.getLogger(__name__) + +PIPE_BUFFER_SIZE = 64 * 1024 + +# Make MemoryView look like a buffer to reuse util.recv +class MvBuffer: + def __init__(self, mv): + self.mv = mv + def read(self, n): + return self.mv[0:n] + +# Based loosely on https://docs.microsoft.com/en-us/windows/win32/ipc/multithreaded-pipe-server +class NamedPipe: + __frame_size_size = struct.calcsize('>L') + + def __close(handle): + """Closes a named pipe handle.""" + if handle == win32file.INVALID_HANDLE_VALUE: + return + win32file.FlushFileBuffers(handle) + win32pipe.DisconnectNamedPipe(handle) + win32api.CloseHandle(handle) + + def open(name): + """Opens a named pipe server for receiving connections.""" + handle = win32pipe.CreateNamedPipe( + name, + win32pipe.PIPE_ACCESS_DUPLEX | win32file.FILE_FLAG_OVERLAPPED, + win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT, + win32pipe.PIPE_UNLIMITED_INSTANCES, + PIPE_BUFFER_SIZE, + PIPE_BUFFER_SIZE, + 0, + None) # Default security attributes + + if handle == win32file.INVALID_HANDLE_VALUE: + log.error("CreateNamedPipe failed (%d)", win32api.GetLastError()) + return None + + try: + pending_io = False + overlapped = win32file.OVERLAPPED() + overlapped.hEvent = win32event.CreateEvent(None, True, True, None) + error_code = win32pipe.ConnectNamedPipe(handle, overlapped) + if error_code == winerror.ERROR_IO_PENDING: + pending_io = True + elif error_code != winerror.ERROR_PIPE_CONNECTED or not win32event.SetEvent(overlapped.hEvent): + log.error('ConnectNamedPipe failed (%d)', error_code) + return None + log.debug('waiting for connection on %s', name) + return NamedPipe(name, handle, overlapped, pending_io) + except: + NamedPipe.__close(handle) + raise + + def __init__(self, name, handle, overlapped, pending_io): + self.name = name + self.handle = handle + self.overlapped = overlapped + self.pending_io = pending_io + + def close(self): + """Close the named pipe.""" + NamedPipe.__close(self.handle) + + def connect(self, timeout): + """Connect to an SSH client with the specified timeout.""" + waitHandle = win32event.WaitForSingleObject( + self.overlapped.hEvent, + timeout) + if waitHandle == win32event.WAIT_TIMEOUT: + return False + if not self.pending_io: + return True + win32pipe.GetOverlappedResult( + self.handle, + self.overlapped, + False) + error_code = win32api.GetLastError() + if error_code == winerror.NO_ERROR: + return True + log.error('GetOverlappedResult failed (%d)', error_code) + return False + + def read_frame(self, quit_event): + """Read the request frame from the SSH client.""" + request_size = None + remaining = None + buf = MvBuffer(win32file.AllocateReadBuffer(PIPE_BUFFER_SIZE)) + while True: + if quit_event.is_set(): + return None + error_code, _ = win32file.ReadFile(self.handle, buf.mv, self.overlapped) + if error_code not in (winerror.NO_ERROR, winerror.ERROR_IO_PENDING, winerror.ERROR_MORE_DATA): + log.error('ReadFile failed (%d)', error_code) + return None + win32event.WaitForSingleObject(self.overlapped.hEvent, win32event.INFINITE) + chunk_size = win32pipe.GetOverlappedResult(self.handle, self.overlapped, False) + error_code = win32api.GetLastError() + if error_code != winerror.NO_ERROR: + log.error('GetOverlappedResult failed (%d)', error_code) + return None + if request_size: + remaining -= chunk_size + else: + request_size, = util.recv(buf, '>L') + remaining = request_size - (chunk_size - NamedPipe.__frame_size_size) + if remaining <= 0: + break + return util.recv(buf, request_size) + + def send(self, reply): + """Send the specified reply to the SSH client.""" + error_code, _ = win32file.WriteFile(self.handle, reply) + if error_code == winerror.NO_ERROR: + return True + log.error('WriteFile failed (%d)', error_code) + return False + + +def handle_connection(pipe, handler, mutex, quit_event): + """ + Handle a single connection using the specified protocol handler in a loop. + + Since this function may be called concurrently from server_thread, + the specified mutex is used to synchronize the device handling. + """ + log.debug('welcome agent') + + try: + while True: + if quit_event.is_set(): + return + msg = pipe.read_frame(quit_event) + if not msg: + return + with mutex: + reply = handler.handle(msg=msg) + if not pipe.send(reply): + return + except pywintypes.error as e: + # Surface errors that aren't related to the client disconnecting + if e.args[0] == winerror.ERROR_BROKEN_PIPE: + log.debug('goodbye agent') + else: + raise + except Exception as e: # pylint: disable=broad-except + log.warning('error: %s', e, exc_info=True) + finally: + pipe.close() + + +def server_thread(pipe_name, handle_conn, quit_event, timeout): + """Run a Windows server on the specified pipe.""" + log.debug('server thread started') + + while True: + if quit_event.is_set(): + break + # A new pipe instance is necessary for each client + pipe = NamedPipe.open(pipe_name) + if not pipe: + break + try: + # Poll for a new client connection + while True: + if quit_event.is_set(): + break + if pipe.connect(timeout * 1000): + # Handle connections from SSH concurrently. + threading.Thread(target=handle_conn, + kwargs=dict(pipe=pipe)).start() + break + except: + pipe.close() + raise + + log.debug('server thread stopped') diff --git a/setup.py b/setup.py index 97b184a4..8f8b5cb4 100755 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ 'pymsgbox>=1.0.6', 'semver>=2.2', 'unidecode>=0.4.20', + 'pypiwin32' ], platforms=['POSIX'], classifiers=[ diff --git a/tox.ini b/tox.ini index 3d6cc1a0..f4473213 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ deps= semver pydocstyle isort<5 + pypiwin32 commands= pycodestyle libagent isort --skip-glob .tox -c -rc libagent From 97416308edaa32fd95db62ba05fdb02639ab3cea Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 25 Apr 2023 17:29:54 +0300 Subject: [PATCH 02/38] Remove unused imports and fix a small lint issue --- libagent/age/__init__.py | 6 +--- libagent/device/onlykey.py | 56 ++++++++++++++++++------------------ libagent/signify/__init__.py | 11 +------ 3 files changed, 30 insertions(+), 43 deletions(-) diff --git a/libagent/age/__init__.py b/libagent/age/__init__.py index 8637655a..95fd03ac 100644 --- a/libagent/age/__init__.py +++ b/libagent/age/__init__.py @@ -9,21 +9,17 @@ import argparse import base64 -import contextlib -import datetime import io import logging import os import sys -import traceback import bech32 import pkg_resources -import semver from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -from .. import device, server, util +from .. import device, util from . import client log = logging.getLogger(__name__) diff --git a/libagent/device/onlykey.py b/libagent/device/onlykey.py index 710f0fed..c0b4b7e5 100644 --- a/libagent/device/onlykey.py +++ b/libagent/device/onlykey.py @@ -159,35 +159,35 @@ def pubkey(self, identity, ecdh=False): else: vk = ecdsa.VerifyingKey.from_string(ok_pubkey, curve=ecdsa.SECP256k1) return vk - else: - ok_pubkey = [] - while time.time() < t_end: - try: - ok_pub_part = self.ok.read_bytes(timeout_ms=100) - if len(ok_pub_part) == 64 and len(set(ok_pub_part[0:63])) != 1: - log.info('received part= %s', repr(ok_pub_part)) - ok_pubkey += ok_pub_part - # Todo know RSA type to know how many packets - except Exception as e: - raise interface.DeviceError(e) - log.info('received= %s', repr(ok_pubkey)) - if len(ok_pubkey) == 256: - # https://security.stackexchange.com/questions/42268/how-do-i-get-the-rsa-bit-length-with-the-pubkey-and-openssl - ok_pubkey = b'\x00\x00\x00\x07' + b'\x73\x73\x68\x2d\x72\x73\x61' + \ - b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ - b'\x00\x00\x01\x01' + b'\x00' + bytes(ok_pubkey) - # ok_pubkey = b'\x00\x00\x00\x07' + b'\x72\x73\x61\x2d\x73\x68\x61\x32\x2d\x32\x35\x - # 36' + b'\x00\x00\x00\x03' + b'\x01\x00\x01' + b'\x00\x00\x01\x01' + b'\x00' + byte - # s(ok_pubkey) - elif len(ok_pubkey) == 512: - ok_pubkey = b'\x00\x00\x00\x07' + b'\x73\x73\x68\x2d\x72\x73\x61' + \ - b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ - b'\x00\x00\x02\x01' + b'\x00' + bytes(ok_pubkey) - else: - raise interface.DeviceError("Error response length is not a valid public key") - log.info('pubkey len = %s', len(ok_pubkey)) - return ok_pubkey + ok_pubkey = [] + while time.time() < t_end: + try: + ok_pub_part = self.ok.read_bytes(timeout_ms=100) + if len(ok_pub_part) == 64 and len(set(ok_pub_part[0:63])) != 1: + log.info('received part= %s', repr(ok_pub_part)) + ok_pubkey += ok_pub_part + # Todo know RSA type to know how many packets + except Exception as e: + raise interface.DeviceError(e) + + log.info('received= %s', repr(ok_pubkey)) + if len(ok_pubkey) == 256: + # https://security.stackexchange.com/questions/42268/how-do-i-get-the-rsa-bit-length-with-the-pubkey-and-openssl + ok_pubkey = b'\x00\x00\x00\x07' + b'\x73\x73\x68\x2d\x72\x73\x61' + \ + b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ + b'\x00\x00\x01\x01' + b'\x00' + bytes(ok_pubkey) + # ok_pubkey = b'\x00\x00\x00\x07' + b'\x72\x73\x61\x2d\x73\x68\x61\x32\x2d\x32\x35\x + # 36' + b'\x00\x00\x00\x03' + b'\x01\x00\x01' + b'\x00\x00\x01\x01' + b'\x00' + byte + # s(ok_pubkey) + elif len(ok_pubkey) == 512: + ok_pubkey = b'\x00\x00\x00\x07' + b'\x73\x73\x68\x2d\x72\x73\x61' + \ + b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ + b'\x00\x00\x02\x01' + b'\x00' + bytes(ok_pubkey) + else: + raise interface.DeviceError("Error response length is not a valid public key") + log.info('pubkey len = %s', len(ok_pubkey)) + return ok_pubkey def sign(self, identity, blob): """Sign given blob and return the signature (as bytes).""" diff --git a/libagent/signify/__init__.py b/libagent/signify/__init__.py index aa0ab5d7..a846ee90 100644 --- a/libagent/signify/__init__.py +++ b/libagent/signify/__init__.py @@ -2,21 +2,12 @@ import argparse import binascii -import contextlib -import functools import hashlib import logging -import os -import re -import struct -import subprocess import sys import time -import pkg_resources -import semver - -from .. import formats, server, util +from .. import util from ..device import interface, ui log = logging.getLogger(__name__) From 1518b7bde0e0eeeb0cfdb951b06e4020d8e2be00 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Tue, 25 Apr 2023 17:59:42 +0300 Subject: [PATCH 03/38] Mark 'libagent' package as stable --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9f61b5d0..0ee4eda9 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ platforms=['POSIX'], classifiers=[ 'Environment :: Console', - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: System Administrators', From bf7324ca89da8d56c7fd40f4402c7d711d21ef71 Mon Sep 17 00:00:00 2001 From: "Julian Smith, Main Street Ventures" Date: Thu, 11 May 2023 15:45:14 +1000 Subject: [PATCH 04/38] Update INSTALL.md Fix install step instruction --- doc/INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 8c82cd43..276f3dda 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -100,7 +100,7 @@ Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_ag Or, on Mac using Homebrew: ``` - $ homebrew install keepkey-agent + $ brew install keepkey-agent ``` Or, directly from the latest source code: From 2b49eacc01b45caa5c7da379b54378d58e60bdd7 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 3 Jun 2023 16:40:00 +0300 Subject: [PATCH 05/38] Run CI also on PRs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd699126..8202a251 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: Build -on: [push] +on: [push, pull_request] jobs: build: From 473a565fc6a32d36b2f00c1e5ba2881416bdfb42 Mon Sep 17 00:00:00 2001 From: Senjuu Date: Mon, 31 Jul 2023 11:51:47 +0200 Subject: [PATCH 06/38] Add Support for ED25519 ssh-certificates --- libagent/formats.py | 41 ++++++++++++++--------- libagent/tests/test_formats.py | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/libagent/formats.py b/libagent/formats.py index 28a911a1..c77d2124 100644 --- a/libagent/formats.py +++ b/libagent/formats.py @@ -21,14 +21,16 @@ ECDH_CURVE25519 = 'curve25519' # SSH key types +SSH_CERT_POSTFIX = b'-cert-v01@openssh.com' SSH_NIST256_DER_OCTET = b'\x04' SSH_NIST256_KEY_PREFIX = b'ecdsa-sha2-' SSH_NIST256_CURVE_NAME = b'nistp256' SSH_NIST256_KEY_TYPE = SSH_NIST256_KEY_PREFIX + SSH_NIST256_CURVE_NAME -SSH_NIST256_CERT_POSTFIX = b'-cert-v01@openssh.com' -SSH_NIST256_CERT_TYPE = SSH_NIST256_KEY_TYPE + SSH_NIST256_CERT_POSTFIX +SSH_NIST256_CERT_TYPE = SSH_NIST256_KEY_TYPE + SSH_CERT_POSTFIX SSH_ED25519_KEY_TYPE = b'ssh-ed25519' -SUPPORTED_KEY_TYPES = {SSH_NIST256_KEY_TYPE, SSH_NIST256_CERT_TYPE, SSH_ED25519_KEY_TYPE} +SSH_ED25519_CERT_TYPE = SSH_ED25519_KEY_TYPE + SSH_CERT_POSTFIX +SUPPORTED_KEY_TYPES = {SSH_NIST256_KEY_TYPE, SSH_NIST256_CERT_TYPE, + SSH_ED25519_KEY_TYPE, SSH_ED25519_CERT_TYPE} hashfunc = hashlib.sha256 @@ -43,6 +45,20 @@ def fingerprint(blob): return ':'.join('{:02x}'.format(c) for c in bytearray(digest)) +def __skip_certificate_fields(s): + _serial_number = util.recv(s, '>Q') + _type = util.recv(s, '>L') + _key_id = util.read_frame(s) + _valid_principals = util.read_frame(s) + _valid_after = util.recv(s, '>Q') + _valid_before = util.recv(s, '>Q') + _critical_options = util.read_frame(s) + _extensions = util.read_frame(s) + _reserved = util.read_frame(s) + _signature_key = util.read_frame(s) + _signature = util.read_frame(s) + + def parse_pubkey(blob): """ Parse SSH public key from given blob. @@ -69,18 +85,7 @@ def parse_pubkey(blob): point = util.read_frame(s) if key_type == SSH_NIST256_CERT_TYPE: - _serial_number = util.recv(s, '>Q') - _type = util.recv(s, '>L') - _key_id = util.read_frame(s) - _valid_principals = util.read_frame(s) - _valid_after = util.recv(s, '>Q') - _valid_before = util.recv(s, '>Q') - _critical_options = util.read_frame(s) - _extensions = util.read_frame(s) - _reserved = util.read_frame(s) - _signature_key = util.read_frame(s) - _signature = util.read_frame(s) - + __skip_certificate_fields(s) assert s.read() == b'' _type, point = point[:1], point[1:] assert _type == SSH_NIST256_DER_OCTET @@ -102,8 +107,12 @@ def ecdsa_verifier(sig, msg): result.update(point=coords, curve=CURVE_NIST256, verifier=ecdsa_verifier) - if key_type == SSH_ED25519_KEY_TYPE: + if key_type in (SSH_ED25519_KEY_TYPE, SSH_ED25519_CERT_TYPE): + if key_type == SSH_ED25519_CERT_TYPE: + _nonce = util.read_frame(s) pubkey = util.read_frame(s) + if key_type == SSH_ED25519_CERT_TYPE: + __skip_certificate_fields(s) assert s.read() == b'' def ed25519_verify(sig, msg): diff --git a/libagent/tests/test_formats.py b/libagent/tests/test_formats.py index e0a777d9..921cc05a 100644 --- a/libagent/tests/test_formats.py +++ b/libagent/tests/test_formats.py @@ -43,6 +43,57 @@ def test_fingerprint(): 'home\n' ) +_public_key_ed25519_cert = ( + 'ssh-ed25519-cert-v01@openssh.com ' + 'AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29' + 'tAAAAIK5TMdCnuxxy4rr0CTHLekAsnL4DAhFyksK5romkuw' + 'xgAAAAIFBdF2tjfSO8nLIi736is+f0erq28RTc7CkM11NZt' + 'TKRAAAAAAAAAAAAAAABAAAACXVuaXQtdGVzdAAAAA0AAAAJ' + 'dW5pdC10ZXN0AAAAAAAAAAD//////////wAAAAAAAACCAAA' + 'AFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybW' + 'l0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb' + '3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAA' + 'AAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAABoAAAAE2V' + 'jZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC' + 'HF5pUcZLVlTUBzos8ojyN34KrS7TnGAZINhRsCoNuRV4NFN' + 'IlEYpEvSwlumQuDx6B1y4Va+3pYzBbZInm6vwgAAABjAAAA' + 'E2VjZHNhLXNoYTItbmlzdHAyNTYAAABIAAAAICUMX1taTy6' + 'y+1Aa1m7kXHI/Qv7ZZIeNp7ndmCRLFCSuAAAAIBaX43k0Ye' + 'Bk8a5zp6FyFCBYVOtis/DUbGm07d7miPnE ' + 'hello\n' +) + +_public_key_ed25519_cert_BLOB = ( + b'\x00\x00\x00 ssh-ed25519-cert-v01@openssh.com' + b'\x00\x00\x00 \xaeS1\xd0\xa7\xbb\x1cr\xe2\xba' + b'\xf4\t1\xcbz@,\x9c\xbe\x03\x02\x11r\x92\xc2\xb9' + b'\xae\x89\xa4\xbb\x0c`\x00\x00\x00 P]\x17kc}#' + b'\xbc\x9c\xb2"\xef~\xa2\xb3\xe7\xf4z\xba\xb6\xf1' + b'\x14\xdc\xec)\x0c\xd7SY\xb52\x91\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00' + b'\x00\tunit-test\x00\x00\x00\r\x00\x00\x00\tun' + b'it-test\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff' + b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00' + b'\x00\x00\x82\x00\x00\x00\x15permit-X11-forwar' + b'ding\x00\x00\x00\x00\x00\x00\x00\x17permit-ag' + b'ent-forwarding\x00\x00\x00\x00\x00\x00\x00\x16' + b'permit-port-forwarding\x00\x00\x00\x00\x00\x00' + b'\x00\npermit-pty\x00\x00\x00\x00\x00\x00\x00' + b'\x0epermit-user-rc\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-n' + b'istp256\x00\x00\x00\x08nistp256\x00\x00\x00A' + b'\x04!\xc5\xe6\x95\x1cd\xb5eM@s\xa2\xcf(\x8f#w' + b'\xe0\xaa\xd2\xed9\xc6\x01\x92\r\x85\x1b\x02\xa0' + b'\xdb\x91W\x83E4\x89Db\x91/K\tn\x99\x0b\x83\xc7' + b'\xa0u\xcb\x85Z\xfbzX\xcc\x16\xd9"y\xba\xbf\x08' + b'\x00\x00\x00c\x00\x00\x00\x13ecdsa-sha2-nistp' + b'256\x00\x00\x00H\x00\x00\x00 %\x0c_[ZO.\xb2\xfb' + b'P\x1a\xd6n\xe4\\r?B\xfe\xd9d\x87\x8d\xa7\xb9' + b'\xdd\x98$K\x14$\xae\x00\x00\x00 \x16\x97\xe3y' + b'4a\xe0d\xf1\xaes\xa7\xa1r\x14 XT\xebb\xb3\xf0' + b'\xd4li\xb4\xed\xde\xe6\x88\xf9\xc4' +) + def test_parse_public_key(): key = formats.import_public_key(_public_key) @@ -86,6 +137,16 @@ def test_parse_ed25519(): assert p['type'] == b'ssh-ed25519' +def test_parse_ed25519_cert(): + p = formats.import_public_key(_public_key_ed25519_cert) + assert p['name'] == b'hello' + assert p['curve'] == 'ed25519' + + assert p['blob'] == _public_key_ed25519_cert_BLOB + assert p['fingerprint'] == '86:b6:17:3e:e1:5c:ba:e0:dc:86:80:b2:47:b4:ad:50' # nopep8 + assert p['type'] == b'ssh-ed25519-cert-v01@openssh.com' + + def test_export_ed25519(): pub = (b'\x00P]\x17kc}#\xbc\x9c\xb2"\xef~\xa2\xb3\xe7\xf4' b'z\xba\xb6\xf1\x14\xdc\xec)\x0c\xd7SY\xb52\x91') From 8cb323c5507b4cd757fae6483f05ea5fc7f7758a Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 12 Aug 2023 11:27:30 +0300 Subject: [PATCH 07/38] Update docs to reference `trezor-agent` instead of `trezor_agent` (#342) --- agents/fake/setup.py | 2 +- doc/INSTALL.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/agents/fake/setup.py b/agents/fake/setup.py index 71c4ae1d..11f52d8f 100644 --- a/agents/fake/setup.py +++ b/agents/fake/setup.py @@ -7,7 +7,7 @@ setup( name='fake_device_agent', version='0.9.0', - description='Testing trezor_agent with a fake device - NOT SAFE!!!', + description='Testing SSH/GPG agent with a fake device - NOT SAFE!!!', author='Roman Zeyde', author_email='roman.zeyde@gmail.com', url='http://github.com/romanz/trezor-agent', diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 276f3dda..86cbb1f0 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -63,7 +63,7 @@ gpg (GnuPG) 2.1.15 2. Make sure that your `udev` rules are configured [correctly](https://wiki.trezor.io/Udev_rules). -3. Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agent) package: +3. Then, install the latest [trezor-agent](https://pypi.python.org/pypi/trezor-agent) package: ``` $ pip3 install Cython hidapi @@ -91,7 +91,7 @@ gpg (GnuPG) 2.1.15 * [KeepKey firmware releases](https://github.com/keepkey/keepkey-firmware/releases): `3.0.17+` 2. 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: +Then, install the latest [keepkey-agent](https://pypi.python.org/pypi/keepkey-agent) package: ``` $ pip3 install keepkey_agent @@ -117,10 +117,10 @@ Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_ag * [Ledger Nano S firmware releases](https://github.com/LedgerHQ/blue-app-ssh-agent): `0.0.3+` (install [SSH/PGP Agent](https://www.ledgerwallet.com/images/apps/chrome-mngr-apps.png) app) 2. Make sure that your `udev` rules are configured [correctly](https://ledger.zendesk.com/hc/en-us/articles/115005165269-What-if-Ledger-Wallet-is-not-recognized-on-Linux-). -3. Then, install the latest [ledger_agent](https://pypi.python.org/pypi/ledger_agent) package: +3. Then, install the latest [ledger-agent](https://pypi.python.org/pypi/ledger-agent) package: ``` - $ pip3 install ledger_agent + $ pip3 install ledger-agent ``` Or, directly from the latest source code: From a247e877fc066a20b208917e9424c3a31ca79f4a Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 12 Aug 2023 11:32:24 +0300 Subject: [PATCH 08/38] No need to install Cython & hidapi --- doc/INSTALL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 86cbb1f0..7011e500 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -66,8 +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 trezor_agent + $ pip3 install trezor-agent ``` Or, directly from the latest source code: From 0acc6cd2efd46a59f22b81d442a37b1500bd2b7c Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 19 Aug 2023 15:36:21 +0300 Subject: [PATCH 09/38] Update email in setup.py --- agents/fake/setup.py | 2 +- agents/keepkey/setup.py | 2 +- agents/ledger/setup.py | 2 +- agents/trezor/setup.py | 2 +- setup.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/agents/fake/setup.py b/agents/fake/setup.py index 11f52d8f..451e908c 100644 --- a/agents/fake/setup.py +++ b/agents/fake/setup.py @@ -9,7 +9,7 @@ version='0.9.0', description='Testing SSH/GPG agent with a fake device - NOT SAFE!!!', author='Roman Zeyde', - author_email='roman.zeyde@gmail.com', + author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', scripts=['fake_device_agent.py'], install_requires=[ diff --git a/agents/keepkey/setup.py b/agents/keepkey/setup.py index f796a679..3b32d1cb 100644 --- a/agents/keepkey/setup.py +++ b/agents/keepkey/setup.py @@ -6,7 +6,7 @@ version='0.9.0', description='Using KeepKey as hardware SSH/GPG agent', author='Roman Zeyde', - author_email='roman.zeyde@gmail.com', + author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', scripts=['keepkey_agent.py'], install_requires=[ diff --git a/agents/ledger/setup.py b/agents/ledger/setup.py index efb70937..e4248b2b 100644 --- a/agents/ledger/setup.py +++ b/agents/ledger/setup.py @@ -6,7 +6,7 @@ version='0.9.0', description='Using Ledger as hardware SSH/GPG agent', author='Roman Zeyde', - author_email='roman.zeyde@gmail.com', + author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', scripts=['ledger_agent.py'], install_requires=[ diff --git a/agents/trezor/setup.py b/agents/trezor/setup.py index 2921a5c1..1c8582a4 100644 --- a/agents/trezor/setup.py +++ b/agents/trezor/setup.py @@ -6,7 +6,7 @@ version='0.12.0', description='Using Trezor as hardware SSH/GPG agent', author='Roman Zeyde', - author_email='roman.zeyde@gmail.com', + author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', scripts=['trezor_agent.py'], install_requires=[ diff --git a/setup.py b/setup.py index 0ee4eda9..57f6e0c6 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ version='0.14.7', description='Using hardware wallets as SSH/GPG agent', author='Roman Zeyde', - author_email='roman.zeyde@gmail.com', + author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', packages=[ 'libagent', From 28cbb941f16e6a309ba10a4a23f56e49038ed67a Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 19 Aug 2023 15:42:03 +0300 Subject: [PATCH 10/38] =?UTF-8?q?Bump=20version:=200.14.7=20=E2=86=92=200.?= =?UTF-8?q?14.8?= 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 8f9abdc4..ba8117e3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,6 +1,6 @@ [bumpversion] commit = True tag = True -current_version = 0.14.7 +current_version = 0.14.8 [bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index 57f6e0c6..8b59fb89 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='libagent', - version='0.14.7', + version='0.14.8', description='Using hardware wallets as SSH/GPG agent', author='Roman Zeyde', author_email='dev@romanzey.de', From 6776971b5a5b9778be99d2f458740d19e061013f Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 2 Sep 2023 13:03:22 +0300 Subject: [PATCH 11/38] Drop unneeded `contrib/` directory --- contrib/neopg-trezor | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100755 contrib/neopg-trezor diff --git a/contrib/neopg-trezor b/contrib/neopg-trezor deleted file mode 100755 index d65a0131..00000000 --- a/contrib/neopg-trezor +++ /dev/null @@ -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:]) From 23c6349c98967f39d9a5d8c803d2d1b6df4b2cc1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 2 Sep 2023 14:34:08 +0300 Subject: [PATCH 12/38] Verify that 'identity-v1' state machine is used Following https://github.com/romanz/trezor-agent/issues/426. --- libagent/age/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libagent/age/__init__.py b/libagent/age/__init__.py index 95fd03ac..93a3cf8a 100644 --- a/libagent/age/__init__.py +++ b/libagent/age/__init__.py @@ -172,8 +172,10 @@ def main(device_type): try: if args.identity: run_pubkey(device_type=device_type, args=args) - elif args.age_plugin: + elif args.age_plugin == 'identity-v1': run_decrypt(device_type=device_type, args=args) + else: + log.error("Unsupported state machine: %r", args.age_plugin) except Exception as e: # pylint: disable=broad-except log.exception("age plugin failed: %s", e) From 37485e5a539e0eae6376839db66fce36e6f7fe37 Mon Sep 17 00:00:00 2001 From: doolio Date: Wed, 6 Sep 2023 13:14:41 +0200 Subject: [PATCH 13/38] Update README to include Blockstream Jade ...as a supported device --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f9a828e..3dfe4eb3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ See the following blog posts about this tool: - [TREZOR Firmware 1.4.0 — GPG decryption support](https://www.reddit.com/r/TREZOR/comments/50h8r9/new_trezor_firmware_fidou2f_and_initial_ethereum/d7420q7/) - [A Step by Step Guide to Securing your SSH Keys with the Ledger Nano S](https://thoughts.t37.net/a-step-by-step-guide-to-securing-your-ssh-keys-with-the-ledger-nano-s-92e58c64a005) -Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Keepkey](https://www.keepkey.com/), [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s), and [OnlyKey](https://onlykey.io) are supported. +Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Keepkey](https://www.keepkey.com/), [Blockstream Jade](https://blockstream.com/jade/), [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s), and [OnlyKey](https://onlykey.io) are supported. ## Components From 81de093ddb084712f3703360225a2d9cb440a28b Mon Sep 17 00:00:00 2001 From: doolio Date: Wed, 6 Sep 2023 13:26:14 +0200 Subject: [PATCH 14/38] Update description in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8b59fb89..34425c83 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='libagent', version='0.14.8', - description='Using hardware wallets as SSH/GPG agent', + description='Using hardware wallets as SSH/GPG/age agent', author='Roman Zeyde', author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', From 3c911e99a0394278104564092225d67c75e74b99 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 6 Sep 2023 19:41:26 +0300 Subject: [PATCH 15/38] Fix JADE link in `README.md` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3dfe4eb3..053fe4c2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ agents to interact with several different hardware devices: * [`libagent`](https://pypi.org/project/libagent/): shared library * [`trezor-agent`](https://pypi.org/project/trezor-agent/): Using Trezor as hardware-based SSH/PGP/age agent * [`ledger_agent`](https://pypi.org/project/ledger_agent/): Using Ledger as hardware-based SSH/PGP agent -* [`jade_agent`](https://pypi.org/project/jade_agent/): Using Blockstream Jade as hardware-based SSH/PGP agent +* [`jade_agent`](https://github.com/Blockstream/Jade/): Using Blockstream Jade as hardware-based SSH/PGP agent * [`keepkey_agent`](https://pypi.org/project/keepkey_agent/): Using KeepKey as hardware-based SSH/PGP agent * [`onlykey-agent`](https://pypi.org/project/onlykey-agent/): Using OnlyKey as hardware-based SSH/PGP agent From de6dec340d1cb7c2804bd06b91fdc23d350f7007 Mon Sep 17 00:00:00 2001 From: SlugFiller <5435495+SlugFiller@users.noreply.github.com> Date: Fri, 15 Sep 2023 00:34:12 +0300 Subject: [PATCH 16/38] Add concurrency tag to CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8202a251..86f07aea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,10 @@ name: Build on: [push, pull_request] +concurrency: + group: ci-${{github.actor}}-${{github.head_ref || github.run_number}}-${{github.ref}} + cancel-in-progress: true + jobs: build: From a35d9ddde86a5d0361c29584e04f45e78b0047ad Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Nov 2023 20:54:53 +0200 Subject: [PATCH 17/38] Bump CI actions and test on Python 3.12 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8202a251..b41c2d8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 68e39c14216f466c8710bf65ef133c744f8f92da Mon Sep 17 00:00:00 2001 From: Branch Vincent Date: Wed, 24 Apr 2024 20:03:04 -0700 Subject: [PATCH 18/38] replace pkg_resources for python 3.12 --- libagent/age/__init__.py | 7 +++---- libagent/gpg/__init__.py | 7 +++---- libagent/ssh/__init__.py | 7 +++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/libagent/age/__init__.py b/libagent/age/__init__.py index dd2fbe66..e20cb3c4 100644 --- a/libagent/age/__init__.py +++ b/libagent/age/__init__.py @@ -13,9 +13,9 @@ import logging import os import sys +from importlib import metadata import bech32 -import pkg_resources from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 @@ -150,9 +150,8 @@ def main(device_type): p = argparse.ArgumentParser() agent_package = device_type.package_name() - resources_map = {r.key: r for r in pkg_resources.require(agent_package)} - resources = [resources_map[agent_package], resources_map['libagent']] - versions = '\n'.join('{}={}'.format(r.key, r.version) for r in resources) + resources = [metadata.distribution(agent_package), metadata.distribution('libagent')] + versions = '\n'.join('{}={}'.format(r.metadata['Name'], r.version) for r in resources) p.add_argument('--version', help='print the version info', action='version', version=versions) diff --git a/libagent/gpg/__init__.py b/libagent/gpg/__init__.py index 6bad4f65..4f1b166d 100644 --- a/libagent/gpg/__init__.py +++ b/libagent/gpg/__init__.py @@ -17,13 +17,13 @@ import stat import subprocess import sys +from importlib import metadata try: # TODO: Not supported on Windows. Use daemoniker instead? import daemon except ImportError: daemon = None -import pkg_resources import semver from .. import device, formats, server, util @@ -308,9 +308,8 @@ def main(device_type): parser = argparse.ArgumentParser(epilog=epilog) agent_package = device_type.package_name() - resources_map = {r.key: r for r in pkg_resources.require(agent_package)} - resources = [resources_map[agent_package], resources_map['libagent']] - versions = '\n'.join('{}={}'.format(r.key, r.version) for r in resources) + resources = [metadata.distribution(agent_package), metadata.distribution('libagent')] + versions = '\n'.join('{}={}'.format(r.metadata['Name'], r.version) for r in resources) parser.add_argument('--version', help='print the version info', action='version', version=versions) diff --git a/libagent/ssh/__init__.py b/libagent/ssh/__init__.py index dee3ee24..14f2656d 100644 --- a/libagent/ssh/__init__.py +++ b/libagent/ssh/__init__.py @@ -13,6 +13,7 @@ import sys import tempfile import threading +from importlib import metadata import configargparse @@ -21,7 +22,6 @@ import daemon except ImportError: daemon = None -import pkg_resources from .. import device, formats, server, util from . import client, protocol @@ -83,9 +83,8 @@ def create_agent_parser(device_type): p.add_argument('-v', '--verbose', default=0, action='count') agent_package = device_type.package_name() - resources_map = {r.key: r for r in pkg_resources.require(agent_package)} - resources = [resources_map[agent_package], resources_map['libagent']] - versions = '\n'.join('{}={}'.format(r.key, r.version) for r in resources) + resources = [metadata.distribution(agent_package), metadata.distribution('libagent')] + versions = '\n'.join('{}={}'.format(r.metadata['Name'], r.version) for r in resources) p.add_argument('--version', help='print the version info', action='version', version=versions) From b958b08f6ba4f4f21911da4f69e69d8ef3424e07 Mon Sep 17 00:00:00 2001 From: Branch Vincent Date: Wed, 24 Apr 2024 20:13:16 -0700 Subject: [PATCH 19/38] bump python to 3.8+ --- .github/workflows/ci.yml | 2 +- agents/fake/setup.py | 1 + agents/jade/setup.py | 1 + agents/keepkey/setup.py | 1 + agents/ledger/setup.py | 1 + agents/onlykey/setup.py | 1 + agents/trezor/setup.py | 1 + setup.py | 1 + 8 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3f2a532..f2c70af9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 diff --git a/agents/fake/setup.py b/agents/fake/setup.py index 451e908c..e84a50b9 100644 --- a/agents/fake/setup.py +++ b/agents/fake/setup.py @@ -11,6 +11,7 @@ author='Roman Zeyde', author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', + python_requires='>=3.8', scripts=['fake_device_agent.py'], install_requires=[ 'libagent>=0.9.0', diff --git a/agents/jade/setup.py b/agents/jade/setup.py index 96903b68..7b4e80b4 100644 --- a/agents/jade/setup.py +++ b/agents/jade/setup.py @@ -8,6 +8,7 @@ author='Jamie C. Driver', author_email='jamie@blockstream.com', url='http://github.com/romanz/trezor-agent', + python_requires='>=3.8', scripts=['jade_agent.py'], install_requires=[ 'libagent>=0.14.5', diff --git a/agents/keepkey/setup.py b/agents/keepkey/setup.py index 3b32d1cb..f68aa838 100644 --- a/agents/keepkey/setup.py +++ b/agents/keepkey/setup.py @@ -8,6 +8,7 @@ author='Roman Zeyde', author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', + python_requires='>=3.8', scripts=['keepkey_agent.py'], install_requires=[ 'libagent>=0.9.0', diff --git a/agents/ledger/setup.py b/agents/ledger/setup.py index e4248b2b..b4bb08f6 100644 --- a/agents/ledger/setup.py +++ b/agents/ledger/setup.py @@ -8,6 +8,7 @@ author='Roman Zeyde', author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', + python_requires='>=3.8', scripts=['ledger_agent.py'], install_requires=[ 'libagent>=0.9.0', diff --git a/agents/onlykey/setup.py b/agents/onlykey/setup.py index 140fb1fb..c333c7bd 100644 --- a/agents/onlykey/setup.py +++ b/agents/onlykey/setup.py @@ -8,6 +8,7 @@ author='CryptoTrust', author_email='t@crp.to', url='http://github.com/trustcrypto/onlykey-agent', + python_requires='>=3.8', scripts=['onlykey_agent.py'], install_requires=[ 'libagent>=0.14.2', diff --git a/agents/trezor/setup.py b/agents/trezor/setup.py index 1c8582a4..28d21736 100644 --- a/agents/trezor/setup.py +++ b/agents/trezor/setup.py @@ -8,6 +8,7 @@ author='Roman Zeyde', author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', + python_requires='>=3.8', scripts=['trezor_agent.py'], install_requires=[ 'libagent>=0.14.0', diff --git a/setup.py b/setup.py index aefdcdc1..efe0b52b 100755 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ author='Roman Zeyde', author_email='dev@romanzey.de', url='http://github.com/romanz/trezor-agent', + python_requires='>=3.8', packages=[ 'libagent', 'libagent.age', From f183758bbe39eda0c087d4c53cf17eb5a22609c1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 5 Sep 2024 21:22:53 +0300 Subject: [PATCH 20/38] Sign tags via bumpversion --- .bumpversion.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ba8117e3..0f73ab22 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,4 +3,6 @@ commit = True tag = True current_version = 0.14.8 +sign_tags = True + [bumpversion:file:setup.py] From 868975fb0cf2941bad51d283f64e1661ace4c8f4 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 5 Sep 2024 21:24:45 +0300 Subject: [PATCH 21/38] =?UTF-8?q?Bump=20version:=200.14.8=20=E2=86=92=200.?= =?UTF-8?q?15.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 3 +-- setup.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0f73ab22..e5b12595 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,8 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 0.14.8 - +current_version = 0.15.0 sign_tags = True [bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index aefdcdc1..10586f2e 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='libagent', - version='0.14.8', + version='0.15.0', description='Using hardware wallets as SSH/GPG/age agent', author='Roman Zeyde', author_email='dev@romanzey.de', From 5e809c0c0afef2642d7b601a295a27f2d2522d8e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 5 Sep 2024 21:40:07 +0300 Subject: [PATCH 22/38] Remove releases section --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 0bb3dd2a..f31ff16b 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,6 @@ agents to interact with several different hardware devices: * [`keepkey_agent`](https://pypi.org/project/keepkey_agent/): Using KeepKey as hardware-based SSH/PGP agent * [`onlykey-agent`](https://pypi.org/project/onlykey-agent/): Using OnlyKey as hardware-based SSH/PGP agent - -The [/releases](/releases) page on Github contains the `libagent` -releases. - ## Documentation * **Installation** instructions are [here](doc/INSTALL.md) From e06f913faca0d981803608cb93e846cd065f6f33 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 8 Nov 2024 14:48:28 +0200 Subject: [PATCH 23/38] Test on Python 3.13 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3f2a532..5775ef36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 From 87f71174813300cea0f1fbd825d5e6fbc0b7b5d6 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Nov 2024 13:13:16 +0200 Subject: [PATCH 24/38] Parse SSH identity with spaces --- libagent/formats.py | 2 +- libagent/tests/test_formats.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libagent/formats.py b/libagent/formats.py index c77d2124..26f91401 100644 --- a/libagent/formats.py +++ b/libagent/formats.py @@ -225,7 +225,7 @@ def export_public_key(vk, label): def import_public_key(line): """Parse public key textual format, as saved at a .pub file.""" log.debug('loading SSH public key: %r', line) - file_type, base64blob, name = line.split() + file_type, base64blob, name = line.strip().split(maxsplit=2) blob = base64.b64decode(base64blob) result = parse_pubkey(blob) result['name'] = name.encode('utf-8') diff --git a/libagent/tests/test_formats.py b/libagent/tests/test_formats.py index 921cc05a..3a0c8a26 100644 --- a/libagent/tests/test_formats.py +++ b/libagent/tests/test_formats.py @@ -60,7 +60,7 @@ def test_fingerprint(): 'E2VjZHNhLXNoYTItbmlzdHAyNTYAAABIAAAAICUMX1taTy6' 'y+1Aa1m7kXHI/Qv7ZZIeNp7ndmCRLFCSuAAAAIBaX43k0Ye' 'Bk8a5zp6FyFCBYVOtis/DUbGm07d7miPnE ' - 'hello\n' + 'hello world\n' ) _public_key_ed25519_cert_BLOB = ( @@ -139,7 +139,7 @@ def test_parse_ed25519(): def test_parse_ed25519_cert(): p = formats.import_public_key(_public_key_ed25519_cert) - assert p['name'] == b'hello' + assert p['name'] == b'hello world' assert p['curve'] == 'ed25519' assert p['blob'] == _public_key_ed25519_cert_BLOB From f1fe7b516253d9ff3c3a0edc4d6b93a329d4b64e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 13 Nov 2024 17:12:21 +0200 Subject: [PATCH 25/38] Support SSH CA generation Fixes https://github.com/romanz/trezor-agent/issues/491. Usage example: ## generate TREZOR-based SSH CA public key $ trezor-agent -v 'SSH Certificate Authority' > /etc/ssh/trezor-ca.pub $ echo 'TrustedUserCAKeys /etc/ssh/trezor-ca.pub' | sudo tee -a /etc/ssh/sshd_config $ sudo systemctl restart ssh ## generate user-specific SSH key and certify it using trezor-agent $ ssh-keygen -t ed25519 -f user-key $ trezor-agent -v 'SSH Certificate Authority' -- \ ssh-keygen -Us trezor-ca.pub -V '+10m' -I user-id -n user user-key.pub ... Signed user key user-key-cert.pub: id "user-id" serial 0 for user valid from 2024-11-23T20:25:00 to 2024-11-23T20:36:27 ## use the certificate to login ssh -v user@localhost -o CertificateFile=user-key-cert.pub -i user-key ... debug1: Will attempt key: user-key-cert.pub ED25519-CERT SHA256:xdbgtQmUs5tUNf04f4Y3oQl5LGdBAMVjCH63R6EHH5Y explicit debug1: Will attempt key: user-key ED25519 SHA256:xdbgtQmUs5tUNf04f4Y3oQl5LGdBAMVjCH63R6EHH5Y explicit ... debug1: Offering public key: user-key-cert.pub ED25519-CERT SHA256:xdbgtQmUs5tUNf04f4Y3oQl5LGdBAMVjCH63R6EHH5Y explicit debug1: Server accepts key: user-key-cert.pub ED25519-CERT SHA256:xdbgtQmUs5tUNf04f4Y3oQl5LGdBAMVjCH63R6EHH5Y explicit Authenticated to localhost ([::1]:22) using "publickey". ... --- libagent/ssh/client.py | 70 +++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/libagent/ssh/client.py b/libagent/ssh/client.py index aa3b47cc..e2b2684b 100644 --- a/libagent/ssh/client.py +++ b/libagent/ssh/client.py @@ -10,6 +10,11 @@ log = logging.getLogger(__name__) +SUPPORTED_CERT_TYPES = { + formats.SSH_ED25519_CERT_TYPE, + formats.SSH_NIST256_CERT_TYPE, +} + class Client: """Client wrapper for SSH authentication device.""" @@ -31,22 +36,17 @@ def export_public_keys(self, identities): def sign_ssh_challenge(self, blob, identity): """Sign given blob using a private key on the device.""" - log.debug('blob: %r', blob) + log.debug('blob (%d bytes): %r', len(blob), blob) msg = parse_ssh_blob(blob) + log.debug('parsed: %r', msg) + + identity_str = identity.to_string() if msg['sshsig']: log.info('please confirm "%s" signature for "%s" using %s...', - msg['namespace'], identity.to_string(), self.device) + msg['namespace'], identity_str, self.device) else: - log.debug('%s: user %r via %r (%r)', - msg['conn'], msg['user'], msg['auth'], msg['key_type']) - log.debug('nonce: %r', msg['nonce']) - fp = msg['public_key']['fingerprint'] - log.debug('fingerprint: %s', fp) - log.debug('hidden challenge size: %d bytes', len(blob)) - - log.info('please confirm user "%s" login to "%s" using %s...', - msg['user'].decode('ascii'), identity.to_string(), - self.device) + log.info('please confirm "%s" signature for "%s" using %s...', + msg['key_type'].decode('ascii'), identity_str, self.device) with self.device: return self.device.sign(blob=blob, identity=identity) @@ -66,17 +66,45 @@ def parse_ssh_blob(data): else: i = io.BytesIO(data) res['sshsig'] = False - res['nonce'] = util.read_frame(i) - i.read(1) # SSH2_MSG_USERAUTH_REQUEST == 50 (from ssh2.h, line 108) - res['user'] = util.read_frame(i) - res['conn'] = util.read_frame(i) - res['auth'] = util.read_frame(i) - i.read(1) # have_sig == 1 (from sshconnect2.c, line 1056) - res['key_type'] = util.read_frame(i) - public_key = util.read_frame(i) - res['public_key'] = formats.parse_pubkey(public_key) + first_frame = util.read_frame(i) + # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys + is_cert = first_frame in SUPPORTED_CERT_TYPES + if is_cert: + # see `sshkey_certify_custom()` for details: + # https://github.com/openssh/openssh-portable/blob/master/sshkey.c + res['key_type'] = first_frame + res['nonce'] = util.read_frame(i) + if first_frame == formats.SSH_NIST256_CERT_TYPE: + res['curve'] = util.read_frame(i) + res['pubkey'] = util.read_frame(i) + res['serial_number'] = util.recv(i, '>Q') + res['type'] = util.recv(i, '>L') + res['key_id'] = util.read_frame(i) + res['valid_principals'] = tuple(_iter_parse_list(util.read_frame(i))) + res['valid_after'] = util.recv(i, '>Q') + res['valid_before'] = util.recv(i, '>Q') + res['critical_options'] = tuple(_iter_parse_list(util.read_frame(i))) + res['extensions'] = tuple(_iter_parse_list(util.read_frame(i))) + res['reserved'] = util.read_frame(i) + res['signature_key'] = util.read_frame(i) + else: + res['nonce'] = first_frame + i.read(1) # SSH2_MSG_USERAUTH_REQUEST == 50 (from ssh2.h, line 108) + res['user'] = util.read_frame(i) + res['conn'] = util.read_frame(i) + res['auth'] = util.read_frame(i) + i.read(1) # have_sig == 1 (from sshconnect2.c, line 1056) + res['key_type'] = util.read_frame(i) + public_key = util.read_frame(i) + res['public_key'] = formats.parse_pubkey(public_key) unparsed = i.read() if unparsed: log.warning('unparsed blob: %r', unparsed) return res + + +def _iter_parse_list(blob): + i = io.BytesIO(blob) + while i.tell() < len(blob): + yield util.read_frame(i) From e8e033fb0bd6e985ecf49a802f490dbd971a1676 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 25 Dec 2024 17:27:33 +0200 Subject: [PATCH 26/38] Dedup sending age response --- libagent/age/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/libagent/age/__init__.py b/libagent/age/__init__.py index e20cb3c4..4ba7f6bd 100644 --- a/libagent/age/__init__.py +++ b/libagent/age/__init__.py @@ -121,9 +121,7 @@ def run_decrypt(device_type, args): for file_index, stanzas in stanza_map.items(): _handle_single_file(file_index, stanzas, identities, c) - sys.stdout.buffer.write('-> done\n\n'.encode()) - sys.stdout.flush() - sys.stdout.close() + send('-> done\n\n') def _handle_single_file(file_index, stanzas, identities, c): @@ -131,20 +129,25 @@ def _handle_single_file(file_index, stanzas, identities, c): for peer_pubkey, encrypted in stanzas: for identity in identities: id_str = identity.to_string() - msg = base64_encode(f'Please confirm {id_str} decryption on {d} device...'.encode()) - sys.stdout.buffer.write(f'-> msg\n{msg}\n'.encode()) - sys.stdout.flush() + msg = f'Please confirm {id_str} decryption on {d} device...' + send(f'-> msg\n{base64_encode(msg.encode())}\n') key = c.ecdh(identity=identity, peer_pubkey=peer_pubkey) + result = decrypt(key=key, encrypted=encrypted) if not result: continue - sys.stdout.buffer.write(f'-> file-key {file_index}\n{base64_encode(result)}\n'.encode()) - sys.stdout.flush() + send(f'-> file-key {file_index}\n{base64_encode(result)}\n') return +def send(msg): + """Send a response back to `age` binary.""" + sys.stdout.buffer.write(msg.encode()) + sys.stdout.flush() + + def main(device_type): """Parse command-line arguments.""" p = argparse.ArgumentParser() From 82f46355ad4ae0a3d63b10bd10d1751a369eb836 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 23 Nov 2025 15:27:32 +0100 Subject: [PATCH 27/38] Parse SSH server host key as well https://github.com/openssh/openssh-portable/commit/266678e19eb0e86fdf865b431b6e172e7a95bf48 --- libagent/ssh/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libagent/ssh/client.py b/libagent/ssh/client.py index e2b2684b..ab3675a2 100644 --- a/libagent/ssh/client.py +++ b/libagent/ssh/client.py @@ -97,6 +97,8 @@ def parse_ssh_blob(data): res['key_type'] = util.read_frame(i) public_key = util.read_frame(i) res['public_key'] = formats.parse_pubkey(public_key) + if res['auth'] == b'publickey-hostbound-v00@openssh.com': + res['server_host_key'] = formats.parse_pubkey(util.read_frame(i)) unparsed = i.read() if unparsed: From 60bed0f411595ea1b0bb122fafdd0852e10d1eec Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 31 Jan 2026 15:46:48 +0100 Subject: [PATCH 28/38] Drop keepkey support Also, simplify invocation examples. --- README.md | 3 +-- agents/keepkey/keepkey_agent.py | 5 ---- agents/keepkey/setup.py | 39 ---------------------------- doc/DESIGN.md | 4 +-- doc/INSTALL.md | 34 +++---------------------- doc/README-GPG.md | 6 ++--- doc/README-PINENTRY.md | 2 +- doc/README-SSH.md | 18 ++++++------- libagent/device/keepkey.py | 45 --------------------------------- libagent/device/keepkey_defs.py | 24 ------------------ libagent/ssh/__init__.py | 2 +- 11 files changed, 18 insertions(+), 164 deletions(-) delete mode 100644 agents/keepkey/keepkey_agent.py delete mode 100644 agents/keepkey/setup.py delete mode 100644 libagent/device/keepkey.py delete mode 100644 libagent/device/keepkey_defs.py diff --git a/README.md b/README.md index f31ff16b..3515c259 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ See the following blog posts about this tool: - [TREZOR Firmware 1.4.0 — GPG decryption support](https://www.reddit.com/r/TREZOR/comments/50h8r9/new_trezor_firmware_fidou2f_and_initial_ethereum/d7420q7/) - [A Step by Step Guide to Securing your SSH Keys with the Ledger Nano S](https://thoughts.t37.net/a-step-by-step-guide-to-securing-your-ssh-keys-with-the-ledger-nano-s-92e58c64a005) -Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Keepkey](https://www.keepkey.com/), [Blockstream Jade](https://blockstream.com/jade/), [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s), and [OnlyKey](https://onlykey.io) are supported. +Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Blockstream Jade](https://blockstream.com/jade/), [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s), and [OnlyKey](https://onlykey.io) are supported. ## Components @@ -25,7 +25,6 @@ agents to interact with several different hardware devices: * [`trezor-agent`](https://pypi.org/project/trezor-agent/): Using Trezor as hardware-based SSH/PGP/age agent * [`ledger_agent`](https://pypi.org/project/ledger_agent/): Using Ledger as hardware-based SSH/PGP agent * [`jade_agent`](https://github.com/Blockstream/Jade/): Using Blockstream Jade as hardware-based SSH/PGP agent -* [`keepkey_agent`](https://pypi.org/project/keepkey_agent/): Using KeepKey as hardware-based SSH/PGP agent * [`onlykey-agent`](https://pypi.org/project/onlykey-agent/): Using OnlyKey as hardware-based SSH/PGP agent ## Documentation diff --git a/agents/keepkey/keepkey_agent.py b/agents/keepkey/keepkey_agent.py deleted file mode 100644 index 03d8ee5e..00000000 --- a/agents/keepkey/keepkey_agent.py +++ /dev/null @@ -1,5 +0,0 @@ -import libagent.gpg -import libagent.ssh -from libagent.device import keepkey - -ssh_agent = lambda: libagent.ssh.main(keepkey.KeepKey) diff --git a/agents/keepkey/setup.py b/agents/keepkey/setup.py deleted file mode 100644 index f68aa838..00000000 --- a/agents/keepkey/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/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='dev@romanzey.de', - url='http://github.com/romanz/trezor-agent', - python_requires='>=3.8', - 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', - ]}, -) diff --git a/doc/DESIGN.md b/doc/DESIGN.md index 2a627515..bc7d10ff 100644 --- a/doc/DESIGN.md +++ b/doc/DESIGN.md @@ -6,7 +6,7 @@ SSH and GPG do this by means of a simple interprocess communication protocol (us These two agents make the connection between the front end (e.g. a `gpg --sign` command, or an `ssh user@fqdn`). And then they wait for a request from the 'front end', and then do the actual asking for a password and subsequent using the private key to sign or decrypt something. -The various hardware wallets (Trezor, KeepKey, Ledger and Jade) each have the ability (as of Firmware 1.3.4) to use the NIST P-256 elliptic curve to sign, encrypt or decrypt. This curve can be used with S/MIME, GPG and SSH. +The various hardware wallets have the ability to use the NIST P-256 elliptic curve to sign, encrypt or decrypt. This curve can be used with S/MIME, GPG and SSH. So when you `ssh` to a machine - rather than consult the normal ssh-agent (which in turn will use your private SSH key in files such as `~/.ssh/id_rsa`) -- the trezor-agent will aks your hardware wallet to use its private key to sign the challenge. @@ -38,8 +38,6 @@ The `trezor-agent` then instructs SSH to connect to the server. It will then eng GPG uses much the same approach as SSH, except in this case it relies on [SLIP-0017 : ECDH using deterministic hierarchy][3] for the mapping to an ECDH key and it maps these to the normal GPG child key infrastructure. -Note: Keepkey does not support en-/de-cryption at this time. - ### 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. diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 7011e500..9696e6a6 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -83,33 +83,7 @@ gpg (GnuPG) 2.1.15 $ brew install trezor-agent ``` -# 3. Install the KeepKey agent - -1. Make sure you are running the latest firmware version on your KeepKey: - - * [KeepKey firmware releases](https://github.com/keepkey/keepkey-firmware/releases): `3.0.17+` - -2. 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: - - ``` - $ pip3 install keepkey_agent - ``` - - Or, on Mac using Homebrew: - - ``` - $ brew install keepkey-agent - ``` - - Or, directly from the latest source code: - - ``` - $ git clone https://github.com/romanz/trezor-agent - $ pip3 install --user -e trezor-agent/agents/keepkey - ``` - -# 4. Install the Ledger Nano S agent +# 3. Install the Ledger Nano S agent 1. Make sure you are running the latest firmware version on your Ledger Nano S: @@ -130,7 +104,7 @@ Then, install the latest [keepkey-agent](https://pypi.python.org/pypi/keepkey-ag $ pip3 install --user -e trezor-agent/agents/ledger ``` -# 5. Install the OnlyKey agent +# 4. Install the OnlyKey agent 1. Make sure you are running the latest firmware version on your OnlyKey: @@ -151,7 +125,7 @@ Then, install the latest [keepkey-agent](https://pypi.python.org/pypi/keepkey-ag $ pip3 install --user -e trezor-agent/agents/onlykey ``` -# 6. Install the Blockstream Jade agent +# 5. Install the Blockstream Jade agent 1. Make sure you are running the latest firmware version on your Blockstream Jade: @@ -175,7 +149,7 @@ Then, install the latest [keepkey-agent](https://pypi.python.org/pypi/keepkey-ag $ pip3 install --user -e trezor-agent/agents/jade ``` -# 7. Installation Troubleshooting +# 6. Installation Troubleshooting If there is an import problem with the installed `protobuf` package, see [this issue](https://github.com/romanz/trezor-agent/issues/28) for fixing it. diff --git a/doc/README-GPG.md b/doc/README-GPG.md index 3542ec48..89076caf 100644 --- a/doc/README-GPG.md +++ b/doc/README-GPG.md @@ -18,14 +18,14 @@ Thanks! Run ``` - $ (trezor|keepkey|ledger|jade|onlykey)-gpg init "Roman Zeyde " + $ trezor-gpg init "Roman Zeyde " ``` Follow the instructions provided to complete the setup. Keep note of the timestamp value which you'll need if you want to regenerate the key later. If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md). -2. Add `export GNUPGHOME=~/.gnupg/(trezor|keepkey|ledger|jade|onlykey)` to your `.bashrc` or other environment file. +2. Add `export GNUPGHOME=~/.gnupg/trezor` to your `.bashrc` or other environment file. This `GNUPGHOME` contains your hardware keyring and agent settings. This agent software assumes all keys are backed by hardware devices so you can't use standard GPG keys in `GNUPGHOME` (if you do mix keys you'll receive an error when you attempt to use them). @@ -203,8 +203,6 @@ Follow [these instructions](enigmail.md) to set up Enigmail in Thunderbird. ##### 1. Create these files in `~/.config/systemd/user` -Replace `trezor` with `keepkey` or `ledger` or `jade` or `onlykey` as required. - ###### `trezor-gpg-agent.service` ```` diff --git a/doc/README-PINENTRY.md b/doc/README-PINENTRY.md index 17aa5c88..31125e3c 100644 --- a/doc/README-PINENTRY.md +++ b/doc/README-PINENTRY.md @@ -45,7 +45,7 @@ to the `[Service]` section to tell the PIN entry program how to connect to the X If you haven't completed initialization yet, run: ``` -$ (trezor|keepkey|ledger)-gpg init --pin-entry-binary trezor-gpg-pinentry-tk "Roman Zeyde " +$ trezor-gpg init --pin-entry-binary trezor-gpg-pinentry-tk "Roman Zeyde " ``` to configure the PIN entry at the same time. diff --git a/doc/README-SSH.md b/doc/README-SSH.md index 5a84127f..cf9b4c17 100644 --- a/doc/README-SSH.md +++ b/doc/README-SSH.md @@ -4,13 +4,13 @@ SSH requires no configuration, but you may put common command line options in `~/.ssh/agent.conf` to avoid repeating them in every invocation. -See `(trezor|keepkey|ledger|jade|onlykey)-agent -h` for details on supported options and the configuration file format. +See `trezor-agent -h` for details on supported options and the configuration file format. If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md). ## Usage -Use the `(trezor|keepkey|ledger|jade|onlykey)-agent` program to work with SSH. It has three main modes of operation: +Use the `trezor-agent` program to work with SSH. It has three main modes of operation: ##### 1. Export public keys @@ -18,7 +18,7 @@ To get your public key so you can add it to `authorized_hosts` or allow ssh access to a service that supports it, run: ``` -(trezor|keepkey|ledger|jade|onlykey)-agent identity@myhost +trezor-agent identity@myhost ``` The identity (ex: `identity@myhost`) is used to derive the public key and is added as a comment to the exported key string. @@ -28,7 +28,7 @@ The identity (ex: `identity@myhost`) is used to derive the public key and is add Run ``` -$ (trezor|keepkey|ledger|jade|onlykey)-agent identity@myhost -- COMMAND --WITH --ARGUMENTS +$ trezor-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. @@ -36,23 +36,23 @@ Note the `--` separator, which is used to separate `trezor-agent`'s arguments fr Example: ``` - (trezor|keepkey|ledger|jade|onlykey)-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob + trezor-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob ``` As a shortcut you can run ``` -$ (trezor|keepkey|ledger|jade|onlykey)-agent identity@myhost -s +$ trezor-agent identity@myhost -s ``` to start a shell with the proper environment. -##### 3. Connect to a server directly via `(trezor|keepkey|ledger|jade|onlykey)-agent` +##### 3. Connect to a server directly via `trezor-agent` If you just want to connect to a server this is the simplest way to do it: ``` -$ (trezor|keepkey|ledger|jade|onlykey)-agent user@remotehost -c +$ trezor-agent user@remotehost -c ``` The identity `user@remotehost` is used as both the destination user and host as well as for key derivation, so you must generate a separate key for each host you connect to. @@ -154,8 +154,6 @@ For more details, see the following great blog post: https://calebhearth.com/sig ##### 1. Create these files in `~/.config/systemd/user` -Replace `trezor` with `keepkey` or `ledger` or `jade` or `onlykey` as required. - ###### `trezor-ssh-agent.service` ```` diff --git a/libagent/device/keepkey.py b/libagent/device/keepkey.py deleted file mode 100644 index a8d265b6..00000000 --- a/libagent/device/keepkey.py +++ /dev/null @@ -1,45 +0,0 @@ -"""KeepKey-related code (see https://www.keepkey.com/).""" - -from .. import formats -from . import trezor - - -def _verify_support(identity, ecdh): - """Make sure the device supports given configuration.""" - protocol = identity.identity_dict['proto'] - if protocol not in {'ssh'}: - raise NotImplementedError( - 'Unsupported protocol: {}'.format(protocol)) - if ecdh: - raise NotImplementedError('No support for ECDH') - if identity.curve_name not in {formats.CURVE_NIST256}: - raise NotImplementedError( - 'Unsupported elliptic curve: {}'.format(identity.curve_name)) - - -class KeepKey(trezor.Trezor): - """Connection to KeepKey device.""" - - @classmethod - def package_name(cls): - """Python package name (at PyPI).""" - return 'keepkey-agent' - - @property - def _defs(self): - from . import keepkey_defs - return keepkey_defs - - required_version = '>=1.0.4' - - def _override_state_handler(self, _): - """No support for `state` handling on Keepkey.""" - - def pubkey(self, identity, ecdh=False): - """Return public key.""" - _verify_support(identity, ecdh) - return trezor.Trezor.pubkey(self, identity=identity, ecdh=ecdh) - - def ecdh(self, identity, pubkey): - """No support for ECDH in KeepKey firmware.""" - _verify_support(identity, ecdh=True) diff --git a/libagent/device/keepkey_defs.py b/libagent/device/keepkey_defs.py deleted file mode 100644 index 85149613..00000000 --- a/libagent/device/keepkey_defs.py +++ /dev/null @@ -1,24 +0,0 @@ -"""KeepKey-related definitions.""" - -# pylint: disable=unused-import,import-error - -from keepkeylib.client import CallException -from keepkeylib.client import KeepKeyClient as Client -from keepkeylib.client import PinException -from keepkeylib.messages_pb2 import PassphraseAck, PinMatrixAck -from keepkeylib.transport_hid import HidTransport -from keepkeylib.transport_webusb import WebUsbTransport -from keepkeylib.types_pb2 import IdentityType - -get_public_node = Client.get_public_node -sign_identity = Client.sign_identity -Client.state = None - - -def find_device(): - """Returns first WebUSB or HID transport.""" - for d in WebUsbTransport.enumerate(): - return WebUsbTransport(d) - - for d in HidTransport.enumerate(): - return HidTransport(d) diff --git a/libagent/ssh/__init__.py b/libagent/ssh/__init__.py index 14f2656d..064bc083 100644 --- a/libagent/ssh/__init__.py +++ b/libagent/ssh/__init__.py @@ -320,7 +320,7 @@ def main(device_type): identity.identity_dict['proto'] = 'ssh' log.info('identity #%d: %s', index, identity.to_string()) - # override default PIN/passphrase entry tools (relevant for TREZOR/Keepkey): + # override default PIN/passphrase entry tools (relevant for TREZOR): device_type.ui = device.ui.UI(device_type=device_type, config=vars(args)) conn = JustInTimeConnection( From 34ec4eee4ee5ea836e375eed949799c58f64ddb8 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 7 Feb 2026 10:02:18 +0100 Subject: [PATCH 29/38] Drop ledger support https://github.com/LedgerHQ/app-ssh-agent has beed deprecated in https://github.com/LedgerHQ/app-ssh-agent/pull/48. --- README.md | 4 +- agents/ledger/ledger_agent.py | 7 -- agents/ledger/setup.py | 41 -------- doc/INSTALL.md | 27 +----- doc/README-GPG.md | 2 +- doc/README-Windows.md | 2 +- libagent/device/ledger.py | 178 ---------------------------------- 7 files changed, 6 insertions(+), 255 deletions(-) delete mode 100644 agents/ledger/ledger_agent.py delete mode 100644 agents/ledger/setup.py delete mode 100644 libagent/device/ledger.py diff --git a/README.md b/README.md index 3515c259..2b03626d 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,8 @@ See the following blog posts about this tool: - [TREZOR Firmware 1.3.4 enables SSH login](https://medium.com/@satoshilabs/trezor-firmware-1-3-4-enables-ssh-login-86a622d7e609) - [TREZOR Firmware 1.3.6 — GPG Signing, SSH Login Updates and Advanced Transaction Features for Segwit](https://medium.com/@satoshilabs/trezor-firmware-1-3-6-20a7df6e692) - [TREZOR Firmware 1.4.0 — GPG decryption support](https://www.reddit.com/r/TREZOR/comments/50h8r9/new_trezor_firmware_fidou2f_and_initial_ethereum/d7420q7/) -- [A Step by Step Guide to Securing your SSH Keys with the Ledger Nano S](https://thoughts.t37.net/a-step-by-step-guide-to-securing-your-ssh-keys-with-the-ledger-nano-s-92e58c64a005) -Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Blockstream Jade](https://blockstream.com/jade/), [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s), and [OnlyKey](https://onlykey.io) are supported. +Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Blockstream Jade](https://blockstream.com/jade/) and [OnlyKey](https://onlykey.io) are supported. ## Components @@ -23,7 +22,6 @@ agents to interact with several different hardware devices: * [`libagent`](https://pypi.org/project/libagent/): shared library * [`trezor-agent`](https://pypi.org/project/trezor-agent/): Using Trezor as hardware-based SSH/PGP/age agent -* [`ledger_agent`](https://pypi.org/project/ledger_agent/): Using Ledger as hardware-based SSH/PGP agent * [`jade_agent`](https://github.com/Blockstream/Jade/): Using Blockstream Jade as hardware-based SSH/PGP agent * [`onlykey-agent`](https://pypi.org/project/onlykey-agent/): Using OnlyKey as hardware-based SSH/PGP agent diff --git a/agents/ledger/ledger_agent.py b/agents/ledger/ledger_agent.py deleted file mode 100644 index 8ba42169..00000000 --- a/agents/ledger/ledger_agent.py +++ /dev/null @@ -1,7 +0,0 @@ -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) diff --git a/agents/ledger/setup.py b/agents/ledger/setup.py deleted file mode 100644 index b4bb08f6..00000000 --- a/agents/ledger/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/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='dev@romanzey.de', - url='http://github.com/romanz/trezor-agent', - python_requires='>=3.8', - 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', - ]}, -) diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 9696e6a6..b59a81b0 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -83,28 +83,7 @@ gpg (GnuPG) 2.1.15 $ brew install trezor-agent ``` -# 3. Install the Ledger Nano S agent - -1. Make sure you are running the latest firmware version on your Ledger Nano S: - - * [Ledger Nano S firmware releases](https://github.com/LedgerHQ/blue-app-ssh-agent): `0.0.3+` (install [SSH/PGP Agent](https://www.ledgerwallet.com/images/apps/chrome-mngr-apps.png) app) - -2. Make sure that your `udev` rules are configured [correctly](https://ledger.zendesk.com/hc/en-us/articles/115005165269-What-if-Ledger-Wallet-is-not-recognized-on-Linux-). -3. Then, install the latest [ledger-agent](https://pypi.python.org/pypi/ledger-agent) package: - - ``` - $ pip3 install ledger-agent - ``` - - Or, directly from the latest source code: - - ``` - $ git clone https://github.com/romanz/trezor-agent - $ pip3 install --user -e trezor-agent - $ pip3 install --user -e trezor-agent/agents/ledger - ``` - -# 4. Install the OnlyKey agent +# 3. Install the OnlyKey agent 1. Make sure you are running the latest firmware version on your OnlyKey: @@ -125,7 +104,7 @@ gpg (GnuPG) 2.1.15 $ pip3 install --user -e trezor-agent/agents/onlykey ``` -# 5. Install the Blockstream Jade agent +# 4. Install the Blockstream Jade agent 1. Make sure you are running the latest firmware version on your Blockstream Jade: @@ -149,7 +128,7 @@ gpg (GnuPG) 2.1.15 $ pip3 install --user -e trezor-agent/agents/jade ``` -# 6. Installation Troubleshooting +# 5. Installation Troubleshooting If there is an import problem with the installed `protobuf` package, see [this issue](https://github.com/romanz/trezor-agent/issues/28) for fixing it. diff --git a/doc/README-GPG.md b/doc/README-GPG.md index 89076caf..a8a518c5 100644 --- a/doc/README-GPG.md +++ b/doc/README-GPG.md @@ -5,7 +5,7 @@ and please let me [know](https://github.com/romanz/trezor-agent/issues/new) if s work well for you. If possible: * record the session (e.g. using [asciinema](https://asciinema.org)) - * attach the GPG agent log from `~/.gnupg/{trezor,ledger,jade}/gpg-agent.log` (can be [encrypted](https://keybase.io/romanz)) + * attach the GPG agent log from `~/.gnupg/trezor/gpg-agent.log` (can be [encrypted](https://keybase.io/romanz)) Thanks! diff --git a/doc/README-Windows.md b/doc/README-Windows.md index 28d08414..68b6e0c7 100644 --- a/doc/README-Windows.md +++ b/doc/README-Windows.md @@ -2,7 +2,7 @@ ## Preface -Since this library supports multiple hardware security devices, this document uses the term `` in commands to refer to the device of your choice. For example, if using Ledger Nano S, the command `-agent` becomes `ledger-agent`. +Since this library supports multiple hardware security devices, this document uses the term `` in commands to refer to the device of your choice. Installation and building has to be done with administrative privileges. Without these, the agent would only be installed for the current user, and could therefore not be used as a service. To run an administrative shell, hold the Windows key on the keyboard, and press R. In the input box that appears, type either "cmd" or "powershell" (Based on your preference. Both work), and then hold the Ctrl and Shift keys, and press Enter. A User Account Control dialog will pop up. Simply press "Yes". diff --git a/libagent/device/ledger.py b/libagent/device/ledger.py deleted file mode 100644 index df846a6f..00000000 --- a/libagent/device/ledger.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Ledger-related code (see https://www.ledgerwallet.com/).""" - -import binascii -import logging -import struct - -from ledgerblue import comm # pylint: disable=import-error - -from .. import formats -from . import interface - -log = logging.getLogger(__name__) - - -def _expand_path(path): - """Convert BIP32 path into bytes.""" - return b''.join((struct.pack('>I', e) for e in path)) - - -def _convert_public_key(ecdsa_curve_name, result): - """Convert Ledger reply into PublicKey object.""" - if ecdsa_curve_name == 'nist256p1': - if (result[64] & 1) != 0: - result = bytearray([0x03]) + result[1:33] - else: - result = bytearray([0x02]) + result[1:33] - else: - result = result[1:] - keyX = bytearray(result[0:32]) - keyY = bytearray(result[32:][::-1]) - if (keyX[31] & 1) != 0: - keyY[31] |= 0x80 - result = b'\x00' + bytes(keyY) - return bytes(result) - - -class LedgerNanoS(interface.Device): - """Connection to Ledger Nano S device.""" - - LEDGER_APP_NAME = "SSH/PGP Agent" - ledger_app_version = None - ledger_app_supports_end_of_frame_byte = True - - def get_app_name_and_version(self, dongle): - """Retrieve currently running Ledger application name and its version string.""" - device_version_answer = dongle.exchange(binascii.unhexlify('B001000000')) - offset = 1 - app_name_length = struct.unpack_from("B", device_version_answer, offset)[0] - offset += 1 - app_name = device_version_answer[offset: offset + app_name_length] - offset += app_name_length - app_version_length = struct.unpack_from("B", device_version_answer, offset)[0] - offset += 1 - app_version = device_version_answer[offset: offset + app_version_length] - log.debug("running app %s, version %s", app_name, app_version) - return (app_name.decode(), app_version.decode()) - - @classmethod - def package_name(cls): - """Python package name (at PyPI).""" - return 'ledger-agent' - - def connect(self): - """Enumerate and connect to the first USB HID interface.""" - try: - dongle = comm.getDongle(debug=True) - (app_name, self.ledger_app_version) = self.get_app_name_and_version(dongle) - - version_parts = self.ledger_app_version.split(".") - if (version_parts[0] == "0" and version_parts[1] == "0" and int(version_parts[2]) <= 7): - self.ledger_app_supports_end_of_frame_byte = False - - if app_name != LedgerNanoS.LEDGER_APP_NAME: - # we could launch the app here if we are in the dashboard - raise interface.DeviceError(f'{self} is not running {LedgerNanoS.LEDGER_APP_NAME}') - - return dongle - except comm.CommException as e: - raise interface.DeviceError( - 'Error ({}) communicating with {}'.format(e, self)) - - def pubkey(self, identity, ecdh=False): - """Get PublicKey object for specified BIP32 address and elliptic curve.""" - curve_name = identity.get_curve_name(ecdh) - path = _expand_path(identity.get_bip32_address(ecdh)) - if curve_name == 'nist256p1': - p2 = '01' - else: - p2 = '02' - apdu = '800200' + p2 - apdu = binascii.unhexlify(apdu) - apdu += bytearray([len(path) + 1, len(path) // 4]) - apdu += path - log.debug('apdu: %r', apdu) - result = bytearray(self.conn.exchange(bytes(apdu))) - log.debug('result: %r', result) - return formats.decompress_pubkey( - pubkey=_convert_public_key(curve_name, result[1:]), - curve_name=identity.curve_name) - - def sign(self, identity, blob): - """Sign given blob and return the signature (as bytes).""" - # pylint: disable=too-many-locals,too-many-branches - path = _expand_path(identity.get_bip32_address(ecdh=False)) - offset = 0 - result = None - while offset != len(blob): - data = bytes() - if offset == 0: - data += bytearray([len(path) // 4]) + path - chunk_size = min(len(blob) - offset, 255 - len(data)) - data += blob[offset:offset + chunk_size] - - if identity.identity_dict['proto'] == 'ssh': - ins = '04' - else: - ins = '08' - - if identity.curve_name == 'nist256p1': - p2 = '81' if identity.identity_dict['proto'] == 'ssh' else '01' - else: - p2 = '82' if identity.identity_dict['proto'] == 'ssh' else '02' - - if offset + chunk_size == len(blob) and self.ledger_app_supports_end_of_frame_byte: - # mark that we are at the end of the frame - p1 = "80" if offset == 0 else "81" - else: - p1 = "00" if offset == 0 else "01" - - apdu = binascii.unhexlify('80' + ins + p1 + p2) + len(data).to_bytes(1, 'little') + data - - log.debug('apdu: %r', apdu) - try: - result = bytearray(self.conn.exchange(bytes(apdu))) - except comm.CommException as e: - raise interface.DeviceError( - 'Error ({}) communicating with {}'.format(e, self)) - - offset += chunk_size - - log.debug('result: %r', result) - if identity.curve_name == 'nist256p1': - offset = 3 - length = result[offset] - r = result[offset+1:offset+1+length] - if r[0] == 0: - r = r[1:] - offset = offset + 1 + length + 1 - length = result[offset] - s = result[offset+1:offset+1+length] - if s[0] == 0: - s = s[1:] - offset = offset + 1 + length - return bytes(r) + bytes(s) - else: - return bytes(result[:64]) - - def ecdh(self, identity, pubkey): - """Get shared session key using Elliptic Curve Diffie-Hellman.""" - path = _expand_path(identity.get_bip32_address(ecdh=True)) - if identity.curve_name == 'nist256p1': - p2 = '01' - else: - p2 = '02' - apdu = '800a00' + p2 - apdu = binascii.unhexlify(apdu) - apdu += bytearray([len(pubkey) + len(path) + 1]) - apdu += bytearray([len(path) // 4]) + path - apdu += pubkey - log.debug('apdu: %r', apdu) - try: - result = bytearray(self.conn.exchange(bytes(apdu))) - except comm.CommException as e: - raise interface.DeviceError( - 'Error ({}) communicating with {}'.format(e, self)) - log.debug('result: %r', result) - assert result[0] == 0x04 - return bytes(result) From 60e4a387517b583d0b70f0ada1b04f2b9706d193 Mon Sep 17 00:00:00 2001 From: nitramiz Date: Wed, 14 Jan 2026 16:50:24 +0000 Subject: [PATCH 30/38] libagent: Add USB IDs for Jade Plus --- libagent/device/jade.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libagent/device/jade.py b/libagent/device/jade.py index 4660aacf..5ff3f1cf 100644 --- a/libagent/device/jade.py +++ b/libagent/device/jade.py @@ -22,7 +22,11 @@ class BlockstreamJade(interface.Device): """Connection to Blockstream Jade device.""" MIN_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 33) - DEVICE_IDS = [(0x10c4, 0xea60), (0x1a86, 0x55d4)] + DEVICE_IDS = [ + (0x10c4, 0xea60), # Jade 1.0 + (0x1a86, 0x55d4), # Jade 1.1 + (0x303a, 0x4001), # Jade 2 (Plus) + ] connection = None @classmethod From 05298ad5a377cf0a72f20bc3c4693cf4131ecf34 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 31 Jan 2026 11:53:52 +0100 Subject: [PATCH 31/38] Switch to trezorlib 0.20 Also support TS7. --- agents/trezor/setup.py | 6 +-- libagent/device/trezor.py | 97 ++++++++++++---------------------- libagent/device/trezor_defs.py | 30 ----------- libagent/device/ui.py | 9 ++++ setup.py | 2 +- 5 files changed, 48 insertions(+), 96 deletions(-) delete mode 100644 libagent/device/trezor_defs.py diff --git a/agents/trezor/setup.py b/agents/trezor/setup.py index 28d21736..02920da7 100644 --- a/agents/trezor/setup.py +++ b/agents/trezor/setup.py @@ -3,7 +3,7 @@ setup( name='trezor_agent', - version='0.12.0', + version='0.13.0', description='Using Trezor as hardware SSH/GPG agent', author='Roman Zeyde', author_email='dev@romanzey.de', @@ -11,8 +11,8 @@ python_requires='>=3.8', scripts=['trezor_agent.py'], install_requires=[ - 'libagent>=0.14.0', - 'trezor[hidapi]>=0.13' + 'libagent>=0.16.0', + 'trezor[hidapi]>=0.20' ], platforms=['POSIX'], classifiers=[ diff --git a/libagent/device/trezor.py b/libagent/device/trezor.py index 65978b39..e1afa236 100644 --- a/libagent/device/trezor.py +++ b/libagent/device/trezor.py @@ -1,9 +1,12 @@ """TREZOR-related code (see http://bitcointrezor.com/).""" -import binascii import logging -import semver +from trezorlib.btc import get_public_node +from trezorlib.client import get_default_client, get_default_session +from trezorlib.exceptions import TrezorFailure +from trezorlib.messages import IdentityType +from trezorlib.misc import get_ecdh_session_key, sign_identity from .. import formats from . import interface @@ -19,64 +22,34 @@ def package_name(cls): """Python package name (at PyPI).""" return 'trezor-agent' - @property - def _defs(self): - from . import trezor_defs - return trezor_defs - required_version = '>=1.4.0' ui = None # can be overridden by device's users - cached_session_id = None - - def _verify_version(self, connection): - f = connection.features - log.debug('connected to %s %s', self, f.device_id) - log.debug('label : %s', f.label) - log.debug('vendor : %s', f.vendor) - current_version = '{}.{}.{}'.format(f.major_version, - f.minor_version, - f.patch_version) - log.debug('version : %s', current_version) - log.debug('revision : %s', binascii.hexlify(f.revision)) - if not semver.match(current_version, self.required_version): - fmt = ('Please upgrade your {} firmware to {} version' - ' (current: {})') - raise ValueError(fmt.format(self, self.required_version, - current_version)) + _session = None # cache one session per agent process + + @property + def session(self): + """Return cached session, or connect and pair if needed.""" + if self.__class__._session is None: + assert self.ui is not None + client = get_default_client( + app_name="trezor-agent", + pin_callback=self.ui.get_pin, + code_entry_callback=self.ui.get_pairing_code, + ) + session = client.get_session(passphrase="") # TODO: support passphrase + log.info("%s @ fpr=%s", session, session.get_root_fingerprint().hex()) + self.__class__._session = session + + return self.__class__._session def connect(self): - """Enumerate and connect to the first available interface.""" - transport = self._defs.find_device() - if not transport: - raise interface.NotFoundError('{} not connected'.format(self)) - - log.debug('using transport: %s', transport) - for _ in range(5): # Retry a few times in case of PIN failures - connection = self._defs.Client(transport=transport, - ui=self.ui, - session_id=self.__class__.cached_session_id) - self._verify_version(connection) - - try: - # unlock PIN and passphrase - self._defs.get_address(connection, - "Testnet", - self._defs.PASSPHRASE_TEST_PATH) - return connection - except (self._defs.PinException, ValueError) as e: - log.error('Invalid PIN: %s, retrying...', e) - continue - except Exception as e: - log.exception('ping failed: %s', e) - connection.close() # so the next HID open() will succeed - raise - return None + """One session is cached.""" + return self def close(self): - """Close connection.""" - self.__class__.cached_session_id = self.conn.session_id - super().close() + """One session is cached.""" + pass def pubkey(self, identity, ecdh=False): """Return public key.""" @@ -84,8 +57,8 @@ def pubkey(self, identity, ecdh=False): log.debug('"%s" getting public key (%s) from %s', identity.to_string(), curve_name, self) addr = identity.get_bip32_address(ecdh=ecdh) - result = self._defs.get_public_node( - self.conn, + result = get_public_node( + self.session, n=addr, ecdsa_curve_name=curve_name) log.debug('result: %s', result) @@ -93,7 +66,7 @@ def pubkey(self, identity, ecdh=False): return formats.decompress_pubkey(pubkey=pubkey, curve_name=identity.curve_name) def _identity_proto(self, identity): - result = self._defs.IdentityType() + result = IdentityType() for name, value in identity.items(): setattr(result, name, value) return result @@ -109,8 +82,8 @@ def sign_with_pubkey(self, identity, blob): log.debug('"%s" signing %r (%s) on %s', identity.to_string(), blob, curve_name, self) try: - result = self._defs.sign_identity( - self.conn, + result = sign_identity( + self.session, identity=self._identity_proto(identity), challenge_hidden=blob, challenge_visual='', @@ -119,7 +92,7 @@ def sign_with_pubkey(self, identity, blob): assert len(result.signature) == 65 assert result.signature[:1] == b'\x00' return bytes(result.signature[1:]), bytes(result.public_key) - except self._defs.TrezorFailure as e: + except TrezorFailure as e: msg = '{} error: {}'.format(self, e) log.debug(msg, exc_info=True) raise interface.DeviceError(msg) @@ -135,8 +108,8 @@ def ecdh_with_pubkey(self, identity, pubkey): log.debug('"%s" shared session key (%s) for %r from %s', identity.to_string(), curve_name, pubkey, self) try: - result = self._defs.get_ecdh_session_key( - self.conn, + result = get_ecdh_session_key( + self.session, identity=self._identity_proto(identity), peer_public_key=pubkey, ecdsa_curve_name=curve_name) @@ -148,7 +121,7 @@ def ecdh_with_pubkey(self, identity, pubkey): self_pubkey = bytes(self_pubkey[1:]) return bytes(result.session_key), self_pubkey - except self._defs.TrezorFailure as e: + except TrezorFailure as e: msg = '{} error: {}'.format(self, e) log.debug(msg, exc_info=True) raise interface.DeviceError(msg) diff --git a/libagent/device/trezor_defs.py b/libagent/device/trezor_defs.py deleted file mode 100644 index b6a36da6..00000000 --- a/libagent/device/trezor_defs.py +++ /dev/null @@ -1,30 +0,0 @@ -"""TREZOR-related definitions.""" - -# pylint: disable=unused-import,import-error,no-name-in-module,no-member -import logging -import os - -import mnemonic -import semver -import trezorlib -from trezorlib.btc import get_address, get_public_node -from trezorlib.client import PASSPHRASE_TEST_PATH -from trezorlib.client import TrezorClient as Client -from trezorlib.exceptions import PinException, TrezorFailure -from trezorlib.messages import IdentityType -from trezorlib.misc import get_ecdh_session_key, sign_identity -from trezorlib.transport import get_transport - -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"), prefix_search=True) - except Exception as e: # pylint: disable=broad-except - log.debug("Failed to find a Trezor device: %s", e) - return None diff --git a/libagent/device/ui.py b/libagent/device/ui.py index 2cf0f130..9d5eb747 100644 --- a/libagent/device/ui.py +++ b/libagent/device/ui.py @@ -49,6 +49,15 @@ def get_pin(self, _code=None): binary=self.pin_entry_binary, options=self.options_getter()) + def get_pairing_code(self): + """Ask the user for pairing code.""" + return interact( + title='{} pairing'.format(self.device_name), + prompt='Pairing code:', + description='Enter 6-digit code show on {} screen'.format(self.device_name), + binary=self.pin_entry_binary, + options=self.options_getter()) + def get_passphrase(self, prompt='Passphrase:', available_on_device=False): """Ask the user for passphrase.""" passphrase = None diff --git a/setup.py b/setup.py index 4c21923a..40efa88d 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='libagent', - version='0.15.0', + version='0.16.0', description='Using hardware wallets as SSH/GPG/age agent', author='Roman Zeyde', author_email='dev@romanzey.de', From de6301e9c8d5459be070a472abf85c59998f8c32 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 1 Mar 2026 12:02:24 +0100 Subject: [PATCH 32/38] Lookup GnuPG user ID (instead of assuming it's the first one) --- libagent/gpg/agent.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/libagent/gpg/agent.py b/libagent/gpg/agent.py index 15c93643..2e04385c 100644 --- a/libagent/gpg/agent.py +++ b/libagent/gpg/agent.py @@ -161,19 +161,26 @@ def get_identity(self, keygrip): keygrip_bytes = binascii.unhexlify(keygrip) pubkey_dict, user_ids = decode.load_by_keygrip( pubkey_bytes=self.pubkey_bytes, keygrip=keygrip_bytes) - # We assume the first user ID is used to generate TREZOR-based GPG keys. - user_id = user_ids[0]['value'].decode('utf-8') + log.debug("pubkey_dict %s", pubkey_dict) + curve_name = protocol.get_curve_name_by_oid(pubkey_dict['curve_oid']) ecdh = pubkey_dict['algo'] == protocol.ECDH_ALGO_ID - identity = client.create_identity(user_id=user_id, curve_name=curve_name) - verifying_key = self.client.pubkey(identity=identity, ecdh=ecdh) - pubkey = protocol.PublicKey( - curve_name=curve_name, created=pubkey_dict['created'], - verifying_key=verifying_key, ecdh=ecdh) - assert pubkey.key_id() == pubkey_dict['key_id'] - assert pubkey.keygrip() == keygrip_bytes - return identity + # Lookup the first user ID that matches the provided keygrip + for user_id_dict in user_ids: + log.debug("user_id: %s", user_id_dict) + user_id = user_id_dict['value'].decode('utf-8') + + identity = client.create_identity(user_id=user_id, curve_name=curve_name) + verifying_key = self.client.pubkey(identity=identity, ecdh=ecdh) + pubkey = protocol.PublicKey( + curve_name=curve_name, created=pubkey_dict['created'], + verifying_key=verifying_key, ecdh=ecdh) + + if pubkey.keygrip() == keygrip_bytes and pubkey.key_id() == pubkey_dict['key_id']: + return identity + + raise KeyError(keygrip) def pksign(self, conn): """Sign a message digest using a private EC key.""" From ccfccbf9e58a581d8df785cb0f780487a6ca4aca Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 1 Mar 2026 12:02:24 +0100 Subject: [PATCH 33/38] Fix `load_by_keygrip()` docstring --- libagent/gpg/decode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libagent/gpg/decode.py b/libagent/gpg/decode.py index 1d03b4b1..3dfbaa50 100644 --- a/libagent/gpg/decode.py +++ b/libagent/gpg/decode.py @@ -294,7 +294,7 @@ def _parse_pubkey_packets(pubkey_bytes): def load_by_keygrip(pubkey_bytes, keygrip): - """Return public key and first user ID for specified keygrip.""" + """Return public key and user IDs for specified keygrip.""" for packets in _parse_pubkey_packets(pubkey_bytes): user_ids = [p for p in packets if p['type'] == 'user_id'] for p in packets: From 29fc6e43abeb8e6da587286c43ec1f8b24d25ec2 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 1 Mar 2026 15:36:50 +0100 Subject: [PATCH 34/38] Fix passphrase support on Trezor --- libagent/device/trezor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libagent/device/trezor.py b/libagent/device/trezor.py index e1afa236..a067c84c 100644 --- a/libagent/device/trezor.py +++ b/libagent/device/trezor.py @@ -3,7 +3,7 @@ import logging from trezorlib.btc import get_public_node -from trezorlib.client import get_default_client, get_default_session +from trezorlib.client import PassphraseSetting, get_default_client from trezorlib.exceptions import TrezorFailure from trezorlib.messages import IdentityType from trezorlib.misc import get_ecdh_session_key, sign_identity @@ -37,7 +37,7 @@ def session(self): pin_callback=self.ui.get_pin, code_entry_callback=self.ui.get_pairing_code, ) - session = client.get_session(passphrase="") # TODO: support passphrase + session = client.get_session(passphrase=PassphraseSetting.AUTO) log.info("%s @ fpr=%s", session, session.get_root_fingerprint().hex()) self.__class__._session = session From 22545a4134bac6a16f4b325adc21e2e3757d9a7c Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 1 Mar 2026 16:30:38 +0100 Subject: [PATCH 35/38] Release libagent 0.16.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 40efa88d..a8add9f5 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='libagent', - version='0.16.0', + version='0.16.1', description='Using hardware wallets as SSH/GPG/age agent', author='Roman Zeyde', author_email='dev@romanzey.de', From d3c37f014b09da603a8d94e45239ab9066683d5e Mon Sep 17 00:00:00 2001 From: onlykey Date: Mon, 25 May 2026 21:48:03 -0400 Subject: [PATCH 36/38] Revert "Complete pkg_resources removal for Python 3.14 + dependency/CI cleanup (#15)" This reverts commit e9d27a0057bc6530cc17e51abfa8844bc51cff8c. --- .github/workflows/ci.yml | 6 ++-- .pylintrc | 2 +- libagent/age/__init__.py | 3 +- libagent/age/client.py | 8 +++--- libagent/device/ledger.py | 1 + libagent/device/onlykey.py | 47 +++++++++++++++---------------- libagent/device/trezor.py | 1 + libagent/device/trezor_defs.py | 1 + libagent/formats.py | 16 +++++++---- libagent/gpg/__init__.py | 30 +++++++++----------- libagent/gpg/agent.py | 37 ++++++++++-------------- libagent/gpg/client.py | 8 +++--- libagent/gpg/decode.py | 5 ++-- libagent/gpg/protocol.py | 11 +++----- libagent/signify/__init__.py | 1 + libagent/ssh/__init__.py | 2 +- libagent/ssh/client.py | 6 ++-- libagent/ssh/protocol.py | 6 ++-- libagent/ssh/tests/test_client.py | 2 -- libagent/util.py | 7 ++++- setup.py | 4 ++- tox.ini | 2 +- 22 files changed, 102 insertions(+), 104 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e7cfd37..cd699126 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pylintrc b/.pylintrc index b932d4ea..36122748 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,4 +1,4 @@ [MESSAGES CONTROL] -disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme,duplicate-code,cyclic-import,import-outside-toplevel,consider-using-with,consider-using-f-string,unspecified-encoding,too-many-statements,too-many-branches,too-many-positional-arguments,possibly-used-before-assignment,inconsistent-return-statements,deprecated-class +disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme,duplicate-code,cyclic-import,import-outside-toplevel,consider-using-with,consider-using-f-string,unspecified-encoding [SIMILARITIES] min-similarity-lines=5 diff --git a/libagent/age/__init__.py b/libagent/age/__init__.py index 7d32e6b2..954f3bf0 100644 --- a/libagent/age/__init__.py +++ b/libagent/age/__init__.py @@ -16,9 +16,9 @@ import os import sys import traceback -from importlib import metadata import bech32 +from importlib import metadata import semver from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 @@ -154,6 +154,7 @@ def main(device_type): p = argparse.ArgumentParser() agent_package = device_type.package_name() + resources_map = {r.key: r for r in pkg_resources.require(agent_package)} resources = [metadata.distribution(agent_package), metadata.distribution('lib-agent')] versions = '\n'.join('{}={}'.format(r.metadata['Name'], r.version) for r in resources) p.add_argument('--version', help='print the version info', diff --git a/libagent/age/client.py b/libagent/age/client.py index 722d3b35..bdac0648 100644 --- a/libagent/age/client.py +++ b/libagent/age/client.py @@ -41,8 +41,8 @@ def ecdh(self, identity, peer_pubkey): pubkey=peer_pubkey, identity=identity) assert result[:1] == b"\x04" hkdf = HKDF( - algorithm=hashes.SHA256(), - length=32, - salt=((peer_pubkey + self_pubkey)), - info=b"age-encryption.org/v1/X25519") + algorithm=hashes.SHA256(), + length=32, + salt=((peer_pubkey + self_pubkey)), + info=b"age-encryption.org/v1/X25519") return hkdf.derive(result[1:]) diff --git a/libagent/device/ledger.py b/libagent/device/ledger.py index df846a6f..7749c08d 100644 --- a/libagent/device/ledger.py +++ b/libagent/device/ledger.py @@ -8,6 +8,7 @@ from .. import formats from . import interface +from .. import formats log = logging.getLogger(__name__) diff --git a/libagent/device/onlykey.py b/libagent/device/onlykey.py index 8adf70ae..9352c8f4 100644 --- a/libagent/device/onlykey.py +++ b/libagent/device/onlykey.py @@ -2,17 +2,16 @@ # pylint: disable=attribute-defined-outside-init """OnlyKey-related code (see https://www.onlykey.io/).""" -import codecs -import hashlib import logging +import hashlib +import codecs import os -import re -import time from os import path - +import time import ecdsa import nacl.signing import unidecode +import re from . import interface @@ -49,8 +48,8 @@ def connect(self): self.okversion = self.okversion[8:] if self.okversion[0] == 'v': break - except Exception as exc: - raise interface.NotFoundError('{} not connected: "{}"') from exc + except Exception: + raise interface.NotFoundError('{} not connected: "{}"') def set_skey(self, skey): """Set signing key to use.""" @@ -74,7 +73,6 @@ def import_pub(self, pubkey): # self.import_pubkey_bytes = bytes(self.import_pubkey_obj) def get_key_by_keygrip(self, keygrip): - """Return key-slot info for the given keygrip.""" if keygrip is None: return None keygriplong = keygrip @@ -94,21 +92,19 @@ def get_key_by_keygrip(self, keygrip): raise KeyError('keygrip %s not found' % keygriplong) return None + def get_sk_dk(self): """Get signing key and decryption key slots from config.""" - fpath = os.path.join(os.environ.get( - 'AGENTHOMEDIR', os.environ.get('GNUPGHOME')), 'run-agent.sh') + fpath = os.path.join(os.environ.get('AGENTHOMEDIR', os.environ.get('GNUPGHOME')), 'run-agent.sh') log.debug('Path to run-agent.sh = %s', fpath) if path.exists(fpath): with open(fpath) as f: s = f.read() if '--skey-slot=ECC' in s: if s[s.find('--skey-slot=')+16:s.find('--skey-slot=')+17] == ' ': - self.set_skey( - int(s[s.find('--skey-slot=')+15:s.find('--skey-slot=')+16])+100) + self.set_skey(int(s[s.find('--skey-slot=')+15:s.find('--skey-slot=')+16])+100) else: - self.set_skey( - int(s[s.find('--skey-slot=')+15:s.find('--skey-slot=')+17])+100) + self.set_skey(int(s[s.find('--skey-slot=')+15:s.find('--skey-slot=')+17])+100) elif '--skey-slot=RSA' in s: self.set_skey(int(s[s.find('--skey-slot=')+15:s.find('--skey-slot=')+16])) elif '--skey-slot=' in s: @@ -118,11 +114,9 @@ def get_sk_dk(self): self.set_skey(int(s[s.find('--skey-slot=')+12:s.find('--skey-slot=')+15])) if '--dkey-slot=ECC' in s: if s[s.find('--dkey-slot=')+16:s.find('--dkey-slot=')+17] == ' ': - self.set_dkey( - int(s[s.find('--dkey-slot=')+15:s.find('--dkey-slot=')+16])+100) + self.set_dkey(int(s[s.find('--dkey-slot=')+15:s.find('--dkey-slot=')+16])+100) else: - self.set_dkey( - int(s[s.find('--dkey-slot=')+15:s.find('--dkey-slot=')+17])+100) + self.set_dkey(int(s[s.find('--dkey-slot=')+15:s.find('--dkey-slot=')+17])+100) elif '--dkey-slot=RSA' in s: self.set_dkey(int(s[s.find('--dkey-slot=')+15:s.find('--dkey-slot=')+16])) elif '--dkey-slot=' in s: @@ -259,16 +253,18 @@ def pubkey(self, identity, ecdh=False): if identity.identity_dict['proto'] == 'ssh': # https://security.stackexchange.com/questions/42268/how-do-i-get-the-rsa-bit-length-with-the-pubkey-and-openssl ok_pubkey = b'\x00\x00\x00\x07' + b'\x73\x73\x68\x2d\x72\x73\x61' + \ - b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ - b'\x00\x00\x01\x01' + b'\x00' + bytes(ok_pubkey) - # (legacy commented-out raw SSH RSA pubkey assembly omitted) + b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ + b'\x00\x00\x01\x01' + b'\x00' + bytes(ok_pubkey) + # ok_pubkey = b'\x00\x00\x00\x07' + b'\x72\x73\x61\x2d\x73\x68\x61\x32\x2d\x32\x35\x + # 36' + b'\x00\x00\x00\x03' + b'\x01\x00\x01' + b'\x00\x00\x01\x01' + b'\x00' + byte + # s(ok_pubkey) else: ok_pubkey = bytes(ok_pubkey) elif len(ok_pubkey) == 512: if identity.identity_dict['proto'] == 'ssh': ok_pubkey = b'\x00\x00\x00\x07' + b'\x73\x73\x68\x2d\x72\x73\x61' + \ - b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ - b'\x00\x00\x02\x01' + b'\x00' + bytes(ok_pubkey) + b'\x00\x00\x00\x03' + b'\x01\x00\x01' + \ + b'\x00\x00\x02\x01' + b'\x00' + bytes(ok_pubkey) else: ok_pubkey = bytes(ok_pubkey) else: @@ -408,6 +404,8 @@ def ecdh_with_pubkey(self, identity, pubkey): self_pubkey = self.pubkey(ecdh=False, identity=identity) log.info('Using self_pubkey= %s', self_pubkey) session_key = self.ecdh(identity, pubkey) + if self_pubkey: + self_pubkey = self_pubkey return session_key, self_pubkey def ecdh(self, identity, pubkey): @@ -504,8 +502,7 @@ def get_button(self, byte): else: return byte % 6 + 1 - -def convert_keyslot(self, s): # pylint: disable=unused-argument +def convert_keyslot (self, s): """Return key slot number.""" if 'ECC' in s: if len(s) == 5: diff --git a/libagent/device/trezor.py b/libagent/device/trezor.py index 65978b39..417ea4d1 100644 --- a/libagent/device/trezor.py +++ b/libagent/device/trezor.py @@ -7,6 +7,7 @@ from .. import formats from . import interface +from .. import formats log = logging.getLogger(__name__) diff --git a/libagent/device/trezor_defs.py b/libagent/device/trezor_defs.py index b6a36da6..84e881c7 100644 --- a/libagent/device/trezor_defs.py +++ b/libagent/device/trezor_defs.py @@ -1,5 +1,6 @@ """TREZOR-related definitions.""" +import logging # pylint: disable=unused-import,import-error,no-name-in-module,no-member import logging import os diff --git a/libagent/formats.py b/libagent/formats.py index ed4d4733..d5e9ba3d 100644 --- a/libagent/formats.py +++ b/libagent/formats.py @@ -6,9 +6,13 @@ import ecdsa import nacl.signing +import Crypto.Hash +import Crypto.PublicKey +import Crypto.Signature +from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256, SHA512 from Crypto.PublicKey import RSA -from Crypto.Signature import pkcs1_15 +import nacl.signing from . import util @@ -34,8 +38,7 @@ SSH_NIST256_CERT_TYPE = SSH_NIST256_KEY_TYPE + SSH_NIST256_CERT_POSTFIX SSH_ED25519_KEY_TYPE = b'ssh-ed25519' SSH_RSA_KEY_TYPE = b'ssh-rsa' -SUPPORTED_KEY_TYPES = {SSH_NIST256_KEY_TYPE, - SSH_NIST256_CERT_TYPE, SSH_ED25519_KEY_TYPE, SSH_RSA_KEY_TYPE} +SUPPORTED_KEY_TYPES = {SSH_NIST256_KEY_TYPE, SSH_NIST256_CERT_TYPE, SSH_ED25519_KEY_TYPE, SSH_RSA_KEY_TYPE} hashfunc = hashlib.sha256 @@ -130,7 +133,7 @@ def ed25519_verify(sig, msg): def rsa_verify(sig, msg): pub = bytes(b'ssh-rsa ' + base64.b64encode(blob)) log.debug('RSA pubkey: %s', pub) - vk = RSA.importKey(pub) + vk = Crypto.PublicKey.RSA.importKey(pub) log.debug('message: %s', msg) if b'rsa-sha2-512' in msg: h = SHA512.new(msg) @@ -140,7 +143,7 @@ def rsa_verify(sig, msg): log.debug('rsa-sha2-256') log.debug('hash: %s', h.hexdigest()) try: - pkcs1_15.new(vk).verify(h, sig) + Crypto.Signature.pkcs1_15.new(vk).verify(h, sig) log.debug('The RSA signature is valid.') except (ValueError, TypeError): log.debug('The RSA signature is not valid.') @@ -230,11 +233,12 @@ def serialize_verifying_key(vk): return key_type, blob if (len(vk) == 279 or len(vk) == 535): - # RSA 2048 or RSA 4096 + #RSA 2048 or RSA 4096 pubkey = vk key_type = SSH_RSA_KEY_TYPE return key_type, pubkey + raise TypeError('unsupported {!r}'.format(vk)) diff --git a/libagent/gpg/__init__.py b/libagent/gpg/__init__.py index a174e0aa..6ac15e7c 100644 --- a/libagent/gpg/__init__.py +++ b/libagent/gpg/__init__.py @@ -17,16 +17,16 @@ import subprocess import sys import time -from importlib import metadata +import daemon +import semver import Crypto.Hash import Crypto.PublicKey import Crypto.Signature -import daemon -import semver +from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256, SHA512 from Crypto.PublicKey import RSA -from Crypto.Signature import pkcs1_15 +from importlib import metadata from .. import device, formats, server, util from . import agent, client, encode, keyring, protocol @@ -36,7 +36,7 @@ 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, ' + #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) c = client.Client(device=device_type()) @@ -45,7 +45,7 @@ def export_public_key(device_type, args): if device_type.package_name() == 'onlykey-agent': if hasattr(device_type, 'import_pubkey'): return device_type.import_pubkey - + verifying_key = c.pubkey(identity=identity, ecdh=False) decryption_key = c.pubkey(identity=identity, ecdh=True) signer_func = functools.partial(c.sign, identity=identity) @@ -125,7 +125,7 @@ def write_file(path, data): def run_init(device_type, args): """Initialize hardware-based GnuPG identity.""" util.setup_logging(verbosity=args.verbose) - # log.warning('This GPG tool is still in EXPERIMENTAL mode, ' + #log.warning('This GPG tool is still in EXPERIMENTAL mode, ' # 'so please note that the API and features may ' # 'change without backwards compatibility!') @@ -137,9 +137,9 @@ def run_init(device_type, args): homedir = args.homedir if not homedir: homedir = os.path.expanduser('~/.gnupg/{}'.format(device_name)) - + # Save homedir as environment variable - os.environ['AGENTHOMEDIR'] = homedir + os.environ['AGENTHOMEDIR']=homedir log.info('GPG home directory: %s', homedir) @@ -204,7 +204,7 @@ def run_init(device_type, args): fi """.format(homedir)) check_call(['chmod', '700', f.name]) - + # Generate new GPG identity and import into GPG keyring pubkey = write_file(os.path.join(homedir, 'pubkey.asc'), export_public_key(device_type, args)) @@ -281,9 +281,9 @@ def run_agent(device_type): def run_agent_internal(args, device_type): """Actually run the server.""" assert args.homedir - + # Save homedir as environment variable - os.environ['AGENTHOMEDIR'] = args.homedir + os.environ['AGENTHOMEDIR']=args.homedir log_file = os.path.join(args.homedir, 'gpg-agent.log') util.setup_logging(verbosity=args.verbose, filename=log_file) @@ -350,11 +350,9 @@ def main(device_type): p.add_argument('-dk', '--dkey', type=str, metavar='DECRYPT_KEY', default='ECC32', help='specify key to use for decryption') - p.add_argument('-i', '--import-pub', type=argparse.FileType('r'), - metavar='IMPORT_PUBLIC_KEY', + p.add_argument('-i', '--import-pub', type=argparse.FileType('r'), metavar='IMPORT_PUBLIC_KEY', default=None, - help=('import existing OpenPGP public key to use ' - '(Load private using OnlyKey App)')) + help='import existing OpenPGP public key to use (Load private using OnlyKey App)') p.add_argument('-t', '--time', type=int, default=0) p.add_argument('--homedir', type=str, default=os.environ.get('GNUPGHOME'), diff --git a/libagent/gpg/agent.py b/libagent/gpg/agent.py index 17c9aae8..8e942ead 100644 --- a/libagent/gpg/agent.py +++ b/libagent/gpg/agent.py @@ -27,7 +27,6 @@ def sig_encode(r, s): s = util.assuan_serialize(util.num2bytes(s, 32)) return b'(7:sig-val(5:ecdsa(1:r32:' + r + b')(1:s32:' + s + b')))' - def sig_encode_rsa(s, length): """Encode RSA signature data into GPG S-expression.""" s = util.assuan_serialize(util.num2bytes(s, length)) @@ -36,27 +35,24 @@ def sig_encode_rsa(s, length): elif length == 512: return b'(7:sig-val(3:rsa(1:s512:' + s + b')))' - def _serialize_point(data): prefix = '{}:'.format(len(data)).encode('ascii') # https://www.gnupg.org/documentation/manuals/assuan/Server-responses.html return b'(5:value' + util.assuan_serialize(prefix + data) + b')' - def _serialize_rsa(data): # https://www.gnupg.org/documentation/manuals/assuan/Server-responses.html - if data[0] == 9: + if (data[0]==9): # AES with 256-bit key # https://datatracker.ietf.org/doc/html/rfc4880#section-9.2 data = data[0:35] - elif data[0] == 7: + elif (data[0]==7): # AES with 128-bit key - data = data[0:19] - + data = data[0:19] + prefix = '{}:'.format(len(data)).encode('ascii') return b'(5:value' + util.assuan_serialize(prefix + data) + b')' - def parse_decrypt(line): """Parse ECDH request and return remote public key.""" prefix, line = line.split(b' ', 1) @@ -145,7 +141,7 @@ def handle_option(self, opt): log.debug('options: %s', self.options) def handle_get_confirmation(self, conn, _): - """Prompt user for OnlyKey Challenge Code.""" + """Prompt user for OnlyKey Challenge Code""" def handle_get_passphrase(self, conn, _): """Allow simple GPG symmetric encryption (using a passphrase).""" @@ -196,9 +192,8 @@ def get_identity(self, keygrip): user_id = user_ids[0]['value'].decode('utf-8') if pubkey_dict['algo'] not in {1, 2, 3}: curve_name = protocol.get_curve_name_by_oid(pubkey_dict['curve_oid']) - ecdh = pubkey_dict['algo'] == protocol.ECDH_ALGO_ID - identity = client.create_identity( - user_id=user_id, curve_name=curve_name, keygrip=keygrip) + ecdh = (pubkey_dict['algo'] == protocol.ECDH_ALGO_ID) + identity = client.create_identity(user_id=user_id, curve_name=curve_name, keygrip=keygrip) verifying_key = self.client.pubkey(identity=identity, ecdh=ecdh) pubkey = protocol.PublicKey( curve_name=curve_name, created=pubkey_dict['created'], @@ -206,15 +201,13 @@ def get_identity(self, keygrip): assert pubkey.key_id() == pubkey_dict['key_id'] assert pubkey.keygrip() == keygrip_bytes elif len(pubkey_dict['_to_hash']) < 350: - identity = client.create_identity( - user_id=user_id, curve_name='rsa2048', keygrip=keygrip) + identity = client.create_identity(user_id=user_id, curve_name='rsa2048', keygrip=keygrip) verifying_key = self.client.pubkey(identity=identity, ecdh=False) pubkey = protocol.PublicKey( curve_name='rsa2048', created=pubkey_dict['created'], verifying_key=verifying_key, ecdh=False) elif len(pubkey_dict['_to_hash']) < 700: - identity = client.create_identity( - user_id=user_id, curve_name='rsa4096', keygrip=keygrip) + identity = client.create_identity(user_id=user_id, curve_name='rsa4096', keygrip=keygrip) verifying_key = self.client.pubkey(identity=identity, ecdh=False) pubkey = protocol.PublicKey( curve_name='rsa4096', created=pubkey_dict['created'], @@ -230,17 +223,17 @@ def pksign(self, conn): """Sign a message digest using a private EC key.""" log.debug('signing %r digest (algo #%s)', self.digest, self.algo) identity = self.get_identity(keygrip=self.keygrip) - if identity.curve_name == 'rsa2048': + if identity.curve_name == 'rsa2048' : s = self.client.sign(identity=identity, - digest=binascii.unhexlify(self.digest)) + digest=binascii.unhexlify(self.digest)) result = sig_encode_rsa(s, 256) elif identity.curve_name == 'rsa4096': s = self.client.sign(identity=identity, - digest=binascii.unhexlify(self.digest)) + digest=binascii.unhexlify(self.digest)) result = sig_encode_rsa(s, 512) else: r, s = self.client.sign(identity=identity, - digest=binascii.unhexlify(self.digest)) + digest=binascii.unhexlify(self.digest)) result = sig_encode(r, s) log.debug('result: %r', result) keyring.sendline(conn, b'D ' + result) @@ -255,11 +248,11 @@ def pkdecrypt(self, conn): remote_pubkey = parse_decrypt(line) identity = self.get_identity(keygrip=self.keygrip) - if identity.curve_name in ('rsa2048', 'rsa4096'): + if identity.curve_name == 'rsa2048' or identity.curve_name == 'rsa4096': dvalue = _serialize_rsa(self.client.ecdh(identity=identity, pubkey=remote_pubkey)) else: dvalue = _serialize_point(self.client.ecdh(identity=identity, pubkey=remote_pubkey)) - + keyring.sendline(conn, b'S PADDING 0') keyring.sendline(conn, b'D ' + dvalue) diff --git a/libagent/gpg/client.py b/libagent/gpg/client.py index 43788460..b77f93c7 100644 --- a/libagent/gpg/client.py +++ b/libagent/gpg/client.py @@ -8,7 +8,7 @@ log = logging.getLogger(__name__) -def create_identity(user_id, curve_name, keygrip=None): +def create_identity(user_id, curve_name, keygrip = None): """Create GPG identity for hardware device.""" result = interface.Identity(identity_str='gpg://', curve_name=curve_name) result.identity_dict['host'] = user_id @@ -36,13 +36,13 @@ def sign(self, identity, digest): digest = digest[:32] # sign the first 256 bits log.debug('signing digest: %s', util.hexlify(digest)) log.debug('identity type: %s', identity.curve_name) - if identity.curve_name in ('rsa2048', 'rsa4096') and len(digest) == 32: + if (identity.curve_name == 'rsa2048' or identity.curve_name == 'rsa4096') and len(digest) == 32: self.device.sig_hash(b'rsa-sha2-256') - elif identity.curve_name in ('rsa2048', 'rsa4096') and len(digest) == 64: + elif (identity.curve_name == 'rsa2048' or identity.curve_name == 'rsa4096') and len(digest) == 64: self.device.sig_hash(b'rsa-sha2-512') with self.device: sig = self.device.sign(blob=digest, identity=identity) - if identity.curve_name in ('rsa2048', 'rsa4096'): + if (identity.curve_name == 'rsa2048' or identity.curve_name == 'rsa4096'): return util.bytes2num(sig) else: return (util.bytes2num(sig[:32]), util.bytes2num(sig[32:])) diff --git a/libagent/gpg/decode.py b/libagent/gpg/decode.py index 2e513cc6..d5804107 100644 --- a/libagent/gpg/decode.py +++ b/libagent/gpg/decode.py @@ -5,6 +5,7 @@ import io import logging import struct +from Crypto.Util.number import long_to_bytes import ecdsa import nacl.signing @@ -174,7 +175,7 @@ def _parse_pubkey(stream, packet_type='pubkey'): parse_mpis(stream, n=4) # DSA keys are not supported elif p['algo'] == ELGAMAL_ALGO_ID: parse_mpis(stream, n=3) # ElGamal keys are not supported - elif p['algo'] in RSA_ALGO_IDS: + elif p['algo'] in RSA_ALGO_IDS: log.debug('parsing rsa key') mpi = parse_mpi(stream) # RSA key log.debug('mpi: %d (%d bits)', mpi, mpi.bit_length()) @@ -189,7 +190,7 @@ def _parse_pubkey(stream, packet_type='pubkey'): size, = util.readfmt(leftover, 'B') p['kdf'] = leftover.read(size) p['secret'] = leftover.read() - + assert not stream.read() # https://tools.ietf.org/html/rfc4880#section-12.2 diff --git a/libagent/gpg/protocol.py b/libagent/gpg/protocol.py index 65df427c..beadc5c0 100644 --- a/libagent/gpg/protocol.py +++ b/libagent/gpg/protocol.py @@ -123,13 +123,11 @@ def keygrip_nist256(vk): ['q', util.num2bytes(q, size=65)], ]) - def keygrip_rsa(n, length): """Compute keygrip for rsa public keys.""" nhex = b'\x00' + util.num2bytes(n, size=int(length/8)) return hashlib.sha1(nhex).digest() - def keygrip_ed25519(vk): """Compute keygrip for Ed25519 public keys.""" # pylint: disable=line-too-long @@ -142,7 +140,6 @@ def keygrip_ed25519(vk): ['q', vk.encode(encoder=nacl.encoding.RawEncoder)], ]) - def keygrip_curve25519(vk): """Compute keygrip for Curve25519 public keys.""" # pylint: disable=line-too-long @@ -199,7 +196,7 @@ class PublicKey: def __init__(self, curve_name, created, verifying_key, ecdh=False): """Contruct using a ECDSA VerifyingKey object.""" self.curve_name = curve_name - if curve_name not in ('rsa2048', 'rsa4096'): + if curve_name != 'rsa2048' and curve_name != 'rsa4096': self.curve_info = SUPPORTED_CURVES[curve_name] self.ecdh = bool(ecdh) if ecdh: @@ -209,7 +206,7 @@ def __init__(self, curve_name, created, verifying_key, ecdh=False): self.algo_id = self.curve_info['algo_id'] self.ecdh_packet = b'' else: - self.algo_id = 1 # RSA (Encrypt or Sign) (0x1) + self.algo_id = 1 # RSA (Encrypt or Sign) (0x1) self.created = int(created) # time since Epoch self.verifying_key = verifying_key @@ -223,11 +220,11 @@ def data(self): 4, # version self.created, # creation self.algo_id) # public key algorithm ID - if self.algo_id != 1: # ECC + if self.algo_id != 1: # ECC oid = util.prefix_len('>B', self.curve_info['oid']) blob = self.curve_info['serialize'](self.verifying_key) return header + oid + blob + self.ecdh_packet - else: # RSA + else: # RSA blob = util.bytes2num(self.verifying_key) return header + blob diff --git a/libagent/signify/__init__.py b/libagent/signify/__init__.py index f92765e7..aa0ab5d7 100644 --- a/libagent/signify/__init__.py +++ b/libagent/signify/__init__.py @@ -13,6 +13,7 @@ import sys import time +import pkg_resources import semver from .. import formats, server, util diff --git a/libagent/ssh/__init__.py b/libagent/ssh/__init__.py index e3c058ab..68566893 100644 --- a/libagent/ssh/__init__.py +++ b/libagent/ssh/__init__.py @@ -10,11 +10,11 @@ import sys import tempfile import threading -from importlib import metadata import configargparse import daemon +from importlib import metadata from .. import device, formats, server, util from . import client, protocol diff --git a/libagent/ssh/client.py b/libagent/ssh/client.py index 63c9bf5a..a67a699c 100644 --- a/libagent/ssh/client.py +++ b/libagent/ssh/client.py @@ -69,10 +69,8 @@ def parse_ssh_blob(data): res['reserved'] = util.read_frame(i) res['hashalg'] = util.read_frame(i) res['message'] = util.read_frame(i) - # logging statements in client.py expect this to be there and raise without it - res['user'] = b'SSHSIG' - # logging statements in client.py expect this to be there and raise without it - res['key_type'] = res['hashalg'] + res['user'] = b'SSHSIG' # logging statements in client.py expect this to be there and raise without it + res['key_type'] = res['hashalg'] # logging statements in client.py expect this to be there and raise without it else: i = io.BytesIO(data) res['sshsig'] = False diff --git a/libagent/ssh/protocol.py b/libagent/ssh/protocol.py index 73d6f916..e6923437 100644 --- a/libagent/ssh/protocol.py +++ b/libagent/ssh/protocol.py @@ -130,7 +130,7 @@ def sign_message(self, buf): key = formats.parse_pubkey(util.read_frame(buf)) log.debug('looking for %s', key['fingerprint']) blob = util.read_frame(buf) - if key['type'] != b'ssh-rsa': + if (key['type'] != b'ssh-rsa'): assert util.read_frame(buf) == b'' assert not buf.read() @@ -159,11 +159,11 @@ def sign_message(self, buf): log.info('signature status: OK') except formats.ecdsa.BadSignatureError as e: log.exception('signature status: ERROR') - raise ValueError('invalid signature') from e + raise ValueError('invalid signature') log.debug('signature size: %d bytes', len(sig_bytes)) - if key['type'] == b'ssh-rsa': + if (key['type'] == b'ssh-rsa'): if b'rsa-sha2-512' in blob: data = util.frame(util.frame(b'rsa-sha2-512'), util.frame(sig_bytes)) else: diff --git a/libagent/ssh/tests/test_client.py b/libagent/ssh/tests/test_client.py index 97440ee1..9c982244 100644 --- a/libagent/ssh/tests/test_client.py +++ b/libagent/ssh/tests/test_client.py @@ -122,6 +122,4 @@ def test_parse_ssh_signature(): 'namespace': b'file', 'reserved': b'', 'sshsig': True, - 'user': b'SSHSIG', - 'key_type': b'sha512', } diff --git a/libagent/util.py b/libagent/util.py index 1a4710e2..df8c38a2 100644 --- a/libagent/util.py +++ b/libagent/util.py @@ -237,7 +237,12 @@ def wrapper(self, *args, **kwargs): @memoize def which(cmd): """Return full path to specified command, or raise OSError if missing.""" - from shutil import which as _which + try: + # For Python 3 + from shutil import which as _which + except ImportError: + # For Python 2 + from backports.shutil_which import which as _which full_path = _which(cmd) if full_path is None: raise OSError('Cannot find {!r} in $PATH'.format(cmd)) diff --git a/setup.py b/setup.py index 153589d6..177402dc 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,10 @@ 'pycryptodome>=3.9.8', 'docutils>=0.16', 'python-daemon>=2.3.0', + 'wheel>=0.32.3', + 'backports.shutil_which>=3.5.1', 'ConfigArgParse>=0.12.1', + 'python-daemon>=2.1.2', 'ecdsa>=0.13', 'pynacl>=1.4.0', 'mnemonic>=0.18', @@ -30,7 +33,6 @@ 'semver>=2.2', 'unidecode>=0.4.20', ], - python_requires='>=3.8', platforms=['POSIX'], classifiers=[ 'Environment :: Console', diff --git a/tox.ini b/tox.ini index ca5b3cff..abaf6ab0 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps= pylint semver pydocstyle - isort>=5 + isort<5 commands= pycodestyle libagent isort --skip-glob .tox -c libagent From 4040d6c51ba52321b65c820bcd7d3626dc4e7be0 Mon Sep 17 00:00:00 2001 From: onlykey Date: Tue, 26 May 2026 12:24:22 -0400 Subject: [PATCH 37/38] Update README-SSH.md --- doc/README-SSH.md | 91 ++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/doc/README-SSH.md b/doc/README-SSH.md index cf9b4c17..b5bcc811 100644 --- a/doc/README-SSH.md +++ b/doc/README-SSH.md @@ -4,13 +4,13 @@ SSH requires no configuration, but you may put common command line options in `~/.ssh/agent.conf` to avoid repeating them in every invocation. -See `trezor-agent -h` for details on supported options and the configuration file format. +See `onlykey-agent -h` for details on supported options and the configuration file format. -If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md). +If you'd like a onlykey-style PIN entry program, follow [these instructions](README-PINENTRY.md). ## Usage -Use the `trezor-agent` program to work with SSH. It has three main modes of operation: +Use the `onlykey-agent` program to work with SSH. It has three main modes of operation: ##### 1. Export public keys @@ -18,7 +18,7 @@ To get your public key so you can add it to `authorized_hosts` or allow ssh access to a service that supports it, run: ``` -trezor-agent identity@myhost +onlykey-agent identity@myhost ``` The identity (ex: `identity@myhost`) is used to derive the public key and is added as a comment to the exported key string. @@ -28,31 +28,31 @@ The identity (ex: `identity@myhost`) is used to derive the public key and is add Run ``` -$ trezor-agent identity@myhost -- COMMAND --WITH --ARGUMENTS +$ onlykey-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. +Note the `--` separator, which is used to separate `onlykey-agent`'s arguments from the SSH command arguments. Example: ``` - trezor-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob + onlykey-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob ``` As a shortcut you can run ``` -$ trezor-agent identity@myhost -s +$ onlykey-agent identity@myhost -s ``` to start a shell with the proper environment. -##### 3. Connect to a server directly via `trezor-agent` +##### 3. Connect to a server directly via `onlykey-agent` If you just want to connect to a server this is the simplest way to do it: ``` -$ trezor-agent user@remotehost -c +$ onlykey-agent user@remotehost -c ``` The identity `user@remotehost` is used as both the destination user and host as well as for key derivation, so you must generate a separate key for each host you connect to. @@ -78,9 +78,9 @@ This way the user can use SSH-related commands (e.g. `ssh`, `ssh-add`, `sshfs`, Run: - /tmp $ trezor-agent user@ssh.hostname.com -v > hostname.pub - 2015-09-02 15:03:18,929 INFO getting "ssh://user@ssh.hostname.com" public key from Trezor... - 2015-09-02 15:03:23,342 INFO disconnected from Trezor + /tmp $ onlykey-agent user@ssh.hostname.com -v > hostname.pub + 2015-09-02 15:03:18,929 INFO getting "ssh://user@ssh.hostname.com" public key from onlykey... + 2015-09-02 15:03:23,342 INFO disconnected from onlykey /tmp $ cat hostname.pub ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGSevcDwmT+QaZPUEWUUjTeZRBICChxMKuJ7dRpBSF8+qt+8S1GBK5Zj8Xicc8SHG/SE/EXKUL2UU3kcUzE7ADQ= ssh://user@ssh.hostname.com @@ -93,7 +93,7 @@ would allow you to login using the corresponding private key signature. Export your public key and register it in your repository web interface (e.g. [GitHub](https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/)): - $ trezor-agent -v -e ed25519 git@github.com > ~/.ssh/github.pub + $ onlykey-agent -v -e ed25519 git@github.com > ~/.ssh/github.pub Add the following configuration to your `~/.ssh/config` file: @@ -102,7 +102,7 @@ Add the following configuration to your `~/.ssh/config` file: Use the following Bash alias for convenient Git operations: - $ alias ssh-shell='trezor-agent ~/.ssh/github.pub -v --shell' + $ alias ssh-shell='onlykey-agent ~/.ssh/github.pub -v --shell' Now, you can use regular Git commands under the "SSH-enabled" sub-shell: @@ -118,7 +118,7 @@ The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.c For more details, see the following great blog post: https://calebhearth.com/sign-git-with-ssh - $ trezor-agent -e ed25519 user@host --shell + $ onlykey-agent -e ed25519 user@host --shell $ ssh-add -L ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDeAmtnhHlyg4dzGP3/OF4WHX7NoYhClS98EK22q/O5+ $ git config --local user.signingkey "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDeAmtnhHlyg4dzGP3/OF4WHX7NoYhClS98EK22q/O5+" @@ -154,50 +154,42 @@ For more details, see the following great blog post: https://calebhearth.com/sig ##### 1. Create these files in `~/.config/systemd/user` -###### `trezor-ssh-agent.service` +###### `onlykey-ssh-agent.service` ```` [Unit] -Description=trezor-agent SSH agent -Requires=trezor-ssh-agent.socket +Description=onlykey-agent SSH agent +Requires=onlykey-ssh-agent.socket [Service] Type=simple Restart=always Environment="DISPLAY=:0" Environment="PATH=/bin:/usr/bin:/usr/local/bin:%h/.local/bin" -ExecStart=/usr/bin/trezor-agent --foreground --sock-path %t/trezor-agent/S.ssh IDENTITY +ExecStart=/usr/bin/onlykey-agent --foreground --sock-path %t/onlykey-agent/S.ssh IDENTITY ```` -If you've installed `trezor-agent` locally you may have to change the path in `ExecStart=`. +If you've installed `onlykey-agent` locally you may have to change the path in `ExecStart=`. Replace `IDENTITY` with the identity you used when exporting the public key. `IDENTITY` can be a path (starting with `/`) to a file containing a list of public keys -generated by Trezor. I.e. `/home/myUser/.ssh/trezor.conf` with one public key per line. +generated by onlykey. I.e. `/home/myUser/.ssh/onlykey.conf` with one public key per line. This is a more convenient way to have a systemd setup that has to handle multiple keys/hosts. -When updating the file, make sure to restart trezor-agent. +When updating the file, make sure to restart onlykey-agent. -If you have multiple Trezors connected, you can select which one to use via a `TREZOR_PATH` -environment variable. Use `trezorctl list` to find the correct path. Then add it -to the agent with the following line: -```` -Environment="TREZOR_PATH=" -```` -Note that USB paths depend on the _USB port_ which you use. - -###### `trezor-ssh-agent.socket` +###### `onlykey-ssh-agent.socket` ```` [Unit] -Description=trezor-agent SSH agent socket +Description=onlykey-agent SSH agent socket [Socket] -ListenStream=%t/trezor-agent/S.ssh +ListenStream=%t/onlykey-agent/S.ssh FileDescriptorName=ssh -Service=trezor-ssh-agent.service +Service=onlykey-ssh-agent.service SocketMode=0600 DirectoryMode=0700 @@ -208,18 +200,18 @@ WantedBy=sockets.target ##### 2. Run ``` -systemctl --user start trezor-ssh-agent.service trezor-ssh-agent.socket -systemctl --user enable trezor-ssh-agent.socket +systemctl --user start onlykey-ssh-agent.service onlykey-ssh-agent.socket +systemctl --user enable onlykey-ssh-agent.socket ``` ##### 3. Add this line to your `.bashrc` or equivalent file: ```bash -export SSH_AUTH_SOCK=$(systemctl show --user --property=Listen trezor-ssh-agent.socket | grep -o "/run.*" | cut -d " " -f 1) +export SSH_AUTH_SOCK=$(systemctl show --user --property=Listen onlykey-ssh-agent.socket | grep -o "/run.*" | cut -d " " -f 1) ``` -Make sure the SSH_AUTH_SOCK variable matches the location of the socket that trezor-agent -is listening on: `ps -x | grep trezor-agent`. In this setup trezor-agent should start +Make sure the SSH_AUTH_SOCK variable matches the location of the socket that onlykey-agent +is listening on: `ps -x | grep onlykey-agent`. In this setup onlykey-agent should start automatically when the socket is opened. ##### 4. SSH will now automatically use your device key in all terminals. @@ -237,12 +229,12 @@ See here for more ssh protocol details: ##### generate SSH public key ``` -$ trezor-agent -e ed25519 git@github.com | tee ~/.ssh/trezor-github.pub +$ onlykey-agent -e ed25519 git@github.com | tee ~/.ssh/onlykey-github.pub ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIvcbhXyaXXNytCLTDfEMlLuwEhtfo0XmPP1U5RsnOZ4 ``` -##### sign the given file using TREZOR +##### sign the given file using onlykey ``` -$ trezor-agent -e ed25519 git@github.com -- ssh-keygen -Y sign -f ~/.ssh/trezor-github.pub -n file README.md +$ onlykey-agent -e ed25519 git@github.com -- ssh-keygen -Y sign -f ~/.ssh/onlykey-github.pub -n file README.md Signing file README.md Write signature to README.md.sig ``` @@ -262,14 +254,9 @@ debug1: allowed:1: matched key and principal Good "file" signature for git@github.com with ED25519 key SHA256:6UBhPb5SOoCUfasGC1/aCBegYov0/P3ajd6eNbYg77A ``` -## Troubleshooting - -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`) . - #### `IdentitiesOnly` SSH option -Note that your local SSH configuration may ignore `trezor-agent`, if it has `IdentitiesOnly` option set to `yes`. +Note that your local SSH configuration may ignore `onlykey-agent`, if it has `IdentitiesOnly` option set to `yes`. IdentitiesOnly Specifies that ssh(1) should only use the authentication identity files configured in @@ -280,7 +267,7 @@ Note that your local SSH configuration may ignore `trezor-agent`, if it has `Ide If you are failing to connect, save your public key using: - $ trezor-agent -vv foobar@hostname.com > ~/.ssh/hostname.pub + $ onlykey-agent -vv foobar@hostname.com > ~/.ssh/hostname.pub And add the following lines to `~/.ssh/config` (providing the public key explicitly to SSH): @@ -290,9 +277,9 @@ And add the following lines to `~/.ssh/config` (providing the public key explici Then, the following commands should successfully command to the remote host: - $ trezor-agent -v foobar@hostname.com -s + $ onlykey-agent -v foobar@hostname.com -s $ ssh foobar@hostname.com or, - $ trezor-agent -v foobar@hostname.com -c + $ onlykey-agent -v foobar@hostname.com -c From 086f22a9655986f5e7749dc0544a932130501061 Mon Sep 17 00:00:00 2001 From: onlykey Date: Tue, 26 May 2026 12:26:47 -0400 Subject: [PATCH 38/38] Update README-Windows.md --- doc/README-Windows.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/README-Windows.md b/doc/README-Windows.md index 68b6e0c7..3b2795d7 100644 --- a/doc/README-Windows.md +++ b/doc/README-Windows.md @@ -40,7 +40,7 @@ Run the following command: pip install -agent ``` -**Note:** Some agent packages use underscore instead of hyphen in the package name. For example, the Trezor agent package is `trezor-agent`, while the Ledger agent package is `ledger_agent`. This only applies to the `pip` package names. All other commands use a hyphen for all devices. +**Note:** Some agent packages use underscore instead of hyphen in the package name. For example, the onlykey agent package is `onlykey-agent`, while the Ledger agent package is `ledger_agent`. This only applies to the `pip` package names. All other commands use a hyphen for all devices. ## Building from source @@ -51,17 +51,17 @@ winget install -e --id Git.Git Create a directory for the source code, and clone the repository. Before running this command, you may want to change to a directory where you usually hold documents or source code packages. ``` -git clone https://github.com/romanz/trezor-agent.git +git clone https://github.com/trustcrypto/onlykey-agent.git ``` Build and install the library: ``` -pip install -e trezor-agent +pip install -e onlykey-agent ``` Build and install the agent of your choice: ``` -pip install -e trezor-agent/agents/ +pip install -e onlykey-agent/agents/ ``` ## Usage @@ -197,7 +197,7 @@ The content of the file may look something like this: ``` # recipient: agewnc7uu1btfhmr95dia9txto4ke1lm7azka3x1zkh17fk52guykrc2xk11 # SLIP-0017: MyIdentityPath -AGE-PLUGIN-TREZOR-1F4U5JER9DE6XJARE2PSHG6Q4UFNE8 +AGE-PLUGIN-onlykey-1F4U5JER9DE6XJARE2PSHG6Q4UFNE8 ``` Next, in Explorer, right click on the file you want to encrypt, and select `Encrypt with age`. Pick the `Recipient` mode. Copy the code appearing after `recipient:` in your `age.identity` file, e.g. `agewnc7uu1btfhmr95dia9txto4ke1lm7azka3x1zkh17fk52guykrc2xk11`, and paste it in the `Recipient, recipient file, or identity file` box. Finally, click on `Encrypt`, and pick the file location to save the encrypted file. Be sure to give it an `.age` suffix, so it can be easily decrypted. @@ -300,7 +300,7 @@ Also look for any other SSH agents you may have installed on your system. If you receive the error: ``` -gpg: invalid size of lockfile 'C:\Users\MyUser/gnupg/trezor/pubring.kbx.lock' +gpg: invalid size of lockfile 'C:\Users\MyUser/gnupg/onlykey/pubring.kbx.lock' ``` It means Git is trying to run the wrong version of GPG. First, Figure out where your GPG is: ``` @@ -327,7 +327,7 @@ gpg --list-secret-keys --keyid-format=long Example output: ``` C:\Users\MyUser>gpg --list-secret-keys --keyid-format=long -C:\Users\MyUser\.gnupg\trezor\pubring.kbx +C:\Users\MyUser\.gnupg\onlykey\pubring.kbx ---------------------------------------- sec ed25519/100A53DB673C6714 1970-01-01 [SC] 1E98503AC72ECBF78CDC3E415188B41C865FD25C @@ -344,7 +344,7 @@ Example output: ``` C:\Users\MyUser>type "%USERPROFILE%\.gnupg\\gpg.conf" # Hardware-based GPG configuration -agent-program "C:\Users\MyUser/.gnupg/trezor\run-agent.bat" +agent-program "C:\Users\MyUser/.gnupg/onlykey\run-agent.bat" personal-digest-preferences SHA512 default-key 0x100A53DB673C6714 ```