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)
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #4
0
  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)