# Authenticode and Catalog Files: The Crypto Foundation Under WDAC

> Every Windows trust decision -- UAC, SmartScreen, WDAC, kernel-driver loading -- bottoms out on the same PKCS#7 SignedData envelope shipped in IE 3 in August 1996. Here is the byte-level reason.

*Published: 2026-05-12*
*Canonical: https://paragmali.com/blog/authenticode-and-catalog-files-the-crypto-foundation-under-w*
*License: CC BY 4.0 - https://creativecommons.org/licenses/by/4.0/*

---
<TLDR>
Every Windows trust decision -- UAC, SmartScreen, App Control for Business (WDAC), and kernel-mode driver loading -- bottoms out on the same PKCS#7 / CMS `SignedData` envelope that Microsoft shipped with Internet Explorer 3 in August 1996. This article dissects that envelope byte by byte: the `WIN_CERTIFICATE` record inside the PE certificate table, the `SpcIndirectDataContent` attribute that signs a hash rather than a file (which is what makes catalog signing and per-page hashing possible), the RFC 3161 timestamp tokens that keep 2010 signatures verifying in 2026, and the `Microsoft Code Verification Root` kernel chain. We follow the named incidents that drove every post-2010 retrenchment -- Stuxnet, Flame, CVE-2013-3900, ShadowHammer, the 2022 Vulnerable Driver Blocklist, the 2026 Bitwarden CLI npm hijack -- and finish at the WDAC rule levels (`Publisher`, `FilePublisher`, `WHQL`) that finally surface those primitives to administrators as policy.
</TLDR>

## 1. The verified-publisher question

On 17 June 2010, Sergey Ulasen and his colleagues at VirusBlokAda in Minsk began circulating a sample of a worm that would, a month later, be named Stuxnet [@wiki-stuxnet][@stuxnet-dossier]. Two of its kernel-mode components, `mrxcls.sys` and `mrxnet.sys`, were signed -- properly, by Authenticode-conformant certificates issued to Realtek Semiconductor Corp. and shortly afterwards by JMicron Technology Corp. [@stuxnet-dossier][@archive-stuxnet-dossier-details]. The Windows kernel loaded them because the certificate chains validated. The chains validated because, cryptographically, nothing was wrong.

That sentence is the lens for everything in this article. Microsoft's code-identity system did its job exactly as designed, and a piece of state-grade sabotage walked through it. The next forty minutes of reading reconstruct what the kernel checks before loading a driver, why those checks could not have caught Stuxnet, and what Microsoft layered on top during the next fourteen years so that the next stolen Realtek private key has less reach.

### Where Authenticode shows up

Most Windows users meet Authenticode without realising it. The [User Account Control dialog](/blog/adminless-how-windows-finally-made-elevation-a-security-boun/) that says "Verified publisher: Microsoft Windows" instead of "Publisher: Unknown" is the user-visible end of a long cryptographic chain that bottoms out in a PKCS#7 / CMS `SignedData` envelope wrapped inside a `WIN_CERTIFICATE` record at the end of the PE file [@mslearn-authenticode-driver][@mslearn-pe-format]. The same plumbing is queried by SmartScreen, by App Control for Business (the 2024 rename of Windows Defender Application Control) [@mslearn-appcontrol-root], by `ci.dll` at kernel-driver load [@mslearn-kmcs-policy], and by Windows Update during servicing. They all read the *same* bytes in the certificate table; the verdicts differ only in which fields they consult and which policy they overlay.

<Mermaid caption="Four distinct Windows trust decisions consume the same SignedData envelope. The envelope is loaded once; each consumer reads its own fields.">
flowchart TD
    SD["PKCS#7 / CMS SignedData<br/>(inside WIN_CERTIFICATE)"]
    UAC["UAC<br/>'Verified publisher'"]
    SS["SmartScreen<br/>reputation lookup"]
    WDAC["App Control for Business (WDAC)<br/>rule evaluation"]
    KMCS["ci.dll / KMCS<br/>kernel-driver load"]
    SD --> UAC
    SD --> SS
    SD --> WDAC
    SD --> KMCS
</Mermaid>

> **Key idea:** Every Windows trust statement -- UAC, SmartScreen, App Control for Business, kernel-mode driver loading -- is a query against the same small set of structures inside the PE certificate table. Once you can read those structures, you can predict every later trust decision the OS makes.

### What you will be able to do by the end of this article

By the end of section 7 you should be able to decode every line of `signtool verify /v /pa /all` output and explain, in one paragraph, why Stuxnet still loaded under a fully patched Windows 7 kernel. By the end of section 11 you should be able to run `certutil -CatDB`, `New-CIPolicyRule -FilePath ... -Level FilePublisher`, and `certutil -hashfile` and explain what every byte of their output corresponds to in the on-disk structure.

Stuxnet's kernel components loaded because the chain validated. The chain validated because, cryptographically, nothing was wrong. To understand why that sentence is true -- and what Microsoft has done in the fourteen years since to keep the next stolen Realtek certificate from getting as far -- we have to start in August 1996.

## 2. 1996: PKCS#7, ActiveX, and the original sin of downloadable code

Counterintuitively, Authenticode was not invented to sign Windows binaries. It was invented to sign downloadable web payloads.

On 7 August 1996, Microsoft and VeriSign jointly announced what their press release called "the first technology for secure downloading of software over the Internet" [@press-pass-1996]. The release introduces Authenticode as a feature of Internet Explorer 3 beta 2, names Hank Vigil ("general manager of the electronic commerce group at Microsoft") and Stratton Sclavos ("president and CEO" of VeriSign), and explicitly anchors the design in *open* standards: "Authenticode and VeriSign's Digital ID service support Internet standards, including the X.509 certificate format and PKCS #7 signature blocks" [@press-pass-1996]. Six days later, on 13 August 1996, Internet Explorer 3 itself shipped as RTM for Microsoft Windows [@wiki-ie3].

The original motivating problem was ActiveX. An ActiveX control was a downloadable COM binary that the browser would load in-process; without a signature, the browser had no idea who built it. The April 1996 W3C submission that preceded Authenticode is described in the press release as a "code-signing proposal supported by more than 40 companies" [@press-pass-1996]<Sidenote>The 40+ company W3C signatory list is the institutional fact that made third-party CA participation possible from day one and seeded the modern multi-vendor code-signing economy. None of the architectural decisions that followed -- catalog signing, RFC 3161 timestamping, EV certificates -- would have been viable inside a single-vendor PKI.</Sidenote>. Anchoring the design in X.509 and PKCS#7 instead of inventing a Microsoft-only signature format is the choice that made everything afterwards possible.

### PKCS#7 was already there

By 1996, the *envelope* part of the design was solved. RSA Laboratories had published PKCS #7 v1.5 in November 1993 as part of the Public-Key Cryptography Standards series [@rfc-2315]; in March 1998 the IETF republished it verbatim as RFC 2315, "Cryptographic Message Syntax Version 1.5," authored by Burt Kaliski [@rfc-2315]. The same envelope evolved further: the IETF rebranded it as Cryptographic Message Syntax (CMS) and shipped progressively richer versions through RFCs 2630 (1999), 3369 (2002), 3852 (2004), and 5652 (2009) [@rfc-5652]. Modern Authenticode parsers consume the CMS dialect, but the on-disk envelope structure has barely moved in thirty years.

<Definition term="PKCS#7 SignedData">
The ASN.1 envelope -- originally PKCS#7 v1.5 (Kaliski, 1993; republished as RFC 2315 in 1998), now generalised as CMS in RFC 5652 -- that carries the signature, signed and unsigned attributes, and the chain of X.509 certificates inside the Authenticode certificate-table entry [@rfc-5652].
</Definition>

Authenticode is, in one sentence, *"PKCS#7 SignedData carrying a Microsoft-defined content type that hashes the PE file in a specific repeatable way"* [@authenticode-pe-docx]. The asymmetric signature inside that envelope is RSA, the public-key system Rivest, Shamir, and Adleman published in 1978 [@rsa-1978], built on the Diffie-Hellman digital-signature concept introduced in 1976 [@diffie-hellman-1976]. None of that primitive cryptography has changed since. Everything that has changed sits *around* the envelope: the algorithms it carries, the catalog store that lets Microsoft sign tens of thousands of files at once, the timestamp tokens that pin a signing moment in time.

