diff --git a/docs/changelog.rst b/docs/changelog.rst index f800825..8bb42d0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,12 +2,17 @@ Release notes ============= This page contains the most significant changes in Signify between each release. -v0.7.2 (unreleased) +v0.8.0 (unreleased) ------------------- * 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 are not (yet) supported. +* Add support for ``SpcRelaxedPeMarkerCheck`` and ``PlatformManifestBinaryID`` as + SignerInfo attributes, although their exact purpose is currently unknown. +* Refactor classes to store the ASN.1 object in the property ``asn1``, and use + property methods as data accessors, instead of assigning all attributes during class + initialization. v0.7.1 (2024-09-11) ------------------- diff --git a/signify/authenticode/authroot.py b/signify/authenticode/authroot.py index 0bb576e..2bc21d6 100644 --- a/signify/authenticode/authroot.py +++ b/signify/authenticode/authroot.py @@ -3,7 +3,7 @@ import datetime import hashlib import pathlib -from typing import Any, Iterable +from typing import Any, Iterable, cast import mscerts from asn1crypto import cms @@ -47,68 +47,65 @@ class CertificateTrustList(signeddata.SignedData): } SubjectIdentifier ::= OCTETSTRING - .. attribute:: data - - The underlying ASN.1 data object - - .. attribute:: subject_usage - - Defines the EKU of the Certificate Trust List. Should be 1.3.6.1.4.1.311.20.1. - - .. attribute:: list_identifier - - The name of the template of the list. - - .. attribute:: sequence_number - - The unique number of this list - - .. attribute:: this_update - - The date of the current CTL. + .. warning:: - .. attribute:: next_update + The CTL itself is currently not verifiable. - The date of the next CTL. + """ - .. attribute:: subject_algorithm + _expected_content_type = "microsoft_ctl" + content_asn1: asn1.ctl.CertificateTrustList - Digest algorithm of verifying the list. + @property + def subject_usage(self) -> list[str]: + """Defines the EKU of the Certificate Trust List. + Should be 1.3.6.1.4.1.311.20.1. + """ + return cast(list[str], self.content_asn1["subject_usage"].native) - .. warning:: + @property + def list_identifier(self) -> bytes | None: + """The name of the template of the list.""" + return cast("bytes | None", self.content_asn1["list_identifier"].native) - The CTL itself is currently not verifiable. + @property + def sequence_number(self) -> int: + """The unique number of this list""" + return cast(int, self.content_asn1["sequence_number"].native) - """ + @property + def this_update(self) -> datetime.datetime | None: + """The date of the current CTL.""" + return cast( + "datetime.datetime | None", self.content_asn1["ctl_this_update"].native + ) - _expected_content_type = "microsoft_ctl" + @property + def next_update(self) -> datetime.datetime | None: + """The date of the next CTL.""" + return cast( + "datetime.datetime | None", self.content_asn1["ctl_next_update"].native + ) - subject_usage: list[str] - list_identifier: bytes | None - sequence_number: int - this_update: datetime.datetime | None - next_update: datetime.datetime | None - subject_algorithm: HashFunction - - def _parse(self) -> None: - super()._parse() - - self.subject_usage = self.content["subject_usage"].native - self.list_identifier = self.content["list_identifier"].native - self.sequence_number = self.content["sequence_number"].native - self.this_update = self.content["ctl_this_update"].native - self.next_update = self.content["ctl_next_update"].native - self.subject_algorithm = _get_digest_algorithm( - self.content["subject_algorithm"], + @property + def subject_algorithm(self) -> HashFunction: + """Digest algorithm of verifying the list.""" + return _get_digest_algorithm( + self.content_asn1["subject_algorithm"], location="CertificateTrustList.subjectAlgorithm", ) - self._subjects = {} - for subj in ( - CertificateTrustSubject(subject) - for subject in self.content["trusted_subjects"] - ): - self._subjects[subj.identifier.hex().lower()] = subj - # TODO: extensions?? + + @property + def _subjects(self) -> dict[str, CertificateTrustSubject]: + return { + subj.identifier.hex().lower(): subj + for subj in ( + CertificateTrustSubject(subject) + for subject in self.content_asn1["trusted_subjects"] + ) + } + + # TODO: extensions?? @property def subjects(self) -> Iterable[CertificateTrustSubject]: @@ -176,67 +173,6 @@ class CertificateTrustSubject: We do not pretend to have a complete picture of all the edge-cases that are considered. - .. attribute:: data - - The underlying ASN.1 data object - - .. attribute:: attributes - - A dictionary mapping of attribute types to values. - - The following values are extracted from the attributes: - - .. attribute:: extended_key_usages - - Defines the EKU's the certificate is valid for. It may be empty, which we take - as 'all is acceptable'. - - .. attribute:: friendly_name - - The friendly name of the certificate. - - .. attribute:: key_identifier - - The sha1 fingerprint of the certificate. - - .. attribute:: subject_name_md5 - - The md5 of the subject name. - - .. attribute:: auth_root_sha256 - - The sha256 fingerprint of the certificate. - - .. attribute:: disallowed_filetime - - The time since when a certificate has been disabled. Digital signatures with a - timestamp prior to this date continue to be valid, but use cases after this date - are prohibited. It may be used in conjunction with - :attr:`disallowed_extended_key_usages` to define specific EKU's to be disabled. - - .. attribute:: root_program_chain_policies - - A list of EKU's probably used for EV certificates. - - .. attribute:: disallowed_extended_key_usages - - Defines the EKU's the certificate is not valid for. When used in combination with - :attr:`disallowed_filetime`, the disabled EKU's are only disabled from that date - onwards, otherwise, it means since the beginning of time. - - .. attribute:: not_before_filetime - - The time since when new certificates from this CA are not trusted. Certificates - from prior the date will continue to validate. When used in conjunction with - :attr:`not_before_extended_key_usages`, this only concerns certificates issued - after this date for the defined EKU's. - - .. attribute:: not_before_extended_key_usages - - Defines the EKU's for which the :attr:`not_before_filetime` is considered. If - that attribute is not defined, we assume that it means since the beginning of - time. - .. warning:: The interpretation of the various attributes and their implications has been @@ -245,54 +181,110 @@ class CertificateTrustSubject: """ - extended_key_usages: list[str] | None - friendly_name: str | None - key_identifier: bytes - subject_name_md5: bytes - auth_root_sha256: bytes - disallowed_filetime: datetime.datetime | None - root_program_chain_policies: list[str] | None - disallowed_extended_key_usages: list[str] | None - not_before_filetime: datetime.datetime | None - not_before_extended_key_usages: list[str] | None - - def __init__(self, data: asn1.ctl.TrustedSubject): - self.data = data - self._parse() - - def _parse(self) -> None: - self.identifier = self.data["subject_identifier"].native - self.attributes = { + def __init__(self, asn1: asn1.ctl.TrustedSubject): + self.asn1 = asn1 + + @property + def identifier(self) -> bytes: + return cast(bytes, self.asn1["subject_identifier"].native) + + @property + def attributes(self) -> dict[str, Any]: + """A dictionary mapping of attribute types to values.""" + return { attr["type"].native: attr["values"].native[0] - for attr in self.data["subject_attributes"] + for attr in self.asn1["subject_attributes"] } - self.extended_key_usages = self.attributes.get( - "microsoft_ctl_enhkey_usage", None + @property + def extended_key_usages(self) -> list[str] | None: + """Defines the EKU's the certificate is valid for. It may be empty, which we + take as 'all is acceptable'. + """ + return cast( + "list[str] | None", self.attributes.get("microsoft_ctl_enhkey_usage", None) ) - self.friendly_name = self.attributes.get("microsoft_ctl_friendly_name", None) - self.key_identifier = self.attributes.get("microsoft_ctl_key_identifier", b"") - self.subject_name_md5 = self.attributes.get( - "microsoft_ctl_subject_name_md5_hash", b"" + + @property + def friendly_name(self) -> str | None: + """The friendly name of the certificate.""" + return cast( + "str | None", self.attributes.get("microsoft_ctl_friendly_name", None) ) - # TODO: RootProgramCertPolicies not implemented - self.auth_root_sha256 = self.attributes.get( - "microsoft_ctl_auth_root_sha256_hash", b"" + + @property + def key_identifier(self) -> bytes: + """The sha1 fingerprint of the certificate.""" + return cast(bytes, self.attributes.get("microsoft_ctl_key_identifier", b"")) + + @property + def subject_name_md5(self) -> bytes: + """The md5 of the subject name.""" + return cast( + bytes, self.attributes.get("microsoft_ctl_subject_name_md5_hash", b"") ) - self.disallowed_filetime = self.attributes.get( - "microsoft_ctl_disallowed_filetime", None + + # TODO: RootProgramCertPolicies not implemented + + @property + def auth_root_sha256(self) -> bytes: + """The sha256 fingerprint of the certificate.""" + return cast( + bytes, self.attributes.get("microsoft_ctl_auth_root_sha256_hash", b"") ) - self.root_program_chain_policies = self.attributes.get( - "microsoft_ctl_root_program_chain_policies", None + + @property + def disallowed_filetime(self) -> datetime.datetime | None: + """The time since when a certificate has been disabled. Digital signatures with + a timestamp prior to this date continue to be valid, but use cases after this + date are prohibited. It may be used in conjunction with + :attr:`disallowed_extended_key_usages` to define specific EKU's to be disabled. + """ + return cast( + "datetime.datetime | None", + self.attributes.get("microsoft_ctl_disallowed_filetime", None), ) - self.disallowed_extended_key_usages = self.attributes.get( - "microsoft_ctl_disallowed_enhkey_usage", None + + @property + def root_program_chain_policies(self) -> list[str] | None: + """A list of EKU's probably used for EV certificates.""" + return cast( + "list[str] | None", + self.attributes.get("microsoft_ctl_root_program_chain_policies", None), ) - self.not_before_filetime = self.attributes.get( - "microsoft_ctl_not_before_filetime", None + + @property + def disallowed_extended_key_usages(self) -> list[str] | None: + """Defines the EKU's the certificate is not valid for. When used in combination + with :attr:`disallowed_filetime`, the disabled EKU's are only disabled from + that date onwards, otherwise, it means since the beginning of time. + """ + return cast( + "list[str] | None", + self.attributes.get("microsoft_ctl_disallowed_enhkey_usage", None), + ) + + @property + def not_before_filetime(self) -> datetime.datetime | None: + """The time since when new certificates from this CA are not trusted. + Certificates from prior the date will continue to validate. When used in + conjunction with :attr:`not_before_extended_key_usages`, this only concerns + certificates issued after this date for the defined EKU's. + """ + return cast( + "datetime.datetime | None", + self.attributes.get("microsoft_ctl_not_before_filetime", None), ) - self.not_before_extended_key_usages = self.attributes.get( - "microsoft_ctl_not_before_enhkey_usage", None + + @property + def not_before_extended_key_usages(self) -> list[str] | None: + """Defines the EKU's for which the :attr:`not_before_filetime` is considered. If + that attribute is not defined, we assume that it means since the beginning of + time. + """ + return cast( + "list[str] | None", + self.attributes.get("microsoft_ctl_not_before_enhkey_usage", None), ) def verify_trust( diff --git a/signify/authenticode/structures.py b/signify/authenticode/structures.py index 3321861..b607161 100644 --- a/signify/authenticode/structures.py +++ b/signify/authenticode/structures.py @@ -31,10 +31,11 @@ import logging import pathlib import warnings -from typing import Any, Callable, Iterable, Sequence +from typing import Any, Callable, Iterable, Sequence, cast import mscerts from asn1crypto import cms, tsp +from asn1crypto.core import Asn1Value from typing_extensions import Literal, ParamSpec from signify import asn1 @@ -148,107 +149,29 @@ class AuthenticodeCounterSignerInfo(CounterSignerInfo): class AuthenticodeSignerInfo(SignerInfo): """Subclass of :class:`SignerInfo` that is used by the verification of Authenticode. - Note that this will contain the same attributes as :class:`SignerInfo`, and - additionally the following: - - .. attribute:: program_name - more_info - publisher_info - - This information is extracted from the SpcSpOpusInfo authenticated attribute, - containing the program's name and an URL with more information. The - publisher_info is almost never set, but is defined in the ASN.1 structure. - - .. attribute:: statement_types - - Defines the key purpose of the signer. This is ignored by the verification. - - .. attribute:: nested_signed_datas - - It is possible for Authenticode SignerInfo objects to contain nested - :class:`signify.pkcs7.SignedData` objects. This is similar to including - multiple SignedData structures in the :class:`signify.authenticode.SignedPEFile`. - This field is extracted from the unauthenticated attributes. + Note that this will contain the same attributes as :class:`SignerInfo`, with + some additions. The :attr:`countersigner` attribute can hold the same as in the normal - :class:`SignerInfo`, but may also contain a :class:`RFC3161SignedData` class: - - .. attribute:: countersigner - - Authenticode may use a different countersigning mechanism, rather than using a - nested :class:`AuthenticodeCounterSignerInfo`, it may use a nested RFC-3161 - response, which is a nested :class:`signify.pkcs7.SignedData` structure - (of type :class:`RFC3161SignedData`). This is also assigned to the countersigner - attribute if this is available. - - + :class:`SignerInfo`, but may also contain a :class:`RFC3161SignedData` class. """ - program_name: str | None - more_info: str | None - publisher_info: str | None - nested_signed_datas: list[AuthenticodeSignedData] - parent: AuthenticodeSignedData - # allow other countersigner as well - countersigner: ( # type: ignore[assignment] - AuthenticodeCounterSignerInfo | RFC3161SignedData | None - ) + _singular_authenticated_attributes = ( + *SignerInfo._singular_authenticated_attributes, + "microsoft_spc_statement_type", + "microsoft_spc_sp_opus_info", + ) + _singular_unauthenticated_attributes = ( + *SignerInfo._singular_unauthenticated_attributes, + "microsoft_time_stamp_token", + ) _countersigner_class = AuthenticodeCounterSignerInfo _expected_content_type = "microsoft_spc_indirect_data_content" - def _parse(self) -> None: - super()._parse() - - # - Retrieve statement types - self.statement_types = None - if "microsoft_spc_statement_type" in self.authenticated_attributes: - if len(self.authenticated_attributes["microsoft_spc_statement_type"]) != 1: - raise AuthenticodeParseError( - "Only one SpcStatementType expected in" - " SignerInfo.authenticatedAttributes" - ) - self.statement_types = self.authenticated_attributes[ - "microsoft_spc_statement_type" - ][0].native - - # - Retrieve object from SpcSpOpusInfo from the authenticated attributes - # (for normal signer) - self.program_name = self.more_info = self.publisher_info = None - if "microsoft_spc_sp_opus_info" in self.authenticated_attributes: - if len(self.authenticated_attributes["microsoft_spc_sp_opus_info"]) != 1: - raise AuthenticodeParseError( - "Only one SpcSpOpusInfo expected in" - " SignerInfo.authenticatedAttributes" - ) - - self.program_name = self.authenticated_attributes[ - "microsoft_spc_sp_opus_info" - ][0]["program_name"].native - self.more_info = self.authenticated_attributes[ - "microsoft_spc_sp_opus_info" - ][0]["more_info"].native - self.publisher_info = self.authenticated_attributes[ - "microsoft_spc_sp_opus_info" - ][0]["publisher_info"].native - - # - Authenticode can use nested signatures through OID 1.3.6.1.4.1.311.2.4.1 - self.nested_signed_datas = [] - if "microsoft_nested_signature" in self.unauthenticated_attributes: - for sig_data in self.unauthenticated_attributes[ - "microsoft_nested_signature" - ]: - content_type = sig_data["content_type"].native - if content_type != "signed_data": - raise AuthenticodeParseError( - "Nested signature is not a SignedData structure" - ) - self.nested_signed_datas.append( - AuthenticodeSignedData( - sig_data["content"], pefile=self.parent.pefile - ) - ) + def _validate_asn1(self) -> None: + super()._validate_asn1() # - Authenticode can be signed using a RFC-3161 timestamp, so we discover this # possibility here @@ -260,21 +183,111 @@ def _parse(self) -> None: "Countersignature and RFC-3161 timestamp present in" " SignerInfo.unauthenticatedAttributes" ) - if "microsoft_time_stamp_token" in self.unauthenticated_attributes: - if len(self.unauthenticated_attributes["microsoft_time_stamp_token"]) != 1: - raise AuthenticodeParseError( - "Only one RFC-3161 timestamp expected in" - " SignerInfo.unauthenticatedAttributes" - ) - ts_data = self.unauthenticated_attributes["microsoft_time_stamp_token"][0] if ts_data["content_type"].native != "signed_data": raise AuthenticodeParseError( "RFC-3161 Timestamp does not contain SignedData structure" ) - self.countersigner = RFC3161SignedData(ts_data["content"]) + @property + def statement_types(self) -> list[str] | None: + """Defines the key purpose of the signer. This is ignored by the + verification. + """ + if "microsoft_spc_statement_type" not in self.authenticated_attributes: + return None + return cast( + list[str], + self.authenticated_attributes["microsoft_spc_statement_type"][0].native, + ) + + @property + def program_name(self) -> str | None: + """This information is extracted from the SpcSpOpusInfo authenticated attribute, + containing the program's name. + """ + if "microsoft_spc_sp_opus_info" not in self.authenticated_attributes: + return None + return cast( + str, + self.authenticated_attributes["microsoft_spc_sp_opus_info"][0][ + "program_name" + ].native, + ) + + @property + def more_info(self) -> str | None: + """This information is extracted from the SpcSpOpusInfo authenticated attribute, + containing the URL with more information. + """ + if "microsoft_spc_sp_opus_info" not in self.authenticated_attributes: + return None + return cast( + str, + self.authenticated_attributes["microsoft_spc_sp_opus_info"][0][ + "more_info" + ].native, + ) + + @property + def publisher_info(self) -> str | None: + """This information is extracted from the SpcSpOpusInfo authenticated attribute, + containing the publisher_info. It is almost never set, but is defined in the + ASN.1 structure. + """ + if "microsoft_spc_sp_opus_info" not in self.authenticated_attributes: + return None + return cast( + str, + self.authenticated_attributes["microsoft_spc_sp_opus_info"][0][ + "publisher_info" + ].native, + ) + + @property + def nested_signed_datas(self) -> list[AuthenticodeSignedData]: + """It is possible for Authenticode SignerInfo objects to contain nested + :class:`signify.pkcs7.SignedData` objects. This is similar to including + multiple SignedData structures in the + :class:`signify.authenticode.SignedPEFile`. + + This field is extracted from the unauthenticated attributes. + """ + if "microsoft_nested_signature" not in self.unauthenticated_attributes: + return [] + + result = [] + for sig_data in self.unauthenticated_attributes[ + "microsoft_nested_signature" + ]: # type: cms.SignedData + content_type = sig_data["content_type"].native + if content_type != "signed_data": + raise AuthenticodeParseError( + "Nested signature is not a SignedData structure" + ) + result.append( + AuthenticodeSignedData(sig_data["content"], pefile=self.parent.pefile) + ) + + return result + + @property + def countersigner(self) -> AuthenticodeCounterSignerInfo | RFC3161SignedData | None: # type: ignore[override] + """Authenticode may use a different countersigning mechanism, rather than using + a nested :class:`AuthenticodeCounterSignerInfo`, it may use a nested RFC-3161 + response, which is a nested :class:`signify.pkcs7.SignedData` structure + (of type :class:`RFC3161SignedData`). This is also assigned to the countersigner + attribute if this is available. + """ + if "microsoft_time_stamp_token" in self.unauthenticated_attributes: + ts_data = cast( + cms.ContentInfo, + self.unauthenticated_attributes["microsoft_time_stamp_token"][0], + ) + return RFC3161SignedData(ts_data["content"]) + + return cast("AuthenticodeCounterSignerInfo | None", super().countersigner) def _verify_issuer( self, @@ -311,72 +324,45 @@ class SpcInfo: digestAlgorithm AlgorithmIdentifier, digest OCTETSTRING } - AlgorithmIdentifier ::= SEQUENCE { - algorithm ObjectID, - parameters [0] EXPLICIT ANY OPTIONAL - } - .. attribute:: data + .. attribute:: asn1 The underlying ASN.1 data object - .. attribute:: content_type - - The contenttype string - - .. attribute:: image_data - - The image data object embedded in the ASN.1 object. - - .. attribute:: image_flags - - The flags used for signing. These flags are ignored during verification. - - .. attribute:: image_publisher - - Obsolete software publisher field (i.e. ``SpcPeImageData.file``). Should now - contain ``<<>>``, although this value does not affect verification. + """ - .. attribute:: digest_algorithm - .. attribute:: digest + asn1: spc.SpcIndirectDataContent + def __init__(self, asn1: spc.SpcIndirectDataContent): + self.asn1 = asn1 - """ + @property + def content_type(self) -> str: + """The contenttype string""" + return cast(str, self.asn1["data"]["type"].native) - data: spc.SpcIndirectDataContent - - content_type: str - image_data: spc.SpcPeImageData - image_flags: set[str] - image_publisher: str - digest_algorithm: HashFunction - digest: bytes - - def __init__(self, data: spc.SpcIndirectDataContent): - self.data = data - self._parse() - - def _parse(self) -> None: - # The data attribute - self.content_type = self.data["data"]["type"].native - - if self.content_type == "microsoft_spc_pe_image_data": - self.image_data = self.data["data"]["value"] - self.image_flags = self.image_data["flags"].native - self.image_publisher = self.image_data["file"].native - elif self.content_type != "microsoft_spc_siginfo": - # XXX: more content types may exist, but we currently know of these - # please open an issue so we can look into adding more :) + @property + def content_asn1(self) -> Asn1Value: + if self.content_type not in { + "microsoft_spc_pe_image_data", + "microsoft_spc_siginfo", + }: warnings.warn( f"SpcInfo contains unknown content type {self.content_type!r}", stacklevel=2, ) + return cast(Asn1Value, self.asn1["data"]["value"]) - self.digest_algorithm = _get_digest_algorithm( - self.data["message_digest"]["digest_algorithm"], + @property + def digest_algorithm(self) -> HashFunction: + return _get_digest_algorithm( + self.asn1["message_digest"]["digest_algorithm"], location="SpcIndirectDataContent.digestAlgorithm", ) - self.digest = self.data["message_digest"]["digest"].native + + @property + def digest(self) -> bytes: + return cast(bytes, self.asn1["message_digest"]["digest"].native) class AuthenticodeSignedData(SignedData): @@ -390,46 +376,41 @@ class AuthenticodeSignedData(SignedData): """ - pefile: signed_pe.SignedPEFile | None - spc_info: SpcInfo signer_infos: Sequence[AuthenticodeSignerInfo] signer_info: AuthenticodeSignerInfo + content_asn1: asn1.spc.SpcIndirectDataContent - content: asn1.spc.SpcIndirectDataContent _expected_content_type = "microsoft_spc_indirect_data_content" _signerinfo_class = AuthenticodeSignerInfo def __init__( self, - data: cms.SignedData, + asn1: cms.SignedData, pefile: signed_pe.SignedPEFile | None = None, ): """ - :param asn1.pkcs7.SignedData data: The ASN.1 structure of the SignedData object + :param asn1.pkcs7.SignedData asn1: The ASN.1 structure of the SignedData object :param pefile: The related PEFile. """ self.pefile = pefile - super().__init__(data) - - def _parse(self) -> None: - super()._parse() - self.spc_info = SpcInfo(self.content) + super().__init__(asn1) - # signerInfos + def _validate_asn1(self) -> None: + super()._validate_asn1() if len(self.signer_infos) != 1: raise AuthenticodeParseError( "SignedData.signerInfos must contain exactly 1 signer," f" not {len(self.signer_infos)}" ) - - self.signer_info = self.signer_infos[0] - - # CRLs - if self.data["crls"]: + if self.asn1["crls"]: raise AuthenticodeParseError( "SignedData.crls is present, but that is unexpected." ) + @property + def spc_info(self) -> SpcInfo: + return SpcInfo(self.content_asn1) + def verify( self, *, @@ -618,72 +599,58 @@ class RFC3161SignerInfo(SignerInfo): class TSTInfo: """This is an implementation of the TSTInfo class as defined by RFC3161, used as - content for a SignedData structure. The following properties are available: - - .. attribute:: data - - The underlying ASN.1 data object - - .. attribute:: policy - - .. attribute:: hash_algorithm - - The hash algorithm of the message imprint. - - .. attribute:: message_digest - - The hashed message - - .. attribute:: serial_number - - The serial number of this signature - - .. attribute:: signing_time - - The time this signature was generated - - .. attribute:: signing_time_accuracy - - The accuracy of the above time - - .. attribute:: signing_authority - - The authority generating this signature - + content for a SignedData structure. """ - policy: str - hash_algorithm: HashFunction - message_digest: bytes - serial_number: int - signing_time: datetime.datetime - signing_time_accuracy: datetime.timedelta - signing_authority: CertificateName - - def __init__(self, data: tsp.TSTInfo): + def __init__(self, asn1: tsp.TSTInfo): """ - - :param data: The ASN.1 structure of the TSTInfo object + :param asn1: The ASN.1 structure of the TSTInfo object """ - self.data = data - self._parse() + self.asn1 = asn1 + self._validate_asn1() - def _parse(self) -> None: - if self.data["version"].native != "v1": + def _validate_asn1(self) -> None: + if self.asn1["version"].native != "v1": raise AuthenticodeParseError( - f"TSTInfo.version must be v1, not {self.data['version'].native}" + f"TSTInfo.version must be v1, not {self.asn1['version'].native}" ) - self.policy = self.data["policy"].native - self.hash_algorithm = _get_digest_algorithm( - self.data["message_imprint"]["hash_algorithm"], + @property + def policy(self) -> str: + return cast(str, self.asn1["policy"].native) + + @property + def hash_algorithm(self) -> HashFunction: + """The hash algorithm of the message imprint.""" + return _get_digest_algorithm( + self.asn1["message_imprint"]["hash_algorithm"], location="TSTInfo.messageImprint.hashAlgorithm", ) - self.message_digest = self.data["message_imprint"]["hashed_message"].native - self.serial_number = self.data["serial_number"].native - self.signing_time = self.data["gen_time"].native - self.signing_time_accuracy = accuracy_to_python(self.data["accuracy"]) - self.signing_authority = CertificateName(self.data["tsa"]) + + @property + def message_digest(self) -> bytes: + """The hashed message""" + return cast(bytes, self.asn1["message_imprint"]["hashed_message"].native) + + @property + def serial_number(self) -> int: + """The serial number of this signature""" + return cast(int, self.asn1["serial_number"].native) + + @property + def signing_time(self) -> datetime.datetime: + """The time this signature was generated""" + return cast(datetime.datetime, self.asn1["gen_time"].native) + + @property + def signing_time_accuracy(self) -> datetime.timedelta: + """The accuracy of the above time""" + return accuracy_to_python(self.asn1["accuracy"]) + + @property + def signing_authority(self) -> CertificateName: + """The authority generating this signature""" + return CertificateName(self.asn1["tsa"]) class RFC3161SignedData(SignedData): @@ -693,31 +660,24 @@ class RFC3161SignedData(SignedData): This is a subclass of :class:`signify.pkcs7.SignedData`, containing a RFC3161 TSTInfo in its content field. - - .. attribute:: tst_info - :type: TSTInfo - - Contains the :class:`TSTInfo` class for this SignedData. """ - content: tsp.TSTInfo + content_asn1: tsp.TSTInfo _expected_content_type = "tst_info" _signerinfo_class = RFC3161SignerInfo - def _parse(self) -> None: - super()._parse() - - # Get the tst_info - self.tst_info = TSTInfo(self.content) - - # signerInfos + def _validate_asn1(self) -> None: + super()._validate_asn1() if len(self.signer_infos) != 1: raise AuthenticodeParseError( "RFC3161 SignedData.signerInfos must contain exactly 1 signer," f" not {len(self.signer_infos)}" ) - self.signer_info = self.signer_infos[0] + @property + def tst_info(self) -> TSTInfo: + """Contains the :class:`TSTInfo` class for this SignedData.""" + return TSTInfo(self.content_asn1) @property def signing_time(self) -> datetime.datetime: @@ -730,7 +690,6 @@ def check_message_digest(self, data: bytes) -> bool: """Given the data, returns whether the hash_algorithm and message_digest match the data provided. """ - auth_attr_hasher = self.tst_info.hash_algorithm() auth_attr_hasher.update(data) return auth_attr_hasher.digest() == self.tst_info.message_digest diff --git a/signify/pkcs7/signeddata.py b/signify/pkcs7/signeddata.py index 0e36139..59cefb0 100644 --- a/signify/pkcs7/signeddata.py +++ b/signify/pkcs7/signeddata.py @@ -30,57 +30,21 @@ class SignedData: } This class supports RFC2315 and RFC5652. - - .. attribute:: data - - The underlying ASN.1 data object - - .. attribute:: digest_algorithm - - The digest algorithm, i.e. the hash algorithm, that is used by the signers of - the data. - - .. attribute:: content_type - - The class of the type of the content in the object. - - .. attribute:: content - - The actual content, as parsed by the :attr:`content_type` spec. - - .. attribute:: certificates - :type: CertificateStore - - A list of all included certificates in the SignedData. These can be used to - determine a valid validation path from the signer to a root certificate. - - .. attribute:: signer_infos - :type: List[SignerInfo] - - A list of all included SignerInfo objects """ - data: cms.SignedData - digest_algorithm: HashFunction - content_type: str - content: Asn1Value - certificates: CertificateStore - signer_infos: Sequence[signerinfo.SignerInfo] - _expected_content_type: str | None = None _signerinfo_class: type[signerinfo.SignerInfo] | str | None = None - def __init__(self, data: cms.SignedData): + def __init__(self, asn1: cms.SignedData): """ - - :param data: The ASN.1 structure of the SignedData object + :param asn1: The ASN.1 structure of the SignedData object """ if isinstance(self._signerinfo_class, str): self._signerinfo_class = globals()[self._signerinfo_class] - self.data = data - self._parse() + self.asn1 = asn1 + self._validate_asn1() @classmethod def from_envelope(cls, data: bytes, *args: Any, **kwargs: Any) -> Self: @@ -96,25 +60,12 @@ def from_envelope(cls, data: bytes, *args: Any, **kwargs: Any) -> Self: signed_data = cls(content_info["content"], *args, **kwargs) return signed_data - def _parse(self) -> None: - # ('version', CMSVersion), - # ('digest_algorithms', DigestAlgorithms), - # ('encap_content_info', None), - # ('certificates', CertificateSet, {'implicit': 0, 'optional': True}), - # ('crls', RevocationInfoChoices, {'implicit': 1, 'optional': True}), - # ('signer_infos', SignerInfos), - - # digestAlgorithms - if len(self.data["digest_algorithms"]) != 1: + def _validate_asn1(self) -> None: + if len(self.asn1["digest_algorithms"]) != 1: raise ParseError( f"SignedData.digestAlgorithms must contain" - f" exactly 1 algorithm, not {len(self.data['digestAlgorithms'])}" + f" exactly 1 algorithm, not {len(self.asn1['digestAlgorithms'])}" ) - self.digest_algorithm = _get_digest_algorithm( - self.data["digest_algorithms"][0], "SignedData.digestAlgorithm" - ) - - self.content_type = self.data["encap_content_info"]["content_type"].native if self.content_type != self._expected_content_type: raise ParseError( @@ -122,29 +73,66 @@ def _parse(self) -> None: f" expected {self._expected_content_type}" ) - self._real_content = self.data["encap_content_info"]["content"] + @property + def digest_algorithm(self) -> HashFunction: + """The digest algorithm, i.e. the hash algorithm, that is used by the signers of + the data. + """ + return _get_digest_algorithm( + self.asn1["digest_algorithms"][0], "SignedData.digestAlgorithm" + ) + + @property + def content_type(self) -> str: + """The class of the type of the content in the object.""" + return cast(str, self.asn1["encap_content_info"]["content_type"].native) + + @property + def _real_content(self) -> Asn1Value: + return self.asn1["encap_content_info"]["content"] + + @property + def content_asn1(self) -> Asn1Value: + """The actual content, as parsed by the :attr:`content_type` spec.""" if hasattr(self._real_content, "parsed"): - self.content = self._real_content.parsed + return self._real_content.parsed else: - self.content = self._real_content + return self._real_content - # Certificates - self.certificates = CertificateStore( + @property + def certificates(self) -> CertificateStore: + """A list of all included certificates in the SignedData. These can be used to + determine a valid validation path from the signer to a root certificate. + """ + return CertificateStore( [ Certificate(cert) - for cert in self.data["certificates"] + for cert in self.asn1["certificates"] if not isinstance(cert, cms.CertificateChoices) or cert.name == "certificate" ] ) - # SignerInfo + @property + def signer_infos(self) -> Sequence[signerinfo.SignerInfo]: + """A list of all included SignerInfo objects""" if self._signerinfo_class is not None: assert not isinstance(self._signerinfo_class, str) - self.signer_infos = [ + return [ self._signerinfo_class(si, parent=self) - for si in self.data["signer_infos"] + for si in self.asn1["signer_infos"] ] + else: + raise AttributeError("No signer_infos expected") + + @property + def signer_info(self) -> signerinfo.SignerInfo: + if len(self.signer_infos) == 1: + return self.signer_infos[0] + raise AttributeError( + "SignedData.signerInfos must contain exactly 1 signer," + f" not {len(self.signer_infos)}" + ) @property def content_digest(self) -> bytes: @@ -158,7 +146,7 @@ def content_digest(self) -> bytes: # self.content.contents may refer to its children hash_content = bytes(self._real_content) else: - hash_content = self.content.contents + hash_content = self.content_asn1.contents blob_hasher = self.digest_algorithm() blob_hasher.update(hash_content) diff --git a/signify/pkcs7/signerinfo.py b/signify/pkcs7/signerinfo.py index 948a292..e48c0f0 100644 --- a/signify/pkcs7/signerinfo.py +++ b/signify/pkcs7/signerinfo.py @@ -1,7 +1,7 @@ from __future__ import annotations import datetime -from typing import Any, Iterable, cast +from typing import Iterable, cast from asn1crypto import cms from asn1crypto.core import Asn1Value @@ -37,7 +37,7 @@ class SignerInfo: This class supports RFC2315 and RFC5652. - .. attribute:: data + .. attribute:: asn1 The underlying ASN.1 data object @@ -46,84 +46,19 @@ class SignerInfo: The parent :class:`SignedData` object (or if other SignerInfos are present, it may be another object) - .. attribute:: issuer - :type: CertificateName - - The issuer of the SignerInfo, i.e. the certificate of the signer of the - SignedData object. - - .. attribute:: serial_number - - The serial number as specified by the issuer. - - .. attribute:: digest_algorithm - - The digest algorithm, i.e. the hash algorithm, under which the content and the - authenticated attributes are - signed. - - .. attribute:: authenticated_attributes - unauthenticated_attributes - - A SignerInfo object can contain both signed and unsigned attributes. These - contain additional information about the signature, but also the content type - and message digest. The difference between signed and unsigned is that unsigned - attributes are not validated. - - The type of this attribute is a dictionary. You should not need to access this - value directly, rather using one of the attributes listed below. - - .. attribute:: digest_encryption_algorithm - - This is the algorithm used for signing the digest with the signer's key. - - .. attribute:: encrypted_digest - - The result of encrypting the message digest and associated information with the - signer's private key. - - - The following attributes are automatically parsed and added to the list of - attributes if present. - - .. attribute:: message_digest - - This is an authenticated attribute, containing the signed digest of the data. - - .. attribute:: content_type - - This is an authenticated attribute, containing the content type of the content - being signed. - - .. attribute:: signing_time - - This is an authenticated attribute, containing the timestamp of signing. Note - that this should only be present in countersigner objects. - - .. attribute:: countersigner - - This is an unauthenticated attribute, containing the countersigner of the - SignerInfo. - """ - issuer: CertificateName - serial_number: int - authenticated_attributes: dict[str, list[Any]] - unauthenticated_attributes: dict[str, list[Any]] - digest_encryption_algorithm: str - encrypted_digest: bytes - digest_algorithm: HashFunction - message_digest: bytes | None - content_type: str | None - signing_time: datetime.datetime | None - countersigner: CounterSignerInfo | None - _countersigner_class: type[CounterSignerInfo] | str | None = "CounterSignerInfo" _required_authenticated_attributes: Iterable[str] = ( "content_type", "message_digest", ) + _singular_authenticated_attributes: Iterable[str] = ( + "message_digest", + "content_type", + "signing_time", + ) + _singular_unauthenticated_attributes: Iterable[str] = ("counter_signature",) _expected_content_type: str | None = None def __init__( @@ -136,126 +71,92 @@ def __init__( if isinstance(self._countersigner_class, str): self._countersigner_class = globals()[self._countersigner_class] - self.data = data + self.asn1 = data self.parent = parent - self._parse() + self._validate_asn1() - def _parse(self) -> None: - if self.data["sid"].name == "subject_key_identifier": + def _validate_asn1(self) -> None: + if self.asn1["sid"].name == "subject_key_identifier": raise SignerInfoParseError( "Cannot handle SignerInfo.sid with a subject_key_identifier" ) - self.issuer = CertificateName(self.data["sid"].chosen["issuer"]) - self.serial_number = self.data["sid"].chosen["serial_number"].native - self.authenticated_attributes = self._parse_attributes( - self.data["signed_attrs"], - required=self._required_authenticated_attributes, - ) - self._encoded_authenticated_attributes = self._encode_attributes( - self.data["signed_attrs"] - ) - self.unauthenticated_attributes = self._parse_attributes( - self.data["unsigned_attrs"] - ) - self.digest_encryption_algorithm = self.data["signature_algorithm"][ - "algorithm" - ].native - self.encrypted_digest = self.data["signature"].native - self.digest_algorithm = _get_digest_algorithm( - self.data["digest_algorithm"], location="SignerInfo.digestAlgorithm" - ) - - # Parse the content of the authenticated attributes - # - The messageDigest - self.message_digest = None - if "message_digest" in self.authenticated_attributes: - if len(self.authenticated_attributes["message_digest"]) != 1: - raise SignerInfoParseError( - "Only one Digest expected in SignerInfo.authenticatedAttributes" - ) - - self.message_digest = self.authenticated_attributes["message_digest"][ - 0 - ].native - - # - The contentType - self.content_type = None - if "content_type" in self.authenticated_attributes: - if len(self.authenticated_attributes["content_type"]) != 1: - raise SignerInfoParseError( - "Only one ContentType expected in" - " SignerInfo.authenticatedAttributes" - ) - - self.content_type = self.authenticated_attributes["content_type"][0].native - - if ( - self._expected_content_type is not None - and self.content_type != self._expected_content_type - ): - raise SignerInfoParseError( - "Unexpected content type for SignerInfo, expected" - f" {self._expected_content_type}, got" - f" {self.content_type}" - ) - - # - The signingTime (used by countersigner) - self.signing_time = None - if "signing_time" in self.authenticated_attributes: - if len(self.authenticated_attributes["signing_time"]) != 1: - raise SignerInfoParseError( - "Only one SigningTime expected in" - " SignerInfo.authenticatedAttributes" - ) - - self.signing_time = self.authenticated_attributes["signing_time"][0].native - - # - The countersigner - self.countersigner = None - if "counter_signature" in self.unauthenticated_attributes: - if len(self.unauthenticated_attributes["counter_signature"]) != 1: - raise SignerInfoParseError( - "Only one CountersignInfo expected in" - " SignerInfo.unauthenticatedAttributes" - ) + # Check if all required attributes are defined. + if not all( + x in self.authenticated_attributes + for x in self._required_authenticated_attributes + ): + raise SignerInfoParseError( + "Not all required attributes found." + f" Required: {self._required_authenticated_attributes};" + f" Found: {self.authenticated_attributes}" + ) - assert self._countersigner_class is not None and not isinstance( - self._countersigner_class, str - ) # typing - self.countersigner = self._countersigner_class( - self.unauthenticated_attributes["counter_signature"][0] + # Check that any defined singular authenticated attribute, is only present + # once. + for attribute in self._singular_authenticated_attributes: + if attribute in self.authenticated_attributes: + if len(self.authenticated_attributes[attribute]) != 1: + raise SignerInfoParseError( + f"Only one {attribute} expected in" + f" SignerInfo.authenticatedAttributes, found" + f" {len(self.authenticated_attributes[attribute])}" + ) + + # Check that any defined singular unauthenticated attribute, is only present + # once. + for attribute in self._singular_unauthenticated_attributes: + if attribute in self.unauthenticated_attributes: + if len(self.unauthenticated_attributes[attribute]) != 1: + raise SignerInfoParseError( + f"Only one {attribute} expected in" + f" SignerInfo.unauthenticatedAttributes, found" + f" {len(self.unauthenticated_attributes[attribute])}" + ) + + # Verify the content type against the expected content type + if ( + "content_type" in self.authenticated_attributes + and self._expected_content_type is not None + and self.content_type != self._expected_content_type + ): + raise SignerInfoParseError( + "Unexpected content type for SignerInfo, expected" + f" {self._expected_content_type}, got" + f" {self.content_type}" ) - def check_message_digest(self, data: bytes) -> bool: - """Given the data, returns whether the hash_algorithm and message_digest match - the data provided. + @property + def issuer(self) -> CertificateName: + """The issuer of the SignerInfo, i.e. the certificate of the signer of the + SignedData object. """ + return CertificateName(self.asn1["sid"].chosen["issuer"]) - auth_attr_hash = self.digest_algorithm() - auth_attr_hash.update(data) - return auth_attr_hash.digest() == self.message_digest + @property + def serial_number(self) -> int: + """The serial number as specified by the issuer.""" + return cast(int, self.asn1["sid"].chosen["serial_number"].native) @classmethod - def _parse_attributes( - cls, data: cms.CMSAttributes, required: Iterable[str] = () - ) -> dict[str, list[Asn1Value]]: + def _parse_attributes(cls, data: cms.CMSAttributes) -> dict[str, list[Asn1Value]]: """Given a set of Attributes, parses them and returns them as a dict :param data: The authenticatedAttributes or unauthenticatedAttributes to process - :param required: A list of required attributes """ + return {attr["type"].native: list(attr["values"]) for attr in data} - result = {attr["type"].native: list(attr["values"]) for attr in data} - - if not all(x in result for x in required): - raise SignerInfoParseError( - "Not all required attributes found." - f" Required: {required};" - f" Found: {result}" - ) + @property + def authenticated_attributes(self) -> dict[str, list[Asn1Value]]: + """A SignerInfo object can contain both signed and unsigned attributes. These + contain additional information about the signature, but also the content type + and message digest. The difference between signed and unsigned is that unsigned + attributes are not validated. - return result + The type of this attribute is a dictionary. You should not need to access this + value directly, rather using one of the attributes listed below. + """ + return self._parse_attributes(self.asn1["signed_attrs"]) @classmethod def _encode_attributes(cls, data: cms.CMSAttributes) -> bytes: @@ -268,6 +169,101 @@ def _encode_attributes(cls, data: cms.CMSAttributes) -> bytes: new_attrs = type(data)(contents=data.contents) return cast(bytes, new_attrs.dump()) + @property + def _encoded_authenticated_attributes(self) -> bytes: + return self._encode_attributes(self.asn1["signed_attrs"]) + + @property + def unauthenticated_attributes(self) -> dict[str, list[Asn1Value]]: + """A SignerInfo object can contain both signed and unsigned attributes. These + contain additional information about the signature, but also the content type + and message digest. The difference between signed and unsigned is that unsigned + attributes are not validated. + + The type of this attribute is a dictionary. You should not need to access this + value directly, rather using one of the attributes listed below. + """ + return self._parse_attributes(self.asn1["unsigned_attrs"]) + + @property + def digest_encryption_algorithm(self) -> str: + """This is the algorithm used for signing the digest with the signer's key.""" + return cast(str, self.asn1["signature_algorithm"]["algorithm"].native) + + @property + def encrypted_digest(self) -> bytes: + """The result of encrypting the message digest and associated information with + the signer's private key.""" + return cast(bytes, self.asn1["signature"].native) + + @property + def digest_algorithm(self) -> HashFunction: + """The digest algorithm, i.e. the hash algorithm, under which the content and + the authenticated attributes are signed. + """ + return _get_digest_algorithm( + self.asn1["digest_algorithm"], location="SignerInfo.digestAlgorithm" + ) + + ### parsed attributes + @property + def message_digest(self) -> bytes | None: + """This is an authenticated attribute, containing the signed digest of + the data. + """ + if "message_digest" in self.authenticated_attributes: + return cast( + bytes, self.authenticated_attributes["message_digest"][0].native + ) + return None + + @property + def content_type(self) -> str | None: + """This is an authenticated attribute, containing the content type of the + content being signed. + """ + if "content_type" in self.authenticated_attributes: + return cast(str, self.authenticated_attributes["content_type"][0].native) + return None + + @property + def signing_time(self) -> datetime.datetime | None: + """This is an authenticated attribute, containing the timestamp of signing. Note + that this should only be present in countersigner objects. + """ + if "signing_time" in self.authenticated_attributes: + return cast( + datetime.datetime, + self.authenticated_attributes["signing_time"][0].native, + ) + return None + + @property + def countersigner(self) -> CounterSignerInfo | None: + """This is an unauthenticated attribute, containing the countersigner of the + SignerInfo. + """ + if "counter_signature" in self.unauthenticated_attributes: + assert self._countersigner_class is not None and not isinstance( + self._countersigner_class, str + ) # typing + return self._countersigner_class( + cast( + cms.SignerInfo, + self.unauthenticated_attributes["counter_signature"][0], + ) + ) + return None + + def check_message_digest(self, data: bytes) -> bool: + """Given the data, returns whether the hash_algorithm and message_digest match + the data provided. + """ + + auth_attr_hash = self.digest_algorithm() + auth_attr_hash.update(data) + return auth_attr_hash.digest() == self.message_digest + def _verify_issuer_signature( self, issuer: Certificate, context: VerificationContext ) -> None: diff --git a/signify/x509/certificates.py b/signify/x509/certificates.py index 2027c12..1652086 100644 --- a/signify/x509/certificates.py +++ b/signify/x509/certificates.py @@ -25,94 +25,73 @@ class Certificate: - """Representation of a Certificate. It is built from an ASN.1 structure. + """Representation of a Certificate. It is built from an ASN.1 structure.""" - .. attribute:: data + asn1: asn1crypto.x509.Certificate - The underlying ASN.1 data object - - .. attribute:: signature_algorithm - signature_value - subject_public_algorithm - subject_public_key - - These values are considered part of the certificate, but not - fully parsed. - - .. attribute:: version - - This is the version of the certificate - - .. attribute:: serial_number - - The full integer serial number of the certificate - - .. attribute:: issuer - subject - - The :class:`CertificateName` for the issuer and subject. - - .. attribute:: valid_from - valid_to - - The datetime objects between which the certificate is valid. - - .. attribute:: extensions - - This is a list of extension objects. - """ - - signature_algorithm: Any - signature_value: Any - version: str - serial_number: int - issuer: CertificateName - valid_from: datetime.datetime - valid_to: datetime.datetime - subject: CertificateName - subject_public_algorithm: AlgorithmIdentifier - subject_public_key: bytes - extensions: dict[str, Any] - - def __init__( - self, - data: asn1crypto.x509.Certificate | cms.CertificateChoices, - ): + def __init__(self, asn1: asn1crypto.x509.Certificate | cms.CertificateChoices): """ - - :type data: asn1.pkcs7.ExtendedCertificateOrCertificate or - asn1.x509.Certificate or asn1.x509.TBSCertificate - :param data: The ASN.1 structure + :param asn1: The ASN.1 structure """ - self.data = data - self._parse() + self.asn1 = asn1 - def _parse(self) -> None: - if isinstance(self.data, cms.ExtendedCertificate): + if isinstance(self.asn1, cms.ExtendedCertificate): raise NotImplementedError( "Support for extendedCertificate is not implemented" ) - - if isinstance(self.data, cms.CertificateChoices): - if self.data.name != "certificate": + elif isinstance(self.asn1, cms.CertificateChoices): + if self.asn1.name != "certificate": raise NotImplementedError( - f"This is not a certificate, but a {self.data.name}" + f"This is not a certificate, but a {self.asn1.name}" ) - self.data = self.data.chosen + self.asn1 = self.asn1.chosen + + @property + def signature_algorithm(self) -> str: + """These values are considered part of the certificate, but not fully parsed.""" + return cast(str, self.asn1.signature_algo) + + @property + def signature_value(self) -> bytes: + """These values are considered part of the certificate, but not fully parsed.""" + return cast(bytes, self.asn1.signature) + + @property + def version(self) -> str: + """This is the version of the certificate""" + return cast(str, self.asn1["tbs_certificate"]["version"].native) + + @property + def serial_number(self) -> int: + """The full integer serial number of the certificate""" + return cast(int, self.asn1.serial_number) + + @property + def issuer(self) -> CertificateName: + """The :class:`CertificateName` for the issuer.""" + return CertificateName(self.asn1.issuer) - self.signature_algorithm = self.data["signature_algorithm"].native - self.signature_value = self.data["signature_value"].native - tbs_certificate = self.data["tbs_certificate"] + @property + def subject(self) -> CertificateName: + """The :class:`CertificateName` for the subject.""" + return CertificateName(self.asn1.subject) - self.version = tbs_certificate["version"].native - self.serial_number = tbs_certificate["serial_number"].native - self.issuer = CertificateName(tbs_certificate["issuer"]) - self.valid_from = tbs_certificate["validity"]["not_before"].native - self.valid_to = tbs_certificate["validity"]["not_after"].native - self.subject = CertificateName(tbs_certificate["subject"]) + @property + def valid_from(self) -> datetime.datetime: + """The datetime objects between which the certificate is valid.""" + return cast(datetime.datetime, self.asn1.not_valid_before) - self.subject_public_algorithm = AlgorithmIdentifier( + @property + def valid_to(self) -> datetime.datetime: + """The datetime objects between which the certificate is valid.""" + return cast(datetime.datetime, self.asn1.not_valid_after) + + @property + def subject_public_algorithm(self) -> AlgorithmIdentifier: + """These values are considered part of the certificate, but not fully parsed.""" + tbs_certificate = self.asn1["tbs_certificate"] + return AlgorithmIdentifier( algorithm=tbs_certificate["subject_public_key_info"]["algorithm"][ "algorithm" ].native, @@ -120,16 +99,26 @@ def _parse(self) -> None: "parameters" ].native, ) - self.subject_public_key = tbs_certificate["subject_public_key_info"][ - "public_key" - ].dump() - self.extensions = {} + @property + def subject_public_key(self) -> bytes: + """These values are considered part of the certificate, but not fully parsed.""" + return cast( + bytes, + self.asn1["tbs_certificate"]["subject_public_key_info"][ + "public_key" + ].dump(), + ) + + @property + def extensions(self) -> dict[str, Any]: + """This is a list of extension objects.""" + result = {} + tbs_certificate = self.asn1["tbs_certificate"] if tbs_certificate["extensions"].native is not None: for extension in tbs_certificate["extensions"]: - self.extensions[extension["extn_id"].native] = extension[ - "extn_value" - ].native + result[extension["extn_id"].native] = extension["extn_value"].native + return result def __str__(self) -> str: return ( @@ -179,15 +168,15 @@ def from_pems(cls, content: bytes) -> Iterator[Certificate]: @cached_property def to_der(self) -> bytes: """Returns the DER-encoded data from this certificate.""" - return cast(bytes, self.data.dump()) + return cast(bytes, self.asn1.dump()) @cached_property def sha256_fingerprint(self) -> str: - return cast(str, self.data.sha256_fingerprint).replace(" ", "").lower() + return cast(str, self.asn1.sha256_fingerprint).replace(" ", "").lower() @cached_property def sha1_fingerprint(self) -> str: - return cast(str, self.data.sha1_fingerprint).replace(" ", "").lower() + return cast(str, self.asn1.sha1_fingerprint).replace(" ", "").lower() def verify_signature( self, @@ -213,7 +202,7 @@ def verify_signature( https://mta.openssl.org/pipermail/openssl-users/2015-September/002053.html """ - public_key = asymmetric.load_public_key(self.data.public_key) + public_key = asymmetric.load_public_key(self.asn1.public_key) if public_key.algorithm == "rsa": verify_func = asymmetric.rsa_pkcs1v15_verify elif public_key.algorithm == "dsa": @@ -270,14 +259,14 @@ class CertificateName: "1.2.840.113549.1.9.1": "EMAIL", # emailaddress } - def __init__(self, data: asn1crypto.x509.Name | asn1crypto.x509.GeneralName): - if isinstance(data, asn1crypto.x509.GeneralName): - if data.name != "directory_name": + def __init__(self, asn1: asn1crypto.x509.Name | asn1crypto.x509.GeneralName): + if isinstance(asn1, asn1crypto.x509.GeneralName): + if asn1.name != "directory_name": raise NotImplementedError( - f"CertificateNames of type {data.name} not supported" + f"CertificateNames of type {asn1.name} not supported" ) - data = data.chosen - self.data = data + asn1 = asn1.chosen + self.asn1 = asn1 def __eq__(self, other: object) -> bool: return isinstance(other, CertificateName) and self.rdns == other.rdns @@ -330,7 +319,7 @@ def get_components( if not provided, yields tuples of (type, value) """ - for n in list(self.data.chosen)[::-1]: + for n in list(self.asn1.chosen)[::-1]: type_value = n[0] # get the AttributeTypeAndValue object type = self.OID_TO_RDN.get( diff --git a/signify/x509/context.py b/signify/x509/context.py index 1350799..51c7cb4 100644 --- a/signify/x509/context.py +++ b/signify/x509/context.py @@ -322,7 +322,7 @@ def verify(self, certificate: Certificate) -> list[Certificate]: # we keep track of our asn1 objects to make sure we return Certificate objects # when we're done - to_check_asn1cert = certificate.data + to_check_asn1cert = certificate.asn1 all_certs = {to_check_asn1cert: certificate} # we need to get lists of our intermediates and trusted certificates @@ -330,7 +330,7 @@ def verify(self, certificate: Certificate) -> list[Certificate]: trust_roots: list[asn1crypto.x509.Certificate] = [] for store in self.stores: for cert in store: - asn1cert = cert.data + asn1cert = cert.asn1 # we short-circuit the check here to ensure we do not check too much # possibilities (trust_roots if store.trusted else intermediates).append(asn1cert)