1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876
|
"""
<Module Name>
common.py
<Author>
Santiago Torres-Arias <santiago@nyu.edu>
<Started>
Nov 15, 2017
<Copyright>
See LICENSE for licensing information.
<Purpose>
Provides algorithm-agnostic gpg public key and signature parsing functions.
The functions select the appropriate functions for each algorithm and
call them.
"""
import binascii
import collections
import logging
import struct
from securesystemslib._gpg import util as gpg_util
from securesystemslib._gpg.constants import (
FULL_KEYID_SUBPACKET,
GPG_HASH_ALGORITHM_STRING,
KEY_EXPIRATION_SUBPACKET,
PACKET_TYPE_PRIMARY_KEY,
PACKET_TYPE_SIGNATURE,
PACKET_TYPE_SUB_KEY,
PACKET_TYPE_USER_ATTR,
PACKET_TYPE_USER_ID,
PARTIAL_KEYID_SUBPACKET,
PRIMARY_USERID_SUBPACKET,
SHA1,
SHA256,
SHA512,
SIG_CREATION_SUBPACKET,
SIGNATURE_TYPE_BINARY,
SIGNATURE_TYPE_CERTIFICATES,
SIGNATURE_TYPE_SUB_KEY_BINDING,
SUPPORTED_PUBKEY_PACKET_VERSIONS,
SUPPORTED_SIGNATURE_PACKET_VERSIONS,
)
from securesystemslib._gpg.exceptions import (
KeyNotFoundError,
PacketParsingError,
PacketVersionNotSupportedError,
SignatureAlgorithmNotSupportedError,
)
from securesystemslib._gpg.handlers import (
SIGNATURE_HANDLERS,
SUPPORTED_SIGNATURE_ALGORITHMS,
)
log = logging.getLogger(__name__)
def parse_pubkey_payload(data):
"""
<Purpose>
Parse the passed public-key packet (payload only) and construct a
public key dictionary.
<Arguments>
data:
An RFC4880 public key packet payload as described in section 5.5.2.
(version 4) of the RFC.
NOTE: The payload can be parsed from a full key packet (header +
payload) by using securesystemslib._gpg.util.parse_packet_header.
WARNING: this doesn't support armored pubkey packets, so use with
care. pubkey packets are a little bit more complicated than the
signature ones
<Exceptions>
ValueError
If the passed public key data is empty.
securesystemslib._gpg.exceptions.PacketVersionNotSupportedError
If the packet version does not match
securesystemslib._gpg.constants.SUPPORTED_PUBKEY_PACKET_VERSIONS
securesystemslib._gpg.exceptions.SignatureAlgorithmNotSupportedError
If the signature algorithm does not match one of
securesystemslib._gpg.constants.SUPPORTED_SIGNATURE_ALGORITHMS
<Side Effects>
None.
<Returns>
A public key dict.
"""
if not data:
raise ValueError("Could not parse empty pubkey payload.")
ptr = 0
keyinfo = {}
version_number = data[ptr]
ptr += 1
if version_number not in SUPPORTED_PUBKEY_PACKET_VERSIONS:
raise PacketVersionNotSupportedError(
f"Pubkey packet version '{version_number}' not supported, "
f"must be one of {SUPPORTED_PUBKEY_PACKET_VERSIONS}"
)
# NOTE: Uncomment this line to decode the time of creation
time_of_creation = struct.unpack(">I", data[ptr : ptr + 4])
ptr += 4
algorithm = data[ptr]
ptr += 1
# TODO: Should we only export keys with signing capabilities?
# Section 5.5.2 of RFC4880 describes a public-key algorithm octet with one
# of the values described in section 9.1 that could be used to determine the
# capabilities. However, in case of RSA subkeys this field doesn't seem to
# correctly encode the capabilities. It always has the value 1, i.e.
# RSA (Encrypt or Sign).
# For RSA public keys we would have to parse the subkey's signature created
# with the master key, for the signature's key flags subpacket, identified
# by the value 27 (see section 5.2.3.1.) containing a list of binary flags
# as described in section 5.2.3.21.
if algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS:
raise SignatureAlgorithmNotSupportedError(
f"Signature algorithm '{algorithm}' not "
"supported, please verify that your gpg configuration is creating "
"either DSA, RSA, or EdDSA signatures (see RFC4880 9.1. Public-Key "
"Algorithms)."
)
keyinfo["type"] = SUPPORTED_SIGNATURE_ALGORITHMS[algorithm]["type"]
keyinfo["method"] = SUPPORTED_SIGNATURE_ALGORITHMS[algorithm]["method"]
handler = SIGNATURE_HANDLERS[keyinfo["type"]]
keyinfo["keyid"] = gpg_util.compute_keyid(data)
key_params = handler.get_pubkey_params(data[ptr:])
return {
"method": keyinfo["method"],
"type": keyinfo["type"],
"hashes": [GPG_HASH_ALGORITHM_STRING],
"creation_time": time_of_creation[0],
"keyid": keyinfo["keyid"],
"keyval": {"private": "", "public": key_params},
}
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 "
f"('{PACKET_TYPE_PRIMARY_KEY}'), got '{packet_type}'."
)
if (
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
if 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:
packets_list = [
PACKET_TYPE_PRIMARY_KEY,
PACKET_TYPE_USER_ID,
PACKET_TYPE_USER_ATTR,
PACKET_TYPE_SUB_KEY,
PACKET_TYPE_SIGNATURE,
]
log.info(
f"Ignoring gpg key packet '{packet_type}', "
"we only handle packets of "
f"types '{packets_list}' (see RFC4880 4.3. Packet Tags)."
)
# Both errors might be raised in parse_packet_header and in this loop
except (PacketParsingError, IndexError) as e:
raise PacketParsingError(
f"Invalid public key data at position {position}: {e}."
)
# Go to next packet
position += packet_length
return key_bundle
def _assign_certified_key_info(bundle):
"""
<Purpose>
Helper function to verify User ID certificates corresponding to a gpg
master key, in order to enrich the master key with additional information
(e.g. expiration dates). The enriched master key is returned.
NOTE: Currently we only consider User ID certificates. We can do the same
for User Attribute certificates by iterating over
bundle[PACKET_TYPE_USER_ATTR] instead of bundle[PACKET_TYPE_USER_ID], and
replacing the signed_content constant '\xb4' with '\xd1' (see RFC4880
section 5.2.4. paragraph 4).
<Arguments>
bundle:
GPG key bundle as parsed in parse_pubkey_bundle().
<Exceptions>
None.
<Side Effects>
None.
<Returns>
A public key dict.
"""
# Create handler shortcut
handler = SIGNATURE_HANDLERS[bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["type"]]
is_primary_user = False
validity_period = None
sig_creation_time = None
# Verify User ID signatures to gather information about primary key
# (see Notes about certification signatures in RFC 4880 5.2.3.3.)
for user_id_packet, packet_data in bundle[PACKET_TYPE_USER_ID].items():
# Construct signed content (see RFC4880 section 5.2.4. paragraph 4)
signed_content = (
bundle[PACKET_TYPE_PRIMARY_KEY]["packet"]
+ b"\xb4\x00\x00\x00"
+ user_id_packet[1:]
)
for signature_packet in packet_data["signatures"]:
try:
signature = parse_signature_packet(
signature_packet,
supported_hash_algorithms={SHA1, SHA256, SHA512},
supported_signature_types=SIGNATURE_TYPE_CERTIFICATES,
include_info=True,
)
# verify_signature requires a "keyid" even if it is short.
# (see parse_signature_packet for more information about keyids)
signature["keyid"] = signature["keyid"] or signature["short_keyid"]
# TODO: Revise exception taxonomy:
# It's okay to ignore some exceptions (unsupported algorithms etc.) but
# we should blow up if a signature is malformed (missing subpackets).
except Exception as e:
log.info(e)
continue
if not bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["keyid"].endswith(
signature["keyid"]
):
log.info(
"Ignoring User ID certificate issued by '{}'.".format(
signature["keyid"]
)
)
continue
is_valid = handler.verify_signature(
signature,
bundle[PACKET_TYPE_PRIMARY_KEY]["key"],
signed_content,
signature["info"]["hash_algorithm"],
)
if not is_valid:
log.info(
"Ignoring invalid User ID self-certificate issued by '{}'.".format(
signature["keyid"]
)
)
continue
# If the signature is valid, we try to extract subpackets relevant to
# the primary key, i.e. expiration time.
# NOTE: There might be multiple User IDs per primary key and multiple
# certificates per User ID. RFC4880 5.2.3.19. and last paragraph of
# 5.2.3.3. provides some suggestions about ambiguity, but delegates the
# responsibility to the implementer.
# Ambiguity resolution scheme:
# We take the key expiration time from the most recent certificate, i.e.
# the certificate with the highest signature creation time. Additionally,
# we prioritize certificates with primary user id flag set True. Note
# that, if the ultimately prioritized certificate does not have a key
# expiration time subpacket, we don't assign one, even if there were
# certificates of lower priority carrying that subpacket.
tmp_validity_period = signature["info"]["subpackets"].get(
KEY_EXPIRATION_SUBPACKET
)
# No key expiration time, go to next certificate
if tmp_validity_period is None:
continue
# Create shortcut to mandatory pre-parsed creation time subpacket
tmp_sig_creation_time = signature["info"]["creation_time"]
tmp_is_primary_user = signature["info"]["subpackets"].get(
PRIMARY_USERID_SUBPACKET
)
if tmp_is_primary_user is not None:
tmp_is_primary_user = bool(tmp_is_primary_user[0])
# If we already have a primary user certified expiration date and this
# is none, we don't consider it, and go to next certificate
if is_primary_user and not tmp_is_primary_user:
continue
if not sig_creation_time or sig_creation_time < tmp_sig_creation_time:
# This is the most recent certificate that has a validity_period and
# doesn't have lower priority in regard to the primary user id flag. We
# accept it the keys validty_period, until we get a newer value from
# a certificate with higher priority.
validity_period = struct.unpack(">I", tmp_validity_period)[0]
# We also keep track of the used certificate's primary user id flag and
# the signature creation time, for prioritization.
is_primary_user = tmp_is_primary_user
sig_creation_time = tmp_sig_creation_time
if validity_period is not None:
bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["validity_period"] = validity_period
return bundle[PACKET_TYPE_PRIMARY_KEY]["key"]
def _get_verified_subkeys(bundle):
"""
<Purpose>
Helper function to verify the subkey binding signature for all subkeys in
the passed bundle in order to enrich subkeys with additional information
(e.g. expiration dates). Only valid (i.e. parsable) subkeys that are
verifiably bound to the the master key of the bundle are returned. All
other subkeys are discarded.
<Arguments>
bundle:
GPG key bundle as parsed in parse_pubkey_bundle().
<Exceptions>
None.
<Side Effects>
None.
<Returns>
A dict of public keys dicts with keyids as dict keys.
"""
# Create handler shortcut
handler = SIGNATURE_HANDLERS[bundle[PACKET_TYPE_PRIMARY_KEY]["key"]["type"]]
# Verify subkey binding signatures and only keep verified keys
# See notes about subkey binding signature in RFC4880 5.2.3.3
verified_subkeys = {}
for subkey_packet, packet_data in bundle[PACKET_TYPE_SUB_KEY].items():
try:
# Parse subkey if possible and skip if invalid (e.g. not-supported)
subkey = parse_pubkey_payload(
bytearray(subkey_packet[-packet_data["body_len"] :])
)
# TODO: Revise exception taxonomy
except Exception as e:
log.info(e)
continue
# Construct signed content (see RFC4880 section 5.2.4. paragraph 3)
signed_content = (
bundle[PACKET_TYPE_PRIMARY_KEY]["packet"] + b"\x99" + subkey_packet[1:]
)
# Filter sub key binding signature from other signatures, e.g. subkey
# binding revocation signatures
key_binding_signatures = []
for signature_packet in packet_data["signatures"]:
try:
signature = parse_signature_packet(
signature_packet,
supported_hash_algorithms={SHA1, SHA256, SHA512},
supported_signature_types={SIGNATURE_TYPE_SUB_KEY_BINDING},
include_info=True,
)
# verify_signature requires a "keyid" even if it is short.
# (see parse_signature_packet for more information about keyids)
signature["keyid"] = signature["keyid"] or signature["short_keyid"]
key_binding_signatures.append(signature)
# TODO: Revise exception taxonomy
except Exception as e:
log.info(e)
continue
# NOTE: As per the V4 key structure diagram in RFC4880 section 12.1., a
# subkey must be followed by exactly one Primary-Key-Binding-Signature.
# Based on inspection of real-world keys and other parts of the RFC (e.g.
# the paragraph below the diagram and paragraph 0x18: Subkey Binding
# Signature in section 5.2.1.) the mandated signature is actually a
# *subkey binding signature*, which in case of a signing subkey, must have
# an *embedded primary key binding signature*.
if len(key_binding_signatures) != 1:
log.info(
"Ignoring subkey '{}' due to wrong amount of key binding "
"signatures ({}), must be exactly 1.".format(
subkey["keyid"], len(key_binding_signatures)
)
)
continue
is_valid = handler.verify_signature(
signature,
bundle[PACKET_TYPE_PRIMARY_KEY]["key"],
signed_content,
signature["info"]["hash_algorithm"],
)
if not is_valid:
log.info(
"Ignoring subkey '{}' due to invalid key binding signature.".format(
subkey["keyid"]
)
)
continue
# If the signature is valid, we may also extract relevant information from
# its "info" field (e.g. subkey expiration date) and assign to it to the
# subkey here
validity_period = signature["info"]["subpackets"].get(KEY_EXPIRATION_SUBPACKET)
if validity_period is not None:
subkey["validity_period"] = struct.unpack(">I", validity_period)[0]
verified_subkeys[subkey["keyid"]] = subkey
return verified_subkeys
def get_pubkey_bundle(data, keyid):
"""
<Purpose>
Call function to extract and verify master key and subkeys from the passed
gpg key data, where either the master key or one of the subkeys matches the
passed keyid.
NOTE:
- If the keyid matches one of the subkeys, a warning is issued to notify
the user about potential privilege escalation
- Subkeys with invalid key binding signatures are discarded
<Arguments>
data:
Public key data as written to stdout by
securesystemslib._gpg.constants.gpg_export_pubkey_command.
keyid:
The keyid of the master key or one of its subkeys expected to be
contained in the passed gpg data.
<Exceptions>
securesystemslib._gpg.exceptions.PacketParsingError
If the key data could not be parsed
securesystemslib._gpg.exceptions.KeyNotFoundError
If the passed data is empty.
If no master key or subkeys could be found that matches the passed
keyid.
<Side Effects>
None.
<Returns>
A public key dict with optional subkeys.
"""
if not data:
raise KeyNotFoundError(
f"Could not find gpg key '{keyid}' in empty exported key data."
)
# Parse out master key and subkeys (enriched and verified via certificates
# and binding signatures)
raw_key_bundle = parse_pubkey_bundle(data)
master_public_key = _assign_certified_key_info(raw_key_bundle)
sub_public_keys = _get_verified_subkeys(raw_key_bundle)
# Since GPG returns all pubkeys associated with a keyid (master key and
# subkeys) we check which key matches the passed keyid.
# If the matching key is a subkey, we warn the user because we return
# the whole bundle (master plus all subkeys) and not only the subkey.
# If no matching key is found we raise a KeyNotFoundError.
for idx, public_key in enumerate(
[master_public_key] + list(sub_public_keys.values())
):
if public_key and public_key["keyid"].endswith(keyid.lower()):
if idx > 1:
log.debug(
"Exporting master key '{}' including subkeys '{}' for"
" passed keyid '{}'.".format(
master_public_key["keyid"],
", ".join(list(sub_public_keys.keys())),
keyid,
)
)
break
else:
raise KeyNotFoundError(
f"Could not find gpg key '{keyid}' in exported key data."
)
# Add subkeys dictionary to master pubkey "subkeys" field if subkeys exist
if sub_public_keys:
master_public_key["subkeys"] = sub_public_keys
return master_public_key
# ruff: noqa: PLR0912, PLR0915
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 dict 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(
f"Signature version '{version_number}' not supported, "
f"must be one of {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(
f"Signature type '{signature_type}' not supported, "
f"must be one of {supported_signature_types} "
"(see RFC4880 5.2.1. Signature Types)."
)
signature_algorithm = data[ptr]
ptr += 1
if signature_algorithm not in SUPPORTED_SIGNATURE_ALGORITHMS:
raise ValueError(
f"Signature algorithm '{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)."
)
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(
f"Hash algorithm '{hash_algorithm}' not supported, "
f"must be one of {supported_hash_algorithms} "
"(see RFC4880 9.4. 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)
if len(hashed_subpackets) != hashed_octet_count: # pragma: no cover
raise ValueError(
"Signature packet contains an unexpected amount of 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 "
f"'{short_keyid}' of the 'Issuer' subpacket must match the "
f"lower 64 bits of the fingerprint '{keyid}' of the 'Issuer "
"Fingerprint' subpacket (see RFC4880 and rfc4880bis-06 5.2.3.28. "
"Issuer Fingerprint)."
)
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": 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
|