mirror of
https://github.com/romanz/amodem.git
synced 2026-04-20 21:26:39 +08:00
gpg: refactor decode to functional style
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Check GPG v2 signature for a given public key."""
|
"""Check GPG v2 signature for a given public key."""
|
||||||
import argparse
|
import argparse
|
||||||
import base64
|
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import pprint
|
|
||||||
|
|
||||||
from . import decode
|
from . import decode
|
||||||
from .. import util
|
from .. import util
|
||||||
@@ -21,7 +18,7 @@ def main():
|
|||||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
|
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
|
||||||
format='%(asctime)s %(levelname)-10s %(message)s')
|
format='%(asctime)s %(levelname)-10s %(message)s')
|
||||||
stream = open(args.pubkey, 'rb')
|
stream = open(args.pubkey, 'rb')
|
||||||
parser = decode.Parser(util.Reader(stream))
|
parser = decode.parse_packets(util.Reader(stream))
|
||||||
pubkey, userid, sig1, subkey, sig2 = parser
|
pubkey, userid, sig1, subkey, sig2 = parser
|
||||||
|
|
||||||
digest = decode.digest_packets([pubkey, userid, sig1])
|
digest = decode.digest_packets([pubkey, userid, sig1])
|
||||||
|
|||||||
@@ -74,146 +74,146 @@ SUPPORTED_CURVES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Parser(object):
|
def _parse_literal(stream):
|
||||||
"""Parse GPG packets from a given stream."""
|
"""See https://tools.ietf.org/html/rfc4880#section-5.9 for details."""
|
||||||
|
p = {'type': 'literal'}
|
||||||
def __init__(self, stream):
|
p['format'] = stream.readfmt('c')
|
||||||
"""Create an empty parser."""
|
filename_len = stream.readfmt('B')
|
||||||
self.stream = stream
|
p['filename'] = stream.read(filename_len)
|
||||||
self.packet_types = {
|
p['date'] = stream.readfmt('>L')
|
||||||
2: self.signature,
|
p['content'] = stream.read()
|
||||||
6: self.pubkey,
|
p['_to_hash'] = p['content']
|
||||||
11: self.literal,
|
return p
|
||||||
13: self.user_id,
|
|
||||||
14: self.subkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Support iterative parsing of available GPG packets."""
|
|
||||||
return self
|
|
||||||
|
|
||||||
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')
|
|
||||||
p['filename'] = stream.read(filename_len)
|
|
||||||
p['date'] = stream.readfmt('>L')
|
|
||||||
p['content'] = stream.read()
|
|
||||||
p['_to_hash'] = p['content']
|
|
||||||
return p
|
|
||||||
|
|
||||||
def _embedded_signatures(self, subpackets):
|
|
||||||
for packet in subpackets:
|
|
||||||
data = bytearray(packet)
|
|
||||||
if data[0] == 32:
|
|
||||||
# https://tools.ietf.org/html/rfc4880#section-5.2.3.26
|
|
||||||
stream = io.BytesIO(data[1:])
|
|
||||||
yield self.signature(util.Reader(stream))
|
|
||||||
|
|
||||||
|
|
||||||
def signature(self, stream):
|
def _parse_embedded_signatures(subpackets):
|
||||||
"""See https://tools.ietf.org/html/rfc4880#section-5.2 for details."""
|
for packet in subpackets:
|
||||||
p = {'type': 'signature'}
|
data = bytearray(packet)
|
||||||
|
if data[0] == 32:
|
||||||
|
# https://tools.ietf.org/html/rfc4880#section-5.2.3.26
|
||||||
|
stream = io.BytesIO(data[1:])
|
||||||
|
yield _parse_signature(util.Reader(stream))
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# https://tools.ietf.org/html/rfc4880#section-5.2.4
|
def _parse_signature(stream):
|
||||||
tail_to_hash = b'\x04\xff' + struct.pack('>L', to_hash.tell())
|
"""See https://tools.ietf.org/html/rfc4880#section-5.2 for details."""
|
||||||
|
p = {'type': 'signature'}
|
||||||
|
|
||||||
p['_to_hash'] = to_hash.getvalue() + tail_to_hash
|
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)
|
||||||
|
|
||||||
p['unhashed_subpackets'] = parse_subpackets(stream)
|
# https://tools.ietf.org/html/rfc4880#section-5.2.4
|
||||||
embedded = list(self._embedded_signatures(p['unhashed_subpackets']))
|
tail_to_hash = b'\x04\xff' + struct.pack('>L', to_hash.tell())
|
||||||
if embedded:
|
|
||||||
p['embedded'] = embedded
|
|
||||||
|
|
||||||
p['hash_prefix'] = stream.readfmt('2s')
|
p['_to_hash'] = to_hash.getvalue() + tail_to_hash
|
||||||
p['sig'] = (parse_mpi(stream), parse_mpi(stream))
|
|
||||||
|
p['unhashed_subpackets'] = parse_subpackets(stream)
|
||||||
|
embedded = list(_parse_embedded_signatures(p['unhashed_subpackets']))
|
||||||
|
if embedded:
|
||||||
|
p['embedded'] = embedded
|
||||||
|
|
||||||
|
p['hash_prefix'] = stream.readfmt('2s')
|
||||||
|
p['sig'] = (parse_mpi(stream), parse_mpi(stream))
|
||||||
|
assert not stream.read()
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pubkey(stream):
|
||||||
|
"""See https://tools.ietf.org/html/rfc4880#section-5.5 for details."""
|
||||||
|
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 in SUPPORTED_CURVES
|
||||||
|
parser = SUPPORTED_CURVES[oid]
|
||||||
|
|
||||||
|
mpi = parse_mpi(stream)
|
||||||
|
log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length())
|
||||||
|
p['verifier'] = parser(mpi)
|
||||||
assert not stream.read()
|
assert not stream.read()
|
||||||
|
|
||||||
return p
|
# 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:]
|
||||||
|
p['_to_hash'] = data_to_hash
|
||||||
|
log.debug('key ID: %s', util.hexlify(p['key_id']))
|
||||||
|
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):
|
|
||||||
p['version'] = stream.readfmt('B')
|
|
||||||
p['created'] = stream.readfmt('>L')
|
|
||||||
p['algo'] = stream.readfmt('B')
|
|
||||||
|
|
||||||
# https://tools.ietf.org/html/rfc6637#section-11
|
def _parse_subkey(stream):
|
||||||
oid_size = stream.readfmt('B')
|
"""See https://tools.ietf.org/html/rfc4880#section-5.5 for details."""
|
||||||
oid = stream.read(oid_size)
|
p = {'type': 'subkey'}
|
||||||
assert oid in SUPPORTED_CURVES
|
packet = io.BytesIO()
|
||||||
parser = SUPPORTED_CURVES[oid]
|
with stream.capture(packet):
|
||||||
|
p['version'] = stream.readfmt('B')
|
||||||
|
p['created'] = stream.readfmt('>L')
|
||||||
|
p['algo'] = stream.readfmt('B')
|
||||||
|
|
||||||
mpi = parse_mpi(stream)
|
# https://tools.ietf.org/html/rfc6637#section-11
|
||||||
log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length())
|
oid_size = stream.readfmt('B')
|
||||||
p['verifier'] = parser(mpi)
|
oid = stream.read(oid_size)
|
||||||
assert not stream.read()
|
assert oid in SUPPORTED_CURVES
|
||||||
|
parser = SUPPORTED_CURVES[oid]
|
||||||
|
|
||||||
# https://tools.ietf.org/html/rfc4880#section-12.2
|
mpi = parse_mpi(stream)
|
||||||
packet_data = packet.getvalue()
|
log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length())
|
||||||
data_to_hash = (b'\x99' + struct.pack('>H', len(packet_data)) +
|
p['verifier'] = parser(mpi)
|
||||||
packet_data)
|
leftover = stream.read() # TBD: what is this?
|
||||||
p['key_id'] = hashlib.sha1(data_to_hash).digest()[-8:]
|
if leftover:
|
||||||
p['_to_hash'] = data_to_hash
|
log.warning('unexpected subkey leftover: %r', leftover)
|
||||||
log.debug('key ID: %s', util.hexlify(p['key_id']))
|
|
||||||
return p
|
|
||||||
|
|
||||||
def subkey(self, stream):
|
# https://tools.ietf.org/html/rfc4880#section-12.2
|
||||||
"""See https://tools.ietf.org/html/rfc4880#section-5.5 for details."""
|
packet_data = packet.getvalue()
|
||||||
p = {'type': 'subkey'}
|
data_to_hash = (b'\x99' + struct.pack('>H', len(packet_data)) +
|
||||||
packet = io.BytesIO()
|
packet_data)
|
||||||
with stream.capture(packet):
|
p['key_id'] = hashlib.sha1(data_to_hash).digest()[-8:]
|
||||||
p['version'] = stream.readfmt('B')
|
p['_to_hash'] = data_to_hash
|
||||||
p['created'] = stream.readfmt('>L')
|
log.debug('key ID: %s', util.hexlify(p['key_id']))
|
||||||
p['algo'] = stream.readfmt('B')
|
return p
|
||||||
|
|
||||||
# https://tools.ietf.org/html/rfc6637#section-11
|
|
||||||
oid_size = stream.readfmt('B')
|
|
||||||
oid = stream.read(oid_size)
|
|
||||||
assert oid in SUPPORTED_CURVES
|
|
||||||
parser = SUPPORTED_CURVES[oid]
|
|
||||||
|
|
||||||
mpi = parse_mpi(stream)
|
def _parse_user_id(stream):
|
||||||
log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length())
|
"""See https://tools.ietf.org/html/rfc4880#section-5.11 for details."""
|
||||||
p['verifier'] = parser(mpi)
|
value = stream.read()
|
||||||
leftover = stream.read() # TBD: what is this?
|
to_hash = b'\xb4' + util.prefix_len('>L', value)
|
||||||
if leftover:
|
return {'type': 'user_id', 'value': value, '_to_hash': to_hash}
|
||||||
log.warning('unexpected subkey leftover: %r', leftover)
|
|
||||||
|
|
||||||
# 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:]
|
|
||||||
p['_to_hash'] = data_to_hash
|
|
||||||
log.debug('key ID: %s', util.hexlify(p['key_id']))
|
|
||||||
return p
|
|
||||||
|
|
||||||
def user_id(self, stream):
|
PACKET_TYPES = {
|
||||||
"""See https://tools.ietf.org/html/rfc4880#section-5.11 for details."""
|
2: _parse_signature,
|
||||||
value = stream.read()
|
6: _parse_pubkey,
|
||||||
to_hash = b'\xb4' + util.prefix_len('>L', value)
|
11: _parse_literal,
|
||||||
return {'type': 'user_id', 'value': value, '_to_hash': to_hash}
|
13: _parse_user_id,
|
||||||
|
14: _parse_subkey,
|
||||||
|
}
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
"""See https://tools.ietf.org/html/rfc4880#section-4.2 for details."""
|
def parse_packets(stream):
|
||||||
|
"""
|
||||||
|
Support iterative parsing of available GPG packets.
|
||||||
|
|
||||||
|
See https://tools.ietf.org/html/rfc4880#section-4.2 for details.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
value = self.stream.readfmt('B')
|
value = stream.readfmt('B')
|
||||||
except EOFError:
|
except EOFError:
|
||||||
raise StopIteration
|
return
|
||||||
|
|
||||||
log.debug('prefix byte: %02x', value)
|
log.debug('prefix byte: %s', bin(value))
|
||||||
assert util.bit(value, 7) == 1
|
assert util.bit(value, 7) == 1
|
||||||
assert util.bit(value, 6) == 0 # new format not supported yet
|
assert util.bit(value, 6) == 0 # new format not supported yet
|
||||||
|
|
||||||
@@ -222,22 +222,24 @@ class Parser(object):
|
|||||||
tag = tag >> 2
|
tag = tag >> 2
|
||||||
fmt = {0: '>B', 1: '>H', 2: '>L'}[length_type]
|
fmt = {0: '>B', 1: '>H', 2: '>L'}[length_type]
|
||||||
log.debug('length_type: %s', fmt)
|
log.debug('length_type: %s', fmt)
|
||||||
packet_size = self.stream.readfmt(fmt)
|
packet_size = stream.readfmt(fmt)
|
||||||
|
|
||||||
log.debug('packet length: %d', packet_size)
|
log.debug('packet length: %d', packet_size)
|
||||||
packet_data = self.stream.read(packet_size)
|
packet_data = stream.read(packet_size)
|
||||||
packet_type = self.packet_types.get(tag)
|
packet_type = PACKET_TYPES.get(tag)
|
||||||
|
|
||||||
if packet_type:
|
if packet_type:
|
||||||
p = packet_type(util.Reader(io.BytesIO(packet_data)))
|
p = packet_type(util.Reader(io.BytesIO(packet_data)))
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown packet type: {}'.format(tag))
|
raise ValueError('Unknown packet type: {}'.format(tag))
|
||||||
|
|
||||||
p['tag'] = tag
|
p['tag'] = tag
|
||||||
log.debug('packet "%s": %s', p['type'], p)
|
log.debug('packet "%s": %s', p['type'], p)
|
||||||
return p
|
yield p
|
||||||
|
|
||||||
next = __next__
|
|
||||||
|
|
||||||
|
|
||||||
def digest_packets(packets):
|
def digest_packets(packets):
|
||||||
|
"""Compute digest on specified packets, according to '_to_hash' field."""
|
||||||
data_to_hash = io.BytesIO()
|
data_to_hash = io.BytesIO()
|
||||||
for p in packets:
|
for p in packets:
|
||||||
data_to_hash.write(p['_to_hash'])
|
data_to_hash.write(p['_to_hash'])
|
||||||
@@ -246,8 +248,8 @@ def digest_packets(packets):
|
|||||||
|
|
||||||
def load_public_key(stream):
|
def load_public_key(stream):
|
||||||
"""Parse and validate GPG public key from an input stream."""
|
"""Parse and validate GPG public key from an input stream."""
|
||||||
parser = Parser(util.Reader(stream))
|
packets = list(parse_packets(util.Reader(stream)))
|
||||||
pubkey, userid, signature = list(parser)
|
pubkey, userid, signature = packets
|
||||||
digest = digest_packets([pubkey, userid, signature])
|
digest = digest_packets([pubkey, userid, signature])
|
||||||
assert signature['hash_prefix'] == digest[:2]
|
assert signature['hash_prefix'] == digest[:2]
|
||||||
log.debug('loaded public key "%s"', userid['value'])
|
log.debug('loaded public key "%s"', userid['value'])
|
||||||
@@ -257,8 +259,8 @@ def load_public_key(stream):
|
|||||||
|
|
||||||
|
|
||||||
def load_signature(stream, original_data):
|
def load_signature(stream, original_data):
|
||||||
parser = Parser(util.Reader(stream))
|
"""Load signature from stream, and compute GPG digest for verification."""
|
||||||
signature, = parser
|
signature, = list(parse_packets(util.Reader(stream)))
|
||||||
digest = digest_packets([{'_to_hash': original_data}, signature])
|
digest = digest_packets([{'_to_hash': original_data}, signature])
|
||||||
assert signature['hash_prefix'] == digest[:2]
|
assert signature['hash_prefix'] == digest[:2]
|
||||||
return signature, digest
|
return signature, digest
|
||||||
|
|||||||
Reference in New Issue
Block a user