Files
Domito/docs/ci-validate-file-object.md
T
nefarius 3fbfe31d15 Add reusable certificate and file validation helpers
Fix process-name ownership and module lookup outputs, add explicit SPKI hashing and bounded chain access, and improve WDM integration. Deprecate superseded entry points with migration guidance and align branch code and tests with the library style.

Add CI validation helpers with explicit signature selection and owned results

Document runtime-confirmed CI signer selection and crypto reuse

Expand CI compatibility matrix to twenty verified samples

Extend CI export matrix with Windows 11 x64 and ARM64 samples

Document CI API availability and historical mincrypt layouts

Add explicit Authenticode workflows and correct CI helper contracts

Document CI validation flag 0x20 and add kernel probe

Harden CI digest parsing and failure cleanup
2026-10-27 00:17:25 +01:00

13 KiB

CiValidateFileObject: validation flag 0x00000020

Issue #3 is documented; its TESTSIGNING-off runtime experiment is pending. A related signer-selection experiment has now run successfully on Windows 11 22631 with TESTSIGNING enabled (below). That run does not close the requested 19041/19045 flag 0 versus 0x20 TESTSIGNING-off comparison.

Meaning supported by the evidence

DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE is Domito's descriptive name for 0x00000020 in the second argument, ValidationFlags. It is not a recovered Microsoft macro name. On the analyzed ci.dll 10.0.26100.8328 x64, the flag causes CipGetAcceptedRootKeys to OR 0xA4FF into its accepted-root mask in the branch where the global at RVA 0x45A88 is zero. Other global state, flags, and the requested signing level also affect the result.

The assembly at preferred image base 0x180000000 establishes the operation:

0x180089e40: MOV EDX, 0xa4ff
0x180089e4c: BT ECX, EDI          ; ECX = 0x8c in this branch; EDI = signing level
0x180089e4f: JC 0x180089e57
0x180089e51: TEST SIL, 0x20      ; saved ValidationFlags
0x180089e55: JZ 0x180089e59
0x180089e57: OR EBX, EDX         ; broaden accepted-root mask

For requested level 0, flags 0, the branch above, and all other optional global additions disabled, the mask is 0x80AA; setting only 0x20 makes it 0xA4FF (additional bits 0x2455). This is an arithmetic witness, not a measurement of the running OS's initialized policy. Levels 2, 3, and 7 already trigger the OR through 0x8C. When the global at RVA 0x45A88 is nonzero, the helper returns 0xA4FF directly, so the flag need not change its result.

Later processing can still remove bit 0x8 when the helper's third argument is nonzero and validation flag 0x00400000 is present. The surrounding CipDisallowEmbeddedPicrootIfTrusted path can add that latter flag itself. Thus 0xA4FF is not a promise about every final policy result.

The widened mask includes categories associated with test roots and unresolved chains as well as other roots. It must not be named "trust any Authenticode signer", "require Microsoft", or "enable TESTSIGNING". It changes the acceptance policy for this operation; it does not change the system boot setting. Further signature, chain, signing-level, and policy checks still matter. A successful call alone does not establish equivalence with WinVerifyTrust or the caller's intended publisher trust policy.

CipGetHashAlgorithmForLegacyScenario does not test 0x20: the flag is not a fixed SHA-1/SHA-256 selector. Also distinguish this input flag from the unrelated MINCRYPT_POLICY_INFO.PolicyBits output bit 0x20, associated with the built-in DMD root category in this build.

Historical generation differences

The same numeric flag does not imply the same internal root mask across Windows versions. Static analysis of the extracted x64 corpus adds these bounded facts:

  • On sampled 10.0.10240.16463 (1507), 10.0.10586.0 (1511), 10.0.14393.2189 (1607) and 10.0.15063.0 (1703), the recovered CipGetAcceptedRootKeys bodies have no direct flag & 0x20 broadening branch. They still test larger masks containing that bit, so this is not evidence that the flag has no effect anywhere in those verifiers.
  • On sampled 10.0.16299.125 (1709) and 10.0.17134.1130 (1803), when the helper's main global mode is zero, flag & 0x20 selects mask 0x224FF (assignment), rather than the modern mask |= 0xA4FF operation. A signing-level predicate can select the same mask. Other global additions occur afterward. The other global mode returns 0x204FF directly.

The observed helper masks include branch-specific diagnostics/categories and must not be interpreted as an official stable root enum. The descriptive Domito constant names the intended broadening use without promising a single mask or identical behavior on every build.

