Authenticode

The Authenticode support of Signify allows you to easily verify a PE or MSI File’s Authenticode signature:

with open("file.exe", "rb") as f:
    signed_file = AuthenticodeFile.from_stream(f)
    signed_file.verify()

This method will raise an error if it is invalid. A simpler API is also available, allowing you to interpret the error if one happens:

with open("file.exe", "rb") as f:
    signed_file = AuthenticodeFile.from_stream(f)
    status, err = signed_file.explain_verify()

if status != AuthenticodeVerificationResult.OK:
    print(f"Invalid: {err}")

If you need to get more information about the signature, you can use this:

with open("file.exe", "rb") as f:
    signed_file = AuthenticodeFile.from_stream(f)
    for signed_data in signed_file.signatures:
        print(signed_data.signer_info.program_name)
        if signed_data.signer_info.countersigner is not None:
            print(signed_data.signer_info.countersigner.signing_time)

A more thorough example can be found in signify/authenticode/cli.py, which is also available as a authenticode command line script.

Note that the file must remain open as long as not all SignedData objects have been parsed or verified.

Authenticode overview

Most of the specification of Authenticode as applied to Portable Executables (normal Windows executables) is documented in a 2008 paper Windows Authenticode Portable Executable Signature Format and still available to download. The specification mostly follows the PKCS#7: SignedData and SignerInfo specification, although most structures have since been updated in more recent RFCs. Of particular note is that the specification defines various “must” and “must not” phrases, which has not been adhered to in more recent uses.

At its core, the paper defines how the certificate table of a PE file contains PKCS#7 SignedData objects. Note that the specification allows for multiple of such objects, perhaps including other signers or signatures. Authenticode SignedData objects contain ‘indirect data’ (microsoft_spc_indirect_data_content, OID 1.3.6.1.4.1.311.2.1.4), which (amongst others) defines the hash of the signed file.

The signed file must be hashed in particular way, as we want to make sure to exclude the signature itself from the hash, as that would alter the hash. In the case of PE files, this means that the certificate table in the data directory is ignored, as the file’s checksum. The signature is valid, in principle, if the hash we calculate is the same as in the indirect data (and the SignedData verifies as well).

Although the paper does not go into this in further detail, Subject Interface Packages (SIPs), can define a similar approach to various other file types, such as MSI files or CAB files. See Supported File Types for more information on this.

There are various other requirements, such as that the signing certificate must have the code_signing extended key usage (OID 1.3.6.1.5.5.7.3.3) and that none of the certificates in the signing chain can be untrusted. For more information about the inner workings of Microsoft’s certificate chains, see Catalog Files and Certificate Trust Lists.

See also

There are various other projects that also deal with Authenticode, which also provide useful insights. These include:

Other useful references include:

There are a few additional gotcha’s when verifying Authenticode signatures, which are not very well defined in the original specification, but we have been able to reverse-engineer or otherwise use to our advantage.

RFC3161 countersignatures

There are two types of countersignature: a regular countersignature, as used in PKCS#7, or a nested Time-Stamp Protocol response (RFC3161). This response, available as unauthenticated attribute with microsoft_time_stamp_token (OID 1.3.6.1.4.1.311.3.3.1), is added as nested authenticode.pkcs7.SignedData object.

This is transparently handled by the AuthenticodeSignature.countersigner attribute, but note that this attribute can return two different types.

Nested signatures

Instead of adding multiple signatures to the certificate table, SignedData objects can also be nested in others as unauthenticated attributes with microsoft_nested_signature (OID 1.3.6.1.4.1.311.2.4.1).

This is transparently handled by the AuthenticodeSignature.iter_recursive_nested() and AuthenticodeFile.iter_embedded_signatures() (with included_nested=True) methods.

Additional attributes and extensions

Some attributes are present on SignerInfo objects that have additional meanings:

microsoft_spc_sp_opus_info (1.3.6.1.4.1.311.2.1.12)

Contains the program name and URL

microsoft_spc_statement_type (1.3.6.1.4.1.311.2.1.11)

Defines that the key purpose is individual (1.3.6.1.4.1.311.2.1.21) or commercial (1.3.6.1.4.1.311.2.1.22), but unused in practice.

microsoft_spc_relaxed_pe_marker_check (1.3.6.1.4.1.311.2.6.1)

Purpose unknown

microsoft_platform_manifest_binary_id (1.3.6.1.4.1.311.10.3.28)

Purpose unknown

For certificates, these extensions are known:

microsoft_spc_sp_agency_info (1.3.6.1.4.1.311.2.1.10)

Purpose unknown

microsoft_spc_financial_criteria (1.3.6.1.4.1.311.2.1.27)

Purpose unknown

The following key purpose is relevant for Authenticode:

microsoft_lifetime_signing (1.3.6.1.4.1.311.10.3.13)

The certificate is only valid for it’s lifetime, and cannot be extend with a counter signature.

