Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: romanz/trezor-agent
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: aeb70544cb37a731cbe07dd962e3be638baf4592
Choose a base ref
..
head repository: romanz/trezor-agent
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0eae0a4e3fdd83349f28e5b5e87cde92a01148d4
Choose a head ref
Showing with 109 additions and 8 deletions.
  1. +3 −3 .travis.yml
  2. +51 −0 doc/DESIGN.md
  3. +32 −0 doc/README-GPG.md
  4. +2 −0 libagent/gpg/decode.py
  5. +1 −0 libagent/gpg/keyring.py
  6. +14 −0 libagent/gpg/protocol.py
  7. +2 −2 libagent/ssh/__init__.py
  8. +1 −0 setup.py
  9. +3 −3 tox.ini
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -13,13 +13,13 @@ cache:
before_install:
- pip install -U pip wheel
- pip install -U setuptools
- pip install -U pylint coverage pep8 pydocstyle
- pip install -U pylint coverage pycodestyle pydocstyle

install:
- pip install -e .
- pip install -U -e .

script:
- pep8 libagent
- pycodestyle libagent
- pylint --reports=no --rcfile .pylintrc libagent
- pydocstyle libagent
- coverage run --source libagent/ -m py.test -v
51 changes: 51 additions & 0 deletions doc/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Design

Most cryptographic tools (such as gpg, ssh and openssl) allow the offloading of some key cryptographic steps to *engines* or *agents*. This is to allow sensitive operations, such as asking for a password or doing the actual encryption step, to be kept separate from the larger body of code. This makes it easier to secure those steps, move them onto hardware or easier to audit.

SSH and GPG do this by means of a simple interprocess communication protocol (usually a unix domain socket) and an agent (`ssh-agent`) or GPG key daemon (`gpg-agent`). The `trezor-agent` mimics these two protocols.

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.

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.

## Key Naming

`trezor-agent` goes to great length to avoid using the valuable parent key.

The rationale behind this is that `trezor-agent` is to some extent condemned to *blindly* signing any NONCE given to it (e.g. as part of a challenge respone, or as the hash/hmac of someting to sign).

And doing so with the master private key is risky - as rogue (ssh) server could possibly provide a doctored NONCE that happens to be tied to a transaction or something else.

It therefore uses only derived child keys pairs instead (according to the [BIP-0032: Hierarchical Deterministic Wallets][1] system) - and ones on different leafs. So the parent key is only used within the device for creating the child keys - and not exposed in any way to `trezor-agent`.

### SSH

It is common for SSH users to use one (or a few) private keys with SSH on all servers they log into. The `trezor-agent` is slightly more cautious and derives a child key that is *unique* to the server and username you are logging into from your master private key on the device.

So taking a commmand such as:

$ trezor-agent -c user@fqdn.com

The `trezor-agent` will take the `user`@`fqdn.com`; canonicalise it (e.g. to add the ssh default port number if none was specified) and then apply some simple hashing (See [SLIP-0013 : Authentication using deterministic hierarchy][2]). The resulting 128bit hash is then used to construct a lead 'HD node' that contains an extened public private *child* key.

This way they keypair is specific to the server/hostname/port and protocol combination used. And it is this private key that is used to sign the nonce passed by the SSH server (as opposed to the master key).

The `trezor-agent` then instructs SSH to connect to the server. It will then engage in the normal challenge response process, ask the hardware wallet to blindly sign any nonce flashed by the server with the derived child private key and return this to the server. It then hands over to normal SSH for the rest of the logged in session.

### GPG

GPG uses much the same approach as SSH, expect in this 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.

This feature is currently not used -- it is set to '0'. This may change in the future.