The ISO-derived 10.0.19041.1 x64 helper provides direct static evidence for the reported build family: in its main global mode it uses mask |= 0x24FF when flag 0x20 or the signing-level predicate applies; the alternate mode returns 0x24FF. With requested level zero and optional global additions disabled, 0xAA becomes 0x24FF (added bits 0x2455). This is an arithmetic witness, not a measurement of initialized OS policy. The exact CI SHA-256 is 2955710d34db26e352e62b20861ba338f0216cc36b378d128057be955e184282. CodeIntegrity-RE retains matched-PDB pseudocode and assembly in historical/ci-19041.1-contracts.json and historical/ci-19041.1-assembly.json, with batch evidence ID ev_13f89502777ee38ea98410f7a07f078bb934e78d792b662ab242f7b11d27e747. Other 19041/19045 servicing revisions and the TESTSIGNING-off runtime workaround still require their own evidence.

Historical records in CodeIntegrity-RE: historical/ci-14393-contracts.json ev_e63a4c4b7613e231795051b784b9571605b9bf81258b5dcd04d32ddc681abb41, historical/ci-15063-contracts.json ev_338658fb82559fa6daa8ae5487b90fe84b8e408ea7578feed6d8dff5d590e114, historical/ci-16299-contracts.json ev_fe5731175f0726f0d46af2f78ca615d284f8598939296815e26b5589db3001cf, and historical/ci-17134-contracts.json ev_0c3e3359b8b7486318006afa6b95eb2257a18abeec9041a86a32db487bcbe996.

Evidence and compatibility limits

The upstream report describes success with flags 0x20, signing level 0, and TESTSIGNING off on Windows builds 19041 and 19045. That is third-party runtime evidence, not a reproduction by this project. Its behavior is consistent with broader root acceptance; it does not establish identical internals across those builds.

Our static evidence uses REA 6.8.0 / Ghidra 12.1.3 and the binary SHA-256 c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b. Public-symbol mapping uses ci.pdb GUID 1CAE574B-CF1F-3564-5F74-49567A128629, age 1. The companion CodeIntegrity-RE repository retains:

  • ci-flag20-assembly-evidence.json: assembly evidence ev_ea763b9be7643a6241b0c805eae5f735a4b4a0a82df1383b1e68da4207f85fb4.
  • evidence/CipGetAcceptedRootKeys.c: decompilation ev_c905f03db9470c8b2d89a6295080143f25d5780ca0909de0ca554e40d078a3f3.
  • evidence/CipGetHashAlgorithmForLegacyScenario.c: ev_addf8f1bc18da58db80db54eeab0dd78ec5ab88b219ece3e082ebd9a8e50b257.
  • evidence/CipDisallowEmbeddedPicrootIfTrusted.c: ev_2475a5804fc87683a54b06acebd3785e5e8eb31f4f542f699fc2ff414c1ab7fe.
  • evidence/CipValidateFileObjectByFileOrPageHash.c: capacity contract ev_3897aa76c17cc64a30f60e72b994e695b88c8eda2ae6f246766bdc9314f06cca.

No complete enumeration of downstream uses of the flag, historical Microsoft name, or cross-version/architecture guarantee is claimed. The public declaration keeps the existing parameter widths. Names now describe the known inputs; on 26100 x64 the third argument is consumed as a byte. Narrowing the ABI everywhere requires separate evidence.

Kernel experiment prepared for the VM

Add tests/ci-file-object-probe.cpp to an existing signed test driver and add include/ to its include path. Link the existing CI imports (ci.lib). Include ntifs.h, Domito.MinCrypt.h, and tests/ci-file-object-probe.h at the call site. Invoke DomitoProbeCiValidateFileObject at PASSIVE_LEVEL with a referenced file object for the fixture and an array of DOMITO_CI_PROBE_RESULT_COUNT results. Keep the file bytes stable and hold the object reference for the entire probe. The surrounding driver owns opening/referencing/closing the file and exporting the result records. The probe has no DriverEntry or installation side effects.

It makes six calls, always at requested level 0:

  1. Flags 0, then 0x20, then 0x20, then 0, with 64-byte capacity each time.
  2. Flags 0, then 0x20, with declared capacity 63 (64-byte backing storage).

Each call uses fresh policies, hash capacity, algorithm, and signing-time storage. Both policy objects are freed after capture, including on failure. The records capture status, both policy statuses/bits, chain presence, signing time, digest algorithm/length/bytes, and a trailing buffer guard. The digest is a file hash, not a certificate thumbprint. Failed-call outputs are raw diagnostics, not valid verification results. Chain internals are deliberately not dereferenced.

