mirror of
https://github.com/romanz/amodem.git
synced 2026-05-03 08:27:26 +08:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4108c9287f | ||
|
|
d9cb75e95d | ||
|
|
2cecd2ed08 | ||
|
|
05f40085b2 | ||
|
|
c7346d621d | ||
|
|
0342b39465 | ||
|
|
fa6d8564b9 | ||
|
|
e09712c793 | ||
|
|
0cbb3bb9fa | ||
|
|
d7a6641ffa | ||
|
|
6fe89241c4 | ||
|
|
c5262d075b | ||
|
|
683d24f4eb | ||
|
|
921e2954c1 | ||
|
|
3f784289d8 | ||
|
|
04d790767d | ||
|
|
97efdf4a45 | ||
|
|
ee2f6b75dc | ||
|
|
a26f0ea034 | ||
|
|
a68f1e5c26 | ||
|
|
93e3c66a15 | ||
|
|
44eaaa6b9c | ||
|
|
b83d4960e7 | ||
|
|
75fe7b4e05 | ||
|
|
742136b22d | ||
|
|
513e99dd57 | ||
|
|
1bd6775c35 | ||
|
|
aaade1737f | ||
|
|
fe185c190e | ||
|
|
1bc0165368 | ||
|
|
0f841ffbc4 | ||
|
|
b2942035a3 | ||
|
|
215b64f253 | ||
|
|
79e68b29c2 | ||
|
|
8265515641 | ||
|
|
749799845d | ||
|
|
eaea35003e | ||
|
|
eefb38ce83 | ||
|
|
0730eb7223 | ||
|
|
5b61702205 | ||
|
|
0ad0ca3b9a | ||
|
|
2843cdcf41 | ||
|
|
c7bc78ebe7 | ||
|
|
a6d9edcb0b | ||
|
|
bc64205a85 | ||
|
|
34dc803856 | ||
|
|
f7ebb02799 | ||
|
|
0ba33a5bc4 | ||
|
|
13752ddcd5 | ||
|
|
487a8e56c4 | ||
|
|
ef56ee4602 | ||
|
|
ae381a38e5 | ||
|
|
446ec99bf4 | ||
|
|
80c6f10533 | ||
|
|
ff984c60e4 | ||
|
|
c9bc079dc9 | ||
|
|
65d2c04478 | ||
|
|
2d57bf4453 | ||
|
|
79b6d31dfe | ||
|
|
7de88a3980 |
@@ -1,2 +1,2 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=fixme, invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking
|
||||
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking
|
||||
|
||||
@@ -8,7 +8,7 @@ First, verify that you have GPG 2.1+ [installed](https://gist.github.com/vt0r/a2
|
||||
|
||||
```
|
||||
$ gpg2 --version | head -n1
|
||||
gpg (GnuPG) 2.1.11
|
||||
gpg (GnuPG) 2.1.15
|
||||
```
|
||||
|
||||
Update you TREZOR firmware to the latest version (at least v1.4.0).
|
||||
@@ -18,87 +18,20 @@ Install latest `trezor-agent` package from GitHub:
|
||||
$ pip install --user git+https://github.com/romanz/trezor-agent.git
|
||||
```
|
||||
|
||||
Define your GPG user ID as an environment variable:
|
||||
```
|
||||
$ export TREZOR_GPG_USER_ID="John Doe <john@doe.bit>"
|
||||
```
|
||||
# Quickstart
|
||||
|
||||
There are two ways to generate TREZOR-based GPG public keys, as described below.
|
||||
## Identity creation
|
||||
[](https://asciinema.org/a/c2yodst21h9obttkn9wgf3783)
|
||||
|
||||
## 1. generate a new GPG identity:
|
||||
|
||||
```
|
||||
$ trezor-gpg create | gpg2 --import # use the TREZOR to confirm signing the primary key
|
||||
gpg: key 5E4D684D: public key "John Doe <john@doe.bit>" imported
|
||||
gpg: Total number processed: 1
|
||||
gpg: imported: 1
|
||||
|
||||
$ gpg2 --edit "${TREZOR_GPG_USER_ID}" trust # set this key to ultimate trust (option #5)
|
||||
|
||||
$ gpg2 -k
|
||||
/home/roman/.gnupg/pubring.kbx
|
||||
------------------------------
|
||||
pub nistp256/5E4D684D 2016-06-17 [SC]
|
||||
uid [ultimate] John Doe <john@doe.bit>
|
||||
sub nistp256/A31D9E25 2016-06-17 [E]
|
||||
```
|
||||
|
||||
## 2. generate a new subkey for an existing GPG identity:
|
||||
|
||||
```
|
||||
$ gpg2 -k # suppose there is already a GPG primary key
|
||||
/home/roman/.gnupg/pubring.kbx
|
||||
------------------------------
|
||||
pub rsa2048/87BB07B4 2016-06-17 [SC]
|
||||
uid [ultimate] John Doe <john@doe.bit>
|
||||
sub rsa2048/7176D31F 2016-06-17 [E]
|
||||
|
||||
$ trezor-gpg create --subkey | gpg2 --import # use the TREZOR to confirm signing the subkey
|
||||
gpg: key 87BB07B4: "John Doe <john@doe.bit>" 2 new signatures
|
||||
gpg: key 87BB07B4: "John Doe <john@doe.bit>" 2 new subkeys
|
||||
gpg: Total number processed: 1
|
||||
gpg: new subkeys: 2
|
||||
gpg: new signatures: 2
|
||||
|
||||
$ gpg2 -k
|
||||
/home/roman/.gnupg/pubring.kbx
|
||||
------------------------------
|
||||
pub rsa2048/87BB07B4 2016-06-17 [SC]
|
||||
uid [ultimate] John Doe <john@doe.bit>
|
||||
sub rsa2048/7176D31F 2016-06-17 [E]
|
||||
sub nistp256/DDE80B36 2016-06-17 [S]
|
||||
sub nistp256/E3D0BA19 2016-06-17 [E]
|
||||
```
|
||||
|
||||
# Usage examples:
|
||||
|
||||
## Start the TREZOR-based gpg-agent:
|
||||
```
|
||||
$ trezor-gpg agent &
|
||||
```
|
||||
Note: this agent intercepts all GPG requests, so make sure to close it (e.g. by using `killall trezor-gpg`),
|
||||
when you are done with the TREZOR-based GPG operations.
|
||||
|
||||
## Sign and verify GPG messages:
|
||||
```
|
||||
$ echo "Hello World!" | gpg2 --sign | gpg2 --verify
|
||||
gpg: Signature made Fri 17 Jun 2016 08:55:13 PM IDT using ECDSA key ID 5E4D684D
|
||||
gpg: Good signature from "Roman Zeyde <roman.zeyde@gmail.com>" [ultimate]
|
||||
```
|
||||
## Encrypt and decrypt GPG messages:
|
||||
```
|
||||
$ date | gpg2 --encrypt -r "${TREZOR_GPG_USER_ID}" | gpg2 --decrypt
|
||||
gpg: encrypted with 256-bit ECDH key, ID A31D9E25, created 2016-06-17
|
||||
"Roman Zeyde <roman.zeyde@gmail.com>"
|
||||
Fri Jun 17 20:55:31 IDT 2016
|
||||
```
|
||||
## Sample usage (signature and decryption)
|
||||
[](https://asciinema.org/a/7x0h9tyoyu5ar6jc8y9oih0ba)
|
||||
|
||||
## Git commit & tag signatures:
|
||||
Git can use GPG to sign and verify commits and tags (see [here](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work)):
|
||||
```
|
||||
$ git config --local gpg.program gpg2
|
||||
$ git config --local gpg.program $(which gpg2)
|
||||
$ 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
|
||||
$ git tag --sign "v1.2.3" # create GPG-signed tag
|
||||
$ git verify-tag "v1.2.3" # verify tag signature
|
||||
```
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
|
||||
Run:
|
||||
|
||||
/tmp $ trezor-agent ssh.hostname.com -v > hostname.pub
|
||||
2015-09-02 15:03:18,929 INFO getting "ssh://ssh.hostname.com" public key from Trezor...
|
||||
/tmp $ trezor-agent user@ssh.hostname.com -v > hostname.pub
|
||||
2015-09-02 15:03:18,929 INFO getting "ssh://user@ssh.hostname.com" public key from Trezor...
|
||||
2015-09-02 15:03:23,342 INFO disconnected from Trezor
|
||||
/tmp $ cat hostname.pub
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGSevcDwmT+QaZPUEWUUjTeZRBICChxMKuJ7dRpBSF8+qt+8S1GBK5Zj8Xicc8SHG/SE/EXKUL2UU3kcUzE7ADQ= ssh://ssh.hostname.com
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGSevcDwmT+QaZPUEWUUjTeZRBICChxMKuJ7dRpBSF8+qt+8S1GBK5Zj8Xicc8SHG/SE/EXKUL2UU3kcUzE7ADQ= ssh://user@ssh.hostname.com
|
||||
|
||||
Append `hostname.pub` contents to `~/.ssh/authorized_keys`
|
||||
Append `hostname.pub` contents to `/home/user/.ssh/authorized_keys`
|
||||
configuration file at `ssh.hostname.com`, so the remote server
|
||||
would allow you to login using the corresponding private key signature.
|
||||
|
||||
@@ -27,9 +27,9 @@ would allow you to login using the corresponding private key signature.
|
||||
|
||||
Run:
|
||||
|
||||
/tmp $ trezor-agent ssh.hostname.com -v -c
|
||||
2015-09-02 15:09:39,782 INFO getting "ssh://ssh.hostname.com" public key from Trezor...
|
||||
2015-09-02 15:09:44,430 INFO please confirm user "roman" login to "ssh://ssh.hostname.com" using Trezor...
|
||||
/tmp $ trezor-agent user@ssh.hostname.com -v -c
|
||||
2015-09-02 15:09:39,782 INFO getting "ssh://user@ssh.hostname.com" public key from Trezor...
|
||||
2015-09-02 15:09:44,430 INFO please confirm user "roman" login to "ssh://user@ssh.hostname.com" using Trezor...
|
||||
2015-09-02 15:09:46,152 INFO signature status: OK
|
||||
Linux lmde 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1+deb8u3 (2015-08-04) x86_64
|
||||
|
||||
@@ -57,3 +57,24 @@ Use the following Bash alias for convinient Git operations:
|
||||
Replace `git` with `git_hub` for remote operations:
|
||||
|
||||
$ git_hub push origin master
|
||||
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
If SSH connection fails to work, please open an [issue](https://github.com/romanz/trezor-agent/issues)
|
||||
with a verbose log attached (by running `trezor-agent -vv`) .
|
||||
|
||||
## Incompatible SSH options
|
||||
|
||||
Note that your local SSH configuration may ignore `trezor-agent`, if it has `IdentitiesOnly` option set to `yes`.
|
||||
|
||||
IdentitiesOnly
|
||||
Specifies that ssh(1) should only use the authentication identity files configured in
|
||||
the ssh_config files, even if ssh-agent(1) or a PKCS11Provider offers more identities.
|
||||
The argument to this keyword must be “yes” or “no”.
|
||||
This option is intended for situations where ssh-agent offers many different identities.
|
||||
The default is “no”.
|
||||
|
||||
If you are failing to connect, try running:
|
||||
|
||||
$ trezor-agent -vv user@host -- ssh -vv -oIdentitiesOnly=no user@host
|
||||
|
||||
22
README.md
22
README.md
@@ -17,8 +17,8 @@ First, make sure that the latest [trezorlib](https://pypi.python.org/pypi/trezor
|
||||
is installed correctly (at least v0.6.6):
|
||||
|
||||
$ apt-get install python-dev libusb-1.0-0-dev libudev-dev
|
||||
$ pip install Cython trezor
|
||||
$ pip install -U setuptools
|
||||
$ pip install Cython trezor
|
||||
|
||||
Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agent) package:
|
||||
|
||||
@@ -41,25 +41,7 @@ For SSH, see the [following instructions](README-SSH.md).
|
||||
|
||||
For GPG, see the [following instructions](README-GPG.md).
|
||||
|
||||
### 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`.
|
||||
See [here](https://github.com/romanz/python-trezor#pin-entering) for PIN entering instructions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
32
scripts/gpg-init
Executable file
32
scripts/gpg-init
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
USER_ID="${1}"
|
||||
HOMEDIR=~/.gnupg/trezor
|
||||
CURVE=${CURVE:="nist256p1"} # or "ed25519"
|
||||
TIMESTAMP=${TIMESTAMP:=`date +%s`} # key creation timestamp
|
||||
|
||||
# Prepare new GPG home directory for TREZOR-based identity
|
||||
rm -rf "${HOMEDIR}"
|
||||
mkdir -p "${HOMEDIR}"
|
||||
chmod 700 "${HOMEDIR}"
|
||||
|
||||
# Generate new GPG identity and import into GPG keyring
|
||||
trezor-gpg-create -v "${USER_ID}" -t "${TIMESTAMP}" -e "${CURVE}" > "${HOMEDIR}/pubkey.asc"
|
||||
gpg2 --homedir "${HOMEDIR}" --import < "${HOMEDIR}/pubkey.asc"
|
||||
rm -f "${HOMEDIR}/S.gpg-agent" # (otherwise, our agent won't be started automatically)
|
||||
|
||||
# Make new GPG identity with "ultimate" trust (via its fingerprint)
|
||||
FINGERPRINT=$(gpg2 --homedir "${HOMEDIR}" --list-public-keys --with-colons | sed -n -E 's/^fpr:::::::::([0-9A-F]+):$/\1/p' | head -n1)
|
||||
echo "${FINGERPRINT}:6" | gpg2 --homedir "${HOMEDIR}" --import-ownertrust
|
||||
|
||||
# Prepare GPG configuration file
|
||||
echo "# TREZOR-based GPG configuration
|
||||
agent-program $(which trezor-gpg-agent)
|
||||
personal-digest-preferences SHA512
|
||||
" | tee "${HOMEDIR}/gpg.conf"
|
||||
|
||||
echo "# TREZOR-based GPG agent emulator
|
||||
log-file ${HOMEDIR}/gpg-agent.log
|
||||
verbosity 2
|
||||
" | tee "${HOMEDIR}/gpg-agent.conf"
|
||||
7
scripts/gpg-shell
Executable file
7
scripts/gpg-shell
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
export GNUPGHOME=~/.gnupg/trezor
|
||||
gpg2 --list-public-keys
|
||||
|
||||
${SHELL}
|
||||
7
setup.py
7
setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='trezor_agent',
|
||||
version='0.7.2',
|
||||
version='0.7.5',
|
||||
description='Using Trezor as hardware SSH agent',
|
||||
author='Roman Zeyde',
|
||||
author_email='roman.zeyde@gmail.com',
|
||||
@@ -28,12 +28,13 @@ setup(
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
extras_require={
|
||||
'trezorlib': ['python-trezor>=0.7.4'],
|
||||
'trezorlib': ['python-trezor>=0.7.6'],
|
||||
'keepkeylib': ['keepkey>=0.7.3'],
|
||||
},
|
||||
entry_points={'console_scripts': [
|
||||
'trezor-agent = trezor_agent.__main__:run_agent',
|
||||
'trezor-git = trezor_agent.__main__:run_git',
|
||||
'trezor-gpg = trezor_agent.gpg.__main__:main',
|
||||
'trezor-gpg-create = trezor_agent.gpg.__main__:main_create',
|
||||
'trezor-gpg-agent = trezor_agent.gpg.__main__:main_agent',
|
||||
]},
|
||||
)
|
||||
|
||||
@@ -73,15 +73,6 @@ def create_git_parser():
|
||||
return p
|
||||
|
||||
|
||||
def setup_logging(verbosity):
|
||||
"""Configure logging for this tool."""
|
||||
fmt = ('%(asctime)s %(levelname)-12s %(message)-100s '
|
||||
'[%(filename)s:%(lineno)d]')
|
||||
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
|
||||
level = levels[min(verbosity, len(levels) - 1)]
|
||||
logging.basicConfig(format=fmt, level=level)
|
||||
|
||||
|
||||
def git_host(remote_name, attributes):
|
||||
"""Extract git SSH host for specified remote name."""
|
||||
try:
|
||||
@@ -132,7 +123,7 @@ def handle_connection_error(func):
|
||||
def run_agent(client_factory=client.Client):
|
||||
"""Run ssh-agent using given hardware client factory."""
|
||||
args = create_agent_parser().parse_args()
|
||||
setup_logging(verbosity=args.verbose)
|
||||
util.setup_logging(verbosity=args.verbose)
|
||||
|
||||
with client_factory(curve=args.ecdsa_curve_name) as conn:
|
||||
label = args.identity
|
||||
@@ -161,7 +152,7 @@ def run_agent(client_factory=client.Client):
|
||||
def run_git(client_factory=client.Client):
|
||||
"""Run git under ssh-agent using given hardware client factory."""
|
||||
args = create_git_parser().parse_args()
|
||||
setup_logging(verbosity=args.verbose)
|
||||
util.setup_logging(verbosity=args.verbose)
|
||||
|
||||
with client_factory(curve=args.ecdsa_curve_name) as conn:
|
||||
label = git_host(args.remote, ['pushurl', 'url'])
|
||||
|
||||
148
trezor_agent/_ledger.py
Normal file
148
trezor_agent/_ledger.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""TREZOR-like interface for Ledger hardware wallet."""
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
from trezorlib.types_pb2 import IdentityType # pylint: disable=import-error,unused-import
|
||||
from . import util
|
||||
|
||||
|
||||
class LedgerClientConnection(object):
|
||||
"""Mock for TREZOR-like connection object."""
|
||||
|
||||
def __init__(self, dongle):
|
||||
"""Create connection."""
|
||||
self.dongle = dongle
|
||||
|
||||
@staticmethod
|
||||
def expand_path(path):
|
||||
"""Convert BIP32 path into bytes."""
|
||||
return b''.join((struct.pack('>I', e) for e in path))
|
||||
|
||||
@staticmethod
|
||||
def convert_public_key(ecdsa_curve_name, result):
|
||||
"""Convert Ledger reply into PublicKey object."""
|
||||
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 = b'\x00' + bytes(keyY)
|
||||
publicKey = PublicKey()
|
||||
publicKey.node.public_key = bytes(result)
|
||||
return publicKey
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_public_node(self, n, ecdsa_curve_name='secp256k1', show_display=False):
|
||||
"""Get PublicKey object for specified BIP32 address and elliptic curve."""
|
||||
donglePath = LedgerClientConnection.expand_path(n)
|
||||
if ecdsa_curve_name == 'nist256p1':
|
||||
p2 = '01'
|
||||
else:
|
||||
p2 = '02'
|
||||
apdu = '800200' + p2
|
||||
apdu = binascii.unhexlify(apdu)
|
||||
apdu += bytearray([len(donglePath) + 1, 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'):
|
||||
"""Sign specified challenges using secret key derived from given identity."""
|
||||
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 = binascii.unhexlify(apdu)
|
||||
apdu += bytearray([len(challenge_hidden) + len(donglePath) + 1])
|
||||
apdu += bytearray([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 = b'\x00' + bytes(r) + bytes(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 = b'\x00' + bytes(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'):
|
||||
"""Create shared secret key for GPG decryption."""
|
||||
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 = binascii.unhexlify(apdu)
|
||||
apdu += bytearray([len(peer_public_key) + len(donglePath) + 1])
|
||||
apdu += bytearray([len(donglePath) // 4]) + donglePath
|
||||
apdu += peer_public_key
|
||||
result = bytearray(self.dongle.exchange(bytes(apdu)))
|
||||
sessionKey = ECDHSessionKey()
|
||||
sessionKey.session_key = bytes(result)
|
||||
return sessionKey
|
||||
|
||||
def clear_session(self):
|
||||
"""Mock for TREZOR interface compatibility."""
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""Close connection."""
|
||||
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):
|
||||
"""Mock for TREZOR interface compatibility."""
|
||||
return msg
|
||||
|
||||
|
||||
class CallException(Exception):
|
||||
"""Ledger-related error (mainly for TREZOR compatibility)."""
|
||||
|
||||
def __init__(self, code, message):
|
||||
"""Create an error."""
|
||||
super(CallException, self).__init__()
|
||||
self.args = [code, message]
|
||||
@@ -67,7 +67,7 @@ class Client(object):
|
||||
log.debug('hidden challenge size: %d bytes', len(blob))
|
||||
|
||||
log.info('please confirm user "%s" login to "%s" using %s...',
|
||||
msg['user'], label, self.device_name)
|
||||
msg['user'].decode('ascii'), label, self.device_name)
|
||||
|
||||
try:
|
||||
result = self.client.sign_identity(identity=identity,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""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(
|
||||
@@ -84,153 +83,24 @@ def _load_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]
|
||||
from ._ledger import LedgerClientConnection, CallException, IdentityType
|
||||
try:
|
||||
from ledgerblue.comm import getDongle
|
||||
from ledgerblue.comm import getDongle, CommException
|
||||
except ImportError as e:
|
||||
log.warning('%s: install via "pip install ledgerblue" '
|
||||
'if you need to support this device', e)
|
||||
return
|
||||
# pylint: disable=bare-except
|
||||
try:
|
||||
from trezorlib.types_pb2 import IdentityType # pylint: disable=import-error
|
||||
dongle = getDongle()
|
||||
except:
|
||||
except CommException:
|
||||
return
|
||||
|
||||
yield ClientWrapper(connection=LedgerClientConnection(dongle),
|
||||
identity_type=IdentityType,
|
||||
device_name="ledger",
|
||||
call_exception=CallException)
|
||||
|
||||
|
||||
LOADERS = [
|
||||
_load_trezor,
|
||||
_load_keepkey,
|
||||
|
||||
@@ -2,30 +2,41 @@
|
||||
"""Create signatures and export public keys for GPG using TREZOR."""
|
||||
import argparse
|
||||
import contextlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from . import agent, device, encode, keyring, protocol
|
||||
from .. import formats, server
|
||||
import semver
|
||||
|
||||
from . import agent, decode, device, encode, keyring, protocol
|
||||
from .. import formats, server, util
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def key_exists(user_id):
|
||||
"""Return True iff there is a GPG key with specified user ID."""
|
||||
for p in decode.parse_packets(io.BytesIO(keyring.export_public_keys())):
|
||||
if p['type'] == 'user_id' and p['value'] == user_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def run_create(args):
|
||||
"""Generate a new pubkey for a new/existing GPG identity."""
|
||||
user_id = os.environ['TREZOR_GPG_USER_ID']
|
||||
log.warning('NOTE: in order to re-generate the exact same GPG key later, '
|
||||
'run this command with "--time=%d" commandline flag (to set '
|
||||
'the timestamp of the GPG key manually).', args.time)
|
||||
conn = device.HardwareSigner(user_id=user_id,
|
||||
conn = device.HardwareSigner(user_id=args.user_id,
|
||||
curve_name=args.ecdsa_curve)
|
||||
verifying_key = conn.pubkey(ecdh=False)
|
||||
decryption_key = conn.pubkey(ecdh=True)
|
||||
|
||||
if args.subkey:
|
||||
primary_bytes = keyring.export_public_key(user_id=user_id)
|
||||
if key_exists(args.user_id): # add as subkey
|
||||
log.info('adding %s GPG subkey for "%s" to existing key',
|
||||
args.ecdsa_curve, args.user_id)
|
||||
# subkey for signing
|
||||
signing_key = protocol.PublicKey(
|
||||
curve_name=args.ecdsa_curve, created=args.time,
|
||||
@@ -34,13 +45,16 @@ def run_create(args):
|
||||
encryption_key = protocol.PublicKey(
|
||||
curve_name=formats.get_ecdh_curve_name(args.ecdsa_curve),
|
||||
created=args.time, verifying_key=decryption_key, ecdh=True)
|
||||
primary_bytes = keyring.export_public_key(args.user_id)
|
||||
result = encode.create_subkey(primary_bytes=primary_bytes,
|
||||
pubkey=signing_key,
|
||||
subkey=signing_key,
|
||||
signer_func=conn.sign)
|
||||
result = encode.create_subkey(primary_bytes=result,
|
||||
pubkey=encryption_key,
|
||||
subkey=encryption_key,
|
||||
signer_func=conn.sign)
|
||||
else:
|
||||
else: # add as primary
|
||||
log.info('creating new %s GPG primary key for "%s"',
|
||||
args.ecdsa_curve, args.user_id)
|
||||
# primary key for signing
|
||||
primary = protocol.PublicKey(
|
||||
curve_name=args.ecdsa_curve, created=args.time,
|
||||
@@ -50,50 +64,54 @@ def run_create(args):
|
||||
curve_name=formats.get_ecdh_curve_name(args.ecdsa_curve),
|
||||
created=args.time, verifying_key=decryption_key, ecdh=True)
|
||||
|
||||
result = encode.create_primary(user_id=user_id,
|
||||
result = encode.create_primary(user_id=args.user_id,
|
||||
pubkey=primary,
|
||||
signer_func=conn.sign)
|
||||
result = encode.create_subkey(primary_bytes=result,
|
||||
pubkey=subkey,
|
||||
subkey=subkey,
|
||||
signer_func=conn.sign)
|
||||
|
||||
sys.stdout.write(protocol.armor(result, 'PUBLIC KEY BLOCK'))
|
||||
|
||||
|
||||
def run_agent(args): # pylint: disable=unused-argument
|
||||
def main_create():
|
||||
"""Main function for GPG identity creation."""
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('user_id')
|
||||
p.add_argument('-e', '--ecdsa-curve', default='nist256p1')
|
||||
p.add_argument('-t', '--time', type=int, default=int(time.time()))
|
||||
p.add_argument('-v', '--verbose', default=0, action='count')
|
||||
|
||||
args = p.parse_args()
|
||||
util.setup_logging(verbosity=args.verbose)
|
||||
log.warning('This GPG tool is still in EXPERIMENTAL mode, '
|
||||
'so please note that the API and features may '
|
||||
'change without backwards compatibility!')
|
||||
|
||||
existing_gpg = keyring.gpg_version().decode('ascii')
|
||||
required_gpg = '>=2.1.15'
|
||||
if semver.match(existing_gpg, required_gpg):
|
||||
run_create(args)
|
||||
else:
|
||||
log.error('Existing gpg2 has version "%s" (%s required)',
|
||||
existing_gpg, required_gpg)
|
||||
|
||||
|
||||
def main_agent():
|
||||
"""Run a simple GPG-agent server."""
|
||||
home_dir = os.environ.get('GNUPGHOME', os.path.expanduser('~/.gnupg/trezor'))
|
||||
config_file = os.path.join(home_dir, 'gpg-agent.conf')
|
||||
lines = (line.strip() for line in open(config_file))
|
||||
lines = (line for line in lines if line and not line.startswith('#'))
|
||||
config = dict(line.split(' ', 1) for line in lines)
|
||||
|
||||
util.setup_logging(verbosity=int(config['verbosity']),
|
||||
filename=config['log-file'])
|
||||
sock_path = keyring.get_agent_sock_path()
|
||||
with server.unix_domain_socket_server(sock_path) as sock:
|
||||
for conn in agent.yield_connections(sock):
|
||||
with contextlib.closing(conn):
|
||||
agent.handle_connection(conn)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('-v', '--verbose', action='store_true', default=False)
|
||||
subparsers = p.add_subparsers()
|
||||
subparsers.required = True
|
||||
subparsers.dest = 'command'
|
||||
|
||||
create_cmd = subparsers.add_parser('create')
|
||||
create_cmd.add_argument('-s', '--subkey', action='store_true', default=False)
|
||||
create_cmd.add_argument('-e', '--ecdsa-curve', default='nist256p1')
|
||||
create_cmd.add_argument('-t', '--time', type=int, default=int(time.time()))
|
||||
create_cmd.set_defaults(run=run_create)
|
||||
|
||||
agent_cmd = subparsers.add_parser('agent')
|
||||
agent_cmd.set_defaults(run=run_agent)
|
||||
|
||||
args = p.parse_args()
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
format='%(asctime)s %(levelname)-10s %(message)s')
|
||||
log.warning('This GPG tool is still in EXPERIMENTAL mode, '
|
||||
'so please note that the API and features may '
|
||||
'change without backwards compatibility!')
|
||||
args.run(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
try:
|
||||
agent.handle_connection(conn)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
log.exception('gpg-agent failed: %s', e)
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
import binascii
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from . import decode, encode, keyring
|
||||
from . import decode, device, keyring, protocol
|
||||
from .. import util
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -25,8 +24,9 @@ def yield_connections(sock):
|
||||
|
||||
def serialize(data):
|
||||
"""Serialize data according to ASSUAN protocol."""
|
||||
for c in ['%', '\n', '\r']:
|
||||
data = data.replace(c, '%{:02X}'.format(ord(c)))
|
||||
for c in [b'%', b'\n', b'\r']:
|
||||
escaped = '%{:02X}'.format(ord(c)).encode('ascii')
|
||||
data = data.replace(c, escaped)
|
||||
return data
|
||||
|
||||
|
||||
@@ -34,19 +34,40 @@ def sig_encode(r, s):
|
||||
"""Serialize ECDSA signature data into GPG S-expression."""
|
||||
r = serialize(util.num2bytes(r, 32))
|
||||
s = serialize(util.num2bytes(s, 32))
|
||||
return '(7:sig-val(5:ecdsa(1:r32:{})(1:s32:{})))'.format(r, s)
|
||||
return b'(7:sig-val(5:ecdsa(1:r32:' + r + b')(1:s32:' + s + b')))'
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open_connection(keygrip_bytes):
|
||||
"""
|
||||
Connect to the device for the specified keygrip.
|
||||
|
||||
Parse GPG public key to find the first user ID, which is used to
|
||||
specify the correct signature/decryption key on the device.
|
||||
"""
|
||||
pubkey_dict, user_ids = decode.load_by_keygrip(
|
||||
pubkey_bytes=keyring.export_public_keys(),
|
||||
keygrip=keygrip_bytes)
|
||||
# We assume the first user ID is used to generate TREZOR-based GPG keys.
|
||||
user_id = user_ids[0]['value']
|
||||
curve_name = protocol.get_curve_name_by_oid(pubkey_dict['curve_oid'])
|
||||
ecdh = (pubkey_dict['algo'] == protocol.ECDH_ALGO_ID)
|
||||
|
||||
conn = device.HardwareSigner(user_id, curve_name=curve_name)
|
||||
with contextlib.closing(conn):
|
||||
pubkey = protocol.PublicKey(
|
||||
curve_name=curve_name, created=pubkey_dict['created'],
|
||||
verifying_key=conn.pubkey(ecdh=ecdh), ecdh=ecdh)
|
||||
assert pubkey.key_id() == pubkey_dict['key_id']
|
||||
assert pubkey.keygrip == keygrip_bytes
|
||||
yield conn
|
||||
|
||||
|
||||
def pksign(keygrip, digest, algo):
|
||||
"""Sign a message digest using a private EC key."""
|
||||
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),
|
||||
use_custom=True, ecdh=False)
|
||||
pubkey, conn = encode.load_from_public_key(pubkey_dict=pubkey_dict)
|
||||
with contextlib.closing(conn):
|
||||
assert pubkey.keygrip == binascii.unhexlify(keygrip)
|
||||
log.debug('signing %r digest (algo #%s)', digest, algo)
|
||||
keygrip_bytes = binascii.unhexlify(keygrip)
|
||||
with open_connection(keygrip_bytes) as conn:
|
||||
r, s = conn.sign(binascii.unhexlify(digest))
|
||||
result = sig_encode(r, s)
|
||||
log.debug('result: %r', result)
|
||||
@@ -54,17 +75,15 @@ def pksign(keygrip, digest, algo):
|
||||
|
||||
|
||||
def _serialize_point(data):
|
||||
data = '{}:'.format(len(data)) + data
|
||||
prefix = '{}:'.format(len(data)).encode('ascii')
|
||||
# https://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
|
||||
for c in ['%', '\n', '\r']:
|
||||
data = data.replace(c, '%{:02X}'.format(ord(c)))
|
||||
return '(5:value' + data + ')'
|
||||
return b'(5:value' + serialize(prefix + data) + b')'
|
||||
|
||||
|
||||
def parse_ecdh(line):
|
||||
"""Parse ECDH request and return remote public key."""
|
||||
prefix, line = line.split(' ', 1)
|
||||
assert prefix == 'D'
|
||||
prefix, line = line.split(b' ', 1)
|
||||
assert prefix == b'D'
|
||||
exp, leftover = keyring.parse(keyring.unescape(line))
|
||||
log.debug('ECDH s-exp: %r', exp)
|
||||
assert not leftover
|
||||
@@ -73,7 +92,7 @@ def parse_ecdh(line):
|
||||
assert exp[0] == b'ecdh'
|
||||
items = exp[1:]
|
||||
log.debug('ECDH parameters: %r', items)
|
||||
return dict(items)['e']
|
||||
return dict(items)[b'e']
|
||||
|
||||
|
||||
def pkdecrypt(keygrip, conn):
|
||||
@@ -85,13 +104,8 @@ def pkdecrypt(keygrip, conn):
|
||||
assert keyring.recvline(conn) == b'END'
|
||||
remote_pubkey = parse_ecdh(line)
|
||||
|
||||
user_id = os.environ['TREZOR_GPG_USER_ID']
|
||||
local_pubkey = decode.load_public_key(
|
||||
pubkey_bytes=keyring.export_public_key(user_id=user_id),
|
||||
use_custom=True, ecdh=True)
|
||||
pubkey, conn = encode.load_from_public_key(pubkey_dict=local_pubkey)
|
||||
with contextlib.closing(conn):
|
||||
assert pubkey.keygrip == binascii.unhexlify(keygrip)
|
||||
keygrip_bytes = binascii.unhexlify(keygrip)
|
||||
with open_connection(keygrip_bytes) as conn:
|
||||
return _serialize_point(conn.ecdh(remote_pubkey))
|
||||
|
||||
|
||||
@@ -100,36 +114,36 @@ def handle_connection(conn):
|
||||
keygrip = None
|
||||
digest = None
|
||||
algo = None
|
||||
version = keyring.gpg_version()
|
||||
version = keyring.gpg_version() # "Clone" existing GPG version
|
||||
|
||||
keyring.sendline(conn, b'OK')
|
||||
for line in keyring.iterlines(conn):
|
||||
parts = line.split(' ')
|
||||
parts = line.split(b' ')
|
||||
command = parts[0]
|
||||
args = parts[1:]
|
||||
if command in {'RESET', 'OPTION', 'HAVEKEY', 'SETKEYDESC'}:
|
||||
if command in {b'RESET', b'OPTION', b'HAVEKEY', b'SETKEYDESC'}:
|
||||
pass # reply with OK
|
||||
elif command == 'GETINFO':
|
||||
elif command == b'GETINFO':
|
||||
keyring.sendline(conn, b'D ' + version)
|
||||
elif command == 'AGENT_ID':
|
||||
keyring.sendline(conn, b'D TREZOR')
|
||||
elif command in {'SIGKEY', 'SETKEY'}:
|
||||
elif command == b'AGENT_ID':
|
||||
keyring.sendline(conn, b'D TREZOR') # "Fake" agent ID
|
||||
elif command in {b'SIGKEY', b'SETKEY'}:
|
||||
keygrip, = args
|
||||
elif command == 'SETHASH':
|
||||
elif command == b'SETHASH':
|
||||
algo, digest = args
|
||||
elif command == 'PKSIGN':
|
||||
elif command == b'PKSIGN':
|
||||
sig = pksign(keygrip, digest, algo)
|
||||
keyring.sendline(conn, b'D ' + sig)
|
||||
elif command == 'PKDECRYPT':
|
||||
elif command == b'PKDECRYPT':
|
||||
sec = pkdecrypt(keygrip, conn)
|
||||
keyring.sendline(conn, b'D ' + sec)
|
||||
elif command == 'KEYINFO':
|
||||
elif command == b'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':
|
||||
fmt = 'S KEYINFO {0} X - - - - - - -'
|
||||
keyring.sendline(conn, fmt.format(keygrip).encode('ascii'))
|
||||
elif command == b'BYE':
|
||||
return
|
||||
else:
|
||||
log.error('unknown request: %r', line)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Decoders for GPG v2 data structures."""
|
||||
import base64
|
||||
import copy
|
||||
import functools
|
||||
import hashlib
|
||||
import io
|
||||
@@ -53,45 +52,29 @@ def parse_mpis(s, n):
|
||||
return [parse_mpi(s) for _ in range(n)]
|
||||
|
||||
|
||||
def _parse_nist256p1_verifier(mpi):
|
||||
def _parse_nist256p1_pubkey(mpi):
|
||||
prefix, x, y = util.split_bits(mpi, 4, 256, 256)
|
||||
assert prefix == 4
|
||||
point = ecdsa.ellipticcurve.Point(curve=ecdsa.NIST256p.curve,
|
||||
x=x, y=y)
|
||||
vk = ecdsa.VerifyingKey.from_public_point(
|
||||
return ecdsa.VerifyingKey.from_public_point(
|
||||
point=point, curve=ecdsa.curves.NIST256p,
|
||||
hashfunc=hashlib.sha256)
|
||||
|
||||
def _nist256p1_verify(signature, digest):
|
||||
result = vk.verify_digest(signature=signature,
|
||||
digest=digest,
|
||||
sigdecode=lambda rs, order: rs)
|
||||
log.debug('nist256p1 ECDSA signature is OK (%s)', result)
|
||||
return _nist256p1_verify, vk
|
||||
|
||||
|
||||
def _parse_ed25519_verifier(mpi):
|
||||
def _parse_ed25519_pubkey(mpi):
|
||||
prefix, value = util.split_bits(mpi, 8, 256)
|
||||
assert prefix == 0x40
|
||||
vk = ed25519.VerifyingKey(util.num2bytes(value, size=32))
|
||||
|
||||
def _ed25519_verify(signature, digest):
|
||||
sig = b''.join(util.num2bytes(val, size=32)
|
||||
for val in signature)
|
||||
result = vk.verify(sig, digest)
|
||||
log.debug('ed25519 ECDSA signature is OK (%s)', result)
|
||||
return _ed25519_verify, vk
|
||||
|
||||
|
||||
def _parse_curve25519_verifier(_):
|
||||
log.warning('Curve25519 ECDH is not verified')
|
||||
return None, None
|
||||
return ed25519.VerifyingKey(util.num2bytes(value, size=32))
|
||||
|
||||
|
||||
SUPPORTED_CURVES = {
|
||||
b'\x2A\x86\x48\xCE\x3D\x03\x01\x07': _parse_nist256p1_verifier,
|
||||
b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01': _parse_ed25519_verifier,
|
||||
b'\x2B\x06\x01\x04\x01\x97\x55\x01\x05\x01': _parse_curve25519_verifier,
|
||||
b'\x2A\x86\x48\xCE\x3D\x03\x01\x07':
|
||||
(_parse_nist256p1_pubkey, protocol.keygrip_nist256),
|
||||
b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01':
|
||||
(_parse_ed25519_pubkey, protocol.keygrip_ed25519),
|
||||
b'\x2B\x06\x01\x04\x01\x97\x55\x01\x05\x01':
|
||||
(_parse_ed25519_pubkey, protocol.keygrip_curve25519),
|
||||
}
|
||||
|
||||
RSA_ALGO_IDS = {1, 2, 3}
|
||||
@@ -144,7 +127,9 @@ def _parse_signature(stream):
|
||||
log.debug('embedded sigs: %s', embedded)
|
||||
p['embedded'] = embedded
|
||||
|
||||
p['_is_custom'] = (protocol.CUSTOM_SUBPACKET in p['unhashed_subpackets'])
|
||||
# Detect our custom public keys by matching subpacket data
|
||||
p['_is_custom'] = any(protocol.CUSTOM_KEY_LABEL == subpacket[1:]
|
||||
for subpacket in p['unhashed_subpackets'])
|
||||
|
||||
p['hash_prefix'] = stream.readfmt('2s')
|
||||
if p['pubkey_alg'] in ECDSA_ALGO_IDS:
|
||||
@@ -175,11 +160,9 @@ def _parse_pubkey(stream, packet_type='pubkey'):
|
||||
oid = stream.read(oid_size)
|
||||
assert oid in SUPPORTED_CURVES, util.hexlify(oid)
|
||||
p['curve_oid'] = oid
|
||||
parser = SUPPORTED_CURVES[oid]
|
||||
|
||||
mpi = parse_mpi(stream)
|
||||
log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length())
|
||||
p['verifier'], p['verifying_key'] = parser(mpi)
|
||||
leftover = stream.read()
|
||||
if leftover:
|
||||
leftover = io.BytesIO(leftover)
|
||||
@@ -188,15 +171,18 @@ def _parse_pubkey(stream, packet_type='pubkey'):
|
||||
size, = util.readfmt(leftover, 'B')
|
||||
p['kdf'] = leftover.read(size)
|
||||
assert not leftover.read()
|
||||
|
||||
parse_func, keygrip_func = SUPPORTED_CURVES[oid]
|
||||
keygrip = keygrip_func(parse_func(mpi))
|
||||
log.debug('keygrip: %s', util.hexlify(keygrip))
|
||||
p['keygrip'] = keygrip
|
||||
|
||||
elif p['algo'] == DSA_ALGO_ID:
|
||||
log.warning('DSA signatures are not verified')
|
||||
parse_mpis(stream, n=4)
|
||||
parse_mpis(stream, n=4) # DSA keys are not supported
|
||||
elif p['algo'] == ELGAMAL_ALGO_ID:
|
||||
log.warning('ElGamal signatures are not verified')
|
||||
parse_mpis(stream, n=3)
|
||||
parse_mpis(stream, n=3) # ElGamal keys are not supported
|
||||
else: # assume RSA
|
||||
log.warning('RSA signatures are not verified')
|
||||
parse_mpis(stream, n=2)
|
||||
parse_mpis(stream, n=2) # RSA keys are not supported
|
||||
assert not stream.read()
|
||||
|
||||
# https://tools.ietf.org/html/rfc4880#section-12.2
|
||||
@@ -287,28 +273,6 @@ def digest_packets(packets, hasher):
|
||||
return hasher.digest()
|
||||
|
||||
|
||||
def collect_packets(packets, types_to_collect):
|
||||
"""Collect specified packet types into their leading packet."""
|
||||
packet = None
|
||||
result = []
|
||||
for p in packets:
|
||||
if p['type'] in types_to_collect:
|
||||
packet.setdefault(p['type'], []).append(p)
|
||||
else:
|
||||
packet = copy.deepcopy(p)
|
||||
result.append(packet)
|
||||
return result
|
||||
|
||||
|
||||
def parse_public_keys(stream):
|
||||
"""Parse GPG public key into hierarchy of packets."""
|
||||
packets = list(parse_packets(stream))
|
||||
packets = collect_packets(packets, {'signature'})
|
||||
packets = collect_packets(packets, {'user_id', 'user_attribute'})
|
||||
packets = collect_packets(packets, {'subkey'})
|
||||
return packets
|
||||
|
||||
|
||||
HASH_ALGORITHMS = {
|
||||
1: 'md5',
|
||||
2: 'sha1',
|
||||
@@ -320,42 +284,22 @@ HASH_ALGORITHMS = {
|
||||
}
|
||||
|
||||
|
||||
def load_public_key(pubkey_bytes, use_custom=False, ecdh=False):
|
||||
"""Parse and validate GPG public key from an input stream."""
|
||||
def load_by_keygrip(pubkey_bytes, keygrip):
|
||||
"""Return public key and first user ID for specified keygrip."""
|
||||
stream = io.BytesIO(pubkey_bytes)
|
||||
packets = list(parse_packets(stream))
|
||||
pubkey, userid, signature = packets[:3]
|
||||
packets = packets[3:]
|
||||
packets_per_pubkey = []
|
||||
for p in packets:
|
||||
if p['type'] == 'pubkey':
|
||||
# Add a new packet list for each pubkey.
|
||||
packets_per_pubkey.append([])
|
||||
packets_per_pubkey[-1].append(p)
|
||||
|
||||
hash_alg = HASH_ALGORITHMS.get(signature['hash_alg'])
|
||||
if hash_alg is not None:
|
||||
digest = digest_packets(packets=[pubkey, userid, signature],
|
||||
hasher=hashlib.new(hash_alg))
|
||||
assert signature['hash_prefix'] == digest[:2]
|
||||
|
||||
log.debug('loaded public key "%s"', userid['value'])
|
||||
if hash_alg is not None and pubkey.get('verifier'):
|
||||
verify_digest(pubkey=pubkey, digest=digest,
|
||||
signature=signature['sig'], label='GPG public key')
|
||||
else:
|
||||
log.warning('public key %s is not verified!',
|
||||
util.hexlify(pubkey['key_id']))
|
||||
|
||||
packet = pubkey
|
||||
while use_custom:
|
||||
if packet['type'] in ('pubkey', 'subkey') and signature['_is_custom']:
|
||||
if ecdh == (packet['algo'] == protocol.ECDH_ALGO_ID):
|
||||
log.debug('found custom %s', packet['type'])
|
||||
break
|
||||
|
||||
while packets[1]['type'] != 'signature':
|
||||
packets = packets[1:]
|
||||
packet, signature = packets[:2]
|
||||
packets = packets[2:]
|
||||
|
||||
packet['user_id'] = userid['value']
|
||||
packet['_is_custom'] = signature['_is_custom']
|
||||
return packet
|
||||
for packets in packets_per_pubkey:
|
||||
user_ids = [p for p in packets if p['type'] == 'user_id']
|
||||
for p in packets:
|
||||
if p.get('keygrip') == keygrip:
|
||||
return p, user_ids
|
||||
|
||||
|
||||
def load_signature(stream, original_data):
|
||||
@@ -368,17 +312,6 @@ def load_signature(stream, original_data):
|
||||
return signature, digest
|
||||
|
||||
|
||||
def verify_digest(pubkey, digest, signature, label):
|
||||
"""Verify a digest signature from a specified public key."""
|
||||
verifier = pubkey['verifier']
|
||||
try:
|
||||
verifier(signature, digest)
|
||||
log.debug('%s is OK', label)
|
||||
except ecdsa.keys.BadSignatureError:
|
||||
log.error('Bad %s!', label)
|
||||
raise ValueError('Invalid ECDSA signature for {}'.format(label))
|
||||
|
||||
|
||||
def remove_armor(armored_data):
|
||||
"""Decode armored data into its binary form."""
|
||||
stream = io.BytesIO(armored_data)
|
||||
@@ -387,11 +320,3 @@ def remove_armor(armored_data):
|
||||
payload, checksum = data[:-3], data[-3:]
|
||||
assert util.crc24(payload) == checksum
|
||||
return payload
|
||||
|
||||
|
||||
def verify(pubkey, signature, original_data):
|
||||
"""Verify correctness of public key and signature."""
|
||||
stream = io.BytesIO(remove_armor(signature))
|
||||
signature, digest = load_signature(stream, original_data)
|
||||
verify_digest(pubkey=pubkey, digest=digest,
|
||||
signature=signature['sig'], label='GPG signature')
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Device abstraction layer for GPG operations."""
|
||||
|
||||
import logging
|
||||
|
||||
from .. import factory, formats, util
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HardwareSigner(object):
|
||||
"""Sign messages and get public keys from a hardware device."""
|
||||
@@ -13,6 +17,7 @@ class HardwareSigner(object):
|
||||
self.identity.proto = 'gpg'
|
||||
self.identity.host = user_id
|
||||
self.curve_name = curve_name
|
||||
self.user_id = user_id
|
||||
|
||||
def pubkey(self, ecdh=False):
|
||||
"""Return public key as VerifyingKey object."""
|
||||
@@ -30,6 +35,11 @@ class HardwareSigner(object):
|
||||
|
||||
def sign(self, digest):
|
||||
"""Sign the digest and return a serialized signature."""
|
||||
log.info('please confirm GPG signature on %s for "%s"...',
|
||||
self.client_wrapper.device_name, self.user_id)
|
||||
if self.curve_name == formats.CURVE_NIST256:
|
||||
digest = digest[:32] # sign the first 256 bits
|
||||
log.debug('signing digest: %s', util.hexlify(digest))
|
||||
result = self.client_wrapper.connection.sign_identity(
|
||||
identity=self.identity,
|
||||
challenge_hidden=digest,
|
||||
@@ -41,6 +51,8 @@ class HardwareSigner(object):
|
||||
|
||||
def ecdh(self, pubkey):
|
||||
"""Derive shared secret using ECDH from remote public key."""
|
||||
log.info('please confirm GPG decryption on %s for "%s"...',
|
||||
self.client_wrapper.device_name, self.user_id)
|
||||
result = self.client_wrapper.connection.get_ecdh_session_key(
|
||||
identity=self.identity,
|
||||
peer_public_key=pubkey,
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
"""Create GPG ECDSA signatures and public keys using TREZOR device."""
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
|
||||
from . import decode, device, keyring, protocol
|
||||
from . import decode, keyring, protocol
|
||||
from .. import util
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _time_format(t):
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t))
|
||||
|
||||
|
||||
def create_primary(user_id, pubkey, signer_func):
|
||||
"""Export new primary GPG public key, ready for "gpg2 --import"."""
|
||||
pubkey_packet = protocol.packet(tag=6, blob=pubkey.data())
|
||||
@@ -21,7 +17,6 @@ def create_primary(user_id, pubkey, signer_func):
|
||||
data_to_sign = (pubkey.data_to_hash() +
|
||||
user_id_packet[:1] +
|
||||
util.prefix_len('>L', user_id.encode('ascii')))
|
||||
log.info('creating primary GPG key "%s"', user_id)
|
||||
hashed_subpackets = [
|
||||
protocol.subpacket_time(pubkey.created), # signature time
|
||||
# https://tools.ietf.org/html/rfc4880#section-5.2.3.7
|
||||
@@ -40,7 +35,6 @@ def create_primary(user_id, pubkey, signer_func):
|
||||
protocol.subpacket(16, pubkey.key_id()), # issuer key id
|
||||
protocol.CUSTOM_SUBPACKET]
|
||||
|
||||
log.info('confirm signing with primary key')
|
||||
signature = protocol.make_signature(
|
||||
signer_func=signer_func,
|
||||
public_algo=pubkey.algo_id,
|
||||
@@ -53,26 +47,26 @@ def create_primary(user_id, pubkey, signer_func):
|
||||
return pubkey_packet + user_id_packet + sign_packet
|
||||
|
||||
|
||||
def create_subkey(primary_bytes, pubkey, signer_func):
|
||||
def create_subkey(primary_bytes, subkey, signer_func, user_id=None):
|
||||
"""Export new subkey to GPG primary key."""
|
||||
subkey_packet = protocol.packet(tag=14, blob=pubkey.data())
|
||||
primary = decode.load_public_key(primary_bytes)
|
||||
log.info('adding subkey to primary GPG key "%s"', primary['user_id'])
|
||||
data_to_sign = primary['_to_hash'] + pubkey.data_to_hash()
|
||||
subkey_packet = protocol.packet(tag=14, blob=subkey.data())
|
||||
packets = list(decode.parse_packets(io.BytesIO(primary_bytes)))
|
||||
primary, user_id, signature = packets[:3]
|
||||
|
||||
if pubkey.ecdh:
|
||||
data_to_sign = primary['_to_hash'] + subkey.data_to_hash()
|
||||
|
||||
if subkey.ecdh:
|
||||
embedded_sig = None
|
||||
else:
|
||||
# Primary Key Binding Signature
|
||||
hashed_subpackets = [
|
||||
protocol.subpacket_time(pubkey.created)] # signature time
|
||||
protocol.subpacket_time(subkey.created)] # signature time
|
||||
unhashed_subpackets = [
|
||||
protocol.subpacket(16, pubkey.key_id())] # issuer key id
|
||||
log.info('confirm signing with new subkey')
|
||||
protocol.subpacket(16, subkey.key_id())] # issuer key id
|
||||
embedded_sig = protocol.make_signature(
|
||||
signer_func=signer_func,
|
||||
data_to_sign=data_to_sign,
|
||||
public_algo=pubkey.algo_id,
|
||||
public_algo=subkey.algo_id,
|
||||
sig_type=0x19,
|
||||
hashed_subpackets=hashed_subpackets,
|
||||
unhashed_subpackets=unhashed_subpackets)
|
||||
@@ -81,10 +75,10 @@ def create_subkey(primary_bytes, pubkey, signer_func):
|
||||
|
||||
# Key flags: https://tools.ietf.org/html/rfc4880#section-5.2.3.21
|
||||
# (certify & sign) (encrypt)
|
||||
flags = (2) if (not pubkey.ecdh) else (4 | 8)
|
||||
flags = (2) if (not subkey.ecdh) else (4 | 8)
|
||||
|
||||
hashed_subpackets = [
|
||||
protocol.subpacket_time(pubkey.created), # signature time
|
||||
protocol.subpacket_time(subkey.created), # signature time
|
||||
protocol.subpacket_byte(0x1B, flags)]
|
||||
|
||||
unhashed_subpackets = []
|
||||
@@ -93,9 +87,8 @@ def create_subkey(primary_bytes, pubkey, signer_func):
|
||||
unhashed_subpackets.append(protocol.subpacket(32, embedded_sig))
|
||||
unhashed_subpackets.append(protocol.CUSTOM_SUBPACKET)
|
||||
|
||||
log.info('confirm signing with primary key')
|
||||
if not primary['_is_custom']:
|
||||
signer_func = keyring.create_agent_signer(primary['user_id'])
|
||||
if not signature['_is_custom']:
|
||||
signer_func = keyring.create_agent_signer(user_id['value'])
|
||||
|
||||
signature = protocol.make_signature(
|
||||
signer_func=signer_func,
|
||||
@@ -106,22 +99,3 @@ def create_subkey(primary_bytes, pubkey, signer_func):
|
||||
unhashed_subpackets=unhashed_subpackets)
|
||||
sign_packet = protocol.packet(tag=2, blob=signature)
|
||||
return primary_bytes + subkey_packet + sign_packet
|
||||
|
||||
|
||||
def load_from_public_key(pubkey_dict):
|
||||
"""Load correct public key from the device."""
|
||||
log.debug('pubkey_dict: %s', pubkey_dict)
|
||||
user_id = pubkey_dict['user_id']
|
||||
created = pubkey_dict['created']
|
||||
curve_name = protocol.get_curve_name_by_oid(pubkey_dict['curve_oid'])
|
||||
ecdh = (pubkey_dict['algo'] == protocol.ECDH_ALGO_ID)
|
||||
|
||||
conn = device.HardwareSigner(user_id, curve_name=curve_name)
|
||||
pubkey = protocol.PublicKey(
|
||||
curve_name=curve_name, created=created,
|
||||
verifying_key=conn.pubkey(ecdh=ecdh), ecdh=ecdh)
|
||||
assert pubkey.key_id() == pubkey_dict['key_id']
|
||||
log.info('%s created at %s for "%s"',
|
||||
pubkey, _time_format(pubkey.created), user_id)
|
||||
|
||||
return pubkey, conn
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Tools for doing signature using gpg-agent."""
|
||||
from __future__ import unicode_literals, absolute_import, print_function
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import binascii
|
||||
import io
|
||||
@@ -24,7 +24,7 @@ def get_agent_sock_path(sp=subprocess):
|
||||
def connect_to_agent(sp=subprocess):
|
||||
"""Connect to GPG agent's UNIX socket."""
|
||||
sock_path = get_agent_sock_path(sp=sp)
|
||||
sp.check_call(['gpg-connect-agent', '/bye'])
|
||||
sp.check_call(['gpg-connect-agent', '/bye']) # Make sure it's running
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(sock_path)
|
||||
return sock
|
||||
@@ -158,7 +158,12 @@ def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None):
|
||||
assert communicate(sock, 'SETKEYDESC '
|
||||
'Sign+a+new+TREZOR-based+subkey') == b'OK'
|
||||
assert communicate(sock, 'PKSIGN') == b'OK'
|
||||
line = recvline(sock).strip()
|
||||
while True:
|
||||
line = recvline(sock).strip()
|
||||
if line.startswith(b'S PROGRESS'):
|
||||
continue
|
||||
else:
|
||||
break
|
||||
line = unescape(line)
|
||||
log.debug('unescaped: %r', line)
|
||||
prefix, sig = line.split(b' ', 1)
|
||||
@@ -184,7 +189,7 @@ 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)
|
||||
output = sp.check_output(args).decode('ascii')
|
||||
return re.findall(r'Keygrip = (\w+)', output)[0]
|
||||
|
||||
|
||||
@@ -206,6 +211,12 @@ def export_public_key(user_id, sp=subprocess):
|
||||
return result
|
||||
|
||||
|
||||
def export_public_keys(sp=subprocess):
|
||||
"""Export all GPG public keys."""
|
||||
args = gpg_command(['--export'])
|
||||
return sp.check_output(args=args)
|
||||
|
||||
|
||||
def create_agent_signer(user_id):
|
||||
"""Sign digest with existing GPG keys using gpg-agent tool."""
|
||||
sock = connect_to_agent()
|
||||
|
||||
@@ -99,7 +99,8 @@ def _compute_keygrip(params):
|
||||
return hashlib.sha1(b''.join(parts)).digest()
|
||||
|
||||
|
||||
def _keygrip_nist256(vk):
|
||||
def keygrip_nist256(vk):
|
||||
"""Compute keygrip for NIST256 curve public keys."""
|
||||
curve = vk.curve.curve
|
||||
gen = vk.curve.generator
|
||||
g = (4 << 512) | (gen.x() << 256) | gen.y()
|
||||
@@ -116,7 +117,8 @@ def _keygrip_nist256(vk):
|
||||
])
|
||||
|
||||
|
||||
def _keygrip_ed25519(vk):
|
||||
def keygrip_ed25519(vk):
|
||||
"""Compute keygrip for Ed25519 public keys."""
|
||||
# pylint: disable=line-too-long
|
||||
return _compute_keygrip([
|
||||
['p', util.num2bytes(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED, size=32)], # nopep8
|
||||
@@ -128,7 +130,8 @@ def _keygrip_ed25519(vk):
|
||||
])
|
||||
|
||||
|
||||
def _keygrip_curve25519(vk):
|
||||
def keygrip_curve25519(vk):
|
||||
"""Compute keygrip for Curve25519 public keys."""
|
||||
# pylint: disable=line-too-long
|
||||
return _compute_keygrip([
|
||||
['p', util.num2bytes(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED, size=32)], # nopep8
|
||||
@@ -146,25 +149,27 @@ SUPPORTED_CURVES = {
|
||||
'oid': b'\x2A\x86\x48\xCE\x3D\x03\x01\x07',
|
||||
'algo_id': 19,
|
||||
'serialize': _serialize_nist256,
|
||||
'keygrip': _keygrip_nist256,
|
||||
'keygrip': keygrip_nist256,
|
||||
},
|
||||
formats.CURVE_ED25519: {
|
||||
'oid': b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01',
|
||||
'algo_id': 22,
|
||||
'serialize': _serialize_ed25519,
|
||||
'keygrip': _keygrip_ed25519,
|
||||
'keygrip': keygrip_ed25519,
|
||||
},
|
||||
formats.ECDH_CURVE25519: {
|
||||
'oid': b'\x2B\x06\x01\x04\x01\x97\x55\x01\x05\x01',
|
||||
'algo_id': 18,
|
||||
'serialize': _serialize_ed25519,
|
||||
'keygrip': _keygrip_curve25519,
|
||||
'keygrip': keygrip_curve25519,
|
||||
},
|
||||
}
|
||||
|
||||
ECDH_ALGO_ID = 18
|
||||
|
||||
CUSTOM_SUBPACKET = subpacket(100, b'TREZOR-GPG') # marks "our" pubkey
|
||||
CUSTOM_KEY_LABEL = b'TREZOR-GPG' # marks "our" pubkey
|
||||
CUSTOM_SUBPACKET_ID = 26 # use "policy URL" subpacket
|
||||
CUSTOM_SUBPACKET = subpacket(CUSTOM_SUBPACKET_ID, CUSTOM_KEY_LABEL)
|
||||
|
||||
|
||||
def get_curve_name_by_oid(oid):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import glob
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
|
||||
@@ -31,84 +30,6 @@ def test_mpi():
|
||||
assert decode.parse_mpis(util.Reader(s), n=2) == [0x123, 5]
|
||||
|
||||
|
||||
def assert_subdict(d, s):
|
||||
for k, v in s.items():
|
||||
assert d[k] == v
|
||||
|
||||
|
||||
def test_primary_nist256p1():
|
||||
# pylint: disable=line-too-long
|
||||
data = b'''-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v2
|
||||
|
||||
mFIEV0hI1hMIKoZIzj0DAQcCAwTXY7aq01xMPSU7gTHU9B7Z2CFoCk1Y4WYb8Tiy
|
||||
hurvIZ5la6+UEgAKF9HXpQo0yE+HQOgufoLlCpdE7NoEUb+HtAd0ZXN0aW5niHYE
|
||||
ExMIABIFAldISNYCGwMCFQgCFgACF4AAFgkQTcCehfpEIPILZFRSRVpPUi1HUEeV
|
||||
3QEApHKmBkbLVZNpsB8q9mBzKytxnOHNB3QWDuoKJu/ERi4A/1wRGZ/B0BDazHck
|
||||
zpR9luXTKwMEl+mlZmwEFKZXBmir
|
||||
=oyj0
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
'''
|
||||
stream = io.BytesIO(decode.remove_armor(data))
|
||||
pubkey, user_id, signature = list(decode.parse_packets(stream))
|
||||
expected_pubkey = {
|
||||
'created': 1464355030, 'type': 'pubkey', 'tag': 6,
|
||||
'version': 4, 'algo': 19, 'key_id': b'M\xc0\x9e\x85\xfaD \xf2',
|
||||
'_to_hash': b'\x99\x00R\x04WHH\xd6\x13\x08*\x86H\xce=\x03\x01\x07\x02\x03\x04\xd7c\xb6\xaa\xd3\\L=%;\x811\xd4\xf4\x1e\xd9\xd8!h\nMX\xe1f\x1b\xf18\xb2\x86\xea\xef!\x9eek\xaf\x94\x12\x00\n\x17\xd1\xd7\xa5\n4\xc8O\x87@\xe8.~\x82\xe5\n\x97D\xec\xda\x04Q\xbf\x87' # nopep8
|
||||
}
|
||||
assert_subdict(pubkey, expected_pubkey)
|
||||
point = pubkey['verifying_key'].pubkey.point
|
||||
assert point.x(), point.y() == (
|
||||
97423441028100245505102979561460969898742433559010922791700160771755342491425,
|
||||
71644624850142103522769833619875243486871666152651730678601507641225861250951
|
||||
)
|
||||
assert_subdict(user_id, {
|
||||
'tag': 13, 'type': 'user_id', 'value': b'testing',
|
||||
'_to_hash': b'\xb4\x00\x00\x00\x07testing'
|
||||
})
|
||||
assert_subdict(signature, {
|
||||
'pubkey_alg': 19, '_is_custom': True, 'hash_alg': 8, 'tag': 2,
|
||||
'sig_type': 19, 'version': 4, 'type': 'signature', 'hash_prefix': b'\x95\xdd',
|
||||
'sig': (74381873592149178031432444136130575481350858387410643140628758456112511206958,
|
||||
41642995320462795718437755373080464775445470754419831653624197847615308982443),
|
||||
'hashed_subpackets': [b'\x02WHH\xd6', b'\x1b\x03', b'\x15\x08', b'\x16\x00', b'\x17\x80'],
|
||||
'unhashed_subpackets': [b'\x10M\xc0\x9e\x85\xfaD \xf2', b'dTREZOR-GPG'],
|
||||
'_to_hash': b'\x04\x13\x13\x08\x00\x12\x05\x02WHH\xd6\x02\x1b\x03\x02\x15\x08\x02\x16\x00\x02\x17\x80\x04\xff\x00\x00\x00\x18' # nopep8
|
||||
})
|
||||
|
||||
digest = decode.digest_packets(packets=[pubkey, user_id, signature],
|
||||
hasher=hashlib.sha256())
|
||||
decode.verify_digest(pubkey=pubkey, digest=digest,
|
||||
signature=signature['sig'],
|
||||
label='GPG primary public key')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
bad_digest = b'\x00' * len(digest)
|
||||
decode.verify_digest(pubkey=pubkey, digest=bad_digest,
|
||||
signature=signature['sig'],
|
||||
label='GPG primary public key')
|
||||
|
||||
message = b'Hello, World!\n'
|
||||
signature = b'''-----BEGIN PGP SIGNATURE-----
|
||||
Version: GnuPG v2
|
||||
|
||||
iF4EABMIAAYFAldIlfQACgkQTcCehfpEIPKOUgD9FjaeWla4wOuDZ7P6fhkT5nZp
|
||||
KDQU0N5KmNwLlt2kwo4A/jQkBII2cI8tTqOVTLNRXXqIOsMf/fG4jKM/VOFc/01c
|
||||
=dC+z
|
||||
-----END PGP SIGNATURE-----
|
||||
'''
|
||||
decode.verify(pubkey=pubkey, signature=signature, original_data=message)
|
||||
|
||||
pubkey = decode.load_public_key(pubkey_bytes=decode.remove_armor(data))
|
||||
assert_subdict(pubkey, expected_pubkey)
|
||||
assert_subdict(pubkey, {'user_id': b'testing'})
|
||||
|
||||
pubkey = decode.load_public_key(pubkey_bytes=decode.remove_armor(data),
|
||||
use_custom=True)
|
||||
assert_subdict(pubkey, expected_pubkey)
|
||||
assert_subdict(pubkey, {'user_id': b'testing'})
|
||||
|
||||
|
||||
cwd = os.path.join(os.path.dirname(__file__))
|
||||
input_files = glob.glob(os.path.join(cwd, '*.gpg'))
|
||||
|
||||
@@ -120,5 +41,5 @@ def public_key_path(request):
|
||||
|
||||
def test_gpg_files(public_key_path): # pylint: disable=redefined-outer-name
|
||||
with open(public_key_path, 'rb') as f:
|
||||
keys = decode.parse_public_keys(f)
|
||||
assert len(keys) > 0
|
||||
packets = list(decode.parse_packets(f))
|
||||
assert len(packets) > 0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import io
|
||||
|
||||
import mock
|
||||
|
||||
from .. import keyring
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""UNIX-domain socket server for ssh-agent implementation."""
|
||||
import contextlib
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
@@ -42,19 +43,24 @@ def unix_domain_socket_server(sock_path):
|
||||
remove_file(sock_path)
|
||||
|
||||
|
||||
def handle_connection(conn, handler):
|
||||
def handle_connection(conn, handler, mutex):
|
||||
"""
|
||||
Handle a single connection using the specified protocol handler in a loop.
|
||||
|
||||
Since this function may be called concurrently from server_thread,
|
||||
the specified mutex is used to synchronize the device handling.
|
||||
|
||||
Exit when EOFError is raised.
|
||||
All other exceptions are logged as warnings.
|
||||
"""
|
||||
try:
|
||||
log.debug('welcome agent')
|
||||
while True:
|
||||
msg = util.read_frame(conn)
|
||||
reply = handler.handle(msg=msg)
|
||||
util.send(conn, reply)
|
||||
with contextlib.closing(conn):
|
||||
while True:
|
||||
msg = util.read_frame(conn)
|
||||
with mutex:
|
||||
reply = handler.handle(msg=msg)
|
||||
util.send(conn, reply)
|
||||
except EOFError:
|
||||
log.debug('goodbye agent')
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
@@ -77,7 +83,7 @@ def retry(func, exception_type, quit_event):
|
||||
pass
|
||||
|
||||
|
||||
def server_thread(sock, handler, quit_event):
|
||||
def server_thread(sock, handle_conn, quit_event):
|
||||
"""Run a server on the specified socket."""
|
||||
log.debug('server thread started')
|
||||
|
||||
@@ -93,8 +99,9 @@ def server_thread(sock, handler, quit_event):
|
||||
except StopIteration:
|
||||
log.debug('server stopped')
|
||||
break
|
||||
with contextlib.closing(conn):
|
||||
handle_connection(conn, handler)
|
||||
# Handle connections from SSH concurrently.
|
||||
threading.Thread(target=handle_conn,
|
||||
kwargs=dict(conn=conn)).start()
|
||||
log.debug('server thread stopped')
|
||||
|
||||
|
||||
@@ -115,14 +122,23 @@ def serve(handler, sock_path=None, timeout=UNIX_SOCKET_TIMEOUT):
|
||||
If no connection is made during the specified timeout,
|
||||
retry until the context is over.
|
||||
"""
|
||||
ssh_version = subprocess.check_output(['ssh', '-V'],
|
||||
stderr=subprocess.STDOUT)
|
||||
log.debug('local SSH version: %r', ssh_version)
|
||||
if sock_path is None:
|
||||
sock_path = tempfile.mktemp(prefix='ssh-agent-')
|
||||
|
||||
environ = {'SSH_AUTH_SOCK': sock_path, 'SSH_AGENT_PID': str(os.getpid())}
|
||||
device_mutex = threading.Lock()
|
||||
with unix_domain_socket_server(sock_path) as sock:
|
||||
sock.settimeout(timeout)
|
||||
quit_event = threading.Event()
|
||||
kwargs = dict(sock=sock, handler=handler, quit_event=quit_event)
|
||||
handle_conn = functools.partial(handle_connection,
|
||||
handler=handler,
|
||||
mutex=device_mutex)
|
||||
kwargs = dict(sock=sock,
|
||||
handle_conn=handle_conn,
|
||||
quit_event=quit_event)
|
||||
with spawn(server_thread, kwargs):
|
||||
try:
|
||||
yield environ
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
import socket
|
||||
@@ -37,30 +38,32 @@ class FakeSocket(object):
|
||||
|
||||
|
||||
def test_handle():
|
||||
mutex = threading.Lock()
|
||||
|
||||
handler = protocol.Handler(keys=[], signer=None)
|
||||
conn = FakeSocket()
|
||||
server.handle_connection(conn, handler)
|
||||
server.handle_connection(conn, handler, mutex)
|
||||
|
||||
msg = bytearray([protocol.msg_code('SSH_AGENTC_REQUEST_RSA_IDENTITIES')])
|
||||
conn = FakeSocket(util.frame(msg))
|
||||
server.handle_connection(conn, handler)
|
||||
server.handle_connection(conn, handler, mutex)
|
||||
assert conn.tx.getvalue() == b'\x00\x00\x00\x05\x02\x00\x00\x00\x00'
|
||||
|
||||
msg = bytearray([protocol.msg_code('SSH2_AGENTC_REQUEST_IDENTITIES')])
|
||||
conn = FakeSocket(util.frame(msg))
|
||||
server.handle_connection(conn, handler)
|
||||
server.handle_connection(conn, handler, mutex)
|
||||
assert conn.tx.getvalue() == b'\x00\x00\x00\x05\x0C\x00\x00\x00\x00'
|
||||
|
||||
msg = bytearray([protocol.msg_code('SSH2_AGENTC_ADD_IDENTITY')])
|
||||
conn = FakeSocket(util.frame(msg))
|
||||
server.handle_connection(conn, handler)
|
||||
server.handle_connection(conn, handler, mutex)
|
||||
conn.tx.seek(0)
|
||||
reply = util.read_frame(conn.tx)
|
||||
assert reply == util.pack('B', protocol.msg_code('SSH_AGENT_FAILURE'))
|
||||
|
||||
conn_mock = mock.Mock(spec=FakeSocket)
|
||||
conn_mock.recv.side_effect = [Exception, EOFError]
|
||||
server.handle_connection(conn=conn_mock, handler=None)
|
||||
server.handle_connection(conn=conn_mock, handler=None, mutex=mutex)
|
||||
|
||||
|
||||
def test_server_thread():
|
||||
@@ -78,8 +81,10 @@ def test_server_thread():
|
||||
def getsockname(self): # pylint: disable=no-self-use
|
||||
return 'fake_server'
|
||||
|
||||
handler = protocol.Handler(keys=[], signer=None),
|
||||
handle_conn = functools.partial(server.handle_connection, handler=handler)
|
||||
server.server_thread(sock=FakeServer(),
|
||||
handler=protocol.Handler(keys=[], signer=None),
|
||||
handle_conn=handle_conn,
|
||||
quit_event=quit_event)
|
||||
|
||||
|
||||
|
||||
@@ -227,3 +227,12 @@ def get_bip32_address(identity, ecdh=False):
|
||||
addr_0 = [13, 17][bool(ecdh)]
|
||||
address_n = [addr_0] + list(recv(s, '<LLLL'))
|
||||
return [(hardened | value) for value in address_n]
|
||||
|
||||
|
||||
def setup_logging(verbosity, **kwargs):
|
||||
"""Configure logging for this tool."""
|
||||
fmt = ('%(asctime)s %(levelname)-12s %(message)-100s '
|
||||
'[%(filename)s:%(lineno)d]')
|
||||
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
|
||||
level = levels[min(verbosity, len(levels) - 1)]
|
||||
logging.basicConfig(format=fmt, level=level, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user