<Mermaid caption="Authenticode is a leaf on the PKCS#7 / CMS family tree. The asymmetric primitive (RSA 1978) and the envelope (PKCS#7 v1.5 1993) both predate the IE3 announcement; what Microsoft added in 1996 was the Authenticode content type and the PE hash exclusions.">
flowchart LR
    DH["Diffie-Hellman (1976)<br/>digital-signature concept"] --> RSA["RSA (1978)"]
    RSA --> P7["PKCS#7 v1.5<br/>(RSA Labs, 1993)"]
    P7 --> R2315["RFC 2315 (1998)"]
    R2315 --> R2630["RFC 2630 (1999)"]
    R2630 --> R3369["RFC 3369 (2002)"]
    R3369 --> R3852["RFC 3852 (2004)"]
    R3852 --> R5652["RFC 5652 / CMS<br/>(2009)"]
    P7 --> AC["Authenticode<br/>(IE3, August 1996)"]
    AC --> WC["WIN_CERTIFICATE<br/>in modern PE"]
</Mermaid>

### From one click to four trust decisions

The original UX of Authenticode in IE 3 was a *modal trust prompt*. The user saw a dialog ("Do you want to install and run [name] signed and distributed by [publisher]?") and clicked Yes or No. The signature was checked once, and that was the entire trust decision. By 2026, the same `SignedData` envelope feeds at least four entirely different trust subsystems -- UAC, SmartScreen, App Control for Business, kernel-mode code integrity -- and most of the time the user clicks nothing at all.

That layering is what the rest of this article is about. Thirty years on, the on-disk bytes have barely changed. The certificate table at the end of every signed Windows binary still carries a PKCS#7 SignedData envelope, and at the head of that envelope is the same content type -- `SpcIndirectDataContent` -- Microsoft defined in 1996. What *has* changed is everything around it: the algorithms inside the envelope, the catalog store, the timestamp tokens, the WDAC policy layer on top. Let's open the envelope and look.

## 3. Anatomy on disk: WIN_CERTIFICATE, PKCS#7 SignedData, SpcIndirectDataContent

Where does the signature actually live in a signed `.exe`? Most engineers can guess "the end of the file." Fewer can name the data directory entry, fewer still the wrapper structure, and almost nobody volunteers the exact ASN.1 content type. Four nesting levels matter. Walk them in order and the whole rest of the architecture starts making sense.

### Level 1: the PE certificate table

The PE optional header carries a `DataDirectory[16]` array. Entry index 4, `IMAGE_DIRECTORY_ENTRY_SECURITY`, points at the *certificate table* -- an offset and size into the file [@mslearn-pe-format]. Unlike every other data directory entry, the certificate table is the only one whose offset is a *file* offset, not a relative virtual address; the certificate table is never mapped into memory at load time.

Inside that offset+size region is a sequence of `WIN_CERTIFICATE` records, each laid out as:

```c
typedef struct _WIN_CERTIFICATE {
    DWORD       dwLength;           // total length of this record, including header
    WORD        wRevision;           // WIN_CERT_REVISION_2_0
    WORD        wCertificateType;    // WIN_CERT_TYPE_PKCS_SIGNED_DATA
    BYTE        bCertificate[ANYSIZE_ARRAY];  // the DER-encoded blob
} WIN_CERTIFICATE;
```

For Authenticode-signed Windows binaries, `wCertificateType == WIN_CERT_TYPE_PKCS_SIGNED_DATA` (constant value `0x0002`), and `bCertificate[]` is a DER-encoded CMS / PKCS#7 SignedData blob [@authenticode-pe-docx]. Multiple `WIN_CERTIFICATE` records are legal; this is how a single binary can carry both a SHA-1 (legacy) and a SHA-256 (modern) signature, or a dual-signed binary carrying both a primary and a nested secondary embedded signature (via the unsignedAttrs nested-signature mechanism).

<Definition term="WIN_CERTIFICATE">
The PE certificate-table record (`dwLength`, `wRevision`, `wCertificateType`, `bCertificate[]`) that wraps a single attribute certificate inside a PE. For Authenticode signatures, `wCertificateType` is `WIN_CERT_TYPE_PKCS_SIGNED_DATA` and `bCertificate` holds a DER-encoded CMS / PKCS#7 SignedData blob [@authenticode-pe-docx][@mslearn-pe-format].
</Definition>

### Level 2: the CMS SignedData envelope

Decoding `bCertificate` produces an ASN.1 SEQUENCE describing a CMS `ContentInfo` whose content type is `signedData` (OID `1.2.840.113549.1.7.2`). Inside that is the `SignedData` structure proper [@rfc-5652]:

- `version` -- an integer, typically 1 or 3.
- `digestAlgorithms` -- the set of hash algorithms used by any signer (commonly `sha256`).
- `encapContentInfo` -- the content the signers are signing over. *This is the field that matters.*
- `certificates` -- the X.509 chain certificates needed to validate the signers.
- `crls` -- optional, almost never populated inline.
- `signerInfos` -- one or more `SignerInfo` structures, each with the actual signature bytes plus signed and unsigned attributes.

Each `SignerInfo` carries the signing certificate identifier, a set of `signedAttrs` (whose digest is what gets signed), an `encryptedDigest` (the actual signature bytes), and a set of `unsignedAttrs`. The single most important unsigned attribute, in practice, is the RFC 3161 `TimeStampToken` -- the counter-signature that pegs the signing event to a moment in time. We will come back to that in section 5.

### Level 3: SpcIndirectDataContent

The `encapContentInfo.eContentType` for Authenticode is `1.3.6.1.4.1.311.2.1.4` -- the OID Microsoft registered for `SpcIndirectDataContent`. Inside, the `eContent` is a Microsoft-specific ASN.1 structure [@authenticode-pe-docx]:

```asn1
SpcIndirectDataContent ::= SEQUENCE {
    data        SpcAttributeTypeAndOptionalValue,
    messageDigest DigestInfo
}

SpcAttributeTypeAndOptionalValue ::= SEQUENCE {
    type   OBJECT IDENTIFIER,   -- 1.3.6.1.4.1.311.2.1.15 for PE images
    value  [0] EXPLICIT ANY DEFINED BY type OPTIONAL
}

DigestInfo ::= SEQUENCE {
    digestAlgorithm AlgorithmIdentifier,
    digest          OCTET STRING
}
```

For a PE binary, `data.type` is `1.3.6.1.4.1.311.2.1.15` (`SPC_PE_IMAGE_DATAOBJ`) and `data.value` carries a `SpcPeImageData` structure describing what kind of image this is (32-bit, 64-bit, importable, executable). The `messageDigest.digest` is the **Authenticode hash** of the PE file [@authenticode-pe-docx]. That hash is *not* SHA-256 over the file bytes.

<Definition term="SpcIndirectDataContent">
Microsoft's `eContentType` registered under OID `1.3.6.1.4.1.311.2.1.4`. Its `messageDigest` field holds the Authenticode hash of the signed artefact, and its `data` field describes what kind of artefact it is (PE image, MSI, script). The fact that this structure signs *a hash* rather than a file is what makes catalog signing possible [@authenticode-pe-docx].
</Definition>

### Level 4: the Authenticode hash and its four exclusions

The Authenticode hash is computed over the PE file with four specific byte ranges excluded [@authenticode-pe-docx]:

| Excluded region | Why excluded | Spec reference |
|---|---|---|
| `OptionalHeader.CheckSum` (4 bytes) | The OS recomputes the optional-header checksum when servicing; signing over it would make every signature invalidate at first patch. | `Authenticode_PE.docx` §3.1 [@authenticode-pe-docx] |
| `DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]` (8 bytes) | The pointer to the certificate table itself moves when a signature is added; signing over the pointer is a chicken-and-egg loop. | `Authenticode_PE.docx` §3.1 [@authenticode-pe-docx] |
| The certificate-table bytes themselves | Same chicken-and-egg loop -- the signature cannot sign itself. | `Authenticode_PE.docx` §3.1 [@authenticode-pe-docx] |
| File-alignment padding after each section | Padding can be different on different builds for harmless reasons (alignment, build-tool quirks); signing over it would punish those harmless differences. | `Authenticode_PE.docx` §3.1 [@authenticode-pe-docx] |

<Definition term="Authenticode hash">
The PE digest computed over the file with four regions excluded: the optional-header `CheckSum` field, the `IMAGE_DIRECTORY_ENTRY_SECURITY` data-directory entry, the certificate-table bytes themselves, and the file-alignment padding after each section. Because the excluded regions include the certificate-table area, the same hash remains valid after the signature is appended [@authenticode-pe-docx].
</Definition>

The exclusion of the certificate-table bytes is the design move that makes the whole architecture work. The Authenticode hash is computed *first*, signed, and then the signature is appended into the very region the hash excluded. After appending, the hash is still valid; verifying simply recomputes the hash with the same four regions excluded and compares.