The repeated order helps reveal cache/order effects but does not eliminate them. Also run the reversed initial order in a fresh VM snapshot when comparing policy. For the analyzed 26100 helper, both 63-byte-capacity calls should return STATUS_BUFFER_TOO_SMALL and set length 64, regardless of the eventual algorithm. Treat older-build differences as observations to investigate, not silently as test success. Every guard must remain intact; successful hash sizes must be at most 64. Inspect non-success statuses rather than treating all failures as invalid signatures (I/O and resource errors are possible).

Run on Windows 10 19041 and 19045 and a current Windows 11 build. The decisive reproduction uses a driver loadable with TESTSIGNING off. Record that setting, Secure Boot, HVCI/WDAC policy, OS build, CI version/hash, driver identity, fixture SHA-256, and each complete result. Keep policy constant between paired calls. Use these fixtures:

  • A known third-party RSA Authenticode executable (ideally the reported fixture).
  • A Microsoft-signed executable and a catalog-signed file as controls.
  • An unsigned executable and a copy of the signed file with a byte changed in a hashed section, leaving its certificate table unchanged.
  • A self-signed/test-root fixture to characterize the broader acceptance policy.
  • If available, timestamped, expired, revoked, and ECC variants, with expectations recorded in advance and revocation/configuration differences retained.

Compare with signtool verify /pa /v as a separate user-mode policy observation, not an equality oracle. The report is reproduced only when the same valid third-party fixture changes from rejection at 0 to success at 0x20, under the reported environment, with usable outputs. Unchanged success can mean the existing policy already accepts it. Unchanged failure is a result to investigate. Unsigned and tampered fixtures must not become accepted as valid signatures. Self-signed acceptance must be reported explicitly, not confused with public-CA publisher trust. Do not use this flag as a new production default based only on one positive fixture.

Multiple signatures: a separate kernel experiment

0x20 is an accepted-root control, not a recovered "PE image" type flag. In the examined 22621.2792 and modern 26100 x64 bodies it leaves the preferred-signing-level search active. PolicyInfo can therefore describe a nested Microsoft signature even when the primary company signature also verifies.

The first-success guard tests aggregate masks 0xE47FFF87 (22621.2792) and 0xE41FFF87 (modern 26100). 0x20 intersects neither; 0x80 intersects both. Compare flags 0x20 versus 0xA0, requested signing level zero, on the same stable dual-signed file. Record the actual selected leaf/thumbprint, both policy statuses and signing time. Do not interpret status success alone as selection of the company certificate. First successful does not necessarily mean first listed: CI may reject the primary and continue.

0x80 has additional policy and fallback effects. Domito exposes it only as DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL and through the opt-in DomitoCiSignatureSelectionFirstSuccessfulExperimental mode in DomitoValidateFileObjectEx. It is not a production default or a stable signer index. See the validation helper contracts. The companion research SIGNATURE-SELECTION.md provides the full static evidence and an alternative primary-signature path using CiCheckSignedFile.

A user-run kernel probe on Windows 11 10.0.22631 x64, CI options 0x00080207 (TESTSIGNING enabled), confirms the selection difference for the exact dual-signed BthPS3 fixture: 0x20 returns Microsoft, while 0xA0 returns the Nefarius leaf FF0CCF318FD8C14775023A0B52C28FFCCA59D7F7 and signing time 2026-10-02T18:32:01.000Z. Direct primary SHA-256 CiCheckSignedFile returns the same Nefarius leaf, signer/timestamp policies and signing time. All three calls return STATUS_SUCCESS; all policy VerificationStatus fields also succeed. The file digest is identical across calls, before/after file hashes match, and TraceFmt reports 149 events with no losses, format errors or unknown formats.

The companion signature-selection/runtime-22631-bthps3.json records exact trace hashes and the on-disk CI hash. This is one test-signing-enabled run; TESTSIGNING-off, other builds/policies and general signature enumeration remain unverified. 0xA0 does not mean "select any desired publisher".

Checks available without the VM

From an x64 Visual Studio developer shell:

python tests\run-ci-probe-tests.py

This compiles the actual probe body with mock CI calls and constants extracted from the public header. It verifies call order, input forwarding, fresh capacity and outputs, result capture, and freeing both policies after success and failure. It does not emulate the flag's trust semantics. Kernel compilation checks the real headers separately; neither compilation nor mock tests substitute for the VM run above.