Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the user to configure a pinentry #202

Closed
wants to merge 14 commits into from
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ Currently [TREZOR](https://trezor.io/), [Keepkey](https://www.keepkey.com/), and
Note: If you're using Windows, see [trezor-ssh-agent](https://github.com/martin-lizner/trezor-ssh-agent) by Martin Lízner.

* **GPG** instructions and common use cases are [here](doc/README-GPG.md)
* Instructions to configure a Trezor-style **PIN entry** program are [here](doc/README-PINENTRY.md)
6 changes: 5 additions & 1 deletion doc/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ gpg (GnuPG) 2.1.15
$ pip install --user -e trezor-agent/agents/trezor
```

Read [these instructions](https://github.com/romanz/python-trezor#pin-entering) on how to enter your PIN with the PIN entry.
Read [these instructions](https://github.com/romanz/python-trezor#pin-entering) on how to enter your PIN with the default PIN entry.

If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md).

# 3. Install the KeepKey agent

Expand All @@ -87,6 +89,8 @@ Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_ag
$ pip install --user -e trezor-agent/agents/keepkey
```

Read [these instructions](https://github.com/romanz/python-trezor#pin-entering) on how to enter your PIN with the default PIN entry.

# 4. Install the Ledger Nano S agent

1. Make sure you are running the latest firmware version on your Ledger Nano S:
Expand Down
2 changes: 2 additions & 0 deletions doc/README-GPG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Thanks!

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)` 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).
Expand Down
58 changes: 58 additions & 0 deletions doc/README-PINENTRY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Custom PIN entry

By default a standard GPG PIN entry program is used when entering your Trezor PIN, but it's difficult to use if you don't have a numeric keypad or want to use your mouse.

You can specify a custom PIN entry program (and separately, a passphrase entry program) such as [trezor-gpg-pinentry-tk](https://github.com/rendaw/trezor-gpg-pinentry-tk) to match your workflow.

##### 1. Install the PIN entry

Run

```
pip install trezor-gpg-pinentry-tk
```

##### 2. SSH

Add the flag `--pinentry trezor-gpg-pinentry-tk` to all calls to `trezor-agent`.

To automatically use this flag, add the line `pinentry=trezor-gpg-pinentry-tk` to `~/.ssh/agent.config`. **Note** this is currently broken due to [this dependency issue](https://github.com/bw2/ConfigArgParse/issues/114).

If you specify the flag in a systemd `.service` file you may need to use the absolute path to `trezor-gpg-pinentry-tk`. You may also need to add this line:

```
Environment="DISPLAY=:0"
```

to the `[Service]` section to tell the PIN entry program how to connect to the X11 server.

##### 3. GPG

If you haven't completed initialization yet, run:

```
$ (trezor|keepkey|ledger)-gpg init --pinentry trezor-gpg-pinentry-tk "Roman Zeyde <roman.zeyde@gmail.com>"
```

to configure the PIN entry at the same time.

Otherwise, open `$GNUPGHOME/trezor/gpg-agent.conf` and add this line:

```
pinentry-program trezor-gpg-pinentry-tk
```

Kill the agent (processes `run-agent.sh` and `trezor-gpg-agent`).

##### 4. Troubleshooting

Add:

```
log-file /home/yourname/.gnupg/trezor/gpg-agent.log
verbosity 2
```

to `$GNUPGHOME/trezor/gpg-agent.conf` and restart the agent.

Any problems running the PIN entry program should appear in the log file.
2 changes: 2 additions & 0 deletions doc/README-SSH.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ SSH requires no configuration, but you may put common command line options in `~

See `(trezor|keepkey|ledger)-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)-agent` program to work with SSH. It has three main modes of operation:
Expand Down
6 changes: 3 additions & 3 deletions libagent/device/fake_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ def close(self):
"""Close connection."""
self.conn = None

def pubkey(self, identity, ecdh=False):
def pubkey(self, identity, ecdh=False, options=None):
"""Return public key."""
_verify_support(identity)
data = self.vk.to_string()
x, y = data[:32], data[32:]
prefix = bytearray([2 + (bytearray(y)[0] & 1)])
return bytes(prefix) + x

def sign(self, identity, blob):
def sign(self, identity, blob, options=None):
"""Sign given blob and return the signature (as bytes)."""
if identity.identity_dict['proto'] in {'ssh'}:
digest = hashlib.sha256(blob).digest()
Expand All @@ -60,7 +60,7 @@ def sign(self, identity, blob):
return self.sk.sign_digest_deterministic(digest=digest,
hashfunc=hashlib.sha256)

def ecdh(self, identity, pubkey):
def ecdh(self, identity, pubkey, options=None):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
assert pubkey[:1] == b'\x04'
peer = ecdsa.VerifyingKey.from_string(
Expand Down
8 changes: 4 additions & 4 deletions libagent/device/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def get_curve_name(self, ecdh=False):
class Device(object):
"""Abstract cryptographic hardware device interface."""

def __init__(self):
def __init__(self, config=None): # pylint: disable=unused-argument
"""C-tor."""
self.conn = None

Expand All @@ -126,15 +126,15 @@ def __exit__(self, *args):
log.exception('close failed: %s', e)
self.conn = None

def pubkey(self, identity, ecdh=False):
def pubkey(self, identity, ecdh=False, options=None):
"""Get public key (as bytes)."""
raise NotImplementedError()

def sign(self, identity, blob):
def sign(self, identity, blob, options=None):
"""Sign given blob and return the signature (as bytes)."""
raise NotImplementedError()

def ecdh(self, identity, pubkey):
def ecdh(self, identity, pubkey, options=None):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
raise NotImplementedError()

Expand Down
7 changes: 4 additions & 3 deletions libagent/device/keepkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ def _defs(self):

required_version = '>=1.0.4'

def pubkey(self, identity, ecdh=False):
def pubkey(self, identity, ecdh=False, options=None):
"""Return public key."""
_verify_support(identity, ecdh)
return trezor.Trezor.pubkey(self, identity=identity, ecdh=ecdh)
return trezor.Trezor.pubkey(self, identity=identity, ecdh=ecdh,
options=options)

def ecdh(self, identity, pubkey):
def ecdh(self, identity, pubkey, options=None):
"""No support for ECDH in KeepKey firmware."""
_verify_support(identity, ecdh=True)
6 changes: 3 additions & 3 deletions libagent/device/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def connect(self):
raise interface.NotFoundError(
'{} not connected: "{}"'.format(self, e))

def pubkey(self, identity, ecdh=False):
def pubkey(self, identity, ecdh=False, options=None):
"""Get PublicKey object for specified BIP32 address and elliptic curve."""
curve_name = identity.get_curve_name(ecdh)
path = _expand_path(identity.get_bip32_address(ecdh))
Expand All @@ -66,7 +66,7 @@ def pubkey(self, identity, ecdh=False):
log.debug('result: %r', result)
return _convert_public_key(curve_name, result[1:])

def sign(self, identity, blob):
def sign(self, identity, blob, options=None):
"""Sign given blob and return the signature (as bytes)."""
path = _expand_path(identity.get_bip32_address(ecdh=False))
if identity.identity_dict['proto'] == 'ssh':
Expand Down Expand Up @@ -103,7 +103,7 @@ def sign(self, identity, blob):
else:
return bytes(result[:64])

def ecdh(self, identity, pubkey):
def ecdh(self, identity, pubkey, options=None):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
path = _expand_path(identity.get_bip32_address(ecdh=True))
if identity.curve_name == 'nist256p1':
Expand Down
135 changes: 110 additions & 25 deletions libagent/device/trezor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,62 @@ def _is_open_tty(stream):
return not stream.closed and os.isatty(stream.fileno())


def _pin_communicate(program, message, error=None, options=None):
args = [program]
options = options or {}
if 'DISPLAY' in os.environ:
args.extend(['--display', os.environ['DISPLAY']])
try:
entry = subprocess.Popen(
args,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
)
except OSError as e:
if e.errno == os.errno.ENOENT:
log.debug('Couldn\'t find pin/pass entry program %s', program)
return None
else:
raise

def expect(prefix=None):
line = entry.stdout.readline().decode('utf-8')
if line.endswith('\n'):
line = line[:-1]
log.debug('PINENTRY <- %s', line)
if line.startswith('ERR '):
raise RuntimeError(line)
if prefix and not line.startswith(prefix):
raise RuntimeError('Received unexpected response from pinentry')
return line[len(prefix) if prefix else 0:]

def send(line):
log.debug('PINENTRY -> %s', line)
entry.stdin.write('{}\n'.format(line).encode('utf-8'))
entry.stdin.flush()

expect('OK')
for k, v in options.items():
if v is not None:
send('OPTION {}={}'.format(k, v))
else:
send('OPTION {}'.format(k))
expect('OK')
send('SETDESC {}'.format(' '.join(message.splitlines())))
expect('OK')
if error:
send('SETERROR {}'.format(error))
expect('OK')
send('GETPIN')
result = expect('D ')
send('BYE')
entry.stdin.close()
try:
entry.communicate()
except Exception: # pylint: disable=broad-except
pass
return result


class Trezor(interface.Device):
"""Connection to TREZOR device."""

Expand All @@ -38,6 +94,12 @@ def package_name(cls):
"""Python package name (at PyPI)."""
return 'trezor-agent'

def __init__(self, config=None):
"""Set up Trezor device object."""
self.config = config or {}
self.options = {}
super(Trezor, self).__init__(config=config)

@property
def _defs(self):
from . import trezor_defs
Expand All @@ -56,20 +118,32 @@ def _override_pin_handler(self, conn):

def new_handler(msg):
try:
if _is_open_tty(sys.stdin):
result = cli_handler(msg) # CLI-based PIN handler
else:
scrambled_pin = _message_box(
'Use the numeric keypad to describe number positions.\n'
'The layout is:\n'
' 7 8 9\n'
' 4 5 6\n'
' 1 2 3\n'
'Please enter PIN:')
fallback_message = (
'Use the numeric keypad to describe number positions.\n'
'The layout is:\n'
' 7 8 9\n'
' 4 5 6\n'
' 1 2 3\n'
'Please enter PIN:')
result = None
pinentry_program = self.config.get('pinentry-program')
scrambled_pin = _pin_communicate(
pinentry_program or 'pinentry',
'Please enter your Trezor PIN' if pinentry_program
else fallback_message,
options=self.options,
)
if not scrambled_pin:
if _is_open_tty(sys.stdin):
result = cli_handler(msg) # CLI-based PIN handler
else:
scrambled_pin = _message_box(fallback_message)
if not result:
if not set(scrambled_pin).issubset('123456789'):
raise self._defs.PinException(
None,
'Invalid scrambled PIN: {!r}'.format(scrambled_pin))
result = self._defs.PinMatrixAck(pin=scrambled_pin)
if not set(result.pin).issubset('123456789'):
raise self._defs.PinException(
None, 'Invalid scrambled PIN: {!r}'.format(result.pin))
return result
except: # noqa
conn.init_device()
Expand All @@ -88,14 +162,23 @@ def new_handler(msg):
log.debug('re-using cached %s passphrase', self)
return self.__class__.cached_passphrase_ack

if _is_open_tty(sys.stdin):
# use CLI-based PIN handler
ack = cli_handler(msg)
else:
passphrase = _message_box('Please enter passphrase:')
passphrase = mnemonic.Mnemonic.normalize_string(passphrase)
ack = self._defs.PassphraseAck(passphrase=passphrase)
ack = None
passphrase_program = self.config.get('passentry-program')
passphrase = _pin_communicate(
passphrase_program or 'pinentry',
'Please enter your passphrase',
options=self.options,
)
if not passphrase:
if _is_open_tty(sys.stdin):
# use CLI-based PIN handler
ack = cli_handler(msg)
else:
passphrase = _message_box('Please enter passphrase:')
passphrase = mnemonic.Mnemonic.normalize_string(passphrase)

if not ack:
ack = self._defs.PassphraseAck(passphrase=passphrase)
self.__class__.cached_passphrase_ack = ack
return ack
except: # noqa
Expand Down Expand Up @@ -136,19 +219,19 @@ def connect(self):
except (self._defs.PinException, ValueError) as e:
log.error('Invalid PIN: %s, retrying...', e)
continue
except Exception as e:
except Exception as e: # pylint: disable=broad-except
log.exception('ping failed: %s', e)
connection.close() # so the next HID open() will succeed
raise

raise interface.NotFoundError('{} not connected'.format(self))

def close(self):
"""Close connection."""
self.conn.close()

def pubkey(self, identity, ecdh=False):
def pubkey(self, identity, ecdh=False, options=None):
"""Return public key."""
self.options = options
curve_name = identity.get_curve_name(ecdh=ecdh)
log.debug('"%s" getting public key (%s) from %s',
identity.to_string(), curve_name, self)
Expand All @@ -164,8 +247,9 @@ def _identity_proto(self, identity):
setattr(result, name, value)
return result

def sign(self, identity, blob):
def sign(self, identity, blob, options=None):
"""Sign given blob and return the signature (as bytes)."""
self.options = options
curve_name = identity.get_curve_name(ecdh=False)
log.debug('"%s" signing %r (%s) on %s',
identity.to_string(), blob, curve_name, self)
Expand All @@ -184,8 +268,9 @@ def sign(self, identity, blob):
log.debug(msg, exc_info=True)
raise interface.DeviceError(msg)

def ecdh(self, identity, pubkey):
def ecdh(self, identity, pubkey, options=None):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
self.options = options
curve_name = identity.get_curve_name(ecdh=True)
log.debug('"%s" shared session key (%s) for %r from %s',
identity.to_string(), curve_name, pubkey, self)
Expand Down
Loading