A kernel driver with 256 zero bytes where its RSA signature should be ought to fail signature validation. Mine loaded. Secure Boot was on, Hypervisor-Protected Code Integrity, or HVCI, was on, and test signing was off. Windows had no complaints.

I found this while tracing the legacy driver validation path in Windows Code Integrity, CI.dll. The result applies to a specific class of driver. Its end-entity signing certificate must have been issued before July 29, 2015, and must chain to a supported cross-signed CA. On that path, I could erase the entire SignerInfo.EncryptedDigest field and still load the driver on Windows 10 and 11.

The interesting part was how much validation still worked. CI.dll rejected broken certificate chains, malformed signature structures, incorrect Authenticode hashes, and bad timestamps. It accepted a signature whose RSA bytes were all zero. I wanted to understand how it could be that particular about the packaging and still miss the proof inside it.

A date decides which checks run

CI.dll has a hardcoded cutoff that splits driver signature validation into legacy and modern paths. Signing certificates issued before July 29, 2015 fall into the grandfather path, with weaker enforcement. The binary stores the cutoff as a FILETIME:

FILETIME: 0x01D0C991830F4000 (July 29, 2015 00:00:00 UTC)
Little-endian bytes: 00 40 0F 83 91 C9 D0 01

Microsoft’s documentation describes a fuller set of checks. Validation should verify the certificate chain, enforce expiration, validate the timestamp, check RSA or ECDSA correctness, compare the Authenticode hash, and check code signing Extended Key Usage, or EKU. The legacy path still does most of that work. Finding the missing check meant separating those decisions instead of treating “signature validation” as one operation.

What is inside the signature

A signed Portable Executable, or PE, stores its signature in the Security Directory as a WIN_CERTIFICATE structure. Inside that is a PKCS#7 blob containing the Authenticode hash of the binary, the certificate chain back to a trusted root, and a SignerInfo block.

SignerInfo.EncryptedDigest holds the raw RSA signature bytes. Those bytes are the cryptographic proof that connects the file to the holder of the private key. A correct verifier uses the public key from the signing certificate to verify the signature and establish that the signed digest agrees with the independently computed file hash. A matching hash by itself cannot prove who signed the file. The signature has to contribute to the decision too.

That distinction gave me something concrete to test. A driver could retain a valid structure, a trusted certificate chain, and a correct file hash while losing the RSA proof entirely.

Breaking one thing at a time

Static reverse engineering gave me the control flow, but it did not make the enforcement gap obvious. I got a clearer answer by starting with a legitimately signed driver that already loaded and changing one signature component per test. If a modified driver failed, I knew which change had caused it. If it loaded, I had a check worth investigating.

I wrote tooling to parse the signature structures, apply each mutation, and reassemble the binary. Every test ran on Windows 10 and 11 with Secure Boot and HVCI enabled and test signing disabled. Keeping that configuration fixed mattered because a successful load with enforcement turned off would tell me very little about CI.dll.

Of the nine mutations, eight caused a load failure:

  • Zeroing certificate signatures.
  • Truncating the PKCS#7 structure.
  • Changing the WIN_CERTIFICATE revision field.
  • Changing the certificate type field.
  • Stripping the timestamp countersignature.
  • Backdating the timestamp.
  • Corrupting certificate data.
  • Corrupting the Authenticode hash.

The ninth was zeroing EncryptedDigest. That driver loaded without error. The failures around it are what make the result useful. They show that the grandfather path was still enforcing structure, certificate chain validity, hash correctness, and timestamp presence. The RSA signature was the component that could be destroyed without changing the outcome.

The zero signature test

The mutation code walks the PKCS#7 blob looking for ASN.1 OCTET STRING tags, 0x04, followed by the long-form length encoding 0x82. That encoding uses a two-byte length field. Driver certificate RSA signatures are between 128 and 512 bytes, so the code zeros matching content regions in that range:

