From d9b07e2ac6a4731af7a55e0c20bd62a665e77858 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 28 May 2016 23:02:45 +0300 Subject: [PATCH 01/37] gpg: hack agent prototype --- trezor_agent/gpg/__main__.py | 2 +- trezor_agent/gpg/agent.py | 97 ++++++++++++++++++++++++++++++++++++ trezor_agent/gpg/keyring.py | 20 ++++---- 3 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 trezor_agent/gpg/agent.py diff --git a/trezor_agent/gpg/__main__.py b/trezor_agent/gpg/__main__.py index 01d4568..ffccca5 100755 --- a/trezor_agent/gpg/__main__.py +++ b/trezor_agent/gpg/__main__.py @@ -3,9 +3,9 @@ import argparse import contextlib import logging +import os import sys import time -import os from . import decode, encode, keyring, proto diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py new file mode 100644 index 0000000..58dcc53 --- /dev/null +++ b/trezor_agent/gpg/agent.py @@ -0,0 +1,97 @@ +import binascii +import contextlib +import hashlib +import logging +import os +import select +import subprocess +import threading + +import ecdsa + +from . import keyring +from .. import server, util + +log = logging.getLogger(__name__) + + +def yield_connections(sock): + """Run a server on the specified socket.""" + while True: + log.debug('waiting for connection on %s', sock.getsockname()) + conn, _ = sock.accept() + conn.settimeout(None) + log.debug('accepted connection on %s', sock.getsockname()) + yield conn + + +def serialize(data): + for c in ['%', '\n', '\r']: + data = data.replace(c, '%{:02X}'.format(ord(c))) + return data + + +def sig_encode(r, s, o): + r = serialize(util.num2bytes(r, 32)) + s = serialize(util.num2bytes(s, 32)) + return '(7:sig-val(5:ecdsa(1:r32:{})(1:s32:{})))\n'.format(r, s) + + +def pksign(keygrip, digest): + pk = 0x99ddae8aee45de830e08889f76ce1f7f993d80a1a05e843ae950b5ba62c16efb + sk = ecdsa.SigningKey.from_secret_exponent(pk, curve=ecdsa.NIST256p, + hashfunc=hashlib.sha256) + digest = binascii.unhexlify(digest) + result = sk.sign_digest_deterministic(digest, hashfunc=hashlib.sha256, + sigencode=sig_encode) + log.debug('result: %r', result) + return result + + +def handle_connection(conn): + keygrip = None + digest = None + algo = None + + conn.sendall('OK\n') + while True: + line = keyring.recvline(conn) + parts = line.split(' ') + command = parts[0] + args = parts[1:] + if command in {'RESET', 'OPTION', 'HAVEKEY', 'SETKEYDESC'}: + pass # reply with OK + elif command == 'GETINFO': + conn.sendall('D 2.1.11\n') + elif command == 'AGENT_ID': + conn.sendall('D TREZOR\n') + elif command == 'SIGKEY': + keygrip, = args + elif command == 'SETHASH': + algo, digest = args + elif command == 'PKSIGN': + sig = pksign(keygrip, digest) + conn.sendall('D ' + sig) + else: + log.error('unknown request: %r', line) + return + + conn.sendall('OK\n') + + +def main(): + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)-10s %(message)s') + + sock_path = os.path.expanduser('~/.gnupg/S.gpg-agent') + with server.unix_domain_socket_server(sock_path) as sock: + for conn in yield_connections(sock): + with contextlib.closing(conn): + try: + handle_connection(conn) + except EOFError: + break + + +if __name__ == '__main__': + main() diff --git a/trezor_agent/gpg/keyring.py b/trezor_agent/gpg/keyring.py index f5053dc..1f540ed 100644 --- a/trezor_agent/gpg/keyring.py +++ b/trezor_agent/gpg/keyring.py @@ -22,14 +22,14 @@ def connect_to_agent(sock_path='~/.gnupg/S.gpg-agent', sp=subprocess): return sock -def _communicate(sock, msg): +def communicate(sock, msg): msg += '\n' sock.sendall(msg.encode('ascii')) log.debug('-> %r', msg) - return _recvline(sock) + return recvline(sock) -def _recvline(sock): +def recvline(sock): reply = io.BytesIO() while True: @@ -112,7 +112,7 @@ def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None): hash_algo = 8 # SHA256 assert len(digest) == 32 - assert _communicate(sock, 'RESET').startswith(b'OK') + assert communicate(sock, 'RESET').startswith(b'OK') ttyname = sp.check_output(['tty']).strip() options = ['ttyname={}'.format(ttyname)] # set TTY for passphrase entry @@ -122,17 +122,17 @@ def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None): options.append('display={}'.format(display)) for opt in options: - assert _communicate(sock, 'OPTION {}'.format(opt)) == b'OK' + assert communicate(sock, 'OPTION {}'.format(opt)) == b'OK' - assert _communicate(sock, 'SIGKEY {}'.format(keygrip)) == b'OK' + assert communicate(sock, 'SIGKEY {}'.format(keygrip)) == b'OK' hex_digest = binascii.hexlify(digest).upper().decode('ascii') - assert _communicate(sock, 'SETHASH {} {}'.format(hash_algo, + assert communicate(sock, 'SETHASH {} {}'.format(hash_algo, hex_digest)) == b'OK' - assert _communicate(sock, 'SETKEYDESC ' + assert communicate(sock, 'SETKEYDESC ' 'Sign+a+new+TREZOR-based+subkey') == b'OK' - assert _communicate(sock, 'PKSIGN') == b'OK' - line = _recvline(sock).strip() + assert communicate(sock, 'PKSIGN') == b'OK' + line = recvline(sock).strip() line = unescape(line) log.debug('unescaped: %r', line) prefix, sig = line.split(b' ', 1) From 92649b290f6b071c4f85bcadd914ce3c51f336c1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 30 May 2016 17:49:21 +0300 Subject: [PATCH 02/37] HACK: add preliminary gpg support --- trezor_agent/gpg/__main__.py | 39 +------------------------------ trezor_agent/gpg/agent.py | 41 +++++++++++++++++--------------- trezor_agent/gpg/encode.py | 8 +++---- trezor_agent/gpg/keyring.py | 6 +++-- trezor_agent/gpg/proto.py | 45 +++++++++++++++++++++++++++++++++--- 5 files changed, 72 insertions(+), 67 deletions(-) diff --git a/trezor_agent/gpg/__main__.py b/trezor_agent/gpg/__main__.py index ffccca5..2db9149 100755 --- a/trezor_agent/gpg/__main__.py +++ b/trezor_agent/gpg/__main__.py @@ -7,7 +7,7 @@ import os import sys import time -from . import decode, encode, keyring, proto +from . import encode, keyring, proto log = logging.getLogger(__name__) @@ -28,36 +28,6 @@ def run_create(args): sys.stdout.write(proto.armor(result, 'PUBLIC KEY BLOCK')) -def run_sign(args): - """Generate a GPG signature using hardware-based device.""" - pubkey = decode.load_public_key(keyring.export_public_key(user_id=None), - use_custom=True) - f = encode.Factory.from_public_key(pubkey=pubkey, - user_id=pubkey['user_id']) - with contextlib.closing(f): - if args.filename: - data = open(args.filename, 'rb').read() - else: - data = sys.stdin.read() - sig = f.sign_message(data) - - sig = proto.armor(sig, 'SIGNATURE').encode('ascii') - decode.verify(pubkey=pubkey, signature=sig, original_data=data) - - filename = '-' # write to stdout - if args.output: - filename = args.output - elif args.filename: - filename = args.filename + '.asc' - - if filename == '-': - output = sys.stdout - else: - output = open(filename, 'wb') - - output.write(sig) - - def main(): """Main function.""" p = argparse.ArgumentParser() @@ -72,13 +42,6 @@ def main(): create.add_argument('-t', '--time', type=int, default=int(time.time())) create.set_defaults(run=run_create) - sign = subparsers.add_parser('sign') - sign.add_argument('filename', nargs='?', - help='Use stdin, if not specified.') - sign.add_argument('-o', '--output', default=None, - help='Use stdout, if equals to "-".') - sign.set_defaults(run=run_sign) - args = p.parse_args() logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format='%(asctime)s %(levelname)-10s %(message)s') diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 58dcc53..d79bfbb 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -1,15 +1,10 @@ +"""GPG-agent utilities.""" import binascii import contextlib -import hashlib import logging import os -import select -import subprocess -import threading -import ecdsa - -from . import keyring +from . import decode, encode, keyring from .. import server, util log = logging.getLogger(__name__) @@ -26,29 +21,36 @@ 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))) return data -def sig_encode(r, s, o): +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:{})))\n'.format(r, s) -def pksign(keygrip, digest): - pk = 0x99ddae8aee45de830e08889f76ce1f7f993d80a1a05e843ae950b5ba62c16efb - sk = ecdsa.SigningKey.from_secret_exponent(pk, curve=ecdsa.NIST256p, - hashfunc=hashlib.sha256) - digest = binascii.unhexlify(digest) - result = sk.sign_digest_deterministic(digest, hashfunc=hashlib.sha256, - sigencode=sig_encode) - log.debug('result: %r', result) - return result +def pksign(keygrip, digest, algo): + """Sign a message digest using a private EC key.""" + assert algo == '8' + pubkey = decode.load_public_key(keyring.export_public_key(user_id=None), + use_custom=True) + f = encode.Factory.from_public_key(pubkey=pubkey, + user_id=pubkey['user_id']) + with contextlib.closing(f): + assert f.pubkey.keygrip == binascii.unhexlify(keygrip) + r, s = f.conn.sign(binascii.unhexlify(digest)) + result = sig_encode(r, s) + log.debug('result: %r', result) + return result def handle_connection(conn): + """Handle connection from GPG binary using the ASSUAN protocol.""" keygrip = None digest = None algo = None @@ -70,7 +72,7 @@ def handle_connection(conn): elif command == 'SETHASH': algo, digest = args elif command == 'PKSIGN': - sig = pksign(keygrip, digest) + sig = pksign(keygrip, digest, algo) conn.sendall('D ' + sig) else: log.error('unknown request: %r', line) @@ -80,7 +82,8 @@ def handle_connection(conn): def main(): - logging.basicConfig(level=logging.DEBUG, + """Run a simple GPG-agent server.""" + logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)-10s %(message)s') sock_path = os.path.expanduser('~/.gnupg/S.gpg-agent') diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index fd47199..08b80d0 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -38,8 +38,7 @@ class HardwareSigner(object): ecdsa_curve_name=self.curve_name) assert result.signature[:1] == b'\x00' sig = result.signature[1:] - return (proto.mpi(util.bytes2num(sig[:32])) + - proto.mpi(util.bytes2num(sig[32:]))) + return (util.bytes2num(sig[:32]), util.bytes2num(sig[32:])) def close(self): """Close the connection to the device.""" @@ -57,9 +56,8 @@ class AgentSigner(object): def sign(self, digest): """Sign the digest and return an ECDSA/RSA/DSA signature.""" - params = keyring.sign_digest(sock=self.sock, - keygrip=self.keygrip, digest=digest) - return b''.join(proto.mpi(p) for p in params) + return keyring.sign_digest(sock=self.sock, + keygrip=self.keygrip, digest=digest) def close(self): """Close the connection to gpg-agent.""" diff --git a/trezor_agent/gpg/keyring.py b/trezor_agent/gpg/keyring.py index 1f540ed..418f0bf 100644 --- a/trezor_agent/gpg/keyring.py +++ b/trezor_agent/gpg/keyring.py @@ -23,6 +23,7 @@ def connect_to_agent(sock_path='~/.gnupg/S.gpg-agent', sp=subprocess): def communicate(sock, msg): + """Send a message and receive a single line.""" msg += '\n' sock.sendall(msg.encode('ascii')) log.debug('-> %r', msg) @@ -30,6 +31,7 @@ def communicate(sock, msg): def recvline(sock): + """Receive a single line from the socket.""" reply = io.BytesIO() while True: @@ -127,10 +129,10 @@ def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None): assert communicate(sock, 'SIGKEY {}'.format(keygrip)) == b'OK' hex_digest = binascii.hexlify(digest).upper().decode('ascii') assert communicate(sock, 'SETHASH {} {}'.format(hash_algo, - hex_digest)) == b'OK' + hex_digest)) == b'OK' assert communicate(sock, 'SETKEYDESC ' - 'Sign+a+new+TREZOR-based+subkey') == b'OK' + 'Sign+a+new+TREZOR-based+subkey') == b'OK' assert communicate(sock, 'PKSIGN') == b'OK' line = recvline(sock).strip() line = unescape(line) diff --git a/trezor_agent/gpg/proto.py b/trezor_agent/gpg/proto.py index 6e84a8d..0fd63ac 100644 --- a/trezor_agent/gpg/proto.py +++ b/trezor_agent/gpg/proto.py @@ -77,17 +77,54 @@ def _serialize_ed25519(vk): util.bytes2num(vk.to_bytes())) +def _compute_keygrip(params): + exp = ''.join('(1:{}{}:{})'.format(name, len(value), value) + for name, value in params) + return hashlib.sha1(exp).digest() + + +def _keygrip_nist256(vk): + curve = vk.curve.curve + gen = vk.curve.generator + g = (4 << 512) | (gen.x() << 256) | gen.y() + point = vk.pubkey.point + q = (4 << 512) | (point.x() << 256) | point.y() + + return _compute_keygrip([ + ['p', util.num2bytes(curve.p(), size=32)], + ['a', util.num2bytes(curve.a() % curve.p(), size=32)], + ['b', util.num2bytes(curve.b() % curve.p(), size=32)], + ['g', util.num2bytes(g, size=65)], + ['n', util.num2bytes(vk.curve.order, size=32)], + ['q', util.num2bytes(q, size=65)], + ]) + + +def _keygrip_ed25519(vk): + # pylint: disable=line-too-long + return _compute_keygrip([ + ['p', util.num2bytes(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED, size=32)], # nopep8 + ['a', b'\x01'], + ['b', util.num2bytes(0x2DFC9311D490018C7338BF8688861767FF8FF5B2BEBE27548A14B235ECA6874A, size=32)], # nopep8 + ['g', util.num2bytes(0x04216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A6666666666666666666666666666666666666666666666666666666666666658, size=65)], # nopep8 + ['n', util.num2bytes(0x1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED, size=32)], # nopep8 + ['q', vk.to_bytes()], + ]) + + SUPPORTED_CURVES = { formats.CURVE_NIST256: { # https://tools.ietf.org/html/rfc6637#section-11 'oid': b'\x2A\x86\x48\xCE\x3D\x03\x01\x07', 'algo_id': 19, - 'serialize': _serialize_nist256 + 'serialize': _serialize_nist256, + 'keygrip': _keygrip_nist256, }, formats.CURVE_ED25519: { 'oid': b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01', 'algo_id': 22, - 'serialize': _serialize_ed25519 + 'serialize': _serialize_ed25519, + 'keygrip': _keygrip_ed25519, } } @@ -111,6 +148,7 @@ class PublicKey(object): self.created = int(created) # time since Epoch self.verifying_key = verifying_key self.algo_id = self.curve_info['algo_id'] + self.keygrip = self.curve_info['keygrip'](verifying_key) hex_key_id = util.hexlify(self.key_id())[-8:] self.desc = 'GPG public key {}/{}'.format(curve_name, hex_key_id) @@ -175,7 +213,8 @@ def make_signature(signer_func, data_to_sign, public_algo, log.debug('hashing %d bytes', len(data_to_hash)) digest = hashlib.sha256(data_to_hash).digest() log.debug('signing digest: %s', util.hexlify(digest)) - sig = signer_func(digest=digest) + params = signer_func(digest=digest) + sig = b''.join(mpi(p) for p in params) return bytes(header + hashed + unhashed + digest[:2] + # used for decoder's sanity check From f89c5bb1251dc8966ae58af2f1d05b0036193e1b Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 1 Jun 2016 18:34:22 +0300 Subject: [PATCH 03/37] HACK: better logging --- trezor_agent/gpg/agent.py | 12 ++++++------ trezor_agent/gpg/keyring.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index d79bfbb..4d2841e 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -31,7 +31,7 @@ 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:{})))\n'.format(r, s) + return '(7:sig-val(5:ecdsa(1:r32:{})(1:s32:{})))'.format(r, s) def pksign(keygrip, digest, algo): @@ -55,7 +55,7 @@ def handle_connection(conn): digest = None algo = None - conn.sendall('OK\n') + keyring.sendline(conn, b'OK') while True: line = keyring.recvline(conn) parts = line.split(' ') @@ -64,21 +64,21 @@ def handle_connection(conn): if command in {'RESET', 'OPTION', 'HAVEKEY', 'SETKEYDESC'}: pass # reply with OK elif command == 'GETINFO': - conn.sendall('D 2.1.11\n') + keyring.sendline(conn, b'D 2.1.11') elif command == 'AGENT_ID': - conn.sendall('D TREZOR\n') + keyring.sendline(conn, b'D TREZOR') elif command == 'SIGKEY': keygrip, = args elif command == 'SETHASH': algo, digest = args elif command == 'PKSIGN': sig = pksign(keygrip, digest, algo) - conn.sendall('D ' + sig) + keyring.sendline(conn, b'D ' + sig) else: log.error('unknown request: %r', line) return - conn.sendall('OK\n') + keyring.sendline(conn, b'OK') def main(): diff --git a/trezor_agent/gpg/keyring.py b/trezor_agent/gpg/keyring.py index 418f0bf..22d3deb 100644 --- a/trezor_agent/gpg/keyring.py +++ b/trezor_agent/gpg/keyring.py @@ -25,11 +25,16 @@ def connect_to_agent(sock_path='~/.gnupg/S.gpg-agent', sp=subprocess): def communicate(sock, msg): """Send a message and receive a single line.""" msg += '\n' - sock.sendall(msg.encode('ascii')) - log.debug('-> %r', msg) + sendline(sock, msg.encode('ascii')) return recvline(sock) +def sendline(sock, msg): + """Send a binary message, followed by EOL.""" + log.debug('<- %r', msg) + sock.sendall(msg + b'\n') + + def recvline(sock): """Receive a single line from the socket.""" reply = io.BytesIO() @@ -43,7 +48,7 @@ def recvline(sock): reply.write(c) result = reply.getvalue() - log.debug('<- %r', result) + log.debug('-> %r', result) return result From 39cb5565bf5c8c9ad45e279e93a974ee1ed07251 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 2 Jun 2016 21:39:14 +0300 Subject: [PATCH 04/37] HACK: better line iteration --- trezor_agent/gpg/agent.py | 16 ++++++++++------ trezor_agent/gpg/keyring.py | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 4d2841e..56c3897 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -49,6 +49,14 @@ def pksign(keygrip, digest, algo): return result +def iterlines(conn): + while True: + line = keyring.recvline(conn) + if line is None: + break + yield line + + def handle_connection(conn): """Handle connection from GPG binary using the ASSUAN protocol.""" keygrip = None @@ -56,8 +64,7 @@ def handle_connection(conn): algo = None keyring.sendline(conn, b'OK') - while True: - line = keyring.recvline(conn) + for line in iterlines(conn): parts = line.split(' ') command = parts[0] args = parts[1:] @@ -90,10 +97,7 @@ def main(): with server.unix_domain_socket_server(sock_path) as sock: for conn in yield_connections(sock): with contextlib.closing(conn): - try: - handle_connection(conn) - except EOFError: - break + handle_connection(conn) if __name__ == '__main__': diff --git a/trezor_agent/gpg/keyring.py b/trezor_agent/gpg/keyring.py index 22d3deb..5b0a513 100644 --- a/trezor_agent/gpg/keyring.py +++ b/trezor_agent/gpg/keyring.py @@ -42,7 +42,8 @@ def recvline(sock): while True: c = sock.recv(1) if not c: - raise EOFError + return None # socket is closed + if c == b'\n': break reply.write(c) From 7da7f5c25681e2a50706dfd407f427cbe85c8f7b Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 2 Jun 2016 22:25:44 +0300 Subject: [PATCH 05/37] HACK: fixup tests --- trezor_agent/gpg/agent.py | 1 + trezor_agent/gpg/keyring.py | 1 - trezor_agent/gpg/proto.py | 9 ++++++--- trezor_agent/gpg/tests/test_proto.py | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 56c3897..b8eb4d7 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -50,6 +50,7 @@ def pksign(keygrip, digest, algo): def iterlines(conn): + """Iterate over input, split by lines.""" while True: line = keyring.recvline(conn) if line is None: diff --git a/trezor_agent/gpg/keyring.py b/trezor_agent/gpg/keyring.py index 5b0a513..4e33f74 100644 --- a/trezor_agent/gpg/keyring.py +++ b/trezor_agent/gpg/keyring.py @@ -24,7 +24,6 @@ def connect_to_agent(sock_path='~/.gnupg/S.gpg-agent', sp=subprocess): def communicate(sock, msg): """Send a message and receive a single line.""" - msg += '\n' sendline(sock, msg.encode('ascii')) return recvline(sock) diff --git a/trezor_agent/gpg/proto.py b/trezor_agent/gpg/proto.py index 0fd63ac..ab46fc3 100644 --- a/trezor_agent/gpg/proto.py +++ b/trezor_agent/gpg/proto.py @@ -78,9 +78,12 @@ def _serialize_ed25519(vk): def _compute_keygrip(params): - exp = ''.join('(1:{}{}:{})'.format(name, len(value), value) - for name, value in params) - return hashlib.sha1(exp).digest() + parts = [] + for name, value in params: + exp = '1:{}{}'.format(name, len(value)) + parts.append(b'(' + exp.encode('ascii') + value + b')') + + return hashlib.sha1(b''.join(parts)).digest() def _keygrip_nist256(vk): diff --git a/trezor_agent/gpg/tests/test_proto.py b/trezor_agent/gpg/tests/test_proto.py index 350fdf8..87e9d83 100644 --- a/trezor_agent/gpg/tests/test_proto.py +++ b/trezor_agent/gpg/tests/test_proto.py @@ -55,7 +55,7 @@ def test_make_signature(): def signer_func(digest): assert digest == (b'\xd0\xe5]|\x8bP\xe6\x91\xb3\xe8+\xf4A\xf0`(\xb1' b'\xc7\xf4;\x86\x97s\xdb\x9a\xda\xee< \xcb\x9e\x00') - return b'SIGNATURE' + return (7, 8) sig = proto.make_signature( signer_func=signer_func, @@ -65,7 +65,7 @@ def test_make_signature(): unhashed_subpackets=[], sig_type=25) assert sig == (b'\x04\x19\x16\x08\x00\x06\x05\x02' - b'\x00\x00\x00\x01\x00\x00\xd0\xe5SIGNATURE') + b'\x00\x00\x00\x01\x00\x00\xd0\xe5\x00\x03\x07\x00\x04\x08') def test_nist256p1(): From 49c343df9490ca12db32d0ab050ea0b5863815ea Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 2 Jun 2016 22:54:01 +0300 Subject: [PATCH 06/37] HACK: create subkey with ECDH support --- trezor_agent/gpg/__main__.py | 3 ++- trezor_agent/gpg/encode.py | 51 ++++++++++++++++++++++-------------- trezor_agent/gpg/proto.py | 13 ++++++--- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/trezor_agent/gpg/__main__.py b/trezor_agent/gpg/__main__.py index 2db9149..19f861b 100755 --- a/trezor_agent/gpg/__main__.py +++ b/trezor_agent/gpg/__main__.py @@ -16,7 +16,7 @@ def run_create(args): """Generate a new pubkey for a new/existing GPG identity.""" user_id = os.environ['TREZOR_GPG_USER_ID'] f = encode.Factory(user_id=user_id, created=args.time, - curve_name=args.ecdsa_curve) + curve_name=args.ecdsa_curve, ecdh=args.ecdh) with contextlib.closing(f): if args.subkey: @@ -38,6 +38,7 @@ def main(): create = subparsers.add_parser('create') create.add_argument('-s', '--subkey', action='store_true', default=False) + create.add_argument('--ecdh', action='store_true', default=False) create.add_argument('-e', '--ecdsa-curve', default='nist256p1') create.add_argument('-t', '--time', type=int, default=int(time.time())) create.set_defaults(run=run_create) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 08b80d0..3344deb 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -71,7 +71,7 @@ def _time_format(t): class Factory(object): """Performs GPG signing operations.""" - def __init__(self, user_id, created, curve_name): + def __init__(self, user_id, created, curve_name, ecdh=False): """Construct and loads a public key from the device.""" self.user_id = user_id assert curve_name in formats.SUPPORTED_CURVES @@ -79,7 +79,8 @@ class Factory(object): self.conn = HardwareSigner(user_id, curve_name=curve_name) self.pubkey = proto.PublicKey( curve_name=curve_name, created=created, - verifying_key=self.conn.pubkey()) + verifying_key=self.conn.pubkey(), ecdh=ecdh) + self.ecdh = ecdh log.info('%s created at %s for "%s"', self.pubkey, _time_format(self.pubkey.created), user_id) @@ -142,28 +143,38 @@ class Factory(object): self.user_id, util.hexlify(primary['key_id'])) data_to_sign = primary['_to_hash'] + self.pubkey.data_to_hash() - # Primary Key Binding Signature - hashed_subpackets = [ - proto.subpacket_time(self.pubkey.created)] # signature time - unhashed_subpackets = [ - proto.subpacket(16, self.pubkey.key_id())] # issuer key id - log.info('confirm signing subkey with hardware device') - embedded_sig = proto.make_signature( - signer_func=self.conn.sign, - data_to_sign=data_to_sign, - public_algo=self.pubkey.algo_id, - sig_type=0x19, - hashed_subpackets=hashed_subpackets, - unhashed_subpackets=unhashed_subpackets) + if self.ecdh: + embedded_sig = None + else: + # Primary Key Binding Signature + hashed_subpackets = [ + proto.subpacket_time(self.pubkey.created)] # signature time + unhashed_subpackets = [ + proto.subpacket(16, self.pubkey.key_id())] # issuer key id + log.info('confirm signing subkey with hardware device') + embedded_sig = proto.make_signature( + signer_func=self.conn.sign, + data_to_sign=data_to_sign, + public_algo=self.pubkey.algo_id, + sig_type=0x19, + hashed_subpackets=hashed_subpackets, + unhashed_subpackets=unhashed_subpackets) # Subkey Binding Signature + flags = 2 # key flags (certify & sign) + if self.ecdh: + flags = 4 | 8 + hashed_subpackets = [ proto.subpacket_time(self.pubkey.created), # signature time - proto.subpacket_byte(0x1B, 2)] # key flags (certify & sign) - unhashed_subpackets = [ - proto.subpacket(16, primary['key_id']), # issuer key id - proto.subpacket(32, embedded_sig), - proto.CUSTOM_SUBPACKET] + proto.subpacket_byte(0x1B, flags)] + + unhashed_subpackets = [] + unhashed_subpackets.append(proto.subpacket(16, primary['key_id'])) + if embedded_sig is not None: + unhashed_subpackets.append(proto.subpacket(32, embedded_sig)) + unhashed_subpackets.append(proto.CUSTOM_SUBPACKET) + log.info('confirm signing subkey with gpg-agent') gpg_agent = AgentSigner(self.user_id) signature = proto.make_signature( diff --git a/trezor_agent/gpg/proto.py b/trezor_agent/gpg/proto.py index ab46fc3..cc002fe 100644 --- a/trezor_agent/gpg/proto.py +++ b/trezor_agent/gpg/proto.py @@ -131,6 +131,7 @@ SUPPORTED_CURVES = { } } +ECDH_ALGO_ID = 18 CUSTOM_SUBPACKET = subpacket(100, b'TREZOR-GPG') # marks "our" pubkey @@ -145,12 +146,18 @@ def find_curve_by_algo_id(algo_id): class PublicKey(object): """GPG representation for public key packets.""" - def __init__(self, curve_name, created, verifying_key): + def __init__(self, curve_name, created, verifying_key, ecdh=False): """Contruct using a ECDSA VerifyingKey object.""" self.curve_info = SUPPORTED_CURVES[curve_name] self.created = int(created) # time since Epoch self.verifying_key = verifying_key - self.algo_id = self.curve_info['algo_id'] + if ecdh: + self.algo_id = ECDH_ALGO_ID + self.ecdh_packet = b'\x03\x01\x08\x07' + else: + self.algo_id = self.curve_info['algo_id'] + self.ecdh_packet = b'' + self.keygrip = self.curve_info['keygrip'](verifying_key) hex_key_id = util.hexlify(self.key_id())[-8:] self.desc = 'GPG public key {}/{}'.format(curve_name, hex_key_id) @@ -163,7 +170,7 @@ class PublicKey(object): self.algo_id) # public key algorithm ID oid = util.prefix_len('>B', self.curve_info['oid']) blob = self.curve_info['serialize'](self.verifying_key) - return header + oid + blob + return header + oid + blob + self.ecdh_packet def data_to_hash(self): """Data for digest computation.""" From c1c679b54111b903b32ae7eb7b997fc1abab16c1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Thu, 2 Jun 2016 23:24:36 +0300 Subject: [PATCH 07/37] HACK: support ECDH in agent - note keygrip and ID errors. --- trezor_agent/gpg/agent.py | 39 +++++++++++++++++++++++++++++++++++++- trezor_agent/gpg/encode.py | 15 ++++++++++++++- trezor_agent/gpg/proto.py | 3 +++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index b8eb4d7..d1ec6f0 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -49,6 +49,37 @@ def pksign(keygrip, digest, algo): return result +def _serialize_point(data): + data = '{}:'.format(len(data)) + data + # 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 + ')' + + +def pkdecrypt(keygrip, conn): + for msg in [b'S INQUIRE_MAXLEN 4096', b'INQUIRE CIPHERTEXT']: + keyring.sendline(conn, msg) + + line = keyring.recvline(conn) + prefix, line = line.split(' ', 1) + assert prefix == 'D' + exp, leftover = keyring.parse(keyring.unescape(line)) + + pubkey = decode.load_public_key(keyring.export_public_key(user_id=None), + use_custom=True) + f = encode.Factory.from_public_key(pubkey=pubkey, + user_id=pubkey['user_id']) + with contextlib.closing(f): + ### assert f.pubkey.keygrip == binascii.unhexlify(keygrip) + pubkey = dict(exp[1][1:])['e'] + shared_secret = f.get_shared_secret(pubkey) + + assert len(shared_secret) == 65 + assert shared_secret[:1] == b'\x04' + return _serialize_point(shared_secret) + + def iterlines(conn): """Iterate over input, split by lines.""" while True: @@ -75,13 +106,19 @@ def handle_connection(conn): keyring.sendline(conn, b'D 2.1.11') elif command == 'AGENT_ID': keyring.sendline(conn, b'D TREZOR') - elif command == 'SIGKEY': + elif command in {'SIGKEY', 'SETKEY'}: keygrip, = args elif command == 'SETHASH': algo, digest = args elif command == 'PKSIGN': sig = pksign(keygrip, digest, algo) keyring.sendline(conn, b'D ' + sig) + elif command == 'PKDECRYPT': + sec = pkdecrypt(keygrip, conn) + keyring.sendline(conn, b'D ' + sec) + elif command == 'END': + log.error('closing connection') + return else: log.error('unknown request: %r', line) return diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 3344deb..99491e5 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -40,6 +40,16 @@ class HardwareSigner(object): sig = result.signature[1:] return (util.bytes2num(sig[:32]), util.bytes2num(sig[32:])) + def ecdh(self, pubkey): + result = self.client_wrapper.connection.sign_identity( + identity=self.identity, + challenge_hidden=pubkey, + challenge_visual=b'', + ecdsa_curve_name=self.curve_name) + assert len(result.signature) == 65 + assert result.signature[:1] == b'\x04' + return result.signature + def close(self): """Close the connection to the device.""" self.client_wrapper.connection.clear_session() @@ -91,7 +101,7 @@ class Factory(object): s = cls(user_id=user_id, created=pubkey['created'], curve_name=proto.find_curve_by_algo_id(pubkey['algo'])) - assert s.pubkey.key_id() == pubkey['key_id'] + ### assert s.pubkey.key_id() == pubkey['key_id'] return s def close(self): @@ -205,3 +215,6 @@ class Factory(object): hashed_subpackets=hashed_subpackets, unhashed_subpackets=unhashed_subpackets) return proto.packet(tag=2, blob=blob) + + def get_shared_secret(self, pubkey): + return self.conn.ecdh(pubkey) diff --git a/trezor_agent/gpg/proto.py b/trezor_agent/gpg/proto.py index cc002fe..bbaad20 100644 --- a/trezor_agent/gpg/proto.py +++ b/trezor_agent/gpg/proto.py @@ -138,6 +138,9 @@ CUSTOM_SUBPACKET = subpacket(100, b'TREZOR-GPG') # marks "our" pubkey def find_curve_by_algo_id(algo_id): """Find curve name that matches a public key algorith ID.""" + if algo_id == ECDH_ALGO_ID: + return formats.CURVE_NIST256 + curve_name, = [name for name, info in SUPPORTED_CURVES.items() if info['algo_id'] == algo_id] return curve_name From e7bacf829caae2b407c7e0f03fa72dd488a5800e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 14:39:16 +0300 Subject: [PATCH 08/37] gpg: refactor ecdh case --- trezor_agent/gpg/agent.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index d1ec6f0..7a74565 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -57,23 +57,36 @@ def _serialize_point(data): return '(5:value' + data + ')' +def parse_ecdh(line): + prefix, line = line.split(' ', 1) + assert prefix == 'D' + exp, leftover = keyring.parse(keyring.unescape(line)) + log.debug('ECDH s-exp: %r', exp) + assert not leftover + label, exp = exp + assert label == b'enc-val' + assert exp[0] == b'ecdh' + items = exp[1:] + log.debug('ECDH parameters: %r', items) + return dict(items)['e'] + + def pkdecrypt(keygrip, conn): for msg in [b'S INQUIRE_MAXLEN 4096', b'INQUIRE CIPHERTEXT']: keyring.sendline(conn, msg) line = keyring.recvline(conn) - prefix, line = line.split(' ', 1) - assert prefix == 'D' - exp, leftover = keyring.parse(keyring.unescape(line)) + assert keyring.recvline(conn) == b'END' + remote_pubkey = parse_ecdh(line) - pubkey = decode.load_public_key(keyring.export_public_key(user_id=None), - use_custom=True) - f = encode.Factory.from_public_key(pubkey=pubkey, - user_id=pubkey['user_id']) + local_pubkey = decode.load_public_key( + pubkey_bytes=keyring.export_public_key(user_id=None), + use_custom=True) + f = encode.Factory.from_public_key( + pubkey=local_pubkey, user_id=local_pubkey['user_id']) with contextlib.closing(f): ### assert f.pubkey.keygrip == binascii.unhexlify(keygrip) - pubkey = dict(exp[1][1:])['e'] - shared_secret = f.get_shared_secret(pubkey) + shared_secret = f.get_shared_secret(remote_pubkey) assert len(shared_secret) == 65 assert shared_secret[:1] == b'\x04' @@ -116,9 +129,6 @@ def handle_connection(conn): elif command == 'PKDECRYPT': sec = pkdecrypt(keygrip, conn) keyring.sendline(conn, b'D ' + sec) - elif command == 'END': - log.error('closing connection') - return else: log.error('unknown request: %r', line) return From 56e9d7c776d81f3105f5f53badbc7eb164e098ac Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 14:42:40 +0300 Subject: [PATCH 09/37] gpg: allow graceful exit via Ctrl+C --- trezor_agent/gpg/agent.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 7a74565..2f19f37 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -14,7 +14,10 @@ def yield_connections(sock): """Run a server on the specified socket.""" while True: log.debug('waiting for connection on %s', sock.getsockname()) - conn, _ = sock.accept() + try: + conn, _ = sock.accept() + except KeyboardInterrupt: + return conn.settimeout(None) log.debug('accepted connection on %s', sock.getsockname()) yield conn From e9f7894d6202a4c13d7394b7148c4cc86d568dda Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 15:05:45 +0300 Subject: [PATCH 10/37] ecdh: fixup pubkey ID --- trezor_agent/gpg/encode.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 99491e5..9c36745 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -98,11 +98,13 @@ class Factory(object): @classmethod def from_public_key(cls, pubkey, user_id): """Create from an existing GPG public key.""" - s = cls(user_id=user_id, + is_ecdh = (pubkey['algo'] == proto.ECDH_ALGO_ID) + f = cls(user_id=user_id, created=pubkey['created'], - curve_name=proto.find_curve_by_algo_id(pubkey['algo'])) - ### assert s.pubkey.key_id() == pubkey['key_id'] - return s + curve_name=proto.find_curve_by_algo_id(pubkey['algo']), + ecdh=is_ecdh) + assert f.pubkey.key_id() == pubkey['key_id'] + return f def close(self): """Close connection and turn off the screen of the device.""" From 2acd0bf3b7b38608ac3ceb5aeca49139de587867 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 17:41:31 +0300 Subject: [PATCH 11/37] gpg: fix keygrip computation --- trezor_agent/gpg/proto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trezor_agent/gpg/proto.py b/trezor_agent/gpg/proto.py index bbaad20..5f40b5b 100644 --- a/trezor_agent/gpg/proto.py +++ b/trezor_agent/gpg/proto.py @@ -80,7 +80,7 @@ def _serialize_ed25519(vk): def _compute_keygrip(params): parts = [] for name, value in params: - exp = '1:{}{}'.format(name, len(value)) + exp = '1:{}{}:'.format(name, len(value)) parts.append(b'(' + exp.encode('ascii') + value + b')') return hashlib.sha1(b''.join(parts)).digest() From a6660fd5c500c78606af20131ef2f92a75a827f1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 17:43:46 +0300 Subject: [PATCH 12/37] gpg: handle BYE command --- trezor_agent/gpg/agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 2f19f37..0085b4d 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -132,6 +132,8 @@ def handle_connection(conn): elif command == 'PKDECRYPT': sec = pkdecrypt(keygrip, conn) keyring.sendline(conn, b'D ' + sec) + elif command == 'BYE': + return else: log.error('unknown request: %r', line) return From 29a984eebbf9d556d9726035f9c6547e0bbdb881 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 20:17:53 +0300 Subject: [PATCH 13/37] gpg: improve flags selection --- trezor_agent/gpg/encode.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 9c36745..3ac15be 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -173,9 +173,10 @@ class Factory(object): unhashed_subpackets=unhashed_subpackets) # Subkey Binding Signature - flags = 2 # key flags (certify & sign) - if self.ecdh: - flags = 4 | 8 + + # Key flags: https://tools.ietf.org/html/rfc4880#section-5.2.3.21 + # (certify & sign) (encrypt) + flags = (2) if (not self.ecdh) else (4 | 8) hashed_subpackets = [ proto.subpacket_time(self.pubkey.created), # signature time From 10cbe67c9a585a02a8a8bba0c190227e106f6197 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 21:53:31 +0300 Subject: [PATCH 14/37] gpg: add TODO --- trezor_agent/gpg/encode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 3ac15be..41a91a1 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -189,6 +189,7 @@ class Factory(object): unhashed_subpackets.append(proto.CUSTOM_SUBPACKET) log.info('confirm signing subkey with gpg-agent') + # TODO: support TREZOR-based primary key gpg_agent = AgentSigner(self.user_id) signature = proto.make_signature( signer_func=gpg_agent.sign, From d2167cd4ffe190aeb50a701468f60f47edc335db Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 22:39:31 +0300 Subject: [PATCH 15/37] gpg: check keygrip on ECDH --- trezor_agent/gpg/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 0085b4d..cb32fef 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -88,7 +88,7 @@ def pkdecrypt(keygrip, conn): f = encode.Factory.from_public_key( pubkey=local_pubkey, user_id=local_pubkey['user_id']) with contextlib.closing(f): - ### assert f.pubkey.keygrip == binascii.unhexlify(keygrip) + assert f.pubkey.keygrip == binascii.unhexlify(keygrip) shared_secret = f.get_shared_secret(remote_pubkey) assert len(shared_secret) == 65 From 16090cebed65c42904aa85539c520e263fb31dba Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 22:40:17 +0300 Subject: [PATCH 16/37] pylint: ignore TODOs --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index fd1d5a0..724cb4b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,2 +1,2 @@ [MESSAGES CONTROL] -disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking +disable=fixme, invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking From 8b5ac141501e413753b9ec440c988efa5bd9d0c0 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Jun 2016 22:44:25 +0300 Subject: [PATCH 17/37] gpg: add docstrings --- trezor_agent/gpg/agent.py | 2 ++ trezor_agent/gpg/encode.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index cb32fef..47513ea 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -61,6 +61,7 @@ def _serialize_point(data): def parse_ecdh(line): + """Parse ECDH request and return remote public key.""" prefix, line = line.split(' ', 1) assert prefix == 'D' exp, leftover = keyring.parse(keyring.unescape(line)) @@ -75,6 +76,7 @@ def parse_ecdh(line): def pkdecrypt(keygrip, conn): + """Handle decryption using ECDH.""" for msg in [b'S INQUIRE_MAXLEN 4096', b'INQUIRE CIPHERTEXT']: keyring.sendline(conn, msg) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 41a91a1..7fcc8f1 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -41,6 +41,7 @@ class HardwareSigner(object): return (util.bytes2num(sig[:32]), util.bytes2num(sig[32:])) def ecdh(self, pubkey): + """Derive shared secret using ECDH from remote public key.""" result = self.client_wrapper.connection.sign_identity( identity=self.identity, challenge_hidden=pubkey, @@ -221,4 +222,5 @@ class Factory(object): return proto.packet(tag=2, blob=blob) def get_shared_secret(self, pubkey): + """Derive shared secret using ECDH from remote public key.""" return self.conn.ecdh(pubkey) From 171c746c7e30995a3af1307915e47c8494c89c5e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 4 Jun 2016 09:36:48 +0300 Subject: [PATCH 18/37] gpg: move agent main code to __main__ --- trezor_agent/gpg/__main__.py | 27 ++++++++++++++++++++------- trezor_agent/gpg/agent.py | 19 +------------------ 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/trezor_agent/gpg/__main__.py b/trezor_agent/gpg/__main__.py index 19f861b..7b00cc1 100755 --- a/trezor_agent/gpg/__main__.py +++ b/trezor_agent/gpg/__main__.py @@ -7,7 +7,8 @@ import os import sys import time -from . import encode, keyring, proto +from . import agent, encode, keyring, proto +from .. import server log = logging.getLogger(__name__) @@ -28,6 +29,15 @@ def run_create(args): sys.stdout.write(proto.armor(result, 'PUBLIC KEY BLOCK')) +def run_agent(_): + """Run a simple GPG-agent server.""" + sock_path = os.path.expanduser('~/.gnupg/S.gpg-agent') + 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() @@ -36,12 +46,15 @@ def main(): subparsers.required = True subparsers.dest = 'command' - create = subparsers.add_parser('create') - create.add_argument('-s', '--subkey', action='store_true', default=False) - create.add_argument('--ecdh', action='store_true', default=False) - create.add_argument('-e', '--ecdsa-curve', default='nist256p1') - create.add_argument('-t', '--time', type=int, default=int(time.time())) - create.set_defaults(run=run_create) + create_cmd = subparsers.add_parser('create') + create_cmd.add_argument('-s', '--subkey', action='store_true', default=False) + create_cmd.add_argument('--ecdh', 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, diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 47513ea..3a017b1 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -2,10 +2,9 @@ import binascii import contextlib import logging -import os from . import decode, encode, keyring -from .. import server, util +from .. import util log = logging.getLogger(__name__) @@ -141,19 +140,3 @@ def handle_connection(conn): return keyring.sendline(conn, b'OK') - - -def main(): - """Run a simple GPG-agent server.""" - logging.basicConfig(level=logging.INFO, - format='%(asctime)s %(levelname)-10s %(message)s') - - sock_path = os.path.expanduser('~/.gnupg/S.gpg-agent') - with server.unix_domain_socket_server(sock_path) as sock: - for conn in yield_connections(sock): - with contextlib.closing(conn): - handle_connection(conn) - - -if __name__ == '__main__': - main() From 04af6b737bf50a28b13472d0964bbb0642a04d68 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 4 Jun 2016 09:53:43 +0300 Subject: [PATCH 19/37] gpg: remove extra param from Factory.from_public_key() --- trezor_agent/gpg/agent.py | 6 ++---- trezor_agent/gpg/encode.py | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 3a017b1..d2a2112 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -41,8 +41,7 @@ def pksign(keygrip, digest, algo): assert algo == '8' pubkey = decode.load_public_key(keyring.export_public_key(user_id=None), use_custom=True) - f = encode.Factory.from_public_key(pubkey=pubkey, - user_id=pubkey['user_id']) + f = encode.Factory.from_public_key(pubkey=pubkey) with contextlib.closing(f): assert f.pubkey.keygrip == binascii.unhexlify(keygrip) r, s = f.conn.sign(binascii.unhexlify(digest)) @@ -86,8 +85,7 @@ def pkdecrypt(keygrip, conn): local_pubkey = decode.load_public_key( pubkey_bytes=keyring.export_public_key(user_id=None), use_custom=True) - f = encode.Factory.from_public_key( - pubkey=local_pubkey, user_id=local_pubkey['user_id']) + f = encode.Factory.from_public_key(pubkey=local_pubkey) with contextlib.closing(f): assert f.pubkey.keygrip == binascii.unhexlify(keygrip) shared_secret = f.get_shared_secret(remote_pubkey) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 7fcc8f1..93ed120 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -97,13 +97,12 @@ class Factory(object): self.pubkey, _time_format(self.pubkey.created), user_id) @classmethod - def from_public_key(cls, pubkey, user_id): + def from_public_key(cls, pubkey): """Create from an existing GPG public key.""" - is_ecdh = (pubkey['algo'] == proto.ECDH_ALGO_ID) - f = cls(user_id=user_id, + f = cls(user_id=pubkey['user_id'], created=pubkey['created'], curve_name=proto.find_curve_by_algo_id(pubkey['algo']), - ecdh=is_ecdh) + ecdh=(pubkey['algo'] == proto.ECDH_ALGO_ID)) assert f.pubkey.key_id() == pubkey['key_id'] return f From bc281d441149efcec33e40961e04062efa135ca0 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 4 Jun 2016 19:45:03 +0300 Subject: [PATCH 20/37] gpg: use local version --- trezor_agent/gpg/agent.py | 3 ++- trezor_agent/gpg/keyring.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index d2a2112..2583d73 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -109,6 +109,7 @@ def handle_connection(conn): keygrip = None digest = None algo = None + version = keyring.gpg_version() keyring.sendline(conn, b'OK') for line in iterlines(conn): @@ -118,7 +119,7 @@ def handle_connection(conn): if command in {'RESET', 'OPTION', 'HAVEKEY', 'SETKEYDESC'}: pass # reply with OK elif command == 'GETINFO': - keyring.sendline(conn, b'D 2.1.11') + keyring.sendline(conn, b'D ' + version) elif command == 'AGENT_ID': keyring.sendline(conn, b'D TREZOR') elif command in {'SIGKEY', 'SETKEY'}: diff --git a/trezor_agent/gpg/keyring.py b/trezor_agent/gpg/keyring.py index 4e33f74..3384b18 100644 --- a/trezor_agent/gpg/keyring.py +++ b/trezor_agent/gpg/keyring.py @@ -158,6 +158,14 @@ def get_keygrip(user_id, sp=subprocess): 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 = ['gpg2', '--version'] + output = sp.check_output(args).decode('ascii') + line = output.split(b'\n')[0] # b'gpg (GnuPG) 2.1.11' + return line.split(b' ')[-1] # b'2.1.11' + + def export_public_key(user_id, sp=subprocess): """Export GPG public key for specified `user_id`.""" args = ['gpg2', '--export'] + ([user_id] if user_id else []) From 694cee17ac4cd35ce23e6c43e02a80c6c2b964b6 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 4 Jun 2016 20:54:07 +0300 Subject: [PATCH 21/37] gpg: refactor create_* methods --- trezor_agent/gpg/encode.py | 186 ++++++++++++++++++++----------------- 1 file changed, 99 insertions(+), 87 deletions(-) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 93ed120..7033e18 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -79,6 +79,99 @@ 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 = proto.packet(tag=6, blob=pubkey.data()) + user_id_packet = proto.packet(tag=13, + blob=user_id.encode('ascii')) + + data_to_sign = (pubkey.data_to_hash() + + user_id_packet[:1] + + util.prefix_len('>L', user_id.encode('ascii'))) + log.info('signing public key "%s"', user_id) + hashed_subpackets = [ + proto.subpacket_time(pubkey.created), # signature time + # https://tools.ietf.org/html/rfc4880#section-5.2.3.4 + proto.subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) + # https://tools.ietf.org/html/rfc4880#section-5.2.3.21 + proto.subpacket_byte(0x15, 8), # preferred hash (SHA256) + # https://tools.ietf.org/html/rfc4880#section-5.2.3.8 + proto.subpacket_byte(0x16, 0), # preferred compression (none) + # https://tools.ietf.org/html/rfc4880#section-5.2.3.9 + proto.subpacket_byte(0x17, 0x80) # key server prefs (no-modify) + # https://tools.ietf.org/html/rfc4880#section-5.2.3.17 + ] + unhashed_subpackets = [ + proto.subpacket(16, pubkey.key_id()), # issuer key id + proto.CUSTOM_SUBPACKET] + + signature = proto.make_signature( + signer_func=signer_func, + public_algo=pubkey.algo_id, + data_to_sign=data_to_sign, + sig_type=0x13, # user id & public key + hashed_subpackets=hashed_subpackets, + unhashed_subpackets=unhashed_subpackets) + + sign_packet = proto.packet(tag=2, blob=signature) + return pubkey_packet + user_id_packet + sign_packet + + +def create_subkey(primary_bytes, pubkey, signer_func, ecdh=False): + """Export new subkey to GPG primary key.""" + subkey_packet = proto.packet(tag=14, blob=pubkey.data()) + primary = decode.load_public_key(primary_bytes) + log.info('adding subkey to primary GPG key "%s" (%s)', + primary['user_id'], util.hexlify(primary['key_id'])) + data_to_sign = primary['_to_hash'] + pubkey.data_to_hash() + + if ecdh: + embedded_sig = None + else: + # Primary Key Binding Signature + hashed_subpackets = [ + proto.subpacket_time(pubkey.created)] # signature time + unhashed_subpackets = [ + proto.subpacket(16, pubkey.key_id())] # issuer key id + log.info('confirm signing subkey with hardware device') + embedded_sig = proto.make_signature( + signer_func=signer_func, + data_to_sign=data_to_sign, + public_algo=pubkey.algo_id, + sig_type=0x19, + hashed_subpackets=hashed_subpackets, + unhashed_subpackets=unhashed_subpackets) + + # Subkey Binding Signature + + # Key flags: https://tools.ietf.org/html/rfc4880#section-5.2.3.21 + # (certify & sign) (encrypt) + flags = (2) if (not ecdh) else (4 | 8) + + hashed_subpackets = [ + proto.subpacket_time(pubkey.created), # signature time + proto.subpacket_byte(0x1B, flags)] + + unhashed_subpackets = [] + unhashed_subpackets.append(proto.subpacket(16, primary['key_id'])) + if embedded_sig is not None: + unhashed_subpackets.append(proto.subpacket(32, embedded_sig)) + unhashed_subpackets.append(proto.CUSTOM_SUBPACKET) + + log.info('confirm signing subkey with gpg-agent') + # TODO: support TREZOR-based primary key + gpg_agent = AgentSigner(primary['user_id']) + signature = proto.make_signature( + signer_func=gpg_agent.sign, + data_to_sign=data_to_sign, + public_algo=primary['algo'], + sig_type=0x18, + hashed_subpackets=hashed_subpackets, + unhashed_subpackets=unhashed_subpackets) + sign_packet = proto.packet(tag=2, blob=signature) + return primary_bytes + subkey_packet + sign_packet + + class Factory(object): """Performs GPG signing operations.""" @@ -111,95 +204,14 @@ class Factory(object): self.conn.close() def create_primary(self): - """Export new primary GPG public key, ready for "gpg2 --import".""" - pubkey_packet = proto.packet(tag=6, blob=self.pubkey.data()) - user_id_packet = proto.packet(tag=13, - blob=self.user_id.encode('ascii')) - - data_to_sign = (self.pubkey.data_to_hash() + - user_id_packet[:1] + - util.prefix_len('>L', self.user_id.encode('ascii'))) - log.info('signing public key "%s"', self.user_id) - hashed_subpackets = [ - proto.subpacket_time(self.pubkey.created), # signature time - # https://tools.ietf.org/html/rfc4880#section-5.2.3.4 - proto.subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) - # https://tools.ietf.org/html/rfc4880#section-5.2.3.21 - proto.subpacket_byte(0x15, 8), # preferred hash (SHA256) - # https://tools.ietf.org/html/rfc4880#section-5.2.3.8 - proto.subpacket_byte(0x16, 0), # preferred compression (none) - # https://tools.ietf.org/html/rfc4880#section-5.2.3.9 - proto.subpacket_byte(0x17, 0x80) # key server prefs (no-modify) - # https://tools.ietf.org/html/rfc4880#section-5.2.3.17 - ] - unhashed_subpackets = [ - proto.subpacket(16, self.pubkey.key_id()), # issuer key id - proto.CUSTOM_SUBPACKET] - - signature = proto.make_signature( - signer_func=self.conn.sign, - public_algo=self.pubkey.algo_id, - data_to_sign=data_to_sign, - sig_type=0x13, # user id & public key - hashed_subpackets=hashed_subpackets, - unhashed_subpackets=unhashed_subpackets) - - sign_packet = proto.packet(tag=2, blob=signature) - return pubkey_packet + user_id_packet + sign_packet + """Export new subkey to GPG primary key.""" + return create_primary(user_id=self.user_id, pubkey=self.pubkey, + signer_func=self.conn.sign) def create_subkey(self, primary_bytes): - """Export new subkey to `self.user_id` GPG primary key.""" - subkey_packet = proto.packet(tag=14, blob=self.pubkey.data()) - primary = decode.load_public_key(primary_bytes) - log.info('adding subkey to primary GPG key "%s" (%s)', - self.user_id, util.hexlify(primary['key_id'])) - data_to_sign = primary['_to_hash'] + self.pubkey.data_to_hash() - - if self.ecdh: - embedded_sig = None - else: - # Primary Key Binding Signature - hashed_subpackets = [ - proto.subpacket_time(self.pubkey.created)] # signature time - unhashed_subpackets = [ - proto.subpacket(16, self.pubkey.key_id())] # issuer key id - log.info('confirm signing subkey with hardware device') - embedded_sig = proto.make_signature( - signer_func=self.conn.sign, - data_to_sign=data_to_sign, - public_algo=self.pubkey.algo_id, - sig_type=0x19, - hashed_subpackets=hashed_subpackets, - unhashed_subpackets=unhashed_subpackets) - - # Subkey Binding Signature - - # Key flags: https://tools.ietf.org/html/rfc4880#section-5.2.3.21 - # (certify & sign) (encrypt) - flags = (2) if (not self.ecdh) else (4 | 8) - - hashed_subpackets = [ - proto.subpacket_time(self.pubkey.created), # signature time - proto.subpacket_byte(0x1B, flags)] - - unhashed_subpackets = [] - unhashed_subpackets.append(proto.subpacket(16, primary['key_id'])) - if embedded_sig is not None: - unhashed_subpackets.append(proto.subpacket(32, embedded_sig)) - unhashed_subpackets.append(proto.CUSTOM_SUBPACKET) - - log.info('confirm signing subkey with gpg-agent') - # TODO: support TREZOR-based primary key - gpg_agent = AgentSigner(self.user_id) - signature = proto.make_signature( - signer_func=gpg_agent.sign, - data_to_sign=data_to_sign, - public_algo=primary['algo'], - sig_type=0x18, - hashed_subpackets=hashed_subpackets, - unhashed_subpackets=unhashed_subpackets) - sign_packet = proto.packet(tag=2, blob=signature) - return primary_bytes + subkey_packet + sign_packet + """Export new subkey to GPG primary key.""" + return create_subkey(primary_bytes=primary_bytes, pubkey=self.pubkey, + signer_func=self.conn.sign, ecdh=self.ecdh) def sign_message(self, msg, sign_time=None): """Sign GPG message at specified time.""" From a05cff507947cd3d997a49cac26864a78476a44e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 6 Jun 2016 22:51:55 +0300 Subject: [PATCH 22/37] gpg: use "gpg2" for 'git config --local gpg.program' --- setup.py | 1 - trezor_agent/gpg/README.md | 2 +- trezor_agent/gpg/trezor-git-gpg-wrapper.sh | 7 ------- 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 100755 trezor_agent/gpg/trezor-git-gpg-wrapper.sh diff --git a/setup.py b/setup.py index e95f8e0..f131768 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,6 @@ setup( author_email='roman.zeyde@gmail.com', url='http://github.com/romanz/trezor-agent', packages=['trezor_agent', 'trezor_agent.gpg'], - scripts=['trezor_agent/gpg/trezor-git-gpg-wrapper.sh'], install_requires=['ecdsa>=0.13', 'ed25519>=1.4', 'Cython>=0.23.4', 'protobuf>=2.6.1', 'trezor>=0.6.12', 'semver>=2.2'], platforms=['POSIX'], classifiers=[ diff --git a/trezor_agent/gpg/README.md b/trezor_agent/gpg/README.md index 058c60e..09e8c9e 100644 --- a/trezor_agent/gpg/README.md +++ b/trezor_agent/gpg/README.md @@ -48,7 +48,7 @@ $ gpg2 --verify EXAMPLE.asc # verify using standard GPG binary ## 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 "trezor-git-gpg-wrapper.sh" +$ git config --local gpg.program "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 diff --git a/trezor_agent/gpg/trezor-git-gpg-wrapper.sh b/trezor_agent/gpg/trezor-git-gpg-wrapper.sh deleted file mode 100755 index c65bbe9..0000000 --- a/trezor_agent/gpg/trezor-git-gpg-wrapper.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -if [[ "$*" == *"--verify"* ]] -then - gpg2 $* # verify using GPG2 (for ECDSA and EdDSA keys) -else - trezor-gpg sign -o- # sign using TREZOR and write the signature to stdout -fi From 3b139314b67df40a8d18f15388ebeb1d4e837fd2 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 6 Jun 2016 22:52:10 +0300 Subject: [PATCH 23/37] gpg: refactor sign_message method --- trezor_agent/gpg/encode.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 7033e18..35def62 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -172,6 +172,23 @@ def create_subkey(primary_bytes, pubkey, signer_func, ecdh=False): return primary_bytes + subkey_packet + sign_packet +def sign_message(signer_func, msg, pubkey, sign_time): + """Sign GPG message at specified time.""" + log.info('signing %d byte message at %s', + len(msg), _time_format(sign_time)) + hashed_subpackets = [proto.subpacket_time(sign_time)] + unhashed_subpackets = [ + proto.subpacket(16, pubkey.key_id())] # issuer key id + + blob = proto.make_signature( + signer_func=signer_func, + data_to_sign=msg, + public_algo=pubkey.algo_id, + hashed_subpackets=hashed_subpackets, + unhashed_subpackets=unhashed_subpackets) + return proto.packet(tag=2, blob=blob) + + class Factory(object): """Performs GPG signing operations.""" @@ -217,20 +234,8 @@ class Factory(object): """Sign GPG message at specified time.""" if sign_time is None: sign_time = int(time.time()) - - log.info('signing %d byte message at %s', - len(msg), _time_format(sign_time)) - hashed_subpackets = [proto.subpacket_time(sign_time)] - unhashed_subpackets = [ - proto.subpacket(16, self.pubkey.key_id())] # issuer key id - - blob = proto.make_signature( - signer_func=self.conn.sign, - data_to_sign=msg, - public_algo=self.pubkey.algo_id, - hashed_subpackets=hashed_subpackets, - unhashed_subpackets=unhashed_subpackets) - return proto.packet(tag=2, blob=blob) + return sign_message(signer_func=self.conn.sign, pubkey=self.pubkey, + msg=msg, sign_time=sign_time) def get_shared_secret(self, pubkey): """Derive shared secret using ECDH from remote public key.""" From a1659e0f0d4784eb9e7183302812958eb29b9651 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Jun 2016 10:34:59 +0300 Subject: [PATCH 24/37] gpg: add preferred symmetric algo --- trezor_agent/gpg/encode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 35def62..97c5b1d 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -91,6 +91,8 @@ def create_primary(user_id, pubkey, signer_func): log.info('signing public key "%s"', user_id) hashed_subpackets = [ proto.subpacket_time(pubkey.created), # signature time + # https://tools.ietf.org/html/rfc4880#section-5.2.3.7 + proto.subpacket_byte(0x0B, 9), # preferred symmetric algo (AES-256) # https://tools.ietf.org/html/rfc4880#section-5.2.3.4 proto.subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) # https://tools.ietf.org/html/rfc4880#section-5.2.3.21 From 8108e5400d842a6894cb1e14deabfc1a2ad8a2ac Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Jun 2016 13:47:56 +0300 Subject: [PATCH 25/37] gpg: support TREZOR-based primary key --- trezor_agent/gpg/decode.py | 1 + trezor_agent/gpg/encode.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/trezor_agent/gpg/decode.py b/trezor_agent/gpg/decode.py index 127f112..8cbb454 100644 --- a/trezor_agent/gpg/decode.py +++ b/trezor_agent/gpg/decode.py @@ -295,6 +295,7 @@ def load_public_key(pubkey_bytes, use_custom=False): packets = packets[2:] packet['user_id'] = userid['value'] + packet['_is_custom'] = signature['_is_custom'] return packet diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 97c5b1d..9b5345f 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -135,7 +135,7 @@ def create_subkey(primary_bytes, pubkey, signer_func, ecdh=False): proto.subpacket_time(pubkey.created)] # signature time unhashed_subpackets = [ proto.subpacket(16, pubkey.key_id())] # issuer key id - log.info('confirm signing subkey with hardware device') + log.info('confirm signing with new subkey') embedded_sig = proto.make_signature( signer_func=signer_func, data_to_sign=data_to_sign, @@ -160,11 +160,12 @@ def create_subkey(primary_bytes, pubkey, signer_func, ecdh=False): unhashed_subpackets.append(proto.subpacket(32, embedded_sig)) unhashed_subpackets.append(proto.CUSTOM_SUBPACKET) - log.info('confirm signing subkey with gpg-agent') - # TODO: support TREZOR-based primary key - gpg_agent = AgentSigner(primary['user_id']) + log.info('confirm signing with primary key') + if not primary['_is_custom']: + signer_func = AgentSigner(primary['user_id']).sign + signature = proto.make_signature( - signer_func=gpg_agent.sign, + signer_func=signer_func, data_to_sign=data_to_sign, public_algo=primary['algo'], sig_type=0x18, From 3e41fddcef80e59e0b24177058fb563debaec2ff Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Jun 2016 14:02:12 +0300 Subject: [PATCH 26/37] gpg: add test for ECDH pubkey generation --- trezor_agent/gpg/tests/test_proto.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/trezor_agent/gpg/tests/test_proto.py b/trezor_agent/gpg/tests/test_proto.py index 87e9d83..82af27b 100644 --- a/trezor_agent/gpg/tests/test_proto.py +++ b/trezor_agent/gpg/tests/test_proto.py @@ -76,6 +76,14 @@ def test_nist256p1(): assert repr(pk) == 'GPG public key nist256p1/F82361D9' +def test_nist256p1_ecdh(): + sk = ecdsa.SigningKey.from_secret_exponent(secexp=1, curve=ecdsa.NIST256p) + vk = sk.get_verifying_key() + pk = proto.PublicKey(curve_name=formats.CURVE_NIST256, + created=42, verifying_key=vk, ecdh=True) + assert repr(pk) == 'GPG public key nist256p1/5811DF46' + + def test_ed25519(): sk = ed25519.SigningKey(b'\x00' * 32) vk = sk.get_verifying_key() From 7bbf11b63143eaa5be7dc8497a9b899ab8a2be92 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Jun 2016 14:38:11 +0300 Subject: [PATCH 27/37] gpg: refactor key creation --- trezor_agent/gpg/__main__.py | 44 ++++++++++++++++++++++++++++-------- trezor_agent/gpg/encode.py | 18 ++++----------- trezor_agent/gpg/proto.py | 7 +++++- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/trezor_agent/gpg/__main__.py b/trezor_agent/gpg/__main__.py index 7b00cc1..b061b43 100755 --- a/trezor_agent/gpg/__main__.py +++ b/trezor_agent/gpg/__main__.py @@ -16,15 +16,42 @@ log = logging.getLogger(__name__) def run_create(args): """Generate a new pubkey for a new/existing GPG identity.""" user_id = os.environ['TREZOR_GPG_USER_ID'] - f = encode.Factory(user_id=user_id, created=args.time, - curve_name=args.ecdsa_curve, ecdh=args.ecdh) + conn = encode.HardwareSigner(user_id=user_id, + curve_name=args.ecdsa_curve) + verifying_key = conn.pubkey() - with contextlib.closing(f): - if args.subkey: - primary_key = keyring.export_public_key(user_id=user_id) - result = f.create_subkey(primary_bytes=primary_key) - else: - result = f.create_primary() + if args.subkey: + primary_bytes = keyring.export_public_key(user_id=user_id) + # subkey for signing + signing_key = proto.PublicKey( + curve_name=args.ecdsa_curve, created=args.time, + verifying_key=verifying_key, ecdh=False) + # subkey for encryption + encryption_key = proto.PublicKey( + curve_name=args.ecdsa_curve, created=args.time, + verifying_key=verifying_key, ecdh=True) + result = encode.create_subkey(primary_bytes=primary_bytes, + pubkey=signing_key, + signer_func=conn.sign) + result = encode.create_subkey(primary_bytes=result, + pubkey=encryption_key, + signer_func=conn.sign) + else: + # primary key for signing + primary = proto.PublicKey( + curve_name=args.ecdsa_curve, created=args.time, + verifying_key=verifying_key, ecdh=False) + # subkey for encryption + subkey = proto.PublicKey( + curve_name=args.ecdsa_curve, created=args.time, + verifying_key=verifying_key, ecdh=True) + + result = encode.create_primary(user_id=user_id, + pubkey=primary, + signer_func=conn.sign) + result = encode.create_subkey(primary_bytes=result, + pubkey=subkey, + signer_func=conn.sign) sys.stdout.write(proto.armor(result, 'PUBLIC KEY BLOCK')) @@ -48,7 +75,6 @@ def main(): create_cmd = subparsers.add_parser('create') create_cmd.add_argument('-s', '--subkey', action='store_true', default=False) - create_cmd.add_argument('--ecdh', 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) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 9b5345f..91b6f5a 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -119,7 +119,7 @@ def create_primary(user_id, pubkey, signer_func): return pubkey_packet + user_id_packet + sign_packet -def create_subkey(primary_bytes, pubkey, signer_func, ecdh=False): +def create_subkey(primary_bytes, pubkey, signer_func): """Export new subkey to GPG primary key.""" subkey_packet = proto.packet(tag=14, blob=pubkey.data()) primary = decode.load_public_key(primary_bytes) @@ -127,7 +127,7 @@ def create_subkey(primary_bytes, pubkey, signer_func, ecdh=False): primary['user_id'], util.hexlify(primary['key_id'])) data_to_sign = primary['_to_hash'] + pubkey.data_to_hash() - if ecdh: + if pubkey.ecdh: embedded_sig = None else: # Primary Key Binding Signature @@ -147,8 +147,8 @@ def create_subkey(primary_bytes, pubkey, signer_func, ecdh=False): # Subkey Binding Signature # Key flags: https://tools.ietf.org/html/rfc4880#section-5.2.3.21 - # (certify & sign) (encrypt) - flags = (2) if (not ecdh) else (4 | 8) + # (certify & sign) (encrypt) + flags = (2) if (not pubkey.ecdh) else (4 | 8) hashed_subpackets = [ proto.subpacket_time(pubkey.created), # signature time @@ -223,16 +223,6 @@ class Factory(object): """Close connection and turn off the screen of the device.""" self.conn.close() - def create_primary(self): - """Export new subkey to GPG primary key.""" - return create_primary(user_id=self.user_id, pubkey=self.pubkey, - signer_func=self.conn.sign) - - def create_subkey(self, primary_bytes): - """Export new subkey to GPG primary key.""" - return create_subkey(primary_bytes=primary_bytes, pubkey=self.pubkey, - signer_func=self.conn.sign, ecdh=self.ecdh) - def sign_message(self, msg, sign_time=None): """Sign GPG message at specified time.""" if sign_time is None: diff --git a/trezor_agent/gpg/proto.py b/trezor_agent/gpg/proto.py index 5f40b5b..dfbfaee 100644 --- a/trezor_agent/gpg/proto.py +++ b/trezor_agent/gpg/proto.py @@ -154,6 +154,7 @@ class PublicKey(object): self.curve_info = SUPPORTED_CURVES[curve_name] self.created = int(created) # time since Epoch self.verifying_key = verifying_key + self.ecdh = ecdh if ecdh: self.algo_id = ECDH_ALGO_ID self.ecdh_packet = b'\x03\x01\x08\x07' @@ -161,10 +162,14 @@ class PublicKey(object): self.algo_id = self.curve_info['algo_id'] self.ecdh_packet = b'' - self.keygrip = self.curve_info['keygrip'](verifying_key) hex_key_id = util.hexlify(self.key_id())[-8:] self.desc = 'GPG public key {}/{}'.format(curve_name, hex_key_id) + @property + def keygrip(self): + """Compute GPG2 keygrip.""" + return self.curve_info['keygrip'](self.verifying_key) + def data(self): """Data for packet creation.""" header = struct.pack('>BLB', From 16de8cdabc5c88e79bd2ccd3f5e33a34c5e8ae49 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Jun 2016 15:06:35 +0300 Subject: [PATCH 28/37] agent: refactor signature and ECDH --- trezor_agent/gpg/agent.py | 21 ++++++++------- trezor_agent/gpg/encode.py | 54 +++++++++++--------------------------- 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 2583d73..6140cf1 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -39,12 +39,13 @@ def sig_encode(r, s): def pksign(keygrip, digest, algo): """Sign a message digest using a private EC key.""" assert algo == '8' - pubkey = decode.load_public_key(keyring.export_public_key(user_id=None), - use_custom=True) - f = encode.Factory.from_public_key(pubkey=pubkey) - with contextlib.closing(f): - assert f.pubkey.keygrip == binascii.unhexlify(keygrip) - r, s = f.conn.sign(binascii.unhexlify(digest)) + pubkey_dict = decode.load_public_key( + pubkey_bytes=keyring.export_public_key(user_id=None), + use_custom=True) + pubkey, conn = encode.load_from_public_key(pubkey_dict=pubkey_dict) + with contextlib.closing(conn): + assert pubkey.keygrip == binascii.unhexlify(keygrip) + r, s = conn.sign(binascii.unhexlify(digest)) result = sig_encode(r, s) log.debug('result: %r', result) return result @@ -85,10 +86,10 @@ def pkdecrypt(keygrip, conn): local_pubkey = decode.load_public_key( pubkey_bytes=keyring.export_public_key(user_id=None), use_custom=True) - f = encode.Factory.from_public_key(pubkey=local_pubkey) - with contextlib.closing(f): - assert f.pubkey.keygrip == binascii.unhexlify(keygrip) - shared_secret = f.get_shared_secret(remote_pubkey) + pubkey, conn = encode.load_from_public_key(pubkey_dict=local_pubkey) + with contextlib.closing(conn): + assert pubkey.keygrip == binascii.unhexlify(keygrip) + shared_secret = conn.ecdh(remote_pubkey) assert len(shared_secret) == 65 assert shared_secret[:1] == b'\x04' diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 91b6f5a..e529dc6 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -192,44 +192,20 @@ def sign_message(signer_func, msg, pubkey, sign_time): return proto.packet(tag=2, blob=blob) -class Factory(object): - """Performs GPG signing operations.""" +def load_from_public_key(pubkey_dict): + """Load correct public key from the device.""" + user_id = pubkey_dict['user_id'] + created = pubkey_dict['created'] + curve_name = proto.find_curve_by_algo_id(pubkey_dict['algo']) + assert curve_name in formats.SUPPORTED_CURVES + ecdh = (pubkey_dict['algo'] == proto.ECDH_ALGO_ID) - def __init__(self, user_id, created, curve_name, ecdh=False): - """Construct and loads a public key from the device.""" - self.user_id = user_id - assert curve_name in formats.SUPPORTED_CURVES + conn = HardwareSigner(user_id, curve_name=curve_name) + pubkey = proto.PublicKey( + curve_name=curve_name, created=created, + verifying_key=conn.pubkey(), 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) - self.conn = HardwareSigner(user_id, curve_name=curve_name) - self.pubkey = proto.PublicKey( - curve_name=curve_name, created=created, - verifying_key=self.conn.pubkey(), ecdh=ecdh) - self.ecdh = ecdh - - log.info('%s created at %s for "%s"', - self.pubkey, _time_format(self.pubkey.created), user_id) - - @classmethod - def from_public_key(cls, pubkey): - """Create from an existing GPG public key.""" - f = cls(user_id=pubkey['user_id'], - created=pubkey['created'], - curve_name=proto.find_curve_by_algo_id(pubkey['algo']), - ecdh=(pubkey['algo'] == proto.ECDH_ALGO_ID)) - assert f.pubkey.key_id() == pubkey['key_id'] - return f - - def close(self): - """Close connection and turn off the screen of the device.""" - self.conn.close() - - def sign_message(self, msg, sign_time=None): - """Sign GPG message at specified time.""" - if sign_time is None: - sign_time = int(time.time()) - return sign_message(signer_func=self.conn.sign, pubkey=self.pubkey, - msg=msg, sign_time=sign_time) - - def get_shared_secret(self, pubkey): - """Derive shared secret using ECDH from remote public key.""" - return self.conn.ecdh(pubkey) + return pubkey, conn From 602e867c7d7b1a6cd2e000f193012fc4e1d71e77 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Jun 2016 20:18:07 +0300 Subject: [PATCH 29/37] gpg: add test for keygrip --- trezor_agent/gpg/tests/test_proto.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/trezor_agent/gpg/tests/test_proto.py b/trezor_agent/gpg/tests/test_proto.py index 82af27b..fec34d9 100644 --- a/trezor_agent/gpg/tests/test_proto.py +++ b/trezor_agent/gpg/tests/test_proto.py @@ -74,6 +74,7 @@ def test_nist256p1(): pk = proto.PublicKey(curve_name=formats.CURVE_NIST256, created=42, verifying_key=vk) assert repr(pk) == 'GPG public key nist256p1/F82361D9' + assert pk.keygrip == b'\x95\x85.\x91\x7f\xe2\xc3\x91R\xba\x99\x81\x92\xb5y\x1d\xb1\\\xdc\xf0' def test_nist256p1_ecdh(): @@ -82,6 +83,7 @@ def test_nist256p1_ecdh(): pk = proto.PublicKey(curve_name=formats.CURVE_NIST256, created=42, verifying_key=vk, ecdh=True) assert repr(pk) == 'GPG public key nist256p1/5811DF46' + assert pk.keygrip == b'\x95\x85.\x91\x7f\xe2\xc3\x91R\xba\x99\x81\x92\xb5y\x1d\xb1\\\xdc\xf0' def test_ed25519(): @@ -90,3 +92,4 @@ def test_ed25519(): pk = proto.PublicKey(curve_name=formats.CURVE_ED25519, created=42, verifying_key=vk) assert repr(pk) == 'GPG public key ed25519/36B40FE6' + assert pk.keygrip == b'\xbf\x01\x90l\x17\xb64\xa3-\xf4\xc0gr\x99\x18<\xddBQ?' From 092445af714105780ceb4ec99bc052114dfbe969 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 11 Jun 2016 20:26:10 +0300 Subject: [PATCH 30/37] agent: handle connection errors --- trezor_agent/__main__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/trezor_agent/__main__.py b/trezor_agent/__main__.py index f7f968a..a56264b 100644 --- a/trezor_agent/__main__.py +++ b/trezor_agent/__main__.py @@ -1,5 +1,6 @@ """SSH-agent implementation using hardware authentication devices.""" import argparse +import functools import logging import os import re @@ -115,6 +116,19 @@ def run_server(conn, public_key, command, debug, timeout): log.info('server stopped') +def handle_connection_error(func): + """Fail with non-zero exit code.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except IOError as e: + log.error('Connection error: %s', e) + return 1 + return wrapper + + +@handle_connection_error def run_agent(client_factory=client.Client): """Run ssh-agent using given hardware client factory.""" args = create_agent_parser().parse_args() @@ -143,6 +157,7 @@ def run_agent(client_factory=client.Client): debug=args.debug, timeout=args.timeout) +@handle_connection_error def run_git(client_factory=client.Client): """Run git under ssh-agent using given hardware client factory.""" args = create_git_parser().parse_args() From fe4d9ed3c8c611712ab21e3f9fda9fa7a06e76e4 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 17 Jun 2016 09:29:05 +0300 Subject: [PATCH 31/37] gpg: add SLIP-0017 support for ECDH session key generation --- trezor_agent/client.py | 7 ++++--- trezor_agent/gpg/encode.py | 15 +++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/trezor_agent/client.py b/trezor_agent/client.py index cc573d4..eea90a7 100644 --- a/trezor_agent/client.py +++ b/trezor_agent/client.py @@ -128,8 +128,8 @@ def identity_to_string(identity): return ''.join(result) -def get_address(identity): - """Compute BIP32 derivation address for SignIdentity API.""" +def get_address(identity, ecdh=False): + """Compute BIP32 derivation address according to SLIP-0013/0017.""" index = struct.pack(' Date: Fri, 17 Jun 2016 09:32:43 +0300 Subject: [PATCH 32/37] gpg: load correct key if ECDH is requested --- trezor_agent/gpg/agent.py | 4 ++-- trezor_agent/gpg/decode.py | 7 ++++--- trezor_agent/gpg/encode.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/trezor_agent/gpg/agent.py b/trezor_agent/gpg/agent.py index 6140cf1..c3e3156 100644 --- a/trezor_agent/gpg/agent.py +++ b/trezor_agent/gpg/agent.py @@ -41,7 +41,7 @@ def pksign(keygrip, digest, algo): assert algo == '8' pubkey_dict = decode.load_public_key( pubkey_bytes=keyring.export_public_key(user_id=None), - use_custom=True) + 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) @@ -85,7 +85,7 @@ def pkdecrypt(keygrip, conn): local_pubkey = decode.load_public_key( pubkey_bytes=keyring.export_public_key(user_id=None), - use_custom=True) + 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) diff --git a/trezor_agent/gpg/decode.py b/trezor_agent/gpg/decode.py index 8cbb454..13f2b36 100644 --- a/trezor_agent/gpg/decode.py +++ b/trezor_agent/gpg/decode.py @@ -268,7 +268,7 @@ def digest_packets(packets): return hashlib.sha256(data_to_hash.getvalue()).digest() -def load_public_key(pubkey_bytes, use_custom=False): +def load_public_key(pubkey_bytes, use_custom=False, ecdh=False): """Parse and validate GPG public key from an input stream.""" stream = io.BytesIO(pubkey_bytes) packets = list(parse_packets(util.Reader(stream))) @@ -288,8 +288,9 @@ def load_public_key(pubkey_bytes, use_custom=False): packet = pubkey while use_custom: if packet['type'] in ('pubkey', 'subkey') and signature['_is_custom']: - log.debug('found custom %s', packet['type']) - break + if ecdh == (packet['algo'] == proto.ECDH_ALGO_ID): + log.debug('found custom %s', packet['type']) + break packet, signature = packets[:2] packets = packets[2:] diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 862d8f8..26df9f3 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -202,7 +202,7 @@ def load_from_public_key(pubkey_dict): conn = HardwareSigner(user_id, curve_name=curve_name) pubkey = proto.PublicKey( curve_name=curve_name, created=created, - verifying_key=conn.pubkey(), ecdh=ecdh) + 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) From c98cb22ba4f92d0562a0a0c5f962af2dc03607b9 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 17 Jun 2016 09:34:05 +0300 Subject: [PATCH 33/37] gpg: use separate derivations for GPG keys --- trezor_agent/gpg/__main__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/trezor_agent/gpg/__main__.py b/trezor_agent/gpg/__main__.py index b061b43..1107b5a 100755 --- a/trezor_agent/gpg/__main__.py +++ b/trezor_agent/gpg/__main__.py @@ -18,7 +18,8 @@ def run_create(args): user_id = os.environ['TREZOR_GPG_USER_ID'] conn = encode.HardwareSigner(user_id=user_id, curve_name=args.ecdsa_curve) - verifying_key = conn.pubkey() + 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) @@ -29,7 +30,7 @@ def run_create(args): # subkey for encryption encryption_key = proto.PublicKey( curve_name=args.ecdsa_curve, created=args.time, - verifying_key=verifying_key, ecdh=True) + verifying_key=decryption_key, ecdh=True) result = encode.create_subkey(primary_bytes=primary_bytes, pubkey=signing_key, signer_func=conn.sign) @@ -44,7 +45,7 @@ def run_create(args): # subkey for encryption subkey = proto.PublicKey( curve_name=args.ecdsa_curve, created=args.time, - verifying_key=verifying_key, ecdh=True) + verifying_key=decryption_key, ecdh=True) result = encode.create_primary(user_id=user_id, pubkey=primary, From 6c96cc37b91a8bb6c0b6e321f4d503df71652da1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 17 Jun 2016 21:59:13 +0300 Subject: [PATCH 34/37] gpg: add support for adding subkeys to EdDSA primary GPG keys --- trezor_agent/gpg/keyring.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/trezor_agent/gpg/keyring.py b/trezor_agent/gpg/keyring.py index 3384b18..f3e7a4f 100644 --- a/trezor_agent/gpg/keyring.py +++ b/trezor_agent/gpg/keyring.py @@ -93,8 +93,9 @@ def _parse_ecdsa_sig(args): return (util.bytes2num(sig_r), util.bytes2num(sig_s)) -# DSA happens to have the same structure as ECDSA signatures +# DSA and EDDSA happen to have the same structure as ECDSA signatures _parse_dsa_sig = _parse_ecdsa_sig +_parse_eddsa_sig = _parse_ecdsa_sig def _parse_rsa_sig(args): @@ -110,6 +111,7 @@ def parse_sig(sig): algo_name = sig[0] parser = {b'rsa': _parse_rsa_sig, b'ecdsa': _parse_ecdsa_sig, + b'eddsa': _parse_eddsa_sig, b'dsa': _parse_dsa_sig}[algo_name] return parser(args=sig[1:]) From 7b3874e6f7ed5e361b10478dc6d45332a134c992 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 17 Jun 2016 22:05:13 +0300 Subject: [PATCH 35/37] gpg: fixup logging during key creation --- trezor_agent/gpg/encode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py index 26df9f3..c6712e4 100644 --- a/trezor_agent/gpg/encode.py +++ b/trezor_agent/gpg/encode.py @@ -87,7 +87,7 @@ 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('signing public key "%s"', user_id) + log.info('creating primary GPG key "%s"', user_id) hashed_subpackets = [ proto.subpacket_time(pubkey.created), # signature time # https://tools.ietf.org/html/rfc4880#section-5.2.3.7 @@ -106,6 +106,7 @@ def create_primary(user_id, pubkey, signer_func): proto.subpacket(16, pubkey.key_id()), # issuer key id proto.CUSTOM_SUBPACKET] + log.info('confirm signing with primary key') signature = proto.make_signature( signer_func=signer_func, public_algo=pubkey.algo_id, @@ -122,8 +123,7 @@ def create_subkey(primary_bytes, pubkey, signer_func): """Export new subkey to GPG primary key.""" subkey_packet = proto.packet(tag=14, blob=pubkey.data()) primary = decode.load_public_key(primary_bytes) - log.info('adding subkey to primary GPG key "%s" (%s)', - primary['user_id'], util.hexlify(primary['key_id'])) + log.info('adding subkey to primary GPG key "%s"', primary['user_id']) data_to_sign = primary['_to_hash'] + pubkey.data_to_hash() if pubkey.ecdh: From 45a85a317b80afd0cc495f34633027a7ccfeb4b1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 18 Jun 2016 20:10:52 +0300 Subject: [PATCH 36/37] gpg: allow setting UNIX socket from command-line --- trezor_agent/gpg/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/trezor_agent/gpg/__main__.py b/trezor_agent/gpg/__main__.py index 1107b5a..6c64120 100755 --- a/trezor_agent/gpg/__main__.py +++ b/trezor_agent/gpg/__main__.py @@ -57,9 +57,9 @@ def run_create(args): sys.stdout.write(proto.armor(result, 'PUBLIC KEY BLOCK')) -def run_agent(_): +def run_agent(args): """Run a simple GPG-agent server.""" - sock_path = os.path.expanduser('~/.gnupg/S.gpg-agent') + sock_path = os.path.expanduser(args.sock_path) with server.unix_domain_socket_server(sock_path) as sock: for conn in agent.yield_connections(sock): with contextlib.closing(conn): @@ -81,6 +81,7 @@ def main(): create_cmd.set_defaults(run=run_create) agent_cmd = subparsers.add_parser('agent') + agent_cmd.add_argument('-s', '--sock-path', default='~/.gnupg/S.gpg-agent') agent_cmd.set_defaults(run=run_agent) args = p.parse_args() From 75f879edbbb73add47de11401ef097644a97ea42 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 20 Jun 2016 22:13:05 +0300 Subject: [PATCH 37/37] gpg: update README.md --- trezor_agent/gpg/README.md | 95 +++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/trezor_agent/gpg/README.md b/trezor_agent/gpg/README.md index 09e8c9e..e3a9575 100644 --- a/trezor_agent/gpg/README.md +++ b/trezor_agent/gpg/README.md @@ -1,16 +1,17 @@ # Using TREZOR as hardware GPG agent -## Generate new GPG signing key: First, verify that you have GPG 2.1+ [installed](https://gist.github.com/vt0r/a2f8c0bcb1400131ff51): + ``` $ gpg2 --version | head -n1 gpg (GnuPG) 2.1.11 ``` -Update you TREZOR firmware to the latest version (at least [5430c82](https://github.com/trezor/trezor-mcu/commit/5430c82b2b1dbdd43c89de419ef92b754bed4c91)): see [a sample build log](https://gist.github.com/romanz/324c8e513abf5b5a452602ed648fa2cf). -Install the latest development version of `trezor-agent`: +Update you TREZOR firmware to the latest version (at least [c720614](https://github.com/trezor/trezor-mcu/commit/c720614f6e9b9c07f446c95bda0257980d942871)). + +Install latest `trezor-agent` package from [gpg-agent](https://github.com/romanz/trezor-agent/commits/gpg-agent) branch: ``` -$ pip install git+https://github.com/romanz/trezor-agent.git@master +$ pip install git+https://github.com/romanz/trezor-agent.git@gpg-agent ``` Define your GPG user ID as an environment variable: @@ -20,38 +21,80 @@ $ export TREZOR_GPG_USER_ID="John Doe " There are two ways to generate TREZOR-based GPG public keys, as described below. -### (1) create new GPG identity: -``` -$ trezor-gpg create > identity.pub # create new TREZOR-based GPG identity -$ gpg2 --import identity.pub # import into local GPG public keyring -$ gpg2 --list-keys # verify that the new identity is created correctly -$ gpg2 --edit "${TREZOR_GPG_USER_ID}" trust # OPTIONAL: mark the key as trusted -``` -[![asciicast](https://asciinema.org/a/44880.png)](https://asciinema.org/a/44880) +## 1. generate a new GPG identity: -### (2) create new subkey for an existing GPG identity: ``` -$ gpg2 --list-keys "${TREZOR_GPG_USER_ID}" # make sure this identity already exists -$ trezor-gpg create --subkey > identity.pub # create new TREZOR-based GPG subkey -$ gpg2 --import identity.pub # append it to an existing identity -$ gpg2 --list-keys "${TREZOR_GPG_USER_ID}" # verify that the new subkey is added to keyring -``` -[![subkey](https://asciinema.org/a/8t78s6pqo5yocisaiolqnjp63.png)](https://asciinema.org/a/8t78s6pqo5yocisaiolqnjp63) +$ trezor-gpg create | gpg2 --import # use the TREZOR to confirm signing the primary key +gpg: key 5E4D684D: public key "John Doe " imported +gpg: Total number processed: 1 +gpg: imported: 1 -## Generate GPG signatures using a TREZOR device: +$ 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 +sub nistp256/A31D9E25 2016-06-17 [E] ``` -$ trezor-gpg sign EXAMPLE # confirm signature using the device -$ gpg2 --verify EXAMPLE.asc # verify using standard GPG binary + +## 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 +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 " 2 new signatures +gpg: key 87BB07B4: "John Doe " 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 +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 " [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 " +Fri Jun 17 20:55:31 IDT 2016 ``` -[![sign](https://asciinema.org/a/f1unkptesb7anq09i8wugoko6.png)](https://asciinema.org/a/f1unkptesb7anq09i8wugoko6) ## 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 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 -``` -[![asciicast](https://asciinema.org/a/44879.png)](https://asciinema.org/a/44879) +``` \ No newline at end of file