All these attributes and extensions are defined in the ASN.1 spec of this library, but not all of them are used.

Future work:

  • 1.3.6.1.4.1.311.2.5.1 (enhanced_hash)

Authenticode-signed File Objects

The basic interface to Authenticode-signed files is AuthenticodeFile.from_stream(). This will make sure that a concrete implementation, such as signify.authenticode.signed_file.SignedPeFile or signify.authenticode.signed_file.SignedMsiFile will be returned, implementing the same interface.

This generic interface allows access to zero or more AuthenticodeSignature objects, and allows validation of the signature.

class signify.authenticode.AuthenticodeFile

An Authenticode-signed file that is to be parsed to find the relevant sections for Authenticode parsing.

add_catalog(catalog: CertificateTrustList | BinaryIO, check: bool = False) None

Add a catalog file for validation. Catalog files can contain additional signatures for signed files. Note that get_fingerprint() must be implemented.

Parameters:
  • catalog – The catalog to add. Can be a CertificateTrustList or a file-like object opened in binary mode.

  • check

    If check is False, the catalog will be added regardless of whether the current file is actually in the file.

    If check is True, the current file will be hashed according to the hashing scheme of the catalog to verify it is contained within the catalog.

property embedded_signatures: Iterator[AuthenticodeSignature]

Returns an iterator over AuthenticodeSignature objects relevant for this file. See iter_embedded_signatures()

explain_verify(*args: Any, **kwargs: Any) tuple[AuthenticodeVerificationResult, Exception | None]

This will return a value indicating the signature status of this PE file. This will not raise an error when the verification fails, but rather indicate this through the resulting enum

Returns:

The verification result, and the exception containing more details (if available or None)

classmethod from_stream(file_obj: BinaryIO, file_name: str | None = None, *, allow_flat: bool = False) AuthenticodeFile

This initializer will return a concrete subclass that is compatible with the provided file object, and otherwise throw an error.

Parameters:
  • file_obj – The file-like object to read from

  • file_name – The optional argument can be used to specify the file name.

  • allow_flat – Indicates whether FlatFile is allowed as a subclass. As this matches anything, this will always be available.

get_fingerprint(digest_algorithm: HashFunction) bytes

Gets the fingerprint for this file

get_fingerprints(*digest_algorithms: HashFunction) dict[str, bytes]

Calculate multiple fingerprints at once.

This can sometimes provide a small speed-up by pre-calculating all hashes.

iter_embedded_signatures(*, include_nested: bool = True, ignore_parse_errors: bool = True) Iterator[AuthenticodeSignature]

Returns an iterator over AuthenticodeSignature objects embedded in this Authenticode-signed file.

Parameters:
  • include_nested – If True, will also iterate over all nested SignedData structures

  • ignore_parse_errors

    Indicates how to handle ParseError that may be raised while fetching embedded AuthenticodeSignature structures.

    When True, which is the default and seems to be how Windows handles this as well, this will fetch as many valid AuthenticodeSignature structures until an exception occurs.

    Note that this will also silence the ParseError that occurs when there’s no valid AuthenticodeSignature to fetch.

    When False, this will raise the ParseError as soon as one occurs.

Raises:
  • ParseError – For parse errors in the signed file

  • signify.authenticode.AuthenticodeParseError – For parse errors in the SignedData

iter_indirect_data(**kwargs: Any) Iterable[tuple[AuthenticodeSignature | CertificateTrustList, IndirectData]]

Simple helper method that allows iterating over all signatures for this Authenticode-signed file with their contained IndirectData objects. The arguments are the same as for iter_signatures().

Using verify_signature() to retrieve the signature, with its IndirectData and verification result, is often the more appropriate method to use.

iter_signatures(*, signature_types: Literal['all', 'all+', 'catalog', 'catalog+', 'embedded'] = 'all', expected_hashes: dict[str, bytes] | None = None, include_nested: bool = True, ignore_parse_errors: bool = True) Iterator[AuthenticodeSignature | CertificateTrustList]

Returns an iterator over AuthenticodeSignature objects embedded in this Authenticode-signed file and CertificateTrustList objects with signatures for this file.

Parameters:
  • signature_types

    Defines which signatures are allowed:

    • embedded will only consider signatures embedded in the file

    • catalog will only consider catalog files added through add_catalog(), excluding those where the current file is not listed in the catalog

    • catalog+ same as catalog, but including those catalog files where the current file is not listed in the catalog, mostly affecting multi_verify_mode when set to all

    • all combines embedded with catalog

    • all+ combines embedded with catalog+

    Embedded signatures are evaluated before catalog signatures.

  • expected_hashes – When provided, should be a mapping of hash names to digests. This is used when using catalog or all. The dictionary is updated to reflect newly-retrieved hashes.

  • include_nested – See iter_embedded_signatures()

  • ignore_parse_errors – See iter_embedded_signatures()

Raises:
  • AuthenticodeVerificationError – when the verification failed

  • ParseError – for parse errors in the signed file