bool mutZeroEncryptedDigest(BYTE* pkcs7, DWORD len) {
    int found = 0;
    for (DWORD i = 0; i < len - 4; i++) {
        if (pkcs7[i] == 0x04 && pkcs7[i+1] == 0x82) {
            DWORD sigLen = (pkcs7[i+2] << 8) | pkcs7[i+3];
            if (sigLen >= 128 && sigLen <= 512 && i + 4 + sigLen <= len) {
                memset(&pkcs7[i+4], 0, sigLen);
                found++;
            }
        }
    }
    return found > 0;
}

In this test, 256 bytes of RSA signature data became 256 zero bytes. The driver still loaded under the same security configuration. I saw no errors, warnings, or audit events.

Checking the result with another binary

A second test let me check the finding independently. I took the signature from a signed donor binary and transplanted it into an unsigned target:

  1. Compute the Authenticode hash of both files.
  2. Extract the donor’s complete signature.
  3. Replace the donor’s Authenticode hash inside that signature with the target’s hash.
  4. Zero the RSA signature bytes, since I could not re-sign without the original private key.
  5. Attach the modified signature to the target.

The target driver loaded. The embedded hash had to match the target, so the Authenticode comparison still mattered. The PKCS#7 wrapper had to remain well formed, so parsing still mattered too. But the transplanted EncryptedDigest contained only zeros, and that did not prevent acceptance.

This was a useful second test because it separated possession of a signature blob from possession of the key that created it. The target had the former and none of the latter.

The timestamp appears to carry the trust

The timestamp countersignature is the most likely explanation. When the signing certificate predates the cutoff and the signature has a valid timestamp from a trusted Time Stamping Authority, or TSA, CI.dll appears to rely on the timestamp signature instead of checking the primary RSA signature. It confirms that the signature is present without confirming that its RSA bytes are correct.

Performance is one possible reason for that behavior. RSA verification has a computational cost, and skipping it could reduce driver load latency if a trusted timestamp is treated as sufficient. I cannot establish that design intent from the mutation results. What the tests show is narrower. Without a valid timestamp, nearly every mutation failed. With one, changing the RSA bytes no longer affected the result.

Following the validation calls

I narrowed the main path to four functions in CI.dll. CiValidateImageHeader is the entry point, reached indirectly from nt!SeValidateImageHeader through the SeCiCallbacks table. CiCheckSignedFile handles primary signature validation and PKCS#7 parsing. MinCryptVerifySignedDataLMode decodes ASN.1 and builds the certificate chain, and I_MinCryptVerifyTimeStampSignature validates the timestamp and compares it with the grandfather date.

The cutoff is also a useful reference when reading the binary. Searching for its bytes leads to the branch where the legacy and modern paths split:

00 40 0F 83 91 C9 D0 01

What can block the load

Secure Boot and HVCI stayed enabled throughout the successful loads. They did not stop a driver with zeroed RSA bytes from passing this particular legacy path. That limit matters because a system can have both features switched on and still reach the affected validation logic.

The most direct countermeasure is the Vulnerable Driver Blocklist. If it blocks the signing certificate or file hash, Windows rejects the load regardless of which signature path the driver would take. Certificate revocation and out-of-band blocklist updates through Windows Update add protection. HVCI remains another enforcement layer, although it did not catch the signature gap in these tests.

Most Driver Signature Enforcement, or DSE, bypasses require disabling Secure Boot or enabling test signing. Bring Your Own Vulnerable Driver, or BYOVD, is a familiar exception because it starts with a legitimately signed but vulnerable driver. This grandfather path also works with Secure Boot active, and in my tests with HVCI active as well.

I do not expect Microsoft to remove the legacy path outright. Thousands of legitimate enterprise drivers depend on it, so blocking abused certificates and tightening signing requirements over successive Windows releases is the more practical approach. Compatibility explains why the path has to exist. It leaves a fairly awkward result to account for, though. A pre-cutoff certificate chain and valid timestamp can get a driver through validation even when its RSA signature contains nothing but zeros.