Merge pull request #382 from JamieDriver/blockstream_jade_support

Add support for the Blockstream Jade hww
This commit is contained in:
Roman Zeyde
2022-03-10 23:37:28 +02:00
committed by GitHub
8 changed files with 230 additions and 15 deletions

View File

@@ -24,6 +24,7 @@ agents to interact with several different hardware devices:
* [`libagent`](https://pypi.org/project/libagent/): shared library * [`libagent`](https://pypi.org/project/libagent/): shared library
* [`trezor-agent`](https://pypi.org/project/trezor-agent/): Using Trezor as hardware-based SSH/PGP agent * [`trezor-agent`](https://pypi.org/project/trezor-agent/): Using Trezor as hardware-based SSH/PGP agent
* [`ledger_agent`](https://pypi.org/project/ledger_agent/): Using Ledger as hardware-based SSH/PGP 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
* [`keepkey_agent`](https://pypi.org/project/keepkey_agent/): Using KeepKey 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 * [`onlykey-agent`](https://pypi.org/project/onlykey-agent/): Using OnlyKey as hardware-based SSH/PGP agent

View File

@@ -0,0 +1,7 @@
import libagent.gpg
import libagent.ssh
from libagent.device.jade import BlockstreamJade as DeviceType
ssh_agent = lambda: libagent.ssh.main(DeviceType)
gpg_tool = lambda: libagent.gpg.main(DeviceType)
gpg_agent = lambda: libagent.gpg.run_agent(DeviceType)

45
agents/jade/setup.py Normal file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='jade_agent',
version='0.1.0',
description='Using Blockstream Jade as hardware SSH agent',
author='Jamie C. Driver',
author_email='jamie@blockstream.com',
url='http://github.com/romanz/trezor-agent',
scripts=['jade_agent.py'],
install_requires=[
# FIXME: will need libagent version that includes jade support - 0.14.5 ??
# FIXME: will need to put the tag version just as we are about to apply it ?
'libagent>=0.14.4',
# Jade py api from github source, v0.1.33
'jadepy @ git+https://github.com/Blockstream/Jade.git@0.1.33#egg=jadepy[requests]'
],
# Not sure why this doesn't work ...
# dependency_links=['https://github.com/Blockstream/Jade/tarball/0.1.33#egg=jadepy[requests]'],
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': [
'jade-agent = jade_agent:ssh_agent',
'jade-gpg = jade_agent:gpg_tool',
'jade-gpg-agent = jade_agent:gpg_agent',
]},
)

View File

@@ -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. 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 and Ledger) 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 (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.
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. 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.

View File

@@ -152,7 +152,31 @@ Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_ag
$ pip3 install --user -e trezor-agent/agents/onlykey $ pip3 install --user -e trezor-agent/agents/onlykey
``` ```
# 6. Installation Troubleshooting # 6. Install the Blockstream Jade agent
1. Make sure you are running the latest firmware version on your Blockstream Jade:
* [Jade firmware releases](https://github.com/Blockstream/Jade/blob/master/CHANGELOG.md): `0.1.33+`
2. Make sure that your `udev` rules are configured [correctly](https://github.com/bitcoin-core/HWI/blob/master/hwilib/udev/55-usb-jade.rules).
3. If necessary, ensure the user is added to the [`dialout` group](https://help.blockstream.com/hc/en-us/articles/900005443223-My-Blockstream-Jade-is-not-recognized-by-my-computer)
4. Then, install the latest [jade-agent](https://pypi.python.org/pypi/jade-agent) package:
```
$ pip3 install jade-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/jade
```
# 7. Installation Troubleshooting
If there is an import problem with the installed `protobuf` package, 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. see [this issue](https://github.com/romanz/trezor-agent/issues/28) for fixing it.

View File

@@ -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: work well for you. If possible:
* record the session (e.g. using [asciinema](https://asciinema.org)) * record the session (e.g. using [asciinema](https://asciinema.org))
* attach the GPG agent log from `~/.gnupg/{trezor,ledger}/gpg-agent.log` (can be [encrypted](https://keybase.io/romanz)) * attach the GPG agent log from `~/.gnupg/{trezor,ledger,jade}/gpg-agent.log` (can be [encrypted](https://keybase.io/romanz))
Thanks! Thanks!
@@ -18,14 +18,14 @@ Thanks!
Run Run
``` ```
$ (trezor|keepkey|ledger|onlykey)-gpg init "Roman Zeyde <roman.zeyde@gmail.com>" $ (trezor|keepkey|ledger|jade|onlykey)-gpg init "Roman Zeyde <roman.zeyde@gmail.com>"
``` ```
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. 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). If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md).
2. Add `export GNUPGHOME=~/.gnupg/(trezor|keepkey|ledger|onlykey)` to your `.bashrc` or other environment file. 2. Add `export GNUPGHOME=~/.gnupg/(trezor|keepkey|ledger|jade|onlykey)` 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). 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,7 +203,7 @@ Follow [these instructions](enigmail.md) to set up Enigmail in Thunderbird.
##### 1. Create these files in `~/.config/systemd/user` ##### 1. Create these files in `~/.config/systemd/user`
Replace `trezor` with `keepkey` or `ledger` or `onlykey` as required. Replace `trezor` with `keepkey` or `ledger` or `jade` or `onlykey` as required.
###### `trezor-gpg-agent.service` ###### `trezor-gpg-agent.service`

View File

@@ -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. 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|onlykey)-agent -h` for details on supported options and the configuration file format. See `(trezor|keepkey|ledger|jade|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 Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md).
## 2. Usage ## 2. Usage
Use the `(trezor|keepkey|ledger|onlykey)-agent` program to work with SSH. It has three main modes of operation: Use the `(trezor|keepkey|ledger|jade|onlykey)-agent` program to work with SSH. It has three main modes of operation:
##### 1. Export public keys ##### 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: ssh access to a service that supports it, run:
``` ```
(trezor|keepkey|ledger|onlykey)-agent identity@myhost (trezor|keepkey|ledger|jade|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. 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 Run
``` ```
$ (trezor|keepkey|ledger|onlykey)-agent identity@myhost -- COMMAND --WITH --ARGUMENTS $ (trezor|keepkey|ledger|jade|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. 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: Example:
``` ```
(trezor|keepkey|ledger|onlykey)-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob (trezor|keepkey|ledger|jade|onlykey)-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob
``` ```
As a shortcut you can run As a shortcut you can run
``` ```
$ (trezor|keepkey|ledger|onlykey)-agent identity@myhost -s $ (trezor|keepkey|ledger|jade|onlykey)-agent identity@myhost -s
``` ```
to start a shell with the proper environment. to start a shell with the proper environment.
##### 3. Connect to a server directly via `(trezor|keepkey|ledger|onlykey)-agent` ##### 3. Connect to a server directly via `(trezor|keepkey|ledger|jade|onlykey)-agent`
If you just want to connect to a server this is the simplest way to do it: If you just want to connect to a server this is the simplest way to do it:
``` ```
$ (trezor|keepkey|ledger|onlykey)-agent user@remotehost -c $ (trezor|keepkey|ledger|jade|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. 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.
@@ -118,7 +118,7 @@ The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.c
##### 1. Create these files in `~/.config/systemd/user` ##### 1. Create these files in `~/.config/systemd/user`
Replace `trezor` with `keepkey` or `ledger` or `onlykey` as required. Replace `trezor` with `keepkey` or `ledger` or `jade` or `onlykey` as required.
###### `trezor-ssh-agent.service` ###### `trezor-ssh-agent.service`

138
libagent/device/jade.py Normal file
View File

@@ -0,0 +1,138 @@
"""Jade-related code (see https://www.keepkey.com/)."""
import ecdsa
import logging
import semver
from serial.tools import list_ports
from jadepy import JadeAPI
from . import interface
from .. import formats
from .. import util
log = logging.getLogger(__name__)
def _verify_support(identity, ecdh):
"""Make sure the device supports given configuration."""
if identity.get_curve_name(ecdh=ecdh) != formats.CURVE_NIST256:
raise NotImplementedError(
'Unsupported elliptic curve: {}'.format(identity.curve_name))
class BlockstreamJade(interface.Device):
"""Connection to Blockstream Jade device."""
MIN_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 33)
DEVICE_IDS = [(0x10c4, 0xea60), (0x1a86, 0x55d4)]
connection = None
@classmethod
def package_name(cls):
"""Python package name (at PyPI)."""
return 'jade-agent'
def connect(self):
# Return the existing connection if we have one
if BlockstreamJade.connection is not None:
return BlockstreamJade.connection
# Jade is a serial (over usb) device, it shows as a serial/com port device.
# Scan com ports looking for the relevant vid and pid, and connect to the
# first matching device. Then call 'auth_user' - this usually requires network
# access in order to unlock the device with a PIN and the remote blind pinserver.
devices = []
for devinfo in list_ports.comports():
device_product_key = (devinfo.vid, devinfo.pid)
if device_product_key in self.DEVICE_IDS:
try:
jade = JadeAPI.create_serial(devinfo.device)
# Monkey-patch a no-op 'close()' method to suppress logged errors
jade.close = lambda: log.debug("Close called")
# Connect and fetch version info
jade.connect()
verinfo = jade.get_version_info()
# Check minimum supported firmware version (ignore candidate/build parts)
fwversion = semver.VersionInfo.parse(verinfo['JADE_VERSION'])
if self.MIN_SUPPORTED_FW_VERSION > fwversion.finalize_version():
msg = ('Outdated {} firmware for device. Please update using'
' a Blockstream Green companion app')
raise ValueError(msg.format(fwversion))
# Authenticate the user (unlock with pin)
# NOTE: usually requires network access unless already unlocked
# (or temporary 'Emergency Restore' wallet is already in use).
network = 'testnet' if verinfo.get('JADE_NETWORKS') == 'TEST' else 'mainnet'
while not jade.auth_user(network):
log.warning("PIN incorrect, please try again")
# Cache the connection to jade
BlockstreamJade.connection = jade
return jade
except Exception as e:
raise interface.NotFoundError(
'{} not connected: "{}"'.format(self, e))
@staticmethod
def get_identity_string(identity):
return interface.identity_to_string(identity.identity_dict)
@staticmethod
def load_uncompressed_pubkey(pubkey, curve_name):
assert curve_name == formats.CURVE_NIST256
assert len(pubkey) == 65 and pubkey[0] == 0x04
curve = ecdsa.NIST256p
point = ecdsa.ellipticcurve.Point(curve.curve,
util.bytes2num(pubkey[1:33]),
util.bytes2num(pubkey[33:65]))
return ecdsa.VerifyingKey.from_public_point(point, curve=curve,
hashfunc=formats.hashfunc)
def pubkey(self, identity, ecdh=False):
"""Get PublicKey object for specified BIP32 address and elliptic curve."""
_verify_support(identity, ecdh)
identity_string = self.get_identity_string(identity)
curve_name = identity.get_curve_name(ecdh=ecdh)
key_type = 'slip-0017' if ecdh else 'slip-0013'
log.debug('"%s" getting %s public key (%s) from %s',
identity_string, key_type, curve_name, self)
result = self.conn.get_identity_pubkey(identity_string, curve_name, key_type)
log.debug('result: %s', result)
assert len(result) == 33 or len(result) == 65
convert_pubkey = formats.decompress_pubkey if len(result) == 33 else self.load_uncompressed_pubkey
return convert_pubkey(pubkey=result, curve_name=curve_name)
def sign(self, identity, blob):
"""Sign given blob and return the signature (as bytes)."""
_verify_support(identity, ecdh=False)
identity_string = self.get_identity_string(identity)
curve_name = identity.get_curve_name(ecdh=False)
log.debug('"%s" signing %r (%s) on %s',
identity_string, blob, curve_name, self)
result = self.conn.sign_identity(identity_string, curve_name, blob)
log.debug('result: %s', result)
signature = result['signature']
assert len(signature) == 64 or (len(signature) == 65 and signature[0] == 0x00)
if len(signature) == 65:
signature = signature[1:]
return signature
def ecdh(self, identity, pubkey):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
_verify_support(identity, ecdh=True)
identity_string = self.get_identity_string(identity)
curve_name = identity.get_curve_name(ecdh=True)
log.debug('"%s" shared session key (%s) for %r from %s',
identity_string, curve_name, pubkey, self)
result = self.conn.get_identity_shared_key(identity_string, curve_name, pubkey)
log.debug('result: %s', result)
return result