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
CertificateTrustListor 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
AuthenticodeSignatureobjects relevant for this file. Seeiter_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
AuthenticodeSignatureobjects embedded in this Authenticode-signed file.- Parameters:
include_nested – If
True, will also iterate over all nested SignedData structuresignore_parse_errors –
Indicates how to handle
ParseErrorthat may be raised while fetching embeddedAuthenticodeSignaturestructures.When
True, which is the default and seems to be how Windows handles this as well, this will fetch as many validAuthenticodeSignaturestructures until an exception occurs.Note that this will also silence the
ParseErrorthat occurs when there’s no validAuthenticodeSignatureto fetch.When
False, this will raise theParseErroras 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
IndirectDataobjects. The arguments are the same as foriter_signatures().Using
verify_signature()to retrieve the signature, with itsIndirectDataand 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
AuthenticodeSignatureobjects embedded in this Authenticode-signed file andCertificateTrustListobjects with signatures for this file.- Parameters:
signature_types –
Defines which signatures are allowed:
embeddedwill only consider signatures embedded in the filecatalogwill only consider catalog files added throughadd_catalog(), excluding those where the current file is not listed in the catalogcatalog+same ascatalog, but including those catalog files where the current file is not listed in the catalog, mostly affectingmulti_verify_modewhen set toallallcombinesembeddedwithcatalogall+combinesembeddedwithcatalog+
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
catalogorall. 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
AuthenticodeSignatureobjects embedded in this Authenticode-signed file andCertificateTrustListobjects with signatures for this file. Seeiter_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
AuthenticodeSignatureobjects in this file. Can be:any(default) to indicate that any of the signatures must validate correctly.firstto 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 signaturesallto indicate that all signatures must verifybestto 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_errors –
iter_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
IndirectDatareferencing this data. ReturnNonewhen 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
IndirectDatamatches this file.This method is called from
AuthenticodeSignature.verify()forIndirectDataobjects contained in anAuthenticodeSignature, butAuthenticodeSignature.verify()does perform some additional checks on the used hashing algorithms. Useverify_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
IndirectDataobject and theSignedDataverification result (i.e. the validation chain).If the provided object is
AuthenticodeSignature, the verification is very straightforward, usingsigned_data.indirect_datafor validation.If the provided object is
CertificateTrustList, the appropriate subject is located, and itsIndirectDatais 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.