diff --git a/docs/authenticode.rst b/docs/authenticode.rst index 082bc81..a868bd8 100644 --- a/docs/authenticode.rst +++ b/docs/authenticode.rst @@ -27,15 +27,145 @@ If you need to get more information about the signature, you can use this:: if signed_data.signer_info.countersigner is not None: print(signed_data.signer_info.countersigner.signing_time) -A more thorough example is available in the examples directory of the Signify repository. - -Note that the file must remain open as long as not all SignedData objects have been parsed. +A more thorough example is available in the examples directory of the Signify +repository. + +Note that the file must remain open as long as not all SignedData objects have been +parsed or verified. + +Authenticode overview +--------------------- +Most of the specification of Authenticode is properly documented in a 2008 paper +`Windows Authenticode Portable Executable Signature Format `_ +and still available to download. The specification mostly follows the :doc:`pkcs7` +specification, although most structures have since been updated in more recent RFCs. Of +particular note is that the specification defines various "must" and "must not" +phrases, which has not been adhered to in more recent uses. + +At its core, it defines how the certificate table of a PE file (a normal Windows +executable) contains PKCS#7 SignedData objects. Note that the specification allows for +multiple of such objects, perhaps including other signers or signatures. + +Authenticode SignedData objects contain ``SpcIndirectDataContent`` contents +(microsoft_spc_indirect_data_content, OID 1.3.6.1.4.1.311.2.1.4), which +(amongst others) define the hash of the PE file. The +PE file is hashed particularly, as we need to skip the PE file checksum, the +pointer to the certificate table in the data directory, and the certificate table +itself. + +The signature is valid, in principle, if the hash we calculate is the same as in +``SpcIndirectDataContent``, and the ``SignerInfo`` contains a hash over this content. + +.. seealso:: + + There are various other projects that also deal with Authenticode, which also + provide useful insights. These include: + + * `μthenticode `_ + * `LIEF `_ + * `osslsigncode `_ + * `winsign `_ + * `AuthenticodeLint `_ + * `jsign `_ + + Other useful references include: + + * `Windows Authenticode Portable Executable Signature Format `_ + * `Caveats for Authenticode Signing `_ + +Additional gotcha's +~~~~~~~~~~~~~~~~~~~ +There are a few additional gotcha's when verifying Authenticode signatures, which are +not very well defined in the original specification, but we have been able to +reverse-engineer or otherwise use to our advantage. + +RFC3161 countersignatures +######################### +There are two types of countersignature: a regular countersignature, as used in PKCS#7, +or a nested Time-Stamp Protocol response (RFC3161). This response, available as +unauthenticated attribute with microsoft_time_stamp_token (OID 1.3.6.1.4.1.311.3.3.1), +is added as nested :class:`authenticode.pkcs7.SignedData` object. + +This is transparently handled by the :attr:`AuthenticodeSignedData.countersigner` +attribute, but note that this attribute can return two different types. + +Nested signatures +################# +Instead of adding multiple signatures to the certificate table, SignedData objects +can also be nested in others as unauthenticated attributes with +microsoft_nested_signature (OID 1.3.6.1.4.1.311.2.4.1). + +This is transparently handled by the :class:`SignedPEFile` class. + +Page hashes +########### +The ``SpcIndirectDataContent`` class may contain a binary structure that defines +hashes for portions of the file (in the ``SpcLink.moniker`` field). If this is the case, +the moniker will use class ID ``a6b586d5-b4a1-2466-ae05-a217da8e60d6``, and its +serialized data will contain another ``SpcAttributeTypeAndOptionalValue`` with OIDs +microsoft_spc_pe_image_page_hashes_v1 (1.3.6.1.4.1.311.2.3.1) for SHA-1 or +microsoft_spc_pe_image_page_hashes_v2 (1.3.6.1.4.1.311.2.3.2) for SHA-256. + +The value will be a binary structure that describes offsets (4 bytes integer) and +hash digest (digest length of the algorithm) of parts of the binary. These offsets +appear to be relative to the entire file, and the final offset is always at the end +of the file (describing the end of the previous hash), and the final hash is ignored:: + + 0000000 08d88d96cb3fddf7a7c73598e95388ce60432c2c5ff17b8c558ce599645db73e + 0001024 5ebe1d0255524e4291105759b80abad8294e269e3e11fce76ed6b2e005a79df0 + 0005120 255d7a5768ac44963184e0b5281d64fd9282f953211d03fd49a3d8190044dc35 + ... + 1436160 35c36ac4c657e82cc3aa1311373c1b17552780f64e000a2c31742125365145cd + 1438720 0000000000000000000000000000000000000000000000000000000000000000 + +Each hash is then calculated between the two defined offsets, using the same omissions +as for normal Authenticode validation. The hashes are filled with NULL bytes when the +hash would be shorter than the page size (typically 4096), ignoring omissions. + +In the example above, for the first hash, we would calculate the hash over the first +1024 bytes of the PE file, skipping the checksum and table locations located in the +PE header file, and then add 3072 NULL bytes to complete a full PE page. Note that the +actual digest is calculated over less than 4096 bytes due to the omissions. + +Additional attributes, extensions +################################# +Some attributes are present on SignerInfo objects that have additional meanings: + +microsoft_spc_sp_opus_info (1.3.6.1.4.1.311.2.1.12) + Contains the program name and URL +microsoft_spc_statement_type (1.3.6.1.4.1.311.2.1.11) + Defines that the key purpose is individual (1.3.6.1.4.1.311.2.1.21) or + commercial (1.3.6.1.4.1.311.2.1.22), but unused in practice. +microsoft_spc_relaxed_pe_marker_check (1.3.6.1.4.1.311.2.6.1) + Purpose unknown +microsoft_platform_manifest_binary_id (1.3.6.1.4.1.311.10.3.28) + Purpose unknown + +For certificates, these extensions are known: + +microsoft_spc_sp_agency_info (1.3.6.1.4.1.311.2.1.10) + Purpose unknown +microsoft_spc_financial_criteria (1.3.6.1.4.1.311.2.1.27) + Purpose unknown + +The following key purpose is relevant for Authenticode: + +microsoft_lifetime_signing (1.3.6.1.4.1.311.10.3.13) + The certificate is only valid for it's lifetime, and cannot be extend with a counter + signature. + +All these attributes and extensions are defined in the ASN.1 spec of this library, +but not all of them are used. + +Future work: + +* 1.3.6.1.4.1.311.2.5.1 (enhanced_hash) Signed PE File -------------- -A regular PE file will contain zero or one :class:`AuthenticodeSignedData` objects. The :class:`SignedPEFile` class -contains helpers to ensure the correct objects can be extracted, and additionally, allows for validating the PE -signatures. +A regular PE file will contain zero or one :class:`AuthenticodeSignedData` objects. +The :class:`SignedPEFile` class contains helpers to ensure the correct objects can be +extracted, and additionally, allows for validating the PE signatures. .. autoclass:: SignedPEFile :members: @@ -45,7 +175,8 @@ signatures. PKCS7 objects ------------- -To help understand the specific SignedData and SignerInfo objects, the following graph may help: +To help understand the specific SignedData and SignerInfo objects, the following graph +may help: .. image:: http://yuml.me/f68f2b83.svg @@ -60,13 +191,14 @@ To help understand the specific SignedData and SignerInfo objects, the following Countersignature ---------------- -The countersignature is used to verify the timestamp of the signature. This is usually done by sending the signature -to a time-stamping service, that provides the countersignature. This allows the signature to continue to be valid, even +The countersignature is used to verify the timestamp of the signature. This is usually +done by sending the signature to a time-stamping service, that provides the +countersignature. This allows the signature to continue to be valid, even after the original certificate chain expiring. -There are two types of countersignature: a regular countersignature, as used in PKCS7, or a nested RFC3161 response. -This nested object is basically a :class:`authenticode.pkcs7.SignedData` object, which holds its own set of -certificates. +There are two types of countersignature: a regular countersignature, as used in PKCS7, +or a nested RFC3161 response. This nested object is basically a +:class:`authenticode.pkcs7.SignedData` object, which holds its own set of certificates. Regular ~~~~~~~ diff --git a/docs/changelog.rst b/docs/changelog.rst index 8bb42d0..4b97a5c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,9 +4,12 @@ This page contains the most significant changes in Signify between each release. v0.8.0 (unreleased) ------------------- +* Add support for page hashes contained within the ``SpcPeImageData`` structure. + +* Renamed ``SpcInfo`` to ``IndirectData``, and split off ``PeImageData`` into a + separate class. * Add support (don't crash) for the ``microsoft_spc_siginfo`` OID in the - ``SpcIndirectDataContent`` structure, used in signing MSI files. We may improve - support for the attributes in this structure in a future release. Note that MSI files + ``SpcIndirectDataContent`` structure, used in signing MSI files. Note that MSI files are not (yet) supported. * Add support for ``SpcRelaxedPeMarkerCheck`` and ``PlatformManifestBinaryID`` as SignerInfo attributes, although their exact purpose is currently unknown. diff --git a/docs/pkcs7.rst b/docs/pkcs7.rst index e8db160..1995355 100644 --- a/docs/pkcs7.rst +++ b/docs/pkcs7.rst @@ -1,28 +1,29 @@ -================================ -PKCS7: SignedData and SignerInfo -================================ +================================= +PKCS#7: SignedData and SignerInfo +================================= .. module:: signify.pkcs7 -To support Authenticode, this library includes some code to parse and validate SignedData structures. These are defined -in several RFC's, most notably RFC2315 (which is the version Authenticode uses). The structure of all relevant RFC's -follow ASN.1 notation to define the relevant structures. These definitions are not always easily digested, but it does -show which fields are available. +To support Authenticode, this library includes some code to parse and validate +SignedData structures. These are defined in several RFCs, which used to be called PKCS#7 +The structure of all relevant RFC's follow ASN.1 notation to define the relevant +structures. These definitions are not always easily digested, but it does show which +fields are available. -This chapter of the documentation shows how these basic structures work, so we can dive deep into their operations -in the next chapter. +This chapter of the documentation shows how these basic structures work, so we can dive +deep into their operations in the next chapter. The following diagram shows the relation between SignedData and SignerInfo: .. image:: https://yuml.me/8e9c7bb6.svg -Note that although this diagram is not very complicated, when discussing Authenticode, we will be creating multiple -SignedData and SignerInfo structures, nested in each other, so it's important to fully understand this structure. +Note that although this diagram is not very complicated, when discussing Authenticode, +we will be creating multiple SignedData and SignerInfo structures, nested in each other, +so it's important to fully understand this structure. SignedData ========== The SignedData object is the root structure for sending encrypted data in PKCS#7. - .. autoclass:: SignedData :members: diff --git a/examples/authenticode_info.py b/examples/authenticode_info.py index fb2f852..baf6630 100644 --- a/examples/authenticode_info.py +++ b/examples/authenticode_info.py @@ -1,68 +1,205 @@ +from __future__ import annotations + import logging +import re +import pathlib import sys +import textwrap +from typing import Any + +from signify.authenticode import ( + SignedPEFile, + AuthenticodeSignedData, + AuthenticodeSignerInfo, + RFC3161SignerInfo, + RFC3161SignedData, +) +from signify.pkcs7 import SignerInfo, SignedData +from signify.x509 import Certificate + + +def indent_text(*items: str, indent: int = 4) -> str: + return "\n".join(textwrap.indent(item, " " * indent) for item in items) + + +def list_item(*items: str, indent: int = 4) -> str: + return re.sub(r"^( *) {2}", r"\1- ", indent_text(*items, indent=indent)) + + +def format_certificate(cert: Certificate, indent: int = 4) -> str: + return list_item( + f"Subject: {cert.subject.dn}", + f"Issuer: {cert.issuer.dn}", + f"Serial: {cert.serial_number}", + f"Valid from: {cert.valid_from}", + f"Valid to: {cert.valid_to}", + indent=indent, + ) + + +def describe_attribute(name: str, values: list[Any]) -> list[str]: + if name in ("microsoft_time_stamp_token", "microsoft_spc_sp_opus_info"): + return [f"{name}: (elided)"] + if name == "message_digest": + return [f"{name}: {values[0].native.hex()}"] + if len(values) == 1: + return [f"{name}: {values[0].native}"] + return [f"{name}:", *[list_item(str(value.native)) for value in values]] + + +def describe_signer_info(signer_info: SignerInfo) -> list[str]: + result = [ + f"Issuer: {signer_info.issuer.dn}", + f"Serial: {signer_info.serial_number}", + f"Digest algorithm: {signer_info.digest_algorithm.__name__}", + f"Digest encryption algorithm: {signer_info.digest_encryption_algorithm}", + f"Encrypted digest: {signer_info.encrypted_digest.hex()}", + ] + if isinstance(signer_info, AuthenticodeSignerInfo): + result += [ + "", + f"Program name: {signer_info.program_name}", + f"More info: {signer_info.more_info}", + ] + + if signer_info.authenticated_attributes: + result += [ + "", + "Authenticated attributes:", + *[ + list_item(*describe_attribute(*attribute)) + for attribute in signer_info.authenticated_attributes.items() + ], + ] + if signer_info.unauthenticated_attributes: + result += [ + "", + "Unauthenticated attributes:", + *[ + list_item(*describe_attribute(*attribute)) + for attribute in signer_info.unauthenticated_attributes.items() + ], + ] + + if signer_info.countersigner: + result += [""] + if hasattr(signer_info.countersigner, "issuer"): + result += [ + "Countersigner:", + indent_text( + f"Signing time: {signer_info.countersigner.signing_time}", + f"Issuer: {signer_info.countersigner.issuer.dn}", + f"Serial: {signer_info.countersigner.serial_number}", + indent=4, + ), + ] + if hasattr(signer_info.countersigner, "signer_info"): + result += [ + "Countersigner (nested RFC3161):", + indent_text( + *describe_signed_data(signer_info.countersigner), + indent=4, + ), + ] + + if hasattr(signer_info.countersigner, "certificates"): + result += [ + indent_text( + "", + "Included certificates:", + *[ + format_certificate(cert) + for cert in signer_info.countersigner.certificates + ], + indent=4, + ) + ] + + return result + + +def describe_signed_data(signed_data: SignedData): + result = [ + "Included certificates:", + *[format_certificate(cert) for cert in signed_data.certificates], + "", + "Signer:", + indent_text(*describe_signer_info(signed_data.signer_info), indent=4), + "", + f"Digest algorithm: {signed_data.digest_algorithm.__name__}", + f"Content type: {signed_data.content_type}", + ] + + if isinstance(signed_data, AuthenticodeSignedData) and signed_data.indirect_data: + result += [ + "", + "Indirect Data:", + indent_text( + f"Digest algorithm: {signed_data.indirect_data.digest_algorithm.__name__}", + f"Digest: {signed_data.indirect_data.digest.hex()}", + f"Content type: {signed_data.indirect_data.content_type}", + ), + ] + if signed_data.indirect_data.content_type == "microsoft_spc_pe_image_data": + pe_image_data = signed_data.indirect_data.content + result += [ + "", + indent_text("PE Image Data:", indent=4), + indent_text( + f"Flags: {pe_image_data.flags}", + f"File Link Type: {pe_image_data.file_link_type}", + indent=8, + ), + ] + if pe_image_data.file_link_type == "moniker": + result += [ + indent_text( + f"Class ID: {pe_image_data.class_id}", + f"Content Type: {','.join(pe_image_data.content_types)}", + indent=8, + ) + ] + else: + result += [ + indent_text( + f"Publisher: {pe_image_data.publisher}", + indent=8, + ) + ] + + if isinstance(signed_data, RFC3161SignedData) and signed_data.tst_info: + result += [ + "", + "TST Info:", + indent_text( + f"Hash algorithm: {signed_data.tst_info.hash_algorithm.__name__}", + f"Digest: {signed_data.tst_info.message_digest.hex()}", + f"Serial number: {signed_data.tst_info.serial_number}", + f"Signing time: {signed_data.tst_info.signing_time}", + f"Signing time acc: {signed_data.tst_info.signing_time_accuracy}", + f"Signing authority: {signed_data.tst_info.signing_authority}", + ), + ] + + if isinstance(signed_data, AuthenticodeSignedData): + verify_result, e = signed_data.explain_verify() + result += ["", str(verify_result)] + if e: + result += [f"{e}"] -from signify.authenticode import SignedPEFile + return result -def main(*filenames): +def main(*filenames: str): logging.basicConfig(level=logging.DEBUG) for filename in filenames: - print("{}:".format(filename)) - with open(filename, "rb") as file_obj: + print(f"{filename}:") + with pathlib.Path(filename).open("rb") as file_obj: try: pe = SignedPEFile(file_obj) for signed_data in pe.signed_datas: - print(" Included certificates:") - for cert in signed_data.certificates: - print(" - Subject: {}".format(cert.subject.dn)) - print(" Issuer: {}".format(cert.issuer.dn)) - print(" Serial: {}".format(cert.serial_number)) - print(" Valid from: {}".format(cert.valid_from)) - print(" Valid to: {}".format(cert.valid_to)) - - print() - print(" Signer:") - print(" Issuer: {}".format(signed_data.signer_info.issuer.dn)) - print(" Serial: {}".format(signed_data.signer_info.serial_number)) - print(" Program name: {}".format(signed_data.signer_info.program_name)) - print(" More info: {}".format(signed_data.signer_info.more_info)) - - if signed_data.signer_info.countersigner: - print() - if hasattr(signed_data.signer_info.countersigner, 'issuer'): - print(" Countersigner:") - print(" Issuer: {}".format(signed_data.signer_info.countersigner.issuer.dn)) - print(" Serial: {}".format(signed_data.signer_info.countersigner.serial_number)) - if hasattr(signed_data.signer_info.countersigner, 'signer_info'): - print(" Countersigner (nested RFC3161):") - print(" Issuer: {}".format( - signed_data.signer_info.countersigner.signer_info.issuer.dn - )) - print(" Serial: {}".format( - signed_data.signer_info.countersigner.signer_info.serial_number - )) - print(" Signing time: {}".format(signed_data.signer_info.countersigner.signing_time)) - - if hasattr(signed_data.signer_info.countersigner, 'certificates'): - print(" Included certificates:") - for cert in signed_data.signer_info.countersigner.certificates: - print(" - Subject: {}".format(cert.subject.dn)) - print(" Issuer: {}".format(cert.issuer.dn)) - print(" Serial: {}".format(cert.serial_number)) - print(" Valid from: {}".format(cert.valid_from)) - print(" Valid to: {}".format(cert.valid_to)) - - print() - print(" Digest algorithm: {}".format(signed_data.digest_algorithm.__name__)) - print(" Digest: {}".format(signed_data.spc_info.digest.hex())) - - print() - - result, e = signed_data.explain_verify() - print(" {}".format(result)) - if e: - print(" {}".format(e)) + print(indent_text(*describe_signed_data(signed_data), indent=4)) print("--------") result, e = pe.explain_verify() @@ -71,8 +208,9 @@ def main(*filenames): print(e) except Exception as e: + raise print(" Error while parsing: " + str(e)) -if __name__ == '__main__': +if __name__ == "__main__": main(*sys.argv[1:]) diff --git a/signify/asn1/spc.py b/signify/asn1/spc.py index 4bf8720..165411d 100755 --- a/signify/asn1/spc.py +++ b/signify/asn1/spc.py @@ -24,6 +24,8 @@ from __future__ import annotations +import uuid + from asn1crypto.algos import DigestInfo from asn1crypto.cms import ( CMSAttribute, @@ -31,6 +33,7 @@ ContentInfo, ContentType, EncapsulatedContentInfo, + SetOfOctetString, ) from asn1crypto.core import ( Any, @@ -43,6 +46,7 @@ Integer, ObjectIdentifier, OctetString, + ParsableOctetString, Sequence, SequenceOf, SetOf, @@ -62,6 +66,20 @@ class SpcUuid(OctetString): # type: ignore[misc] SpcUuid ::= OCTETSTRING """ + def set(self, value: uuid.UUID | str) -> None: + if not isinstance(value, uuid.UUID): + value = uuid.UUID(value) + super().set(value.bytes) + self._native = str(value) + + @property + def native(self) -> str | None: + if self.contents is None: + return None + if self._native is None: + self._native = str(uuid.UUID(bytes=self._merge_chunks())) + return self._native + class SpcSerializedObject(Sequence): # type: ignore[misc] """SpcSerializedObject. @@ -77,8 +95,10 @@ class SpcSerializedObject(Sequence): # type: ignore[misc] _fields = [ ("class_id", SpcUuid), - ("serialized_data", OctetString), + ("serialized_data", ParsableOctetString), ] + _oid_pair = ("class_id", "serialized_data") + _oid_specs: dict[str, type[Asn1Value]] = {} class SpcString(Choice): # type: ignore[misc] @@ -222,6 +242,8 @@ class SpcAttributeType(ObjectIdentifier): # type: ignore[misc] _map: dict[str, str] = { "1.3.6.1.4.1.311.2.1.15": "microsoft_spc_pe_image_data", "1.3.6.1.4.1.311.2.1.30": "microsoft_spc_siginfo", + "1.3.6.1.4.1.311.2.3.1": "microsoft_spc_pe_image_page_hashes_v1", + "1.3.6.1.4.1.311.2.3.2": "microsoft_spc_pe_image_page_hashes_v2", } @@ -251,9 +273,16 @@ class SpcAttributeTypeAndOptionalValue(Sequence): # type: ignore[misc] _oid_specs: dict[str, type[Asn1Value]] = { "microsoft_spc_pe_image_data": SpcPeImageData, "microsoft_spc_siginfo": SpcSigInfo, + # used as content in SpcLink.moniker's SpcSerializedObject.serializedData + "microsoft_spc_pe_image_page_hashes_v1": SetOfOctetString, + "microsoft_spc_pe_image_page_hashes_v2": SetOfOctetString, } +class SetOfSpcAttributeTypeAndOptionalValue(SetOf): # type: ignore[misc] + _child_spec = SpcAttributeTypeAndOptionalValue + + class SpcIndirectDataContent(Sequence): # type: ignore[misc] """Indirect data content. @@ -349,6 +378,10 @@ class SetOfSpcRelaxedPeMarkerCheck(SetOf): # type: ignore[misc] _child_spec = SpcRelaxedPeMarkerCheck +SpcSerializedObject._oid_specs["a6b586d5-b4a1-2466-ae05-a217da8e60d6"] = ( + SetOfSpcAttributeTypeAndOptionalValue +) + ContentType._map["1.3.6.1.4.1.311.2.1.4"] = "microsoft_spc_indirect_data_content" EncapsulatedContentInfo._oid_specs["microsoft_spc_indirect_data_content"] = ( ContentInfo._oid_specs["microsoft_spc_indirect_data_content"] diff --git a/signify/authenticode/__init__.py b/signify/authenticode/__init__.py index 2b04510..369f7cd 100644 --- a/signify/authenticode/__init__.py +++ b/signify/authenticode/__init__.py @@ -8,9 +8,9 @@ AuthenticodeSignedData, AuthenticodeSignerInfo, AuthenticodeVerificationResult, + IndirectData, RFC3161SignedData, RFC3161SignerInfo, - SpcInfo, TSTInfo, ) @@ -21,7 +21,7 @@ "AuthenticodeVerificationResult", "AuthenticodeCounterSignerInfo", "AuthenticodeSignerInfo", - "SpcInfo", + "IndirectData", "AuthenticodeSignedData", "RFC3161SignerInfo", "TSTInfo", diff --git a/signify/authenticode/signed_pe.py b/signify/authenticode/signed_pe.py index 8c980cc..fb66c5f 100644 --- a/signify/authenticode/signed_pe.py +++ b/signify/authenticode/signed_pe.py @@ -37,11 +37,13 @@ import logging import os import struct -from typing import Any, BinaryIO, Iterable, Iterator +from functools import cached_property +from typing import Any, BinaryIO, Iterable, Iterator, cast from typing_extensions import Literal, TypedDict from signify import fingerprinter +from signify._typing import HashFunction from signify.asn1.hashing import ACCEPTED_DIGEST_ALGORITHMS from signify.authenticode import structures from signify.exceptions import AuthenticodeNotSignedError, SignedPEParseError @@ -96,15 +98,11 @@ def get_authenticode_omit_sections(self) -> dict[str, RelRange] | None: if k in ["checksum", "datadir_certtable", "certtable"] } - def _parse_pe_header_locations(self) -> dict[str, RelRange]: - """Parses a PE file to find the sections to exclude from the AuthentiCode hash. - - See http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx for - information about the structure. + def _seek_start_of_pe(self) -> int: + """Seeks in the file to the start of the PE header. After this method, + the file header should be after ``b"PE\0\0"``. """ - location = {} - # Check if file starts with MZ self.file.seek(0, os.SEEK_SET) if self.file.read(2) != b"MZ": @@ -112,7 +110,7 @@ def _parse_pe_header_locations(self) -> dict[str, RelRange]: # Offset to e_lfanew (which is the PE header) is at 0x3C of the MZ header self.file.seek(0x3C, os.SEEK_SET) - pe_offset = struct.unpack("= self._filelength: raise SignedPEParseError( "PE header location is beyond file boundaries" @@ -124,7 +122,16 @@ def _parse_pe_header_locations(self) -> dict[str, RelRange]: if self.file.read(4) != b"PE\0\0": raise SignedPEParseError("PE header not found") - # The COFF header contains the size of the optional header + return pe_offset + + def _seek_optional_header(self) -> tuple[int, int]: + """Seeks in the file for the start and size of the optional COFF header. + After this method, the file header should be at the start of the optional + header. + """ + + pe_offset = self._seek_start_of_pe() + self.file.seek(pe_offset + 20, os.SEEK_SET) optional_header_size = struct.unpack(" dict[str, RelRange]: f"which is insufficient for authenticode", ) - # The optional header contains the signature of the image self.file.seek(optional_header_offset, os.SEEK_SET) + return optional_header_offset, optional_header_size + + def _parse_pe_header_locations(self) -> dict[str, RelRange]: + """Parses a PE file to find the sections to exclude from the AuthentiCode hash. + + See http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx for + information about the structure. + """ + + location = {} + optional_header_offset, optional_header_size = self._seek_optional_header() + + # The optional header contains the signature of the image signature = struct.unpack(" Iterator[ParsedCertTable]: } position += length + (8 - length % 8) % 8 + @cached_property + def page_size(self) -> int: + """Gets the page size from the optional COFF header, or if not available, + returns 4096 as best guess. + """ + optional_header_offset, optional_header_size = self._seek_optional_header() + + if optional_header_size < 36: + return 4096 + + self.file.seek(optional_header_offset + 32, os.SEEK_SET) + return cast(int, struct.unpack(" fingerprinter.AuthenticodeFingerprinter: """Returns a fingerprinter object for this file. @@ -255,6 +287,25 @@ def get_fingerprinter(self) -> fingerprinter.AuthenticodeFingerprinter: """ return fingerprinter.AuthenticodeFingerprinter(self.file) + def get_fingerprint( + self, + digest_algorithm: HashFunction, + start: int = 0, + end: int = -1, + aligned: bool = False, + ) -> bytes: + """Gets the fingerprint for this file, with the provided start and end, + and optionally aligned to the PE file's alignment. + """ + fingerprinter = self.get_fingerprinter() + fingerprinter.add_authenticode_hashers( + digest_algorithm, + start=start, + end=end, + block_size=self.page_size if aligned else None, + ) + return fingerprinter.hash()[digest_algorithm().name] + @property def signed_datas(self) -> Iterator[structures.AuthenticodeSignedData]: """Returns an iterator over :class:`AuthenticodeSignedData` objects relevant for diff --git a/signify/authenticode/structures.py b/signify/authenticode/structures.py index b607161..27348fa 100644 --- a/signify/authenticode/structures.py +++ b/signify/authenticode/structures.py @@ -28,10 +28,12 @@ import datetime import enum +import hashlib import logging import pathlib +import struct import warnings -from typing import Any, Callable, Iterable, Sequence, cast +from typing import Any, Callable, ClassVar, Iterable, Sequence, cast import mscerts from asn1crypto import cms, tsp @@ -43,12 +45,14 @@ from signify.asn1 import spc from signify.asn1.hashing import _get_digest_algorithm from signify.asn1.helpers import accuracy_to_python +from signify.asn1.spc import SpcPeImageData from signify.authenticode import signed_pe from signify.authenticode.authroot import CertificateTrustList from signify.exceptions import ( AuthenticodeCounterSignerError, AuthenticodeInconsistentDigestAlgorithmError, AuthenticodeInvalidDigestError, + AuthenticodeInvalidPageHashError, AuthenticodeNotSignedError, AuthenticodeParseError, CertificateVerificationError, @@ -114,6 +118,9 @@ class AuthenticodeVerificationResult(enum.Enum): """ COUNTERSIGNER_ERROR = enum.auto() """Something went wrong when verifying the countersignature.""" + INVALID_PAGE_HASH = enum.auto() + """The page hash does not match the calculated page hash for the section. + """ @classmethod def call( @@ -127,6 +134,8 @@ def call( return cls.INCONSISTENT_DIGEST_ALGORITHM, exc except AuthenticodeInvalidDigestError as exc: return cls.INVALID_DIGEST, exc + except AuthenticodeInvalidPageHashError as exc: + return cls.INVALID_PAGE_HASH, exc except AuthenticodeCounterSignerError as exc: return cls.COUNTERSIGNER_ERROR, exc except CertificateVerificationError as exc: @@ -306,7 +315,204 @@ def _verify_issuer( return super()._verify_issuer(issuer, context, signing_time) -class SpcInfo: +class PeImageData: + """Information about the PE file, as provided in the :class:`IndirectData`. It + is based on the following structure:: + + SpcPeImageData ::= SEQUENCE { + flags SpcPeImageFlags DEFAULT { includeResources }, + file SpcLink + } + SpcPeImageFlags ::= BIT STRING { + includeResources (0), + includeDebugInfo (1), + includeImportAddressTable (2) + } + SpcLink ::= CHOICE { + url [0] IMPLICIT IA5STRING, + moniker [1] IMPLICIT SpcSerializedObject, + file [2] EXPLICIT SpcString + } + SpcSerializedObject ::= SEQUENCE { + classId SpcUuid, + serializedData OCTETSTRING + } + + This structure contains flags, which define which parts of the PE file are hashed. + It is always ignored. + + The file attribute originally contained information that describes the software + publisher, but can now be a URL (which is ignored), a file, which is set to a + SpcString set to ``<<>>``, or the moniker setting a SpcSerializedObject. + + If used, the moniker always has UUID a6b586d5-b4a1-2466-ae05-a217da8e60d6 + (bytes ``a6 b5 86 d5 b4 a1 24 66 ae 05 a2 17 da 8e 60 d6``), and a binary + structure. Ominously, this is left outside of scope of the Authenticode + documentation, noting that it contains a binary structure that contains page hashes. + + """ + + def __init__(self, asn1: spc.SpcPeImageData): + self.asn1 = asn1 + + @property + def flags(self) -> set[str]: + """Defines which parts of the PE file are hashed. It is always ignored.""" + return cast("set[str]", self.asn1["flags"].native) + + @property + def file_link_type(self) -> Literal["url", "moniker", "file"]: + """Describes which of the options is used in this content.""" + return cast(Literal["url", "moniker", "file"], self.asn1["file"].name) + + @property + def publisher(self) -> str: + """Available if :attr:`file_link_type` is ``url`` or ``file``. + Contains the information in the attribute in string form. + """ + if self.file_link_type not in {"url", "file"}: + raise AttributeError( + "Property only available when file_link_type is url or file." + ) + return cast(str, self.asn1["file"].native) + + @property + def class_id(self) -> str: + """Available if :attr:`file_link_type` is ``moniker``. + Contains the class ID. Should be a6b586d5-b4a1-2466-ae05-a217da8e60d6. + """ + if self.file_link_type != "moniker": + raise AttributeError( + "Property only available when file_link_type is moniker." + ) + return cast(str, self.asn1["file"].chosen["class_id"].native) + + @property + def serialized_data(self) -> bytes: + """Available if :attr:`file_link_type` is ``moniker``. + Raw serialized data as bytes. + """ + if self.file_link_type != "moniker": + raise AttributeError( + "Property only available when file_link_type is moniker." + ) + return bytes(self.asn1["file"].chosen["serialized_data"]) + + @property + def serialized_data_asn1_available(self) -> bool: + """Defines whether the property :attr:`serialized_data_asn1` is available.""" + return ( + self.file_link_type == "moniker" + and self.class_id == "a6b586d5-b4a1-2466-ae05-a217da8e60d6" + ) + + @property + def serialized_data_asn1(self) -> list[spc.SpcAttributeTypeAndOptionalValue]: + """Available if :attr:`serialized_data_asn1_available` is :const:`True`. + Return the data in ASN.1 form. + """ + if not self.serialized_data_asn1_available: + raise AttributeError("Serialized data unavailable.") + return cast( + "list[spc.SpcAttributeTypeAndOptionalValue]", + self.asn1["file"].chosen["serialized_data"].parsed, + ) + + @property + def content_pairs(self) -> Iterable[tuple[str, list[bytes]]]: + """Available if :attr:`serialized_data_asn1_available` is :const:`True`.""" + for attr in self.serialized_data_asn1: + yield attr["type"].native, attr["value"].native + + @property + def content_types(self) -> list[str]: + """Available if :attr:`serialized_data_asn1_available` is :const:`True`.""" + return [c["type"].native for c in self.serialized_data_asn1] + + @property + def content_type(self) -> str: + """Available if :attr:`serialized_data_asn1_available` is :const:`True`.""" + if len(self.content_types) == 1: + return self.content_type[0] + raise AttributeError( + "SpcPeImageData.content_types contained multiple content types" + ) + + @property + def contents(self) -> list[list[bytes]]: + """Available if :attr:`serialized_data_asn1_available` is :const:`True`.""" + return [c["value"].native for c in self.serialized_data_asn1] + + @property + def content(self) -> list[bytes]: + """Available if :attr:`serialized_data_asn1_available` is :const:`True`.""" + if len(self.contents) == 1: + return self.contents[0] + raise AttributeError("SpcPeImageData.contents contained multiple entries") + + @property + def page_hashes(self) -> Iterable[tuple[int, int, bytes, HashFunction]]: + """Iterates over all page hash ranges, and their hash digests, as defined + in the SpcSerializedObject. If not available, will simply return an empty list. + """ + if not self.serialized_data_asn1_available: + return + for content_type, contents in self.content_pairs: + hash_algorithm = self.page_hash_algorithm(content_type) + for content in contents: + for start, end, digest in self.parse_page_hash_content( + hash_algorithm, content + ): + yield start, end, digest, hash_algorithm + + PAGE_HASH_ALGORITHMS: ClassVar[dict[str, HashFunction]] = { + "microsoft_spc_pe_image_page_hashes_v1": hashlib.sha1, + "microsoft_spc_pe_image_page_hashes_v2": hashlib.sha256, + } + + @classmethod + def page_hash_algorithm(cls, content_type: str) -> HashFunction: + if content_type not in cls.PAGE_HASH_ALGORITHMS: + raise AuthenticodeParseError( + f"Unknown content type for page hashes: {content_type!r}" + ) + return cls.PAGE_HASH_ALGORITHMS[content_type] + + @property + def page_hash_algorithms(self) -> list[HashFunction]: + """Returns all used page hash algorithms in this structure.""" + return [ + self.page_hash_algorithm(content_type) + for content_type in self.content_types + ] + + @classmethod + def parse_page_hash_content( + cls, hash_algorithm: HashFunction, content: bytes + ) -> Iterable[tuple[int, int, bytes]]: + """Parses the content in the page hash content blob. It is constructed + as 4 bytes offset, and the hash digest. The final entry will be the final offset + and a zero hash (0000...). + + This method yields tuples of start offset, end offset, and the hash digest. + """ + + d = hash_algorithm() + d.update(b"") + hash_length = len(d.digest()) + + position, previous_offset, digest = 0, None, None + while position < len(content): + offset = struct.unpack(" Asn1Value: ) return cast(Asn1Value, self.asn1["data"]["value"]) + @property + def content(self) -> PeImageData | None: + if self.content_type == "microsoft_spc_pe_image_data": + return PeImageData(cast(SpcPeImageData, self.content_asn1)) + return None + @property def digest_algorithm(self) -> HashFunction: return _get_digest_algorithm( @@ -408,8 +614,12 @@ def _validate_asn1(self) -> None: ) @property - def spc_info(self) -> SpcInfo: - return SpcInfo(self.content_asn1) + def content(self) -> IndirectData: + return IndirectData(self.content_asn1) + + @property + def indirect_data(self) -> IndirectData: + return self.content def verify( self, @@ -419,6 +629,7 @@ def verify( cs_verification_context: VerificationContext | None = None, trusted_certificate_store: CertificateStore = TRUSTED_CERTIFICATE_STORE, verification_context_kwargs: dict[str, Any] | None = None, + verify_page_hashes: bool = True, countersignature_mode: Literal["strict", "permit", "ignore"] = "strict", ) -> Iterable[list[Certificate]]: """Verifies the SignedData structure: @@ -465,6 +676,8 @@ def verify( :param dict verification_context_kwargs: If provided, keyword arguments that are passed to the instantiation of :class:`VerificationContext` s created in this function. Used for e.g. providing a timestamp. + :param str verify_page_hashes: Defines whether page hashes should be verified, + if present. :param str countersignature_mode: Changes how countersignatures are handled. Defaults to 'strict', which means that errors in the countersignature result in verification failure. If set to 'permit', the countersignature is @@ -503,7 +716,7 @@ def verify( ) # Check that the digest algorithms match - if self.digest_algorithm != self.spc_info.digest_algorithm: + if self.digest_algorithm != self.indirect_data.digest_algorithm: raise AuthenticodeInconsistentDigestAlgorithmError( "SignedData.digestAlgorithm must equal SpcInfo.digestAlgorithm" ) @@ -517,11 +730,9 @@ def verify( # 1. The hash of the file if expected_hash is None: assert self.pefile is not None - fingerprinter = self.pefile.get_fingerprinter() - fingerprinter.add_authenticode_hashers(self.digest_algorithm) - expected_hash = fingerprinter.hash()[self.digest_algorithm().name] + expected_hash = self.pefile.get_fingerprint(self.digest_algorithm) - if expected_hash != self.spc_info.digest: + if expected_hash != self.indirect_data.digest: raise AuthenticodeInvalidDigestError( "The expected hash does not match the digest in SpcInfo" ) @@ -532,46 +743,87 @@ def verify( "The expected hash of the SpcInfo does not match SignerInfo" ) + if verify_page_hashes: + self._verify_page_hashes() + # Can't check authAttr hash against encrypted hash, done implicitly in # M2's pubkey.verify. signing_time = None if self.signer_info.countersigner and countersignature_mode != "ignore": assert cs_verification_context is not None + signing_time = self._verify_countersigner( + cs_verification_context, countersignature_mode + ) - try: - # 3. Check the countersigner hash. - # Make sure to use the same digest_algorithm that the countersigner used - if not self.signer_info.countersigner.check_message_digest( - self.signer_info.encrypted_digest - ): - raise AuthenticodeCounterSignerError( - "The expected hash of the encryptedDigest does not match" - " countersigner's SignerInfo" - ) + return self.signer_info.verify(verification_context, signing_time) + + def _verify_page_hashes(self) -> None: + """Verifies the page hashes (if available) in the SpcPeImageData field.""" - cs_verification_context.timestamp = ( - self.signer_info.countersigner.signing_time + # can only verify page hashes when the indirect data is + # microsoft_spc_pe_image_data + if self.indirect_data.content_type != "microsoft_spc_pe_image_data": + return + + assert self.pefile is not None + assert isinstance(self.indirect_data.content, PeImageData) # typing + image_data = self.indirect_data.content + + for start, end, digest, hash_algorithm in image_data.page_hashes: + expected_hash = self.pefile.get_fingerprint( + hash_algorithm, start, end, aligned=True + ) + + if expected_hash != digest: + raise AuthenticodeInvalidPageHashError( + f"The page hash for page {start}-{end} is invalid." ) - # We could be calling SignerInfo.verify or RFC3161SignedData.verify - # here, but those have identical signatures. Note that - # RFC3161SignedData accepts a trusted_certificate_store argument, but - # we pass in an explicit context anyway - self.signer_info.countersigner.verify(cs_verification_context) - except Exception as e: - if countersignature_mode != "strict": - pass - else: - raise AuthenticodeCounterSignerError( - f"An error occurred while validating the countersignature: {e}" - ) + def _verify_countersigner( + self, + verification_context: VerificationContext, + countersignature_mode: Literal["strict", "permit", "ignore"] = "strict", + ) -> datetime.datetime | None: + """Verifies the countersigner of the SignerInfo, if available. + + Returns the verified signing time of the binary, if correct, or returns None. + """ + + assert self.signer_info.countersigner is not None + assert countersignature_mode != "ignore" + + try: + # 3. Check the countersigner hash. + # Make sure to use the same digest_algorithm that the countersigner used + if not self.signer_info.countersigner.check_message_digest( + self.signer_info.encrypted_digest + ): + raise AuthenticodeCounterSignerError( + "The expected hash of the encryptedDigest does not match" + " countersigner's SignerInfo" + ) + + verification_context.timestamp = self.signer_info.countersigner.signing_time + + # We could be calling SignerInfo.verify or RFC3161SignedData.verify + # here, but those have identical signatures. Note that + # RFC3161SignedData accepts a trusted_certificate_store argument, but + # we pass in an explicit context anyway + self.signer_info.countersigner.verify(verification_context) + except Exception as e: + if countersignature_mode != "strict": + pass else: - # If no errors occur, we should be fine setting the timestamp to the - # countersignature's timestamp - signing_time = self.signer_info.countersigner.signing_time + raise AuthenticodeCounterSignerError( + f"An error occurred while validating the countersignature: {e}" + ) + else: + # If no errors occur, we should be fine setting the timestamp to the + # countersignature's timestamp + return self.signer_info.countersigner.signing_time - return self.signer_info.verify(verification_context, signing_time) + return None def explain_verify( self, *args: Any, **kwargs: Any @@ -674,10 +926,14 @@ def _validate_asn1(self) -> None: f" not {len(self.signer_infos)}" ) + @property + def content(self) -> TSTInfo: + return TSTInfo(self.content_asn1) + @property def tst_info(self) -> TSTInfo: """Contains the :class:`TSTInfo` class for this SignedData.""" - return TSTInfo(self.content_asn1) + return self.content @property def signing_time(self) -> datetime.datetime: diff --git a/signify/exceptions.py b/signify/exceptions.py index d7dc3c1..78c7ae2 100644 --- a/signify/exceptions.py +++ b/signify/exceptions.py @@ -50,6 +50,10 @@ class AuthenticodeInvalidDigestError(AuthenticodeVerificationError): pass +class AuthenticodeInvalidPageHashError(AuthenticodeVerificationError): + pass + + class AuthenticodeCounterSignerError(AuthenticodeVerificationError): pass diff --git a/signify/fingerprinter.py b/signify/fingerprinter.py index 021f107..abc5ac6 100644 --- a/signify/fingerprinter.py +++ b/signify/fingerprinter.py @@ -51,18 +51,27 @@ class Finger: """ def __init__( - self, hashers: list[hashlib._Hash], ranges: list[Range], description: str + self, + hashers: list[hashlib._Hash], + ranges: list[Range], + description: str, + block_size: int | None = None, ): """ :param hashers: A list of hashers to feed. :param ranges: A list of Ranges that are hashed. :param description: The description of this Finger. + :param block_size: Defines a virtual block size that should be used to + complement the provided ranges with NULL bytes. """ self._ranges = ranges self.hashers = hashers self.description = description + self.block_size = block_size + + self._virtual_range = self._ranges[-1].end - self._ranges[0].start @property def current_range(self) -> Range | None: @@ -111,6 +120,17 @@ def update(self, block: bytes) -> None: for hasher in self.hashers: hasher.update(block) + def update_block_size(self) -> None: + """Feed the hashes NULL bytes to ensure that a certain (virtual) block size is + read. Note that this is calculated by using the first offset in the provided + ranges, and the last offset in the provided ranges, and not the actual amount + of bytes read. + """ + if self.block_size is None or (self._virtual_range % self.block_size) == 0: + return + + self.update(b"\0" * (self.block_size - (self._virtual_range % self.block_size))) + class Fingerprinter: def __init__(self, file_obj: BinaryIO, block_size: int = 1000000): @@ -132,11 +152,46 @@ def __init__(self, file_obj: BinaryIO, block_size: int = 1000000): self._fingers: list[Finger] = [] + def _adjust_ranges( + self, ranges: list[Range], start: int = 0, end: int = -1 + ) -> list[Range]: + """Adjusts provided ranges to all be between the provided start and end + intervals. + + :param ranges: A list of Ranges to limit between start and end. + :param start: The start interval for the ranges to be limited to. + :param end: The end interval for the ranges to be limited to. If negative, + equals to the end of the file. + """ + if end < 0: + end = self._filelength + + result = [] + for range in ranges: + if range.end < start or range.start > end: + # ignore any ranges that are outside of the allowed range + continue + if range.start >= start and range.end <= end: + # directly append anything that is within the provided range + result.append(range) + else: + # otherwise, only append the limited range for the provided range + result.append( + Range( + max(start, range.start), + min(end, range.end), + ) + ) + return result + def add_hashers( self, *hashers: HashFunction, ranges: list[Range] | None = None, description: str = "generic", + start: int = 0, + end: int = -1, + block_size: int | None = None, ) -> None: """Add hash methods to the fingerprinter. @@ -146,12 +201,22 @@ def add_hashers( to :const:`None`, it is set to the entire file. :param description: The name for the hashers. This name will return in :meth:`hashes` + :param start: Beginning of range to be hashed, limiting the provided ranges to + the provided value. + :param end: End of range to be hashed. If -1, this is equal to the entire file. + :param block_size: When set, adds NULL bytes to the end of the range to ensure + a certain block size is read. """ concrete_hashers = [x() for x in hashers] if not ranges: ranges = [Range(0, self._filelength)] - finger = Finger(concrete_hashers, ranges, description) + finger = Finger( + concrete_hashers, + self._adjust_ranges(ranges, start, end), + description, + block_size, + ) self._fingers.append(finger) @property @@ -180,7 +245,7 @@ def _next_interval(self) -> Range | None: return Range(min_start, min_end) def _hash_block(self, block: bytes, start: int, end: int) -> None: - """_HashBlock feeds data blocks into the hashers of fingers. + """Feed data blocks into the hashers of fingers. This function must be called before adjusting fingers for next interval, otherwise the lack of remaining ranges will cause the @@ -191,7 +256,7 @@ def _hash_block(self, block: bytes, start: int, end: int) -> None: :param block: The data block. :param start: Beginning offset of this block. - :param offset: Offset of the next byte after the block. + :param end: Next byte after the block. :raises RuntimeError: If the provided and expected ranges don't match. """ for finger in self._fingers: @@ -246,6 +311,8 @@ def hashes(self) -> dict[str, dict[str, bytes]]: ): raise RuntimeError("Non-empty range remains.") + finger.update_block_size() + res = {} for hasher in finger.hashers: res[hasher.name] = hasher.digest() @@ -274,7 +341,13 @@ class AuthenticodeFingerprinter(Fingerprinter): authentihashes of PE Files. """ - def add_authenticode_hashers(self, *hashers: HashFunction) -> bool: + def add_authenticode_hashers( + self, + *hashers: HashFunction, + start: int = 0, + end: int = -1, + block_size: int | None = None, + ) -> bool: """Specialized method of :meth:`add_hashers` to add hashers with ranges limited to those that are needed to calculate the hash of signed PE Files. """ @@ -286,13 +359,20 @@ def add_authenticode_hashers(self, *hashers: HashFunction) -> bool: return False ranges = [] - start = 0 + range_start = 0 for start_length in sorted(omit.values()): - ranges.append(Range(start, start_length.start)) - start = sum(start_length) - ranges.append(Range(start, self._filelength)) - - self.add_hashers(*hashers, ranges=ranges, description="authentihash") + ranges.append(Range(range_start, start_length.start)) + range_start = sum(start_length) + ranges.append(Range(range_start, self._filelength)) + + self.add_hashers( + *hashers, + ranges=ranges, + description="authentihash", + start=start, + end=end, + block_size=block_size, + ) return True diff --git a/signify/pkcs7/signeddata.py b/signify/pkcs7/signeddata.py index 59cefb0..c865ca8 100644 --- a/signify/pkcs7/signeddata.py +++ b/signify/pkcs7/signeddata.py @@ -29,7 +29,16 @@ class SignedData: signerInfos SignerInfos } - This class supports RFC2315 and RFC5652. + In general, it describes some form of data, that is (obviously) signed. In the + ASN.1 structure, you see the ``contentInfo``, which describes the signed content. + (See :attr:`content_type` and :attr:`content_asn1`. + + Each :class:`SignedData` object may contain multiple signers. Information about + these is found in :attr:`signer_infos`, pointing to one or more :class:`SignerInfo` + classes. + + Additionally, :attr:`certificates` contains any additional (intermediate) + certificates that may be required to verify these signers. """ _expected_content_type: str | None = None @@ -136,7 +145,7 @@ def signer_info(self) -> signerinfo.SignerInfo: @property def content_digest(self) -> bytes: - """Returns the digest of the content of the SignedData object, + """Returns the actual digest of the content of the SignedData object, adhering to the specs in RFC2315, 9.3; the identifier (tag) and length need to be stripped for hashing. """ diff --git a/signify/pkcs7/signerinfo.py b/signify/pkcs7/signerinfo.py index e48c0f0..9328725 100644 --- a/signify/pkcs7/signerinfo.py +++ b/signify/pkcs7/signerinfo.py @@ -35,7 +35,18 @@ class SignerInfo: unauthenticatedAttributes [1] IMPLICIT Attributes OPTIONAL } - This class supports RFC2315 and RFC5652. + The most important part of this structure are the authenticated attributes. These + will at least contain the hash of the content of the :class:`SignedData` structure. + We can verify this hash by hashing the same content using the hash in + :attr:`digest_algorithm` + + The :attr:`encrypted_digest` contains a signature by the issuer over these + authenticated attributes (the authenticated attributes are hashed and verified + using the :attr:`digest_encryption_algorithm`). The :attr:`issuer` and + :attr:`serial_number` contains a reference to the certificate of the issuer, that + is used for this signature. + + This class defines how a certain signer, (identified by their :attr:`issuer`) .. attribute:: asn1 @@ -62,16 +73,16 @@ class SignerInfo: _expected_content_type: str | None = None def __init__( - self, data: cms.SignerInfo, parent: signeddata.SignedData | None = None + self, asn1: cms.SignerInfo, parent: signeddata.SignedData | None = None ): """ - :param data: The ASN.1 structure of the SignerInfo. + :param asn1: The ASN.1 structure of the SignerInfo. :param parent: The parent :class:`SignedData` object. """ if isinstance(self._countersigner_class, str): self._countersigner_class = globals()[self._countersigner_class] - self.asn1 = data + self.asn1 = asn1 self.parent = parent self._validate_asn1() @@ -397,9 +408,11 @@ def potential_chains( class CounterSignerInfo(SignerInfo): - """The class CounterSignerInfo is a subclass of :class:`SignerInfo`. It is used as - the SignerInfo of a SignerInfo, containing the timestamp the SignerInfo was created - on. This normally works by sending the digest of the SignerInfo to an external + """A counter-signer provides information about when a :class:`SignerInfo` was + signed. It basically acts as the SignerInfo of the SignerInfo, linking the + message digest to the original SignerInfo's encrypted_digest. + + This normally works by sending the digest of the SignerInfo to an external trusted service, that will include a signed time in its response. """