diff --git a/docs/changelog.rst b/docs/changelog.rst index 41ae8a0..a91ae47 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,14 +4,29 @@ This page contains the most significant changes in Signify between each release. v0.7.0 (unreleased) ------------------- -* Remove dependency of ``pyasn1`` and ``pyasn1-modules`` entirely to provide more robust parsing. The replacement, - ``asn1crypto``, was already a dependency of this project, so we are mostly slimming down. This does have a serious - impact if you use certain functions to deeply inspect the original data (as all these structures have now changed) - and on some parts of the API to better align with the new dependency. Most notably, all OIDs are now strings, - rather than integer tuples, and references to attributes and subclasses are now strings as well (such as in - attribute lists). - -* Add support for SignedData versions other than v1 +* Remove dependency of ``pyasn1`` and ``pyasn1-modules`` entirely to provide more robust + parsing of ASN.1 structures, adding the ability to parse structures independent of + RFC version. Certain bugs bugs we've encountered in the past, have now been resolved + as a result of this. On top of that, structures defined in the replacement, + ``asn1crypto`` are a lot more Pythonic, and parsing speed has been sliced in more + than half. +* This does have a serious impact if you use certain functions to deeply inspect the + original data (as all these structures have now changed) and on some parts of the API + to better align with the new dependency. Most notably, all OIDs are now strings, + rather than integer tuples, and references to attributes and subclasses are now + strings as well (such as in attribute lists). + +* Add support for ``SignedData`` versions other than v1 +* Add support for ``SignerInfo`` versions other than v1 +* Parse the ``SpcPeImageData`` as part of the SpcInfo. This adds the attributes + ``image_flags`` and ``image_publisher``, although this information is never used. +* Parse the ``SpcStatementType`` as part of the authenticated attributes of the + ``AuthenticodeSignerInfo``. This adds the attribute ``statement_types``, although this + information is never used. +* Parse the ``SpcFinancialCriteria`` (``microsoft_spc_financial_criteria``) and + (partially) ``SpcSpAgencyInfo`` (``microsoft_spc_sp_agency_info``) as part of the + ``extensions`` of ``Certificate``. These extensions are poorly documented, but may + provide some additional information, such as when researching CVE-2019–1388. v0.6.1 (2024-03-21) ------------------- diff --git a/signify/asn1/ctl.py b/signify/asn1/ctl.py index 2e1409c..e91521e 100644 --- a/signify/asn1/ctl.py +++ b/signify/asn1/ctl.py @@ -19,25 +19,51 @@ from asn1crypto.util import utc_with_dst from asn1crypto.x509 import Extensions, ExtKeyUsageSyntax, Time -# Based on http://download.microsoft.com/download/C/8/8/C8862966-5948-444D-87BD-07B976ADA28C/%5BMS-CAESO%5D.pdf +# Based on https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/WinArchive/%5bMS-CAESO%5d.pdf class CTLVersion(Integer): # type: ignore[misc] + """Version of the CTL structure. + + Based on `MS-CAESO + `_:: + + CTLVersion ::= INTEGER {v1(0)} + """ + _map = { 0: "v1", } class SubjectUsage(ExtKeyUsageSyntax): # type: ignore[misc] - pass + """Subject usage of the CTL structure. + + Based on `MS-CAESO + `_:: + + SubjectUsage ::= EnhancedKeyUsage + """ class ListIdentifier(OctetString): # type: ignore[misc] - pass + """List identifier of the CTL structure. + + Based on `MS-CAESO + `_:: + + ListIdentifier ::= OCTETSTRING + """ class SubjectIdentifier(OctetString): # type: ignore[misc] - pass + """Subject identifier of the CTL structure. + + Based on `MS-CAESO + `_:: + + SubjectIdentifier ::= OCTETSTRING + """ class SubjectAttributeType(ObjectIdentifier): # type: ignore[misc] @@ -57,6 +83,10 @@ class SubjectAttributeType(ObjectIdentifier): # type: ignore[misc] class SetOfSpecificOctetString(SetOf): # type: ignore[misc] + """Specific implementation of a SetOf OctetString that allows parsing directly as + a value, or as a sequence, depending on the child type. + """ + _child_spec = OctetString children: Any @@ -105,6 +135,12 @@ def set(self, value: Any) -> None: class SubjectAttribute(Sequence): # type: ignore[misc] + """Subject attributes of the trusted subject in the CTL structure. + + Based on `MS-CAESO + `_. + """ + _fields = [ ("type", SubjectAttributeType), ("values", SetOfSpecificOctetString), @@ -130,10 +166,27 @@ def _values_spec(self) -> type[Asn1Value] | None: class SubjectAttributes(SetOf): # type: ignore[misc] + """Subject attributes of the trusted subject in the CTL structure. + + Based on `MS-CAESO + `_. + """ + _child_spec = SubjectAttribute class TrustedSubject(Sequence): # type: ignore[misc] + """Trusted subject in the CTL structure. + + Based on `MS-CAESO + `_:: + + TrustedSubject ::= SEQUENCE{ + subjectIdentifier SubjectIdentifier, + subjectAttributes Attributes OPTIONAL + } + """ + _fields = [ ("subject_identifier", SubjectIdentifier), ("subject_attributes", SubjectAttributes, {"optional": True}), @@ -141,10 +194,36 @@ class TrustedSubject(Sequence): # type: ignore[misc] class TrustedSubjects(SequenceOf): # type: ignore[misc] + """Trusted subjects in the CTL structure. + + Based on `MS-CAESO + `_:: + + TrustedSubjects ::= SEQUENCE OF TrustedSubject + """ + _child_spec = TrustedSubject class CertificateTrustList(Sequence): # type: ignore[misc] + """CTL structure. + + Based on `MS-CAESO + `_:: + + CertificateTrustList ::= SEQUENCE { + version CTLVersion DEFAULT v1, + subjectUsage SubjectUsage, + listIdentifier ListIdentifier OPTIONAL, + sequenceNumber HUGEINTEGER OPTIONAL, + ctlThisUpdate ChoiceOfTime, + ctlNextUpdate ChoiceOfTime OPTIONAL, + subjectAlgorithm AlgorithmIdentifier, + trustedSubjects TrustedSubjects OPTIONAL, + ctlExtensions [0] EXPLICIT Extensions OPTIONAL + } + """ + _fields = [ ("version", CTLVersion, {"default": "v1"}), ("subject_usage", SubjectUsage), diff --git a/signify/asn1/spc.py b/signify/asn1/spc.py index 066d620..1d08a60 100755 --- a/signify/asn1/spc.py +++ b/signify/asn1/spc.py @@ -29,47 +29,48 @@ ContentInfo, ContentType, EncapsulatedContentInfo, - SetOfContentInfo, ) from asn1crypto.core import ( Any, Asn1Value, + BitString, BMPString, + Boolean, Choice, IA5String, ObjectIdentifier, OctetString, Sequence, + SequenceOf, SetOf, ) +from asn1crypto.x509 import Extension, ExtensionId +# based on https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/authenticode_pe.docx -class SpcAttributeType(ObjectIdentifier): # type: ignore[misc] - _map: dict[str, str] = {} +class SpcUuid(OctetString): # type: ignore[misc] + """SpcUuid. -class SpcAttributeTypeAndOptionalValue(Sequence): # type: ignore[misc] - _fields = [ - ("type", SpcAttributeType), - ("value", Any, {"optional": True}), - ] + Based on `Windows Authenticode Portable Executable Signature Format + `_:: - _oid_pair = ("type", "value") - _oid_specs: dict[str, type[Asn1Value]] = {} + SpcUuid ::= OCTETSTRING + """ -class SpcIndirectDataContent(Sequence): # type: ignore[misc] - _fields = [ - ("data", SpcAttributeTypeAndOptionalValue), - ("message_digest", DigestInfo), - ] - +class SpcSerializedObject(Sequence): # type: ignore[misc] + """SpcSerializedObject. -class SpcUuid(OctetString): # type: ignore[misc] - pass + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + SpcSerializedObject ::= SEQUENCE { + classId SpcUuid, + serializedData OCTETSTRING + } + """ -class SpcSerializedObject(Sequence): # type: ignore[misc] _fields = [ ("class_id", SpcUuid), ("serialized_data", OctetString), @@ -77,6 +78,17 @@ class SpcSerializedObject(Sequence): # type: ignore[misc] class SpcString(Choice): # type: ignore[misc] + """SpcString. + + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + + SpcString ::= CHOICE { + unicode [0] IMPLICIT BMPSTRING, + ascii [1] IMPLICIT IA5STRING + } + """ + _alternatives = [ ("unicode", BMPString, {"implicit": 0}), ("ascii", IA5String, {"implicit": 1}), @@ -84,14 +96,141 @@ class SpcString(Choice): # type: ignore[misc] class SpcLink(Choice): # type: ignore[misc] + """SpcLink. + + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + + SpcLink ::= CHOICE { + url [0] IMPLICIT IA5STRING, + moniker [1] IMPLICIT SpcSerializedObject, + file [2] EXPLICIT SpcString + } + """ + _alternatives = [ ("url", IA5String, {"implicit": 0}), ("moniker", SpcSerializedObject, {"implicit": 1}), - ("file", SpcString, {"implicit": 2}), + ("file", SpcString, {"explicit": 2}), + ] + + +class SpcPeImageFlags(BitString): # type: ignore[misc] + """SpcPeImageFlags. + + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + + SpcPeImageFlags ::= BIT STRING { + includeResources (0), + includeDebugInfo (1), + includeImportAddressTable (2) + } + + """ + + _map = { + 0: "include_resources", + 1: "include_debug_info", + 2: "include_import_address_table", + } + + +class SpcPeImageData(Sequence): # type: ignore[misc] + """SpcPeImageData. + + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + + SpcPeImageData ::= SEQUENCE { + flags SpcPeImageFlags DEFAULT { includeResources }, + file SpcLink + } + + Note that although this is not in the spec, it is actually explicitly tagged. + And although it is not optional in the spec, it is actually optional as shown in + the accompanying text. It is possible that the specs for + ``SpcAttributeTypeAndOptionalValue.value`` and ``SpcPeImageData.file`` were + accidentally flipped. + """ + + _fields = [ + ("flags", SpcPeImageFlags, {"default": {"include_resources"}}), + ("file", SpcLink, {"optional": True, "explicit": 0}), + ] + + +class SpcAttributeType(ObjectIdentifier): # type: ignore[misc] + """Specific attribute type of a SPC attribute.""" + + _map: dict[str, str] = { + "1.3.6.1.4.1.311.2.1.15": "microsoft_spc_pe_image_data", + } + + +class SpcAttributeTypeAndOptionalValue(Sequence): # type: ignore[misc] + """Attribute type and optional value. + + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + + SpcAttributeTypeAndOptionalValue ::= SEQUENCE { + type ObjectID, + value [0] EXPLICIT ANY OPTIONAL + } + + Note that although the spec defines this value as explicitly tagged, that's not + actually the case. It is possible that the specs for + `SpcAttributeTypeAndOptionalValue.value`` and ``SpcPeImageData.file`` were + accidentally flipped. + """ + + _fields = [ + ("type", SpcAttributeType), + ("value", Any), + ] + + _oid_pair = ("type", "value") + _oid_specs: dict[str, type[Asn1Value]] = { + "microsoft_spc_pe_image_data": SpcPeImageData, + } + + +class SpcIndirectDataContent(Sequence): # type: ignore[misc] + """Indirect data content. + + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + + SpcIndirectDataContent ::= SEQUENCE { + data SpcAttributeTypeAndOptionalValue, + messageDigest DigestInfo + } + + Note: although DigestInfo is explicitly defined in the docs, it is simply a copy of + the RFC DigestInfo. + """ + + _fields = [ + ("data", SpcAttributeTypeAndOptionalValue), + ("message_digest", DigestInfo), ] class SpcSpOpusInfo(Sequence): # type: ignore[misc] + """SpcLink. + + Based on `Windows Authenticode Portable Executable Signature Format + `_:: + + SpcLink ::= CHOICE { + url [0] IMPLICIT IA5STRING, + moniker [1] IMPLICIT SpcSerializedObject, + file [2] EXPLICIT SpcString + } + + """ + _fields = [ ("program_name", SpcString, {"optional": True, "explicit": 0}), ("more_info", SpcLink, {"optional": True, "explicit": 1}), @@ -102,8 +241,28 @@ class SetOfSpcSpOpusInfo(SetOf): # type: ignore[misc] _child_spec = SpcSpOpusInfo -class SpcStatementType(ObjectIdentifier): # type: ignore[misc] - _map: dict[str, str] = {} +class SpcStatementTypeIdentifier(ObjectIdentifier): # type: ignore[misc] + _map: dict[str, str] = { + "1.3.6.1.4.1.311.2.1.21": "microsoft_spc_individual_sp_key_purpose", + "1.3.6.1.4.1.311.2.1.22": "microsoft_spc_commercial_sp_key_purpose", + } + + +class SpcStatementType(SequenceOf): # type: ignore[misc] + """SpcStatementType. + + Based on `MS-OSHARED + `_:: + + SpcStatementType ::= SEQUENCE of OBJECT IDENTIFIER + + """ + + _child_spec = SpcStatementTypeIdentifier + + +class SetOfSpcStatementType(SetOf): # type: ignore[misc] + _child_spec = SpcStatementType ContentType._map["1.3.6.1.4.1.311.2.1.4"] = "microsoft_spc_indirect_data_content" @@ -111,5 +270,42 @@ class SpcStatementType(ObjectIdentifier): # type: ignore[misc] ContentInfo._oid_specs["microsoft_spc_indirect_data_content"] ) = SpcIndirectDataContent +CMSAttributeType._map["1.3.6.1.4.1.311.2.1.11"] = "microsoft_spc_statement_type" CMSAttributeType._map["1.3.6.1.4.1.311.2.1.12"] = "microsoft_spc_sp_opus_info" CMSAttribute._oid_specs["microsoft_spc_sp_opus_info"] = SetOfSpcSpOpusInfo +CMSAttribute._oid_specs["microsoft_spc_statement_type"] = SetOfSpcStatementType + + +# reverse-engineered certificate extensions + + +class SpcSpAgencyInfo(Sequence): # type: ignore[misc] + """Reverse-engineered extension for certificates, indicating certain information + on certificate policies. Based on + https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Security/WinTrust/struct.SPC_SP_AGENCY_INFO.html + + See also + https://sotharo-meas.medium.com/cve-2019-1388-windows-privilege-escalation-through-uac-22693fa23f5f + """ + + _fields = [ + ("policy_information", SpcLink, {"explicit": 0}), + ("policy_display_text", SpcString, {"optional": True, "explicit": 1}), + ("logo_image", OctetString, {"optional": True, "explicit": 2}), # TODO + ("logo_link", SpcLink, {"optional": True, "explicit": 3}), + ] + + +class SpcFinancialCriteria(Sequence): # type: ignore[misc] + """Reverse-engineered extension for certificates""" + + _fields = [ + ("financial_info_available", Boolean, {"default": False}), + ("meets_criteria", Boolean, {"default": False}), + ] + + +ExtensionId._map["1.3.6.1.4.1.311.2.1.10"] = "microsoft_spc_sp_agency_info" +Extension._oid_specs["microsoft_spc_sp_agency_info"] = SpcSpAgencyInfo +ExtensionId._map["1.3.6.1.4.1.311.2.1.27"] = "microsoft_spc_financial_criteria" +Extension._oid_specs["microsoft_spc_financial_criteria"] = SpcFinancialCriteria diff --git a/signify/authenticode/signed_pe.py b/signify/authenticode/signed_pe.py index 38d56ef..7179afb 100644 --- a/signify/authenticode/signed_pe.py +++ b/signify/authenticode/signed_pe.py @@ -114,8 +114,8 @@ def _parse_pe_header_locations(self) -> dict[str, RelRange]: pe_offset = struct.unpack("= self._filelength: raise SignedPEParseError( - "PE header location is beyond file boundaries (%d >= %d)" - % (pe_offset, self._filelength) + "PE header location is beyond file boundaries" + f"({pe_offset} >= {self._filelength})" ) # Check if the PE header is PE @@ -131,17 +131,16 @@ def _parse_pe_header_locations(self) -> dict[str, RelRange]: # This is not strictly a failure for windows, but such files better # be treated as generic files. They can not be carrying SignedData. raise SignedPEParseError( - "The optional header exceeds the file length (%d + %d > %d)" - % (optional_header_size, optional_header_offset, self._filelength) + f"The optional header exceeds the file length ({optional_header_size} " + f"+ {optional_header_offset} > {self._filelength})" ) if optional_header_size < 68: # We can't do authenticode-style hashing. If this is a valid binary, # which it can be, the header still does not even contain a checksum. raise SignedPEParseError( - "The optional header size is %d < 68, which is insufficient for" - " authenticode", - optional_header_size, + f"The optional header size is {optional_header_size} < 68, " + f"which is insufficient for authenticode", ) # The optional header contains the signature of the image diff --git a/signify/authenticode/structures.py b/signify/authenticode/structures.py index 82b7a8b..5eb8ef4 100644 --- a/signify/authenticode/structures.py +++ b/signify/authenticode/structures.py @@ -156,6 +156,10 @@ class AuthenticodeSignerInfo(SignerInfo): This information is extracted from the SpcSpOpusInfo authenticated attribute, containing the program's name and an URL with more information. + .. 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 @@ -193,6 +197,18 @@ class AuthenticodeSignerInfo(SignerInfo): 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 = None @@ -283,17 +299,33 @@ class SpcInfo: .. attribute:: content_type - The contenttype class + 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 """ + data: spc.SpcIndirectDataContent + content_type: str - image_data: None + image_data: spc.SpcPeImageData + image_flags: set[str] + image_publisher: str digest_algorithm: HashFunction digest: bytes @@ -304,7 +336,14 @@ def __init__(self, data: spc.SpcIndirectDataContent): def _parse(self) -> None: # The data attribute self.content_type = self.data["data"]["type"].native - self.image_data = None # TODO: not parsed + + if self.content_type != "microsoft_spc_pe_image_data": + raise AuthenticodeParseError("SpcInfo does not contain SpcPeImageData") + + self.image_data = self.data["data"]["value"] + self.image_flags = self.image_data["flags"].native + self.image_publisher = self.image_data["file"].native + self.digest_algorithm = _get_digest_algorithm( self.data["message_digest"]["digest_algorithm"], location="SpcIndirectDataContent.digestAlgorithm", @@ -351,8 +390,8 @@ def _parse(self) -> None: # signerInfos if len(self.signer_infos) != 1: raise AuthenticodeParseError( - "SignedData.signerInfos must contain exactly 1 signer, not %d" - % len(self.signer_infos) + "SignedData.signerInfos must contain exactly 1 signer," + f" not {len(self.signer_infos)}" ) self.signer_info = self.signer_infos[0] @@ -605,7 +644,7 @@ def __init__(self, data: tsp.TSTInfo): def _parse(self) -> None: if self.data["version"].native != "v1": raise AuthenticodeParseError( - "TSTInfo.version must be 1, not %d" % self.data["version"] + f"TSTInfo.version must be v1, not {self.data['version'].native}" ) self.policy = self.data["policy"].native @@ -647,8 +686,8 @@ def _parse(self) -> None: # signerInfos if len(self.signer_infos) != 1: raise AuthenticodeParseError( - "RFC3161 SignedData.signerInfos must contain exactly 1 signer, not %d" - % len(self.signer_infos) + "RFC3161 SignedData.signerInfos must contain exactly 1 signer," + f" not {len(self.signer_infos)}" ) self.signer_info = self.signer_infos[0]