Compare commits

...

31 Commits

Author SHA1 Message Date
Roman Zeyde
dfde6dbee4 bump version 2016-10-12 19:12:04 +03:00
Cédric Félizard
085a3e81c7 Add explanation about entering PIN (#47)
[ci skip]
2016-10-11 21:31:25 +03:00
Cédric Félizard
3082d61deb Fix typo (#48) 2016-10-11 21:29:54 +03:00
Roman Zeyde
e3286a4510 gpg: don't clear the session after PIN is entered
This would allow single PIN entry when running multiple GPG commands.
2016-10-11 08:43:39 +03:00
Roman Zeyde
fcd5671626 Handle keyinfo request (#44)
gpg: handle KEYINFO request

See https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=agent/command.c;h=9522f898997e95207d59122d056f0f0be03ccecb;hb=6bee88dd067e03e7767ceacf6a849d9ba38cc11d#l1027 for more details.
2016-10-04 23:11:12 +03:00
Roman Zeyde
1454d2f4d7 Merge pull request #43 from romanz/fix-gpg-manual
GPG: fix installation instructions
2016-10-04 20:01:44 +03:00
Roman Zeyde
9b395363a3 GPG: fix installation instructions 2016-10-04 19:59:08 +03:00
Roman Zeyde
5bb9dd7770 Merge pull request #42 from romanz/ssh-git
README: add an example for remote git repository
2016-10-04 11:43:55 +03:00
Roman Zeyde
51df023a23 README: add an example for remote git repository 2016-10-04 11:40:14 +03:00
Roman Zeyde
d74f375637 Merge pull request #41 from romanz/protobuf-note
README: add note about protobuf permissions issue
2016-10-04 11:27:14 +03:00
Roman Zeyde
1fd0659051 README: add note about protobuf permissions issue 2016-10-04 11:24:39 +03:00
Roman Zeyde
18be290bd6 Merge branch 'fix_agent' of https://github.com/Solution4Future/trezor-agent into Solution4Future-fix_agent 2016-10-04 11:16:56 +03:00
Roman Zeyde
a1ab496bf4 Merge branch 'ledger' 2016-10-04 10:39:08 +03:00
Roman Zeyde
784e14647a Merge branch 'master' into HEAD
Conflicts:
	trezor_agent/factory.py
2016-10-04 10:37:52 +03:00
Dominik Kozaczko
7d2c649e83 don't stop polling for more devices as having more than one inserted raises more problems and we need to keep the check 2016-10-01 12:38:16 +02:00
Dominik Kozaczko
cf27b345f6 better handling of keepkey dependency; fixes #36 2016-10-01 12:30:00 +02:00
Dominik Kozaczko
386ed5a81f Merge pull request #1 from romanz/master
pull changes from upstream
2016-10-01 12:10:18 +02:00
Roman Zeyde
5a64954324 Merge pull request #37 from Solution4Future/fix_agent
removed .decode('ascii') and added missing bytestrings and also some missing deps
2016-10-01 11:42:47 +03:00
Dominik Kozaczko
3aebd137b0 removed .decode('ascii') and added missing bytestrings 2016-10-01 10:02:46 +02:00
Nicolas Pouillard
1fa35e7f1a Fix the URL for the TREZOR firmware 2016-09-30 21:07:35 +03:00
Roman Zeyde
459b882b89 ledger: don't use debug=True 2016-09-14 23:07:27 +03:00
Roman Zeyde
57e09248db Merge pull request #31 from romanz/master
Update ledger branch with the latest changes from master branch
2016-09-05 22:28:07 +03:00
Roman Zeyde
030ae4c3f6 gpg: include unsupport hash algorithm ID in exception message 2016-08-13 10:06:52 +03:00
Roman Zeyde
4897b70888 factory: fix pylint import-error warnings 2016-08-11 22:38:12 +03:00
Roman Zeyde
f4ecd47ed6 factory: fix pep8 and pylint warnings 2016-08-11 22:31:24 +03:00
Roman Zeyde
c4bbac0e77 util: move BIP32 address related functions 2016-08-11 22:30:59 +03:00
BTChip
5d0b0f65d3 Merge branch 'ledger' of https://github.com/btchip/trezor-agent into ledger 2016-08-09 13:02:47 +02:00
BTChip
33747592ca Fix eddsa, SSH optimization with signature + key, cleanup 2016-08-09 13:01:57 +02:00
BTChip
adb09cd8ca Ledger integration 2016-08-09 13:01:57 +02:00
BTChip
bc1d7a5448 Fix eddsa, SSH optimization with signature + key, cleanup 2016-08-03 15:17:04 +02:00
BTChip
8fe16d24c2 Ledger integration 2016-07-31 10:00:46 +02:00
14 changed files with 278 additions and 77 deletions

View File

@@ -11,9 +11,9 @@ $ gpg2 --version | head -n1
gpg (GnuPG) 2.1.11
```
Update you TREZOR firmware to the latest version (at least [c720614](https://github.com/trezor/trezor-mcu/commit/c720614f6e9b9c07f446c95bda0257980d942871)).
Update you TREZOR firmware to the latest version (at least v1.4.0).
Install latest `trezor-agent` package from [gpg-agent](https://github.com/romanz/trezor-agent/commits/gpg-agent) branch:
Install latest `trezor-agent` package from GitHub:
```
$ pip install --user git+https://github.com/romanz/trezor-agent.git
```
@@ -101,4 +101,4 @@ $ git commit --gpg-sign # create GPG-signed commit
$ git log --show-signature -1 # verify commit signature
$ git tag --sign "TAG" # create GPG-signed tag
$ git verify-tag "TAG" # verify tag signature
```
```

View File

@@ -43,3 +43,17 @@ Run:
~ $
Make sure to confirm SSH signature on the Trezor device when requested.
## Accessing remote Git repositories
Use your SSH public key to access your remote repository (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 | xclip
Use the following Bash alias for convinient Git operations:
$ alias git_hub='trezor-agent -v -e ed25519 git@github.com -- git'
Replace `git` with `git_hub` for remote operations:
$ git_hub push origin master

View File

@@ -24,7 +24,7 @@ Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agen
$ pip install trezor_agent
Finally, verify that you are running the latest [TREZOR firmware](https://mytrezor.com/data/firmware/releases.json) version (at least v1.4.0):
Finally, verify that you are running the latest [TREZOR firmware](https://wallet.mytrezor.com/data/firmware/releases.json) version (at least v1.4.0):
$ trezorctl get_features | head
vendor: "bitcointrezor.com"
@@ -33,15 +33,39 @@ Finally, verify that you are running the latest [TREZOR firmware](https://mytrez
patch_version: 0
...
If you have an error regarding `protobuf` imports (after installing it), please see [this issue](https://github.com/romanz/trezor-agent/issues/28).
## Usage
For SSH, see the [following instructions](README-SSH.md).
For GPG, see the [following instructions](README-GPG.md).
Questions, suggestions and discussions are welcome: [![Chat](https://badges.gitter.im/romanz/trezor-agent.svg)](https://gitter.im/romanz/trezor-agent)
### Entering PIN
Look at the digits shown on the TREZOR display and enter their positions using this regular numeric keyboard mapping:
```
|7|8|9|
|4|5|6|
|1|2|3|
```
For example, if your PIN is `1234` and your TREZOR is displaying the following:
```
|3|1|2|
|7|5|8|
|6|4|9|
```
You have to enter `8972`.
## 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.
### Gitter
Questions, suggestions and discussions are welcome: [![Chat](https://badges.gitter.im/romanz/trezor-agent.svg)](https://gitter.im/romanz/trezor-agent)

View File

@@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='trezor_agent',
version='0.7.0',
version='0.7.1',
description='Using Trezor as hardware SSH agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
@@ -27,6 +27,10 @@ setup(
'Topic :: Security',
'Topic :: Utilities',
],
extras_require={
'trezorlib': ['python-trezor>=0.7.4'],
'keepkeylib': ['keepkey>=0.7.3'],
},
entry_points={'console_scripts': [
'trezor-agent = trezor_agent.__main__:run_agent',
'trezor-git = trezor_agent.__main__:run_git',

View File

@@ -1,5 +1,5 @@
[tox]
envlist = py27,py34
envlist = py27,py3
[pep8]
max-line-length = 100
[testenv]

View File

@@ -7,14 +7,14 @@ import re
import subprocess
import sys
from . import client, formats, protocol, server
from . import client, formats, protocol, server, util
log = logging.getLogger(__name__)
def ssh_args(label):
"""Create SSH command for connecting specified server."""
identity = client.string_to_identity(label, identity_type=dict)
identity = util.string_to_identity(label, identity_type=dict)
args = []
if 'port' in identity:

View File

@@ -6,8 +6,6 @@ It is used for getting SSH public keys and ECDSA signing of server requests.
import binascii
import io
import logging
import re
import struct
from . import factory, formats, util
@@ -39,7 +37,7 @@ class Client(object):
def get_identity(self, label, index=0):
"""Parse label string into Identity protobuf."""
identity = string_to_identity(label, self.identity_type)
identity = util.string_to_identity(label, self.identity_type)
identity.proto = 'ssh'
identity.index = index
return identity
@@ -47,10 +45,10 @@ class Client(object):
def get_public_key(self, label):
"""Get SSH public key corresponding to specified by label."""
identity = self.get_identity(label=label)
label = identity_to_string(identity) # canonize key label
label = util.identity_to_string(identity) # canonize key label
log.info('getting "%s" public key (%s) from %s...',
label, self.curve, self.device_name)
addr = get_address(identity)
addr = util.get_bip32_address(identity)
node = self.client.get_public_node(n=addr,
ecdsa_curve_name=self.curve)
@@ -92,55 +90,6 @@ class Client(object):
return result.signature[1:]
_identity_regexp = re.compile(''.join([
'^'
r'(?:(?P<proto>.*)://)?',
r'(?:(?P<user>.*)@)?',
r'(?P<host>.*?)',
r'(?::(?P<port>\w*))?',
r'(?P<path>/.*)?',
'$'
]))
def string_to_identity(s, identity_type):
"""Parse string into Identity protobuf."""
m = _identity_regexp.match(s)
result = m.groupdict()
log.debug('parsed identity: %s', result)
kwargs = {k: v for k, v in result.items() if v}
return identity_type(**kwargs)
def identity_to_string(identity):
"""Dump Identity protobuf into its string representation."""
result = []
if identity.proto:
result.append(identity.proto + '://')
if identity.user:
result.append(identity.user + '@')
result.append(identity.host)
if identity.port:
result.append(':' + identity.port)
if identity.path:
result.append(identity.path)
return ''.join(result)
def get_address(identity, ecdh=False):
"""Compute BIP32 derivation address according to SLIP-0013/0017."""
index = struct.pack('<L', identity.index)
addr = index + identity_to_string(identity).encode('ascii')
log.debug('address string: %r', addr)
digest = formats.hashfunc(addr).digest()
s = io.BytesIO(bytearray(digest))
hardened = 0x80000000
addr_0 = [13, 17][bool(ecdh)]
address_n = [addr_0] + list(util.recv(s, '<LLLL'))
return [(hardened | value) for value in address_n]
def _parse_ssh_blob(data):
res = {}
i = io.BytesIO(data)

View File

@@ -1,10 +1,13 @@
"""Thin wrapper around trezor/keepkey libraries."""
from __future__ import absolute_import
import binascii
import collections
import logging
import semver
from . import util
log = logging.getLogger(__name__)
ClientWrapper = collections.namedtuple(
@@ -77,9 +80,156 @@ def _load_keepkey():
log.exception('Missing module: install via "pip install keepkey"')
def _load_ledger():
import struct
class LedgerClientConnection(object):
def __init__(self, dongle):
self.dongle = dongle
@staticmethod
def expand_path(path):
result = ""
for pathElement in path:
result = result + struct.pack(">I", pathElement)
return result
@staticmethod
def convert_public_key(ecdsa_curve_name, result):
from trezorlib.messages_pb2 import PublicKey # pylint: disable=import-error
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 = chr(0) + str(keyY)
publicKey = PublicKey()
publicKey.node.public_key = str(result)
return publicKey
# pylint: disable=unused-argument
def get_public_node(self, n, ecdsa_curve_name="secp256k1", show_display=False):
donglePath = LedgerClientConnection.expand_path(n)
if ecdsa_curve_name == "nist256p1":
p2 = "01"
else:
p2 = "02"
apdu = "800200" + p2
apdu = apdu.decode('hex')
apdu += chr(len(donglePath) + 1) + chr(len(donglePath) / 4)
apdu += donglePath
result = bytearray(self.dongle.exchange(bytes(apdu)))[1:]
return LedgerClientConnection.convert_public_key(ecdsa_curve_name, result)
# pylint: disable=too-many-locals
def sign_identity(self, identity, challenge_hidden, challenge_visual,
ecdsa_curve_name="secp256k1"):
from trezorlib.messages_pb2 import SignedIdentity # pylint: disable=import-error
n = util.get_bip32_address(identity)
donglePath = LedgerClientConnection.expand_path(n)
if identity.proto == 'ssh':
ins = "04"
p1 = "00"
else:
ins = "08"
p1 = "00"
if ecdsa_curve_name == "nist256p1":
p2 = "81" if identity.proto == 'ssh' else "01"
else:
p2 = "82" if identity.proto == 'ssh' else "02"
apdu = "80" + ins + p1 + p2
apdu = apdu.decode('hex')
apdu += chr(len(challenge_hidden) + len(donglePath) + 1)
apdu += chr(len(donglePath) / 4) + donglePath
apdu += challenge_hidden
result = bytearray(self.dongle.exchange(bytes(apdu)))
if ecdsa_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
signature = SignedIdentity()
signature.signature = chr(0) + str(r) + str(s)
if identity.proto == 'ssh':
keyData = result[offset:]
pk = LedgerClientConnection.convert_public_key(ecdsa_curve_name, keyData)
signature.public_key = pk.node.public_key
return signature
else:
signature = SignedIdentity()
signature.signature = chr(0) + str(result[0:64])
if identity.proto == 'ssh':
keyData = result[64:]
pk = LedgerClientConnection.convert_public_key(ecdsa_curve_name, keyData)
signature.public_key = pk.node.public_key
return signature
def get_ecdh_session_key(self, identity, peer_public_key, ecdsa_curve_name="secp256k1"):
from trezorlib.messages_pb2 import ECDHSessionKey # pylint: disable=import-error
n = util.get_bip32_address(identity, True)
donglePath = LedgerClientConnection.expand_path(n)
if ecdsa_curve_name == "nist256p1":
p2 = "01"
else:
p2 = "02"
apdu = "800a00" + p2
apdu = apdu.decode('hex')
apdu += chr(len(peer_public_key) + len(donglePath) + 1)
apdu += chr(len(donglePath) / 4) + donglePath
apdu += peer_public_key
result = bytearray(self.dongle.exchange(bytes(apdu)))
sessionKey = ECDHSessionKey()
sessionKey.session_key = str(result)
return sessionKey
def clear_session(self):
pass
def close(self):
self.dongle.close()
# pylint: disable=unused-argument
# pylint: disable=no-self-use
def ping(self, msg, button_protection=False, pin_protection=False,
passphrase_protection=False):
return msg
class CallException(Exception):
def __init__(self, code, message):
super(CallException, self).__init__()
self.args = [code, message]
try:
from ledgerblue.comm import getDongle
except ImportError:
log.exception('Missing module: install via "pip install ledgerblue"')
# pylint: disable=bare-except
try:
from trezorlib.types_pb2 import IdentityType # pylint: disable=import-error
dongle = getDongle()
except:
return
yield ClientWrapper(connection=LedgerClientConnection(dongle),
identity_type=IdentityType,
device_name="ledger",
call_exception=CallException)
LOADERS = [
_load_trezor,
_load_keepkey
_load_keepkey,
_load_ledger
]

View File

@@ -39,7 +39,7 @@ def sig_encode(r, s):
def pksign(keygrip, digest, algo):
"""Sign a message digest using a private EC key."""
assert algo == '8'
assert algo == '8', 'Unsupported hash algorithm ID {}'.format(algo)
user_id = os.environ['TREZOR_GPG_USER_ID']
pubkey_dict = decode.load_public_key(
pubkey_bytes=keyring.export_public_key(user_id=user_id),
@@ -127,6 +127,12 @@ def handle_connection(conn):
elif command == 'PKDECRYPT':
sec = pkdecrypt(keygrip, conn)
keyring.sendline(conn, b'D ' + sec)
elif command == 'KEYINFO':
keygrip, = args
# Dummy reply (mainly for 'gpg --edit' to succeed).
# For details, see GnuPG agent KEYINFO command help.
fmt = b'S KEYINFO {0} X - - - - - - -'
keyring.sendline(conn, fmt.format(keygrip))
elif command == 'BYE':
return
else:

View File

@@ -3,7 +3,7 @@ import logging
import time
from . import decode, keyring, protocol
from .. import client, factory, formats, util
from .. import factory, formats, util
log = logging.getLogger(__name__)
@@ -21,7 +21,7 @@ class HardwareSigner(object):
def pubkey(self, ecdh=False):
"""Return public key as VerifyingKey object."""
addr = client.get_address(identity=self.identity, ecdh=ecdh)
addr = util.get_bip32_address(identity=self.identity, ecdh=ecdh)
public_node = self.client_wrapper.connection.get_public_node(
n=addr, ecdsa_curve_name=self.curve_name)
@@ -52,7 +52,6 @@ class HardwareSigner(object):
def close(self):
"""Close the connection to the device."""
self.client_wrapper.connection.clear_session()
self.client_wrapper.connection.close()

View File

@@ -1,4 +1,5 @@
"""Tools for doing signature using gpg-agent."""
from __future__ import unicode_literals, absolute_import, print_function
import binascii
import io
@@ -15,9 +16,9 @@ log = logging.getLogger(__name__)
def get_agent_sock_path(sp=subprocess):
"""Parse gpgconf output to find out GPG agent UNIX socket path."""
lines = sp.check_output(['gpgconf', '--list-dirs']).strip().split('\n')
dirs = dict(line.split(':', 1) for line in lines)
return dirs['agent-socket']
lines = sp.check_output(['gpgconf', '--list-dirs']).strip().split(b'\n')
dirs = dict(line.split(b':', 1) for line in lines)
return dirs[b'agent-socket']
def connect_to_agent(sp=subprocess):
@@ -183,14 +184,14 @@ def gpg_command(args, env=None):
def get_keygrip(user_id, sp=subprocess):
"""Get a keygrip of the primary GPG key of the specified user."""
args = gpg_command(['--list-keys', '--with-keygrip', user_id])
output = sp.check_output(args).decode('ascii')
output = sp.check_output(args)
return re.findall(r'Keygrip = (\w+)', output)[0]
def gpg_version(sp=subprocess):
"""Get a keygrip of the primary GPG key of the specified user."""
args = gpg_command(['--version'])
output = sp.check_output(args).decode('ascii')
output = sp.check_output(args)
line = output.split(b'\n')[0] # b'gpg (GnuPG) 2.1.11'
return line.split(b' ')[-1] # b'2.1.11'

View File

@@ -122,7 +122,7 @@ class Handler(object):
SSH v2 public key authentication is performed.
If the required key is not supported, raise KeyError
If the signature is invalid, rause ValueError
If the signature is invalid, raise ValueError
"""
key = formats.parse_pubkey(util.read_frame(buf))
log.debug('looking for %s', key['fingerprint'])

View File

@@ -87,8 +87,8 @@ def test_ssh_agent():
def ssh_sign_identity(identity, challenge_hidden,
challenge_visual, ecdsa_curve_name):
assert (client.identity_to_string(identity) ==
client.identity_to_string(ident))
assert (util.identity_to_string(identity) ==
util.identity_to_string(ident))
assert challenge_hidden == BLOB
assert challenge_visual == ''
assert ecdsa_curve_name == 'nist256p1'
@@ -133,4 +133,4 @@ def test_utils():
identity.path = '/path'
url = 'https://user@host:443/path'
assert client.identity_to_string(identity) == url
assert util.identity_to_string(identity) == url

View File

@@ -1,9 +1,14 @@
"""Various I/O and serialization utilities."""
import binascii
import contextlib
import hashlib
import io
import logging
import re
import struct
log = logging.getLogger(__name__)
def send(conn, data):
"""Send data blob to connection socket."""
@@ -173,3 +178,52 @@ class Reader(object):
yield
finally:
self._captured = None
_identity_regexp = re.compile(''.join([
'^'
r'(?:(?P<proto>.*)://)?',
r'(?:(?P<user>.*)@)?',
r'(?P<host>.*?)',
r'(?::(?P<port>\w*))?',
r'(?P<path>/.*)?',
'$'
]))
def string_to_identity(s, identity_type):
"""Parse string into Identity protobuf."""
m = _identity_regexp.match(s)
result = m.groupdict()
log.debug('parsed identity: %s', result)
kwargs = {k: v for k, v in result.items() if v}
return identity_type(**kwargs)
def identity_to_string(identity):
"""Dump Identity protobuf into its string representation."""
result = []
if identity.proto:
result.append(identity.proto + '://')
if identity.user:
result.append(identity.user + '@')
result.append(identity.host)
if identity.port:
result.append(':' + identity.port)
if identity.path:
result.append(identity.path)
return ''.join(result)
def get_bip32_address(identity, ecdh=False):
"""Compute BIP32 derivation address according to SLIP-0013/0017."""
index = struct.pack('<L', identity.index)
addr = index + identity_to_string(identity).encode('ascii')
log.debug('address string: %r', addr)
digest = hashlib.sha256(addr).digest()
s = io.BytesIO(bytearray(digest))
hardened = 0x80000000
addr_0 = [13, 17][bool(ecdh)]
address_n = [addr_0] + list(recv(s, '<LLLL'))
return [(hardened | value) for value in address_n]