<Sidenote>ASN.1 DER's tag-length-value shape means that, given enough patience, you can decode every level of the certificate table with nothing but a hex dump. This accessibility is also why parser bugs are particularly damaging: a verifier that re-encodes or normalises before hashing can be tricked into hashing different bytes than the bytes that get loaded -- the structural failure mode at the bottom of CVE-2013-3900 [@nvd-cve-2013-3900].</Sidenote>

### A separate, smaller hash per 4 KiB page

Authenticode supports an optional signed attribute, `SpcPeImagePageHashes2`, with OID `1.3.6.1.4.1.311.2.3.2` (SHA-256). It carries a sequence of `(RVA, hash)` pairs, one hash per 4 KiB page of the PE image [@authenticode-pe-docx]. The older `1.3.6.1.4.1.311.2.3.1` SHA-1 variant is effectively deprecated. Under [Hypervisor-Protected Code Integrity (HVCI)](/blog/wdac--hvci-code-integrity-at-every-layer-in-windows/), the page hashes are validated at demand-fault time: when the OS faults in a page from disk, HVCI hashes the page and compares it to the signed page-hash entry before mapping the page as executable. Whole-file integrity checking at load is *not* the same as runtime integrity checking at fault; page hashes are what closes that gap.<Sidenote>ARM64 Windows configurations have used 4 KiB native pages on the systems that ship Authenticode page-hash enforcement to date. The page-hash attribute encodes RVAs into the on-disk image, so any future move to 16 KiB or 64 KiB page granularity would require a corresponding spec revision.</Sidenote>

<Definition term="Page hash (SpcPeImagePageHashes2)">
An optional signed attribute (OID `1.3.6.1.4.1.311.2.3.2` for SHA-256) carrying a sequence of `(RVA, SHA-256)` pairs, one per 4 KiB page of the PE image. The hashes are checked at demand-fault time by HVCI / Code Integrity, not just at load time [@authenticode-pe-docx].
</Definition>

### The whole nest, in one picture

<Mermaid caption="The four nesting levels of an Authenticode signature on disk. The Authenticode hash inside SpcIndirectDataContent is signed by the SignerInfo, and the RFC 3161 timestamp arrives as a counter-signature in the unsigned attributes.">
flowchart TD
    PE["PE file on disk"]
    OH["Optional header"]
    DD["DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY] (entry 4)"]
    WC["WIN_CERTIFICATE record<br/>(dwLength, wRevision, wCertificateType, bCertificate[])"]
    SD["PKCS#7 / CMS SignedData"]
    Certs["certificates: X.509 chain"]
    SI["SignerInfo"]
    Sa["signedAttrs"]
    SIDC["encapContentInfo: SpcIndirectDataContent"]
    SPI["data: SpcPeImageData (SPC_PE_IMAGE_DATAOBJ)"]
    MD["messageDigest: Authenticode hash"]
    PH["SpcPeImagePageHashes2 (optional)"]
    ED["encryptedDigest: signature bytes"]
    Ua["unsignedAttrs"]
    TST["RFC 3161 TimeStampToken<br/>(OID 1.2.840.113549.1.9.16.2.14)"]
    PE --> OH
    OH --> DD
    DD --> WC
    WC --> SD
    SD --> Certs
    SD --> SI
    SI --> Sa
    Sa --> SIDC
    SIDC --> SPI
    SIDC --> MD
    SIDC --> PH
    SI --> ED
    SI --> Ua
    Ua --> TST
</Mermaid>

### Try it yourself

<RunnableCode lang="python" title="Decode a PE certificate table">{`
# Decode the four nesting levels of an Authenticode signature.
# Requires: pip install pefile asn1crypto

const catalogSignedInboxFile = {
  Path: "C:\\\\Windows\\\\System32\\\\ntoskrnl.exe",
  // PowerShell: (Get-AuthenticodeSignature ntoskrnl.exe).SignatureType -> Catalog
  SignatureType: "Catalog",
  Status: "Valid",
  CatalogFile: "C:\\\\Windows\\\\System32\\\\CatRoot\\\\{F750E6C3-...}\\\\Microsoft-Windows-Client-Drivers-Package~31bf3856ad364e35~amd64~~10.0.x.y.cat",
  SignerCertificate: { Subject: "CN=Microsoft Windows Production PCA 2011, ..." }
};

console.log("Embedded signature:", embeddedSignedBinary.SignatureType, embeddedSignedBinary.CatalogFile);
console.log("Catalog signature: ", catalogSignedInboxFile.SignatureType, catalogSignedInboxFile.CatalogFile);
`}</RunnableCode>

Once you can sign a hash instead of a file, and once you can pin a signing event to a moment in time that outlives the certificate, the rest of the architecture stops being a sequence of crypto choices and starts being a sequence of *policy* choices: which roots do we trust for ring 0, which file-publisher tuples does this enterprise authorise, which drivers does Microsoft deny by hash? To see those policy choices in operation, watch a single `WinVerifyTrust` call end to end.

## 6. A modern WinVerifyTrust call, end to end

A user double-clicks a Microsoft-signed `.exe` on Windows 11 24H2. HVCI is on, Smart App Control is on, an enterprise App Control policy is loaded. The shell calls `ShellExecute`. Before the OS hands control to the new process, the kernel's code-integrity stack (`ci.dll`) and user-mode `WinVerifyTrust` between them answer the question *"is this binary trusted?"* in roughly the following seven stages.

### Stage 1: read the certificate table

`ci.dll` reads the optional header, finds `DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]`, walks the certificate-table region, and enumerates the `WIN_CERTIFICATE` records. Multiple records may be present (e.g. a SHA-1 record for compatibility with Windows 7 verifiers, and a SHA-256 record for modern ones); the verifier picks the strongest record whose algorithm is allowed by current policy [@authenticode-pe-docx][@mslearn-pe-format].

### Stage 2: decode the SignedData

For each candidate record with `wCertificateType == WIN_CERT_TYPE_PKCS_SIGNED_DATA`, the verifier DER-decodes `bCertificate` into a CMS `ContentInfo`, then into a `SignedData` structure [@rfc-5652]. The verifier reads `signerInfos`, picks the signer (usually one), and extracts the signed and unsigned attributes.

### Stage 3: verify the content type

The verifier confirms `encapContentInfo.eContentType == 1.3.6.1.4.1.311.2.1.4` (`SpcIndirectDataContent`), then decodes the inner structure and confirms `data.type == 1.3.6.1.4.1.311.2.1.15` (`SPC_PE_IMAGE_DATAOBJ`) [@authenticode-pe-docx]. The inner `messageDigest` is the Authenticode hash this signature claims to cover; the `digestAlgorithm` says how it was computed.

### Stage 4: recompute the Authenticode hash

The verifier re-reads the PE file bytes, applies the four exclusions (`CheckSum`, the SECURITY data-directory entry, the certificate-table bytes, and section-padding), hashes the remaining bytes with the claimed algorithm, and compares to `SpcIndirectDataContent.messageDigest` [@authenticode-pe-docx]. If they differ, the signature is rejected.

### Stage 5: validate page hashes under HVCI

If `SpcPeImagePageHashes2` is attached and the running policy includes HVCI, the page-hash table is preserved across the verification call and consulted later by the secure kernel at demand-fault time [@authenticode-pe-docx]. The full-file Authenticode hash check is *necessary* but not *sufficient* for runtime integrity; pages on disk can be tampered after load by a kernel-level attacker who bypasses file-system protections. Page hashes are what closes that gap by re-checking each page at the moment it is mapped executable.

### Stage 6: build the chain

The verifier collects the `certificates` SET from the `SignedData`, plus any AIA-fetched certificates needed to complete the chain, and tries to terminate the path at a trusted root. For kernel-mode loads, the legacy anchor is the `Microsoft Code Verification Root`; for portal-signed drivers, the chain may instead terminate at one of the Microsoft Root Authority anchors. The KMCS policy page describes the Windows 10 1607+ kernel-mode anchors verbatim: *"Microsoft Root Authority 2010, Microsoft Root Certificate Authority, Microsoft Root Authority"* with Secure Boot on [@mslearn-kmcs-policy]. For user-mode loads, the chain may terminate at any root in the system Trusted Root store; the enterprise's App Control policy narrows the trust further by referencing specific anchors at the RootCertificate / PcaCertificate rule level [@mslearn-select-types-of-rules].

<Definition term="WinVerifyTrust">
The CryptoAPI function (`wintrust.dll!WinVerifyTrust`) that orchestrates the Authenticode verification pipeline: certificate-table read, SignedData decode, content-type check, Authenticode-hash recomputation, optional page-hash association, chain build, catalog fallback for unsigned PEs, and timestamp validation. It returns a success or specific error code; the caller (UAC, SmartScreen, `ci.dll`, WDAC) interprets the result against its own policy.
</Definition>

