From 40c70ffe448953d3d686801b6bd26f64e4c15cd9 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 5 Feb 2026 18:19:23 +0100 Subject: [PATCH 1/2] Add deprecation warning message --- README.md | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 429f390..e8d25df 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,54 @@ -# blue-app-ssh-agent +# app-ssh-agent + +## ⚠️ DEPRECATION WARNING + +> **This application is no longer maintained and will be deprecated.** +> +> Please migrate to one of the following alternatives: +> +> - **[app-openpgp](https://github.com/LedgerHQ/app-openpgp)** - OpenPGP application for Ledger devices +> - Check [ssh support](https://github.com/LedgerHQ/app-openpgp/blob/develop/doc/user/app-openpgp.rst#ssh) +> - **[app-security-key](https://github.com/LedgerHQ/app-security-key)** - FIDO2/U2F security key application +> - Check [Web authn support](https://www.ledger.com/blog/strengthen-the-security-of-your-accounts-with-webauthn) +> +> No further updates or support will be provided for app-ssh-agent. + +### Ledger SSH Methods Comparison + +| Feature | **app-openpgp** (The "Smart Card" Way) | **app-security-key** (The "Modern" Way) | +| :--------------------------------- | :--------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------- | +| **Protocol** | **OpenPGP Card** (ISO 7816) | **FIDO2 / WebAuthn** (CTAP2) | +| **Integration** | **Bridge:** Uses `gpg-agent` as a translator between SSH and the device. | **Native:** Built directly into OpenSSH (v8.2+) via the `-sk` extension. | +| **Complexity** | **Configuration Required:** Involves setting up the GPG agent and environment variables. | **Minimal:** Standard `ssh-keygen` command. No extra software needed. | +| **Algorithms** | **RSA** (up to 4096), Ed25519, SECP256K1, SECP256R1 (NIST P-256). | **Ed25519**, NIST P-256. (**NO RSA**). | +| **Compatibility** | **Universal:** Works with almost any SSH server (legacy & modern). | **Modern:** Client requires OpenSSH 8.2+. Server requires recent OpenSSH. | +| **Key Storage** | **Stateful:** Private keys are stored permanently in the Ledger's NVRAM. | **Stateless / Hybrid:** "Key Handle" file stored on PC; Secret derived on Ledger during login. | +| **Agent Used** | `gpg-agent` (impersonating `ssh-agent`). | Standard `ssh-agent`. | +| **Touch Policy** | **Flexible:** Can be cached for a session (if configured). | **Mandatory:** Requires a physical touch for every login attempt. | +| **Migration from `app-ssh-agent`** | **Not Possible:** Protocol mismatch. You must generate new keys. | **Not Possible:** Protocol mismatch. You must generate new keys. | + +## App overview A simple PGP and SSH agent for Ledger Blue, supporting prime256v1 and ed25519 keys. -This agent is compatible with the third party SSH/PGP host client from Roman Zeyde available at https://github.com/romanz/trezor-agent - it is recommended to use it for extra functionalities +This agent is compatible with the third party SSH/PGP host client from Roman Zeyde available at [trezor](https://github.com/romanz/trezor-agent). +It is recommended to use it for extra functionalities. -You can also use the SSH functionalities with the following instructions using Python 2 : +You can also use the SSH functionalities with the following instructions using Python 2: -Run getPublicKey.py to get the public key in SSH format, to be added to your authorized keys on the target +Run `getPublicKey.py` to get the public key in SSH format, to be added to your authorized keys on the target -``` +```bash python getPublicKey.py ecdsa-sha2-nistp256 AAAA.... ``` -Run agent.py, providing the base64 encoded key retrieved earlier +Run `agent.py`, providing the base64 encoded key retrieved earlier -``` +```bash python agent.py --key AAAA.... ``` Export the environment variables in your shell to use it -You can also set the derivation path from the master seed by providing it with the --path parameter. - +You can also set the derivation path from the master seed by providing it with the `--path` parameter. From c336d053798581f4170ec665bc3121e223a997bb Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Fri, 6 Feb 2026 12:18:32 +0100 Subject: [PATCH 2/2] Migrate python scripts to python3 --- README.md | 2 +- agent.py | 92 +++++++++++++++++++++--------------------------- getPublicKey.py | 25 +++++++------ requirements.txt | 1 + 4 files changed, 55 insertions(+), 65 deletions(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index e8d25df..4b9e32b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ A simple PGP and SSH agent for Ledger Blue, supporting prime256v1 and ed25519 ke This agent is compatible with the third party SSH/PGP host client from Roman Zeyde available at [trezor](https://github.com/romanz/trezor-agent). It is recommended to use it for extra functionalities. -You can also use the SSH functionalities with the following instructions using Python 2: +You can also use the SSH functionalities with the following instructions using Python. Run `getPublicKey.py` to get the public key in SSH format, to be added to your authorized keys on the target diff --git a/agent.py b/agent.py index 4ccd288..965934b 100644 --- a/agent.py +++ b/agent.py @@ -18,14 +18,13 @@ ******************************************************************************** """ from ledgerblue.comm import getDongle -from ledgerblue.commException import CommException import argparse import struct import base64 import os import socket import tempfile -import thread +import threading import logging SIG_HEADER = "ecdsa-sha2-nistp256" @@ -44,64 +43,56 @@ def handleRequestIdentities(message, key, eddsa, path): logging.debug("Request identities") - response = chr(SSH2_AGENT_IDENTITIES_ANSWER) + response = bytes([SSH2_AGENT_IDENTITIES_ANSWER]) response += struct.pack(">I", 1) response += struct.pack(">I", len(key)) + key - response += struct.pack(">I", len(path)) + path + response += struct.pack(">I", len(path)) + path.encode() return response def handleSignRequest(message, key, eddsa, path): logging.debug("Sign request") blobSize = struct.unpack(">I", message[0:4])[0] blob = message[4 : 4 + blobSize] - if blob <> key: - logging.debug("Client sent a different blob " + blob.encode('hex')) - return chr(SSH_AGENT_FAILURE) + if blob != key: + logging.debug("Client sent a different blob " + blob.hex()) + return bytes([SSH_AGENT_FAILURE]) challengeSize = struct.unpack(">I", message[4 + blobSize : 4 + blobSize + 4])[0] challenge = message[4 + blobSize + 4: 4 + blobSize + 4 + challengeSize] # Send the challenge in chunks dongle = getDongle(logging.getLogger().isEnabledFor(logging.DEBUG)) donglePath = parse_bip32_path(args.path) offset = 0 - while offset <> len(challenge): - data = "" + while offset != len(challenge): + data = b"" if offset == 0: donglePath = parse_bip32_path(path) - data = chr(len(donglePath) / 4) + donglePath - if (len(challenge) - offset) > (255 - len(data)): - chunkSize = (255 - len(data)) - else: - chunkSize = len(challenge) - offset + data = bytes([len(donglePath) // 4]) + donglePath + chunkSize = min(255 - len(data), len(challenge) - offset) data += challenge[offset : offset + chunkSize] - if offset == 0: - p1 = 0x00 - else: - p1 = 0x01 - if eddsa: - p2 = 0x02 - else: - p2 = 0x01 + p1 = 0x00 if offset == 0 else 0x01 + p2 = 0x02 if eddsa else 0x01 offset += chunkSize if offset == len(challenge): p1 |= 0x80 - apdu = "8004".decode('hex') + chr(p1) + chr(p2) + chr(len(data)) + data - signature = dongle.exchange(bytes(apdu)) + apdu = bytes.fromhex("8004") + bytes([p1, p2, len(data)]) + data + signature = dongle.exchange(apdu) dongle.close() # Parse r and s rLength = signature[3] r = signature[4 : 4 + rLength] sLength = signature[4 + rLength + 1] s = signature[4 + rLength + 2:] - r = str(r) - s = str(s) + r = bytes(r) + s = bytes(s) encodedSignatureValue = struct.pack(">I", len(r)) + r encodedSignatureValue += struct.pack(">I", len(s)) + s - encodedSignature = struct.pack(">I", len(SIG_HEADER)) + SIG_HEADER + sig_header = SIG_HEADER_EDDSA if eddsa else SIG_HEADER + encodedSignature = struct.pack(">I", len(sig_header)) + sig_header.encode() encodedSignature += struct.pack(">I", len(encodedSignatureValue)) + encodedSignatureValue - response = chr(SSH2_AGENT_SIGN_RESPONSE) + response = bytes([SSH2_AGENT_SIGN_RESPONSE]) response += struct.pack(">I", len(encodedSignature)) + encodedSignature return response @@ -122,44 +113,44 @@ def clientHandlerInternal(connection, key, eddsa, comment): message = connection.recv(size) except socket.timeout: logging.debug("Timeout") - message = "" + message = "" if len(message) == 0: logging.debug("Client dropped connection") break - logging.debug("<= " + message.encode('hex')) - messageType = ord(message[0]) + logging.debug("<= " + message.hex()) + messageType = message[0] if messageType == SSH2_AGENTC_REQUEST_IDENTITIES: response = handleRequestIdentities(message[1:], key, eddsa, comment) elif messageType == SSH2_AGENTC_SIGN_REQUEST: response = handleSignRequest(message[1:], key, eddsa, comment) else: logging.debug("Unhandled message") - response = chr(SSH_AGENT_FAILURE) + response = bytes([SSH_AGENT_FAILURE]) agentResponse = struct.pack(">I", len(response)) + response - logging.debug("=> " + agentResponse.encode('hex')) - connection.send(agentResponse) + logging.debug("=> " + agentResponse.hex()) + connection.send(agentResponse) -def clientHandler(connection, key, eddsa, comment): +def clientHandler(connection, key, eddsa, comment): try: clientHandlerInternal(connection, key, eddsa, comment) except Exception: logging.debug("Internal error handling client", exc_info=True) - response = chr(SSH_AGENT_FAILURE) + response = bytes([SSH_AGENT_FAILURE]) agentResponse = struct.pack(">I", len(response)) + response - logging.debug("=> " + agentResponse.encode('hex')) - connection.send(agentResponse) + logging.debug("=> " + agentResponse.hex()) + connection.send(agentResponse) def parse_bip32_path(path): if len(path) == 0: - return "" - result = "" + return b"" + result = b"" elements = path.split('/') for pathElement in elements: element = pathElement.split('\'') if len(element) == 1: - result = result + struct.pack(">I", int(element[0])) + result += struct.pack(">I", int(element[0])) else: - result = result + struct.pack(">I", 0x80000000 | int(element[0])) + result += struct.pack(">I", 0x80000000 | int(element[0])) return result parser = argparse.ArgumentParser() @@ -169,23 +160,23 @@ def parse_bip32_path(path): parser.add_argument('--debug', help="Display debugging information", action='store_true') args = parser.parse_args() -if args.path == None: +if args.path is None: args.path = "44'/535348'/0'/0/0" -if args.key == None: +if args.key is None: raise Exception("No key specified") if args.debug: - logging.getLogger().setLevel(logging.DEBUG) + logging.basicConfig(level=logging.DEBUG) keyBlob = base64.b64decode(args.key) socketPath = tempfile.NamedTemporaryFile(prefix=SOCK_PREFIX, delete=False) os.unlink(socketPath.name) -print "Export those variables in your shell to use this agent" -print "export SSH_AUTH_SOCK=" + socketPath.name -print "export SSH_AGENT_PID=" + str(os.getpid()) -print "Agent running ..." +print("Export those variables in your shell to use this agent") +print("export SSH_AUTH_SOCK=" + socketPath.name) +print("export SSH_AGENT_PID=" + str(os.getpid())) +print("Agent running ...") server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.bind(socketPath.name) @@ -196,9 +187,8 @@ def parse_bip32_path(path): connection, addr = server.accept() logging.debug("New client connected") connection.settimeout(TIMEOUT) - thread.start_new_thread(clientHandler, (connection, keyBlob, args.ed25519, args.path)) + threading.Thread(target=clientHandler, args=(connection, keyBlob, args.ed25519, args.path)).start() except KeyboardInterrupt: pass finally: os.unlink(socketPath.name) - diff --git a/getPublicKey.py b/getPublicKey.py index 9832162..a2573d3 100644 --- a/getPublicKey.py +++ b/getPublicKey.py @@ -18,7 +18,6 @@ ******************************************************************************** """ from ledgerblue.comm import getDongle -from ledgerblue.commException import CommException import argparse import struct import base64 @@ -29,13 +28,13 @@ def parse_bip32_path(path): if len(path) == 0: - return "" - result = "" + return b"" + result = b"" elements = path.split('/') for pathElement in elements: element = pathElement.split('\'') if len(element) == 1: - result = result + struct.pack(">I", int(element[0])) + result = result + struct.pack(">I", int(element[0])) else: result = result + struct.pack(">I", 0x80000000 | int(element[0])) return result @@ -43,6 +42,7 @@ def parse_bip32_path(path): parser = argparse.ArgumentParser() parser.add_argument('--path', help="BIP 32 path to retrieve") parser.add_argument("--ed25519", help="Use Ed25519 curve", action='store_true') +parser.add_argument("--verbose", help="Enable verbose output", action='store_true') args = parser.parse_args() if args.path == None: @@ -57,21 +57,20 @@ def parse_bip32_path(path): donglePath = parse_bip32_path(args.path) apdu = "800200" + p2 -apdu = apdu.decode('hex') + chr(len(donglePath) + 1) + chr(len(donglePath) / 4) + donglePath +apdu = bytes.fromhex(apdu) + bytes([len(donglePath) + 1]) + bytes([len(donglePath) // 4]) + donglePath -dongle = getDongle(True) +dongle = getDongle(args.verbose) result = dongle.exchange(bytes(apdu)) -key = str(result[1:]) -blob = struct.pack(">I", len(KEY_HEADER)) + keyHeader +key = result[1:] +blob = struct.pack(">I", len(keyHeader)) + keyHeader.encode() if args.ed25519: keyX = bytearray(key[0:32]) keyY = bytearray(key[32:][::-1]) - if ((keyX[31] & 1)<>0): + if ((keyX[31] & 1) != 0): keyY[31] |= 0x80 - key = str(keyY) + key = bytes(keyY) else: - blob += struct.pack(">I", len(CURVE_NAME)) + CURVE_NAME + blob += struct.pack(">I", len(CURVE_NAME)) + CURVE_NAME.encode() blob += struct.pack(">I", len(key)) + key -print keyHeader + " " + base64.b64encode(blob) - +print(f"{keyHeader} {base64.b64encode(blob).decode()}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2b620d3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +ledgerblue