[1]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
[2]: https://github.com/satoshilabs/slips/blob/master/slip-0013.md
[3]: https://github.com/satoshilabs/slips/blob/master/slip-0017.md
32 changes: 32 additions & 0 deletions doc/README-GPG.md
Original file line number Diff line number Diff line change
@@ -112,6 +112,38 @@ retrieve the timestamp with the following command (substitute "john@doe.bit" for
$ gpg2 --export 'john@doe.bit' | gpg2 --list-packets | grep created | head -n1
```

## Adding new user IDs

After your main identity is created, you can add new user IDs using the regular GnuPG commands:
```
$ trezor-gpg init "Foobar" -vv
$ export GNUPGHOME=${HOME}/.gnupg/trezor
$ gpg2 -K
------------------------------------------
sec nistp256/6275E7DA 2017-12-05 [SC]
uid [ultimate] Foobar
ssb nistp256/35F58F26 2017-12-05 [E]
$ gpg2 --edit Foobar
gpg> adduid
Real name: Xyzzy
Email address:
Comment:
You selected this USER-ID:
"Xyzzy"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
gpg> save
$ gpg2 -K
------------------------------------------
sec nistp256/6275E7DA 2017-12-05 [SC]
uid [ultimate] Xyzzy
uid [ultimate] Foobar
ssb nistp256/35F58F26 2017-12-05 [E]
```

## GnuPG subkey generation
In order to add TREZOR-based subkey to an existing GnuPG identity, use the `--subkey` flag:
```
2 changes: 2 additions & 0 deletions libagent/gpg/decode.py
Original file line number Diff line number Diff line change
@@ -199,6 +199,7 @@ def _parse_pubkey(stream, packet_type='pubkey'):
log.debug('key ID: %s', util.hexlify(p['key_id']))
return p


_parse_subkey = functools.partial(_parse_pubkey, packet_type='subkey')


@@ -208,6 +209,7 @@ def _parse_user_id(stream, packet_type='user_id'):
to_hash = b'\xb4' + util.prefix_len('>L', value)
return {'type': packet_type, 'value': value, '_to_hash': to_hash}


# User attribute is handled as an opaque user ID
_parse_attribute = functools.partial(_parse_user_id,
packet_type='user_attribute')
1 change: 1 addition & 0 deletions libagent/gpg/keyring.py
Original file line number Diff line number Diff line change
@@ -113,6 +113,7 @@ def _parse_ecdsa_sig(args):
return (util.bytes2num(sig_r),
util.bytes2num(sig_s))


# DSA and EDDSA happen to have the same structure as ECDSA signatures
_parse_dsa_sig = _parse_ecdsa_sig
_parse_eddsa_sig = _parse_ecdsa_sig
14 changes: 14 additions & 0 deletions libagent/gpg/protocol.py
Original file line number Diff line number Diff line change
@@ -101,6 +101,7 @@ def _compute_keygrip(params):
exp = '{}:{}{}:'.format(len(name), name, len(value))
parts.append(b'(' + exp.encode('ascii') + value + b')')

log.debug('keygrip parts: %s', parts)
return hashlib.sha1(b''.join(parts)).digest()


@@ -124,6 +125,19 @@ def keygrip_nist256(vk):

def keygrip_nist521(vk):
"""Compute keygrip for NIST521 curve public keys."""
curve = vk.curve.curve
gen = vk.curve.generator
g = (4 << 1200) | (gen.x() << 600) | gen.y()
point = vk.pubkey.point
q = (4 << 1200) | (point.x() << 600) | point.y()
return _compute_keygrip([
['p', util.num2bytes(curve.p(), size=66)],
['a', util.num2bytes(curve.a() % curve.p(), size=66)],
['b', util.num2bytes(curve.b() % curve.p(), size=66)],
['g', util.num2bytes(g, size=160)],
['n', util.num2bytes(vk.curve.order, size=66)],
['q', util.num2bytes(q, size=160)],
])


def keygrip_ed25519(vk):
4 changes: 2 additions & 2 deletions libagent/ssh/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""SSH-agent implementation using hardware authentication devices."""
import argparse
import contextlib
import functools
import io
@@ -11,6 +10,7 @@
import tempfile
import threading

import configargparse

from .. import device, formats, server, util
from . import client, protocol
@@ -57,7 +57,7 @@ def _to_unicode(s):

def create_agent_parser():
"""Create an ArgumentParser for this tool."""
p = argparse.ArgumentParser()
p = configargparse.ArgParser(default_config_files=['~/.ssh/agent.config'])
p.add_argument('-v', '--verbose', default=0, action='count')

curve_names = [name for name in formats.SUPPORTED_CURVES]
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
],
install_requires=[
'backports.shutil_which>=3.5.1',
'ConfigArgParse>=0.12.0',
'ecdsa>=0.13',
'ed25519>=1.4',
'pymsgbox>=1.0.6',
6 changes: 3 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
[tox]
envlist = py27,py3
[pep8]
[pycodestyle]
max-line-length = 100
[pep257]
add-ignore = D401
[testenv]
deps=
pytest
mock
pep8
pycodestyle
coverage
pylint
semver
pydocstyle
isort
commands=
pep8 libagent
pycodestyle libagent
# isort --skip-glob .tox -c -r libagent
pylint --reports=no --rcfile .pylintrc libagent
pydocstyle libagent