<Definition term="Code Integrity / ci.dll">
The Windows kernel-mode component that enforces the Kernel-Mode Code Signing policy on driver loads (Vista x64 and later [@wiki-kernel-patch-protection][@mslearn-kmcs-policy]) and, under HVCI, evaluates page hashes at fault time. `ci.dll` is the kernel-side caller of `WinVerifyTrust` semantics for driver loads.
</Definition>

<Definition term="Microsoft Code Verification Root">
The historical kernel-mode trust anchor whose name appears in Microsoft's KMCS documentation and whose intermediate cross-signed third-party code-signing CAs for pre-July-2015 drivers [@mslearn-kmcs-policy]. Microsoft Learn does not publish a single canonical page with the root's SHA-1 / SHA-256 thumbprint, validity dates, or issuance year; in practice the thumbprint is read by running `certutil -store` on a recent Windows system.
</Definition>

<Sidenote>The Microsoft Code Verification Root metadata absence is real: although the root is named in the KMCS policy document [@mslearn-kmcs-policy], no Microsoft Learn URL publishes its thumbprint or validity dates on a stable page. Practitioners should reference the root by name in policy and treat the actual thumbprint as something to be enumerated via `certutil -store` on the running system rather than copy-pasted from a published document.</Sidenote>

### Stage 7: catalog fallback for unsigned PEs

