VYPR
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.

PackageAffected versionsPatched versions
signifyPyPI
< 0.9.20.9.2

Affected products

1
  • cpe:2.3:a:ralphje:signify:*:*:*:*:*:python:*:*
    Range: <0.9.2

Patches

1
64f21c0cc06c

Check key usage when present, refs #60

https://github.com/ralphje/signifyRalph BroeninkDec 25, 2025via ghsa
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

News mentions

1