High severity8.8NVD Advisory· Published Mar 25, 2026· Updated Apr 1, 2026
CVE-2025-70887
CVE-2025-70887
Description
An issue in ralphje Signify before v.0.9.2 allows a remote attacker to escalate privileges via the signed_data.py and the context.py components
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
signifyPyPI | < 0.9.2 | 0.9.2 |
Affected products
1Patches
164f21c0cc06cCheck key usage when present, refs #60
4 files changed · +75 −6
signify/authenticode/signed_data.py+24 −0 modified@@ -161,6 +161,7 @@ def verify( # type: ignore[override] *, expected_hash: bytes | None = None, verify_additional_hashes: bool = True, + strict_validation: bool = True, cs_verification_context: VerificationContext | None = None, trusted_certificate_store: CertificateStore = TRUSTED_CERTIFICATE_STORE, verification_context_kwargs: dict[str, Any] | None = None, @@ -180,6 +181,11 @@ def verify( # type: ignore[override] :param verify_additional_hashes: Defines whether additional hashes, should be verified, such as page hashes for PE files and extended digests for MSI files. + :param strict_validation: Defines whether we verify signatures more strictly. + For now, we only verify key usages more strictly, i.e. require the + extendedKeyUsage extension to be present, and verify the keyUsage extension + when it is present. If :const:`False`, we skip these checks, aligning with + what Windows seems to be doing. :param verification_context: See :meth:`SignedData.verify` :param cs_verification_context: See :meth:`SignedData.verify` :param trusted_certificate_store: See :meth:`SignedData.verify` @@ -206,6 +212,24 @@ def verify( # type: ignore[override] verify_additional_hashes=verify_additional_hashes, ) + if verification_context_kwargs is None: + verification_context_kwargs = {} + + # The specification does not actually require this keyUsage to be set, + # and Windows does not actually verify that this is properly set, even if it is + # marked critical. This appears to be a bug with Windows itself. We therefore + # only verify the key usage when we want to be strict about it. + if strict_validation: + verification_context_kwargs.setdefault("key_usages", ["digital_signature"]) + verification_context_kwargs.setdefault("optional_ku", True) + # The Authenticode specification requires the code_signing EKU to be present + # (or the entire chain must not contain any EKU). Windows does not actually + # care whether the EKU is present, even if the chain does. We choose to verify + # the EKU when it is present, and ignore failures when it is not, aligning with + # what Windows does. In strict mode, we require the EKU to be present and set. + if strict_validation: + verification_context_kwargs.setdefault("optional_eku", False) + try: return super().verify( verification_context=verification_context,
signify/x509/context.py+35 −6 modified@@ -266,8 +266,9 @@ def __init__( *stores: CertificateStore, timestamp: datetime.datetime | None = None, key_usages: Iterable[str] | None = None, + optional_ku: Literal["critical"] | bool = True, extended_key_usages: Iterable[str] | None = None, - optional_eku: bool = True, + optional_eku: Literal["critical"] | bool = True, allow_legacy: bool = True, revocation_mode: Literal["soft-fail", "hard-fail", "require"] = "soft-fail", allow_fetching: bool = False, @@ -284,10 +285,20 @@ def __init__( current time is used. Must be a timezone-aware timestamp. :param key_usages: An iterable with the keyUsages to check for. For valid options, see :meth:`certvalidator.CertificateValidator.validate_usage` + :param optional_ku: If :const:`True`, sets the ``key_usages`` as optionally + present in the certificates, i.e. the certificate successfully validates + when the keyUsage extension is missing from the certificate. When + :const:`False`, the ``key_usages`` must be present. When ``critical``, the + key usage is considered validly absent when the keyUsage extension is not + marked as critical. :param extended_key_usages: An iterable with the EKU's to check for. See :meth:`certvalidator.CertificateValidator.validate_usage` :param optional_eku: If :const:`True`, sets the ``extended_key_usages`` as - optionally present in the certificates. + optionally present in the certificates, i.e. the certificate validates + successfully validates when the extendedKeyUsage extension is missing from + the certificate. When :const:`False`, the ``extended_key_usages`` must be + present. When ``critical``, the extended key usage is considered validly + absent when the extendedKeyUsage extension is not marked as critical. :param allow_legacy: If :const:`True`, allows chain verification if the signature hash algorithm is very old (e.g. MD2). Additionally, allows the verification of encrypted hashes in :meth:`Certificate.verify_signature` @@ -309,6 +320,7 @@ def __init__( self.stores = list(stores) self.timestamp = timestamp self.key_usages = key_usages + self.optional_ku = optional_ku self.extended_key_usages = extended_key_usages self.optional_eku = optional_eku self.allow_legacy = allow_legacy @@ -438,14 +450,31 @@ def verify(self, certificate: Certificate) -> list[Certificate]: validation_context=context, ) - # verify the chain + # Verify the chain and their usage. + # Do not verify key_usage if optional_ku is set and the certificate does not + # provide a key usage. This is because the validator does not handle this case. try: chain = validator.validate_usage( - key_usage=set(self.key_usages) if self.key_usages else set(), + key_usage=( + set(self.key_usages) + if self.key_usages + and (not self.optional_ku or to_check_asn1cert.key_usage_value) + and ( + self.optional_ku != "critical" + or "key_usage" in to_check_asn1cert.critical_extensions + ) + else set() + ), extended_key_usage=( - set(self.extended_key_usages) if self.extended_key_usages else set() + set(self.extended_key_usages) + if self.extended_key_usages + and ( + self.optional_eku != "critical" + or "extended_key_usage" in to_check_asn1cert.critical_extensions + ) + else set() ), - extended_optional=self.optional_eku, + extended_optional=bool(self.optional_eku), ) except Exception as e: raise CertificateVerificationError(
tests/authenticode/file_types/test_pe.py+16 −0 modified@@ -65,6 +65,8 @@ def test_valid_signature(filename): "19e818d0da361c4feedd456fca63d68d4b024fbbd3d9265f606076c7ee72e8f8.ViR", # This sample is expired and revoked "jameslth", + # This sample has an invalid keyUsage + "17ad1735a13898c978cc6bbe6b2056cb8471329b", ], ) def test_invalid_signature(filename): @@ -324,3 +326,17 @@ def test_pe_sample_with_catalog(): pefile.add_catalog(cat) assert len(list(pefile.signatures)) == 2 pefile.verify(signature_types="embedded") + + +def test_17ad_valid_without_strict(): + """test whether the 17ad sample with incorrect keyUsage is valid when not performing + strict validation + """ + with open_test_data("17ad1735a13898c978cc6bbe6b2056cb8471329b") as f: + pefile = SignedPEFile(f) + pefile.verify( + strict_validation=False, + verification_context_kwargs={ + "timestamp": datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) + }, + )
tests/test_data/17ad1735a13898c978cc6bbe6b2056cb8471329b+0 −0 added
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/ralphje/signify/commit/64f21c0cc06cea0536370686ca3ba7a01e4adaa8nvdPatchWEB
- github.com/advisories/GHSA-p4hh-mq57-gq8xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-70887ghsaADVISORY
- github.com/mtrojnar/osslsigncode/issues/475nvdNot ApplicableWEB
- github.com/mtrojnar/osslsigncode/pull/477nvdNot ApplicableWEB
- github.com/mtrojnar/osslsigncode/releases/tag/2.11nvdNot ApplicableWEB
- github.com/ralphje/signify/issues/60nvdIssue TrackingWEB
News mentions
1- After Mythos: New Playbooks For a Zero-Window EraThe Hacker News · Apr 28, 2026