If the PE has no embedded signature, the verifier computes the Authenticode hash and queries `CryptSvc`: is this hash a member of any installed catalog under `%SystemRoot%\System32\CatRoot\`? If yes, the verifier uses the catalog's signer as the effective signer for the PE [@mslearn-catalog-files][@mslearn-authenticode-driver]. Cross-system files installed by Windows Update (most drivers, most inbox executables) take this path.

### Stage 8: validate the RFC 3161 timestamp

If the unsigned attributes carry a `TimeStampToken` (OID `1.2.840.113549.1.9.16.2.14`), the verifier decodes it, validates the TSA's chain, extracts `genTime`, and confirms the signing certificate was valid at `genTime` [@rfc-3161]. This is how a 2010 signature still verifies in 2026: not because the 2010 certificate is still valid, but because a TSA attested at signing time that the signature existed when the certificate was valid.

### Stage 9: WDAC policy evaluation

With cryptographic verdicts in hand, the App Control policy engine evaluates the file against the active policy: does any allow rule match, does any deny rule match, including the default-on Vulnerable Driver Blocklist supplemental deny [@mslearn-recommended-driver-block-rules]? The matching rule -- by Hash, FileName, Publisher, FilePublisher, WHQL, WHQLPublisher, WHQLFilePublisher, LeafCertificate, PcaCertificate, or RootCertificate level [@mslearn-select-types-of-rules] -- decides the final outcome. Audit-mode hits produce event ID 3076; enforcement-mode blocks produce event ID 3077 [@mslearn-event-id-explanations].

### Stage 10: legacy parser hardening, if opted in

A hardened environment will also have `EnableCertPaddingCheck=1` set [@nvd-cve-2013-3900], enabling the strict parser that rejects the CVE-2013-3900 appended-data form. CISA added the CVE to its Known Exploited Vulnerabilities catalogue on 10 January 2022 with a federal due date of 10 July 2022 [@nvd-cve-2013-3900]; environments subject to federal compliance regimes treat this as mandatory.<Sidenote>For practitioners: the registry key needs to be set in both `HKLM\Software\Microsoft\Cryptography\Wintrust\Config` and the matching `Wow6432Node` path, because the 32-bit and 64-bit `WinVerifyTrust` code paths read separate copies. Setting only one and rebooting is one of the more common configuration mistakes in hardened-baseline rollouts.</Sidenote>

<Mermaid caption="A modern WinVerifyTrust call on Windows 11 24H2 with HVCI and Smart App Control on, against an enterprise-authored App Control policy. The cryptographic verdicts and the policy verdicts are independent: either alone can fail the load.">
flowchart TD
    Start["ShellExecute / driver load"]
    CT["Read PE certificate table"]
    Decode["Decode WIN_CERTIFICATE -> CMS SignedData"]
    OID&#123;"eContentType =<br/>SpcIndirectDataContent?"&#125;
    Hash["Recompute Authenticode hash"]
    HashOK&#123;"Hash matches<br/>messageDigest?"&#125;
    Chain["Build certificate chain"]
    Cat["Catalog fallback?<br/>(if PE unsigned)"]
    TS["Validate RFC 3161 token"]
    PHash["Associate SpcPeImagePageHashes2<br/>(HVCI fault-time check)"]
    Pol["WDAC policy evaluation"]
    EPC["EnableCertPaddingCheck<br/>strict parser (if opt-in)"]
    Done["LOAD or DENY"]
    Start --> CT --> Decode --> OID
    OID -->|yes| Hash
    OID -->|no| Done
    Hash --> HashOK
    HashOK -->|yes| Chain
    HashOK -->|no| Done
    Chain --> Cat --> TS --> PHash --> EPC --> Pol --> Done
</Mermaid>

> **Note:** There is no separate certificate table per trust subsystem. UAC, SmartScreen, `ci.dll`, WDAC, and the catalog-fallback path all read the *same* bytes inside the same `WIN_CERTIFICATE` record. What differs is which fields each consumer cares about and what policy each consumer overlays on top. Once you read the on-disk structures, every later trust decision is predictable.

<Aside label="What WinVerifyTrust does *not* check">
`WinVerifyTrust` does not execute the binary. It does not appraise behaviour or reputation -- that is SmartScreen's job, downstream. It does not verify runtime page integrity -- HVCI does, in the secure kernel, at demand-fault time. It does not enforce the App Control policy -- the policy engine does, downstream. It does not check OCSP unless the caller opts in; chain-revocation behaviour is governed by `WinVerifyTrust` flags supplied by the caller. The function answers only the narrow cryptographic question: does the SignedData blob parse, does the recomputed hash match, does the chain build, and (if a token is attached) did the signing event happen inside the signing certificate's validity window?
</Aside>

By the seventh stage of this pipeline, the answer to "is this binary trusted?" is no longer a yes-or-no statement about cryptography. It is a *composite* of cryptographic verdicts (signature integrity, hash match, chain build, timestamp validity, page hashes) and *policy* verdicts (allowed by WDAC, not on the blocklist). Authenticode supplies the inputs to a policy; WDAC writes the policy. Let us look at the policy language.

## 7. WDAC rule levels: Authenticode as policy input, not policy itself

App Control for Business (WDAC) is where the Authenticode primitives finally surface to administrators as policy. The `SignerInfo`, the subject CN of the leaf certificate, the file's `OriginalFileName` and `ProductVersion` from the version resource, the page-hash table, even the choice of catalog signer -- all of them become inputs to a small rule language.

### Rule levels: what Authenticode field each level consults

The verbatim rule-level catalogue from Microsoft Learn is [@mslearn-select-types-of-rules]:

| Rule level | Authenticode field(s) consulted | Example use case |
|---|---|---|
| `Hash` | Authenticode hash of the file | Pinning a single binary by exact bytes; brittle across patches. |
| `FileName` | `OriginalFileName` from the PE version resource | Convenience for inbox files; not cryptographic. |
| `FilePath` | Filesystem path | UNC or absolute path; not cryptographic. Use sparingly. |
| `SignedVersion` | Publisher + `OriginalFileName` + version range | Allow a publisher's binary at a given version or higher. |
| `Publisher` | Issuing CA + leaf-cert subject CN | Allow anything signed by a given vendor under a given CA. |
| `FilePublisher` | Publisher + `OriginalFileName` + minimum `FileVersion` | Allow a specific binary from a specific vendor at min version. |
| `WHQL` | The Windows Hardware Quality Labs EKU | Allow any WHQL-signed driver. |
| `WHQLPublisher` | WHQL EKU + leaf-cert subject CN | Allow WHQL drivers from a specific OEM. |
| `WHQLFilePublisher` | WHQL EKU + `OriginalFileName` + min `FileVersion` | The strictest driver rule. |
| `LeafCertificate` | Leaf cert subject + issuer | Pin to a specific signing cert. |
| `PcaCertificate` | The PCA (intermediate) cert | Useful for "anything Microsoft-signed" without enumerating leaves. |
| `RootCertificate` | The root anchor | Broadest; usually too coarse. |

### Policy options

App Control policies are XML documents with a `<Rules>` section that toggles broad behavioural options [@mslearn-select-types-of-rules]:

- **`0 Enabled:UMCI`** -- *"App Control policies restrict both kernel-mode and user-mode binaries. By default, only kernel-mode binaries are restricted. Enabling this rule option validates user mode executables and scripts"* [@mslearn-select-types-of-rules].
- **`2 Required:WHQL`** -- *"By default, kernel drivers that aren't Windows Hardware Quality Labs (WHQL) signed are allowed to run. Enabling this rule requires that every driver is WHQL signed and removes legacy driver support"* [@mslearn-select-types-of-rules].
- **`8 Required:EV Signers`** -- documented but, per the same Microsoft Learn page, *"This option isn't currently supported."*<MarginNote>The Required:EV Signers option is in every published rule-options table but never makes it past parsing today. The EV requirement is enforced contractually via the Hardware Developer Center submission gate, not via the rule option. Treat it as documentation of intent rather than runtime enforcement.</MarginNote>

The Vulnerable Driver Blocklist is shipped as a *supplemental* deny policy that overlays the user's primary policy. From Windows 11 22H2 onward it is default-on and automatically enforced under HVCI, Smart App Control, or S Mode [@mslearn-recommended-driver-block-rules]. Updates arrive quarterly. The blocklist is deliberately conservative: Microsoft's own documentation acknowledges *"It's often necessary for us to hold back some blocks to avoid breaking existing functionality while we work with our partners who are engaging their users to update to patched versions"* [@mslearn-recommended-driver-block-rules].

<Definition term="App Control for Business (WDAC)">
The post-2024 rename of Windows Defender Application Control [@mslearn-appcontrol-root]; a code-integrity policy language that consumes Authenticode primitives (chain, leaf-cert subject, `OriginalFileName`, version, WHQL EKU, page-hash table, embedded-vs-catalog provenance) as inputs to administrator-authored allow and deny rules.
</Definition>

<Definition term="FilePublisher rule">
A WDAC rule level that allows or denies a binary if it is signed by a given Publisher (issuing CA + leaf-cert subject CN) **and** the PE's `OriginalFileName` matches **and** the PE's `FileVersion` is at or above a minimum. The tightest commonly used rule level; brittle across self-updating applications whose binaries change without warning [@mslearn-select-types-of-rules][@mslearn-use-code-signing].
</Definition>

### A worked example

Generating a FilePublisher rule for a Microsoft-signed binary on PowerShell:

```powershell
New-CIPolicyRule -FilePath "C:\Path\To\App.exe" -Level FilePublisher
```

produces a `<FileRule>` whose XML carries the issuing CA, the leaf-cert subject CN, the `OriginalFileName` from the version resource, and a `MinimumFileVersion` attribute. Every one of those fields is a direct read of the Authenticode `SignerInfo` and the PE version resource; nothing in the rule generation step talks to Microsoft. The administrator owns the rule.

> **Note:** Microsoft's own guidance is verbatim: *"Be aware of self-updating apps, as their app binaries may change without your knowledge"* [@mslearn-use-code-signing]. FilePublisher rules pin a minimum version; if a self-updating app rolls out a build with a different `OriginalFileName` casing, or with `ProductVersion` changes that some packagers reuse as `FileVersion`, the rule silently stops matching. For self-updating apps, prefer `Publisher` (CA + subject CN only) and accept the looser blast radius.

<Sidenote>Operational tip: audit-mode hits write event ID 3076 to the *Microsoft-Windows-CodeIntegrity/Operational* channel, enforcement-mode blocks write event ID 3077 [@mslearn-event-id-explanations]. Stage every policy in audit mode for at least one full patch cycle before flipping to enforcement; the 3076 stream is your inventory of what the rules would have denied.</Sidenote>

WDAC's vocabulary makes one structural choice explicit that the article has been implicit about until now: trust is *administrator-authored*, not *vendor-authored*. The cryptographic identity is supplied by the same Authenticode primitives we just dissected; the policy is whatever the organisation writes. Before we look at the limits of what this stack can prove, one quick detour into how other operating systems have approached the same problem.

## 8. Catalog-vs-embedded across operating systems

Windows is unusual in two specific ways: it stores the catalog on the endpoint, and it refreshes the catalog through the OS update channel. No other mainstream OS does both.

| System | Signature carrier | Catalog model? | Transparency log? | Counter-signing for longevity |
|---|---|---|---|---|
| Windows (Authenticode) | PKCS#7 / CMS SignedData inside `WIN_CERTIFICATE` | Yes -- `.cat` files in `CatRoot`, refreshed by Windows Update [@mslearn-catalog-files] | No | Yes -- RFC 3161 token as unsigned attribute [@rfc-3161] |
| macOS | Apple-issued code signature + Notarization ticket; ticket stapled to artefact or fetched online [@apple-notarization] | No -- Notarization ticket attests, but there is no on-disk "list of hashes" structure | No | Stapled ticket effectively gives a signing-time guarantee; no third-party TSA |
| Linux IMA / EVM | Extended-attribute signatures on individual files [@linux-ima-wiki] | No -- per-file `security.ima` xattr | No | Out of scope; appraised against locally trusted keyring |
| Android | APK Signature Scheme v3 (block inside the APK) [@android-apk-v3] | No | No (signatures live inside the APK) | Proof-of-rotation chain inside the v3 block lets a publisher rotate keys without re-signing |
| sigstore (OCI artefacts) | Detached signature in OCI registry; short-lived Fulcio cert [@sigstore-overview] | Closest analogue -- detached signature can cover blobs [@cosign-blobs] | Yes -- Rekor [@rekor-github] | TSA-style entries possible via Rekor |

The closest design analogue to the Windows catalog model is sigstore. Both decouple the signature from the artefact, and both let a single signing event cover many files. The difference is *where the detached signature lives*. Windows puts the `.cat` on the endpoint and refreshes the catalog through the OS update channel; sigstore stores the detached signature in an OCI registry and writes an attestation to a Rekor transparency log. That difference is also what gives Windows the offline-stale-catalog problem (a disconnected endpoint cannot freshness-check `CatRoot`) and gives sigstore the offline-no-Rekor problem (a disconnected verifier cannot consult the log).<Sidenote>Readers who want the broader cross-platform identity comparison should consult the earlier [*App Identity in Windows*](/blog/who-is-this-code----the-quiet-33-year-reinvention-of-app-ide/) article in this series, which compares Apple's package identity, Android's app IDs, and Linux's lack of a unified equivalent in more depth. The present article only summarises the *signature-carrier* side of the comparison.</Sidenote>

Whether Microsoft puts the catalog on the endpoint or in an OCI registry is a deployment choice. The *limit* of what any signature -- catalog, embedded, sigstore-anchored, Apple-notarised -- can prove is a deeper, more uncomfortable claim. We turn to that next.

## 9. What signatures cannot prove

Stuxnet did not break Authenticode. It walked through it. The same is true of Flame, of ShadowHammer, and of the Bitwarden CLI npm hijack. Every named incident on the modern Windows code-signing timeline is an instance of the same structural lower bound: signatures prove *who*, not *what*. The Windows code-identity stack has spent fourteen years adding layers that narrow the consequences of that bound. None of them eliminate it.

Four limits are worth naming explicitly.

### L1. Provenance is not safety

By Rice's theorem corollary, no decision procedure can determine arbitrary non-trivial semantic properties of a program. A signing system can therefore certify only "this binary came from a key-holder," never "this binary is benign." Stuxnet 2010 [@stuxnet-dossier], Flame 2012 [@stevens-counter-cryptanalysis][@ms-advisory-2718704], Operation ShadowHammer 2019 [@securelist-shadowhammer], and the Bitwarden CLI npm hijack of 22 April 2026 [@bitwarden-statement][@stepsecurity-bitwarden][@hackernews-bitwarden] are four independent instances of the same gap, across four entirely different attack surfaces (stolen kernel-driver key; forged sub-CA via MD5 collision; compromised ASUS Live Update certificate; compromised npm OIDC trusted-publishing). The empirical scale is large: Kim, Kwon, and Dumitraș measured millions of certificates and hundreds of thousands of signed-but-malicious PE samples in the Windows code-signing PKI in their CCS 2017 paper [@dumitras-ccs-2017].

The mathematics of Rice's theorem is succinct. Let $P$ be any non-trivial semantic property of programs (e.g. *is malicious*). For any algorithm $A$ that on input program $p$ outputs $A(p) \in \{\text{yes}, \text{no}\}$ claiming whether $p$ has property $P$, there exists a program $q$ where $A(q)$ is wrong. A signature scheme is not such an algorithm $A$ in the first place: it computes $\text{Sig}_{\text{sk}}(\text{hash}(p))$. The signature output has no semantic content about $p$'s behaviour; it asserts only that the holder of $\text{sk}$ touched $\text{hash}(p)$.

### L2. CA cardinality and the weakest-link property

The trust graph for kernel-mode loads is narrow: a small number of Microsoft roots [@mslearn-kmcs-policy]. The trust graph for user-mode loads is the union of every root in the system Trusted Root store -- a much larger set. *Any one* root, if compromised, degrades the entire user-mode code-identity trust graph; *any one* sub-CA, if forged, opens the kernel-mode path for the lifetime of the certificate. The Sotirov / Stevens / Appelbaum / Lenstra / Molnar / Osvik / de Weger rogue-CA work from December 2008 [@hashclash-rogue-ca] demonstrated this dynamic for the web PKI; the same family of attack was then mounted in Flame in 2012 against the Microsoft Enforced Licensing Intermediate PCA [@ms-advisory-2718704]. The CSBR's EV-on-hardware requirements [@cabf-cs-documents] reduce stolen-key risk at the leaf level, but a forged sub-CA bypasses the leaf entirely.

### L3. Catalog-store freshness on disconnected endpoints

A disconnected endpoint cannot freshness-check its `CatRoot`. The catalog database is whatever Windows Update last delivered -- which means freshly issued catalogs covering newly shipped inbox files cannot be trusted on machines that have been offline. The Vulnerable Driver Blocklist faces the same problem in reverse: a freshly blocked driver does not become *un*-trusted on a disconnected endpoint until the supplemental policy lands. Microsoft acknowledges this in the VDB documentation: *"It's often necessary for us to hold back some blocks to avoid breaking existing functionality"* [@mslearn-recommended-driver-block-rules]. The publication lag is deliberate, not accidental, and there is no in-band way for an endpoint to ask "is my VDB current?"

### L4. TSA centralisation and antedating

RFC 3161 has no transparency log. A compromised TSA can issue countersignatures with arbitrary `genTime` undetectably, until and unless the TSA's root is revoked. Sigstore Rekor [@rekor-github] is the canonical answer to this problem in the OSS world; nothing equivalent ships in the Authenticode stack. The consequence is asymmetric: a compromised TSA can antedate a signature backwards, making a freshly signed but recently malicious binary appear to have been signed before the malicious campaign began -- which on most verifiers means it will *still* verify even after the actual signing certificate is revoked.

<Mermaid caption="Each layer of Windows code identity narrows the gap between provenance and safety, but the gap cannot be closed.">
flowchart TD
    L1["L1: Provenance != safety<br/>(Rice's theorem corollary)"]
    L2["L2: CA cardinality<br/>(weakest-link property)"]
    L3["L3: CatRoot freshness<br/>(offline endpoints stale)"]
    L4["L4: TSA centralisation<br/>(no transparency log)"]
    Floor["What is actually being proved:<br/>a key-holder touched hash(p) at genTime"]
    L1 --> Floor
    L2 --> Floor
    L3 --> Floor
    L4 --> Floor
</Mermaid>

<PullQuote>
A valid signature proves only who signed the binary, never what the binary does.
</PullQuote>

> **Key idea:** Authenticode is the floor of Windows trust, not the ceiling. Every later layer -- Kernel-Mode Code Signing, App Control for Business, the Vulnerable Driver Blocklist, HVCI page-hash enforcement -- exists because the floor cannot, by construction, do more.

<Aside label="Aha moment: Stuxnet, but generalised">
Stuxnet 2010, Flame 2012, ShadowHammer 2019, and Bitwarden CLI 2026 are four instances of the same lower bound, fourteen years apart, across four entirely different surfaces: a stolen private key for a kernel driver; a forged Microsoft sub-CA via cryptographic collision; a compromised ASUSTeK signing certificate used to sign a malicious updater; a compromised npm OIDC trusted-publishing pipeline used to publish a malicious CLI release. In each case the signature was valid. In each case the binary was malicious. The layers we add -- cross-signing deprecation, EV-on-hardware, the VDB, WDAC -- do not close the gap. They reduce the blast radius of the inevitable next incident.
</Aside>

Once you see provenance and safety as separate questions, every open problem in the code-signing stack lines up in one direction: how do you reduce the blast radius of the inevitable next valid-but-malicious signature?

## 10. Open problems

Five problems are concrete enough to call out as ongoing work.

**O1. Post-quantum Authenticode.** Microsoft has not yet published a `SpcIndirectDataContent` variant that references the [ML-DSA](/blog/post-quantum-cryptography-on-windows-the-thirty-year-migrati/) (FIPS 204 [@fips-204]) or SLH-DSA (FIPS 205 [@fips-205]) OIDs. The CA/B Forum CSBR has not named a post-quantum algorithm for code-signing certificates; the current CSBR v3.8 [@cabf-cs-documents] still rests on RSA and ECDSA. NIST's PQC programme has set a 2035 deadline to deprecate quantum-vulnerable algorithms [@nist-pqc]. The CMS extensibility precedents are there: RFC 8554 profiles stateful LMS [@rfc-8554], RFC 8419 profiles EdDSA in CMS [@rfc-8419], and there is no architectural reason the same approach cannot profile ML-DSA. A hybrid-signed binary that carries both an RSA and an ML-DSA `SignerInfo` inside the same `SignedData` is technically possible today, and Microsoft will likely have to ship it before catastrophic loss of confidence in RSA can happen.<Sidenote>FIPS 204 (ML-DSA) and FIPS 205 (SLH-DSA) were both finalised on 13 August 2024 [@fips-204][@fips-205]. The standards are stable; what is missing is the Authenticode-side OID registration and the Hardware Developer Center portal-signing pipeline that would emit a PQ counter-signature. The CSBR side and the Microsoft side both have to move; neither has publicly committed to a date.</Sidenote>

**O2. Per-page integrity for non-PE artefacts.** Page hashes inside `SpcPeImagePageHashes2` [@authenticode-pe-docx] are PE-specific. PowerShell scripts, MSIX packages, Appx packages, and the `.cat` files themselves rely on whole-file Authenticode hashing; if an attacker can corrupt a single byte after load, the OS does not currently re-hash. HVCI gives PE binaries a runtime check; the script and package side does not yet have an equivalent.

**O3. Transparency logs for Authenticode countersignatures.** RFC 3161 TSAs do not publish their issued tokens. A backdated countersignature from a compromised TSA is currently undetectable beyond CA revocation. Sigstore Rekor [@rekor-github] demonstrates that a transparency log integrates with a signing pipeline at low overhead; there is no equivalent for the Microsoft-signed-driver world or for third-party Authenticode signers.

**O4. Revocation propagation latency.** The gap between "the CA revokes" and "every endpoint refuses to verify" is empirically days to weeks. CRLs are downloaded on a cadence (with `EnableCertPaddingCheck` aside, OCSP is not even applied to Authenticode by default). The VDB's quarterly cadence [@mslearn-recommended-driver-block-rules] is faster than CRL-only and slower than the rate at which attackers can stand up an attack with a freshly stolen certificate. Some of this is unavoidable -- you cannot push a revocation faster than an offline endpoint can reach Windows Update -- but a structurally better answer is one of the open questions.

**O5. Post-CrowdStrike (July 2024) kernel-driver-loading discipline.** Microsoft's Windows Resiliency Initiative was announced in the wake of the 19 July 2024 CrowdStrike Falcon Sensor outage; a fully-specified replacement for today's third-party kernel-driver model has not yet shipped. A successful answer would push parts of today's Authenticode + KMCS + WDAC story toward sandboxed user-mode driver frameworks, with the kernel restricted to a much narrower interface. The Authenticode primitives this article has dissected will still be the substrate; what gets layered on top is the open architectural question.

> **Note:** This article is about the *crypto foundation* under WDAC: the bytes on disk, the envelope structures, the chain of trust. It does not cover the runtime enforcement layer -- how Code Integrity, HVCI, and the secure kernel use these primitives at process- and driver-load time, how page hashes are checked at fault time, how the Vulnerable Driver Blocklist is loaded as a supplemental policy. That story is the subject of the next post in this series.

The next decade of Windows code-signing is going to be dominated by post-quantum migration and by whatever the Windows Resiliency Initiative converges to. Both will be evolution, not revolution: they will sit on top of the certificate-table, catalog-store, and timestamp-token primitives that have been load-bearing since 1996. To finish, the day-to-day commands that interrogate every byte we have discussed.

## 11. Practical guide: signtool, certutil, New-CIPolicyRule

If you have read this far, you should be able to run the following commands on a Windows host and explain every field of their output. Microsoft's `signtool`, `certutil`, and the `ConfigCI` PowerShell module are the canonical tools [@mslearn-crypto-tools].

### Verify a signed binary end to end

```text
signtool verify /v /pa /all "C:\Path\To\binary.exe"
```

The output prints, in order: the SHA-256 of the file's Authenticode hash, the leaf certificate's subject and issuer, every intermediate up to the trusted root, the RFC 3161 timestamp's `genTime`, and the policy used to validate. `/pa` opts into the "default authenticode" policy (instead of the deprecated `MicrosoftRoot` policy); `/all` walks every signature on the file rather than just the strongest.

### Compute and look up an Authenticode hash

```text
certutil -hashfile "C:\Path\To\driver.sys" SHA256
certutil -CatDB "C:\Windows\System32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}" /v /search <hash>
```

The `-hashfile` command emits the *file* SHA-256, which is *not* the Authenticode hash (the file SHA-256 includes the certificate-table bytes; the Authenticode hash excludes them). The Authenticode hash is what is stored inside each catalog's `CatalogList`. `Get-AuthenticodeSignature` is the easier PowerShell route to the Authenticode hash directly.

### Walk the catalog store

```text
Get-ChildItem "C:\Windows\System32\CatRoot" -Recurse | Select-Object FullName
```

The GUID-named subfolder is the CryptSvc policy database identifier; the `.cat` files inside are individually-signed `SignedData` blobs whose `encapContentInfo` is a `CatalogList` [@mslearn-catalog-files]. `CatRoot2` holds staging copies and the catalog database index.

### Generate a WDAC rule

```powershell
New-CIPolicyRule -FilePath "C:\Path\To\App.exe" -Level FilePublisher
```

This produces an XML `<FileRule>` element with the issuer, subject CN, original file name, and minimum file version. Pipe the result into `New-CIPolicy` to build a policy XML; convert to binary with `ConvertFrom-CIPolicy` and deploy via Group Policy or Intune.

### Decide between embedded and catalog signing

For an internal line-of-business app shipped as a single MSI, embedded signing is the default and the cleanest choice. For a multi-binary package where some files are third-party and unsignable, the Package Inspector workflow [@mslearn-deploy-catalog-files] builds a `.cat` covering the post-installation file set without modifying any binary:

```text
PackageInspector.exe Start C:\
... install your app ...
PackageInspector.exe Stop C:\ -Name MyApp.cat -ResultsFile C:\Temp\MyApp_inspection.txt
```

### Confirm a kernel-mode chain

```text
signtool verify /v /pa /kp "C:\Windows\System32\drivers\example.sys"
```

The `/kp` policy uses the kernel-mode driver policy: the chain must terminate at a kernel-mode-trusted root (the `Microsoft Code Verification Root` family of anchors, or a portal-signed-driver Microsoft Root Authority anchor). `certutil -store -enterprise root` enumerates the local kernel-mode roots; the legacy `Microsoft Code Verification Root` is named on the KMCS policy page [@mslearn-kmcs-policy] but its thumbprint is not published on a stable Microsoft Learn URL -- you read it via `certutil -store` on the running system.

### Make an informed `EnableCertPaddingCheck` decision

The strict-parser registry value lives in two places. Set both:

```text
reg add "HKLM\Software\Microsoft\Cryptography\Wintrust\Config" /v EnableCertPaddingCheck /t REG_DWORD /d 1 /f
reg add "HKLM\Software\Wow6432Node\Microsoft\Cryptography\Wintrust\Config" /v EnableCertPaddingCheck /t REG_DWORD /d 1 /f
```

CISA added CVE-2013-3900 to the Known Exploited Vulnerabilities catalogue on 10 January 2022 [@nvd-cve-2013-3900]; treat this as effectively mandatory in any hardened-baseline build.

### Annotated `signtool verify` output

```text
Verifying: notepad.exe
Hash of file (sha256): 6B9B7E...   <-- Authenticode hash, the same one
                                       inside SpcIndirectDataContent.messageDigest