property signatures: Iterator[AuthenticodeSignature | CertificateTrustList]

Returns an iterator over AuthenticodeSignature objects embedded in this Authenticode-signed file and CertificateTrustList objects with signatures for this file. See iter_signatures()

verify(*, multi_verify_mode: Literal['any', 'first', 'all', 'best'] = 'any', signature_types: Literal['all', 'all+', 'catalog', 'catalog+', 'embedded'] = 'all', expected_hashes: dict[str, bytes] | None = None, ignore_parse_errors: bool = True, **kwargs: Any) list[tuple[SignedData, IndirectData | None, Iterable[list[Certificate]]]]

Verifies the SignedData structures. This is a little bit more efficient than calling all verify-methods separately.

Parameters:
  • multi_verify_mode

    Indicates how to verify when there are multiple AuthenticodeSignature objects in this file. Can be:

    • any (default) to indicate that any of the signatures must validate correctly.

    • first to indicate that the first signature must verify correctly (the default of tools such as sigcheck.exe); this is done in file order, followed by any provided catalog signatures

    • all to indicate that all signatures must verify

    • best to indicate that the signature using the best hashing algorithm must verify (e.g. if both SHA-1 and SHA-256 are present, only SHA-256 is checked); if multiple signatures exist with the same algorithm, any may verify

    This argument has no effect when only one signature is present.

  • signature_types – See iter_signatures(). Note that this affects the multi_verify_mode as well. Embedded signatures are evaluated before catalog signatures.

  • expected_hashes – When provided, should be a mapping of hash names to digests. This could speed up the verification process.

  • ignore_parse_errorsiter_embedded_signatures()

Returns:

the used structure(s) in validation, as a list of tuples, in the form (signed data object, indirect data object, certificate chain)

Raises:
  • AuthenticodeVerificationError – when the verification failed

  • ParseError – for parse errors in the signed file

verify_additional_hashes(indirect_data: IndirectData) None

Verifies additional hashes that may be present in the IndirectData referencing this data. Return None when the verification succeeds, or raises an error otherwise.

The default implementation is to do nothing.

verify_indirect_data(indirect_data: IndirectData, *, expected_hash: bytes | None = None, verify_additional_hashes: bool = True) None

Verifies the provided IndirectData against the current file. This does not verify that the signature itself is properly signed, but verifies that the IndirectData matches this file.

This method is called from AuthenticodeSignature.verify() for IndirectData objects contained in an AuthenticodeSignature, but AuthenticodeSignature.verify() does perform some additional checks on the used hashing algorithms. Use verify_signature() to call the method for verification.

If no expected hash is provided, the hash is calculated by calling get_fingerprint() with the appropriate algorithm.

Then, this function will simply verify that the expected hash matches that in the provided IndirectData.

Finally, this function calls verify_additional_hashes() if requested.

Parameters:
  • expected_hash – The expected hash digest of the AuthenticodeFile.

  • verify_additional_hashes – Defines whether additional hashes, should be verified, such as page hashes for PE files and extended digests for MSI files.

verify_signature(signed_data: AuthenticodeSignature | CertificateTrustList, *, expected_hashes: dict[str, bytes], **kwargs: Any) tuple[SignedData, IndirectData | None, Iterable[list[Certificate]]]

Verifies a SignedData object, returning the object itself, the IndirectData object and the SignedData verification result (i.e. the validation chain).

If the provided object is AuthenticodeSignature, the verification is very straightforward, using signed_data.indirect_data for validation.

If the provided object is CertificateTrustList, the appropriate subject is located, and its IndirectData is used for validation.

class signify.authenticode.AuthenticodeVerificationResult(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

This represents the result of an Authenticode verification. If everything is OK, it will equal to AuthenticodeVerificationResult.OK, otherwise one of the other enum items will be returned. Remember that onl the first exception is processed - there may be more wrong.

CERTIFICATE_ERROR = 6

An error occurred during the processing of a certificate (e.g. during chain building), or when verifying the certificate’s signature.

COUNTERSIGNER_ERROR = 9

Something went wrong when verifying the countersignature.

INCONSISTENT_DIGEST_ALGORITHM = 7

A highly specific error raised when different digest algorithms are used in SignedData, SpcInfo or SignerInfo.

INVALID_ADDITIONAL_HASH = 10

The additional file hash, such as the page hash for PE files, or the extended digest for MSI files, does not match the calculated hash.

INVALID_DIGEST = 8

The verified digest does not match the calculated digest of the file. This is a tell-tale sign that the file may have been tampered with.

NOT_SIGNED = 2

The provided PE file is not signed.

OK = 1

The signature is valid.

PARSE_ERROR = 3

The Authenticode signature could not be parsed.

UNKNOWN_ERROR = 5

An unknown error occurred during parsing or verifying.

VERIFY_ERROR = 4

The Authenticode signature could not be verified. This is a more generic error than other possible statuses and is used as a catch-all.