From 4335740abebe7de0312c3a9fdacfe6458581287b Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 16 Apr 2016 21:21:12 +0300 Subject: [PATCH 01/31] Add experimental support for GPG signing via TREZOR In order to use this feature, GPG "modern" (v2.1) is required [1]. Also, since TREZOR protocol does not support arbitrary long fields, TREZOR firmware needs to be adapted with the following patch [2], to support signing fixed-size digests of GPG messages of arbitrary size. [1] https://gist.github.com/vt0r/a2f8c0bcb1400131ff51 [2] https://gist.github.com/romanz/b66f5df1ca8ef15641df8ea5bb09fd47 --- gpg/check.py | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++ gpg/demo.sh | 14 +++ gpg/signer.py | 222 ++++++++++++++++++++++++++++++++++++++ gpg/util.py | 18 ++++ 4 files changed, 544 insertions(+) create mode 100755 gpg/check.py create mode 100755 gpg/demo.sh create mode 100755 gpg/signer.py create mode 100644 gpg/util.py diff --git a/gpg/check.py b/gpg/check.py new file mode 100755 index 0000000..88d5423 --- /dev/null +++ b/gpg/check.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +import argparse +import base64 +import binascii +import contextlib +import hashlib +import io +import logging +import struct + +import ecdsa + +import util + +log = logging.getLogger(__name__) + + +def bit(value, i): + return 1 if value & (1 << i) else 0 + + +def low_bits(value, n): + return value & ((1 << n) - 1) + + +def readfmt(stream, fmt): + size = struct.calcsize(fmt) + blob = stream.read(size) + return struct.unpack(fmt, blob) + + +class Reader(object): + def __init__(self, stream): + self.s = stream + self._captured = None + + def readfmt(self, fmt): + size = struct.calcsize(fmt) + blob = self.read(size) + obj, = struct.unpack(fmt, blob) + return obj + + def read(self, size=None): + blob = self.s.read(size) + if size is not None and len(blob) < size: + raise EOFError + if self._captured: + self._captured.write(blob) + return blob + + @contextlib.contextmanager + def capture(self, stream): + self._captured = stream + try: + yield + finally: + self._captured = None + +length_types = {0: '>B', 1: '>H', 2: '>L'} + + +def parse_subpackets(s): + subpackets = [] + total_size = s.readfmt('>H') + data = s.read(total_size) + s = Reader(io.BytesIO(data)) + + while True: + try: + subpacket_len = s.readfmt('B') + except EOFError: + break + + subpackets.append(s.read(subpacket_len)) + + return subpackets + + +def parse_mpi(s): + bits = s.readfmt('>H') + blob = bytearray(s.read(int((bits + 7) // 8))) + return sum(v << (8 * i) for i, v in enumerate(reversed(blob))) + + +def split_bits(value, *bits): + result = [] + for b in reversed(bits): + mask = (1 << b) - 1 + result.append(value & mask) + value = value >> b + assert value == 0 + return reversed(result) + + +class Parser(object): + def __init__(self, stream, to_hash=None): + self.stream = stream + self.packet_types = { + 2: self.signature, + 4: self.onepass, + 6: self.pubkey, + 11: self.literal, + 13: self.user_id, + } + self.to_hash = io.BytesIO() + if to_hash: + self.to_hash.write(to_hash) + + def __iter__(self): + return self + + def onepass(self, stream): + # pylint: disable=no-self-use + p = {'type': 'onepass'} + p['version'] = stream.readfmt('B') + p['sig_type'] = stream.readfmt('B') + p['hash_alg'] = stream.readfmt('B') + p['pubkey_alg'] = stream.readfmt('B') + p['key_id'] = stream.readfmt('8s') + p['nested'] = stream.readfmt('B') + assert not stream.read() + return p + + def literal(self, stream): + p = {'type': 'literal'} + p['format'] = stream.readfmt('c') + filename_len = stream.readfmt('B') + p['filename'] = stream.read(filename_len) + p['date'] = stream.readfmt('>L') + with stream.capture(self.to_hash): + p['content'] = stream.read() + return p + + def signature(self, stream): + p = {'type': 'signature'} + + to_hash = io.BytesIO() + with stream.capture(to_hash): + p['version'] = stream.readfmt('B') + p['sig_type'] = stream.readfmt('B') + p['pubkey_alg'] = stream.readfmt('B') + p['hash_alg'] = stream.readfmt('B') + p['hashed_subpackets'] = parse_subpackets(stream) + self.to_hash.write(to_hash.getvalue()) + + # https://tools.ietf.org/html/rfc4880#section-5.2.4 + self.to_hash.write(b'\x04\xff' + struct.pack('>L', to_hash.tell())) + data_to_sign = self.to_hash.getvalue() + log.debug('hashing %d bytes for signature: %r', + len(data_to_sign), data_to_sign) + digest = hashlib.sha256(data_to_sign).digest() + + p['unhashed_subpackets'] = parse_subpackets(stream) + p['hash_prefix'] = stream.readfmt('2s') + if p['hash_prefix'] != digest[:2]: + log.warning('Bad hash prefix: %r (expected %r)', + digest[:2], p['hash_prefix']) + else: + p['digest'] = digest + p['sig'] = (parse_mpi(stream), parse_mpi(stream)) + assert not stream.read() + + return p + + def pubkey(self, stream): + p = {'type': 'pubkey'} + packet = io.BytesIO() + with stream.capture(packet): + p['version'] = stream.readfmt('B') + p['created'] = stream.readfmt('>L') + p['algo'] = stream.readfmt('B') + + # https://tools.ietf.org/html/rfc6637#section-11 + oid_size = stream.readfmt('B') + oid = stream.read(oid_size) + assert oid == b'\x2A\x86\x48\xCE\x3D\x03\x01\x07' # NIST P-256 + + mpi = parse_mpi(stream) + log.debug('mpi: %x', mpi) + prefix, x, y = split_bits(mpi, 4, 256, 256) + assert prefix == 4 + p['point'] = (x, y) + assert not stream.read() + + # https://tools.ietf.org/html/rfc4880#section-12.2 + packet_data = packet.getvalue() + data_to_hash = (b'\x99' + struct.pack('>H', len(packet_data)) + + packet_data) + p['key_id'] = hashlib.sha1(data_to_hash).digest()[-8:] + log.debug('key ID: %s', binascii.hexlify(p['key_id']).decode('ascii')) + self.to_hash.write(data_to_hash) + + return p + + def user_id(self, stream): + value = stream.read() + self.to_hash.write(b'\xb4' + struct.pack('>L', len(value))) + self.to_hash.write(value) + return {'type': 'user_id', 'value': value} + + def __next__(self): + try: + # https://tools.ietf.org/html/rfc4880#section-4.2 + value = self.stream.readfmt('B') + except EOFError: + raise StopIteration + + log.debug('prefix byte: %02x', value) + assert bit(value, 7) == 1 + assert bit(value, 6) == 0 # new format not supported yet + + tag = low_bits(value, 6) + length_type = low_bits(tag, 2) + tag = tag >> 2 + fmt = length_types[length_type] + log.debug('length_type: %s', fmt) + packet_size = self.stream.readfmt(fmt) + log.debug('packet length: %d', packet_size) + packet_data = self.stream.read(packet_size) + packet_type = self.packet_types.get(tag) + if packet_type: + p = packet_type(Reader(io.BytesIO(packet_data))) + else: + p = {'type': 'UNKNOWN'} + p['tag'] = tag + log.debug('packet "%s": %s', p['type'], p) + return p + + next = __next__ + + +def original_data(filename): + parts = filename.rsplit('.', 1) + if len(parts) == 2 and parts[1] in ('sig', 'asc'): + log.debug('loading file %s', parts[0]) + return open(parts[0], 'rb').read() + + +def load_public_key(filename): + parser = Parser(Reader(open(filename, 'rb'))) + pubkey, userid, signature = list(parser) + log.info('loaded %s public key', userid['value']) + verify_digest(pubkey=pubkey, digest=signature['digest'], + signature=signature['sig'], label=filename) + return pubkey + + +def check(pubkey, sig_file): + d = open(sig_file, 'rb') + if d.name.endswith('.asc'): + lines = d.readlines()[3:-1] + data = base64.b64decode(''.join(lines)) + payload, checksum = data[:-3], data[-3:] + assert util.crc24(payload) == checksum + d = io.BytesIO(payload) + parser = Parser(Reader(d), original_data(sig_file)) + signature, = list(parser) + verify_digest(pubkey=pubkey, digest=signature['digest'], + signature=signature['sig'], label=sig_file) + + +def verify_digest(pubkey, digest, signature, label): + coords = pubkey['point'] + point = ecdsa.ellipticcurve.Point(curve=ecdsa.NIST256p.curve, + x=coords[0], y=coords[1]) + v = ecdsa.VerifyingKey.from_public_point(point=point, + curve=ecdsa.curves.NIST256p, + hashfunc=hashlib.sha256) + try: + v.verify_digest(signature=signature, + digest=digest, + sigdecode=lambda rs, order: rs) + log.info('%s is OK', label) + except ecdsa.keys.BadSignatureError: + log.error('%s has bad signature!', label) + raise + + +def main(): + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)-10s %(message)s') + p = argparse.ArgumentParser() + p.add_argument('pubkey') + p.add_argument('signature') + args = p.parse_args() + check(pubkey=load_public_key(args.pubkey), + sig_file=args.signature) + +if __name__ == '__main__': + main() diff --git a/gpg/demo.sh b/gpg/demo.sh new file mode 100755 index 0000000..6bc7480 --- /dev/null +++ b/gpg/demo.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -x +CREATED=1460731897 # needed for consistent public key creation +NAME="trezor_key" # will be used as GPG user id and public key name + +echo "Hello GPG World!" > EXAMPLE +./signer.py $NAME --time $CREATED --public-key --file EXAMPLE --verbose +./check.py $NAME.pub EXAMPLE.sig # pure Python verification + +# Install GPG v2.1 (modern) and verify the signature +gpg2 --import $NAME.pub +gpg2 --list-keys $NAME +# gpg2 --edit-key trezor_key trust # optional: mark it as trusted +gpg2 --verify EXAMPLE.sig diff --git a/gpg/signer.py b/gpg/signer.py new file mode 100755 index 0000000..1faf0a5 --- /dev/null +++ b/gpg/signer.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +import argparse +import base64 +import binascii +import hashlib +import logging +import struct +import time + +import ecdsa + +import trezor_agent.client +import trezor_agent.formats +import trezor_agent.util +import util + +log = logging.getLogger(__name__) + + +def prefix_len(fmt, blob): + return struct.pack(fmt, len(blob)) + blob + + +def packet(tag, blob): + assert len(blob) < 256 + length_type = 0 # : 1 byte for length + leading_byte = 0x80 | (tag << 2) | (length_type) + return struct.pack('>B', leading_byte) + prefix_len('>B', blob) + + +def subpacket(subpacket_type, fmt, *values): + blob = struct.pack(fmt, *values) if values else fmt + return struct.pack('>B', subpacket_type) + blob + + +def subpacket_long(subpacket_type, value): + return subpacket(subpacket_type, '>L', value) + + +def subpacket_time(value): + return subpacket_long(2, value) + + +def subpacket_byte(subpacket_type, value): + return subpacket(subpacket_type, '>B', value) + + +def subpackets(*items): + prefixed = [prefix_len('>B', item) for item in items] + return prefix_len('>H', b''.join(prefixed)) + + +def mpi(value): + bits = value.bit_length() + data_size = (bits + 7) // 8 + data_bytes = [0] * data_size + for i in range(data_size): + data_bytes[i] = value & 0xFF + value = value >> 8 + + data_bytes.reverse() + return struct.pack('>H', bits) + bytearray(data_bytes) + + +def time_format(t): + return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t)) + + +def hexlify(blob): + return binascii.hexlify(blob).decode('ascii').upper() + + +def split_lines(body, size): + lines = [] + for i in range(0, len(body), size): + lines.append(body[i:i+size] + '\n') + return ''.join(lines) + +def armor_sig(blob): + head = '-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v2\n\n' + body = base64.b64encode(blob) + checksum = base64.b64encode(util.crc24(blob)) + tail = '-----END PGP SIGNATURE-----\n' + return head + split_lines(body + '=' + checksum, 64) + tail + +class Signer(object): + + curve = ecdsa.NIST256p + ecdsa_curve_name = trezor_agent.formats.CURVE_NIST256 + + def __init__(self, user_id, created): + self.user_id = user_id + self.client_wrapper = trezor_agent.factory.load() + + # This requires the following patch to trezor-mcu to work: + # https://gist.github.com/romanz/b66f5df1ca8ef15641df8ea5bb09fd47 + self.identity = self.client_wrapper.identity_type() + self.identity.proto = 'gpg' + self.identity.host = user_id + addr = trezor_agent.client.get_address(self.identity) + public_node = self.client_wrapper.connection.get_public_node( + n=addr, ecdsa_curve_name=self.ecdsa_curve_name) + + verifying_key = trezor_agent.formats.decompress_pubkey( + pubkey=public_node.node.public_key, + curve_name=self.ecdsa_curve_name) + + self.created = int(created) + header = struct.pack('>BLB', + 4, # version + self.created, # creation + 19) # ECDSA + + # https://tools.ietf.org/html/rfc6637#section-11 (NIST P-256 OID) + oid = prefix_len('>B', b'\x2A\x86\x48\xCE\x3D\x03\x01\x07') + + point = verifying_key.pubkey.point + self.pubkey_data = header + oid + mpi((4 << 512) | + (point.x() << 256) | + (point.y())) + + self.data_to_hash = b'\x99' + prefix_len('>H', self.pubkey_data) + fingerprint = hashlib.sha1(self.data_to_hash).digest() + self.key_id = fingerprint[-8:] + log.info('key %s created at %s', + hexlify(fingerprint[-4:]), time_format(self.created)) + + def close(self): + self.client_wrapper.connection.clear_session() + self.client_wrapper.connection.close() + + def export(self): + pubkey_packet = packet(tag=6, blob=self.pubkey_data) + user_id_packet = packet(tag=13, blob=self.user_id) + + user_id_to_hash = user_id_packet[:1] + prefix_len('>L', self.user_id) + data_to_sign = self.data_to_hash + user_id_to_hash + log.info('signing user_id: %r', self.user_id.decode('ascii')) + hashed_subpackets = [ + subpacket_time(self.created), # signature creaion time + subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) + subpacket_byte(0x15, 8), # preferred hash (SHA256) + subpacket_byte(0x16, 0), # preferred compression (none) + subpacket_byte(0x17, 0x80)] # key server prefs (no-modify) + signature = self._make_signature(visual='Sign GPG public key', + data_to_sign=data_to_sign, + sig_type=0x13, # user id & public key + hashed_subpackets=hashed_subpackets) + + sign_packet = packet(tag=2, blob=signature) + return pubkey_packet + user_id_packet + sign_packet + + def sign(self, msg, sign_time=None): + if sign_time is None: + sign_time = int(time.time()) + + log.info('signing message %r at %s', msg, + time_format(sign_time)) + hashed_subpackets = [subpacket_time(sign_time)] + blob = self._make_signature( + visual='Sign GPG message', + data_to_sign=msg, hashed_subpackets=hashed_subpackets) + return packet(tag=2, blob=blob) + + def _make_signature(self, visual, data_to_sign, + hashed_subpackets, sig_type=0): + header = struct.pack('>BBBB', + 4, # version + sig_type, # rfc4880 (section-5.2.1) + 19, # pubkey_alg (ECDSA) + 8) # hash_alg (SHA256) + hashed = subpackets(*hashed_subpackets) + unhashed = subpackets( + subpacket(16, self.key_id) # issuer key id + ) + tail = b'\x04\xff' + struct.pack('>L', len(header) + len(hashed)) + data_to_hash = data_to_sign + header + hashed + tail + + log.debug('hashing %d bytes', len(data_to_hash)) + digest = hashlib.sha256(data_to_hash).digest() + + result = self.client_wrapper.connection.sign_identity( + identity=self.identity, + challenge_hidden=hashlib.sha256(data_to_hash).digest(), + challenge_visual=visual, + ecdsa_curve_name=self.ecdsa_curve_name) + assert result.signature[:1] == b'\x00' + sig = result.signature[1:] + sig = [trezor_agent.util.bytes2num(sig[:32]), + trezor_agent.util.bytes2num(sig[32:])] + + hash_prefix = digest[:2] # used for decoder's sanity check + signature = mpi(sig[0]) + mpi(sig[1]) # actual ECDSA signature + return header + hashed + unhashed + hash_prefix + signature + + +def main(): + p = argparse.ArgumentParser() + p.add_argument('user_id') + p.add_argument('-t', '--time', type=int, default=int(time.time())) + p.add_argument('-f', '--filename') + p.add_argument('-a', '--armor', action='store_true', default=False) + p.add_argument('-p', '--public-key', action='store_true', default=False) + p.add_argument('-v', '--verbose', action='store_true', default=False) + + args = p.parse_args() + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, + format='%(asctime)s %(levelname)-10s %(message)s') + s = Signer(user_id=args.user_id.encode('ascii'), created=args.time) + if args.public_key: + open(args.user_id + '.pub', 'wb').write(s.export()) + if args.filename: + data = open(args.filename, 'rb').read() + sig, ext = s.sign(data), '.sig' + if args.armor: + sig, ext = armor_sig(sig), '.asc' + open(args.filename + ext, 'wb').write(sig) + s.close() + + +if __name__ == '__main__': + main() diff --git a/gpg/util.py b/gpg/util.py new file mode 100644 index 0000000..a2b1f57 --- /dev/null +++ b/gpg/util.py @@ -0,0 +1,18 @@ +import struct + + +def crc24(blob): + CRC24_INIT = 0xB704CEL + CRC24_POLY = 0x1864CFBL + + crc = CRC24_INIT + for octet in bytearray(blob): + crc ^= (octet << 16) + for _ in range(8): + crc <<= 1 + if crc & 0x1000000: + crc ^= CRC24_POLY + assert 0 <= crc < 0x1000000 + crc_bytes = struct.pack('>L', crc) + assert crc_bytes[0] == b'\x00' + return crc_bytes[1:] From b9ba4a308278fed351d99384038b8bab70e51a5e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 17 Apr 2016 22:18:31 +0300 Subject: [PATCH 02/31] split decoding functionality --- gpg/check.py | 255 +------------------------------------------------- gpg/decode.py | 250 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 250 deletions(-) create mode 100644 gpg/decode.py diff --git a/gpg/check.py b/gpg/check.py index 88d5423..25a82dd 100755 --- a/gpg/check.py +++ b/gpg/check.py @@ -1,234 +1,15 @@ #!/usr/bin/env python import argparse import base64 -import binascii -import contextlib -import hashlib import io import logging -import struct - -import ecdsa +import decode import util log = logging.getLogger(__name__) -def bit(value, i): - return 1 if value & (1 << i) else 0 - - -def low_bits(value, n): - return value & ((1 << n) - 1) - - -def readfmt(stream, fmt): - size = struct.calcsize(fmt) - blob = stream.read(size) - return struct.unpack(fmt, blob) - - -class Reader(object): - def __init__(self, stream): - self.s = stream - self._captured = None - - def readfmt(self, fmt): - size = struct.calcsize(fmt) - blob = self.read(size) - obj, = struct.unpack(fmt, blob) - return obj - - def read(self, size=None): - blob = self.s.read(size) - if size is not None and len(blob) < size: - raise EOFError - if self._captured: - self._captured.write(blob) - return blob - - @contextlib.contextmanager - def capture(self, stream): - self._captured = stream - try: - yield - finally: - self._captured = None - -length_types = {0: '>B', 1: '>H', 2: '>L'} - - -def parse_subpackets(s): - subpackets = [] - total_size = s.readfmt('>H') - data = s.read(total_size) - s = Reader(io.BytesIO(data)) - - while True: - try: - subpacket_len = s.readfmt('B') - except EOFError: - break - - subpackets.append(s.read(subpacket_len)) - - return subpackets - - -def parse_mpi(s): - bits = s.readfmt('>H') - blob = bytearray(s.read(int((bits + 7) // 8))) - return sum(v << (8 * i) for i, v in enumerate(reversed(blob))) - - -def split_bits(value, *bits): - result = [] - for b in reversed(bits): - mask = (1 << b) - 1 - result.append(value & mask) - value = value >> b - assert value == 0 - return reversed(result) - - -class Parser(object): - def __init__(self, stream, to_hash=None): - self.stream = stream - self.packet_types = { - 2: self.signature, - 4: self.onepass, - 6: self.pubkey, - 11: self.literal, - 13: self.user_id, - } - self.to_hash = io.BytesIO() - if to_hash: - self.to_hash.write(to_hash) - - def __iter__(self): - return self - - def onepass(self, stream): - # pylint: disable=no-self-use - p = {'type': 'onepass'} - p['version'] = stream.readfmt('B') - p['sig_type'] = stream.readfmt('B') - p['hash_alg'] = stream.readfmt('B') - p['pubkey_alg'] = stream.readfmt('B') - p['key_id'] = stream.readfmt('8s') - p['nested'] = stream.readfmt('B') - assert not stream.read() - return p - - def literal(self, stream): - p = {'type': 'literal'} - p['format'] = stream.readfmt('c') - filename_len = stream.readfmt('B') - p['filename'] = stream.read(filename_len) - p['date'] = stream.readfmt('>L') - with stream.capture(self.to_hash): - p['content'] = stream.read() - return p - - def signature(self, stream): - p = {'type': 'signature'} - - to_hash = io.BytesIO() - with stream.capture(to_hash): - p['version'] = stream.readfmt('B') - p['sig_type'] = stream.readfmt('B') - p['pubkey_alg'] = stream.readfmt('B') - p['hash_alg'] = stream.readfmt('B') - p['hashed_subpackets'] = parse_subpackets(stream) - self.to_hash.write(to_hash.getvalue()) - - # https://tools.ietf.org/html/rfc4880#section-5.2.4 - self.to_hash.write(b'\x04\xff' + struct.pack('>L', to_hash.tell())) - data_to_sign = self.to_hash.getvalue() - log.debug('hashing %d bytes for signature: %r', - len(data_to_sign), data_to_sign) - digest = hashlib.sha256(data_to_sign).digest() - - p['unhashed_subpackets'] = parse_subpackets(stream) - p['hash_prefix'] = stream.readfmt('2s') - if p['hash_prefix'] != digest[:2]: - log.warning('Bad hash prefix: %r (expected %r)', - digest[:2], p['hash_prefix']) - else: - p['digest'] = digest - p['sig'] = (parse_mpi(stream), parse_mpi(stream)) - assert not stream.read() - - return p - - def pubkey(self, stream): - p = {'type': 'pubkey'} - packet = io.BytesIO() - with stream.capture(packet): - p['version'] = stream.readfmt('B') - p['created'] = stream.readfmt('>L') - p['algo'] = stream.readfmt('B') - - # https://tools.ietf.org/html/rfc6637#section-11 - oid_size = stream.readfmt('B') - oid = stream.read(oid_size) - assert oid == b'\x2A\x86\x48\xCE\x3D\x03\x01\x07' # NIST P-256 - - mpi = parse_mpi(stream) - log.debug('mpi: %x', mpi) - prefix, x, y = split_bits(mpi, 4, 256, 256) - assert prefix == 4 - p['point'] = (x, y) - assert not stream.read() - - # https://tools.ietf.org/html/rfc4880#section-12.2 - packet_data = packet.getvalue() - data_to_hash = (b'\x99' + struct.pack('>H', len(packet_data)) + - packet_data) - p['key_id'] = hashlib.sha1(data_to_hash).digest()[-8:] - log.debug('key ID: %s', binascii.hexlify(p['key_id']).decode('ascii')) - self.to_hash.write(data_to_hash) - - return p - - def user_id(self, stream): - value = stream.read() - self.to_hash.write(b'\xb4' + struct.pack('>L', len(value))) - self.to_hash.write(value) - return {'type': 'user_id', 'value': value} - - def __next__(self): - try: - # https://tools.ietf.org/html/rfc4880#section-4.2 - value = self.stream.readfmt('B') - except EOFError: - raise StopIteration - - log.debug('prefix byte: %02x', value) - assert bit(value, 7) == 1 - assert bit(value, 6) == 0 # new format not supported yet - - tag = low_bits(value, 6) - length_type = low_bits(tag, 2) - tag = tag >> 2 - fmt = length_types[length_type] - log.debug('length_type: %s', fmt) - packet_size = self.stream.readfmt(fmt) - log.debug('packet length: %d', packet_size) - packet_data = self.stream.read(packet_size) - packet_type = self.packet_types.get(tag) - if packet_type: - p = packet_type(Reader(io.BytesIO(packet_data))) - else: - p = {'type': 'UNKNOWN'} - p['tag'] = tag - log.debug('packet "%s": %s', p['type'], p) - return p - - next = __next__ - - def original_data(filename): parts = filename.rsplit('.', 1) if len(parts) == 2 and parts[1] in ('sig', 'asc'): @@ -236,15 +17,6 @@ def original_data(filename): return open(parts[0], 'rb').read() -def load_public_key(filename): - parser = Parser(Reader(open(filename, 'rb'))) - pubkey, userid, signature = list(parser) - log.info('loaded %s public key', userid['value']) - verify_digest(pubkey=pubkey, digest=signature['digest'], - signature=signature['sig'], label=filename) - return pubkey - - def check(pubkey, sig_file): d = open(sig_file, 'rb') if d.name.endswith('.asc'): @@ -253,27 +25,10 @@ def check(pubkey, sig_file): payload, checksum = data[:-3], data[-3:] assert util.crc24(payload) == checksum d = io.BytesIO(payload) - parser = Parser(Reader(d), original_data(sig_file)) + parser = decode.Parser(decode.Reader(d), original_data(sig_file)) signature, = list(parser) - verify_digest(pubkey=pubkey, digest=signature['digest'], - signature=signature['sig'], label=sig_file) - - -def verify_digest(pubkey, digest, signature, label): - coords = pubkey['point'] - point = ecdsa.ellipticcurve.Point(curve=ecdsa.NIST256p.curve, - x=coords[0], y=coords[1]) - v = ecdsa.VerifyingKey.from_public_point(point=point, - curve=ecdsa.curves.NIST256p, - hashfunc=hashlib.sha256) - try: - v.verify_digest(signature=signature, - digest=digest, - sigdecode=lambda rs, order: rs) - log.info('%s is OK', label) - except ecdsa.keys.BadSignatureError: - log.error('%s has bad signature!', label) - raise + decode.verify_digest(pubkey=pubkey, digest=signature['digest'], + signature=signature['sig'], label=sig_file) def main(): @@ -283,7 +38,7 @@ def main(): p.add_argument('pubkey') p.add_argument('signature') args = p.parse_args() - check(pubkey=load_public_key(args.pubkey), + check(pubkey=decode.load_public_key(args.pubkey), sig_file=args.signature) if __name__ == '__main__': diff --git a/gpg/decode.py b/gpg/decode.py new file mode 100644 index 0000000..ec89336 --- /dev/null +++ b/gpg/decode.py @@ -0,0 +1,250 @@ +import binascii +import contextlib +import hashlib +import io +import logging +import struct + +import ecdsa + +log = logging.getLogger(__name__) + + +def bit(value, i): + return 1 if value & (1 << i) else 0 + + +def low_bits(value, n): + return value & ((1 << n) - 1) + + +def readfmt(stream, fmt): + size = struct.calcsize(fmt) + blob = stream.read(size) + return struct.unpack(fmt, blob) + + +class Reader(object): + def __init__(self, stream): + self.s = stream + self._captured = None + + def readfmt(self, fmt): + size = struct.calcsize(fmt) + blob = self.read(size) + obj, = struct.unpack(fmt, blob) + return obj + + def read(self, size=None): + blob = self.s.read(size) + if size is not None and len(blob) < size: + raise EOFError + if self._captured: + self._captured.write(blob) + return blob + + @contextlib.contextmanager + def capture(self, stream): + self._captured = stream + try: + yield + finally: + self._captured = None + +length_types = {0: '>B', 1: '>H', 2: '>L'} + + +def parse_subpackets(s): + subpackets = [] + total_size = s.readfmt('>H') + data = s.read(total_size) + s = Reader(io.BytesIO(data)) + + while True: + try: + subpacket_len = s.readfmt('B') + except EOFError: + break + + subpackets.append(s.read(subpacket_len)) + + return subpackets + + +def parse_mpi(s): + bits = s.readfmt('>H') + blob = bytearray(s.read(int((bits + 7) // 8))) + return sum(v << (8 * i) for i, v in enumerate(reversed(blob))) + + +def split_bits(value, *bits): + result = [] + for b in reversed(bits): + mask = (1 << b) - 1 + result.append(value & mask) + value = value >> b + assert value == 0 + return reversed(result) + + +class Parser(object): + def __init__(self, stream, to_hash=None): + self.stream = stream + self.packet_types = { + 2: self.signature, + 4: self.onepass, + 6: self.pubkey, + 11: self.literal, + 13: self.user_id, + } + self.to_hash = io.BytesIO() + if to_hash: + self.to_hash.write(to_hash) + + def __iter__(self): + return self + + def onepass(self, stream): + # pylint: disable=no-self-use + p = {'type': 'onepass'} + p['version'] = stream.readfmt('B') + p['sig_type'] = stream.readfmt('B') + p['hash_alg'] = stream.readfmt('B') + p['pubkey_alg'] = stream.readfmt('B') + p['key_id'] = stream.readfmt('8s') + p['nested'] = stream.readfmt('B') + assert not stream.read() + return p + + def literal(self, stream): + p = {'type': 'literal'} + p['format'] = stream.readfmt('c') + filename_len = stream.readfmt('B') + p['filename'] = stream.read(filename_len) + p['date'] = stream.readfmt('>L') + with stream.capture(self.to_hash): + p['content'] = stream.read() + return p + + def signature(self, stream): + p = {'type': 'signature'} + + to_hash = io.BytesIO() + with stream.capture(to_hash): + p['version'] = stream.readfmt('B') + p['sig_type'] = stream.readfmt('B') + p['pubkey_alg'] = stream.readfmt('B') + p['hash_alg'] = stream.readfmt('B') + p['hashed_subpackets'] = parse_subpackets(stream) + self.to_hash.write(to_hash.getvalue()) + + # https://tools.ietf.org/html/rfc4880#section-5.2.4 + self.to_hash.write(b'\x04\xff' + struct.pack('>L', to_hash.tell())) + data_to_sign = self.to_hash.getvalue() + log.debug('hashing %d bytes for signature: %r', + len(data_to_sign), data_to_sign) + digest = hashlib.sha256(data_to_sign).digest() + + p['unhashed_subpackets'] = parse_subpackets(stream) + p['hash_prefix'] = stream.readfmt('2s') + if p['hash_prefix'] != digest[:2]: + log.warning('Bad hash prefix: %r (expected %r)', + digest[:2], p['hash_prefix']) + else: + p['digest'] = digest + p['sig'] = (parse_mpi(stream), parse_mpi(stream)) + assert not stream.read() + + return p + + def pubkey(self, stream): + p = {'type': 'pubkey'} + packet = io.BytesIO() + with stream.capture(packet): + p['version'] = stream.readfmt('B') + p['created'] = stream.readfmt('>L') + p['algo'] = stream.readfmt('B') + + # https://tools.ietf.org/html/rfc6637#section-11 + oid_size = stream.readfmt('B') + oid = stream.read(oid_size) + assert oid == b'\x2A\x86\x48\xCE\x3D\x03\x01\x07' # NIST P-256 + + mpi = parse_mpi(stream) + log.debug('mpi: %x', mpi) + prefix, x, y = split_bits(mpi, 4, 256, 256) + assert prefix == 4 + p['point'] = (x, y) + assert not stream.read() + + # https://tools.ietf.org/html/rfc4880#section-12.2 + packet_data = packet.getvalue() + data_to_hash = (b'\x99' + struct.pack('>H', len(packet_data)) + + packet_data) + p['key_id'] = hashlib.sha1(data_to_hash).digest()[-8:] + log.debug('key ID: %s', binascii.hexlify(p['key_id']).decode('ascii')) + self.to_hash.write(data_to_hash) + + return p + + def user_id(self, stream): + value = stream.read() + self.to_hash.write(b'\xb4' + struct.pack('>L', len(value))) + self.to_hash.write(value) + return {'type': 'user_id', 'value': value} + + def __next__(self): + try: + # https://tools.ietf.org/html/rfc4880#section-4.2 + value = self.stream.readfmt('B') + except EOFError: + raise StopIteration + + log.debug('prefix byte: %02x', value) + assert bit(value, 7) == 1 + assert bit(value, 6) == 0 # new format not supported yet + + tag = low_bits(value, 6) + length_type = low_bits(tag, 2) + tag = tag >> 2 + fmt = length_types[length_type] + log.debug('length_type: %s', fmt) + packet_size = self.stream.readfmt(fmt) + log.debug('packet length: %d', packet_size) + packet_data = self.stream.read(packet_size) + packet_type = self.packet_types.get(tag) + if packet_type: + p = packet_type(Reader(io.BytesIO(packet_data))) + else: + p = {'type': 'UNKNOWN'} + p['tag'] = tag + log.debug('packet "%s": %s', p['type'], p) + return p + + next = __next__ + + +def load_public_key(filename): + parser = Parser(Reader(open(filename, 'rb'))) + pubkey, userid, signature = list(parser) + log.info('loaded %s public key', userid['value']) + verify_digest(pubkey=pubkey, digest=signature['digest'], + signature=signature['sig'], label=filename) + return pubkey + + +def verify_digest(pubkey, digest, signature, label): + coords = pubkey['point'] + point = ecdsa.ellipticcurve.Point(curve=ecdsa.NIST256p.curve, + x=coords[0], y=coords[1]) + v = ecdsa.VerifyingKey.from_public_point(point=point, + curve=ecdsa.curves.NIST256p, + hashfunc=hashlib.sha256) + try: + v.verify_digest(signature=signature, + digest=digest, + sigdecode=lambda rs, order: rs) + log.info('%s is OK', label) + except ecdsa.keys.BadSignatureError: + log.error('%s has bad signature!', label) + raise From 34670c601d0c1d6de7978bf792095b02312afe70 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 17 Apr 2016 22:19:14 +0300 Subject: [PATCH 03/31] Fix PEP8 warnings --- gpg/signer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gpg/signer.py b/gpg/signer.py index 1faf0a5..f654b37 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -76,6 +76,7 @@ def split_lines(body, size): lines.append(body[i:i+size] + '\n') return ''.join(lines) + def armor_sig(blob): head = '-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v2\n\n' body = base64.b64encode(blob) @@ -83,6 +84,7 @@ def armor_sig(blob): tail = '-----END PGP SIGNATURE-----\n' return head + split_lines(body + '=' + checksum, 64) + tail + class Signer(object): curve = ecdsa.NIST256p From add90e3c51e91afbef767e7c92bb7c9c244b9247 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 17 Apr 2016 22:40:02 +0300 Subject: [PATCH 04/31] signer: support armoring public keys --- gpg/signer.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/gpg/signer.py b/gpg/signer.py index f654b37..310711c 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -70,21 +70,6 @@ def hexlify(blob): return binascii.hexlify(blob).decode('ascii').upper() -def split_lines(body, size): - lines = [] - for i in range(0, len(body), size): - lines.append(body[i:i+size] + '\n') - return ''.join(lines) - - -def armor_sig(blob): - head = '-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v2\n\n' - body = base64.b64encode(blob) - checksum = base64.b64encode(util.crc24(blob)) - tail = '-----END PGP SIGNATURE-----\n' - return head + split_lines(body + '=' + checksum, 64) + tail - - class Signer(object): curve = ecdsa.NIST256p @@ -196,6 +181,21 @@ class Signer(object): return header + hashed + unhashed + hash_prefix + signature +def split_lines(body, size): + lines = [] + for i in range(0, len(body), size): + lines.append(body[i:i+size] + '\n') + return ''.join(lines) + + +def armor(blob, type_str): + head = '-----BEGIN PGP {}-----\nVersion: GnuPG v2\n\n'.format(type_str) + body = base64.b64encode(blob) + checksum = base64.b64encode(util.crc24(blob)) + tail = '-----END PGP {}-----\n'.format(type_str) + return head + split_lines(body, 64) + '=' + checksum + '\n' + tail + + def main(): p = argparse.ArgumentParser() p.add_argument('user_id') @@ -210,13 +210,20 @@ def main(): format='%(asctime)s %(levelname)-10s %(message)s') s = Signer(user_id=args.user_id.encode('ascii'), created=args.time) if args.public_key: - open(args.user_id + '.pub', 'wb').write(s.export()) + pubkey = s.export() + ext = '.pub' + if args.armor: + pubkey = armor(pubkey, 'PUBLIC KEY BLOCK') + ext = '.asc' + open(args.user_id + ext, 'wb').write(pubkey) + if args.filename: data = open(args.filename, 'rb').read() sig, ext = s.sign(data), '.sig' if args.armor: - sig, ext = armor_sig(sig), '.asc' + sig, ext = armor(sig, 'SIGNATURE'), '.asc' open(args.filename + ext, 'wb').write(sig) + s.close() From 447faf973c5487e0d720b6066b6fb142f926a08d Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 17 Apr 2016 23:03:41 +0300 Subject: [PATCH 05/31] signer should export public key or sign a file --- gpg/check.py | 4 ++-- gpg/decode.py | 8 ++++---- gpg/demo.sh | 11 ++++++++--- gpg/signer.py | 27 ++++++++++++++++++++++----- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/gpg/check.py b/gpg/check.py index 25a82dd..db4e0e1 100755 --- a/gpg/check.py +++ b/gpg/check.py @@ -28,7 +28,7 @@ def check(pubkey, sig_file): parser = decode.Parser(decode.Reader(d), original_data(sig_file)) signature, = list(parser) decode.verify_digest(pubkey=pubkey, digest=signature['digest'], - signature=signature['sig'], label=sig_file) + signature=signature['sig'], label='GPG signature') def main(): @@ -38,7 +38,7 @@ def main(): p.add_argument('pubkey') p.add_argument('signature') args = p.parse_args() - check(pubkey=decode.load_public_key(args.pubkey), + check(pubkey=decode.load_public_key(open(args.pubkey, 'rb')), sig_file=args.signature) if __name__ == '__main__': diff --git a/gpg/decode.py b/gpg/decode.py index ec89336..c936c03 100644 --- a/gpg/decode.py +++ b/gpg/decode.py @@ -224,12 +224,12 @@ class Parser(object): next = __next__ -def load_public_key(filename): - parser = Parser(Reader(open(filename, 'rb'))) +def load_public_key(stream): + parser = Parser(Reader(stream)) pubkey, userid, signature = list(parser) log.info('loaded %s public key', userid['value']) verify_digest(pubkey=pubkey, digest=signature['digest'], - signature=signature['sig'], label=filename) + signature=signature['sig'], label='GPG public key') return pubkey @@ -246,5 +246,5 @@ def verify_digest(pubkey, digest, signature, label): sigdecode=lambda rs, order: rs) log.info('%s is OK', label) except ecdsa.keys.BadSignatureError: - log.error('%s has bad signature!', label) + log.error('Bad %s!', label) raise diff --git a/gpg/demo.sh b/gpg/demo.sh index 6bc7480..a012157 100755 --- a/gpg/demo.sh +++ b/gpg/demo.sh @@ -4,11 +4,16 @@ CREATED=1460731897 # needed for consistent public key creation NAME="trezor_key" # will be used as GPG user id and public key name echo "Hello GPG World!" > EXAMPLE -./signer.py $NAME --time $CREATED --public-key --file EXAMPLE --verbose -./check.py $NAME.pub EXAMPLE.sig # pure Python verification +# Create, sign and export the public key +./signer.py $NAME --time $CREATED --public-key --verbose -# Install GPG v2.1 (modern) and verify the signature +# Install GPG v2.1 (modern) and import the public key gpg2 --import $NAME.pub gpg2 --list-keys $NAME + +# Perform actual GPG signature using TREZOR +./signer.py $NAME --file EXAMPLE --verbose +./check.py $NAME.pub EXAMPLE.sig # pure Python verification + # gpg2 --edit-key trezor_key trust # optional: mark it as trusted gpg2 --verify EXAMPLE.sig diff --git a/gpg/signer.py b/gpg/signer.py index 310711c..ac64fe3 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -3,12 +3,15 @@ import argparse import base64 import binascii import hashlib +import io import logging import struct +import subprocess import time import ecdsa +import decode import trezor_agent.client import trezor_agent.formats import trezor_agent.util @@ -196,20 +199,29 @@ def armor(blob, type_str): return head + split_lines(body, 64) + '=' + checksum + '\n' + tail +def load_from_gpg(user_id): + pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) + pubkey = decode.load_public_key(io.BytesIO(pubkey_bytes)) + return pubkey + + def main(): p = argparse.ArgumentParser() p.add_argument('user_id') p.add_argument('-t', '--time', type=int, default=int(time.time())) - p.add_argument('-f', '--filename') p.add_argument('-a', '--armor', action='store_true', default=False) - p.add_argument('-p', '--public-key', action='store_true', default=False) p.add_argument('-v', '--verbose', action='store_true', default=False) + g = p.add_mutually_exclusive_group() + g.add_argument('-f', '--filename', help='File to sign') + g.add_argument('-p', '--public-key', action='store_true', default=False) + args = p.parse_args() logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format='%(asctime)s %(levelname)-10s %(message)s') - s = Signer(user_id=args.user_id.encode('ascii'), created=args.time) + user_id = args.user_id.encode('ascii') if args.public_key: + s = Signer(user_id=user_id, created=args.time) pubkey = s.export() ext = '.pub' if args.armor: @@ -217,11 +229,16 @@ def main(): ext = '.asc' open(args.user_id + ext, 'wb').write(pubkey) - if args.filename: + elif args.filename: + pubkey = load_from_gpg(args.user_id) + s = Signer(user_id=user_id, created=pubkey['created']) + assert s.key_id == pubkey['key_id'] + data = open(args.filename, 'rb').read() sig, ext = s.sign(data), '.sig' if args.armor: - sig, ext = armor(sig, 'SIGNATURE'), '.asc' + sig = armor(sig, 'SIGNATURE') + ext = '.asc' open(args.filename + ext, 'wb').write(sig) s.close() From 01dafb0ebdff2a7670e321d3b766b67d51fe853e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 17 Apr 2016 23:11:18 +0300 Subject: [PATCH 06/31] signer: show key ID on TREZOR screen --- gpg/signer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gpg/signer.py b/gpg/signer.py index ac64fe3..16bf610 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -132,7 +132,8 @@ class Signer(object): subpacket_byte(0x15, 8), # preferred hash (SHA256) subpacket_byte(0x16, 0), # preferred compression (none) subpacket_byte(0x17, 0x80)] # key server prefs (no-modify) - signature = self._make_signature(visual='Sign GPG public key', + visual = hexlify(self.key_id[-4:]) + signature = self._make_signature(visual=visual, data_to_sign=data_to_sign, sig_type=0x13, # user id & public key hashed_subpackets=hashed_subpackets) @@ -147,8 +148,9 @@ class Signer(object): log.info('signing message %r at %s', msg, time_format(sign_time)) hashed_subpackets = [subpacket_time(sign_time)] + visual = hexlify(self.key_id[-4:]) blob = self._make_signature( - visual='Sign GPG message', + visual=visual, data_to_sign=msg, hashed_subpackets=hashed_subpackets) return packet(tag=2, blob=blob) From b2d078eec6758c3794ffc41db46e15e2ac7f6da1 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 18 Apr 2016 21:55:23 +0300 Subject: [PATCH 07/31] simplify signer usage and make less INFO loggin --- gpg/check.py | 1 + gpg/decode.py | 4 ++-- gpg/demo.sh | 4 ++-- gpg/signer.py | 31 +++++++++++++++---------------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/gpg/check.py b/gpg/check.py index db4e0e1..ab2937d 100755 --- a/gpg/check.py +++ b/gpg/check.py @@ -40,6 +40,7 @@ def main(): args = p.parse_args() check(pubkey=decode.load_public_key(open(args.pubkey, 'rb')), sig_file=args.signature) + log.info('OK') if __name__ == '__main__': main() diff --git a/gpg/decode.py b/gpg/decode.py index c936c03..494af6c 100644 --- a/gpg/decode.py +++ b/gpg/decode.py @@ -227,7 +227,7 @@ class Parser(object): def load_public_key(stream): parser = Parser(Reader(stream)) pubkey, userid, signature = list(parser) - log.info('loaded %s public key', userid['value']) + log.debug('loaded public key "%s"', userid['value']) verify_digest(pubkey=pubkey, digest=signature['digest'], signature=signature['sig'], label='GPG public key') return pubkey @@ -244,7 +244,7 @@ def verify_digest(pubkey, digest, signature, label): v.verify_digest(signature=signature, digest=digest, sigdecode=lambda rs, order: rs) - log.info('%s is OK', label) + log.debug('%s is OK', label) except ecdsa.keys.BadSignatureError: log.error('Bad %s!', label) raise diff --git a/gpg/demo.sh b/gpg/demo.sh index a012157..655a547 100755 --- a/gpg/demo.sh +++ b/gpg/demo.sh @@ -5,14 +5,14 @@ NAME="trezor_key" # will be used as GPG user id and public key name echo "Hello GPG World!" > EXAMPLE # Create, sign and export the public key -./signer.py $NAME --time $CREATED --public-key --verbose +./signer.py $NAME --time $CREATED # Install GPG v2.1 (modern) and import the public key gpg2 --import $NAME.pub gpg2 --list-keys $NAME # Perform actual GPG signature using TREZOR -./signer.py $NAME --file EXAMPLE --verbose +./signer.py $NAME EXAMPLE ./check.py $NAME.pub EXAMPLE.sig # pure Python verification # gpg2 --edit-key trezor_key trust # optional: mark it as trusted diff --git a/gpg/signer.py b/gpg/signer.py index 16bf610..bbeaa7f 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -104,10 +104,10 @@ class Signer(object): # https://tools.ietf.org/html/rfc6637#section-11 (NIST P-256 OID) oid = prefix_len('>B', b'\x2A\x86\x48\xCE\x3D\x03\x01\x07') - point = verifying_key.pubkey.point + self._point = verifying_key.pubkey.point self.pubkey_data = header + oid + mpi((4 << 512) | - (point.x() << 256) | - (point.y())) + (self._point.x() << 256) | + (self._point.y())) self.data_to_hash = b'\x99' + prefix_len('>H', self.pubkey_data) fingerprint = hashlib.sha1(self.data_to_hash).digest() @@ -125,15 +125,15 @@ class Signer(object): user_id_to_hash = user_id_packet[:1] + prefix_len('>L', self.user_id) data_to_sign = self.data_to_hash + user_id_to_hash - log.info('signing user_id: %r', self.user_id.decode('ascii')) + key_id = hexlify(self.key_id[-4:]) + log.info('signing public key "%s": %s', self.user_id, key_id) hashed_subpackets = [ subpacket_time(self.created), # signature creaion time subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) subpacket_byte(0x15, 8), # preferred hash (SHA256) subpacket_byte(0x16, 0), # preferred compression (none) subpacket_byte(0x17, 0x80)] # key server prefs (no-modify) - visual = hexlify(self.key_id[-4:]) - signature = self._make_signature(visual=visual, + signature = self._make_signature(visual=key_id, data_to_sign=data_to_sign, sig_type=0x13, # user id & public key hashed_subpackets=hashed_subpackets) @@ -148,9 +148,9 @@ class Signer(object): log.info('signing message %r at %s', msg, time_format(sign_time)) hashed_subpackets = [subpacket_time(sign_time)] - visual = hexlify(self.key_id[-4:]) + key_id = hexlify(self.key_id[-4:]) blob = self._make_signature( - visual=visual, + visual=key_id, data_to_sign=msg, hashed_subpackets=hashed_subpackets) return packet(tag=2, blob=blob) @@ -173,13 +173,16 @@ class Signer(object): result = self.client_wrapper.connection.sign_identity( identity=self.identity, - challenge_hidden=hashlib.sha256(data_to_hash).digest(), + challenge_hidden=digest, challenge_visual=visual, ecdsa_curve_name=self.ecdsa_curve_name) assert result.signature[:1] == b'\x00' sig = result.signature[1:] sig = [trezor_agent.util.bytes2num(sig[:32]), trezor_agent.util.bytes2num(sig[32:])] + decode.verify_digest(pubkey={'point': (self._point.x(), self._point.y())}, + digest=digest, + signature=sig, label='GPG signature') hash_prefix = digest[:2] # used for decoder's sanity check signature = mpi(sig[0]) + mpi(sig[1]) # actual ECDSA signature @@ -210,19 +213,16 @@ def load_from_gpg(user_id): def main(): p = argparse.ArgumentParser() p.add_argument('user_id') + p.add_argument('filename', nargs='?', ) p.add_argument('-t', '--time', type=int, default=int(time.time())) p.add_argument('-a', '--armor', action='store_true', default=False) p.add_argument('-v', '--verbose', action='store_true', default=False) - g = p.add_mutually_exclusive_group() - g.add_argument('-f', '--filename', help='File to sign') - g.add_argument('-p', '--public-key', action='store_true', default=False) - args = p.parse_args() logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format='%(asctime)s %(levelname)-10s %(message)s') user_id = args.user_id.encode('ascii') - if args.public_key: + if not args.filename: s = Signer(user_id=user_id, created=args.time) pubkey = s.export() ext = '.pub' @@ -230,8 +230,7 @@ def main(): pubkey = armor(pubkey, 'PUBLIC KEY BLOCK') ext = '.asc' open(args.user_id + ext, 'wb').write(pubkey) - - elif args.filename: + else: pubkey = load_from_gpg(args.user_id) s = Signer(user_id=user_id, created=pubkey['created']) assert s.key_id == pubkey['key_id'] From 96592269b68c93acc7f994e8758dea1533c92f1f Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 18 Apr 2016 22:10:00 +0300 Subject: [PATCH 08/31] signer: refactor a bit --- gpg/signer.py | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/gpg/signer.py b/gpg/signer.py index bbeaa7f..e362ea6 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -9,8 +9,6 @@ import struct import subprocess import time -import ecdsa - import decode import trezor_agent.client import trezor_agent.formats @@ -75,18 +73,18 @@ def hexlify(blob): class Signer(object): - curve = ecdsa.NIST256p ecdsa_curve_name = trezor_agent.formats.CURVE_NIST256 def __init__(self, user_id, created): self.user_id = user_id self.client_wrapper = trezor_agent.factory.load() - # This requires the following patch to trezor-mcu to work: + # This requires the following patch to trezor-mcu in order to work: # https://gist.github.com/romanz/b66f5df1ca8ef15641df8ea5bb09fd47 self.identity = self.client_wrapper.identity_type() self.identity.proto = 'gpg' self.identity.host = user_id + addr = trezor_agent.client.get_address(self.identity) public_node = self.client_wrapper.connection.get_public_node( n=addr, ecdsa_curve_name=self.ecdsa_curve_name) @@ -96,36 +94,41 @@ class Signer(object): curve_name=self.ecdsa_curve_name) self.created = int(created) + self.pubkey_point = verifying_key.pubkey.point + log.info('key %s created at %s', + hexlify(self._fingerprint()[-4:]), time_format(self.created)) + + def _pubkey_data(self): header = struct.pack('>BLB', 4, # version self.created, # creation 19) # ECDSA - # https://tools.ietf.org/html/rfc6637#section-11 (NIST P-256 OID) oid = prefix_len('>B', b'\x2A\x86\x48\xCE\x3D\x03\x01\x07') + return header + oid + mpi((4 << 512) | + (self.pubkey_point.x() << 256) | + (self.pubkey_point.y())) - self._point = verifying_key.pubkey.point - self.pubkey_data = header + oid + mpi((4 << 512) | - (self._point.x() << 256) | - (self._point.y())) + def _pubkey_data_to_hash(self): + return b'\x99' + prefix_len('>H', self._pubkey_data()) - self.data_to_hash = b'\x99' + prefix_len('>H', self.pubkey_data) - fingerprint = hashlib.sha1(self.data_to_hash).digest() - self.key_id = fingerprint[-8:] - log.info('key %s created at %s', - hexlify(fingerprint[-4:]), time_format(self.created)) + def _fingerprint(self): + return hashlib.sha1(self._pubkey_data_to_hash()).digest() + + def key_id(self): + return self._fingerprint()[-8:] def close(self): self.client_wrapper.connection.clear_session() self.client_wrapper.connection.close() def export(self): - pubkey_packet = packet(tag=6, blob=self.pubkey_data) + pubkey_packet = packet(tag=6, blob=self._pubkey_data()) user_id_packet = packet(tag=13, blob=self.user_id) user_id_to_hash = user_id_packet[:1] + prefix_len('>L', self.user_id) - data_to_sign = self.data_to_hash + user_id_to_hash - key_id = hexlify(self.key_id[-4:]) + data_to_sign = self._pubkey_data_to_hash() + user_id_to_hash + key_id = hexlify(self.key_id()[-4:]) log.info('signing public key "%s": %s', self.user_id, key_id) hashed_subpackets = [ subpacket_time(self.created), # signature creaion time @@ -148,7 +151,7 @@ class Signer(object): log.info('signing message %r at %s', msg, time_format(sign_time)) hashed_subpackets = [subpacket_time(sign_time)] - key_id = hexlify(self.key_id[-4:]) + key_id = hexlify(self.key_id()[-4:]) blob = self._make_signature( visual=key_id, data_to_sign=msg, hashed_subpackets=hashed_subpackets) @@ -163,7 +166,7 @@ class Signer(object): 8) # hash_alg (SHA256) hashed = subpackets(*hashed_subpackets) unhashed = subpackets( - subpacket(16, self.key_id) # issuer key id + subpacket(16, self.key_id()) # issuer key id ) tail = b'\x04\xff' + struct.pack('>L', len(header) + len(hashed)) data_to_hash = data_to_sign + header + hashed + tail @@ -180,7 +183,8 @@ class Signer(object): sig = result.signature[1:] sig = [trezor_agent.util.bytes2num(sig[:32]), trezor_agent.util.bytes2num(sig[32:])] - decode.verify_digest(pubkey={'point': (self._point.x(), self._point.y())}, + decode.verify_digest(pubkey={'point': (self.pubkey_point.x(), + self.pubkey_point.y())}, digest=digest, signature=sig, label='GPG signature') @@ -233,7 +237,7 @@ def main(): else: pubkey = load_from_gpg(args.user_id) s = Signer(user_id=user_id, created=pubkey['created']) - assert s.key_id == pubkey['key_id'] + assert s.key_id() == pubkey['key_id'] data = open(args.filename, 'rb').read() sig, ext = s.sign(data), '.sig' From af6d0caf33ed42d26edd0e0a1322bb055bc66d02 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Mon, 18 Apr 2016 23:02:14 +0300 Subject: [PATCH 09/31] Add GPG-wrapper script for Git --- gpg/git_gpg_wrapper.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 gpg/git_gpg_wrapper.py diff --git a/gpg/git_gpg_wrapper.py b/gpg/git_gpg_wrapper.py new file mode 100755 index 0000000..679a0c2 --- /dev/null +++ b/gpg/git_gpg_wrapper.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +import sys +import subprocess as sp +import time +import logging +import os + +import signer + +logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)-10s %(message)s') + +def main(): + args = sys.argv[1:] + if '--verify' in args: + sp.check_call(['gpg2'] + args) + else: + user_id = os.environ['GPG_USER_ID'] + user_id = user_id.encode('ascii') + pubkey = signer.load_from_gpg(user_id) + s = signer.Signer(user_id=user_id, created=pubkey['created']) + assert s.key_id() == pubkey['key_id'] + + data = sys.stdin.read() + sig = s.sign(data) + sig = signer.armor(sig, 'SIGNATURE') + sys.stdout.write(sig) + s.close() + +if __name__ == '__main__': + main() From 5651452c0d42b71aba12f4e05762746bf87c8302 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 20 Apr 2016 21:41:31 +0300 Subject: [PATCH 10/31] gpg: rename GPG public key file --- gpg/signer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpg/signer.py b/gpg/signer.py index e362ea6..e07ec6f 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -233,7 +233,7 @@ def main(): if args.armor: pubkey = armor(pubkey, 'PUBLIC KEY BLOCK') ext = '.asc' - open(args.user_id + ext, 'wb').write(pubkey) + open(s.hex_short_key_id() + ext, 'wb').write(pubkey) else: pubkey = load_from_gpg(args.user_id) s = Signer(user_id=user_id, created=pubkey['created']) From ab64505cdb2080d51593476957d0a4c6fdd03187 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 20 Apr 2016 21:41:46 +0300 Subject: [PATCH 11/31] gpg: refactor hexlification of key_id --- gpg/signer.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/gpg/signer.py b/gpg/signer.py index e07ec6f..101aeca 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -96,7 +96,7 @@ class Signer(object): self.created = int(created) self.pubkey_point = verifying_key.pubkey.point log.info('key %s created at %s', - hexlify(self._fingerprint()[-4:]), time_format(self.created)) + self.hex_short_key_id(), time_format(self.created)) def _pubkey_data(self): header = struct.pack('>BLB', @@ -118,6 +118,9 @@ class Signer(object): def key_id(self): return self._fingerprint()[-8:] + def hex_short_key_id(self): + return hexlify(self.key_id()[-4:]) + def close(self): self.client_wrapper.connection.clear_session() self.client_wrapper.connection.close() @@ -128,15 +131,14 @@ class Signer(object): user_id_to_hash = user_id_packet[:1] + prefix_len('>L', self.user_id) data_to_sign = self._pubkey_data_to_hash() + user_id_to_hash - key_id = hexlify(self.key_id()[-4:]) - log.info('signing public key "%s": %s', self.user_id, key_id) + log.info('signing public key "%s"', self.user_id) hashed_subpackets = [ subpacket_time(self.created), # signature creaion time subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) subpacket_byte(0x15, 8), # preferred hash (SHA256) subpacket_byte(0x16, 0), # preferred compression (none) subpacket_byte(0x17, 0x80)] # key server prefs (no-modify) - signature = self._make_signature(visual=key_id, + signature = self._make_signature(visual=self.hex_short_key_id(), data_to_sign=data_to_sign, sig_type=0x13, # user id & public key hashed_subpackets=hashed_subpackets) @@ -151,9 +153,8 @@ class Signer(object): log.info('signing message %r at %s', msg, time_format(sign_time)) hashed_subpackets = [subpacket_time(sign_time)] - key_id = hexlify(self.key_id()[-4:]) blob = self._make_signature( - visual=key_id, + visual=self.hex_short_key_id(), data_to_sign=msg, hashed_subpackets=hashed_subpackets) return packet(tag=2, blob=blob) From 33ff9ba6671bf8e7c38062a2106cd9f0f4a2a87e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Wed, 20 Apr 2016 22:42:47 +0300 Subject: [PATCH 12/31] signer: update required patch link --- gpg/signer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gpg/signer.py b/gpg/signer.py index 101aeca..ea6adf4 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -79,8 +79,8 @@ class Signer(object): self.user_id = user_id self.client_wrapper = trezor_agent.factory.load() - # This requires the following patch to trezor-mcu in order to work: - # https://gist.github.com/romanz/b66f5df1ca8ef15641df8ea5bb09fd47 + # This requires the following patch to trezor-mcu in order to work correctly: + # https://github.com/trezor/trezor-mcu/pull/79 self.identity = self.client_wrapper.identity_type() self.identity.proto = 'gpg' self.identity.host = user_id From 1402918bb3041bfd31d87521ec81006afae2bd33 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 22 Apr 2016 21:36:56 +0300 Subject: [PATCH 13/31] gpg: use user name instead of key id --- gpg/git_gpg_wrapper.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gpg/git_gpg_wrapper.py b/gpg/git_gpg_wrapper.py index 679a0c2..130ea55 100755 --- a/gpg/git_gpg_wrapper.py +++ b/gpg/git_gpg_wrapper.py @@ -7,16 +7,20 @@ import os import signer -logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)-10s %(message)s') +log = logging.getLogger(__name__) + def main(): + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)-10s %(message)s') + + log.debug('sys.argv: %s', sys.argv) args = sys.argv[1:] if '--verify' in args: sp.check_call(['gpg2'] + args) else: - user_id = os.environ['GPG_USER_ID'] - user_id = user_id.encode('ascii') + command, user_id = args + assert command == '-bsau' # --detach-sign --sign --armor --local-user pubkey = signer.load_from_gpg(user_id) s = signer.Signer(user_id=user_id, created=pubkey['created']) assert s.key_id() == pubkey['key_id'] From 7ef0958c3347b31b7d80493ec22722e43fc44fd4 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 22 Apr 2016 21:37:23 +0300 Subject: [PATCH 14/31] gpg: minor fixes --- gpg/signer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gpg/signer.py b/gpg/signer.py index ea6adf4..11d8bb2 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -210,6 +210,7 @@ def armor(blob, type_str): def load_from_gpg(user_id): + log.info('loading GPG public key for %r', user_id) pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) pubkey = decode.load_public_key(io.BytesIO(pubkey_bytes)) return pubkey @@ -218,7 +219,7 @@ def load_from_gpg(user_id): def main(): p = argparse.ArgumentParser() p.add_argument('user_id') - p.add_argument('filename', nargs='?', ) + p.add_argument('filename', nargs='?') p.add_argument('-t', '--time', type=int, default=int(time.time())) p.add_argument('-a', '--armor', action='store_true', default=False) p.add_argument('-v', '--verbose', action='store_true', default=False) From 74f7ebf228b7e18926434fe4ee482ad6cfd98ba4 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 22 Apr 2016 21:43:54 +0300 Subject: [PATCH 15/31] gpg: support ed25519 decoding --- gpg/decode.py | 53 ++++++++++++++++++++++++++++++++------------ trezor_agent/util.py | 2 +- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/gpg/decode.py b/gpg/decode.py index 494af6c..ffb4c2f 100644 --- a/gpg/decode.py +++ b/gpg/decode.py @@ -6,6 +6,9 @@ import logging import struct import ecdsa +import ed25519 + +from trezor_agent.util import num2bytes log = logging.getLogger(__name__) @@ -86,6 +89,10 @@ def split_bits(value, *bits): assert value == 0 return reversed(result) +SUPPORTED_CURVES = { + b'\x2A\x86\x48\xCE\x3D\x03\x01\x07': 'nist256p1', + b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01': 'ed25519', +} class Parser(object): def __init__(self, stream, to_hash=None): @@ -168,13 +175,38 @@ class Parser(object): # https://tools.ietf.org/html/rfc6637#section-11 oid_size = stream.readfmt('B') oid = stream.read(oid_size) - assert oid == b'\x2A\x86\x48\xCE\x3D\x03\x01\x07' # NIST P-256 + assert oid in SUPPORTED_CURVES + curve_name = SUPPORTED_CURVES[oid] mpi = parse_mpi(stream) - log.debug('mpi: %x', mpi) - prefix, x, y = split_bits(mpi, 4, 256, 256) - assert prefix == 4 - p['point'] = (x, y) + log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length()) + if curve_name == 'nist256p1': + prefix, x, y = 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( + point=point, curve=ecdsa.curves.NIST256p, + hashfunc=hashlib.sha256) + + def _nist256p1_verify(signature, digest): + vk.verify_digest(signature=signature, + digest=digest, + sigdecode=lambda rs, order: rs) + p['verifier'] = _nist256p1_verify + elif curve_name == 'ed25519': + prefix, value = split_bits(mpi, 8, 256) + assert prefix == 0x40 + vk = ed25519.VerifyingKey(num2bytes(value, size=32)) + + def _ed25519_verify(signature, digest): + sig = b''.join(num2bytes(val, size=32) + for val in signature) + vk.verify(sig, digest) + p['verifier'] = _ed25519_verify + else: + raise ValueError('unsupported curve {}'.format(curve_name)) + assert not stream.read() # https://tools.ietf.org/html/rfc4880#section-12.2 @@ -234,16 +266,9 @@ def load_public_key(stream): def verify_digest(pubkey, digest, signature, label): - coords = pubkey['point'] - point = ecdsa.ellipticcurve.Point(curve=ecdsa.NIST256p.curve, - x=coords[0], y=coords[1]) - v = ecdsa.VerifyingKey.from_public_point(point=point, - curve=ecdsa.curves.NIST256p, - hashfunc=hashlib.sha256) + verifier = pubkey['verifier'] try: - v.verify_digest(signature=signature, - digest=digest, - sigdecode=lambda rs, order: rs) + verifier(signature, digest) log.debug('%s is OK', label) except ecdsa.keys.BadSignatureError: log.error('Bad %s!', label) diff --git a/trezor_agent/util.py b/trezor_agent/util.py index 8e0e29b..8119a82 100644 --- a/trezor_agent/util.py +++ b/trezor_agent/util.py @@ -60,7 +60,7 @@ def num2bytes(value, size): res.append(value & 0xFF) value = value >> 8 assert value == 0 - return bytearray(list(reversed(res))) + return bytes(bytearray(list(reversed(res)))) def pack(fmt, *args): From 276dec57281c6c5bba6729c0b4e601fe5f9a5b8b Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 22 Apr 2016 23:37:04 +0300 Subject: [PATCH 16/31] gpg: support ed25519 public keys and signatures --- gpg/decode.py | 7 ++--- gpg/signer.py | 73 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/gpg/decode.py b/gpg/decode.py index ffb4c2f..9eb7778 100644 --- a/gpg/decode.py +++ b/gpg/decode.py @@ -94,6 +94,7 @@ SUPPORTED_CURVES = { b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01': 'ed25519', } + class Parser(object): def __init__(self, stream, to_hash=None): self.stream = stream @@ -191,11 +192,11 @@ class Parser(object): def _nist256p1_verify(signature, digest): vk.verify_digest(signature=signature, - digest=digest, - sigdecode=lambda rs, order: rs) + digest=digest, + sigdecode=lambda rs, order: rs) p['verifier'] = _nist256p1_verify elif curve_name == 'ed25519': - prefix, value = split_bits(mpi, 8, 256) + prefix, value = split_bits(mpi, 8, 256) assert prefix == 0x40 vk = ed25519.VerifyingKey(num2bytes(value, size=32)) diff --git a/gpg/signer.py b/gpg/signer.py index 11d8bb2..d33bb47 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -71,43 +71,65 @@ def hexlify(blob): return binascii.hexlify(blob).decode('ascii').upper() +def _dump_nist256(vk): + return mpi((4 << 512) | + (vk.pubkey.point.x() << 256) | + (vk.pubkey.point.y())) + + +def _dump_ed25519(vk): + return mpi((0x40 << 256) | + trezor_agent.util.bytes2num(vk.to_bytes())) + + +SUPPORTED_CURVES = { + trezor_agent.formats.CURVE_NIST256: { + # https://tools.ietf.org/html/rfc6637#section-11 + 'oid': b'\x2A\x86\x48\xCE\x3D\x03\x01\x07', + 'algo_id': 19, + 'dump': _dump_nist256 + }, + trezor_agent.formats.CURVE_ED25519: { + 'oid': b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01', + 'algo_id': 22, + 'dump': _dump_ed25519 + } +} + + class Signer(object): - ecdsa_curve_name = trezor_agent.formats.CURVE_NIST256 - - def __init__(self, user_id, created): + def __init__(self, user_id, created, curve_name): self.user_id = user_id + assert curve_name in trezor_agent.formats.SUPPORTED_CURVES + self.curve_name = curve_name self.client_wrapper = trezor_agent.factory.load() - # This requires the following patch to trezor-mcu in order to work correctly: - # https://github.com/trezor/trezor-mcu/pull/79 self.identity = self.client_wrapper.identity_type() self.identity.proto = 'gpg' self.identity.host = user_id addr = trezor_agent.client.get_address(self.identity) public_node = self.client_wrapper.connection.get_public_node( - n=addr, ecdsa_curve_name=self.ecdsa_curve_name) + n=addr, ecdsa_curve_name=self.curve_name) - verifying_key = trezor_agent.formats.decompress_pubkey( + self.verifying_key = trezor_agent.formats.decompress_pubkey( pubkey=public_node.node.public_key, - curve_name=self.ecdsa_curve_name) + curve_name=self.curve_name) self.created = int(created) - self.pubkey_point = verifying_key.pubkey.point log.info('key %s created at %s', self.hex_short_key_id(), time_format(self.created)) def _pubkey_data(self): + curve_info = SUPPORTED_CURVES[self.curve_name] header = struct.pack('>BLB', 4, # version self.created, # creation - 19) # ECDSA - # https://tools.ietf.org/html/rfc6637#section-11 (NIST P-256 OID) - oid = prefix_len('>B', b'\x2A\x86\x48\xCE\x3D\x03\x01\x07') - return header + oid + mpi((4 << 512) | - (self.pubkey_point.x() << 256) | - (self.pubkey_point.y())) + curve_info['algo_id']) + oid = prefix_len('>B', curve_info['oid']) + blob = curve_info['dump'](self.verifying_key) + return header + oid + blob def _pubkey_data_to_hash(self): return b'\x99' + prefix_len('>H', self._pubkey_data()) @@ -160,10 +182,11 @@ class Signer(object): def _make_signature(self, visual, data_to_sign, hashed_subpackets, sig_type=0): + curve_info = SUPPORTED_CURVES[self.curve_name] header = struct.pack('>BBBB', 4, # version sig_type, # rfc4880 (section-5.2.1) - 19, # pubkey_alg (ECDSA) + curve_info['algo_id'], 8) # hash_alg (SHA256) hashed = subpackets(*hashed_subpackets) unhashed = subpackets( @@ -179,19 +202,16 @@ class Signer(object): identity=self.identity, challenge_hidden=digest, challenge_visual=visual, - ecdsa_curve_name=self.ecdsa_curve_name) + ecdsa_curve_name=self.curve_name) assert result.signature[:1] == b'\x00' sig = result.signature[1:] sig = [trezor_agent.util.bytes2num(sig[:32]), trezor_agent.util.bytes2num(sig[32:])] - decode.verify_digest(pubkey={'point': (self.pubkey_point.x(), - self.pubkey_point.y())}, - digest=digest, - signature=sig, label='GPG signature') hash_prefix = digest[:2] # used for decoder's sanity check signature = mpi(sig[0]) + mpi(sig[1]) # actual ECDSA signature return header + hashed + unhashed + hash_prefix + signature + # TODO: add verification def split_lines(body, size): @@ -223,22 +243,27 @@ def main(): p.add_argument('-t', '--time', type=int, default=int(time.time())) p.add_argument('-a', '--armor', action='store_true', default=False) p.add_argument('-v', '--verbose', action='store_true', default=False) + p.add_argument('-e', '--ecdsa-curve', default='nist256p1') args = p.parse_args() logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format='%(asctime)s %(levelname)-10s %(message)s') user_id = args.user_id.encode('ascii') if not args.filename: - s = Signer(user_id=user_id, created=args.time) + s = Signer(user_id=user_id, created=args.time, + curve_name=args.ecdsa_curve) pubkey = s.export() ext = '.pub' if args.armor: pubkey = armor(pubkey, 'PUBLIC KEY BLOCK') ext = '.asc' - open(s.hex_short_key_id() + ext, 'wb').write(pubkey) + filename = s.hex_short_key_id() + ext + open(filename, 'wb').write(pubkey) + log.info('import to local keyring using "gpg2 --import %s"', filename) else: pubkey = load_from_gpg(args.user_id) - s = Signer(user_id=user_id, created=pubkey['created']) + s = Signer(user_id=user_id, created=pubkey['created'], + curve_name=args.ecdsa_curve) # TODO: deduce from existing pubkey assert s.key_id() == pubkey['key_id'] data = open(args.filename, 'rb').read() From 8c0848b4598de8a6f6292c8651dbebd631f1dddf Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 22 Apr 2016 23:37:19 +0300 Subject: [PATCH 17/31] gpg: debug during check --- gpg/check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpg/check.py b/gpg/check.py index ab2937d..d228620 100755 --- a/gpg/check.py +++ b/gpg/check.py @@ -32,7 +32,7 @@ def check(pubkey, sig_file): def main(): - logging.basicConfig(level=logging.INFO, + logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)-10s %(message)s') p = argparse.ArgumentParser() p.add_argument('pubkey') From fb368d24eb0569751d87adbd18382e0ce344dbb9 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 22 Apr 2016 23:44:46 +0300 Subject: [PATCH 18/31] gpg: use subprocess.call() --- gpg/git_gpg_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpg/git_gpg_wrapper.py b/gpg/git_gpg_wrapper.py index 130ea55..1c59f9a 100755 --- a/gpg/git_gpg_wrapper.py +++ b/gpg/git_gpg_wrapper.py @@ -17,7 +17,7 @@ def main(): log.debug('sys.argv: %s', sys.argv) args = sys.argv[1:] if '--verify' in args: - sp.check_call(['gpg2'] + args) + return sp.call(['gpg2'] + args) else: command, user_id = args assert command == '-bsau' # --detach-sign --sign --armor --local-user From 80f29469d04bebc3304632231e66d043df283cd9 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 00:08:45 +0300 Subject: [PATCH 19/31] gpg: deduce curve name from existing pubkey information --- gpg/git_gpg_wrapper.py | 4 +--- gpg/signer.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/gpg/git_gpg_wrapper.py b/gpg/git_gpg_wrapper.py index 1c59f9a..9467ffe 100755 --- a/gpg/git_gpg_wrapper.py +++ b/gpg/git_gpg_wrapper.py @@ -21,9 +21,7 @@ def main(): else: command, user_id = args assert command == '-bsau' # --detach-sign --sign --armor --local-user - pubkey = signer.load_from_gpg(user_id) - s = signer.Signer(user_id=user_id, created=pubkey['created']) - assert s.key_id() == pubkey['key_id'] + s = signer.load_from_gpg(user_id) data = sys.stdin.read() sig = s.sign(data) diff --git a/gpg/signer.py b/gpg/signer.py index d33bb47..fe6f857 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -96,6 +96,11 @@ SUPPORTED_CURVES = { } } +def find_curve_by_algo_id(algo_id): + curve_name, = [name for name, info in SUPPORTED_CURVES.items() + if info['algo_id'] == algo_id] + return curve_name + class Signer(object): @@ -233,7 +238,11 @@ def load_from_gpg(user_id): log.info('loading GPG public key for %r', user_id) pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) pubkey = decode.load_public_key(io.BytesIO(pubkey_bytes)) - return pubkey + s = Signer(user_id=user_id, + created=pubkey['created'], + curve_name=find_curve_by_algo_id(pubkey['algo'])) + assert s.key_id() == pubkey['key_id'] + return s def main(): @@ -261,11 +270,7 @@ def main(): open(filename, 'wb').write(pubkey) log.info('import to local keyring using "gpg2 --import %s"', filename) else: - pubkey = load_from_gpg(args.user_id) - s = Signer(user_id=user_id, created=pubkey['created'], - curve_name=args.ecdsa_curve) # TODO: deduce from existing pubkey - assert s.key_id() == pubkey['key_id'] - + s = load_from_gpg(user_id) data = open(args.filename, 'rb').read() sig, ext = s.sign(data), '.sig' if args.armor: From 9dc955aae88a5352be9315a193ac6338c05076c3 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 19:31:23 +0300 Subject: [PATCH 20/31] gpg: fix signer logging --- gpg/signer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gpg/signer.py b/gpg/signer.py index fe6f857..06a3c65 100755 --- a/gpg/signer.py +++ b/gpg/signer.py @@ -123,7 +123,7 @@ class Signer(object): curve_name=self.curve_name) self.created = int(created) - log.info('key %s created at %s', + log.info('%s GPG public key %s created at %s', self.curve_name, self.hex_short_key_id(), time_format(self.created)) def _pubkey_data(self): @@ -177,8 +177,8 @@ class Signer(object): if sign_time is None: sign_time = int(time.time()) - log.info('signing message %r at %s', msg, - time_format(sign_time)) + log.info('signing %d byte message at %s', + len(msg), time_format(sign_time)) hashed_subpackets = [subpacket_time(sign_time)] blob = self._make_signature( visual=self.hex_short_key_id(), @@ -235,7 +235,7 @@ def armor(blob, type_str): def load_from_gpg(user_id): - log.info('loading GPG public key for %r', user_id) + log.info('loading public key %r from local GPG keyring', user_id) pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) pubkey = decode.load_public_key(io.BytesIO(pubkey_bytes)) s = Signer(user_id=user_id, From 5506310239901277d7aed8eae64c8395b927d516 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 21:47:30 +0300 Subject: [PATCH 21/31] gpg: move under trezor_agent --- {gpg => trezor_agent/gpg}/check.py | 0 {gpg => trezor_agent/gpg}/decode.py | 0 {gpg => trezor_agent/gpg}/demo.sh | 0 {gpg => trezor_agent/gpg}/git_gpg_wrapper.py | 0 {gpg => trezor_agent/gpg}/signer.py | 0 {gpg => trezor_agent/gpg}/util.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {gpg => trezor_agent/gpg}/check.py (100%) rename {gpg => trezor_agent/gpg}/decode.py (100%) rename {gpg => trezor_agent/gpg}/demo.sh (100%) rename {gpg => trezor_agent/gpg}/git_gpg_wrapper.py (100%) rename {gpg => trezor_agent/gpg}/signer.py (100%) rename {gpg => trezor_agent/gpg}/util.py (100%) diff --git a/gpg/check.py b/trezor_agent/gpg/check.py similarity index 100% rename from gpg/check.py rename to trezor_agent/gpg/check.py diff --git a/gpg/decode.py b/trezor_agent/gpg/decode.py similarity index 100% rename from gpg/decode.py rename to trezor_agent/gpg/decode.py diff --git a/gpg/demo.sh b/trezor_agent/gpg/demo.sh similarity index 100% rename from gpg/demo.sh rename to trezor_agent/gpg/demo.sh diff --git a/gpg/git_gpg_wrapper.py b/trezor_agent/gpg/git_gpg_wrapper.py similarity index 100% rename from gpg/git_gpg_wrapper.py rename to trezor_agent/gpg/git_gpg_wrapper.py diff --git a/gpg/signer.py b/trezor_agent/gpg/signer.py similarity index 100% rename from gpg/signer.py rename to trezor_agent/gpg/signer.py diff --git a/gpg/util.py b/trezor_agent/gpg/util.py similarity index 100% rename from gpg/util.py rename to trezor_agent/gpg/util.py From 76ce25fab19dd97ceb88cba9b0d3e1c08befa432 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 22:08:18 +0300 Subject: [PATCH 22/31] gpg: fixup imports --- trezor_agent/__main__.py | 2 +- trezor_agent/gpg/check.py | 4 ++-- trezor_agent/gpg/git_gpg_wrapper.py | 8 +++----- trezor_agent/gpg/signer.py | 28 ++++++++++++++-------------- trezor_agent/gpg/util.py | 18 ------------------ trezor_agent/util.py | 17 +++++++++++++++++ 6 files changed, 37 insertions(+), 40 deletions(-) delete mode 100644 trezor_agent/gpg/util.py diff --git a/trezor_agent/__main__.py b/trezor_agent/__main__.py index bebb043..5248a9f 100644 --- a/trezor_agent/__main__.py +++ b/trezor_agent/__main__.py @@ -2,8 +2,8 @@ import argparse import functools import logging -import re import os +import re import subprocess import sys import time diff --git a/trezor_agent/gpg/check.py b/trezor_agent/gpg/check.py index d228620..b1b3557 100755 --- a/trezor_agent/gpg/check.py +++ b/trezor_agent/gpg/check.py @@ -4,8 +4,8 @@ import base64 import io import logging -import decode -import util +from . import decode +from .. import util log = logging.getLogger(__name__) diff --git a/trezor_agent/gpg/git_gpg_wrapper.py b/trezor_agent/gpg/git_gpg_wrapper.py index 9467ffe..f3296bd 100755 --- a/trezor_agent/gpg/git_gpg_wrapper.py +++ b/trezor_agent/gpg/git_gpg_wrapper.py @@ -1,11 +1,9 @@ #!/usr/bin/env python -import sys -import subprocess as sp -import time import logging -import os +import subprocess as sp +import sys -import signer +from . import signer log = logging.getLogger(__name__) diff --git a/trezor_agent/gpg/signer.py b/trezor_agent/gpg/signer.py index 06a3c65..3b644d1 100755 --- a/trezor_agent/gpg/signer.py +++ b/trezor_agent/gpg/signer.py @@ -9,11 +9,9 @@ import struct import subprocess import time -import decode -import trezor_agent.client -import trezor_agent.formats -import trezor_agent.util -import util +from . import decode +from .. import client, factory, formats +from .. import util log = logging.getLogger(__name__) @@ -79,23 +77,24 @@ def _dump_nist256(vk): def _dump_ed25519(vk): return mpi((0x40 << 256) | - trezor_agent.util.bytes2num(vk.to_bytes())) + util.bytes2num(vk.to_bytes())) SUPPORTED_CURVES = { - trezor_agent.formats.CURVE_NIST256: { + formats.CURVE_NIST256: { # https://tools.ietf.org/html/rfc6637#section-11 'oid': b'\x2A\x86\x48\xCE\x3D\x03\x01\x07', 'algo_id': 19, 'dump': _dump_nist256 }, - trezor_agent.formats.CURVE_ED25519: { + formats.CURVE_ED25519: { 'oid': b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01', 'algo_id': 22, 'dump': _dump_ed25519 } } + def find_curve_by_algo_id(algo_id): curve_name, = [name for name, info in SUPPORTED_CURVES.items() if info['algo_id'] == algo_id] @@ -106,19 +105,19 @@ class Signer(object): def __init__(self, user_id, created, curve_name): self.user_id = user_id - assert curve_name in trezor_agent.formats.SUPPORTED_CURVES + assert curve_name in formats.SUPPORTED_CURVES self.curve_name = curve_name - self.client_wrapper = trezor_agent.factory.load() + self.client_wrapper = factory.load() self.identity = self.client_wrapper.identity_type() self.identity.proto = 'gpg' self.identity.host = user_id - addr = trezor_agent.client.get_address(self.identity) + addr = client.get_address(self.identity) public_node = self.client_wrapper.connection.get_public_node( n=addr, ecdsa_curve_name=self.curve_name) - self.verifying_key = trezor_agent.formats.decompress_pubkey( + self.verifying_key = formats.decompress_pubkey( pubkey=public_node.node.public_key, curve_name=self.curve_name) @@ -185,6 +184,7 @@ class Signer(object): data_to_sign=msg, hashed_subpackets=hashed_subpackets) return packet(tag=2, blob=blob) + def _make_signature(self, visual, data_to_sign, hashed_subpackets, sig_type=0): curve_info = SUPPORTED_CURVES[self.curve_name] @@ -210,8 +210,8 @@ class Signer(object): ecdsa_curve_name=self.curve_name) assert result.signature[:1] == b'\x00' sig = result.signature[1:] - sig = [trezor_agent.util.bytes2num(sig[:32]), - trezor_agent.util.bytes2num(sig[32:])] + sig = [util.bytes2num(sig[:32]), + util.bytes2num(sig[32:])] hash_prefix = digest[:2] # used for decoder's sanity check signature = mpi(sig[0]) + mpi(sig[1]) # actual ECDSA signature diff --git a/trezor_agent/gpg/util.py b/trezor_agent/gpg/util.py deleted file mode 100644 index a2b1f57..0000000 --- a/trezor_agent/gpg/util.py +++ /dev/null @@ -1,18 +0,0 @@ -import struct - - -def crc24(blob): - CRC24_INIT = 0xB704CEL - CRC24_POLY = 0x1864CFBL - - crc = CRC24_INIT - for octet in bytearray(blob): - crc ^= (octet << 16) - for _ in range(8): - crc <<= 1 - if crc & 0x1000000: - crc ^= CRC24_POLY - assert 0 <= crc < 0x1000000 - crc_bytes = struct.pack('>L', crc) - assert crc_bytes[0] == b'\x00' - return crc_bytes[1:] diff --git a/trezor_agent/util.py b/trezor_agent/util.py index 8119a82..822dbe7 100644 --- a/trezor_agent/util.py +++ b/trezor_agent/util.py @@ -75,3 +75,20 @@ def frame(*msgs): res.write(msg) msg = res.getvalue() return pack('L', len(msg)) + msg + + +def crc24(blob): + CRC24_INIT = 0xB704CEL + CRC24_POLY = 0x1864CFBL + + crc = CRC24_INIT + for octet in bytearray(blob): + crc ^= (octet << 16) + for _ in range(8): + crc <<= 1 + if crc & 0x1000000: + crc ^= CRC24_POLY + assert 0 <= crc < 0x1000000 + crc_bytes = struct.pack('>L', crc) + assert crc_bytes[0] == b'\x00' + return crc_bytes[1:] From 6f4f33bfa5fa81b3ff0ed39372cc89c4c69d5a4e Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 22:41:43 +0300 Subject: [PATCH 23/31] gpg: verify signature after signing --- trezor_agent/gpg/check.py | 6 +++--- trezor_agent/gpg/signer.py | 28 +++++++++++++++++----------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/trezor_agent/gpg/check.py b/trezor_agent/gpg/check.py index b1b3557..c36788a 100755 --- a/trezor_agent/gpg/check.py +++ b/trezor_agent/gpg/check.py @@ -17,7 +17,7 @@ def original_data(filename): return open(parts[0], 'rb').read() -def check(pubkey, sig_file): +def verify(pubkey, sig_file): d = open(sig_file, 'rb') if d.name.endswith('.asc'): lines = d.readlines()[3:-1] @@ -29,6 +29,7 @@ def check(pubkey, sig_file): signature, = list(parser) decode.verify_digest(pubkey=pubkey, digest=signature['digest'], signature=signature['sig'], label='GPG signature') + log.info('%s OK', sig_file) def main(): @@ -38,9 +39,8 @@ def main(): p.add_argument('pubkey') p.add_argument('signature') args = p.parse_args() - check(pubkey=decode.load_public_key(open(args.pubkey, 'rb')), + verify(pubkey=decode.load_public_key(open(args.pubkey, 'rb')), sig_file=args.signature) - log.info('OK') if __name__ == '__main__': main() diff --git a/trezor_agent/gpg/signer.py b/trezor_agent/gpg/signer.py index 3b644d1..b37be83 100755 --- a/trezor_agent/gpg/signer.py +++ b/trezor_agent/gpg/signer.py @@ -9,9 +9,8 @@ import struct import subprocess import time -from . import decode -from .. import client, factory, formats -from .. import util +from . import decode, check +from .. import client, factory, formats, util log = logging.getLogger(__name__) @@ -125,6 +124,15 @@ class Signer(object): log.info('%s GPG public key %s created at %s', self.curve_name, self.hex_short_key_id(), time_format(self.created)) + @classmethod + def from_public_key(cls, pubkey, user_id): + s = Signer(user_id=user_id, + created=pubkey['created'], + curve_name=find_curve_by_algo_id(pubkey['algo'])) + assert s.key_id() == pubkey['key_id'] + return s + + def _pubkey_data(self): curve_info = SUPPORTED_CURVES[self.curve_name] header = struct.pack('>BLB', @@ -237,12 +245,7 @@ def armor(blob, type_str): def load_from_gpg(user_id): log.info('loading public key %r from local GPG keyring', user_id) pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) - pubkey = decode.load_public_key(io.BytesIO(pubkey_bytes)) - s = Signer(user_id=user_id, - created=pubkey['created'], - curve_name=find_curve_by_algo_id(pubkey['algo'])) - assert s.key_id() == pubkey['key_id'] - return s + return decode.load_public_key(io.BytesIO(pubkey_bytes)) def main(): @@ -270,13 +273,16 @@ def main(): open(filename, 'wb').write(pubkey) log.info('import to local keyring using "gpg2 --import %s"', filename) else: - s = load_from_gpg(user_id) + pubkey = load_from_gpg(user_id) + s = Signer.from_public_key(pubkey=pubkey, user_id=user_id) data = open(args.filename, 'rb').read() sig, ext = s.sign(data), '.sig' if args.armor: sig = armor(sig, 'SIGNATURE') ext = '.asc' - open(args.filename + ext, 'wb').write(sig) + filename = args.filename + ext + open(filename, 'wb').write(sig) + check.verify(pubkey=pubkey, sig_file=filename) s.close() From 489c8fe3574b98c62449cde9738714dbb089fc71 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 22:45:11 +0300 Subject: [PATCH 24/31] gpg: rename git wrapper --- trezor_agent/gpg/{git_gpg_wrapper.py => git_cmd.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename trezor_agent/gpg/{git_gpg_wrapper.py => git_cmd.py} (100%) diff --git a/trezor_agent/gpg/git_gpg_wrapper.py b/trezor_agent/gpg/git_cmd.py similarity index 100% rename from trezor_agent/gpg/git_gpg_wrapper.py rename to trezor_agent/gpg/git_cmd.py From 40377fc66b254416a17ebcf61c6880e304290d67 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 22:46:24 +0300 Subject: [PATCH 25/31] gpg: add __init__.py --- trezor_agent/gpg/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 trezor_agent/gpg/__init__.py diff --git a/trezor_agent/gpg/__init__.py b/trezor_agent/gpg/__init__.py new file mode 100644 index 0000000..e69de29 From 0c94363595abf90d58be0c50fbdaaf3f9d8dfb26 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 22:55:34 +0300 Subject: [PATCH 26/31] gpg: export command-line tool --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 601e124..6b63997 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,6 @@ setup( entry_points={'console_scripts': [ 'trezor-agent = trezor_agent.__main__:run_agent', 'trezor-git = trezor_agent.__main__:run_git', + 'trezor-gpg = trezor_agent.gpg.signer:main', ]}, ) From 6cc3a629a80472c7bfaad53c62d956dd10a840e3 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 23:13:06 +0300 Subject: [PATCH 27/31] gpg: export git-gpg wrapper should be used as 'gpg.program' in .git/config --- setup.py | 1 + trezor_agent/gpg/{git_cmd.py => git_wrapper.py} | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) rename trezor_agent/gpg/{git_cmd.py => git_wrapper.py} (78%) diff --git a/setup.py b/setup.py index 6b63997..5e0da7a 100644 --- a/setup.py +++ b/setup.py @@ -31,5 +31,6 @@ setup( 'trezor-agent = trezor_agent.__main__:run_agent', 'trezor-git = trezor_agent.__main__:run_git', 'trezor-gpg = trezor_agent.gpg.signer:main', + 'trezor-git-gpg-wrapper = trezor_agent.gpg.git_wrapper:main', ]}, ) diff --git a/trezor_agent/gpg/git_cmd.py b/trezor_agent/gpg/git_wrapper.py similarity index 78% rename from trezor_agent/gpg/git_cmd.py rename to trezor_agent/gpg/git_wrapper.py index f3296bd..24f85bf 100755 --- a/trezor_agent/gpg/git_cmd.py +++ b/trezor_agent/gpg/git_wrapper.py @@ -17,9 +17,11 @@ def main(): if '--verify' in args: return sp.call(['gpg2'] + args) else: - command, user_id = args + command = args[0] + user_id = ' '.join(args[1:]) assert command == '-bsau' # --detach-sign --sign --armor --local-user - s = signer.load_from_gpg(user_id) + pubkey = signer.load_from_gpg(user_id) + s = signer.Signer.from_public_key(user_id=user_id, pubkey=pubkey) data = sys.stdin.read() sig = s.sign(data) From b6dbc4aa8182e9125af8697937797dcfbd69d734 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 23 Apr 2016 23:37:11 +0300 Subject: [PATCH 28/31] gpg: small fixes before merging to master --- trezor_agent/gpg/check.py | 2 +- trezor_agent/gpg/signer.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/trezor_agent/gpg/check.py b/trezor_agent/gpg/check.py index c36788a..15d4ac6 100755 --- a/trezor_agent/gpg/check.py +++ b/trezor_agent/gpg/check.py @@ -40,7 +40,7 @@ def main(): p.add_argument('signature') args = p.parse_args() verify(pubkey=decode.load_public_key(open(args.pubkey, 'rb')), - sig_file=args.signature) + sig_file=args.signature) if __name__ == '__main__': main() diff --git a/trezor_agent/gpg/signer.py b/trezor_agent/gpg/signer.py index b37be83..986b947 100755 --- a/trezor_agent/gpg/signer.py +++ b/trezor_agent/gpg/signer.py @@ -132,7 +132,6 @@ class Signer(object): assert s.key_id() == pubkey['key_id'] return s - def _pubkey_data(self): curve_info = SUPPORTED_CURVES[self.curve_name] header = struct.pack('>BLB', @@ -192,7 +191,6 @@ class Signer(object): data_to_sign=msg, hashed_subpackets=hashed_subpackets) return packet(tag=2, blob=blob) - def _make_signature(self, visual, data_to_sign, hashed_subpackets, sig_type=0): curve_info = SUPPORTED_CURVES[self.curve_name] @@ -224,7 +222,6 @@ class Signer(object): hash_prefix = digest[:2] # used for decoder's sanity check signature = mpi(sig[0]) + mpi(sig[1]) # actual ECDSA signature return header + hashed + unhashed + hash_prefix + signature - # TODO: add verification def split_lines(body, size): From a114242243b065fc2109d826329dc56c19e30895 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 24 Apr 2016 10:33:29 +0300 Subject: [PATCH 29/31] gpg: small fixes before merging to master --- trezor_agent/gpg/decode.py | 63 ++++++++++++++++++++------------------ trezor_agent/gpg/signer.py | 16 +++++----- trezor_agent/util.py | 4 +-- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/trezor_agent/gpg/decode.py b/trezor_agent/gpg/decode.py index 9eb7778..726b9e4 100644 --- a/trezor_agent/gpg/decode.py +++ b/trezor_agent/gpg/decode.py @@ -89,9 +89,38 @@ def split_bits(value, *bits): assert value == 0 return reversed(result) + +def _parse_nist256p1_verifier(mpi): + prefix, x, y = 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( + point=point, curve=ecdsa.curves.NIST256p, + hashfunc=hashlib.sha256) + + def _nist256p1_verify(signature, digest): + vk.verify_digest(signature=signature, + digest=digest, + sigdecode=lambda rs, order: rs) + return _nist256p1_verify + + +def _parse_ed25519_verifier(mpi): + prefix, value = split_bits(mpi, 8, 256) + assert prefix == 0x40 + vk = ed25519.VerifyingKey(num2bytes(value, size=32)) + + def _ed25519_verify(signature, digest): + sig = b''.join(num2bytes(val, size=32) + for val in signature) + vk.verify(sig, digest) + return _ed25519_verify + + SUPPORTED_CURVES = { - b'\x2A\x86\x48\xCE\x3D\x03\x01\x07': 'nist256p1', - b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01': 'ed25519', + 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, } @@ -177,37 +206,11 @@ class Parser(object): oid_size = stream.readfmt('B') oid = stream.read(oid_size) assert oid in SUPPORTED_CURVES - curve_name = SUPPORTED_CURVES[oid] + parser = SUPPORTED_CURVES[oid] mpi = parse_mpi(stream) log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length()) - if curve_name == 'nist256p1': - prefix, x, y = 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( - point=point, curve=ecdsa.curves.NIST256p, - hashfunc=hashlib.sha256) - - def _nist256p1_verify(signature, digest): - vk.verify_digest(signature=signature, - digest=digest, - sigdecode=lambda rs, order: rs) - p['verifier'] = _nist256p1_verify - elif curve_name == 'ed25519': - prefix, value = split_bits(mpi, 8, 256) - assert prefix == 0x40 - vk = ed25519.VerifyingKey(num2bytes(value, size=32)) - - def _ed25519_verify(signature, digest): - sig = b''.join(num2bytes(val, size=32) - for val in signature) - vk.verify(sig, digest) - p['verifier'] = _ed25519_verify - else: - raise ValueError('unsupported curve {}'.format(curve_name)) - + p['verifier'] = parser(mpi) assert not stream.read() # https://tools.ietf.org/html/rfc4880#section-12.2 diff --git a/trezor_agent/gpg/signer.py b/trezor_agent/gpg/signer.py index 986b947..4480615 100755 --- a/trezor_agent/gpg/signer.py +++ b/trezor_agent/gpg/signer.py @@ -216,12 +216,11 @@ class Signer(object): ecdsa_curve_name=self.curve_name) assert result.signature[:1] == b'\x00' sig = result.signature[1:] - sig = [util.bytes2num(sig[:32]), - util.bytes2num(sig[32:])] + sig = mpi(util.bytes2num(sig[:32])) + mpi(util.bytes2num(sig[32:])) - hash_prefix = digest[:2] # used for decoder's sanity check - signature = mpi(sig[0]) + mpi(sig[1]) # actual ECDSA signature - return header + hashed + unhashed + hash_prefix + signature + return (header + hashed + unhashed + + digest[:2] + # used for decoder's sanity check + sig) # actual ECDSA signature def split_lines(body, size): @@ -240,9 +239,12 @@ def armor(blob, type_str): def load_from_gpg(user_id): - log.info('loading public key %r from local GPG keyring', user_id) pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) - return decode.load_public_key(io.BytesIO(pubkey_bytes)) + if pubkey_bytes: + return decode.load_public_key(io.BytesIO(pubkey_bytes)) + else: + log.error('could not find public key %r in local GPG keyring', user_id) + raise KeyError(user_id) def main(): diff --git a/trezor_agent/util.py b/trezor_agent/util.py index 822dbe7..350114b 100644 --- a/trezor_agent/util.py +++ b/trezor_agent/util.py @@ -78,8 +78,8 @@ def frame(*msgs): def crc24(blob): - CRC24_INIT = 0xB704CEL - CRC24_POLY = 0x1864CFBL + CRC24_INIT = 0x0B704CE + CRC24_POLY = 0x1864CFB crc = CRC24_INIT for octet in bytearray(blob): From d7913a84d5cfe132957a8c1cd950892766066406 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 24 Apr 2016 12:22:02 +0300 Subject: [PATCH 30/31] gpg: pydocstyle fixes --- trezor_agent/gpg/__init__.py | 9 +++++++ trezor_agent/gpg/check.py | 4 +++ trezor_agent/gpg/decode.py | 44 ++++++++++++++++++++++----------- trezor_agent/gpg/git_wrapper.py | 2 ++ trezor_agent/gpg/signer.py | 35 +++++++++++++++++++++++--- trezor_agent/util.py | 1 + 6 files changed, 76 insertions(+), 19 deletions(-) diff --git a/trezor_agent/gpg/__init__.py b/trezor_agent/gpg/__init__.py index e69de29..f0448b4 100644 --- a/trezor_agent/gpg/__init__.py +++ b/trezor_agent/gpg/__init__.py @@ -0,0 +1,9 @@ +""" +TREZOR support for ECDSA GPG signatures. + +See these links for more details: + - https://www.gnupg.org/faq/whats-new-in-2.1.html + - https://tools.ietf.org/html/rfc4880 + - https://tools.ietf.org/html/rfc6637 + - https://tools.ietf.org/html/draft-irtf-cfrg-eddsa-05 +""" diff --git a/trezor_agent/gpg/check.py b/trezor_agent/gpg/check.py index 15d4ac6..f179582 100755 --- a/trezor_agent/gpg/check.py +++ b/trezor_agent/gpg/check.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +"""Check GPG v2 signature for a given public key.""" import argparse import base64 import io @@ -11,6 +12,7 @@ log = logging.getLogger(__name__) def original_data(filename): + """Locate and load original file data, whose signature is provided.""" parts = filename.rsplit('.', 1) if len(parts) == 2 and parts[1] in ('sig', 'asc'): log.debug('loading file %s', parts[0]) @@ -21,6 +23,7 @@ def verify(pubkey, sig_file): d = open(sig_file, 'rb') if d.name.endswith('.asc'): lines = d.readlines()[3:-1] + """Verify correctness of public key and signature.""" data = base64.b64decode(''.join(lines)) payload, checksum = data[:-3], data[-3:] assert util.crc24(payload) == checksum @@ -33,6 +36,7 @@ def verify(pubkey, sig_file): def main(): + """Main function.""" logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)-10s %(message)s') p = argparse.ArgumentParser() diff --git a/trezor_agent/gpg/decode.py b/trezor_agent/gpg/decode.py index 726b9e4..75fd030 100644 --- a/trezor_agent/gpg/decode.py +++ b/trezor_agent/gpg/decode.py @@ -1,3 +1,4 @@ +"""Decoders for GPG v2 data structures.""" import binascii import contextlib import hashlib @@ -14,31 +15,39 @@ log = logging.getLogger(__name__) def bit(value, i): + """Extract the i-th bit out of value.""" return 1 if value & (1 << i) else 0 def low_bits(value, n): + """Extract the lowest n bits out of value.""" return value & ((1 << n) - 1) def readfmt(stream, fmt): + """Read and unpack an object from stream, using a struct format string.""" size = struct.calcsize(fmt) blob = stream.read(size) return struct.unpack(fmt, blob) class Reader(object): + """Read basic type objects out of given stream.""" + def __init__(self, stream): + """Create a non-capturing reader.""" self.s = stream self._captured = None def readfmt(self, fmt): + """Read a specified object, using a struct format string.""" size = struct.calcsize(fmt) blob = self.read(size) obj, = struct.unpack(fmt, blob) return obj def read(self, size=None): + """Read `size` bytes from stream.""" blob = self.s.read(size) if size is not None and len(blob) < size: raise EOFError @@ -48,6 +57,7 @@ class Reader(object): @contextlib.contextmanager def capture(self, stream): + """Capture all data read during this context.""" self._captured = stream try: yield @@ -58,6 +68,7 @@ length_types = {0: '>B', 1: '>H', 2: '>L'} def parse_subpackets(s): + """See https://tools.ietf.org/html/rfc4880#section-5.2.3.1 for details.""" subpackets = [] total_size = s.readfmt('>H') data = s.read(total_size) @@ -75,12 +86,18 @@ def parse_subpackets(s): def parse_mpi(s): + """See https://tools.ietf.org/html/rfc4880#section-3.2 for details.""" bits = s.readfmt('>H') blob = bytearray(s.read(int((bits + 7) // 8))) return sum(v << (8 * i) for i, v in enumerate(reversed(blob))) def split_bits(value, *bits): + """ + Split integer value into list of ints, according to `bits` list. + + For example, split_bits(0x1234, 4, 8, 4) == [0x1, 0x23, 0x4] + """ result = [] for b in reversed(bits): mask = (1 << b) - 1 @@ -125,11 +142,13 @@ SUPPORTED_CURVES = { class Parser(object): + """Parse GPG packets from a given stream.""" + def __init__(self, stream, to_hash=None): + """Create an empty parser.""" self.stream = stream self.packet_types = { 2: self.signature, - 4: self.onepass, 6: self.pubkey, 11: self.literal, 13: self.user_id, @@ -139,21 +158,11 @@ class Parser(object): self.to_hash.write(to_hash) def __iter__(self): + """Support iterative parsing of available GPG packets.""" return self - def onepass(self, stream): - # pylint: disable=no-self-use - p = {'type': 'onepass'} - p['version'] = stream.readfmt('B') - p['sig_type'] = stream.readfmt('B') - p['hash_alg'] = stream.readfmt('B') - p['pubkey_alg'] = stream.readfmt('B') - p['key_id'] = stream.readfmt('8s') - p['nested'] = stream.readfmt('B') - assert not stream.read() - return p - def literal(self, stream): + """See https://tools.ietf.org/html/rfc4880#section-5.9 for details.""" p = {'type': 'literal'} p['format'] = stream.readfmt('c') filename_len = stream.readfmt('B') @@ -164,6 +173,7 @@ class Parser(object): return p def signature(self, stream): + """See https://tools.ietf.org/html/rfc4880#section-5.2 for details.""" p = {'type': 'signature'} to_hash = io.BytesIO() @@ -195,6 +205,7 @@ class Parser(object): return p def pubkey(self, stream): + """See https://tools.ietf.org/html/rfc4880#section-5.5 for details.""" p = {'type': 'pubkey'} packet = io.BytesIO() with stream.capture(packet): @@ -224,14 +235,15 @@ class Parser(object): return p def user_id(self, stream): + """See https://tools.ietf.org/html/rfc4880#section-5.11 for details.""" value = stream.read() self.to_hash.write(b'\xb4' + struct.pack('>L', len(value))) self.to_hash.write(value) return {'type': 'user_id', 'value': value} def __next__(self): + """See https://tools.ietf.org/html/rfc4880#section-4.2 for details.""" try: - # https://tools.ietf.org/html/rfc4880#section-4.2 value = self.stream.readfmt('B') except EOFError: raise StopIteration @@ -252,7 +264,7 @@ class Parser(object): if packet_type: p = packet_type(Reader(io.BytesIO(packet_data))) else: - p = {'type': 'UNKNOWN'} + raise ValueError('Unknown packet type: {}'.format(packet_type)) p['tag'] = tag log.debug('packet "%s": %s', p['type'], p) return p @@ -261,6 +273,7 @@ class Parser(object): def load_public_key(stream): + """Parse and validate GPG public key from an input stream.""" parser = Parser(Reader(stream)) pubkey, userid, signature = list(parser) log.debug('loaded public key "%s"', userid['value']) @@ -270,6 +283,7 @@ def load_public_key(stream): def verify_digest(pubkey, digest, signature, label): + """Verify a digest signature from a specified public key.""" verifier = pubkey['verifier'] try: verifier(signature, digest) diff --git a/trezor_agent/gpg/git_wrapper.py b/trezor_agent/gpg/git_wrapper.py index 24f85bf..7b6d9a2 100755 --- a/trezor_agent/gpg/git_wrapper.py +++ b/trezor_agent/gpg/git_wrapper.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +"""A simple wrapper for Git commit/tag GPG signing.""" import logging import subprocess as sp import sys @@ -9,6 +10,7 @@ log = logging.getLogger(__name__) def main(): + """Main function.""" logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)-10s %(message)s') diff --git a/trezor_agent/gpg/signer.py b/trezor_agent/gpg/signer.py index 4480615..b77a626 100755 --- a/trezor_agent/gpg/signer.py +++ b/trezor_agent/gpg/signer.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +"""Create GPG ECDSA signatures and public keys using TREZOR device.""" import argparse import base64 import binascii @@ -16,10 +17,12 @@ log = logging.getLogger(__name__) def prefix_len(fmt, blob): + """Prefix `blob` with its size, serialized using `fmt` format.""" return struct.pack(fmt, len(blob)) + blob def packet(tag, blob): + """Create small GPG packet.""" assert len(blob) < 256 length_type = 0 # : 1 byte for length leading_byte = 0x80 | (tag << 2) | (length_type) @@ -27,28 +30,34 @@ def packet(tag, blob): def subpacket(subpacket_type, fmt, *values): + """Create GPG subpacket.""" blob = struct.pack(fmt, *values) if values else fmt return struct.pack('>B', subpacket_type) + blob def subpacket_long(subpacket_type, value): + """Create GPG subpacket with 32-bit unsigned integer.""" return subpacket(subpacket_type, '>L', value) def subpacket_time(value): + """Create GPG subpacket with time in seconds (since Epoch).""" return subpacket_long(2, value) def subpacket_byte(subpacket_type, value): + """Create GPG subpacket with 8-bit unsigned integer.""" return subpacket(subpacket_type, '>B', value) def subpackets(*items): + """Serialize several GPG subpackets.""" prefixed = [prefix_len('>B', item) for item in items] return prefix_len('>H', b''.join(prefixed)) def mpi(value): + """Serialize multipresicion integer using GPG format.""" bits = value.bit_length() data_size = (bits + 7) // 8 data_bytes = [0] * data_size @@ -61,10 +70,12 @@ def mpi(value): def time_format(t): + """Utility for consistent time formatting.""" return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t)) def hexlify(blob): + """Utility for consistent hexadecimal formatting.""" return binascii.hexlify(blob).decode('ascii').upper() @@ -94,15 +105,17 @@ SUPPORTED_CURVES = { } -def find_curve_by_algo_id(algo_id): +def _find_curve_by_algo_id(algo_id): curve_name, = [name for name, info in SUPPORTED_CURVES.items() if info['algo_id'] == algo_id] return curve_name class Signer(object): + """Performs GPG operations with the TREZOR.""" def __init__(self, user_id, created, curve_name): + """Construct and loads a public key from the device.""" self.user_id = user_id assert curve_name in formats.SUPPORTED_CURVES self.curve_name = curve_name @@ -126,9 +139,15 @@ class Signer(object): @classmethod def from_public_key(cls, pubkey, user_id): + """ + Create from an existing GPG public key. + + `pubkey` should be loaded via `load_from_gpg(user_id)` + from the local GPG keyring. + """ s = Signer(user_id=user_id, created=pubkey['created'], - curve_name=find_curve_by_algo_id(pubkey['algo'])) + curve_name=_find_curve_by_algo_id(pubkey['algo'])) assert s.key_id() == pubkey['key_id'] return s @@ -149,16 +168,20 @@ class Signer(object): return hashlib.sha1(self._pubkey_data_to_hash()).digest() def key_id(self): + """Short (8 byte) GPG key ID.""" return self._fingerprint()[-8:] def hex_short_key_id(self): + """Short (8 hexadecimal digits) GPG key ID.""" return hexlify(self.key_id()[-4:]) def close(self): + """Close connection and turn off the screen of the device.""" self.client_wrapper.connection.clear_session() self.client_wrapper.connection.close() def export(self): + """Export GPG public key, ready for "gpg2 --import".""" pubkey_packet = packet(tag=6, blob=self._pubkey_data()) user_id_packet = packet(tag=13, blob=self.user_id) @@ -180,6 +203,7 @@ class Signer(object): return pubkey_packet + user_id_packet + sign_packet def sign(self, msg, sign_time=None): + """Sign GPG message at specified time.""" if sign_time is None: sign_time = int(time.time()) @@ -223,7 +247,7 @@ class Signer(object): sig) # actual ECDSA signature -def split_lines(body, size): +def _split_lines(body, size): lines = [] for i in range(0, len(body), size): lines.append(body[i:i+size] + '\n') @@ -231,14 +255,16 @@ def split_lines(body, size): def armor(blob, type_str): + """See https://tools.ietf.org/html/rfc4880#section-6 for details.""" head = '-----BEGIN PGP {}-----\nVersion: GnuPG v2\n\n'.format(type_str) body = base64.b64encode(blob) checksum = base64.b64encode(util.crc24(blob)) tail = '-----END PGP {}-----\n'.format(type_str) - return head + split_lines(body, 64) + '=' + checksum + '\n' + tail + return head + _split_lines(body, 64) + '=' + checksum + '\n' + tail def load_from_gpg(user_id): + """Load existing GPG public key for `user_id` from local keyring.""" pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) if pubkey_bytes: return decode.load_public_key(io.BytesIO(pubkey_bytes)) @@ -248,6 +274,7 @@ def load_from_gpg(user_id): def main(): + """Main function.""" p = argparse.ArgumentParser() p.add_argument('user_id') p.add_argument('filename', nargs='?') diff --git a/trezor_agent/util.py b/trezor_agent/util.py index 350114b..7e96428 100644 --- a/trezor_agent/util.py +++ b/trezor_agent/util.py @@ -78,6 +78,7 @@ def frame(*msgs): def crc24(blob): + """See https://tools.ietf.org/html/rfc4880#section-6.1 for details.""" CRC24_INIT = 0x0B704CE CRC24_POLY = 0x1864CFB From 9a435ae23ea52781d7390dd5e846c16ce60ca975 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sun, 24 Apr 2016 13:04:53 +0300 Subject: [PATCH 31/31] gpg: minor renames and code refactoring --- trezor_agent/gpg/check.py | 10 +- trezor_agent/gpg/decode.py | 109 +++---------- trezor_agent/gpg/encode.py | 243 +++++++++++++++++++++++++++ trezor_agent/gpg/git_wrapper.py | 8 +- trezor_agent/gpg/signer.py | 280 +------------------------------- trezor_agent/util.py | 84 ++++++++++ 6 files changed, 369 insertions(+), 365 deletions(-) create mode 100644 trezor_agent/gpg/encode.py diff --git a/trezor_agent/gpg/check.py b/trezor_agent/gpg/check.py index f179582..58ddba9 100755 --- a/trezor_agent/gpg/check.py +++ b/trezor_agent/gpg/check.py @@ -20,15 +20,15 @@ def original_data(filename): def verify(pubkey, sig_file): - d = open(sig_file, 'rb') - if d.name.endswith('.asc'): - lines = d.readlines()[3:-1] """Verify correctness of public key and signature.""" + stream = open(sig_file, 'rb') + if stream.name.endswith('.asc'): + lines = stream.readlines()[3:-1] data = base64.b64decode(''.join(lines)) payload, checksum = data[:-3], data[-3:] assert util.crc24(payload) == checksum - d = io.BytesIO(payload) - parser = decode.Parser(decode.Reader(d), original_data(sig_file)) + stream = io.BytesIO(payload) + parser = decode.Parser(util.Reader(stream), original_data(sig_file)) signature, = list(parser) decode.verify_digest(pubkey=pubkey, digest=signature['digest'], signature=signature['sig'], label='GPG signature') diff --git a/trezor_agent/gpg/decode.py b/trezor_agent/gpg/decode.py index 75fd030..035285d 100644 --- a/trezor_agent/gpg/decode.py +++ b/trezor_agent/gpg/decode.py @@ -1,78 +1,24 @@ """Decoders for GPG v2 data structures.""" -import binascii -import contextlib import hashlib import io import logging import struct +import subprocess import ecdsa import ed25519 -from trezor_agent.util import num2bytes +from .. import util log = logging.getLogger(__name__) -def bit(value, i): - """Extract the i-th bit out of value.""" - return 1 if value & (1 << i) else 0 - - -def low_bits(value, n): - """Extract the lowest n bits out of value.""" - return value & ((1 << n) - 1) - - -def readfmt(stream, fmt): - """Read and unpack an object from stream, using a struct format string.""" - size = struct.calcsize(fmt) - blob = stream.read(size) - return struct.unpack(fmt, blob) - - -class Reader(object): - """Read basic type objects out of given stream.""" - - def __init__(self, stream): - """Create a non-capturing reader.""" - self.s = stream - self._captured = None - - def readfmt(self, fmt): - """Read a specified object, using a struct format string.""" - size = struct.calcsize(fmt) - blob = self.read(size) - obj, = struct.unpack(fmt, blob) - return obj - - def read(self, size=None): - """Read `size` bytes from stream.""" - blob = self.s.read(size) - if size is not None and len(blob) < size: - raise EOFError - if self._captured: - self._captured.write(blob) - return blob - - @contextlib.contextmanager - def capture(self, stream): - """Capture all data read during this context.""" - self._captured = stream - try: - yield - finally: - self._captured = None - -length_types = {0: '>B', 1: '>H', 2: '>L'} - - def parse_subpackets(s): """See https://tools.ietf.org/html/rfc4880#section-5.2.3.1 for details.""" subpackets = [] total_size = s.readfmt('>H') data = s.read(total_size) - s = Reader(io.BytesIO(data)) + s = util.Reader(io.BytesIO(data)) while True: try: @@ -92,23 +38,8 @@ def parse_mpi(s): return sum(v << (8 * i) for i, v in enumerate(reversed(blob))) -def split_bits(value, *bits): - """ - Split integer value into list of ints, according to `bits` list. - - For example, split_bits(0x1234, 4, 8, 4) == [0x1, 0x23, 0x4] - """ - result = [] - for b in reversed(bits): - mask = (1 << b) - 1 - result.append(value & mask) - value = value >> b - assert value == 0 - return reversed(result) - - def _parse_nist256p1_verifier(mpi): - prefix, x, y = split_bits(mpi, 4, 256, 256) + 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) @@ -124,12 +55,12 @@ def _parse_nist256p1_verifier(mpi): def _parse_ed25519_verifier(mpi): - prefix, value = split_bits(mpi, 8, 256) + prefix, value = util.split_bits(mpi, 8, 256) assert prefix == 0x40 - vk = ed25519.VerifyingKey(num2bytes(value, size=32)) + vk = ed25519.VerifyingKey(util.num2bytes(value, size=32)) def _ed25519_verify(signature, digest): - sig = b''.join(num2bytes(val, size=32) + sig = b''.join(util.num2bytes(val, size=32) for val in signature) vk.verify(sig, digest) return _ed25519_verify @@ -229,7 +160,7 @@ class Parser(object): data_to_hash = (b'\x99' + struct.pack('>H', len(packet_data)) + packet_data) p['key_id'] = hashlib.sha1(data_to_hash).digest()[-8:] - log.debug('key ID: %s', binascii.hexlify(p['key_id']).decode('ascii')) + log.debug('key ID: %s', util.hexlify(p['key_id'])) self.to_hash.write(data_to_hash) return p @@ -249,20 +180,20 @@ class Parser(object): raise StopIteration log.debug('prefix byte: %02x', value) - assert bit(value, 7) == 1 - assert bit(value, 6) == 0 # new format not supported yet + assert util.bit(value, 7) == 1 + assert util.bit(value, 6) == 0 # new format not supported yet - tag = low_bits(value, 6) - length_type = low_bits(tag, 2) + tag = util.low_bits(value, 6) + length_type = util.low_bits(tag, 2) tag = tag >> 2 - fmt = length_types[length_type] + fmt = {0: '>B', 1: '>H', 2: '>L'}[length_type] log.debug('length_type: %s', fmt) packet_size = self.stream.readfmt(fmt) log.debug('packet length: %d', packet_size) packet_data = self.stream.read(packet_size) packet_type = self.packet_types.get(tag) if packet_type: - p = packet_type(Reader(io.BytesIO(packet_data))) + p = packet_type(util.Reader(io.BytesIO(packet_data))) else: raise ValueError('Unknown packet type: {}'.format(packet_type)) p['tag'] = tag @@ -274,7 +205,7 @@ class Parser(object): def load_public_key(stream): """Parse and validate GPG public key from an input stream.""" - parser = Parser(Reader(stream)) + parser = Parser(util.Reader(stream)) pubkey, userid, signature = list(parser) log.debug('loaded public key "%s"', userid['value']) verify_digest(pubkey=pubkey, digest=signature['digest'], @@ -282,6 +213,16 @@ def load_public_key(stream): return pubkey +def load_from_gpg(user_id): + """Load existing GPG public key for `user_id` from local keyring.""" + pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) + if pubkey_bytes: + return load_public_key(io.BytesIO(pubkey_bytes)) + else: + log.error('could not find public key %r in local GPG keyring', user_id) + raise KeyError(user_id) + + def verify_digest(pubkey, digest, signature, label): """Verify a digest signature from a specified public key.""" verifier = pubkey['verifier'] diff --git a/trezor_agent/gpg/encode.py b/trezor_agent/gpg/encode.py new file mode 100644 index 0000000..db23955 --- /dev/null +++ b/trezor_agent/gpg/encode.py @@ -0,0 +1,243 @@ +"""Create GPG ECDSA signatures and public keys using TREZOR device.""" +import base64 +import hashlib +import logging +import struct +import time + +from .. import client, factory, formats, util + +log = logging.getLogger(__name__) + + +def packet(tag, blob): + """Create small GPG packet.""" + assert len(blob) < 256 + length_type = 0 # : 1 byte for length + leading_byte = 0x80 | (tag << 2) | (length_type) + return struct.pack('>B', leading_byte) + util.prefix_len('>B', blob) + + +def subpacket(subpacket_type, fmt, *values): + """Create GPG subpacket.""" + blob = struct.pack(fmt, *values) if values else fmt + return struct.pack('>B', subpacket_type) + blob + + +def subpacket_long(subpacket_type, value): + """Create GPG subpacket with 32-bit unsigned integer.""" + return subpacket(subpacket_type, '>L', value) + + +def subpacket_time(value): + """Create GPG subpacket with time in seconds (since Epoch).""" + return subpacket_long(2, value) + + +def subpacket_byte(subpacket_type, value): + """Create GPG subpacket with 8-bit unsigned integer.""" + return subpacket(subpacket_type, '>B', value) + + +def subpackets(*items): + """Serialize several GPG subpackets.""" + prefixed = [util.prefix_len('>B', item) for item in items] + return util.prefix_len('>H', b''.join(prefixed)) + + +def mpi(value): + """Serialize multipresicion integer using GPG format.""" + bits = value.bit_length() + data_size = (bits + 7) // 8 + data_bytes = [0] * data_size + for i in range(data_size): + data_bytes[i] = value & 0xFF + value = value >> 8 + + data_bytes.reverse() + return struct.pack('>H', bits) + bytearray(data_bytes) + + +def _dump_nist256(vk): + return mpi((4 << 512) | + (vk.pubkey.point.x() << 256) | + (vk.pubkey.point.y())) + + +def _dump_ed25519(vk): + return mpi((0x40 << 256) | + util.bytes2num(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, + 'dump': _dump_nist256 + }, + formats.CURVE_ED25519: { + 'oid': b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01', + 'algo_id': 22, + 'dump': _dump_ed25519 + } +} + + +def _find_curve_by_algo_id(algo_id): + curve_name, = [name for name, info in SUPPORTED_CURVES.items() + if info['algo_id'] == algo_id] + return curve_name + + +class Signer(object): + """Performs GPG operations with the TREZOR.""" + + def __init__(self, user_id, created, curve_name): + """Construct and loads a public key from the device.""" + self.user_id = user_id + assert curve_name in formats.SUPPORTED_CURVES + self.curve_name = curve_name + self.client_wrapper = factory.load() + + self.identity = self.client_wrapper.identity_type() + self.identity.proto = 'gpg' + self.identity.host = user_id + + addr = client.get_address(self.identity) + public_node = self.client_wrapper.connection.get_public_node( + n=addr, ecdsa_curve_name=self.curve_name) + + self.verifying_key = formats.decompress_pubkey( + pubkey=public_node.node.public_key, + curve_name=self.curve_name) + + self.created = int(created) + log.info('%s GPG public key %s created at %s', self.curve_name, + self.hex_short_key_id(), util.time_format(self.created)) + + @classmethod + def from_public_key(cls, pubkey, user_id): + """ + Create from an existing GPG public key. + + `pubkey` should be loaded via `load_from_gpg(user_id)` + from the local GPG keyring. + """ + s = Signer(user_id=user_id, + created=pubkey['created'], + curve_name=_find_curve_by_algo_id(pubkey['algo'])) + assert s.key_id() == pubkey['key_id'] + return s + + def _pubkey_data(self): + curve_info = SUPPORTED_CURVES[self.curve_name] + header = struct.pack('>BLB', + 4, # version + self.created, # creation + curve_info['algo_id']) + oid = util.prefix_len('>B', curve_info['oid']) + blob = curve_info['dump'](self.verifying_key) + return header + oid + blob + + def _pubkey_data_to_hash(self): + return b'\x99' + util.prefix_len('>H', self._pubkey_data()) + + def _fingerprint(self): + return hashlib.sha1(self._pubkey_data_to_hash()).digest() + + def key_id(self): + """Short (8 byte) GPG key ID.""" + return self._fingerprint()[-8:] + + def hex_short_key_id(self): + """Short (8 hexadecimal digits) GPG key ID.""" + return util.hexlify(self.key_id()[-4:]) + + def close(self): + """Close connection and turn off the screen of the device.""" + self.client_wrapper.connection.clear_session() + self.client_wrapper.connection.close() + + def export(self): + """Export GPG public key, ready for "gpg2 --import".""" + pubkey_packet = packet(tag=6, blob=self._pubkey_data()) + user_id_packet = packet(tag=13, blob=self.user_id) + + data_to_sign = (self._pubkey_data_to_hash() + + user_id_packet[:1] + + util.prefix_len('>L', self.user_id)) + log.info('signing public key "%s"', self.user_id) + hashed_subpackets = [ + subpacket_time(self.created), # signature creaion time + subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) + subpacket_byte(0x15, 8), # preferred hash (SHA256) + subpacket_byte(0x16, 0), # preferred compression (none) + subpacket_byte(0x17, 0x80)] # key server prefs (no-modify) + signature = self._make_signature(visual=self.hex_short_key_id(), + data_to_sign=data_to_sign, + sig_type=0x13, # user id & public key + hashed_subpackets=hashed_subpackets) + + sign_packet = packet(tag=2, blob=signature) + return pubkey_packet + user_id_packet + sign_packet + + def sign(self, msg, sign_time=None): + """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), util.time_format(sign_time)) + hashed_subpackets = [subpacket_time(sign_time)] + blob = self._make_signature( + visual=self.hex_short_key_id(), + data_to_sign=msg, hashed_subpackets=hashed_subpackets) + return packet(tag=2, blob=blob) + + def _make_signature(self, visual, data_to_sign, + hashed_subpackets, sig_type=0): + curve_info = SUPPORTED_CURVES[self.curve_name] + header = struct.pack('>BBBB', + 4, # version + sig_type, # rfc4880 (section-5.2.1) + curve_info['algo_id'], + 8) # hash_alg (SHA256) + hashed = subpackets(*hashed_subpackets) + unhashed = subpackets( + subpacket(16, self.key_id()) # issuer key id + ) + tail = b'\x04\xff' + struct.pack('>L', len(header) + len(hashed)) + data_to_hash = data_to_sign + header + hashed + tail + + log.debug('hashing %d bytes', len(data_to_hash)) + digest = hashlib.sha256(data_to_hash).digest() + + result = self.client_wrapper.connection.sign_identity( + identity=self.identity, + challenge_hidden=digest, + challenge_visual=visual, + ecdsa_curve_name=self.curve_name) + assert result.signature[:1] == b'\x00' + sig = result.signature[1:] + sig = mpi(util.bytes2num(sig[:32])) + mpi(util.bytes2num(sig[32:])) + + return (header + hashed + unhashed + + digest[:2] + # used for decoder's sanity check + sig) # actual ECDSA signature + + +def _split_lines(body, size): + lines = [] + for i in range(0, len(body), size): + lines.append(body[i:i+size] + '\n') + return ''.join(lines) + + +def armor(blob, type_str): + """See https://tools.ietf.org/html/rfc4880#section-6 for details.""" + head = '-----BEGIN PGP {}-----\nVersion: GnuPG v2\n\n'.format(type_str) + body = base64.b64encode(blob) + checksum = base64.b64encode(util.crc24(blob)) + tail = '-----END PGP {}-----\n'.format(type_str) + return head + _split_lines(body, 64) + '=' + checksum + '\n' + tail diff --git a/trezor_agent/gpg/git_wrapper.py b/trezor_agent/gpg/git_wrapper.py index 7b6d9a2..33df87e 100755 --- a/trezor_agent/gpg/git_wrapper.py +++ b/trezor_agent/gpg/git_wrapper.py @@ -4,7 +4,7 @@ import logging import subprocess as sp import sys -from . import signer +from . import decode, encode log = logging.getLogger(__name__) @@ -22,12 +22,12 @@ def main(): command = args[0] user_id = ' '.join(args[1:]) assert command == '-bsau' # --detach-sign --sign --armor --local-user - pubkey = signer.load_from_gpg(user_id) - s = signer.Signer.from_public_key(user_id=user_id, pubkey=pubkey) + pubkey = decode.load_from_gpg(user_id) + s = encode.Signer.from_public_key(user_id=user_id, pubkey=pubkey) data = sys.stdin.read() sig = s.sign(data) - sig = signer.armor(sig, 'SIGNATURE') + sig = encode.armor(sig, 'SIGNATURE') sys.stdout.write(sig) s.close() diff --git a/trezor_agent/gpg/signer.py b/trezor_agent/gpg/signer.py index b77a626..0af2342 100755 --- a/trezor_agent/gpg/signer.py +++ b/trezor_agent/gpg/signer.py @@ -1,278 +1,14 @@ #!/usr/bin/env python -"""Create GPG ECDSA signatures and public keys using TREZOR device.""" +"""Create signatures and export public keys for GPG using TREZOR.""" import argparse -import base64 -import binascii -import hashlib -import io import logging -import struct -import subprocess import time -from . import decode, check -from .. import client, factory, formats, util +from . import check, decode, encode log = logging.getLogger(__name__) -def prefix_len(fmt, blob): - """Prefix `blob` with its size, serialized using `fmt` format.""" - return struct.pack(fmt, len(blob)) + blob - - -def packet(tag, blob): - """Create small GPG packet.""" - assert len(blob) < 256 - length_type = 0 # : 1 byte for length - leading_byte = 0x80 | (tag << 2) | (length_type) - return struct.pack('>B', leading_byte) + prefix_len('>B', blob) - - -def subpacket(subpacket_type, fmt, *values): - """Create GPG subpacket.""" - blob = struct.pack(fmt, *values) if values else fmt - return struct.pack('>B', subpacket_type) + blob - - -def subpacket_long(subpacket_type, value): - """Create GPG subpacket with 32-bit unsigned integer.""" - return subpacket(subpacket_type, '>L', value) - - -def subpacket_time(value): - """Create GPG subpacket with time in seconds (since Epoch).""" - return subpacket_long(2, value) - - -def subpacket_byte(subpacket_type, value): - """Create GPG subpacket with 8-bit unsigned integer.""" - return subpacket(subpacket_type, '>B', value) - - -def subpackets(*items): - """Serialize several GPG subpackets.""" - prefixed = [prefix_len('>B', item) for item in items] - return prefix_len('>H', b''.join(prefixed)) - - -def mpi(value): - """Serialize multipresicion integer using GPG format.""" - bits = value.bit_length() - data_size = (bits + 7) // 8 - data_bytes = [0] * data_size - for i in range(data_size): - data_bytes[i] = value & 0xFF - value = value >> 8 - - data_bytes.reverse() - return struct.pack('>H', bits) + bytearray(data_bytes) - - -def time_format(t): - """Utility for consistent time formatting.""" - return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t)) - - -def hexlify(blob): - """Utility for consistent hexadecimal formatting.""" - return binascii.hexlify(blob).decode('ascii').upper() - - -def _dump_nist256(vk): - return mpi((4 << 512) | - (vk.pubkey.point.x() << 256) | - (vk.pubkey.point.y())) - - -def _dump_ed25519(vk): - return mpi((0x40 << 256) | - util.bytes2num(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, - 'dump': _dump_nist256 - }, - formats.CURVE_ED25519: { - 'oid': b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01', - 'algo_id': 22, - 'dump': _dump_ed25519 - } -} - - -def _find_curve_by_algo_id(algo_id): - curve_name, = [name for name, info in SUPPORTED_CURVES.items() - if info['algo_id'] == algo_id] - return curve_name - - -class Signer(object): - """Performs GPG operations with the TREZOR.""" - - def __init__(self, user_id, created, curve_name): - """Construct and loads a public key from the device.""" - self.user_id = user_id - assert curve_name in formats.SUPPORTED_CURVES - self.curve_name = curve_name - self.client_wrapper = factory.load() - - self.identity = self.client_wrapper.identity_type() - self.identity.proto = 'gpg' - self.identity.host = user_id - - addr = client.get_address(self.identity) - public_node = self.client_wrapper.connection.get_public_node( - n=addr, ecdsa_curve_name=self.curve_name) - - self.verifying_key = formats.decompress_pubkey( - pubkey=public_node.node.public_key, - curve_name=self.curve_name) - - self.created = int(created) - log.info('%s GPG public key %s created at %s', self.curve_name, - self.hex_short_key_id(), time_format(self.created)) - - @classmethod - def from_public_key(cls, pubkey, user_id): - """ - Create from an existing GPG public key. - - `pubkey` should be loaded via `load_from_gpg(user_id)` - from the local GPG keyring. - """ - s = Signer(user_id=user_id, - created=pubkey['created'], - curve_name=_find_curve_by_algo_id(pubkey['algo'])) - assert s.key_id() == pubkey['key_id'] - return s - - def _pubkey_data(self): - curve_info = SUPPORTED_CURVES[self.curve_name] - header = struct.pack('>BLB', - 4, # version - self.created, # creation - curve_info['algo_id']) - oid = prefix_len('>B', curve_info['oid']) - blob = curve_info['dump'](self.verifying_key) - return header + oid + blob - - def _pubkey_data_to_hash(self): - return b'\x99' + prefix_len('>H', self._pubkey_data()) - - def _fingerprint(self): - return hashlib.sha1(self._pubkey_data_to_hash()).digest() - - def key_id(self): - """Short (8 byte) GPG key ID.""" - return self._fingerprint()[-8:] - - def hex_short_key_id(self): - """Short (8 hexadecimal digits) GPG key ID.""" - return hexlify(self.key_id()[-4:]) - - def close(self): - """Close connection and turn off the screen of the device.""" - self.client_wrapper.connection.clear_session() - self.client_wrapper.connection.close() - - def export(self): - """Export GPG public key, ready for "gpg2 --import".""" - pubkey_packet = packet(tag=6, blob=self._pubkey_data()) - user_id_packet = packet(tag=13, blob=self.user_id) - - user_id_to_hash = user_id_packet[:1] + prefix_len('>L', self.user_id) - data_to_sign = self._pubkey_data_to_hash() + user_id_to_hash - log.info('signing public key "%s"', self.user_id) - hashed_subpackets = [ - subpacket_time(self.created), # signature creaion time - subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign) - subpacket_byte(0x15, 8), # preferred hash (SHA256) - subpacket_byte(0x16, 0), # preferred compression (none) - subpacket_byte(0x17, 0x80)] # key server prefs (no-modify) - signature = self._make_signature(visual=self.hex_short_key_id(), - data_to_sign=data_to_sign, - sig_type=0x13, # user id & public key - hashed_subpackets=hashed_subpackets) - - sign_packet = packet(tag=2, blob=signature) - return pubkey_packet + user_id_packet + sign_packet - - def sign(self, msg, sign_time=None): - """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 = [subpacket_time(sign_time)] - blob = self._make_signature( - visual=self.hex_short_key_id(), - data_to_sign=msg, hashed_subpackets=hashed_subpackets) - return packet(tag=2, blob=blob) - - def _make_signature(self, visual, data_to_sign, - hashed_subpackets, sig_type=0): - curve_info = SUPPORTED_CURVES[self.curve_name] - header = struct.pack('>BBBB', - 4, # version - sig_type, # rfc4880 (section-5.2.1) - curve_info['algo_id'], - 8) # hash_alg (SHA256) - hashed = subpackets(*hashed_subpackets) - unhashed = subpackets( - subpacket(16, self.key_id()) # issuer key id - ) - tail = b'\x04\xff' + struct.pack('>L', len(header) + len(hashed)) - data_to_hash = data_to_sign + header + hashed + tail - - log.debug('hashing %d bytes', len(data_to_hash)) - digest = hashlib.sha256(data_to_hash).digest() - - result = self.client_wrapper.connection.sign_identity( - identity=self.identity, - challenge_hidden=digest, - challenge_visual=visual, - ecdsa_curve_name=self.curve_name) - assert result.signature[:1] == b'\x00' - sig = result.signature[1:] - sig = mpi(util.bytes2num(sig[:32])) + mpi(util.bytes2num(sig[32:])) - - return (header + hashed + unhashed + - digest[:2] + # used for decoder's sanity check - sig) # actual ECDSA signature - - -def _split_lines(body, size): - lines = [] - for i in range(0, len(body), size): - lines.append(body[i:i+size] + '\n') - return ''.join(lines) - - -def armor(blob, type_str): - """See https://tools.ietf.org/html/rfc4880#section-6 for details.""" - head = '-----BEGIN PGP {}-----\nVersion: GnuPG v2\n\n'.format(type_str) - body = base64.b64encode(blob) - checksum = base64.b64encode(util.crc24(blob)) - tail = '-----END PGP {}-----\n'.format(type_str) - return head + _split_lines(body, 64) + '=' + checksum + '\n' + tail - - -def load_from_gpg(user_id): - """Load existing GPG public key for `user_id` from local keyring.""" - pubkey_bytes = subprocess.check_output(['gpg2', '--export', user_id]) - if pubkey_bytes: - return decode.load_public_key(io.BytesIO(pubkey_bytes)) - else: - log.error('could not find public key %r in local GPG keyring', user_id) - raise KeyError(user_id) - - def main(): """Main function.""" p = argparse.ArgumentParser() @@ -288,23 +24,23 @@ def main(): format='%(asctime)s %(levelname)-10s %(message)s') user_id = args.user_id.encode('ascii') if not args.filename: - s = Signer(user_id=user_id, created=args.time, - curve_name=args.ecdsa_curve) + s = encode.Signer(user_id=user_id, created=args.time, + curve_name=args.ecdsa_curve) pubkey = s.export() ext = '.pub' if args.armor: - pubkey = armor(pubkey, 'PUBLIC KEY BLOCK') + pubkey = encode.armor(pubkey, 'PUBLIC KEY BLOCK') ext = '.asc' filename = s.hex_short_key_id() + ext open(filename, 'wb').write(pubkey) log.info('import to local keyring using "gpg2 --import %s"', filename) else: - pubkey = load_from_gpg(user_id) - s = Signer.from_public_key(pubkey=pubkey, user_id=user_id) + pubkey = decode.load_from_gpg(user_id) + s = encode.Signer.from_public_key(pubkey=pubkey, user_id=user_id) data = open(args.filename, 'rb').read() sig, ext = s.sign(data), '.sig' if args.armor: - sig = armor(sig, 'SIGNATURE') + sig = encode.armor(sig, 'SIGNATURE') ext = '.asc' filename = args.filename + ext open(filename, 'wb').write(sig) diff --git a/trezor_agent/util.py b/trezor_agent/util.py index 7e96428..89c3170 100644 --- a/trezor_agent/util.py +++ b/trezor_agent/util.py @@ -1,6 +1,9 @@ """Various I/O and serialization utilities.""" +import binascii +import contextlib import io import struct +import time def send(conn, data): @@ -93,3 +96,84 @@ def crc24(blob): crc_bytes = struct.pack('>L', crc) assert crc_bytes[0] == b'\x00' return crc_bytes[1:] + + +def bit(value, i): + """Extract the i-th bit out of value.""" + return 1 if value & (1 << i) else 0 + + +def low_bits(value, n): + """Extract the lowest n bits out of value.""" + return value & ((1 << n) - 1) + + +def split_bits(value, *bits): + """ + Split integer value into list of ints, according to `bits` list. + + For example, split_bits(0x1234, 4, 8, 4) == [0x1, 0x23, 0x4] + """ + result = [] + for b in reversed(bits): + mask = (1 << b) - 1 + result.append(value & mask) + value = value >> b + assert value == 0 + return reversed(result) + + +def readfmt(stream, fmt): + """Read and unpack an object from stream, using a struct format string.""" + size = struct.calcsize(fmt) + blob = stream.read(size) + return struct.unpack(fmt, blob) + + +def prefix_len(fmt, blob): + """Prefix `blob` with its size, serialized using `fmt` format.""" + return struct.pack(fmt, len(blob)) + blob + + +def time_format(t): + """Utility for consistent time formatting.""" + return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t)) + + +def hexlify(blob): + """Utility for consistent hexadecimal formatting.""" + return binascii.hexlify(blob).decode('ascii').upper() + + +class Reader(object): + """Read basic type objects out of given stream.""" + + def __init__(self, stream): + """Create a non-capturing reader.""" + self.s = stream + self._captured = None + + def readfmt(self, fmt): + """Read a specified object, using a struct format string.""" + size = struct.calcsize(fmt) + blob = self.read(size) + obj, = struct.unpack(fmt, blob) + return obj + + def read(self, size=None): + """Read `size` bytes from stream.""" + blob = self.s.read(size) + if size is not None and len(blob) < size: + raise EOFError + if self._captured: + self._captured.write(blob) + return blob + + @contextlib.contextmanager + def capture(self, stream): + """Capture all data read during this context.""" + self._captured = stream + try: + yield + finally: + self._captured = None