Signing Certificate Chain:
  Issued to: Microsoft Root Certificate Authority 2010   <-- root anchor
    Issued by: Microsoft Root Certificate Authority 2010
  Issued to: Microsoft Windows Production PCA 2011        <-- intermediate / PCA
    Issued by: Microsoft Root Certificate Authority 2010
  Issued to: Microsoft Windows                             <-- leaf / signer
    Issued by: Microsoft Windows Production PCA 2011
The signature is timestamped: Thu Jul ...                 <-- RFC 3161 genTime
Timestamp Verified by:
  Issued to: Microsoft Time-Stamp PCA 2010                <-- TSA chain
  Issued to: Microsoft Time-Stamp Service
File is signed and the signature was verified.
```

<RunnableCode lang="ts" title="Wrap certutil -CatDB to look up an Authenticode hash">{`
// Cross-platform pedagogy: this snippet shows the flow of a catalog lookup.
// On Windows, "certutil -CatDB <CatRootPath> /v /search <hash>" returns the
// covering catalog file. Off Windows, we mock the output so the flow is visible.

interface CatalogLookupResult {
  hash: string;
  catalogFile: string | null;
  signerSubject: string | null;
}

function lookupCatalog(authenticodeHash: string): CatalogLookupResult {
  // Real implementation would shell out to:
  //   certutil -CatDB <CatRoot GUID path> /v /search <authenticodeHash>
  // Parse the output for "Hash: <hash>  Catalog: <path>".
  const known: Record<string, CatalogLookupResult> = {
    "6B9B7E...": {
      hash: "6B9B7E...",
      catalogFile: "C:\\\\Windows\\\\System32\\\\CatRoot\\\\{F750E6C3-...}\\\\Package_for_KB12345.cat",
      signerSubject: "CN=Microsoft Windows Production PCA 2011"
    }
  };
  return known[authenticodeHash] || { hash: authenticodeHash, catalogFile: null, signerSubject: null };
}

