def test_parse_pubkey_bundle(self): """Assert presence of packets expected returned from `parse_pubkey_bundle` for specific test key). See ``` gpg --homedir tests/gpg_keyrings/rsa/ --export 9EA70BD13D883381 | \ gpg --list-packets ``` """ # Expect parsed primary key matching GPG_PUBKEY_SCHEMA self.assertTrue( GPG_PUBKEY_SCHEMA.matches( self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"])) # Parse corresponding raw packet for comparison _, header_len, _, _ = parse_packet_header( self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"]) # pylint: disable=unsubscriptable-object parsed_raw_packet = parse_pubkey_payload( bytearray(self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] [header_len:])) # And compare self.assertDictEqual( self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"], parsed_raw_packet) # Expect one primary key signature (revocation signature) self.assertEqual( len(self.raw_key_bundle[PACKET_TYPE_PRIMARY_KEY]["signatures"]), 1) # Expect one User ID packet, one User Attribute packet and one Subkey, # each with correct data for _type in [ PACKET_TYPE_USER_ID, PACKET_TYPE_USER_ATTR, PACKET_TYPE_SUB_KEY ]: # Of each type there is only one packet self.assertTrue(len(self.raw_key_bundle[_type]) == 1) # The raw packet is stored as key in the per-packet type collection raw_packet = next(iter(self.raw_key_bundle[_type])) # Its values are the raw packets header and body length self.assertEqual( len(raw_packet), self.raw_key_bundle[_type][raw_packet]["header_len"] + self.raw_key_bundle[_type][raw_packet]["body_len"]) # and one self-signature self.assertEqual( len(self.raw_key_bundle[_type][raw_packet]["signatures"]), 1)
def parse_signature_packet(data, supported_signature_types=None, supported_hash_algorithms=None, include_info=False): """ <Purpose> Parse the signature information on an RFC4880-encoded binary signature data buffer. NOTE: Older gpg versions (< FULLY_SUPPORTED_MIN_VERSION) might only reveal the partial key id. It is the callers responsibility to determine the full keyid based on the partial keyid, e.g. by exporting the related public and replacing the partial keyid with the full keyid. <Arguments> data: the RFC4880-encoded binary signature data buffer as described in section 5.2 (and 5.2.3.1). supported_signature_types: (optional) a set of supported signature_types, the signature packet may be (see securesystemslib.gpg.constants for available types). If None is specified the signature packet must be of type SIGNATURE_TYPE_BINARY. supported_hash_algorithms: (optional) a set of supported hash algorithm ids, the signature packet may use. Available ids are SHA1, SHA256, SHA512 (see securesystemslib.gpg.constants). If None is specified, the signature packet must use SHA256. include_info: (optional) a boolean that indicates whether an opaque dictionary should be added to the returned signature under the key "info". Default is False. <Exceptions> ValueError: if the signature packet is not supported or the data is malformed IndexError: if the signature packet is incomplete <Side Effects> None. <Returns> A signature dictionary matching securesystemslib.formats.GPG_SIGNATURE_SCHEMA with the following special characteristics: - The "keyid" field is an empty string if it cannot be determined - The "short_keyid" is not added if it cannot be determined - At least one of non-empty "keyid" or "short_keyid" are part of the signature """ if not supported_signature_types: supported_signature_types = {SIGNATURE_TYPE_BINARY} if not supported_hash_algorithms: supported_hash_algorithms = {SHA256} _, header_len, _, packet_len = gpg_util.parse_packet_header( data, PACKET_TYPE_SIGNATURE) data = bytearray(data[header_len:packet_len]) ptr = 0 # we get the version number, which we also expect to be v4, or we bail # FIXME: support v3 type signatures (which I haven't seen in the wild) version_number = data[ptr] ptr += 1 if version_number not in SUPPORTED_SIGNATURE_PACKET_VERSIONS: raise ValueError( "Signature version '{}' not supported, must be one of " "{}.".format(version_number, SUPPORTED_SIGNATURE_PACKET_VERSIONS)) # Per default we only parse "signatures of a binary document". Other types # may be allowed by passing type constants via `supported_signature_types`. # Types include revocation signatures, key binding signatures, persona # certifications, etc. (see RFC 4880 section 5.2.1.). signature_type = data[ptr] ptr += 1 if signature_type not in supported_signature_types: raise ValueError( "Signature type '{}' not supported, must be one of {} " "(see RFC4880 5.2.1. Signature Types).".format( signature_type, supported_signature_types)) signature_algorithm = data[ptr] ptr += 1 if signature_algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS: raise ValueError( "Signature algorithm '{}' not " "supported, please verify that your gpg configuration is creating " "either DSA, RSA, or EdDSA signatures (see RFC4880 9.1. Public-Key " "Algorithms).".format(signature_algorithm)) key_type = SUPPORTED_SIGNATURE_ALGORITHMS[signature_algorithm]['type'] handler = SIGNATURE_HANDLERS[key_type] hash_algorithm = data[ptr] ptr += 1 if hash_algorithm not in supported_hash_algorithms: raise ValueError("Hash algorithm '{}' not supported, must be one of {}" " (see RFC4880 9.4. Hash Algorithms).".format( hash_algorithm, supported_hash_algorithms)) # Obtain the hashed octets hashed_octet_count = struct.unpack(">H", data[ptr:ptr + 2])[0] ptr += 2 hashed_subpackets = data[ptr:ptr + hashed_octet_count] hashed_subpacket_info = gpg_util.parse_subpackets(hashed_subpackets) # Check whether we were actually able to read this much hashed octets if len(hashed_subpackets) != hashed_octet_count: # pragma: no cover raise ValueError("This signature packet seems to be corrupted." "It is missing hashed octets!") ptr += hashed_octet_count other_headers_ptr = ptr unhashed_octet_count = struct.unpack(">H", data[ptr:ptr + 2])[0] ptr += 2 unhashed_subpackets = data[ptr:ptr + unhashed_octet_count] unhashed_subpacket_info = gpg_util.parse_subpackets(unhashed_subpackets) ptr += unhashed_octet_count # Use the info dict to return further signature information that may be # needed for intermediate processing, but does not have to be on the eventual # signature datastructure info = { "signature_type": signature_type, "hash_algorithm": hash_algorithm, "creation_time": None, "subpackets": {}, } keyid = "" short_keyid = "" # Parse "Issuer" (short keyid) and "Issuer Fingerprint" (full keyid) type # subpackets # Strategy: Loop over all unhashed and hashed subpackets (in that order!) and # store only the last of a type. Due to the order in the loop, hashed # subpackets are prioritized over unhashed subpackets (see NOTEs below). # NOTE: A subpacket may be found either in the hashed or unhashed subpacket # sections of a signature. If a subpacket is not hashed, then the information # in it cannot be considered definitive because it is not part of the # signature proper. (see RFC4880 5.2.3.2.) # NOTE: Signatures may contain conflicting information in subpackets. In most # cases, an implementation SHOULD use the last subpacket, but MAY use any # conflict resolution scheme that makes more sense. (see RFC4880 5.2.4.1.) for idx, subpacket_tuple in \ enumerate(unhashed_subpacket_info + hashed_subpacket_info): # The idx indicates if the info is from the unhashed (first) or # hashed (second) of the above concatenated lists is_hashed = (idx >= len(unhashed_subpacket_info)) subpacket_type, subpacket_data = subpacket_tuple # Warn if expiration subpacket is not hashed if subpacket_type == KEY_EXPIRATION_SUBPACKET: if not is_hashed: log.warning( "Expiration subpacket not hashed, gpg client possibly " "exporting a weakly configured key.") # Full keyids are only available in newer signatures # (see RFC4880 and rfc4880bis-06 5.2.3.1.) if subpacket_type == FULL_KEYID_SUBPACKET: # pragma: no cover # Exclude from coverage for consistent results across test envs # NOTE: The first byte of the subpacket payload is a version number # (see rfc4880bis-06 5.2.3.28.) keyid = binascii.hexlify(subpacket_data[1:]).decode("ascii") # We also return the short keyid, because the full might not be available if subpacket_type == PARTIAL_KEYID_SUBPACKET: short_keyid = binascii.hexlify(subpacket_data).decode("ascii") if subpacket_type == SIG_CREATION_SUBPACKET: info["creation_time"] = struct.unpack(">I", subpacket_data)[0] info["subpackets"][subpacket_type] = subpacket_data # Fail if there is no keyid at all (this should not happen) if not (keyid or short_keyid): # pragma: no cover raise ValueError( "This signature packet seems to be corrupted. It does " "not have an 'Issuer' or 'Issuer Fingerprint' subpacket (see RFC4880 " "and rfc4880bis-06 5.2.3.1. Signature Subpacket Specification).") # Fail if keyid and short keyid are specified but don't match if keyid and not keyid.endswith(short_keyid): # pragma: no cover raise ValueError( "This signature packet seems to be corrupted. The key ID " "'{}' of the 'Issuer' subpacket must match the lower 64 bits of the " "fingerprint '{}' of the 'Issuer Fingerprint' subpacket (see RFC4880 " "and rfc4880bis-06 5.2.3.28. Issuer Fingerprint).".format( short_keyid, keyid)) if not info["creation_time"]: # pragma: no cover raise ValueError( "This signature packet seems to be corrupted. It does " "not have a 'Signature Creation Time' subpacket (see RFC4880 5.2.3.4 " "Signature Creation Time).") # Uncomment this variable to obtain the left-hash-bits information (used for # early rejection) #left_hash_bits = struct.unpack(">H", data[ptr:ptr+2])[0] ptr += 2 # Finally, fetch the actual signature (as opposed to signature metadata). signature = handler.get_signature_params(data[ptr:]) signature_data = { 'keyid': "{}".format(keyid), 'other_headers': binascii.hexlify(data[:other_headers_ptr]).decode('ascii'), 'signature': binascii.hexlify(signature).decode('ascii') } if short_keyid: # pragma: no branch signature_data["short_keyid"] = short_keyid if include_info: signature_data["info"] = info return signature_data
def parse_pubkey_bundle(data): """ <Purpose> Parse packets from passed gpg public key data, associating self-signatures with the packets they correspond to, based on the structure of V4 keys defined in RFC4880 12.1 Key Structures. The returned raw key bundle may be used to further enrich the master key, with certified information (e.g. key expiration date) taken from self-signatures, and/or to verify that the parsed subkeys are bound to the primary key via signatures. <Arguments> data: Public key data as written to stdout by GPG_EXPORT_PUBKEY_COMMAND. <Exceptions> securesystemslib.gpg.exceptions.PacketParsingError If data is empty. If data cannot be parsed. <Side Effects> None. <Returns> A raw public key bundle where self-signatures are associated with their corresponding packets. See `key_bundle` for details. """ if not data: raise PacketParsingError("Cannot parse keys from empty gpg data.") # Temporary data structure to hold parsed gpg packets key_bundle = { PACKET_TYPE_PRIMARY_KEY: { "key": {}, "packet": None, "signatures": [] }, PACKET_TYPE_USER_ID: collections.OrderedDict(), PACKET_TYPE_USER_ATTR: collections.OrderedDict(), PACKET_TYPE_SUB_KEY: collections.OrderedDict() } # Iterate over gpg data and parse out packets of different types position = 0 while position < len(data): try: packet_type, header_len, body_len, packet_length = \ gpg_util.parse_packet_header(data[position:]) packet = data[position:position + packet_length] payload = packet[header_len:] # The first (and only the first) packet in the bundle must be the master # key. See RFC4880 12.1 Key Structures, V4 version keys # TODO: Do we need additional key structure assertions? e.g. # - there must be least one User ID packet, or # - order and type of signatures, or # - disallow duplicate packets if packet_type != PACKET_TYPE_PRIMARY_KEY and \ not key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"]: raise PacketParsingError( "First packet must be a primary key ('{}'), " "got '{}'.".format(PACKET_TYPE_PRIMARY_KEY, packet_type)) elif packet_type == PACKET_TYPE_PRIMARY_KEY and \ key_bundle[PACKET_TYPE_PRIMARY_KEY]["key"]: raise PacketParsingError("Unexpected primary key.") # Fully parse master key to fail early, e.g. if key is malformed # or not supported, but also retain original packet for subkey binding # signature verification elif packet_type == PACKET_TYPE_PRIMARY_KEY: key_bundle[PACKET_TYPE_PRIMARY_KEY] = { "key": parse_pubkey_payload(bytearray(payload)), "packet": packet, "signatures": [] } # Other non-signature packets in the key bundle include User IDs and User # Attributes, required to verify primary key certificates, and subkey # packets. For each packet we create a new ordered dictionary entry. We # use a dictionary to aggregate signatures by packet below, # and it must be ordered because each signature packet belongs to the # most recently parsed packet of a type. elif packet_type in { PACKET_TYPE_USER_ID, PACKET_TYPE_USER_ATTR, PACKET_TYPE_SUB_KEY }: key_bundle[packet_type][packet] = { "header_len": header_len, "body_len": body_len, "signatures": [] } # The remaining relevant packets are signatures, required to bind subkeys # to the primary key, or to gather additional information about the # primary key, e.g. expiration date. # A signature corresponds to the most recently parsed packet of a type, # where the type is given by the availability of respective packets. # We test availability and assign accordingly as per the order of packet # types defined in RFC4880 12.1 (bottom-up). elif packet_type == PACKET_TYPE_SIGNATURE: for _type in [ PACKET_TYPE_SUB_KEY, PACKET_TYPE_USER_ATTR, PACKET_TYPE_USER_ID ]: if key_bundle[_type]: # Add to most recently added packet's signatures of matching type key_bundle[_type][next(reversed(key_bundle[_type]))]\ ["signatures"].append(packet) break else: # If no packets are available for any of above types (yet), the # signature belongs to the primary key key_bundle[PACKET_TYPE_PRIMARY_KEY]["signatures"].append( packet) else: log.info( "Ignoring gpg key packet '{}', we only handle packets of " "types '{}' (see RFC4880 4.3. Packet Tags).".format( packet_type, [ PACKET_TYPE_PRIMARY_KEY, PACKET_TYPE_USER_ID, PACKET_TYPE_USER_ATTR, PACKET_TYPE_SUB_KEY, PACKET_TYPE_SIGNATURE ])) # Both errors might be raised in parse_packet_header and in this loop except (PacketParsingError, IndexError) as e: raise PacketParsingError( "Invalid public key data at position {}: {}.".format( position, e)) # Go to next packet position += packet_length return key_bundle
def test_parse_packet_header(self): """Test parse_packet_header with manually crafted data. """ data_list = [ ## New format packet length with mock packet type 100001 # one-octet length, header len: 2, body len: 0 to 191 [0b01100001, 0], [0b01100001, 191], # two-octet length, header len: 3, body len: 192 to 8383 [0b01100001, 192, 0], [0b01100001, 223, 255], # five-octet length, header len: 6, body len: 0 to 4,294,967,295 [0b01100001, 255, 0, 0, 0, 0], [0b01100001, 255, 255, 255, 255, 255], ## Old format packet lengths with mock packet type 1001 # one-octet length, header len: 2, body len: 0 to 255 [0b00100100, 0], [0b00100100, 255], # two-octet length, header len: 3, body len: 0 to 65,535 [0b00100101, 0, 0], [0b00100101, 255, 255], # four-octet length, header len: 5, body len: 0 to 4,294,967,295 [0b00100110, 0, 0, 0, 0, 0], [0b00100110, 255, 255, 255, 255, 255], ] # packet_type | header_len | body_len | packet_len expected = [ (33, 2, 0, 2), (33, 2, 191, 193), (33, 3, 192, 195), (33, 3, 8383, 8386), (33, 6, 0, 6), (33, 6, 4294967295, 4294967301), (9, 2, 0, 2), (9, 2, 255, 257), (9, 3, 0, 3), (9, 3, 65535, 65538), (9, 5, 0, 5), (9, 5, 4294967295, 4294967300), ] for idx, data in enumerate(data_list): result = parse_packet_header(bytearray(data)) self.assertEqual(result, expected[idx]) # New Format Packet Lengths with Partial Body Lengths range for second_octet in [224, 254]: with self.assertRaises(PacketParsingError): parse_packet_header(bytearray([0b01100001, second_octet])) # Old Format Packet Lengths with indeterminate length (length type 3) with self.assertRaises(PacketParsingError): parse_packet_header(bytearray([0b00100111])) # Get expected type parse_packet_header(bytearray([0b01100001, 0]), expected_type=33) # Raise with unexpected type with self.assertRaises(PacketParsingError): parse_packet_header(bytearray([0b01100001, 0]), expected_type=34)