Add support for the Blockstream Jade hww
Supports ssh and gpg, incl. ecdh/decryption. Initially only supports curve 'nist256p1'.
This commit is contained in:
@@ -24,6 +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 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
|
||||
* [`onlykey-agent`](https://pypi.org/project/onlykey-agent/): Using OnlyKey as hardware-based SSH/PGP agent
|
||||
|
||||
|
||||
7
agents/jade/jade_agent.py
Normal file
7
agents/jade/jade_agent.py
Normal 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
45
agents/jade/setup.py
Normal 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',
|
||||
]},
|
||||
)
|
||||
@@ -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 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.
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
# 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,
|
||||
see [this issue](https://github.com/romanz/trezor-agent/issues/28) for fixing it.
|
||||
|
||||
@@ -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}/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!
|
||||
|
||||
@@ -18,14 +18,14 @@ Thanks!
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -203,7 +203,7 @@ 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 `onlykey` as required.
|
||||
Replace `trezor` with `keepkey` or `ledger` or `jade` or `onlykey` as required.
|
||||
|
||||
###### `trezor-gpg-agent.service`
|
||||
|
||||
|
||||
@@ -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|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).
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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|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.
|
||||
@@ -28,7 +28,7 @@ The identity (ex: `identity@myhost`) is used to derive the public key and is add
|
||||
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.
|
||||
@@ -36,23 +36,23 @@ Note the `--` separator, which is used to separate `trezor-agent`'s arguments fr
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
$ (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.
|
||||
|
||||
##### 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:
|
||||
|
||||
```
|
||||
$ (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.
|
||||
@@ -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`
|
||||
|
||||
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`
|
||||
|
||||
|
||||
138
libagent/device/jade.py
Normal file
138
libagent/device/jade.py
Normal 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
|
||||
Reference in New Issue
Block a user