const r = lookupCatalog("6B9B7E...");
console.log(r.catalogFile ? "Catalog-signed by " + r.signerSubject : "Not catalog-covered");
`}</RunnableCode>

> **Note:** The most common practitioner mistake is `signtool sign /n <name>` without `/tr <tsa-url> /td sha256`. A signature produced this way silently loses validity the moment the end-entity certificate expires -- which can be years later, when the signer has long since lost access to whatever signing key produced it. The fix is to always include `/tr` and a strong `/td`. RFC 3161 [@rfc-3161] is the entire reason long-lived signatures still verify; opting out of it is opting out of the longevity guarantee.

<Spoiler kind="solution" label="Why your internally-signed LOB app trips SmartScreen">
SmartScreen Application Reputation is not gated on Authenticode validity. It is gated on certificate *class* (EV vs. OV) and on aggregate *download volume* and reporting. An internally signed enterprise LOB app has neither: it is signed with an OV certificate, and its download volume is at most a few hundred enterprise users. The fix has two paths. The cheap one is to ride your enterprise WDAC policy rather than fight SmartScreen -- App Control rules allow the binary unconditionally inside your organisation. The expensive one is to buy an EV certificate, push the binary through a small early-access user pool, and let SmartScreen accumulate the reputation signal. Both work. Fighting SmartScreen with a louder OV signature does not.
</Spoiler>

These seven commands cover the full surface of what Authenticode, catalog signing, and WDAC let a Windows engineer actually inspect. Everything else in this article is context for what those command outputs *mean*.

## 12. Frequently asked questions

<FAQ title="Frequently asked questions">

<FAQItem question="What is the difference between an Authenticode signature and a generic 'code signature'?">
Authenticode is a specific PKCS#7 / CMS profile for signing Windows portable executables, catalog files, and a small set of related artefacts. It is defined by Microsoft's `Authenticode_PE.docx` specification [@authenticode-pe-docx] and is characterised by a PE-specific Authenticode hash (with four exclusions), the `SpcIndirectDataContent` content type at OID `1.3.6.1.4.1.311.2.1.4`, and the `WIN_CERTIFICATE` certificate-table wrapper. Other code-signing schemes -- JAR signing for Java, APK Signature Scheme v3 for Android [@android-apk-v3], sigstore/cosign for OCI artefacts [@sigstore-overview], Apple Notarization for macOS [@apple-notarization] -- are not Authenticode-compatible. They solve similar problems with different envelopes.
</FAQItem>

<FAQItem question="If a publisher's end-entity certificate expires, does my signed binary stop working?">
Not if the signature was RFC 3161 timestamped at signing time. The `TimeStampToken` in the unsigned attributes pegs the signing event to a `genTime` from a Trusted Time-Stamping Authority [@rfc-3161]; later verifiers compare `genTime` to the signing certificate's validity window and honour the signature so long as `genTime` was inside that window. The signature *will* stop working on hash-only WDAC rules (which do not consult certificate expiry at all) and on the rare verifiers that enforce chain time at validation. Signing without `/tr` is the way to produce a signature that silently loses validity at end-entity-cert expiry; that is the single most common Authenticode mistake at signing time.
</FAQItem>

<FAQItem question="Can I still install an unsigned kernel driver on Windows 11?">
Only by enabling Test Signing mode (which puts a watermark on the desktop and refuses to coexist with Secure Boot), or by booting with Driver Signature Enforcement disabled (which is a one-boot bypass), or by using a vulnerable signed driver to load your unsigned code (the entire point of the Vulnerable Driver Blocklist [@mslearn-recommended-driver-block-rules]). Production loading of an unsigned driver on a normally configured Windows 11 system is not supported. Cross-signing for new end-entity certs has been closed since the 29 July 2015 issuance cutoff [@mslearn-kmcs-policy]; cross-certificates expired by July 2021 [@mslearn-deprecation-spc-crc].
</FAQItem>

<FAQItem question="Why does my company's internally signed app still trip SmartScreen?">
See the §11 Spoiler *"Why your internally-signed LOB app trips SmartScreen"* for the detailed explanation of why SmartScreen Application Reputation weights certificate class (EV vs. OV) and download volume rather than Authenticode validity, and for the two production fixes (ride your enterprise App Control policy, or buy an EV certificate and let reputation accumulate). The one-line summary: Authenticode and SmartScreen are different decision systems that happen to read the same `SignerInfo` -- making your signature *louder* in Authenticode does not buy you reputation in SmartScreen.
</FAQItem>

<FAQItem question="What is the difference between Microsoft Code Verification Root and Microsoft Code Signing PCA?">
The `Microsoft Code Verification Root` is the historical kernel-mode trust anchor whose intermediate cross-signed third-party kernel code-signing CAs for pre-July-2015 drivers [@mslearn-kmcs-policy]. It is named in the KMCS policy document; its thumbprint is not published on a stable Microsoft Learn URL, so practitioners read it via `certutil -store` on the running system. The `Microsoft Code Signing PCA` family of intermediates (and its newer cousins like `Microsoft Windows Production PCA 2011`) are user-mode signing chains used for Microsoft-internal binaries and most WHQL catalogs. Both feed into `WinVerifyTrust`; they differ in which downstream consumer treats them as authoritative -- the kernel for the former, user-mode trust decisions for the latter.
</FAQItem>

<FAQItem question="Is the Authenticode hash the same as SHA-256 of the file?">
No. The Authenticode hash excludes four PE regions: the optional-header `CheckSum` (4 bytes), the `IMAGE_DIRECTORY_ENTRY_SECURITY` data-directory entry (8 bytes), the certificate-table bytes themselves, and the file-alignment padding after each section [@authenticode-pe-docx]. So `(Get-AuthenticodeSignature notepad.exe).Hash` returns a different value than `certutil -hashfile notepad.exe SHA256`. The Authenticode hash is what is stored inside `SpcIndirectDataContent.messageDigest` and what is matched against catalog `memberHash` entries; the file SHA-256 is useful for forensic identification but does not appear anywhere in the signature flow.
</FAQItem>

<FAQItem question="Why does WDAC distinguish Publisher, FilePublisher, and WHQLFilePublisher?">
They differ in precision and in which Authenticode fields they consult [@mslearn-select-types-of-rules]. `Publisher` allows anything signed by a given issuing CA + leaf-cert subject CN; broadest but loosest. `FilePublisher` adds `OriginalFileName` + `MinimumFileVersion` constraints; tightens to a specific binary at a min version. `WHQLFilePublisher` further requires the WHQL EKU; the strictest commonly used rule level. Self-updating apps invalidate `FilePublisher` rules silently when their `OriginalFileName` or `FileVersion` change without warning [@mslearn-use-code-signing]; most enterprises start at `Publisher` and tighten only for high-risk binaries.
</FAQItem>

<FAQItem question="Did the CVE-2013-3900 fix ever ship as default-on?">
No. NVD's verbatim Microsoft language: *"Microsoft does not plan to enforce the stricter verification behavior as a default functionality on supported releases of Microsoft Windows. This behavior remains available as an opt-in feature via reg key setting, and is available on supported editions of Windows released since December 10, 2013"* [@nvd-cve-2013-3900]. CISA added the CVE to the Known Exploited Vulnerabilities catalogue on 10 January 2022 with a federal due date of 10 July 2022. Hardened environments should set `EnableCertPaddingCheck=1` in both the native and `Wow6432Node` registry paths.
</FAQItem>

</FAQ>

## 13. Closing reflection

In August 1996 the Authenticode trust decision was a single yes/no answer to a single question: did this PKCS#7 SignedData blob, attached to this downloadable ActiveX control, validate against a CA in the user's browser? Thirty years later, the trust decision is a chained question composing every primitive in this article: a `WIN_CERTIFICATE` record points to a `SignedData` envelope; the envelope's `SpcIndirectDataContent` carries an Authenticode hash and optional page hashes; an unsigned attribute carries an RFC 3161 timestamp; the catalog store may carry a parallel signature for the same hash; the certificate chain terminates at one of a small set of Microsoft anchors for kernel-mode loads; an administrator's App Control policy decides whether the verdict survives the rule evaluation; the Vulnerable Driver Blocklist denies a small curated list outright.

The cryptography has not moved. The certificate table is still where the bytes live. PKCS#7 SignedData is still the envelope. RSA is still the signature algorithm. What has changed -- and what is going to keep changing through the post-quantum migration and whatever the Windows Resiliency Initiative converges to -- is the layering of policy on top.

Authenticode is not the ceiling. It is the floor. Everything else is built on top, and the next time a Realtek certificate is stolen, those layers are what decides whether the next Stuxnet still loads.

<StudyGuide slug="authenticode-and-catalog-files-the-crypto-foundation-under-wdac" keyTerms={[
  { term: "Authenticode", definition: "Microsoft's PKCS#7 / CMS profile for signing Windows PE binaries, defined by Authenticode_PE.docx." },
  { term: "WIN_CERTIFICATE", definition: "The PE certificate-table record (dwLength, wRevision, wCertificateType, bCertificate[]) wrapping the PKCS#7 SignedData blob." },
  { term: "SpcIndirectDataContent", definition: "Microsoft eContentType (OID 1.3.6.1.4.1.311.2.1.4) whose messageDigest is the Authenticode hash; signs a hash, not a file." },
  { term: "Authenticode hash", definition: "The PE digest computed with four regions excluded (CheckSum, SECURITY data-directory entry, certificate-table bytes, section-padding)." },
  { term: "Page hash (SpcPeImagePageHashes2)", definition: "Signed attribute carrying per-4 KiB-page hashes for HVCI demand-fault-time verification." },
  { term: "Catalog file (.cat)", definition: "A degenerate SignedData whose encapsulated content is a CatalogList of (memberHash, attributes) tuples; detached signature." },
  { term: "CatRoot / CryptSvc", definition: "On-endpoint catalog store at %SystemRoot%\\System32\\CatRoot\\{GUID}\\ and the service that indexes member hashes." },
  { term: "Trusted Time-Stamping Authority (TSA)", definition: "RFC 3161 service that counter-signs a signature's hash with a trusted genTime, attached as an unsigned attribute." },
  { term: "WinVerifyTrust", definition: "CryptoAPI function orchestrating the Authenticode verification pipeline." },
  { term: "Code Integrity / ci.dll", definition: "Windows kernel-mode component enforcing KMCS on driver loads and feeding page hashes to HVCI." },
  { term: "Microsoft Code Verification Root", definition: "Historical kernel-mode trust anchor for cross-signed third-party drivers; thumbprint read via certutil -store." },
  { term: "App Control for Business (WDAC)", definition: "Post-2024 rename of Windows Defender Application Control; consumes Authenticode primitives as policy inputs." },
  { term: "FilePublisher rule", definition: "WDAC rule level allowing Publisher + OriginalFileName + MinimumFileVersion combinations." },
  { term: "Vulnerable Driver Blocklist (VDB)", definition: "Microsoft-curated supplemental deny policy enabled by default since Windows 11 22H2; quarterly cadence." },
  { term: "RFC 3161 TimeStampToken", definition: "CMS SignedData over hash(signature) || genTime, attached at OID 1.2.840.113549.1.9.16.2.14 as an unsigned attribute." }
]} />
