diff --git a/Domito.props b/Domito.props
index 40705ee..251f70f 100644
--- a/Domito.props
+++ b/Domito.props
@@ -4,14 +4,15 @@
<_PropertySheetDisplayName>Domito Kernel Library
- $(SolutionDir)..\Domito
+ $(MSBuildThisFileDirectory)
+ $(Platform)
$(DomitoRootPath)\include;$(IntDir);%(AdditionalIncludeDirectories)
- $(DomitoRootPath)\lib\$(DDKPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DDKPlatform)\ci.lib;$(DDK_LIB_PATH)cng.lib;%(AdditionalDependencies);
+ $(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\ci.lib;cng.lib;%(AdditionalDependencies);
diff --git a/NOTES.md b/NOTES.md
index cc4d759..c4461c5 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -1,16 +1,76 @@
-# CI APIs
+# CI API availability observations
-## Available starting with 1511
+See the [CI API compatibility matrix](docs/ci-api-compatibility.md) for the
+per-API tables covering 20 VM, ISO and modern x64/ARM64 samples, with exact
+versions, file hashes and machine-readable results. The six-sample notes below
+describe the original VM corpus and its subsequent ISO follow-up.
-- CiCheckSignedFile
-- CiFindPageHashesInCatalog
-- CiFindPageHashesInSignedFile
-- CiFreePolicyInfo
-- CiGetPEInformation
-- CiInitialize
-- CiValidateFileObject
-- CiVerifyHashInCatalog
+These are export-table observations from six extracted x64 `ci.dll` files, not
+claims about every servicing revision or a proof of the original introduction
+build. The companion CodeIntegrity-RE `corpus-index.json` records identities,
+extraction aliases, manifest checks, and metadata. VM directory names are labels;
+versions below come from the PE version resources. No target was executed.
-## Added in 1709
+## Named exports common to all six samples
-- CiSetTrustedOriginClaimId
+- `CiCheckSignedFile`
+- `CiFindPageHashesInCatalog`
+- `CiFindPageHashesInSignedFile`
+- `CiFreePolicyInfo`
+- `CiGetPEInformation`
+- `CiInitialize`
+- `CiValidateFileObject`
+- `CiVerifyHashInCatalog`
+
+`CiValidateFileObject` is already present in sampled Windows 10 **1507**
+`10.0.10240.16463`. The old “starting with 1511” list was therefore too narrow.
+Export presence does not establish a complete compatible parameter/structure
+contract; consult the per-build static analysis before using it.
+
+## Additional names and absent names
+
+`CiSetTrustedOriginClaimId` is present in sampled `10.0.15063.0` (**1703**) and
+in the sampled 1709/1803 files. It is absent from the sampled 1507,1511,1607
+export tables. This corrects the old “added in 1709” note, without claiming the
+first Windows build on which the API ever existed.
+
+`CiGetCertPublisherName` is absent from **all six** sampled 1507–1803 export
+tables. The ISO follow-up also finds it absent in x64 `10.0.19041.1`, present
+in x64 `10.0.19041.1865` and `10.0.22000.318`, and the original investigation
+finds it present in `10.0.26100.8328`. This brackets Windows 10 servicing
+observations without identifying the first update adding the export. The
+CodeIntegrity-RE `iso-corpus-index.json` records the exact files and hashes.
+The Windows 10 ARM64 21H2 sample additionally exports it in CI
+`10.0.19041.1566`; this does not establish x64 availability at that revision.
+The matrix keeps architecture-specific observations separate.
+A header declaration,
+import-library symbol, or `NTDDI_VERSION >= NTDDI_WIN10` alone does not make an
+unconditional static import loadable on the older samples. Callers supporting
+those samples must avoid that import or use optional resolution with an
+explicit unavailable/fallback path. `CiSetTrustedOriginClaimId` likewise cannot
+be assumed available on every Windows 10 build; it is a CI-global setter.
+
+The original modern x64 sample has display `FileVersion` **26100.8328**, but
+its numeric fixed version is **26100.9444**. The matrix records both under the
+same SHA-256 identity and uses numeric fixed versions in its tables.
+
+## Exact sampled files
+
+- Windows 10 1507: `10.0.10240.16463` x64, SHA-256
+ `e83d900f4675c6e6ffc3fb7dcd3074610876a1398e495796769988df810fd3bc`.
+- Windows 10 1511: `10.0.10586.0` x64, SHA-256
+ `f949bfa614addea6174b28c3818c56b327c9290fd41bd91dfc545414b3df6783`.
+- Windows 10 1607: `10.0.14393.2189` x64, SHA-256
+ `ef0c02ecbd0818e84be9f61421f919825f96a3c59199460cab08548eee2881df`.
+- Windows 10 1703: `10.0.15063.0` x64, SHA-256
+ `3705aaf408f05326e028621380aa5c96a1f3583a2de8a139b188da4b82577863`.
+- Windows 10 1709: `10.0.16299.125` x64, SHA-256
+ `1db7ae5a08bd2361fc7fcdd373fb980e8d680035e67b02e5967095db0443effd`.
+- Windows 10 1803: `10.0.17134.1130` x64, SHA-256
+ `8ce3b8fe38c23b699f30b045383640019a429d3a9d513285c655bffe03a796d9`.
+
+Matching Microsoft public PDB GUIDs/ages were verified in
+`historical/ci--symbols.json` in CodeIntegrity-RE. Public names help map
+functions; they are not a full typed SDK. The default Domito declarations remain
+conservative. See [current contracts and limits](docs/mincrypt-contracts.md) and
+[implementation status](docs/implementation-status.md) for ABI and runtime work.
diff --git a/README.md b/README.md
index 7123638..bf5e369 100644
--- a/README.md
+++ b/README.md
@@ -24,12 +24,28 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
- I do not want to force any consumer of the library to drag C++ paradigms into their project. I do expect the user to utilize a modern compiler though, so the library sources themselves may have some 'C++-ish touches' here and there, for my own convenience 😉
- Compatibility with every Windows 10 version.
- APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞
- - **Caution:** this claim comes with an asterisk though; due to PatchGuard (or some other security mechanism) I couldn't get run-time dynamic linking for `ci.dll` exports to work, so you need to actively avoid implementing code depending on exports that do not exist on earlier versions of Windows. Once I have a compatibility matrix for all of them I'll update the documentation accordingly.
+ - CI entry points are currently static imports from `ci.lib`; consumers must avoid imports unavailable on their target Windows builds. The [CI API compatibility matrix](docs/ci-api-compatibility.md) records all ten named exports across 20 exact Windows 10/11 x64 and ARM64 CI files. Export lookup uses the correct name-ordinal mapping and rejects forwarders. Runtime ABI verification remains separate; see the remaining-work ledger below.
- No conflicts with WDF or DMF
- The consuming driver *may* (but doesn't have to) utilize Microsoft WDF or DMF in addition without having to fear any incompatibilities 💪
- Reliable SAL annotations.
- I made sure to enrich the majority of the code with correct, tested annotations for Code Analysis to help you spot potential accidental API misuse 😎
+## Authenticode validation helpers
+
+The [CI validation helpers](docs/ci-validation-helpers.md) provide shared result
+ownership/cleanup, file-object validation with explicit selection policy, and
+algorithm-explicit embedded verification using CI. First-success selection is
+an opt-in x64 experiment; existing APIs retain their behavior. Always enforce
+the expected signer/trust policy separately from a successful CI return status.
+
+## Consumer helpers
+
+[Migration and contracts](docs/consumer-helpers.md) cover file-handle/path CI
+validation, bounded x64 chain views, optional publisher-name extraction,
+certificate/SPKI hashing (including explicit legacy content-only pins), strict
+hex conversion, process-name cleanup and combined module/export lookup.
+Application authorization and trust policies remain caller-provided.
+
## How to use
- Add the `include` directory to your project's headers search path.
@@ -55,7 +71,11 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
### Using the property sheet
-Copy the provided `Domito.props` file into the directory of your `.vcxproj` file and customize it like below:
+Import `Domito.props` from the Domito package, or set `DomitoRootPath` explicitly
+if copying the sheet elsewhere. Its default root is its own directory. The sheet
+links both WDM and KMDF final drivers; static-library consumers must also import
+it into the final driver project. `DomitoLibraryPlatform` defaults to `$(Platform)`.
+For example:
```xml
@@ -71,6 +91,14 @@ Copy the provided `Domito.props` file into the directory of your `.vcxproj` file
```
+## Code Integrity API notes
+
+- [Algorithm-explicit Authenticode hashing and legacy catalog policy](docs/authenticode-digests.md)
+- [Mincrypt contracts, lifetimes, flags and opt-in x64 26100 layouts](docs/mincrypt-contracts.md)
+- [Implemented corrections and remaining VM/binary work](docs/implementation-status.md)
+
+- [CiValidateFileObject flag 0x20: evidence, limitations, and kernel test procedure](docs/ci-validate-file-object.md)
+
## Sources & 3rd party credits
This library benefits from these awesome projects ❤ (appearance in no special order):
diff --git a/docs/authenticode-digests.md b/docs/authenticode-digests.md
new file mode 100644
index 0000000..1525f32
--- /dev/null
+++ b/docs/authenticode-digests.md
@@ -0,0 +1,142 @@
+# Authenticode digests and legacy verification
+
+Prefer `DomitoCalculatePortableExecutableDigestEx` when computing a PE digest.
+It takes an explicit SHA-1/256/384/512 algorithm, validates its supported PE layout,
+and returns a digest independently of whether the file has an embedded signature.
+This allows SHA-256 catalog lookup for a file without an embedded certificate.
+Hash calculation is separate from signature verification and signer acceptance.
+
+The older `DomitoCalculatePortableExecutableDigest` and
+`DomitoGetPortableExecutableDigestKind` retain their compatibility behavior:
+linear byte hashing and heuristic pattern discovery with a SHA-1 fallback. They
+remain bounded, but should not be used to discover a general signature policy.
+The new API is additive so existing callers do not silently acquire different
+selection or rejection rules.
+
+## Supported PE layout
+
+The explicit API validates DOS/NT/optional-header spans, PE32 and PE32+ magic,
+declared data-directory count, the complete section table, `SizeOfHeaders`, every
+nonempty section's raw interval, and the certificate table. It sorts section
+descriptors by raw file offset before validating their sequence. At most 96
+sections are accepted, keeping stack storage and sorting work bounded.
+
+Nonempty sections must form a contiguous sequence starting at `SizeOfHeaders`.
+Section-header order can differ from raw-file order. Zero-raw-size sections
+contribute no bytes. An overlay between the raw sections and the certificate
+table, or EOF for an unsigned file, is included. The checksum and physical
+certificate-directory slot are excluded. A present certificate table must be
+8-byte aligned and end at EOF; every entry must have a bounded length, version
+`0x200`, type PKCS#7 SignedData, and complete 8-byte alignment storage.
+
+For this subset, sorted section bytes coincide with linear file order, so the
+implementation can stream validated ranges directly to BCrypt without copying
+or allocating a whole-file scratch buffer. Digest allocation is the only pool
+allocation in the hashing path and is released on every failure.
+
+Gaps, overlapping raw sections, certificates in the middle of a file, and absent
+certificate-directory slots are unsupported. Some malformed inputs return a
+format error; ambiguous or excluded layouts return `STATUS_NOT_SUPPORTED`.
+The missing-slot boundary is deliberate: Windows SIP produced a different hash
+than the old linear helper on a fixture with zero declared directories. Do not
+infer Windows parity from the older helper's successful return on that input.
+
+This is a validated subset, not a claim that every PE accepted by every Windows
+SIP is supported. Microsoft's [PE format documentation](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format)
+describes section ordering and exclusions, but unusual overlay behavior requires
+measurements against the relevant Windows implementation. The local tests use
+the actual [CryptSIPCreateIndirectData](https://learn.microsoft.com/en-us/windows/win32/api/mssip/nf-mssip-cryptsipcreateindirectdata)
+provider as a differential oracle, rather than treating a prose summary as the
+complete algorithm. SIP hash agreement does not establish CI trust-policy parity.
+
+## Discovering the first embedded content's digest algorithm
+
+`DomitoGetPortableExecutableDigestKindEx` navigates a bounded DER subset:
+PKCS#7 ContentInfo / SignedData / encapsulated `SpcIndirectDataContent` /
+`DigestInfo.AlgorithmIdentifier`. It reads the algorithm associated with the
+file digest, avoiding unrelated algorithm patterns in certificates, attributes,
+timestamps, or signer information. SHA-1/256/384/512 are recognized. Algorithm
+parameters may be absent or ASN.1 NULL; the digest OCTET STRING must have the
+corresponding length. DER lengths must be bounded, definite, and minimally
+encoded. Up to seven zero bytes of aligned padding after the outer DER are
+accepted when included in `WIN_CERTIFICATE.dwLength`, as observed on Microsoft
+system binaries. Other trailing bytes are rejected.
+
+An absent certificate table returns `STATUS_NOT_FOUND`, an unsupported algorithm
+or certificate type returns `STATUS_NOT_SUPPORTED`, and malformed supported
+structure returns a format error. The output algorithm is zero on failure; no
+default algorithm is silently manufactured.
+
+The caller supplies an array containing the security-directory entry and a
+stable input buffer. The helper inspects the first table entry. It does not
+enumerate nested/multiple signatures, validate the entire ASN.1 document,
+validate signerInfos, verify cryptographic signatures, or establish trust.
+Indefinite-length BER is outside this supported DER subset. Choose explicit
+algorithms when handling catalog-only files or application-defined lookup order.
+
+## Explicit legacy catalog policy
+
+`DomitoValidateFileLegacyModeEx` takes a caller-selected
+`AcceptedCatalogPolicyBits` mask, a `RequireTrustedCatalog` marker gate, and a
+`ValidationSource` output. Source is `DOMITO_CI_SOURCE_EMBEDDED` or
+`DOMITO_CI_SOURCE_CATALOG` on success and zero on failure.
+
+This helper accepts SHA-1 with exactly 20 bytes or SHA-256 with exactly 32 bytes.
+Unsupported algorithms return `STATUS_NOT_SUPPORTED`; mismatched lengths return
+`STATUS_INVALID_PARAMETER`. The underlying observed embedded API supports more
+algorithms than this helper's catalog workflow, as documented in
+[the mincrypt contracts](mincrypt-contracts.md).
+
+Both policy objects must be zero-initialized before first use, or own valid CI
+results. The helper frees previous contents on entry and before each retry and
+clears signing time between attempts. The caller owns both final policy results,
+including any returned on failure, and frees them with `CiFreePolicyInfo`.
+Embedded-to-catalog fallback and catalog reload occur only for
+`STATUS_INVALID_IMAGE_HASH`; resource, I/O, and other policy errors propagate.
+
+Before certificate allocation, the helper references the file object and checks
+the supplied table interval against the current file size. It then uses the
+exact-read `DomitoReadFileAt`, which waits on a private completion event. An EOF
+short read fails before CI sees any uninitialized bytes. The caller must keep
+file identity/bytes stable between hashing and consumption; a size check cannot
+prevent concurrent mutation. Handles must be valid in the current process
+context and satisfy the documented kernel I/O/APC requirements.
+
+The older `DomitoValidateFileLegacyMode` delegates with the existing
+`DOMITO_CI_LEGACY_CATALOG_POLICY_BITS` (`0x2007F`) and a false marker gate. That
+value is a compatibility default with broad categories, not an application trust
+recommendation. The explicit mask uses **any-overlap** catalog filtering.
+
+For a catalog-only file, choose an allowed algorithm sequence as application
+policy (for example SHA-256 followed by SHA-1 when legacy acceptance is intended).
+For each algorithm, call `DomitoCalculatePortableExecutableDigestEx`, then
+`DomitoValidateFileLegacyModeEx` with a zero security directory. Free the digest
+and both policy objects before retrying. Continue to another algorithm only on
+the specifically intended lookup/signature-mismatch result, not on all failures.
+Preserve the actual algorithm and validation source in the application's result.
+This supports an explicit search strategy; it does not reproduce every native
+CI scenario-selection rule or WinVerifyTrust policy.
+
+## Validation performed
+
+On the local Windows 26100 system, host tests compare all four digest algorithms
+against Windows SIP for PE32/PE32+, unsigned/signed fixtures, overlays, reverse
+section-header order, multiple certificate entries, and certificate alignment.
+The installed `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` form a
+real-file corpus. Embedded digest algorithms are independently extracted with
+Crypt32, and computed file digests are compared with the signed
+`SpcIndirectDataContent` values where an embedded signature exists. This reads
+files and hashes bytes; it does not load or execute them as code.
+
+Guard-page truncations, malformed offsets/lengths, unsupported layouts,
+algorithm selection, injected CNG/allocation failures, retry ownership, exact
+read failures, and explicit catalog controls are also covered. Actual CI calls
+are mocked in those host tests. The kernel translation units and layout
+assertions compile for x64/ARM64 with warnings treated as errors; the opt-in
+26100 runtime-layout observations remain x64-specific.
+
+Still pending are historical/ARM64 CI runtime compatibility, Driver Verifier,
+real catalog-only acceptance across algorithms, nested/multiple-signature
+selection, weak-crypto/revocation/timestamp policy, and extension of supported PE
+layouts. Use the [remaining-work ledger](implementation-status.md) and the
+[flag probe](ci-validate-file-object.md) to plan the VM run.
diff --git a/docs/ci-api-compatibility.json b/docs/ci-api-compatibility.json
new file mode 100644
index 0000000..86c39e7
--- /dev/null
+++ b/docs/ci-api-compatibility.json
@@ -0,0 +1,651 @@
+{
+ "schema_version": 2,
+ "scope": "Observed named PE exports in exact architecture-specific ci.dll samples; not runtime or ABI certification",
+ "source_hash_encoding": "Canonical JSON: UTF-8, sorted keys, comma/colon separators, ensure_ascii=false",
+ "generated_from": {
+ "corpus-index.json": "d2c6507794314ca38a2015c74f3b66e81a4bd82d982be0ed1911dcff1d79475a",
+ "iso-corpus-index.json": "ba526a8548f3be100e8d13019aed57de9703a142615c509870720572bc636217",
+ "manifest.json": "bd215ebd3deb480a39b5d71a6ed709dad20368cbe90879a52070b56b023284d4",
+ "ci-26100.8328-evidence.json": "ea0f939df9d8b7abde40e3402c01b2ac398253728f329e8c2b7134de8df6305f"
+ },
+ "apis": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "samples": [
+ {
+ "label": "Windows 10 1507",
+ "ci_file_version": "10.0.10240.16463",
+ "architecture": "x64",
+ "sha256": "e83d900f4675c6e6ffc3fb7dcd3074610876a1398e495796769988df810fd3bc",
+ "source": "corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": false,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1511",
+ "ci_file_version": "10.0.10586.0",
+ "architecture": "x64",
+ "sha256": "f949bfa614addea6174b28c3818c56b327c9290fd41bd91dfc545414b3df6783",
+ "source": "corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": false,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1607",
+ "ci_file_version": "10.0.14393.2189",
+ "architecture": "x64",
+ "sha256": "ef0c02ecbd0818e84be9f61421f919825f96a3c59199460cab08548eee2881df",
+ "source": "corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": false,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1703",
+ "ci_file_version": "10.0.15063.0",
+ "architecture": "x64",
+ "sha256": "3705aaf408f05326e028621380aa5c96a1f3583a2de8a139b188da4b82577863",
+ "source": "corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1709",
+ "ci_file_version": "10.0.16299.125",
+ "architecture": "x64",
+ "sha256": "1db7ae5a08bd2361fc7fcdd373fb980e8d680035e67b02e5967095db0443effd",
+ "source": "corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1803",
+ "ci_file_version": "10.0.17134.1130",
+ "architecture": "x64",
+ "sha256": "8ce3b8fe38c23b699f30b045383640019a429d3a9d513285c655bffe03a796d9",
+ "source": "corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1809 initial x64 ISO",
+ "ci_file_version": "10.0.17763.1",
+ "architecture": "x64",
+ "sha256": "eb177481f2aea3cae0c2f97f7058e7447c0695e4489428406598f168d4dcc5e8",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1809 October v2 x64 ISO",
+ "ci_file_version": "10.0.17763.348",
+ "architecture": "x64",
+ "sha256": "da8282173cbce7ea38f8fca64db0972c3d9614ddbca98f6b96d11a4cadde92ad",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1903 v1 x64 ISO",
+ "ci_file_version": "10.0.18362.1",
+ "architecture": "x64",
+ "sha256": "5d01bb84fa516e23c25def8ea03fc6b696082ccc67a61575fd00d1fe7c273d72",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1903 v2 x64 ISO",
+ "ci_file_version": "10.0.18362.329",
+ "architecture": "x64",
+ "sha256": "d12131f14f781f8c314a795384e02afc3c2902bf67da9c2e75d8163c891bdd89",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 1909 x64 ISO",
+ "ci_file_version": "10.0.18362.418",
+ "architecture": "x64",
+ "sha256": "c3c5d32da42fa69b0795cd0f8c57252670d673fb036685db5c7d6392ffee0293",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 2004 ISO",
+ "ci_file_version": "10.0.19041.1",
+ "architecture": "x64",
+ "sha256": "2955710d34db26e352e62b20861ba338f0216cc36b378d128057be955e184282",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": false,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 22H2 consumer ISO",
+ "ci_file_version": "10.0.19041.1865",
+ "architecture": "x64",
+ "sha256": "d0408a9f64eafb72e8aa1cf127d07d919f0cab38a6bdf2b406255909c85e3a8d",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 11 English x64 v1 ISO",
+ "ci_file_version": "10.0.22000.318",
+ "architecture": "x64",
+ "sha256": "301ae49ac5b87cf1cbe07d9d3c6486a066d045b6bf55fc8eb392d52951ec69ff",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 11 23H2 English ISO",
+ "ci_file_version": "10.0.22621.2792",
+ "architecture": "x64",
+ "sha256": "9ed77b8a53946ced8c3540edb26a765060dff908544634e7b069aa49f2b41559",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 11 24H2 sample",
+ "ci_file_version": "10.0.26100.9444",
+ "architecture": "x64",
+ "sha256": "c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b",
+ "source": "ci-26100.8328-evidence.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ },
+ "display_file_version": "10.0.26100.8328"
+ },
+ {
+ "label": "Windows 10 21H2 ARM64 ISO (archive Raspberry Pi folder)",
+ "ci_file_version": "10.0.19041.1566",
+ "architecture": "ARM64",
+ "sha256": "a35f651261691cc67acc9c8bfd00bba7a64dd9f16d2c4b6fe1d183e34aeead78",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 10 22H2.7 ARM64 ISO",
+ "ci_file_version": "10.0.19041.2965",
+ "architecture": "ARM64",
+ "sha256": "33f2d1256f60392bbb84c4251d6435461dfa852a100e065376f83dc8a4dd2ec6",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 11 22H2.9 ARM64 ISO",
+ "ci_file_version": "10.0.22621.1702",
+ "architecture": "ARM64",
+ "sha256": "4164ac08fd19628c5ab6cbca6234265ac2bf7f39d5149f57d3845646d8a8ad60",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ },
+ {
+ "label": "Windows 11 24H2 ARM64 ISO",
+ "ci_file_version": "10.0.26100.1591",
+ "architecture": "ARM64",
+ "sha256": "a0eba64a9a3f2006d5832079412eca42cc1b9ed66d19b9b0adb7e59ea354415d",
+ "source": "iso-corpus-index.json",
+ "named_exports": [
+ "CiCheckSignedFile",
+ "CiFindPageHashesInCatalog",
+ "CiFindPageHashesInSignedFile",
+ "CiFreePolicyInfo",
+ "CiGetCertPublisherName",
+ "CiGetPEInformation",
+ "CiInitialize",
+ "CiSetTrustedOriginClaimId",
+ "CiValidateFileObject",
+ "CiVerifyHashInCatalog"
+ ],
+ "ordinal_only_export_count": 3,
+ "api_presence": {
+ "CiCheckSignedFile": true,
+ "CiFindPageHashesInCatalog": true,
+ "CiFindPageHashesInSignedFile": true,
+ "CiFreePolicyInfo": true,
+ "CiGetCertPublisherName": true,
+ "CiGetPEInformation": true,
+ "CiInitialize": true,
+ "CiSetTrustedOriginClaimId": true,
+ "CiValidateFileObject": true,
+ "CiVerifyHashInCatalog": true
+ }
+ }
+ ]
+}
diff --git a/docs/ci-api-compatibility.md b/docs/ci-api-compatibility.md
new file mode 100644
index 0000000..ad5ae16
--- /dev/null
+++ b/docs/ci-api-compatibility.md
@@ -0,0 +1,184 @@
+# CI API compatibility matrix
+
+This matrix records **named export presence in 20 exact `ci.dll` files**,
+with architecture stated separately for each sample.
+**Yes** means the complete PE export directory contains the name; **No** means
+it does not. Neither result is inferred from a header, import library or PDB.
+Export presence alone does not establish a compatible ABI or successful verification.
+
+Versions in the tables are **CI file versions**, not necessarily the kernel version.
+In particular, the Windows 10 22H2 consumer ISO contains CI **19041.1865**.
+Do not extrapolate a cell to every servicing revision of that Windows release.
+
+[Machine-readable matrix](ci-api-compatibility.json) includes exact SHA-256 identities,
+all per-API results and canonical JSON hashes of the retained source inventories.
+Source hashes ignore whitespace and line-ending differences between checkouts;
+the binary SHA-256 identities always cover the exact original bytes.
+
+The tables use numeric `VS_FIXEDFILEINFO` versions. The original modern x64
+sample has **fixed version 26100.9444** but its string `FileVersion` reports
+**26100.8328**. Original research/header labels retain that string version;
+they identify the same SHA-256 file, not two different CI samples.
+
+## Windows 10 1507–1803 x64 samples
+
+| API | 10240.16463 | 10586.0 | 14393.2189 | 15063.0 | 16299.125 | 17134.1130 |
+| --- | --- | --- | --- | --- | --- | --- |
+| `CiCheckSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiFindPageHashesInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiFindPageHashesInSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiFreePolicyInfo` | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiGetCertPublisherName` | No | No | No | No | No | No |
+| `CiGetPEInformation` | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiInitialize` | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiSetTrustedOriginClaimId` | No | No | No | Yes | Yes | Yes |
+| `CiValidateFileObject` | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiVerifyHashInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes |
+
+## Later Windows 10 x64 samples
+
+| API | 17763.1 | 17763.348 | 18362.1 | 18362.329 | 18362.418 | 19041.1 | 19041.1865 |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| `CiCheckSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiFindPageHashesInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiFindPageHashesInSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiFreePolicyInfo` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiGetCertPublisherName` | No | No | No | No | No | No | Yes |
+| `CiGetPEInformation` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiInitialize` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiSetTrustedOriginClaimId` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiValidateFileObject` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| `CiVerifyHashInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+
+## Windows 11 x64 samples
+
+| API | 22000.318 | 22621.2792 | 26100.9444 |
+| --- | --- | --- | --- |
+| `CiCheckSignedFile` | Yes | Yes | Yes |
+| `CiFindPageHashesInCatalog` | Yes | Yes | Yes |
+| `CiFindPageHashesInSignedFile` | Yes | Yes | Yes |
+| `CiFreePolicyInfo` | Yes | Yes | Yes |
+| `CiGetCertPublisherName` | Yes | Yes | Yes |
+| `CiGetPEInformation` | Yes | Yes | Yes |
+| `CiInitialize` | Yes | Yes | Yes |
+| `CiSetTrustedOriginClaimId` | Yes | Yes | Yes |
+| `CiValidateFileObject` | Yes | Yes | Yes |
+| `CiVerifyHashInCatalog` | Yes | Yes | Yes |
+
+## Windows 10 ARM64 samples
+
+| API | 19041.1566 | 19041.2965 |
+| --- | --- | --- |
+| `CiCheckSignedFile` | Yes | Yes |
+| `CiFindPageHashesInCatalog` | Yes | Yes |
+| `CiFindPageHashesInSignedFile` | Yes | Yes |
+| `CiFreePolicyInfo` | Yes | Yes |
+| `CiGetCertPublisherName` | Yes | Yes |
+| `CiGetPEInformation` | Yes | Yes |
+| `CiInitialize` | Yes | Yes |
+| `CiSetTrustedOriginClaimId` | Yes | Yes |
+| `CiValidateFileObject` | Yes | Yes |
+| `CiVerifyHashInCatalog` | Yes | Yes |
+
+## Windows 11 ARM64 samples
+
+| API | 22621.1702 | 26100.1591 |
+| --- | --- | --- |
+| `CiCheckSignedFile` | Yes | Yes |
+| `CiFindPageHashesInCatalog` | Yes | Yes |
+| `CiFindPageHashesInSignedFile` | Yes | Yes |
+| `CiFreePolicyInfo` | Yes | Yes |
+| `CiGetCertPublisherName` | Yes | Yes |
+| `CiGetPEInformation` | Yes | Yes |
+| `CiInitialize` | Yes | Yes |
+| `CiSetTrustedOriginClaimId` | Yes | Yes |
+| `CiValidateFileObject` | Yes | Yes |
+| `CiVerifyHashInCatalog` | Yes | Yes |
+
+## Exact sample identities
+
+- Windows 10 1507: **10.0.10240.16463 x64**, SHA-256
+ `e83d900f4675c6e6ffc3fb7dcd3074610876a1398e495796769988df810fd3bc`.
+- Windows 10 1511: **10.0.10586.0 x64**, SHA-256
+ `f949bfa614addea6174b28c3818c56b327c9290fd41bd91dfc545414b3df6783`.
+- Windows 10 1607: **10.0.14393.2189 x64**, SHA-256
+ `ef0c02ecbd0818e84be9f61421f919825f96a3c59199460cab08548eee2881df`.
+- Windows 10 1703: **10.0.15063.0 x64**, SHA-256
+ `3705aaf408f05326e028621380aa5c96a1f3583a2de8a139b188da4b82577863`.
+- Windows 10 1709: **10.0.16299.125 x64**, SHA-256
+ `1db7ae5a08bd2361fc7fcdd373fb980e8d680035e67b02e5967095db0443effd`.
+- Windows 10 1803: **10.0.17134.1130 x64**, SHA-256
+ `8ce3b8fe38c23b699f30b045383640019a429d3a9d513285c655bffe03a796d9`.
+- Windows 10 1809 initial x64 ISO: **10.0.17763.1 x64**, SHA-256
+ `eb177481f2aea3cae0c2f97f7058e7447c0695e4489428406598f168d4dcc5e8`.
+- Windows 10 1809 October v2 x64 ISO: **10.0.17763.348 x64**, SHA-256
+ `da8282173cbce7ea38f8fca64db0972c3d9614ddbca98f6b96d11a4cadde92ad`.
+- Windows 10 1903 v1 x64 ISO: **10.0.18362.1 x64**, SHA-256
+ `5d01bb84fa516e23c25def8ea03fc6b696082ccc67a61575fd00d1fe7c273d72`.
+- Windows 10 1903 v2 x64 ISO: **10.0.18362.329 x64**, SHA-256
+ `d12131f14f781f8c314a795384e02afc3c2902bf67da9c2e75d8163c891bdd89`.
+- Windows 10 1909 x64 ISO: **10.0.18362.418 x64**, SHA-256
+ `c3c5d32da42fa69b0795cd0f8c57252670d673fb036685db5c7d6392ffee0293`.
+- Windows 10 2004 ISO: **10.0.19041.1 x64**, SHA-256
+ `2955710d34db26e352e62b20861ba338f0216cc36b378d128057be955e184282`.
+- Windows 10 22H2 consumer ISO: **10.0.19041.1865 x64**, SHA-256
+ `d0408a9f64eafb72e8aa1cf127d07d919f0cab38a6bdf2b406255909c85e3a8d`.
+- Windows 11 English x64 v1 ISO: **10.0.22000.318 x64**, SHA-256
+ `301ae49ac5b87cf1cbe07d9d3c6486a066d045b6bf55fc8eb392d52951ec69ff`.
+- Windows 11 23H2 English ISO: **10.0.22621.2792 x64**, SHA-256
+ `9ed77b8a53946ced8c3540edb26a765060dff908544634e7b069aa49f2b41559`.
+- Windows 11 24H2 sample: **10.0.26100.9444 x64**, SHA-256
+ `c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b`.
+ String FileVersion: **10.0.26100.8328** (original research label).
+- Windows 10 21H2 ARM64 ISO (archive Raspberry Pi folder): **10.0.19041.1566 ARM64**, SHA-256
+ `a35f651261691cc67acc9c8bfd00bba7a64dd9f16d2c4b6fe1d183e34aeead78`.
+- Windows 10 22H2.7 ARM64 ISO: **10.0.19041.2965 ARM64**, SHA-256
+ `33f2d1256f60392bbb84c4251d6435461dfa852a100e065376f83dc8a4dd2ec6`.
+- Windows 11 22H2.9 ARM64 ISO: **10.0.22621.1702 ARM64**, SHA-256
+ `4164ac08fd19628c5ab6cbca6234265ac2bf7f39d5149f57d3845646d8a8ad60`.
+- Windows 11 24H2 ARM64 ISO: **10.0.26100.1591 ARM64**, SHA-256
+ `a0eba64a9a3f2006d5832079412eca42cc1b9ed66d19b9b0adb7e59ea354415d`.
+
+## What callers can rely on
+
+- `CiValidateFileObject` is already exported in the sampled Windows 10 1507 file.
+- `CiSetTrustedOriginClaimId` is present in the sampled 1703 file and absent in
+ the three earlier samples. This does not prove its first introduction build.
+- On sampled x64 files, `CiGetCertPublisherName` is absent in 19041.1
+ and present in 19041.1865.
+ Its first Windows 10 servicing update remains unknown. It is also present
+ in the sampled 22000.318 and 26100.9444 fixed-version files.
+- The Windows 10 ARM64 21H2 sample already exports the publisher-name API
+ in CI 19041.1566. That does not establish x64 availability at that revision.
+- CI entry points currently use static imports from `ci.lib`. An unavailable
+ import can prevent driver loading; this matrix does not add an automatic
+ `STATUS_NOT_IMPLEMENTED` fallback. Supporting these older samples requires
+ avoiding unavailable imports or implementing optional resolution.
+- Choose structures using a verified runtime profile. The sampled x64 chain
+ headers differ: 1507 uses 56 bytes, 1511 uses 72, and 1607–1803 use 104.
+ Export presence, compile-target `NTDDI_VERSION` and total allocation `Size`
+ do not select a compatible layout. See [mincrypt contracts](mincrypt-contracts.md).
+
+## Coverage gaps and evidence
+
+**Unknown / not sampled:** x86 export availability; pre-Windows-10;
+earlier Windows 10 ARM64 releases; and other servicing revisions
+or architecture/release combinations absent from these tables.
+These are gaps, not negative export results. No interval between
+two samples is certified. Runtime verification and cross-version ABI checks
+remain in the [implementation ledger](implementation-status.md).
+
+The tables cover every named export found in these samples, including
+`CiInitialize`; inclusion is not a recommendation to call initialization or
+global-policy routines. Each sample also has three ordinal-only exports,
+which are outside this named-API matrix. Private `Mincrypt*` PDB symbols are
+not automatically callable exports.
+
+Source inventories are retained in the companion
+[CodeIntegrity-RE repository](https://git.nefarius.at/nefarius/CodeIntegrity-RE):
+`corpus-index.json` (VM samples), `iso-corpus-index.json` (ISO samples), and
+`ci-26100.8328-evidence.json` with `manifest.json` (original 26100 sample).
+Regenerate with its `generate_compatibility_matrix.py --output `.
+ARM64 cells establish export presence only; the historical x64 layout views
+must not be selected for ARM64 solely because the same API name is exported.
+No Windows binary or PDB is distributed with the matrix.
diff --git a/docs/ci-validate-file-object.md b/docs/ci-validate-file-object.md
new file mode 100644
index 0000000..dedfd6e
--- /dev/null
+++ b/docs/ci-validate-file-object.md
@@ -0,0 +1,236 @@
+# 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:
+
+```text
+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](https://github.com/Ido-Moshe-Github/CiDllDemo/issues/5#issuecomment-2725851295)
+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](ci-validation-helpers.md). 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:
+
+```powershell
+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.
diff --git a/docs/ci-validation-helpers.md b/docs/ci-validation-helpers.md
new file mode 100644
index 0000000..9727193
--- /dev/null
+++ b/docs/ci-validation-helpers.md
@@ -0,0 +1,118 @@
+# CI validation helpers
+
+These additive APIs are declared in `Domito.h` for Windows 10 compile targets.
+They reuse CI for cryptographic verification. They do not implement RSA/ECDSA,
+certificate-chain building or timestamp-signature verification themselves.
+Raw CI declarations and existing digest/legacy helpers are unchanged.
+
+## Result lifetime
+
+Zero-initialize `DOMITO_CI_VALIDATION_RESULT` before first use. Each validation
+call frees its previous signer/timestamp policies before reuse and resets the
+result. Do not pass uninitialized storage or copy an owning result as a second
+owner. On every exit, including a failed call, release it with
+`DomitoFreeCiValidationResult`. The destructor accepts NULL and empty results.
+All three APIs require PASSIVE_LEVEL. Referenced input objects/buffers must stay
+valid and file bytes must stay stable throughout validation and consumption.
+
+On success, `Hash`/`HashSize`/`HashAlgId` describe a known SHA-1/256/384/512 file
+digest with a consistent length. Unused digest bytes are zero. This is not a
+certificate thumbprint. On failure, digest bytes/length/algorithm and signing
+time are zero; policy objects may retain owned diagnostic allocations. Input
+flags/level forwarded to the file-object export remain recorded if it was called.
+Those fields are not CI's final internally normalized policy flags.
+
+Check the returned NTSTATUS first. Policy verification status, policy bits,
+certificate identity, timestamps and the application's intended trust policy
+remain separate observations/checks. Returning STATUS_SUCCESS does not pin a
+company signer, reproduce WinVerifyTrust policy or prove driver-load acceptance.
+No helper traverses the private chain layout or matches a certificate for you.
+
+## File-object path
+
+`DomitoValidateFileObjectEx` takes a referenced FILE_OBJECT, caller-selected
+validation flags, a byte-sized requested signing level and a selection mode.
+It supplies fresh 64-byte hash capacity and initialized policy/time outputs on
+every call. It does not read, duplicate or acquire ownership of the file object.
+
+- `DomitoCiSignatureSelectionNative` forwards the caller's flags unchanged.
+ This is native policy, not a guarantee of preferred-level selection: the
+ caller's other flags may already change the search/exit path.
+- `DomitoCiSignatureSelectionFirstSuccessfulExperimental` additionally sets
+ `DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL` (0x80). On examined x64
+ builds this changes a successful-candidate early-exit predicate. It also
+ affects other policy/fallback paths. This explicit mode returns
+ STATUS_NOT_SUPPORTED on non-x64 builds. No runtime behavior on ARM64 is implied.
+
+Neither mode automatically adds `0x20`. Broadening accepted roots is a separate
+caller decision. Neither mode selects an arbitrary signer index or guarantees
+that signature zero succeeds. A rejected primary may still lead to another signer.
+
+```c
+DOMITO_CI_VALIDATION_RESULT result = {0};
+NTSTATUS status = DomitoValidateFileObjectEx(
+ fileObject,
+ DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
+ 0,
+ DomitoCiSignatureSelectionFirstSuccessfulExperimental,
+ &result
+);
+if (NT_SUCCESS(status))
+{
+ // Apply your expected-certificate/trust policy to result.PolicyInfo.
+ // result.SigningTime belongs to that selected verification result.
+}
+DomitoFreeCiValidationResult(&result);
+```
+
+The native kernel experiment on the supplied dual-SHA256 BthPS3 fixture returned
+Microsoft for flags 0x20 and Nefarius for flags 0xA0 on a Windows 11 22631 x64
+TESTSIGNING-enabled machine. It does not establish all older builds, TESTSIGNING-off
+behavior or every side effect of 0x80. See [the flag evidence](ci-validate-file-object.md).
+Do not rename this mode "require our company" or "select primary signature".
+
+## Embedded PE-buffer path
+
+`DomitoValidatePortableExecutableEmbeddedSignature` accepts a stable PE buffer
+and an explicit CALG_SHA1/CALG_SHA_256/CALG_SHA_384/CALG_SHA_512. It calls
+`DomitoCalculatePortableExecutableDigestEx` and supplies the resulting digest
+and original first WIN_CERTIFICATE to `CiCheckSignedFile`. The digest allocation
+is always freed, and only copied digest bytes/owned CI policies remain in the
+result. Unsupported algorithm/layout and CI errors propagate without trying a
+different algorithm or a catalog. An absent certificate returns
+STATUS_INVALID_IMAGE_HASH; the helper cannot turn an unsigned file into a
+successful catalog verification.
+
+```c
+DOMITO_CI_VALIDATION_RESULT result = {0};
+NTSTATUS status = DomitoValidatePortableExecutableEmbeddedSignature(stablePeBytes, peSize, CALG_SHA_256, &result);
+if (NT_SUCCESS(status))
+{
+ // Enforce the expected certificate and intended trust policy here.
+}
+DomitoFreeCiValidationResult(&result);
+```
+
+This uses the explicit digest helper's [supported PE layout subset](authenticode-digests.md):
+bounded headers, contiguous nonoverlapping raw sections in file order and an EOF
+certificate table. It is not an arbitrary PE/SIP parser. CI can select nested
+signatures according to its algorithm rules, so the helper is deliberately named
+**embedded**, not **force primary**. The matching SHA-256 request selected the
+company primary in the tested BthPS3 layout; another layout may select another
+signer or fail. General enumeration remains separate work.
+
+## Support and validation
+
+The functions are C-compatible and compile for x64/ARM64. The experimental
+file-object mode is deliberately gated to x64; native forwarding/embedded code
+compilation does not certify every architecture/build's CI runtime ABI. CI
+imports are static and must exist on the target OS, as described in the
+[availability matrix](ci-api-compatibility.md). Structure/chain interpretation
+remains subject to the [mincrypt contracts](mincrypt-contracts.md).
+
+The new host suite checks production code forwarding, hash invariants, failures,
+partial allocations, cleanup, result reuse and the non-x64 conditional branch.
+The existing digest suite independently compares Windows SIP and Crypt32 results.
+Kernel compilation covers the full library, public C clients and layout checks.
+CodeIntegrity-RE probe v2 now calls the new APIs; a further kernel run is needed
+to validate these wrappers rather than merely their previously tested raw calls.
diff --git a/docs/consumer-helpers.md b/docs/consumer-helpers.md
new file mode 100644
index 0000000..4fa4333
--- /dev/null
+++ b/docs/consumer-helpers.md
@@ -0,0 +1,247 @@
+# Migrating consumer Authenticode helpers
+
+These helpers centralize file validation, certificate inspection, hashing and
+allocation cleanup. The contracts keep application identity and trust policy
+explicit. Host tests and kernel compilation do not establish new CI runtime
+guarantees.
+
+## Verified fixes and existing coverage
+
+- **Implementation defect fixed:** `DomitoGetProcessImageName` freed its allocation
+ on a failed second query but left that freed address in the caller's output.
+ It also left old caller output untouched on earlier failures. It now clears
+ the output first, publishes only a successful bounded result, and frees its
+ local allocation on all other paths. The process handle uses limited-query
+ access (`0x1000`) instead of `PROCESS_ALL_ACCESS`. A PID is resolved once to a
+ process handle; no later query re-resolves that PID.
+- **Output-contract defect fixed:** `DomitoFindModuleBaseAddress` could preserve
+ an old address when lookup failed. It now clears the optional output first and
+ validates the counted module name. The export-by-module wrapper preserves the
+ underlying status, including missing exports and unsupported forwarders.
+- **Annotation defect fixed:** the `DomitoReadFileAt` definition's single-object
+ output annotation disagreed with its public byte-count annotation. Both now
+ describe `BufferSize` writable bytes.
+- **Already addressed before this review:** exact file reads return
+ `STATUS_END_OF_FILE` on short reads and wait for their own pending request.
+ Explicit digest helpers bound PE32/PE32+ headers, sections, certificate spans
+ and DER navigation. Certificate pointers borrow the stable input buffer.
+ [CI validation results](ci-validation-helpers.md) initialize outputs and own
+ both policies, including diagnostic allocations on failure. The current raw
+ CI declaration names the flags and byte-sized signing level; `0x20` broadens
+ root acceptance in examined builds, rather than declaring a file to be PE.
+
+Applications should avoid truncating file lengths to `EndOfFile.LowPart`,
+reparsing fixed-kind PE headers, consuming uninitialized CI policies, skipping
+cleanup on failure, or accepting invalid hexadecimal digits. The helpers below
+provide reusable primitives without defining application authorization policy.
+
+## File object, handle and path validation
+
+`DomitoValidateFileObjectEx`, `DomitoValidateFileHandleEx` and
+`DomitoValidateFilePathEx` expose the same explicit flags, requested signing
+level, selection mode and owning `DOMITO_CI_VALIDATION_RESULT`. None adds flags
+or falls back to a different validation policy. They require PASSIVE_LEVEL and
+Windows 10 compile targets; the CI imports must exist on the target OS.
+They do not require `DomitoInit`.
+
+The handle wrapper requires a trusted **kernel handle** with `FILE_READ_DATA`.
+It references its FILE_OBJECT, releases that reference on every exit and leaves
+the caller's handle open. Never pass an unchecked user handle: the reference is
+made using KernelMode. The caller must keep the handle valid during validation.
+
+The path wrapper accepts a counted absolute native path, for example
+`\??\C:\Program Files\Example\app.exe`. It creates a read-only kernel handle,
+shares only reads, references that same opened file for CI, and closes its own
+handle. Existing incompatible writers cause an open failure. Special kernel APCs
+must be enabled for synchronous I/O. Reparse points may redirect the open;
+validation describes the opened object. It does not establish the identity of
+a previously looked-up process image or certify in-memory process contents.
+File-object/handle callers must independently provide stable file bytes.
+
+```c
+DOMITO_CI_VALIDATION_RESULT result = {0};
+NTSTATUS status = DomitoValidateFilePathEx(&nativePath, selectedCiFlags, 0, DomitoCiSignatureSelectionNative, &result);
+if (NT_SUCCESS(status) && NT_SUCCESS(result.PolicyInfo.VerificationStatus))
+{
+ // Apply caller-provided certificate/publisher/pin policy here.
+ // A nonzero SigningTime is a separate consumer requirement, if desired.
+}
+DomitoFreeCiValidationResult(&result); // Also on CI or application-policy failure.
+```
+
+Modern file-object verification can select a preferred Microsoft nested signer
+where direct embedded verification selected the company signer. Changing paths
+therefore requires an explicit migration decision. Keep
+`DomitoValidatePortableExecutableEmbeddedSignature` for the stable PE-buffer,
+explicit-algorithm route. Its supported PE subset and nested-selection limitations
+still apply. It verifies the original first WIN_CERTIFICATE through CI, with no
+algorithm retry or catalog fallback. Existing legacy/catalog APIs remain
+available with explicit catalog policy; new convenience wrappers do not invoke
+them implicitly. An absent optional API is not permission to relax trust policy.
+
+## Certificate fingerprints and SPKI pins
+
+`DomitoGetCertificateSpki` extracts two borrowed views from stable certificate
+DER: `Encoded` contains the complete SPKI SEQUENCE TLV; `Content` omits that
+sequence's tag and length. It handles optional X.509 version fields and bounded,
+minimal definite DER lengths. It validates navigation/framing and the SPKI field
+shape, rather than every X.509 field, extension, name, algorithm parameter or
+signature. It does not verify trust. Failed extraction clears both views.
+
+`DomitoHashBuffer` hashes exact bytes using SHA-1/256/384/512 and Windows CNG.
+`HashSize` is capacity on entry, actual length on success, required length on
+`STATUS_BUFFER_TOO_SMALL`, and zero on other failures. A NULL hash buffer queries
+the required size. Operational CNG failures erase the selected digest bytes.
+Input/output buffers must not overlap. Only consume a digest after success.
+
+`DomitoHashCertificate` takes an explicit `DOMITO_CERTIFICATE_HASH_KIND`:
+
+- `DomitoCertificateHashDer`: exact complete certificate bytes, suitable for a
+ DER thumbprint. This mode hashes bytes; it does not parse or validate them.
+- `DomitoCertificateHashSpkiDer`: complete SPKI encoding, including outer TLV.
+- `DomitoCertificateHashSpkiContentLegacy`: **preserves the consumers' existing
+ content-only convention**. Existing pins must use this mode. Switching to the
+ complete encoding requires recomputing pins from the intended certificates;
+ do not silently treat hashes from the two representations as interchangeable.
+
+```c
+UCHAR pin[MINCRYPT_SHA256_LENGTH];
+ULONG pinSize = sizeof(pin);
+status =
+ DomitoHashCertificate(cert.Data, cert.Size, DomitoCertificateHashSpkiContentLegacy, CALG_SHA_256, pin, &pinSize);
+```
+
+`DomitoDecodeHex` accepts an exact counted Unicode hex string for a caller-selected
+binary length, rejects invalid digits/odd or mismatched lengths, and zeroes its
+destination on malformed input. `DomitoEncodeHex` writes uppercase Unicode hex
+with a NUL terminator; its capacity is in WCHARs including the terminator.
+Decode configuration pins once into binary and compare like representations.
+The library embeds no publisher names, root pins, or certificate allowlists.
+Hashing does not prove that a certificate is a root or trusted issuer.
+
+## Chain inspection and publisher ownership
+
+`DomitoGetCiChainView` requires a known, readable, initialized CI-owned policy
+and an explicit `DomitoCiChainLayoutObservedX64` argument. This asserts the
+sampled x64 common prefix (+0x28 elements, +0x30 count, +0x34 opaque RootId,
+minimum 56-byte header, 120-byte elements). It is **not** automatic OS/layout
+detection. Non-x64 builds return `STATUS_NOT_SUPPORTED`; allocation size and
+compile-target NTDDI cannot establish runtime layout compatibility.
+
+The accessor bounds the element array and every exposed certificate/name blob
+within CI's owned allocation before returning a view. It rejects oversized hash
+lengths and absent certificate bytes. It exposes neither trailing attributes nor
+EKU/public-key arrays. It cannot make a forged or freed kernel pointer safe;
+allocation extent/readability and the asserted layout remain caller preconditions.
+All returned pointers expire when the policy is freed or its result reused.
+
+Sampled CI results are leaf-first. Inspect `Elements[0]` for that selected leaf.
+`Elements[Count-1]` is the last represented certificate; this API does **not**
+promise it is self-signed, the configured trust anchor, or that the chain includes
+every certificate through a root. `RootId` is opaque and does not establish those
+properties. A consumer requiring a specific root must establish its intended CI
+policy and represented-anchor assumptions instead of inferring trust from position.
+
+The accessor deliberately imposes no bytewise issuer/next-subject equality.
+CI name blobs are not an independent chain verifier;
+their encoding and representation are version-sensitive. Current evidence does
+not establish universal byte equality as a CI API contract. Preserve such a check
+as an explicit downstream restriction if compatibility demands it, and assess its
+false-rejection risk. CI return status, policy verification status and application
+identity/pin requirements remain separate checks.
+
+`DomitoGetCertificatePublisherName` dynamically resolves the optional CI export
+instead of adding a static import. It requires `DomitoInit`; missing modules,
+exports and unsupported export forwarders propagate their lookup status.
+The caller passes an empty `UNICODE_STRING`, and uses
+`DomitoFreeCertificatePublisherName` to release/reset it. Partial allocations on
+failure are cleaned internally; the destructor is safe again afterward. Its
+private pool allocator is independent of configurable Domito callbacks. Never
+free this string through `G_Memory`, RtlFreeUnicodeString or the process-name free
+API. Display-name comparison remains caller policy, not certificate identity.
+
+## Process names, allocators and optional exports
+
+`DomitoGetProcessImageName` returns one Domito allocation containing the
+`UNICODE_STRING` and its buffer. `DomitoFreeProcessImageName` frees the entire
+allocation once, without querying callbacks in consumer code. Its output is
+NULL on failure. The existing ULONG PID interface remains; a PID lookup can race
+process exit/reuse before the initial open and is not a durable process identity.
+
+`DomitoGetProcessImageNameEx` instead accepts a caller-referenced PEPROCESS.
+It uses `SeLocateProcessImageName`, copies the name into the same Domito-owned
+representation and frees the OS allocation internally. The caller must keep the
+process referenced for the call. No Domito initialization is needed for this Ex
+API. The PID API still requires `DomitoInit` for its query routine.
+Neither name API proves that a subsequent path open addresses the original
+image file. WDF/requestor-process lifetime and authorization remain downstream.
+
+**Allocator contract:** the existing callback API does not attach allocator
+provenance to each result and is not synchronized for concurrent replacement.
+Configure a compatible allocate/free pair during initialization, before concurrent
+calls, and keep it unchanged until all callback-backed allocations and in-flight
+calls are released. A dedicated free name does not make changing callback pairs
+with outstanding allocations safe. Do not retrofit a hidden prefix onto existing
+allocations: callers may already free the public base address with their callback.
+CI policies and the publisher helper use their dedicated free APIs instead.
+
+`DomitoFindModuleExportedFunctionAddress` combines the existing full-module-path
+and export-name lookups, clears its output and preserves useful failure status.
+It requires `DomitoInit`, and does not pin the module. The caller must guarantee
+that the module remains mapped throughout lookup and use of its returned address.
+
+## Integration and consumer boundary
+
+Import `Domito.props` from the Domito package location or override
+`DomitoRootPath`. The default root is the sheet's own directory. x64/ARM64 and
+Debug/Release remain selected by the project; `DomitoLibraryPlatform` can override
+the library platform directory. Both WDM and KMDF final driver projects receive
+Domito, CI and CNG dependencies. Importing a sheet into a static library does not
+make link dependencies transitive: import it into the final driver as well.
+
+WDF caches, PID caches, administrator checks, developer bypasses, request allowlists,
+build expiry and selected-signing-time requirements stay in consumer code.
+No default trust evaluator is added: that would silently choose policy absent
+from the generic SDK. WPP diagnostics can enumerate the bounded chain view without
+fixed-depth indentation arrays. Report the returned NTSTATUS and local policy
+failure reason separately rather than mapping all failures inside the SDK.
+
+The new host suite compiles the production code unchanged apart from its kernel
+include, uses deterministic kernel doubles for ownership/failure paths, compares
+SPKI bytes and fingerprints with Crypt32, and uses real CNG for hashing. It tests
+both architecture conditional branches, which is not ARM64 runtime emulation.
+Real WDK compilation separately covers x64 and ARM64. These checks do not certify
+kernel CI behavior, every chain layout, or full X.509 validation.
+
+References: [SeLocateProcessImageName ownership](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-selocateprocessimagename),
+[BCryptCreateHash memory management and kernel requirements](https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash).
+
+## Deprecated compatibility entry points
+
+The public header uses MSVC `__declspec(deprecated("migration guidance"))` for
+four older functions. Calling or taking their address produces C4996 in C and
+C++; declarations and binary entry points remain available. Projects treating
+warnings as errors must migrate, or suppress C4996 locally around a deliberate
+compatibility call. Do not disable the warning globally to conceal accidental use.
+
+- `DomitoGetPortableExecutableDigestKind` →
+ `DomitoGetPortableExecutableDigestKindEx`: check NTSTATUS; unrecognized input
+ no longer silently chooses SHA-1. Neither function validates signature trust.
+- `DomitoCalculatePortableExecutableDigest` →
+ `DomitoCalculatePortableExecutableDigestEx`: select the algorithm explicitly
+ and review the supported layout/rejection rules. This is not a drop-in semantic
+ change for arbitrary files previously accepted by linear hashing.
+- `DomitoValidateFileLegacyMode` → `DomitoValidateFileLegacyModeEx`: select catalog
+ policy and trusted-catalog requirements explicitly, and consume the validation
+ source. The Ex legacy path remains available where catalog/older-OS behavior
+ is required; migration does not implicitly switch to modern signer selection.
+- `DomitoGetProcessImageName` → `DomitoGetProcessImageNameEx`: acquire and hold the
+ intended PEPROCESS reference, then use the dedicated name destructor. Looking
+ up a PID immediately before the Ex call cannot eliminate an earlier PID-reuse
+ race; obtain the process reference from the operation's intended identity.
+ This migration does not solve subsequent path-reopen identity races.
+
+No old type has an unconditional replacement. Raw CI/mincrypt types still
+describe the native ABI; the owning validation result and borrowed chain view
+serve different purposes. Raw Windows CI exports, allocator callbacks, module
+lookup primitives, exact file reads and digest cleanup remain supported.
diff --git a/docs/implementation-status.md b/docs/implementation-status.md
new file mode 100644
index 0000000..30d865f
--- /dev/null
+++ b/docs/implementation-status.md
@@ -0,0 +1,127 @@
+# CI SDK implementation status
+
+This ledger tracks the source audit S01–S18 and the remaining binary research.
+The original CodeIntegrity-RE audit remains a snapshot of commit `7985b23`;
+it should not be read as describing the corrected source unchanged.
+
+## Implemented with host tests or compilation
+
+- Additive CI result/selection helpers now package native file-object validation,
+ opt-in x64 first-success selection and explicit embedded verification, with
+ shared policy/digest cleanup. Existing APIs retain their behavior. See the
+ [helper contracts](ci-validation-helpers.md). Host failure/forwarding tests and
+ x64/ARM64 builds cover the wrappers; WPP probe v2 consumes them and awaits a
+ returned kernel trace. The previous v1 trace establishes the raw native routes.
+
+- **S01–S04:** subtraction-safe PE ranges, no fixed 4 MiB copy, bounded certificate
+ intervals, deterministic failure outputs, and digest cleanup. Compatibility
+ and explicit paths share the bounded hashing core.
+- **S05:** actual certificate read length checked; allocation bounded against
+ file size; per-operation completion waits through the exact-read helper.
+- **S06:** `DomitoReadFileAt` uses an explicit offset and private kernel event;
+ `DomitoReadFile` reads exactly from offset zero. Pending completion cannot
+ outlive its stack state or caller buffer. Short reads return EOF.
+- **S10:** export lookup uses the name ordinal to index the EAT, bounds tables,
+ names and resolved RVAs, and rejects forwarders. A valid loader-mapped module
+ remains a precondition because the API has no externally supplied image size.
+- **S11–S12:** corrected policy pointer typedef and publisher return/ownership docs.
+- **S14:** policy cleanup before embedded/catalog/reload retries, with host tests
+ allocating results even on failures. Real CI failure ownership still needs
+ the VM tests below.
+- **S17:** legacy hash algorithm/length combinations validated and calculation
+ versus verification capabilities documented.
+- **S18:** byte-count SAL, buffer-capacity rules, scalar input annotations, and
+ kernel read context/APC requirements documented.
+
+## Implemented within an explicit support boundary
+
+- **S07:** new bounded DER discovery reads the first outer SPC file-digest
+ AlgorithmIdentifier; new explicit digest API needs no discovery. The old
+ heuristic entry point retains compatibility behavior. Arbitrary BER, nested
+ signatures, and full signature enumeration remain outside this parser.
+- **S08:** unsigned/catalog-only files can be hashed with a caller-chosen
+ SHA-1 or SHA-256 strategy. The policy-explicit legacy helper forwards catalog
+ controls and reports the successful source. Real catalog lookup comparisons
+ across OS versions are pending; no implicit algorithm retry policy is imposed.
+- **S09:** the explicit digest path enforces a contiguous raw-section subset and
+ EOF certificate tables, with Windows SIP and real embedded-digest comparisons.
+ Gaps, overlaps, missing directory slots and nonterminal certificates are
+ rejected. The old linear compatibility entry point is not a universal hasher.
+- **S13/S15/S16:** recovered fields, parameter meanings, policy categories, sizes,
+ name encoding, borrowed lifetimes, and allocator ownership are documented.
+ The opt-in x64 26100 header provides distinct observed layouts and assertions.
+ Default declarations retain source compatibility; later chain fields require
+ a confirmed runtime layout. The sampled 1507 chain header is 56 bytes and has
+ no attributes descriptor at +0x38; 1511 uses 72 bytes; 1607–1803 samples use 104.
+ Distinct views are available in `Domito.MinCrypt.Historical.h`. Neither
+ `NTDDI_VERSION` nor total allocation Size identifies the layout.
+
+## Requires the test VM or additional matching binaries
+
+The first user-run kernel trace is available for Windows 11 22631 x64 with
+TESTSIGNING enabled. It confirms the BthPS3 signer-selection comparison and
+primary-signature CI path below. That one run, extracted artifacts, host mocks
+and compilation do not close the remaining work:
+
+1. Run the paired `CiValidateFileObject` flag `0`/`0x20` probe on 19041/19045
+ with TESTSIGNING off and a loadable signed test driver. Preserve complete
+ statuses, policies and fixture/environment identities (issue #3).
+2. Complete historical and ARM64 ABI/structure verification and runtime tests.
+ The [published export matrix](ci-api-compatibility.md) now covers all ten
+ named APIs across 20 exact Windows 10/11 x64 and ARM64 CI files.
+ ISO coverage includes Windows 10 1809/1903 revisions, 1909, 21H2/22H2 ARM64,
+ and Windows 11 23H2 x64 plus 22H2/24H2 ARM64.
+ The ARM64 observations establish export presence; their structure layouts
+ and runtime behavior remain unverified. This closes the initial export-presence
+ matrix; additional releases, servicing revisions and architectures remain
+ unsampled. It does not certify ABI or runtime behavior. The six 1507–1803
+ sampled bodies establish the
+ 48-byte policy storage and 64-byte hash-capacity gate. Chain constructors
+ establish 56/72/104-byte headers; opt-in views prevent treating 1507 payload
+ as the later attributes descriptor. Unsampled layouts/runtime remain open.
+3. Test real CI policy ownership on every failure/retry path under Driver
+ Verifier, including allocation failures and initialized outputs.
+4. Compare real embedded/catalog-only SHA-1/SHA-256 results, catalog cache/reload
+ behavior, trust-mask choices, requested signing levels and flag combinations.
+5. Measure signing-time propagation, zero/sentinel values, timestamp expiry,
+ revocation freshness/network policy and weak-crypto policy. Record OS policy,
+ Secure Boot/HVCI/WDAC and exact files, not just a boolean result.
+6. Extend the observed multiple/nested selection result beyond the first run.
+ On Windows 11 22631 x64 with TESTSIGNING enabled, `0x20` returns Microsoft;
+ `0xA0` and direct primary SHA-256 `CiCheckSignedFile` return Nefarius with the
+ same signing time and matching signer/timestamp policy observations. All
+ calls and policy statuses succeed, the file snapshots match, and no trace
+ events are lost. TESTSIGNING-off, general enumeration, full `0x80` semantics
+ and cross-build/policy behavior remain open. Extend supported PE gaps/overlays
+ only after differential evidence.
+7. Resolve page-hash allocation destruction, normalized-entry/truncation meaning,
+ terminal-entry handling and cross-version layouts before publishing a general
+ page-hash API. `CiFreePolicyInfo` is not an established destructor for it.
+8. Investigate protected-media/initialization and ordinal-only exports separately
+ if they are needed. Recovered export names alone are insufficient contracts.
+
+## Checks
+
+Run the six host runners in `tests/README.md`. Kernel compilation also covers
+the default modern x64/ARM64 structures, the pre-Windows-10 x64 compile variant,
+and 14 assertions in the 26100 opt-in header plus 12 historical-view assertions. Those are build checks,
+not a claim that historical or ARM64 CI runtime ABI has been verified.
+
+## Consumer-driven helpers
+
+The [consumer migration review](consumer-helpers.md) separates verified defects,
+existing corrections, new helpers and downstream policy. Additions include
+handle/path native CI wrappers, explicit SPKI representation/hash helpers,
+strict hex conversion, an explicit-layout x64 chain view, optional publisher
+resolution with matching cleanup, a referenced-process name API and dedicated
+process-name cleanup. The PID path fixes dangling failure output and excessive
+process access; module-base lookup clears stale output. WDM/KMDF linking is
+covered by the package-relative property sheet. No default publisher/root trust
+policy, automatic legacy fallback, or process authorization/cache is introduced.
+
+Four compatibility functions carry MSVC deprecation annotations with explicit
+migration messages: heuristic digest-kind discovery, compatibility PE hashing,
+implicit-policy legacy validation and PID-based process-name lookup. Native CI
+types/functions remain available for low-level callers. The compile-only
+deprecation suite checks actual public headers in C/C++, including modern API
+controls and expected C4996 diagnostics for each older entry point.
diff --git a/docs/mincrypt-contracts.md b/docs/mincrypt-contracts.md
new file mode 100644
index 0000000..e9da9a9
--- /dev/null
+++ b/docs/mincrypt-contracts.md
@@ -0,0 +1,327 @@
+# Code Integrity and mincrypt contracts
+
+The default header retains the existing structure layouts and calling shapes.
+Parameter names and ownership comments now distinguish the known controls from
+the verification result. `VerificationStatus` is an `NTSTATUS` with the same
+32-bit storage. The additional `Domito.MinCrypt.26100.h` header is an **opt-in
+description of ci.dll 10.0.26100.8328 x64**. It neither adds imports nor extends
+the default structures on every Windows target.
+
+The evidence is static REA/Ghidra analysis of SHA-256
+`c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b`, with
+Microsoft ci.pdb GUID `1CAE574B-CF1F-3564-5F74-49567A128629`, age 1 supplying the
+address/name map. The companion CodeIntegrity-RE repository retains the full
+records and readable `evidence/` files. The historical corpus below adds
+sampled x64 Windows 10 observations; ARM64 and unsampled-build compatibility
+remain open. Compile-target `NTDDI_VERSION` is not runtime layout negotiation.
+
+The original modern research and `Domito.MinCrypt.26100.h` use the string
+`FileVersion` label **26100.8328**. That same exact binary has numeric
+`VS_FIXEDFILEINFO` version **26100.9444**, which the export matrix uses. Its
+SHA-256 and recovered contracts are unchanged; these labels identify one file.
+
+## Historical x64 corpus: availability and structure evolution
+
+The [CI API compatibility matrix](ci-api-compatibility.md) lists observed named
+exports for 20 exact x64/ARM64 CI files, including the later ISO samples. Consult it
+for import availability, and the contracts below for layout and ownership limits.
+
+[NOTES.md](../NOTES.md) lists the exact identities of six sampled Windows 10
+files: `10.0.10240.16463`, `10.0.10586.0`, `10.0.14393.2189`, `10.0.15063.0`,
+`10.0.16299.125`, and `10.0.17134.1130`. All export `CiValidateFileObject` and
+the digest/catalog functions. The global setter is already present in sampled
+1703, correcting the old 1709 note. The publisher-name export is absent from
+all six. ISO samples additionally show absence in 19041.1 and presence in
+19041.1865 and 22000.318 x64; the original 26100 file also exports it. The first
+Windows 10 servicing update adding it remains unresolved. See CodeIntegrity-RE
+`iso-corpus-index.json`. Header guards do not negotiate runtime import availability.
+
+All six sampled x64 file-object exports transfer 48 bytes into each caller
+policy, with the owned chain pointer at `+0x10`; cleanup zeros `0x30` bytes. Their
+file-object helpers require hash capacity >=64, return `STATUS_BUFFER_TOO_SMALL`
+and required length 64 for a smaller buffer, and consume the third input as a
+byte. This supports the stable policy storage prefix on these exact artifacts;
+it does not establish all field semantics or every servicing build's behavior.
+
+**The chain header evolves within Windows 10. The default header's
+`NTDDI_VERSION >= NTDDI_WIN10` condition cannot safely expose every later field
+on Windows 10 1507.** Constructor evidence establishes:
+
+- `10.0.10240.16463` (1507): fixed header 56 bytes (`0x38`), root ID at `+0x34`,
+ variable payload beginning at `+0x38`. There is **no AuthenticodeAttributes
+ header field at `+0x38`**. Accessing the default structure's member on this
+ layout would interpret variable payload bytes as a blob descriptor.
+- `10.0.10586.0` (1511): fixed header 72 bytes (`0x48`), attributes descriptor at
+ `+0x38`, payload beginning at `+0x48`.
+- Sampled 1607/1703/1709/1803: fixed header 104 bytes (`0x68`), root ID still at
+ `+0x34`, attributes at `+0x38`, payload beginning at `+0x68`. Constructor evidence
+ establishes space for the 32-byte extension at `+0x48`, not its semantic
+ contents on every build. The historical view leaves this region reserved.
+
+`Domito.MinCrypt.Historical.h` supplies distinct opt-in x64 views for those
+observed profiles with 12 compile-time assertions. The common prefix ends after
+root ID at `+0x34`; chain elements use 120-byte strides in these constructors,
+but the prefix views expose their storage as opaque. Do not assume every element
+field is populated on every version. The 26100-specific header separately names
+its recovered binary-ID field.
+
+Choose a view only after establishing the runtime artifact/layout. `Size` is
+**total allocation bytes including payload**: a 1507 allocation can be larger
+than 72 or 104 bytes, so `Size >= sizeof(later_header)` is not version detection.
+Once layout identity is established, a size check is an additional field-read
+bound, not a substitute for identity. The default declarations remain unchanged
+for source compatibility; their later fields require this explicit runtime scope.
+
+Successful exported/helper records in CodeIntegrity-RE retain the evidence:
+
+- `historical/1507-export-contracts.json`:
+ `ev_489c3b57057b0bc5c86950d7bb4ea25108bbc7762abc363e1be7eb5cca034796`.
+- `historical/ci-10240-helpers.json`:
+ `ev_6d602c57de9614e7c5c28b9532c139f750f0e014b151000ecb41ad36f54bf764`.
+- `historical/ci-10586-exports.json`:
+ `ev_a09e04976cfd90e9ba69cc6618892f15c3da166c64fa9241efae0db85234fd6e`.
+- `historical/ci-10586-helpers.json`:
+ `ev_3403d5b8bf7c322fd924880d4cd9b296b98e94238cac2e1c1e9605c4b4d27255`.
+- `historical/ci-14393-contracts.json`:
+ `ev_e63a4c4b7613e231795051b784b9571605b9bf81258b5dcd04d32ddc681abb41`.
+- `historical/ci-15063-contracts.json`:
+ `ev_338658fb82559fa6daa8ae5487b90fe84b8e408ea7578feed6d8dff5d590e114`.
+- `historical/ci-16299-contracts.json`:
+ `ev_fe5731175f0726f0d46af2f78ca615d284f8598939296815e26b5589db3001cf`.
+- `historical/ci-17134-contracts.json`:
+ `ev_0c3e3359b8b7486318006afa6b95eb2257a18abeec9041a86a32db487bcbe996`.
+
+## Results and trust policy
+
+Check the API's returned `NTSTATUS` first. `PolicyInfo.VerificationStatus`,
+`PolicyBits`, signer details, and timestamps supply additional observations.
+Successful chain processing does not imply that the caller's file digest matched
+the signed digest; the comparison can occur afterward. Diagnostic policy data
+may exist on failure. An extracted publisher name is display information and
+does not establish publisher identity or signer trust.
+
+Define the policy the application needs: embedded signature validation,
+membership in an accepted installed catalog, an expected publisher/key, or a
+Windows signing scenario. Preserve which path produced a result. A nonzero
+`PolicyBits` value is not automatically failure, and any one root bit is not a
+complete trust decision. File bytes and object identity must remain tied to the
+bytes the application will subsequently use.
+
+The [file-object flag document](ci-validate-file-object.md) covers `0x20`, hash
+capacity, signing level, and its pending VM experiment. Other observed flag
+effects on 26100.8328 x64 are:
+
+- `0x1` selects hash scenario 5.
+- Otherwise `0x2` selects scenario 4 for signing levels <=4; higher levels use
+ the level mapping.
+- Otherwise `0x80000000` selects scenario 5 for levels <=4; higher levels use
+ the level mapping. It takes precedence over `0x4`.
+- Otherwise `0x4` selects scenario 1; without these choices the signing level
+ maps to a scenario.
+- `0x00400000` suppresses accepted-root bit `0x8` when the relevant helper mode
+ is enabled; CI can add this flag internally. Bits `0x2` and `0x8` also affect
+ later signing-level/EKU restrictions.
+
+The scenarios resolve through initialized signing-policy state. Static defaults
+are not a guarantee of a running system's algorithm, and the flags should not
+be named as universal fixed SHA selectors. Unknown bits remain unknown. Evidence:
+`CipGetHashAlgorithmForLegacyScenario`, `CipGetAcceptedRootKeys`, and
+`CipMinCryptToSigningLevel` in the research bundle.
+
+## Ownership and initialization
+
+Zero-initialize both `MINCRYPT_POLICY_INFO` output objects before their first
+use. Free both with `CiFreePolicyInfo` after use and before reuse, including
+failure paths. Never call the destructor on arbitrary uninitialized stack data.
+Do not clear a nonzero `Size` while a chain is still owned: the observed cleanup
+helper skips an object whose `Size` is zero.
+
+`ChainInfo` owns one allocation. Public-key blobs, names, encoded certificates,
+EKUs, attributes, and chain elements are borrowed views into it. Do not free
+children separately or retain them after releasing the owner. Copying the policy
+structure with `memcpy` creates two aliases to one allocation, not independent
+ownership. CI's internal deep-copy helper uses the total allocation size and
+rebases interior pointers; Domito does not expose that helper.
+
+The default `CiFreePolicyInfo` declaration retains its legacy `PVOID` return to
+avoid a broad source compatibility change. **Ignore that value**: observed x64
+assembly provides no defined pointer result. The opt-in build-specific function
+pointer type uses `VOID`.
+
+`CatalogName` is a separate `RtlDuplicateUnicodeString` allocation in the observed
+catalog export. Initialize it empty, release a returned buffer with
+`RtlFreeUnicodeString`, and release both policies separately. Publisher-name
+storage is allocated through the caller's callback and must be released with
+that allocator's matching free operation, including an allocated buffer on a
+later conversion failure. It is not owned by either policy object.
+
+Evidence: `MincryptFreePolicyInfo`, `MincryptCopyPolicyInfo`,
+`I_MincryptAddChainInfo`, `CiVerifyHashInCatalog`, and `CiGetCertPublisherName`.
+
+The additive [validation helpers](ci-validation-helpers.md) package safe result
+ownership, explicit file-object selection and embedded CI verification. They do
+not change raw CI declarations or existing legacy-helper behavior.
+
+## Multiple signatures and selected results
+
+`CiValidateFileObject` returns one selected signature result and its timestamp
+policy. Those two policy outputs do not enumerate two file signatures. The
+output file digest and algorithm are not expected-certificate selectors.
+
+REA tracing of x64 CI 22621.2792 and the original 26100 sample found a
+flag-dependent distinction: ordinary `0x20` can continue nested-signature
+verification and retain a policy-preferred signing level. Adding `0x80` enters
+an early-exit path after the first successful full candidate verification.
+`0xA0` is therefore a concrete kernel experiment, not a proved stable
+"select primary signature" flag: `0x80` also affects other policy/fallback paths.
+An arbitrary signer index, expected-certificate predicate and continuation
+interface are not exposed by these examined wrappers.
+
+For the supplied BthPS3 fixture, host SignTool verifies a primary Nefarius
+SHA-256 signature and a nested Microsoft SHA-256 signature. On both examined CI
+builds, `CiCheckSignedFile` with the matching SHA-256 digest follows the primary
+signature directly. This gives a CI-based alternative worth testing without
+implementing RSA/ECDSA, chain or timestamp verification. A subsequent user-run
+probe on Windows 11 22631 x64 with TESTSIGNING enabled confirms that `0x20`
+returns Microsoft while `0xA0` and direct SHA-256 `CiCheckSignedFile` return the
+Nefarius leaf and its signing time. Every top-level and signer/timestamp policy
+status succeeds; the two company-signer routes have matching policy observations.
+The companion `signature-selection/runtime-22631-bthps3.json` retains exact
+fixture/trace identities. This does not establish TESTSIGNING-off or general
+cross-build behavior. A different layout may still require bounded signature
+framing/enumeration.
+
+The companion CodeIntegrity-RE `SIGNATURE-SELECTION.md` retains fixture hashes,
+56 procedure bodies, assembly, flag side effects and the kernel test plan.
+The 23H2 batch is
+`ev_049bd7d3aa492a9e3e4d8fcbf365ae00a6057751b26f93f3c1663c9436952ff7`;
+its primary-selection helper is in
+`ev_c867c73ca7eae7e4fbca2f44c6f6fb5904fb00459d0b84f2da8294b5b05a3866`.
+
+## Digest-based verification inputs
+
+`CiCheckSignedFile` consumes a caller-computed file digest. It does not reopen
+the file or compute its Authenticode hash. On the observed build it accepts
+SHA-1/256/384/512 with exactly 20/32/48/64 bytes respectively. A wrong algorithm,
+length, or digest fails the final comparison.
+
+Its signature input points to a `WIN_CERTIFICATE` header. The observed wrapper
+requires 4-byte pointer alignment, supplied size >=12, revision `0x200`, type 2,
+and `8 < dwLength <= supplied size`. The PKCS#7 payload begins at header+8.
+The wrapper selects the first certificate-table entry. Multiple table entries
+and nested signatures are separate concepts; this is not a complete signature
+enumeration interface.
+
+These exports are present in all six sampled Windows 10 x64 files and the
+analyzed Windows 11 file. The older header's “Win7–8.1 only” comments described
+a workflow, not verified export availability. Earlier first-available builds
+and pre/post-KB3033929 ABI changes still require matching Windows 7/8 artifacts.
+Import-library names and stdcall stack-byte counts alone do not settle semantics.
+
+## Catalog controls
+
+`ReloadCatalogs` controls catalog-store reload and related context setup.
+`RequireTrustedCatalog` is a nonzero gate requiring an observed catalog extension
+marker: its matching bytes include `PE`, integer `0x10001`, and `TRUSTED`.
+The marker is an additional catalog condition, not proof of a Microsoft signer.
+
+`AcceptedCatalogPolicyBits` accepts a candidate when
+`(RequestedMask & Catalog.PolicyInfo.PolicyBits) != 0`: **any overlap** is enough.
+It neither enables policy flags nor requires every requested bit. The existing
+Domito compatibility value is named `DOMITO_CI_LEGACY_CATALOG_POLICY_BITS`
+(`0x2007F`). It includes test/DMD categories and diagnostic bit `0x20000`, while
+omitting other categories. It is retained as an existing behavior, not promoted
+as a universally safe trust mask. Establish and test the application's accepted
+signer policy before changing it.
+
+Observed file-hash lookup supports SHA-1 and SHA-256; the internal path has no
+SHA-384/512 file-hash case. Embedded-signature algorithm support and catalog
+lookup support are different capabilities. Catalog-only files need an explicit,
+tested algorithm strategy rather than assuming SHA-1 from an absent embedded
+table. Catalog stores, caches, silo/process context, and signing policy affect
+results. The new `DomitoValidateFileLegacyModeEx` forwards a caller-selected mask and
+marker gate and reports embedded/catalog origin. Its compatibility wrapper retains
+the existing mask. See [digest and workflow details](authenticode-digests.md).
+
+Domito's older-Windows helper keeps its system-process attachment until
+historical runtime evidence supports changing it.
+
+Evidence: `I_FindFileOrHeaderHashInLoadedCatalogs` and its assembly
+`ev_c42e7ee76099d55e7cf7cdd3d9f79e99030ba637680365612afb4906694ae3a8`,
+`I_ParseCatalog`, and marker matcher
+`ev_a1d3a5844a2b6be6c16175e307956c9819d0b64a4fe70216f292c12e4ea38d82`.
+
+## Opt-in x64 26100 structure observations
+
+The version header has distinct type names and 14 compile-time layout assertions.
+Use it only after independently establishing the matching runtime build and
+architecture. A large `ChainInfo->Size` alone does not identify the header: it
+includes variable trailing data, so an older allocation could also be large.
+Only after layout identity is established may a size check protect a field read.
+
+- `DOMITO_CI26100_NAME_BLOB`: pointer at +0, byte length at +8, ASN.1 string tag
+ at +10. Names are not necessarily NUL-terminated. Observed conversion handles
+ UTF8String tag `0x0C` and BMPString `0x1E`; `PCHAR` in the default declaration
+ must not be read as a guarantee of ANSI encoding.
+- `DOMITO_CI26100_CHAIN_INFO`: `Size` is total allocation bytes; fixed header is
+ 104 bytes. Root ID at +0x34 is an opaque integer, not policy bits. The 32 bytes
+ at +0x48 are a platform-manifest binary ID from an authenticated attribute,
+ not a file digest or certificate thumbprint. Zero/nonzero does not by itself
+ prove trust. Require the matching layout and `Size >=104` before reading it.
+- `DOMITO_CI26100_POLICY_INFO`: 48 bytes; `NotBefore`/`NotAfter` come from the
+ signer certificate validity fields, not an intersection of the whole chain.
+ Interpret `RevocationTime` only with the relevant result. A zero signing time
+ is not a real timestamp to display as a signing date.
+
+Construction/copy evidence: `ev_d7110cfc711074a4af1874b5c8e701c13bcc3dc58a1596c64dca4ca98966c79e`,
+`ev_858afecf2170484325379b6a8bc307fb72dad2f9a5a765169192c7363a3386af`,
+`ev_f82cbee977715399323bb398781c125a9d19b4ea29acd05063ec55b711cfc670`.
+
+## Partial policy-bit dictionary
+
+Names in the opt-in header are Domito descriptions of observed 26100 branches,
+not official Microsoft constants or a universal trusted-root enumeration.
+
+Root/provenance categories include `0x1` self-issued terminal path, `0x2` selected
+built-in Microsoft roots, `0x4` test roots, `0x8` the built-in root associated with
+embedded picroot handling, `0x10` tolerated partial/unresolved chain, `0x20` DMD,
+`0x40` DMD test, `0x80` another built-in Microsoft root, `0x400` flight root,
+`0x2000` third-party marketplace, `0x4000` explicit supplied certificate/key
+acceptance, and `0x8000` Microsoft Identity Root 2020. `0x800` and `0x1000` are
+additional discriminators on some root entries; their independent meanings
+remain unresolved. These categories do not establish a full trust result.
+
+Observed diagnostic branches set `0x20000` chain failure, `0x40000` invalid
+cryptographic/signature data, `0x80000` unmet EKU, `0x100000` page-hash failure,
+`0x200000` revocation, `0x400000` certificate validity failure (including
+not-yet-valid), `0x08000000` path-length constraints, `0x10000000` generic fallback,
+and `0x20000000` CA/BasicConstraints failure. `0x04000000` marks a no-usable-
+timestamp branch during revoked-certificate recovery, not universal timestamp
+absence. `0x50000` and `0x60000` are observed composites; do not infer a unique
+meaning for every constituent bit. Timestamp-before-revocation processing can
+clear a revocation bit. Internal checks of high bits are not a replacement for
+the API status and application policy.
+
+## Remaining runtime and API coverage
+
+The missing page-hash exports have recovered argument shapes in the research
+report. Their normalized allocation uses a 24-byte x64 header and 24-byte entries
+even for stronger source algorithms. Field meaning, truncation intent, source
+terminal-entry handling, and a supported destructor remain open. `CiFreePolicyInfo`
+is not a demonstrated destructor for that allocation. No universal page-hash SDK
+or static import is added until those ownership and cross-version contracts can
+be tested.
+
+`CiGetPEInformation` is a protected-media dispatcher, not a general PE parser.
+`CiInitialize` is initialization machinery. `CiSetTrustedOriginClaimId` writes a
+CI global; it is not an ordinary per-file trust selector. Ordinal-only exports
+and other initialization interfaces require their own evidence and callers.
+
+Still required: complete sampled-build/ARM64 layout and parameter verification, real CI
+output lifetimes and retry behavior under failures, timestamp propagation,
+revocation freshness/network policy, weak-crypto selection, multiple/nested
+signatures, catalog-only algorithms, and extension of the bounded digest subset to additional PE layouts.
+The explicit digest helper now has local Windows SIP and real embedded-digest
+comparisons; this does not close those historical or unusual-layout cases. Run the prepared probes in disposable VMs with exact OS,
+CI binary, configuration, and fixture identities. Compilation and host mocks do
+not close those runtime questions.
diff --git a/include/Domito.Consumer.h b/include/Domito.Consumer.h
new file mode 100644
index 0000000..f2d9b79
--- /dev/null
+++ b/include/Domito.Consumer.h
@@ -0,0 +1,200 @@
+#pragma once
+#include "Domito.h"
+
+// Consumer helpers: see docs/consumer-helpers.md for ownership and trust limits.
+typedef struct _DOMITO_BYTE_VIEW
+{
+ const UCHAR* Data;
+ ULONG Size;
+} DOMITO_BYTE_VIEW;
+
+typedef struct _DOMITO_CERTIFICATE_SPKI
+{
+ DOMITO_BYTE_VIEW Encoded; // Complete SPKI SEQUENCE, including tag and length.
+ DOMITO_BYTE_VIEW Content; // Legacy consumer convention: excludes that header.
+} DOMITO_CERTIFICATE_SPKI;
+
+typedef enum _DOMITO_CERTIFICATE_HASH_KIND
+{
+ DomitoCertificateHashDer = 0,
+ DomitoCertificateHashSpkiDer = 1,
+ DomitoCertificateHashSpkiContentLegacy = 2
+} DOMITO_CERTIFICATE_HASH_KIND;
+
+// Bounded positional DER extraction, not X.509 validation. Views borrow CertData.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoGetCertificateSpki(
+ _In_reads_bytes_(CertSize) const UCHAR* CertData,
+ _In_ ULONG CertSize,
+ _Out_ DOMITO_CERTIFICATE_SPKI* Spki
+);
+
+// SHA-1/256/384/512. HashSize is capacity on entry, bytes on success, required
+// size on BUFFER_TOO_SMALL, zero on other failures. Hash may be NULL to size.
+// Input/output must not overlap. Hash contains zero bytes on operational failure.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoHashBuffer(
+ _In_reads_bytes_opt_(Size) const UCHAR* Bytes,
+ _In_ ULONG Size,
+ _In_ ALG_ID Algorithm,
+ _Out_writes_bytes_to_opt_(*HashSize, *HashSize) UCHAR* Hash,
+ _Inout_ ULONG* HashSize
+);
+
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoHashCertificate(
+ _In_reads_bytes_(CertSize) const UCHAR* CertData,
+ _In_ ULONG CertSize,
+ _In_ DOMITO_CERTIFICATE_HASH_KIND Kind,
+ _In_ ALG_ID Algorithm,
+ _Out_writes_bytes_to_opt_(*HashSize, *HashSize) UCHAR* Hash,
+ _Inout_ ULONG* HashSize
+);
+
+// Strict length-delimited hex: no spaces, prefixes, separators or invalid digits.
+// Nonoverlapping buffers. Decode clears its output on malformed input.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoDecodeHex(
+ _In_ const UNICODE_STRING* Hex,
+ _Out_writes_bytes_(ByteCount) UCHAR* Bytes,
+ _In_ ULONG ByteCount
+);
+
+// Writes uppercase hex plus NUL; HexChars includes terminator capacity.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoEncodeHex(
+ _In_reads_bytes_opt_(ByteCount) const UCHAR* Bytes,
+ _In_ ULONG ByteCount,
+ _Out_writes_(HexChars) WCHAR* Hex,
+ _In_ ULONG HexChars
+);
+
+// Resolve optional exports without creating a static import. Requires DomitoInit.
+// ModuleName is the full loaded-module path used by FindModuleBaseAddress.
+// Caller must keep that module mapped; this does not acquire a loader reference.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoFindModuleExportedFunctionAddress(
+ _In_ PANSI_STRING ModuleName,
+ _In_ PANSI_STRING FunctionName,
+ _Outptr_result_maybenull_ PVOID* FunctionAddress
+);
+
+// Results from either process-name API use one Domito allocation. The memory
+// callback pair must remain unchanged until all allocations are released.
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+void
+DomitoFreeProcessImageName(
+ _In_opt_ PUNICODE_STRING ImageName
+);
+
+// Caller holds a referenced PEPROCESS throughout the call. No PID re-lookup.
+// Returns a copy owned by Domito; use DomitoFreeProcessImageName once.
+// Name lookup does not establish the identity of a later reopened file.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoGetProcessImageNameEx(
+ _In_ PEPROCESS Process,
+ _Outptr_result_maybenull_ PUNICODE_STRING* ImageName
+);
+
+#if (NTDDI_VERSION >= NTDDI_WIN10)
+// These wrappers do not require DomitoInit. Handle must be a trusted kernel
+// handle with FILE_READ_DATA; it is borrowed, referenced for the call, not closed.
+// Same initialized-result/cleanup rules and explicit flags as FileObjectEx.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoValidateFileHandleEx(
+ _In_ HANDLE FileHandle,
+ _In_ UINT32 ValidationFlags,
+ _In_ UINT8 RequestedSigningLevel,
+ _In_ DOMITO_CI_SIGNATURE_SELECTION Selection,
+ _Inout_ DOMITO_CI_VALIDATION_RESULT* Result
+);
+
+// Absolute native path (e.g. \??\C:\...). Opens read-only, shares only reads,
+// closes its own handle. The path can be reparsed; this validates the opened
+// file, not process memory or a previous path identity. No fallback is implicit.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoValidateFilePathEx(
+ _In_ const UNICODE_STRING* Path,
+ _In_ UINT32 ValidationFlags,
+ _In_ UINT8 RequestedSigningLevel,
+ _In_ DOMITO_CI_SIGNATURE_SELECTION Selection,
+ _Inout_ DOMITO_CI_VALIDATION_RESULT* Result
+);
+
+typedef enum _DOMITO_CI_CHAIN_LAYOUT
+{
+ // Explicit caller assertion: observed x64 +0x28 array/+0x30 count, 120-byte
+ // elements and a minimum 56-byte header. NOT selected by allocation Size.
+ DomitoCiChainLayoutObservedX64 = 1
+} DOMITO_CI_CHAIN_LAYOUT;
+
+typedef struct _DOMITO_CI_CHAIN_VIEW
+{
+ const MINCRYPT_CHAIN_ELEMENT* Elements;
+ ULONG Count;
+ ULONG RootId; // Opaque CI identifier, not proof the last element is a root.
+} DOMITO_CI_CHAIN_VIEW;
+
+// Only accepts a readable, initialized CI-owned policy, never arbitrary pointers.
+// Checks every exposed name/certificate span against CI's owned allocation.
+// Leaf-first order is observed in sampled layouts. Last element is not promised
+// to be a self-signed/trusted root. No issuer/subject byte equality is imposed.
+// View borrows policy storage until CiFreePolicyInfo/result reuse. x64 only.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoGetCiChainView(
+ _In_ const MINCRYPT_POLICY_INFO* Policy,
+ _In_ DOMITO_CI_CHAIN_LAYOUT Layout,
+ _Out_ DOMITO_CI_CHAIN_VIEW* View
+);
+
+// Optional export, resolved at call time; requires DomitoInit. No static import
+// of CiGetCertPublisherName. Extracts a display name, not certificate trust.
+// Start with {0}; release even after failure using the dedicated free function.
+// Never pass an owning previous string: free it before making another call.
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoGetCertificatePublisherName(
+ _In_ const MINCERT_BLOB* Certificate,
+ _Out_ PUNICODE_STRING PublisherName
+);
+
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+void
+DomitoFreeCertificatePublisherName(
+ _Inout_ PUNICODE_STRING PublisherName
+);
+#endif
diff --git a/include/Domito.MinCrypt.26100.h b/include/Domito.MinCrypt.26100.h
new file mode 100644
index 0000000..ec5c7cf
--- /dev/null
+++ b/include/Domito.MinCrypt.26100.h
@@ -0,0 +1,212 @@
+/* Opt-in observed-build declarations, not a universal Windows CI SDK.
+ * Observed: ci.dll 10.0.26100.8328, x64, SHA-256 documented in docs/mincrypt-contracts.md.
+ * Include after ntddk.h. Uses distinct names and function-pointer types so
+ * including this file does not change the default layouts or add static imports.
+ * Verify the actual CI build/architecture before use; Size alone cannot prove
+ * layout identity. See docs/mincrypt-contracts.md for provenance and limits.
+ * Field/parameter names are descriptive analyst names unless stated otherwise.
+ */
+#pragma once
+
+#if !defined(_M_AMD64) && !defined(__x86_64__)
+#error This research layout is validated only for x64.
+#endif
+
+#pragma pack(push, 8)
+
+typedef struct _DOMITO_CI26100_BLOB
+{
+ ULONG Size;
+ UCHAR* Data;
+} DOMITO_CI26100_BLOB;
+
+typedef struct _DOMITO_CI26100_NAME_BLOB
+{
+ UCHAR* Data; /* Borrowed, length-delimited encoded bytes. */
+ USHORT Size; /* Bytes, not characters. */
+ UCHAR Asn1StringTag; /* Observed: 0x0c UTF8String, 0x1e BMPString. */
+} DOMITO_CI26100_NAME_BLOB;
+
+typedef struct _DOMITO_CI26100_CHAIN_ELEMENT
+{
+ ULONG HashAlgId;
+ ULONG HashSize;
+ UCHAR Hash[64];
+ DOMITO_CI26100_NAME_BLOB Subject;
+ DOMITO_CI26100_NAME_BLOB Issuer;
+ DOMITO_CI26100_BLOB Certificate;
+} DOMITO_CI26100_CHAIN_ELEMENT;
+
+typedef struct _DOMITO_CI26100_CHAIN_INFO
+{
+ ULONG Size; /* Entire allocation, including trailing data. */
+ DOMITO_CI26100_BLOB* PublicKeys;
+ ULONG NumberOfPublicKeys;
+ DOMITO_CI26100_BLOB* EKUs;
+ ULONG NumberOfEKUs;
+ DOMITO_CI26100_CHAIN_ELEMENT* ChainElements;
+ ULONG NumberOfChainElements;
+ ULONG RootId; /* Opaque classification, NOT policy bits. */
+ DOMITO_CI26100_BLOB AuthenticodeAttributes;
+ UCHAR PlatformManifestBinaryId[32]; /* NOT a file or certificate hash. */
+} DOMITO_CI26100_CHAIN_INFO;
+
+typedef struct _DOMITO_CI26100_POLICY_INFO
+{
+ ULONG Size;
+ NTSTATUS VerificationStatus; /* Does not replace the API return status. */
+ ULONG PolicyBits;
+ DOMITO_CI26100_CHAIN_INFO* ChainInfo;
+ LARGE_INTEGER RevocationTime;
+ LARGE_INTEGER NotBefore;
+ LARGE_INTEGER NotAfter;
+} DOMITO_CI26100_POLICY_INFO, *PDOMITO_CI26100_POLICY_INFO;
+
+#pragma pack(pop)
+
+C_ASSERT(sizeof(DOMITO_CI26100_BLOB) == 16);
+C_ASSERT(sizeof(DOMITO_CI26100_NAME_BLOB) == 16);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_NAME_BLOB,
+ Asn1StringTag
+ ) == 10
+);
+C_ASSERT(sizeof(DOMITO_CI26100_CHAIN_ELEMENT) == 120);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_CHAIN_ELEMENT,
+ Certificate
+ ) == 104
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_CHAIN_INFO,
+ RootId
+ ) == 52
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_CHAIN_INFO,
+ AuthenticodeAttributes
+ ) == 56
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_CHAIN_INFO,
+ PlatformManifestBinaryId
+ ) == 72
+);
+C_ASSERT(sizeof(DOMITO_CI26100_CHAIN_INFO) == 104);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_POLICY_INFO,
+ ChainInfo
+ ) == 16
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_POLICY_INFO,
+ RevocationTime
+ ) == 24
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_POLICY_INFO,
+ NotBefore
+ ) == 32
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI26100_POLICY_INFO,
+ NotAfter
+ ) == 40
+);
+C_ASSERT(sizeof(DOMITO_CI26100_POLICY_INFO) == 48);
+
+/* Zero-initialize policies before first use. Free existing owned chains before
+ * reuse. Never copy these structs as independently owned objects with memcpy.
+ * Size is an output/initialization marker here, not runtime ABI negotiation.
+ */
+typedef VOID(NTAPI* DOMITO_CI26100_FREE_POLICY_INFO)(DOMITO_CI26100_POLICY_INFO* PolicyInfo);
+
+typedef NTSTATUS(NTAPI* DOMITO_CI26100_VALIDATE_FILE_OBJECT)(
+ PFILE_OBJECT FileObject,
+ ULONG ValidationFlags,
+ UCHAR RequestedSigningLevel,
+ DOMITO_CI26100_POLICY_INFO* PolicyInfo,
+ DOMITO_CI26100_POLICY_INFO* TimeStampPolicyInfo,
+ LARGE_INTEGER* SigningTime,
+ UCHAR* Hash, /* Provide 64-byte capacity, even for SHA-1. */
+ ULONG* HashSize, /* In: capacity >=64. Out success: actual size. */
+ ULONG* HashAlgId
+);
+
+typedef NTSTATUS(NTAPI* DOMITO_CI26100_CHECK_SIGNED_FILE)(
+ PVOID Hash,
+ ULONG HashSize,
+ ULONG HashAlgId,
+ PVOID WinCertificate, /* WIN_CERTIFICATE header, not bCertificate. */
+ ULONG SizeOfSecurityDirectory,
+ DOMITO_CI26100_POLICY_INFO* PolicyInfo,
+ LARGE_INTEGER* SigningTime,
+ DOMITO_CI26100_POLICY_INFO* TimeStampPolicyInfo
+);
+
+/* The existing BOOLEAN reload parameter ABI was not independently resolved
+ * beyond the full-width use in this build; retain Domito's declaration until
+ * cross-architecture call-site evidence settles its formal type.
+ * Fifth argument nonzero filters on the catalog TRUSTED marker.
+ * Sixth argument requires ANY overlap with catalog PolicyInfo.PolicyBits.
+ */
+typedef NTSTATUS(NTAPI* DOMITO_CI26100_VERIFY_HASH_IN_CATALOG)(
+ PVOID Hash,
+ ULONG HashSize,
+ ULONG HashAlgId,
+ BOOLEAN ReloadCatalogs,
+ ULONG RequireTrustedCatalog,
+ ULONG AcceptedCatalogPolicyBits,
+ DOMITO_CI26100_POLICY_INFO* PolicyInfo,
+ UNICODE_STRING* CatalogName, /* Optional; separate RtlFreeUnicodeString. */
+ LARGE_INTEGER* SigningTime,
+ DOMITO_CI26100_POLICY_INFO* TimeStampPolicyInfo
+);
+
+typedef PVOID(NTAPI* DOMITO_CI26100_ALLOCATE)(SIZE_T ByteSize);
+typedef NTSTATUS(NTAPI* DOMITO_CI26100_GET_CERT_PUBLISHER_NAME)(
+ DOMITO_CI26100_BLOB* Certificate,
+ DOMITO_CI26100_ALLOCATE AllocateRoutine,
+ UNICODE_STRING* PublisherName
+); /* Free with callback's allocator pair. */
+
+/* Proposed descriptive diagnostic names. Not official Microsoft names, not
+ * an exhaustive dictionary, and not accepted-input validation flags.
+ * See the report for narrower branch conditions and composite values.
+ */
+#define DOMITO_CI26100_POLICY_SELF_ISSUED_TERMINAL 0x00000001UL
+// Also present on rgbMicrosoftRoot4 together with bit 0x800.
+#define DOMITO_CI26100_POLICY_MICROSOFT_ROOT_0_1_2 0x00000002UL
+#define DOMITO_CI26100_POLICY_TEST_ROOT 0x00000004UL
+#define DOMITO_CI26100_POLICY_MICROSOFT_ROOT_3 0x00000008UL
+#define DOMITO_CI26100_POLICY_DMD_ROOT 0x00000020UL
+#define DOMITO_CI26100_POLICY_DMD_TEST_ROOT 0x00000040UL
+#define DOMITO_CI26100_POLICY_MICROSOFT_ROOT_5 0x00000080UL
+#define DOMITO_CI26100_POLICY_FLIGHT_ROOT 0x00000400UL
+#define DOMITO_CI26100_POLICY_MARKETPLACE_ROOT 0x00002000UL
+#define DOMITO_CI26100_POLICY_SUPPLIED_CERT_OR_KEY 0x00004000UL
+#define DOMITO_CI26100_POLICY_IDENTITY_ROOT_2020 0x00008000UL
+#define DOMITO_CI26100_POLICY_PARTIAL_CHAIN 0x00000010UL
+#define DOMITO_CI26100_POLICY_CHAIN_FAILURE 0x00020000UL
+#define DOMITO_CI26100_POLICY_CRYPTO_FAILURE 0x00040000UL
+#define DOMITO_CI26100_POLICY_EKU_FAILURE 0x00080000UL
+#define DOMITO_CI26100_POLICY_PAGE_HASH_FAILURE 0x00100000UL
+#define DOMITO_CI26100_POLICY_CERT_REVOKED 0x00200000UL
+#define DOMITO_CI26100_POLICY_CERT_VALIDITY_FAILURE 0x00400000UL
+#define DOMITO_CI26100_POLICY_PATH_LENGTH_FAILURE 0x08000000UL
+#define DOMITO_CI26100_POLICY_GENERIC_FAILURE 0x10000000UL
+#define DOMITO_CI26100_POLICY_CA_CONSTRAINT_FAILURE 0x20000000UL
+
+/* Narrow branch observation: no usable timestamp during revoked-cert recovery;
+ * not a universal flag for all signatures without a timestamp.
+ */
+#define DOMITO_CI26100_POLICY_REVOCATION_NO_TIMESTAMP 0x04000000UL
diff --git a/include/Domito.MinCrypt.Historical.h b/include/Domito.MinCrypt.Historical.h
new file mode 100644
index 0000000..3939172
--- /dev/null
+++ b/include/Domito.MinCrypt.Historical.h
@@ -0,0 +1,120 @@
+/* Opt-in x64 chain views for exact observed Windows 10 CI artifacts.
+ * Include after ntddk.h. This does not alter Domito's default declarations.
+ * Select a view only after establishing the matching runtime layout from
+ * build/artifact evidence. ChainInfo->Size is TOTAL allocation size; it cannot
+ * identify the header version, because variable payload follows the header.
+ * See docs/mincrypt-contracts.md and NOTES.md for exact sampled versions/hashes.
+ */
+#pragma once
+#include "Domito.MinCrypt.h"
+
+#if !defined(_M_AMD64) && !defined(__x86_64__)
+#error Historical CI views are validated only for the documented x64 artifacts.
+#endif
+
+#pragma pack(push, 8)
+
+// Observed ci.dll 10.0.10240.16463. The variable payload begins at +0x38;
+// AuthenticodeAttributes does not exist at that offset in this header.
+typedef struct _DOMITO_CI10240_CHAIN_INFO
+{
+ UINT32 Size;
+ MINCERT_BLOB* PublicKeys;
+ UINT32 NumberOfPublicKeys;
+ MINCERT_BLOB* EKUs;
+ UINT32 NumberOfEKUs;
+ PVOID ChainElements; // Constructor copies 120-byte elements; borrowed storage.
+ UINT32 NumberOfChainElements;
+ UINT32 RootId; // Opaque identifier, not PolicyBits.
+} DOMITO_CI10240_CHAIN_INFO, *PDOMITO_CI10240_CHAIN_INFO;
+
+// Observed ci.dll 10.0.10586.0. The variable payload begins at +0x48.
+typedef struct _DOMITO_CI10586_CHAIN_INFO
+{
+ UINT32 Size;
+ MINCERT_BLOB* PublicKeys;
+ UINT32 NumberOfPublicKeys;
+ MINCERT_BLOB* EKUs;
+ UINT32 NumberOfEKUs;
+ PVOID ChainElements;
+ UINT32 NumberOfChainElements;
+ UINT32 RootId;
+ MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
+} DOMITO_CI10586_CHAIN_INFO, *PDOMITO_CI10586_CHAIN_INFO;
+
+// Observed 10.0.14393.2189, 15063.0, 16299.125 and 17134.1130 x64.
+// The variable payload begins at +0x68. Constructor evidence establishes the
+// 32-byte extension's space, not its semantic contents on every sampled build.
+typedef struct _DOMITO_CI14393_CHAIN_INFO
+{
+ UINT32 Size;
+ MINCERT_BLOB* PublicKeys;
+ UINT32 NumberOfPublicKeys;
+ MINCERT_BLOB* EKUs;
+ UINT32 NumberOfEKUs;
+ PVOID ChainElements;
+ UINT32 NumberOfChainElements;
+ UINT32 RootId;
+ MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
+ UINT8 ReservedExtension[32];
+} DOMITO_CI14393_CHAIN_INFO, *PDOMITO_CI14393_CHAIN_INFO;
+
+#pragma pack(pop)
+
+C_ASSERT(sizeof(DOMITO_CI10240_CHAIN_INFO) == 56);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI10240_CHAIN_INFO,
+ PublicKeys
+ ) == 8
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI10240_CHAIN_INFO,
+ EKUs
+ ) == 24
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI10240_CHAIN_INFO,
+ ChainElements
+ ) == 40
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI10240_CHAIN_INFO,
+ RootId
+ ) == 52
+);
+C_ASSERT(sizeof(DOMITO_CI10586_CHAIN_INFO) == 72);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI10586_CHAIN_INFO,
+ RootId
+ ) == 52
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI10586_CHAIN_INFO,
+ AuthenticodeAttributes
+ ) == 56
+);
+C_ASSERT(sizeof(DOMITO_CI14393_CHAIN_INFO) == 104);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI14393_CHAIN_INFO,
+ RootId
+ ) == 52
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI14393_CHAIN_INFO,
+ AuthenticodeAttributes
+ ) == 56
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ DOMITO_CI14393_CHAIN_INFO,
+ ReservedExtension
+ ) == 72
+);
diff --git a/include/Domito.MinCrypt.h b/include/Domito.MinCrypt.h
index 63d1596..48a5dbb 100644
--- a/include/Domito.MinCrypt.h
+++ b/include/Domito.MinCrypt.h
@@ -22,16 +22,16 @@
#pragma once
- // Warnings which disabled for compiling
+// Warnings which disabled for compiling
#if _MSC_VER >= 1200
#pragma warning(push)
// nonstandard extension used : nameless struct/union
-#pragma warning(disable:4201)
+#pragma warning(disable : 4201)
// 'struct_name' : structure was padded due to __declspec(align())
-#pragma warning(disable:4324)
+#pragma warning(disable : 4324)
// 'enumeration': a forward declaration of an unscoped enumeration must have an
// underlying type (int assumed)
-#pragma warning(disable:4471)
+#pragma warning(disable : 4471)
#endif
EXTERN_C_START
@@ -47,144 +47,144 @@ EXTERN_C_START
//
// ALG_ID crackers
-#define GET_ALG_CLASS(x) (x & (7 << 13))
-#define GET_ALG_TYPE(x) (x & (15 << 9))
-#define GET_ALG_SID(x) (x & (511))
+#define GET_ALG_CLASS(x) (x & (7 << 13))
+#define GET_ALG_TYPE(x) (x & (15 << 9))
+#define GET_ALG_SID(x) (x & (511))
// Algorithm classes
// certenrolld_begin -- ALG_CLASS_*
-#define ALG_CLASS_ANY (0)
-#define ALG_CLASS_SIGNATURE (1 << 13)
-#define ALG_CLASS_MSG_ENCRYPT (2 << 13)
-#define ALG_CLASS_DATA_ENCRYPT (3 << 13)
-#define ALG_CLASS_HASH (4 << 13)
-#define ALG_CLASS_KEY_EXCHANGE (5 << 13)
-#define ALG_CLASS_ALL (7 << 13)
+#define ALG_CLASS_ANY (0)
+#define ALG_CLASS_SIGNATURE (1 << 13)
+#define ALG_CLASS_MSG_ENCRYPT (2 << 13)
+#define ALG_CLASS_DATA_ENCRYPT (3 << 13)
+#define ALG_CLASS_HASH (4 << 13)
+#define ALG_CLASS_KEY_EXCHANGE (5 << 13)
+#define ALG_CLASS_ALL (7 << 13)
// certenrolld_end
// Algorithm types
-#define ALG_TYPE_ANY (0)
-#define ALG_TYPE_DSS (1 << 9)
-#define ALG_TYPE_RSA (2 << 9)
-#define ALG_TYPE_BLOCK (3 << 9)
-#define ALG_TYPE_STREAM (4 << 9)
-#define ALG_TYPE_DH (5 << 9)
-#define ALG_TYPE_SECURECHANNEL (6 << 9)
+#define ALG_TYPE_ANY (0)
+#define ALG_TYPE_DSS (1 << 9)
+#define ALG_TYPE_RSA (2 << 9)
+#define ALG_TYPE_BLOCK (3 << 9)
+#define ALG_TYPE_STREAM (4 << 9)
+#define ALG_TYPE_DH (5 << 9)
+#define ALG_TYPE_SECURECHANNEL (6 << 9)
#if (NTDDI_VERSION >= NTDDI_VISTA)
-#define ALG_TYPE_ECDH (7 << 9)
+#define ALG_TYPE_ECDH (7 << 9)
#endif //(NTDDI_VERSION >= NTDDI_VISTA)
#if (NTDDI_VERSION >= NTDDI_WIN10_RS1)
-#define ALG_TYPE_THIRDPARTY (8 << 9)
+#define ALG_TYPE_THIRDPARTY (8 << 9)
#endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1)
// Generic sub-ids
-#define ALG_SID_ANY (0)
+#define ALG_SID_ANY (0)
// Generic ThirdParty sub-ids
#if (NTDDI_VERSION >= NTDDI_WIN10_RS1)
-#define ALG_SID_THIRDPARTY_ANY (0)
+#define ALG_SID_THIRDPARTY_ANY (0)
#endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1)
// Some RSA sub-ids
-#define ALG_SID_RSA_ANY 0
-#define ALG_SID_RSA_PKCS 1
-#define ALG_SID_RSA_MSATWORK 2
-#define ALG_SID_RSA_ENTRUST 3
-#define ALG_SID_RSA_PGP 4
+#define ALG_SID_RSA_ANY 0
+#define ALG_SID_RSA_PKCS 1
+#define ALG_SID_RSA_MSATWORK 2
+#define ALG_SID_RSA_ENTRUST 3
+#define ALG_SID_RSA_PGP 4
// Some DSS sub-ids
//
-#define ALG_SID_DSS_ANY 0
-#define ALG_SID_DSS_PKCS 1
-#define ALG_SID_DSS_DMS 2
+#define ALG_SID_DSS_ANY 0
+#define ALG_SID_DSS_PKCS 1
+#define ALG_SID_DSS_DMS 2
#if (NTDDI_VERSION >= NTDDI_VISTA)
-#define ALG_SID_ECDSA 3
+#define ALG_SID_ECDSA 3
#endif //(NTDDI_VERSION >= NTDDI_VISTA)
// Block cipher sub ids
// DES sub_ids
-#define ALG_SID_DES 1
-#define ALG_SID_3DES 3
-#define ALG_SID_DESX 4
-#define ALG_SID_IDEA 5
-#define ALG_SID_CAST 6
-#define ALG_SID_SAFERSK64 7
-#define ALG_SID_SAFERSK128 8
-#define ALG_SID_3DES_112 9
-#define ALG_SID_CYLINK_MEK 12
-#define ALG_SID_RC5 13
+#define ALG_SID_DES 1
+#define ALG_SID_3DES 3
+#define ALG_SID_DESX 4
+#define ALG_SID_IDEA 5
+#define ALG_SID_CAST 6
+#define ALG_SID_SAFERSK64 7
+#define ALG_SID_SAFERSK128 8
+#define ALG_SID_3DES_112 9
+#define ALG_SID_CYLINK_MEK 12
+#define ALG_SID_RC5 13
#if (NTDDI_VERSION >= NTDDI_WINXP)
-#define ALG_SID_AES_128 14
-#define ALG_SID_AES_192 15
-#define ALG_SID_AES_256 16
-#define ALG_SID_AES 17
+#define ALG_SID_AES_128 14
+#define ALG_SID_AES_192 15
+#define ALG_SID_AES_256 16
+#define ALG_SID_AES 17
#endif //(NTDDI_VERSION >= NTDDI_WINXP)
// Fortezza sub-ids
-#define ALG_SID_SKIPJACK 10
-#define ALG_SID_TEK 11
+#define ALG_SID_SKIPJACK 10
+#define ALG_SID_TEK 11
// KP_MODE
-#define CRYPT_MODE_CBCI 6 // ANSI CBC Interleaved
-#define CRYPT_MODE_CFBP 7 // ANSI CFB Pipelined
-#define CRYPT_MODE_OFBP 8 // ANSI OFB Pipelined
-#define CRYPT_MODE_CBCOFM 9 // ANSI CBC + OF Masking
-#define CRYPT_MODE_CBCOFMI 10 // ANSI CBC + OFM Interleaved
+#define CRYPT_MODE_CBCI 6 // ANSI CBC Interleaved
+#define CRYPT_MODE_CFBP 7 // ANSI CFB Pipelined
+#define CRYPT_MODE_OFBP 8 // ANSI OFB Pipelined
+#define CRYPT_MODE_CBCOFM 9 // ANSI CBC + OF Masking
+#define CRYPT_MODE_CBCOFMI 10 // ANSI CBC + OFM Interleaved
// RC2 sub-ids
-#define ALG_SID_RC2 2
+#define ALG_SID_RC2 2
// Stream cipher sub-ids
-#define ALG_SID_RC4 1
-#define ALG_SID_SEAL 2
+#define ALG_SID_RC4 1
+#define ALG_SID_SEAL 2
// Diffie-Hellman sub-ids
-#define ALG_SID_DH_SANDF 1
-#define ALG_SID_DH_EPHEM 2
-#define ALG_SID_AGREED_KEY_ANY 3
-#define ALG_SID_KEA 4
+#define ALG_SID_DH_SANDF 1
+#define ALG_SID_DH_EPHEM 2
+#define ALG_SID_AGREED_KEY_ANY 3
+#define ALG_SID_KEA 4
#if (NTDDI_VERSION >= NTDDI_VISTA)
-#define ALG_SID_ECDH 5
-#define ALG_SID_ECDH_EPHEM 6
+#define ALG_SID_ECDH 5
+#define ALG_SID_ECDH_EPHEM 6
#endif //(NTDDI_VERSION >= NTDDI_VISTA)
// Hash sub ids
-#define ALG_SID_MD2 1
-#define ALG_SID_MD4 2
-#define ALG_SID_MD5 3
-#define ALG_SID_SHA 4
-#define ALG_SID_SHA1 4
-#define ALG_SID_MAC 5
-#define ALG_SID_RIPEMD 6
-#define ALG_SID_RIPEMD160 7
-#define ALG_SID_SSL3SHAMD5 8
-#define ALG_SID_HMAC 9
-#define ALG_SID_TLS1PRF 10
+#define ALG_SID_MD2 1
+#define ALG_SID_MD4 2
+#define ALG_SID_MD5 3
+#define ALG_SID_SHA 4
+#define ALG_SID_SHA1 4
+#define ALG_SID_MAC 5
+#define ALG_SID_RIPEMD 6
+#define ALG_SID_RIPEMD160 7
+#define ALG_SID_SSL3SHAMD5 8
+#define ALG_SID_HMAC 9
+#define ALG_SID_TLS1PRF 10
#if (NTDDI_VERSION >= NTDDI_WINXP)
-#define ALG_SID_HASH_REPLACE_OWF 11
+#define ALG_SID_HASH_REPLACE_OWF 11
#endif //(NTDDI_VERSION >= NTDDI_WINXP)
#if (NTDDI_VERSION > NTDDI_WINXPSP2)
-#define ALG_SID_SHA_256 12
-#define ALG_SID_SHA_384 13
-#define ALG_SID_SHA_512 14
+#define ALG_SID_SHA_256 12
+#define ALG_SID_SHA_384 13
+#define ALG_SID_SHA_512 14
#endif //(NTDDI_VERSION > NTDDI_WINXPSP2)
// secure channel sub ids
-#define ALG_SID_SSL3_MASTER 1
-#define ALG_SID_SCHANNEL_MASTER_HASH 2
-#define ALG_SID_SCHANNEL_MAC_KEY 3
-#define ALG_SID_PCT1_MASTER 4
-#define ALG_SID_SSL2_MASTER 5
-#define ALG_SID_TLS1_MASTER 6
-#define ALG_SID_SCHANNEL_ENC_KEY 7
+#define ALG_SID_SSL3_MASTER 1
+#define ALG_SID_SCHANNEL_MASTER_HASH 2
+#define ALG_SID_SCHANNEL_MAC_KEY 3
+#define ALG_SID_PCT1_MASTER 4
+#define ALG_SID_SSL2_MASTER 5
+#define ALG_SID_TLS1_MASTER 6
+#define ALG_SID_SCHANNEL_ENC_KEY 7
#if (NTDDI_VERSION >= NTDDI_VISTA)
// misc ECC sub ids
-#define ALG_SID_ECMQV 1
+#define ALG_SID_ECMQV 1
#endif //(NTDDI_VERSION >= NTDDI_VISTA)
// Our silly example sub-id
-#define ALG_SID_EXAMPLE 80
+#define ALG_SID_EXAMPLE 80
// certenrolls_begin -- PROV_ENUMALGS_EX
#ifndef ALGIDDEF
@@ -194,132 +194,139 @@ typedef unsigned int ALG_ID;
// certenrolls_end
// algorithm identifier definitions
-#define CALG_MD2 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD2)
-#define CALG_MD4 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD4)
-#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5)
-#define CALG_SHA (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA)
-#define CALG_SHA1 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA1)
-#define CALG_MAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MAC) // Deprecated. Don't use.
-#define CALG_RSA_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_RSA | ALG_SID_RSA_ANY)
-#define CALG_DSS_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_DSS_ANY)
+#define CALG_MD2 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD2)
+#define CALG_MD4 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD4)
+#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5)
+#define CALG_SHA (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA)
+#define CALG_SHA1 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA1)
+#define CALG_MAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MAC) // Deprecated. Don't use.
+#define CALG_RSA_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_RSA | ALG_SID_RSA_ANY)
+#define CALG_DSS_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_DSS_ANY)
#if (NTDDI_VERSION >= NTDDI_WINXP)
-#define CALG_NO_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_ANY | ALG_SID_ANY)
+#define CALG_NO_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_ANY | ALG_SID_ANY)
#endif //(NTDDI_VERSION >= NTDDI_WINXP)
-#define CALG_RSA_KEYX (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_RSA|ALG_SID_RSA_ANY)
-#define CALG_DES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_DES)
-#define CALG_3DES_112 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_3DES_112)
-#define CALG_3DES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_3DES)
-#define CALG_DESX (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_DESX)
-#define CALG_RC2 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_RC2)
-#define CALG_RC4 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_STREAM|ALG_SID_RC4)
-#define CALG_SEAL (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_STREAM|ALG_SID_SEAL)
-#define CALG_DH_SF (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_DH_SANDF)
-#define CALG_DH_EPHEM (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_DH_EPHEM)
-#define CALG_AGREEDKEY_ANY (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_AGREED_KEY_ANY)
-#define CALG_KEA_KEYX (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_KEA)
-#define CALG_HUGHES_MD5 (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_ANY|ALG_SID_MD5)
-#define CALG_SKIPJACK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_SKIPJACK)
-#define CALG_TEK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_TEK)
-#define CALG_CYLINK_MEK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_CYLINK_MEK) // Deprecated. Do not use
-#define CALG_SSL3_SHAMD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SSL3SHAMD5)
-#define CALG_SSL3_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SSL3_MASTER)
-#define CALG_SCHANNEL_MASTER_HASH (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_MASTER_HASH)
-#define CALG_SCHANNEL_MAC_KEY (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_MAC_KEY)
-#define CALG_SCHANNEL_ENC_KEY (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_ENC_KEY)
-#define CALG_PCT1_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_PCT1_MASTER)
-#define CALG_SSL2_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SSL2_MASTER)
-#define CALG_TLS1_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_TLS1_MASTER)
-#define CALG_RC5 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_RC5)
-#define CALG_HMAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HMAC)
-#define CALG_TLS1PRF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_TLS1PRF)
+#define CALG_RSA_KEYX (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_RSA | ALG_SID_RSA_ANY)
+#define CALG_DES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_DES)
+#define CALG_3DES_112 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_3DES_112)
+#define CALG_3DES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_3DES)
+#define CALG_DESX (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_DESX)
+#define CALG_RC2 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_RC2)
+#define CALG_RC4 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_STREAM | ALG_SID_RC4)
+#define CALG_SEAL (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_STREAM | ALG_SID_SEAL)
+#define CALG_DH_SF (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_DH_SANDF)
+#define CALG_DH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_DH_EPHEM)
+#define CALG_AGREEDKEY_ANY (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_AGREED_KEY_ANY)
+#define CALG_KEA_KEYX (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_KEA)
+#define CALG_HUGHES_MD5 (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_MD5)
+#define CALG_SKIPJACK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_SKIPJACK)
+#define CALG_TEK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_TEK)
+#define CALG_CYLINK_MEK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_CYLINK_MEK) // Deprecated. Do not use
+#define CALG_SSL3_SHAMD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SSL3SHAMD5)
+#define CALG_SSL3_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SSL3_MASTER)
+#define CALG_SCHANNEL_MASTER_HASH (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_MASTER_HASH)
+#define CALG_SCHANNEL_MAC_KEY (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_MAC_KEY)
+#define CALG_SCHANNEL_ENC_KEY (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_ENC_KEY)
+#define CALG_PCT1_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_PCT1_MASTER)
+#define CALG_SSL2_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SSL2_MASTER)
+#define CALG_TLS1_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_TLS1_MASTER)
+#define CALG_RC5 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_RC5)
+#define CALG_HMAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HMAC)
+#define CALG_TLS1PRF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_TLS1PRF)
#if (NTDDI_VERSION >= NTDDI_WINXP)
-#define CALG_HASH_REPLACE_OWF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HASH_REPLACE_OWF)
-#define CALG_AES_128 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_128)
-#define CALG_AES_192 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_192)
-#define CALG_AES_256 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_256)
-#define CALG_AES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES)
+#define CALG_HASH_REPLACE_OWF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HASH_REPLACE_OWF)
+#define CALG_AES_128 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_128)
+#define CALG_AES_192 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_192)
+#define CALG_AES_256 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_256)
+#define CALG_AES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES)
#endif //(NTDDI_VERSION >= NTDDI_WINXP)
#if (NTDDI_VERSION > NTDDI_WINXPSP2)
-#define CALG_SHA_256 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_256)
-#define CALG_SHA_384 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_384)
-#define CALG_SHA_512 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_512)
+#define CALG_SHA_256 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_256)
+#define CALG_SHA_384 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_384)
+#define CALG_SHA_512 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_512)
#endif //(NTDDI_VERSION > NTDDI_WINXPSP2)
#if (NTDDI_VERSION >= NTDDI_VISTA)
-#define CALG_ECDH (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_ECDH)
-#define CALG_ECDH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ECDH | ALG_SID_ECDH_EPHEM)
-#define CALG_ECMQV (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_ECMQV)
-#define CALG_ECDSA (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_ECDSA)
-#define CALG_NULLCIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_ANY | 0)
+#define CALG_ECDH (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_ECDH)
+#define CALG_ECDH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ECDH | ALG_SID_ECDH_EPHEM)
+#define CALG_ECMQV (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_ECMQV)
+#define CALG_ECDSA (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_ECDSA)
+#define CALG_NULLCIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_ANY | 0)
#endif //(NTDDI_VERSION >= NTDDI_VISTA)
#if (NTDDI_VERSION >= NTDDI_WIN10_RS1)
-#define CALG_THIRDPARTY_KEY_EXCHANGE (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
-#define CALG_THIRDPARTY_SIGNATURE (ALG_CLASS_SIGNATURE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
-#define CALG_THIRDPARTY_CIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
-#define CALG_THIRDPARTY_HASH (ALG_CLASS_HASH | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
+#define CALG_THIRDPARTY_KEY_EXCHANGE (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
+#define CALG_THIRDPARTY_SIGNATURE (ALG_CLASS_SIGNATURE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
+#define CALG_THIRDPARTY_CIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
+#define CALG_THIRDPARTY_HASH (ALG_CLASS_HASH | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY)
#endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1)
#include
+
typedef struct _MINCRYPTOAPI_BLOB
{
UINT32 Size;
- _Field_size_bytes_(Size) UINT8* Data;
+ _Field_size_bytes_(Size) UINT8* Data;
-} MINCRYPT_INTEGER_BLOB, * PMINCRYPT_INTEGER_BLOB,
-MINCRYPT_UINT_BLOB, * PMINCRYPT_UINT_BLOB,
-MINCRYPT_OBJID_BLOB, * PMINCRYPT_OBJID_BLOB,
-MINCERT_NAME_BLOB, * PMINCERT_NAME_BLOB,
-MINCERT_RDN_VALUE_BLOB, * PMINCERT_RDN_VALUE_BLOB,
-MINCERT_BLOB, * PMINCERT_BLOB,
-MINCRL_BLOB, * PMINCRL_BLOB,
-MINCRYPT_DATA_BLOB, * PMINCRYPT_DATA_BLOB,
-MINCRYPT_HASH_BLOB, * PMINCRYPT_HASH_BLOB,
-MINCRYPT_DIGEST_BLOB, * PMINCRYPT_DIGEST_BLOB,
-MINCRYPT_DER_BLOB, * PMINCRYPT_DER_BLOB,
-MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB;
+} MINCRYPT_INTEGER_BLOB, *PMINCRYPT_INTEGER_BLOB, MINCRYPT_UINT_BLOB, *PMINCRYPT_UINT_BLOB, MINCRYPT_OBJID_BLOB,
+ *PMINCRYPT_OBJID_BLOB, MINCERT_NAME_BLOB, *PMINCERT_NAME_BLOB, MINCERT_RDN_VALUE_BLOB, *PMINCERT_RDN_VALUE_BLOB,
+ MINCERT_BLOB, *PMINCERT_BLOB, MINCRL_BLOB, *PMINCRL_BLOB, MINCRYPT_DATA_BLOB, *PMINCRYPT_DATA_BLOB,
+ MINCRYPT_HASH_BLOB, *PMINCRYPT_HASH_BLOB, MINCRYPT_DIGEST_BLOB, *PMINCRYPT_DIGEST_BLOB, MINCRYPT_DER_BLOB,
+ *PMINCRYPT_DER_BLOB, MINCRYPT_ATTR_BLOB, *PMINCRYPT_ATTR_BLOB;
+// Encoded, length-delimited name bytes; not necessarily ANSI or NUL-terminated.
+// The observed 26100 x64 string tag occupies padding; see the opt-in version header.
typedef struct _MINCRYPT_NAME_BLOB
{
- _Field_size_bytes_(Size) PCHAR Data;
+ _Field_size_bytes_(Size) PCHAR Data;
USHORT Size;
-}MINCRYPT_NAME_BLOB, * PMINCRYPT_NAME_BLOB;
+} MINCRYPT_NAME_BLOB, *PMINCRYPT_NAME_BLOB;
-#define MINCRYPT_MAX_HASH_LENGTH (64)
-#define MINCRYPT_SHA1_LENGTH (160/8)
-#define MINCRYPT_SHA256_LENGTH (256/8)
+#define MINCRYPT_MAX_HASH_LENGTH (64)
+#define MINCRYPT_SHA1_LENGTH (160 / 8)
+#define MINCRYPT_SHA256_LENGTH (256 / 8)
+#define MINCRYPT_SHA384_LENGTH (384 / 8)
+#define MINCRYPT_SHA512_LENGTH (512 / 8)
typedef struct _MINCRYPT_CHAIN_ELEMENT
{
- ALG_ID HashAlgId; // CALG_SHA1, CALG_SHA_256
- UINT32 HashSize; // SHA1(160bit/8), SHA256(256bit/8)
- UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH];
+ ALG_ID HashAlgId; // CALG_SHA1, CALG_SHA_256
+ UINT32 HashSize; // SHA1(160bit/8), SHA256(256bit/8)
+ UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH];
MINCRYPT_NAME_BLOB Subject;
MINCRYPT_NAME_BLOB Issuer;
- MINCERT_BLOB Certificate;
+ MINCERT_BLOB Certificate;
-}MINCRYPT_CHAIN_ELEMENT, * PMINCRYPT_CHAIN_ELEMENT;
+} MINCRYPT_CHAIN_ELEMENT, *PMINCRYPT_CHAIN_ELEMENT;
+// CI owns one chain allocation; nested pointers borrow that allocation.
+// Do not free child blobs separately or use memcpy to create a second owner.
typedef struct _MINCRYPT_CHAIN_INFO
{
- UINT32 Size;
+ UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data.
- MINCERT_BLOB* PublicKeys;
- UINT32 NumberOfPublicKeys;
+ MINCERT_BLOB* PublicKeys;
+ UINT32 NumberOfPublicKeys;
- MINCERT_BLOB* EKUs;
- UINT32 NumberOfEKUs;
+ MINCERT_BLOB* EKUs;
+ UINT32 NumberOfEKUs;
#if (NTDDI_VERSION >= NTDDI_WIN10)
PMINCRYPT_CHAIN_ELEMENT ChainElements;
- UINT32 NumberOfChainElement;
+ UINT32 NumberOfChainElement;
- // ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc.
+ // REQUIRES a confirmed runtime layout (10586 and later sampled profiles).
+ // Sampled 10240 has only 56 header bytes: +0x38 begins variable payload.
+ // NTDDI_VERSION and allocation Size cannot establish field availability.
+ // See Domito.MinCrypt.Historical.h for distinct observed x64 prefix views.
MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
- // UINT8 Hash[32];
+ // RootId occupies padding at +0x34 in all six historical x64 samples and
+ // 26100. The historical views name it without extending this default layout.
+ // 26100's 32-byte platform-manifest binary ID at +0x48 is named only in its
+ // explicit version header. Earlier 104-byte views reserve that extension.
+ // Do not infer field availability solely from NTDDI_VERSION or Size.
#endif // NTDDI_VERSION >= NTDDI_WIN10
@@ -333,60 +340,66 @@ typedef struct _MINCRYPT_CHAIN_INFO
// ChainElements[NumberOfChainElement]
-}MINCRYPT_CHAIN_INFO, * PMINCRYPT_CHAIN_INFO;
+} MINCRYPT_CHAIN_INFO, *PMINCRYPT_CHAIN_INFO;
typedef struct _MINCRYPT_POLICY_INFO
{
- UINT32 Size;
- UINT32 VerificationStatus;
- UINT32 PolicyBits;
+ UINT32 Size;
+ NTSTATUS VerificationStatus; // Check API return status separately.
+ UINT32 PolicyBits;
MINCRYPT_CHAIN_INFO* ChainInfo;
- LARGE_INTEGER RevocationTime; // When was the certificate revoked (if applicable)
+ LARGE_INTEGER RevocationTime; // When was the certificate revoked (if applicable)
#if (NTDDI_VERSION >= NTDDI_WIN10)
- LARGE_INTEGER NotBefore; // The certificate is not valid before this time
- LARGE_INTEGER NotAfter; // The certificate is not valid after this time
+ LARGE_INTEGER NotBefore; // Observed: signer certificate validity start.
+ LARGE_INTEGER NotAfter; // Observed: signer certificate validity end.
#endif // NTDDI_VERSION >= NTDDI_WIN10
-}MINCRYPT_POLICY_INFO, PMINCRYPT_POLICY_INFO;
+} MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO;
+
#include
-
-
/**
-* Resets a PolicyInfo struct - frees the dynamically allocated buffer in PolicyInfo (ChainInfo) if not null.
-* Zeros the entire PolicyInfo struct.
+* Release a known initialized policy's owned ChainInfo allocation and reset it.
+* Zero-initialize policy objects before first use. Release both signer and TSA
+* policies after use, including failures, and before reusing output storage.
+* Borrowed child pointers become invalid when the owner is released. Never clear
+* Size while a chain is still owned. Size does not negotiate the runtime layout.
*
-* @param PolicyInfo - the struct to reset.
+* @param PolicyInfo - initialized policy, not arbitrary uninitialized storage.
*
-* @return the struct which was reset.
+* Ignore the return value. The legacy PVOID declaration is retained for source
+* compatibility; observed 26100 x64 code provides no defined pointer return.
*/
_IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI
-PVOID
-NTAPI
+PVOID NTAPI
CiFreePolicyInfo(
_Inout_ MINCRYPT_POLICY_INFO* PolicyInfo
);
-
/**
-* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10!
+* Digest-based workflow, also implemented on observed Windows 11 build 26100.
+* Prefer the file-object workflow when its policy fits. Older-build ABI and
+* KB3033929 requirements need a separately verified compatibility matrix.
*
* Given a file digest and signature of a file, verify the signature and provide information regarding
* the certificates that was used for signing (the entire certificate chain)
*
-* @param Hash - buffer containing the digest
+* @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
*
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
*
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
*
-* @param SecurityDirectory - pointer to the start of the security directory
+* @param SecurityDirectory - WIN_CERTIFICATE header, not the PKCS#7 payload.
+* Observed 26100 x64 checks 4-byte pointer alignment, revision 0x200,
+* type 2, supplied size >=12 and 8 < dwLength <= supplied size.
+* This wrapper selects the first entry; it does not enumerate the table.
*
* @param SizeOfSecurityDirectory - size the security directory
*
@@ -396,94 +409,109 @@ CiFreePolicyInfo(
*
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain
*
-* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified.
+* @return Overall verification NTSTATUS. A successful policy VerificationStatus
+* does not replace this return value: the final file-digest comparison
+* can still fail. Both policy outputs may carry diagnostics on failure.
+* See docs/mincrypt-contracts.md for ownership and policy limits.
* Various error values otherwise, for example:
-* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
+* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/
_IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI
-NTSTATUS
-NTAPI
+NTSTATUS NTAPI
CiCheckSignedFile(
- _In_ PVOID Hash,
- _In_ UINT32 HashSize,
- _In_ ALG_ID HashAlgId,
- _In_ PVOID SecurityDirectory,
- _In_ UINT32 SizeOfSecurityDirectory,
+ _In_reads_bytes_(HashSize) PVOID Hash,
+ _In_ UINT32 HashSize,
+ _In_ ALG_ID HashAlgId,
+ _In_reads_bytes_(SizeOfSecurityDirectory) PVOID SecurityDirectory,
+ _In_ UINT32 SizeOfSecurityDirectory,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
- _Out_ LARGE_INTEGER* SigningTime,
+ _Out_ LARGE_INTEGER* SigningTime,
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo
);
+// Existing Domito compatibility filter, not a universal trusted-root policy.
+// Includes test/DMD categories and a diagnostic bit; select policy deliberately.
+#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
/**
-* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10!
+* Digest-based workflow, also implemented on observed Windows 11 build 26100.
+* Prefer the file-object workflow when its policy fits. Older-build ABI and
+* KB3033929 requirements need a separately verified compatibility matrix.
*
-* Checks if the SHA-1 message digest is contained within a verified system catalog
+* Finds a caller-computed file digest in an accepted installed catalog.
+* Observed 26100 x64 file-hash lookup supports SHA-1 and SHA-256.
*
* @note must be attached to the PsInitialSystemProcess first!
*
-* @param Hash - buffer containing the digest
+* @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
*
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
*
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
*
-* @param IsReloadCatalogs - is reload catalogs cache.
+* @param ReloadCatalogs - request catalog-store reload; process/silo and cache state matter.
*
-* @param Always0 - this is for IsReloadCatalogs, Always0 != 0 ? 16 : 24;
+* @param RequireTrustedCatalog - nonzero requires the observed catalog TRUSTED
+* extension marker. This does not itself establish Microsoft signer trust.
*
-* @param Always2007F - unknown, always 0x2007F, maybe a mask.
+* @param AcceptedCatalogPolicyBits - candidate catalog policy-bit filter.
+* Observed 26100 x64 requires ANY overlap with catalog PolicyBits, not
+* every requested bit. This is not a flags-to-enable input.
*
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
*
-* @param CatalogName[out option] - catalog file name.
+* @param CatalogName[out option] - initialize empty; observed 26100 x64 returns
+* a separate duplicated string allocation. Release a returned Buffer
+* with RtlFreeUnicodeString, separately from both policy outputs.
*
* @param SigningTime[out] - when the file was signed (FILETIME format)
*
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain.
*
-* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified.
+* @return Overall verification NTSTATUS. A successful policy VerificationStatus
+* does not replace this return value. Catalog selection and the
+* selected policy still apply. Outputs may carry diagnostics on failure.
+* See docs/mincrypt-contracts.md for ownership and policy limits.
* Various error values otherwise, for example:
-* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
+* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/
_IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI
-NTSTATUS
-NTAPI
+NTSTATUS NTAPI
CiVerifyHashInCatalog(
- _In_ PVOID Hash,
- _In_ UINT32 HashSize,
- _In_ ALG_ID HashAlgId,
- _In_ BOOLEAN IsReloadCatalogs,
- _In_ UINT32 Always0,
- _In_ UINT32 Always2007F,
+ _In_reads_bytes_(HashSize) PVOID Hash,
+ _In_ UINT32 HashSize,
+ _In_ ALG_ID HashAlgId,
+ _In_ BOOLEAN ReloadCatalogs,
+ _In_ UINT32 RequireTrustedCatalog,
+ _In_ UINT32 AcceptedCatalogPolicyBits,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
- _Out_opt_ UNICODE_STRING* CatalogName,
- _Out_ LARGE_INTEGER* SigningTime,
+ _Out_opt_ UNICODE_STRING* CatalogName,
+ _Out_ LARGE_INTEGER* SigningTime,
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo
);
-
#if (NTDDI_VERSION >= NTDDI_WIN10)
-typedef
-_IRQL_requires_same_
-_Function_class_(MINCRYPT_ALLOCATE_ROUTINE)
-__drv_allocatesMem(Mem)
+typedef _IRQL_requires_same_ _Function_class_(MINCRYPT_ALLOCATE_ROUTINE)
+__drv_allocatesMem(
+ Mem
+)
PVOID
NTAPI
-MINCRYPT_ALLOCATE_ROUTINE(
- _In_ SIZE_T ByteSize
-);
+MINCRYPT_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize);
typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE;
/**
-* Parse the publisher name from the certificate
+* Parse the publisher name from the certificate.
+* Export absent from sampled Windows 10 x64 builds 10240–17134; observed present
+* on 19041.1865, 22000.318 and 26100 x64; absent on sampled 19041.1 x64.
+* The NTDDI guard does not establish runtime availability. See NOTES.md.
*
* @param Certificate - &PolicyInfo.ChainInfo->ChainElements[x].Certificate
*
@@ -491,7 +519,10 @@ typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE;
*
* @param PublisherName[out] - publisher name.
*
-* @return buffer length.
+* @return NTSTATUS. The string byte count is returned in PublisherName->Length.
+* Free PublisherName->Buffer with the allocator paired with AllocateRoutine,
+* including on failure if a buffer was allocated. This routine extracts a name;
+* it does not verify the certificate or its signer.
*/
MINCRYPTAPI
NTSTATUS
@@ -502,23 +533,40 @@ CiGetCertPublisherName(
_Out_ PUNICODE_STRING PublisherName
);
-
+// Export observed already in 15063.0 x64 (1703); absent from sampled
+// 10240/10586/14393 files. This writes CI-global state, not per-file policy.
MINCRYPTAPI
-VOID
-NTAPI
+VOID NTAPI
CiSetTrustedOriginClaimId(
_In_ UINT32 ClaimId
);
+// Domito descriptive name, not an official Microsoft flag name.
+// Observed on CI 10.0.26100.8328 x64: broadens the accepted-root mask in
+// CipGetAcceptedRootKeys. Sampled 1709/1803 instead select 0x224FF; earlier helper
+// generations differ. This is not a WinVerifyTrust-equivalent trust policy.
+// See docs/ci-validate-file-object.md for evidence and version limitations.
+#define DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE 0x00000020UL
+
+// Descriptive experimental bit, not an official or isolated selector flag.
+// Participates in first-success guards in examined 22621/26100 x64 CI bodies;
+// also changes default-path policy and fallback. One 22631 TESTSIGNING-enabled
+// BthPS3 run confirms 0x20|0x80 returns its primary company signer. It is not an
+// arbitrary signer index, guaranteed primary selection, or a production default.
+#define DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL 0x00000080UL
+
/**
* Given a file object, verify the signature and provide information regarding
* the certificates that was used for signing (the entire certificate chain)
*
* @param FileObject - FileObject of the PE in question
*
-* @param Unkonwn1 - unknown, 0 is a valid value. (Unkonwn1 and Unkonwn2 together calculate the minimum support algorithm)
+* @param ValidationFlags - validation policy flags; 0 is a valid value.
+* See DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE and the accompanying documentation.
*
-* @param Unkonwn2 - unknown, 0 is a valid value. (^ the words above refer to 'CipGetHashAlgorithmForLegacyScenario')
+* @param RequestedSigningLevel - requested signing level; 0 is used by the probe.
+* CI 26100 x64 consumes the low byte. UINT32 is retained for ABI compatibility
+* until other supported architectures/builds are verified; use values in 0..255.
*
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
*
@@ -526,32 +574,37 @@ CiSetTrustedOriginClaimId(
*
* @param SigningTime[out] - when the file was signed (FILETIME format)
*
-* @param Hash - buffer containing the digest
+* @param Hash[out] - file digest, not a certificate thumbprint. Supply at least
+* MINCRYPT_MAX_HASH_LENGTH bytes (required by the observed 26100 x64 helper).
*
-* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
+* @param HashSize[in,out] - buffer capacity on entry; actual digest size on success.
+* Initialize to sizeof(Hash) for every call, including retries.
*
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
*
-* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified.
+* Initialize both policy outputs to empty objects and free both with CiFreePolicyInfo
+* before reuse. Do not interpret failed-call outputs as a verification result.
+*
+* @return STATUS_SUCCESS when verification succeeds under the selected CI policy.
+* This does not by itself establish the application's signer trust policy.
* Various error values otherwise, for example:
-* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
+* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/
_IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI
-NTSTATUS
-NTAPI
+NTSTATUS NTAPI
CiValidateFileObject(
- _In_ FILE_OBJECT* FileObject,
- _In_opt_ UINT32 Unkonwn1,
- _In_opt_ UINT32 Unkonwn2,
+ _In_ FILE_OBJECT* FileObject,
+ _In_ UINT32 ValidationFlags,
+ _In_ UINT32 RequestedSigningLevel,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
- _Out_ LARGE_INTEGER* SigningTime,
- _Out_ UINT8* Hash,
- _Inout_ UINT32* HashSize,
- _Out_ ALG_ID* HashAlgId
+ _Out_ LARGE_INTEGER* SigningTime,
+ _Out_writes_bytes_(*HashSize) UINT8* Hash,
+ _Inout_ UINT32* HashSize,
+ _Out_ ALG_ID* HashAlgId
);
#endif // NTDDI_VERSION >= NTDDI_WIN10
diff --git a/include/Domito.h b/include/Domito.h
index 68d22f4..d3e0737 100644
--- a/include/Domito.h
+++ b/include/Domito.h
@@ -2,7 +2,6 @@
#include
-
/* ___
* / __|___ _ __ _ __ ___ _ _
* | (__/ _ \ ' \| ' \/ _ \ ' \
@@ -12,7 +11,7 @@
//
// Library initialization tasks. Call once in your DriverEntry
-//
+//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -25,7 +24,6 @@ EXTERN_C
void
DomitoShutdown();
-
/* __ __ __ __ _
* | \/ |___ _ __ ___ _ _ _ _ | \/ |__ _ _ _ __ _ __ _ ___ _ __ ___ _ _| |_
* | |\/| / -_) ' \/ _ \ '_| || | | |\/| / _` | ' \/ _` / _` / -_) ' \/ -_) ' \ _|
@@ -35,34 +33,26 @@ DomitoShutdown();
//
// Allocator function the library uses.
-//
-typedef
-_IRQL_requires_same_
-_Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE)
-__drv_allocatesMem(mem)
+//
+typedef _IRQL_requires_same_ _Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE)
+__drv_allocatesMem(
+ mem
+)
PVOID
NTAPI
-EVT_DOMITO_ALLOCATE_ROUTINE(
- _In_ SIZE_T ByteSize
-);
+EVT_DOMITO_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize);
typedef EVT_DOMITO_ALLOCATE_ROUTINE* PFN_DOMITO_ALLOCATE_ROUTINE;
//
// Freeing function the library uses.
-//
-typedef
-_IRQL_requires_same_
-_Function_class_(EVT_DOMITO_FREE_ROUTINE)
-void
-NTAPI
-EVT_DOMITO_FREE_ROUTINE(
- _In_ __drv_freesMem(mem) PVOID Memory
-);
+//
+typedef _IRQL_requires_same_ _Function_class_(EVT_DOMITO_FREE_ROUTINE) void NTAPI
+EVT_DOMITO_FREE_ROUTINE(_In_ __drv_freesMem(mem) PVOID Memory);
typedef EVT_DOMITO_FREE_ROUTINE* PFN_DOMITO_FREE_ROUTINE;
//
// Get the original set of Domito memory functions.
-//
+//
EXTERN_C
void
DomitoGetOriginalMemoryFunctions(
@@ -72,7 +62,7 @@ DomitoGetOriginalMemoryFunctions(
//
// Get the current set of Domito memory functions.
-//
+//
EXTERN_C
void
DomitoGetMemoryFunctions(
@@ -81,7 +71,9 @@ DomitoGetMemoryFunctions(
);
//
-// Replace Domito's memory allocation functions with a custom set
+// Replace Domito's memory allocation functions with a compatible pair during
+// quiescent initialization. No in-flight calls or outstanding callback-backed
+// allocations may span a pair change. Replacement is not synchronized.
EXTERN_C
void
DomitoSetMemoryFunctions(
@@ -89,7 +81,6 @@ DomitoSetMemoryFunctions(
_In_opt_ PFN_DOMITO_FREE_ROUTINE Free
);
-
/* ___ _ _
* / __|_ _ _ _ _ __| |_ ___ __ _ _ _ __ _ _ __| |_ _ _
* | (__| '_| || | '_ \ _/ _ \/ _` | '_/ _` | '_ \ ' \ || |
@@ -97,9 +88,9 @@ DomitoSetMemoryFunctions(
* |__/|_| |___/ |_| |__/
*/
- //
- // This structure encapsulates a signature used in verifying executable files.
- //
+//
+// This structure encapsulates a signature used in verifying executable files.
+//
#if !defined(WIN_CERTIFICATE)
typedef struct _WIN_CERTIFICATE
{
@@ -112,16 +103,16 @@ typedef struct _WIN_CERTIFICATE
//
// UM definitions of WinCrypt.h
-//
+//
#if !defined(WIN_CERT_TYPE_X509)
-#define WIN_CERT_TYPE_X509 (0x0001) // The bCertificate member contains an X.509 certificate.
+#define WIN_CERT_TYPE_X509 (0x0001) // The bCertificate member contains an X.509 certificate.
#endif
#if !defined(WIN_CERT_TYPE_PKCS_SIGNED_DATA)
-#define WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002) // The bCertificate member contains a PKCS SignedData structure.
+#define WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002) // The bCertificate member contains a PKCS SignedData structure.
#endif
#if !defined(WIN_CERT_TYPE_PKCS1_SIGN)
-#define WIN_CERT_TYPE_PKCS1_SIGN (0x0009) // The bCertificate member contains PKCS1_MODULE_SIGN fields.
+#define WIN_CERT_TYPE_PKCS1_SIGN (0x0009) // The bCertificate member contains PKCS1_MODULE_SIGN fields.
#endif
#if !defined(CALG_SHA1)
@@ -141,6 +132,7 @@ typedef struct _WIN_CERTIFICATE
// Converts a WinCrypt CALG_ID to a BCRYPT_ALGORITHM identifier.
//
PCWSTR
+
FORCEINLINE
DOMITO_CALG_TO_BCRYPT_ALGORITHM(
_In_ UINT32 Calg
@@ -161,7 +153,6 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM(
}
}
-
/* ___ _ ___ _ _ _
* / __|___ __| |___ |_ _|_ _| |_ ___ __ _ _ _(_) |_ _ _
* | (__/ _ \/ _` / -_) | || ' \ _/ -_) _` | '_| | _| || |
@@ -170,26 +161,74 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM(
*/
//
-// Extracts the CALG_ID from a signed PE that was used to
-// calculate the message digest when it was signed
+// Heuristically detects the CALG_ID in the first WIN_CERTIFICATE using byte patterns.
+// Returns CALG_SHA1 when absent, malformed, or unrecognized; this is not ASN.1
+// validation or signature verification. The directory array must contain the
+// IMAGE_DIRECTORY_ENTRY_SECURITY entry.
//
_IRQL_requires_max_(DISPATCH_LEVEL)
EXTERN_C
-UINT32
+__declspec(deprecated(
+ "Use DomitoGetPortableExecutableDigestKindEx; heuristic detection silently falls back to SHA-1. Check the returned NTSTATUS."
+)) UINT32
DomitoGetPortableExecutableDigestKind(
_In_ PUCHAR pPeBytes,
_In_ ULONG PeSize,
_In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory
);
-//
-// Extracts Authenticode signing information and calculates the file digest of a PE file.
-//
+// Reads the first PKCS#7 entry's SpcIndirectDataContent DigestInfo algorithm
+// using bounded DER navigation. Does not validate the signature or select among
+// multiple/nested signatures. Absent table => STATUS_NOT_FOUND; unsupported OID
+// or certificate type => STATUS_NOT_SUPPORTED; malformed DER => format error.
+// Unlike the compatibility helper above, it never silently falls back to SHA-1.
+_IRQL_requires_max_(DISPATCH_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoGetPortableExecutableDigestKindEx(
+ _In_reads_bytes_(PeSize) const UCHAR* PeBytes,
+ _In_ ULONG PeSize,
+ _In_reads_(IMAGE_DIRECTORY_ENTRY_SECURITY + 1) const IMAGE_DATA_DIRECTORY* Directories,
+ _Out_ PUINT32 DigestCalg
+);
+
+// Algorithm-explicit Authenticode digest for the supported layout subset:
+// bounded PE32/PE32+ headers, contiguous nonoverlapping raw sections in file
+// offset order, optional overlay before a complete EOF certificate table.
+// SHA-1/256/384/512 supported. Gaps/overlaps/nonterminal certificates are rejected.
+// No signature trust decision is made. Outputs are cleared on failure; the
+// certificate borrows stable input memory, and only the digest must be freed.
+// See docs/authenticode-digests.md for policy and Windows SIP comparisons.
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
+DomitoCalculatePortableExecutableDigestEx(
+ _In_reads_bytes_(PeSize) const UCHAR* PeBytes,
+ _In_ ULONG PeSize,
+ _In_ UINT32 HashAlgId,
+ _Out_ PULONG DigestSize,
+ _Outptr_result_bytebuffer_(*DigestSize) PVOID* Digest,
+ _Outptr_result_maybenull_ LPWIN_CERTIFICATE* Certificate,
+ _Out_ PULONG SecuritySize
+);
+
+//
+// Calculates a PE digest using the existing linear hashing convention.
+// Validates accessed header/certificate ranges and clears outputs on failure
+// when all output pointers are valid. The input buffer must remain stable.
+// The returned certificate pointer borrows the input buffer; free only the digest
+// with DomitoFreePortableExecutableDigest. Algorithm detection is heuristic;
+// general section-ordered Authenticode hashing is not implemented here.
+//
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+__declspec(deprecated(
+ "Use DomitoCalculatePortableExecutableDigestEx with an explicit algorithm; review its supported PE layout and failure statuses."
+)) NTSTATUS
DomitoCalculatePortableExecutableDigest(
_In_ PUCHAR pPeBytes,
_In_ ULONG PeSize,
@@ -202,7 +241,7 @@ DomitoCalculatePortableExecutableDigest(
//
// Frees the memory allocated by DomitoCalculatePortableExecutableDigest.
-//
+//
_IRQL_requires_max_(DISPATCH_LEVEL)
EXTERN_C
void
@@ -210,14 +249,121 @@ DomitoFreePortableExecutableDigest(
_In_ PVOID pDigestOut
);
-//
-// Verifies if the Authenticode signature of a give PE file matches the provided (e.g. SHA1) file digest.
-//
+#if (NTDDI_VERSION >= NTDDI_WIN10)
+// Native forwards caller flags unchanged. The experimental x64 mode additionally
+// sets 0x80; it stops after the first accepted candidate in examined CI builds,
+// and also changes other policy/fallback paths. Neither mode selects a publisher
+// or enumerates signatures. No root-acceptance flag is added automatically.
+typedef enum _DOMITO_CI_SIGNATURE_SELECTION
+{
+ DomitoCiSignatureSelectionNative = 0,
+ DomitoCiSignatureSelectionFirstSuccessfulExperimental = 1
+} DOMITO_CI_SIGNATURE_SELECTION;
+
+// Zero-initialize before first use. Both policies own their CI chain allocations,
+// including possible diagnostic allocations on failure. Do not copy as an owner.
+// Hash is a file digest, not a certificate thumbprint. Hash/algorithm/signing time
+// are cleared on failure; policies may retain diagnostics. Check return NTSTATUS.
+typedef struct _DOMITO_CI_VALIDATION_RESULT
+{
+ MINCRYPT_POLICY_INFO PolicyInfo;
+ MINCRYPT_POLICY_INFO TimeStampPolicyInfo;
+ LARGE_INTEGER SigningTime;
+ UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH];
+ UINT32 HashSize;
+ ALG_ID HashAlgId;
+ UINT32 FileObjectValidationFlags; // Flags passed to CI export; zero for embedded helper.
+ UINT8 RequestedSigningLevel; // File-object helper only.
+} DOMITO_CI_VALIDATION_RESULT, *PDOMITO_CI_VALIDATION_RESULT;
+
+// Safe for NULL or an empty/initialized result; resets all fields after cleanup.
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+void
+DomitoFreeCiValidationResult(
+ _Inout_opt_ DOMITO_CI_VALIDATION_RESULT* Result
+);
+
+// Referenced FILE_OBJECT at PASSIVE_LEVEL; keep file bytes stable. Previous result
+// policies are released before reuse, including invalid-argument failures.
+// Supplies a fresh 64-byte hash buffer on every call. FirstSuccessfulExperimental
+// is opt-in and x64-only; other architectures return STATUS_NOT_SUPPORTED for it.
+// Only one selected signer plus its timestamp policy is returned. The caller
+// must enforce its expected certificate and intended trust policy separately.
+_Success_(return >= 0)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoValidateFileObjectEx(
+ _In_ PFILE_OBJECT FileObject,
+ _In_ UINT32 ValidationFlags,
+ _In_ UINT8 RequestedSigningLevel,
+ _In_ DOMITO_CI_SIGNATURE_SELECTION Selection,
+ _Inout_ DOMITO_CI_VALIDATION_RESULT* Result
+);
+
+// Hash and verify the first WIN_CERTIFICATE in a stable PE buffer. Uses the
+// explicit digest helper's supported contiguous-section/EOF-certificate subset.
+// SHA-1/256/384/512 are explicit; no algorithm retry or catalog fallback occurs.
+// CI may select a nested signature under its algorithm rules: this is not a
+// universal force-primary API. Matching SHA-256 selects the company primary in
+// the tested dual-signed BthPS3 fixture. Caller still checks the returned signer.
+_Success_(return >= 0)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoValidatePortableExecutableEmbeddedSignature(
+ _In_reads_bytes_(PeSize) const UCHAR* PeBytes,
+ _In_ ULONG PeSize,
+ _In_ ALG_ID HashAlgId,
+ _Inout_ DOMITO_CI_VALIDATION_RESULT* Result
+);
+#endif // NTDDI_VERSION >= NTDDI_WIN10
+
+#define DOMITO_CI_SOURCE_EMBEDDED 1UL
+#define DOMITO_CI_SOURCE_CATALOG 2UL
+
+// Policy-explicit legacy helper. SHA-1/256 only; hash size must match algorithm.
+// Policy objects must be zero-initialized or own valid CI results; both are freed
+// before each attempt. Caller owns both final policy outputs, including failure.
+// Embedded validation is tried first; only INVALID_IMAGE_HASH permits catalog
+// fallback/reload. AcceptedCatalogPolicyBits uses ANY-overlap catalog filtering;
+// RequireTrustedCatalog gates the observed marker, not publisher identity.
+// Source is 0 on failure, EMBEDDED/CATALOG on success. Stable file bytes and a
+// handle valid in the caller's current process context are required during I/O.
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
+DomitoValidateFileLegacyModeEx(
+ _In_ HANDLE FileHandle,
+ _In_reads_bytes_(HashSize) PVOID Hash,
+ _In_ UINT32 HashSize,
+ _In_ ALG_ID HashAlgId,
+ _In_ const IMAGE_DATA_DIRECTORY* SecurityDirectory,
+ _Inout_ MINCRYPT_POLICY_INFO* PolicyInfo,
+ _Out_ LARGE_INTEGER* SigningTime,
+ _Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
+ _In_ UINT32 AcceptedCatalogPolicyBits,
+ _In_ BOOLEAN RequireTrustedCatalog,
+ _Out_ PULONG ValidationSource
+);
+
+//
+// Compatibility wrapper around DomitoValidateFileLegacyModeEx, with catalog
+// mask DOMITO_CI_LEGACY_CATALOG_POLICY_BITS and RequireTrustedCatalog = FALSE.
+// The same initialization, ownership, hash, handle, and stable-file rules apply.
+//
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+__declspec(deprecated(
+ "Use DomitoValidateFileLegacyModeEx with explicit catalog policy and validation-source output; do not change trust policy implicitly."
+)) NTSTATUS
DomitoValidateFileLegacyMode(
_In_ HANDLE FileHandle,
_In_ PVOID Hash,
@@ -229,7 +375,6 @@ DomitoValidateFileLegacyMode(
_Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo
);
-
/* __ __ _
* | \/ (_)___ __
* | |\/| | (_- _|_
@@ -237,9 +382,9 @@ DomitoValidateFileLegacyMode(
*
*/
- //
- // Finds the base address of a driver module.
- //
+//
+// Finds the base address of a driver module. Clears the optional output first.
+//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -251,8 +396,11 @@ DomitoFindModuleBaseAddress(
);
//
-// Finds the address of an exported function by name.
-//
+// Finds an export by name (case-insensitive) in a valid loader-mapped module.
+// ModuleBase must remain mapped; arbitrary file buffers are not supported.
+// Export table spans are checked against SizeOfImage. Forwarded exports return
+// STATUS_NOT_SUPPORTED; the optional output is cleared before lookup.
+//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -266,7 +414,7 @@ DomitoFindExportedFunctionAddress(
//
// Scans a provided buffer for a memory pattern.
-//
+//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(DISPATCH_LEVEL)
@@ -282,8 +430,29 @@ DomitoMemorySearchPattern(
);
//
-// Reads from the beginning of a file until the end or the buffer size is reached.
-//
+// Reads exactly BufferSize bytes at a nonnegative absolute file offset.
+// Same completion, buffer, handle, APC, and short-read contract as DomitoReadFile.
+//
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+EXTERN_C
+NTSTATUS
+DomitoReadFileAt(
+ _In_ HANDLE FileHandle,
+ _Out_writes_bytes_(BufferSize) PVOID Buffer,
+ _In_ ULONG BufferSize,
+ _In_ const LARGE_INTEGER* ByteOffset
+);
+
+//
+// Reads exactly BufferSize bytes from file offset zero. Short reads return
+// STATUS_END_OF_FILE; Buffer may then contain a partial read. Zero length succeeds.
+// Waits for pending I/O on a private event before returning. Requires PASSIVE_LEVEL
+// with special kernel APCs enabled, a readable handle valid in the current process
+// (or a kernel handle), and a buffer that remains valid for the entire call.
+// For synchronous handles, the explicit read also updates the file position.
+//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -296,14 +465,23 @@ DomitoReadFile(
);
//
-// Gets the name of the main image of the process identified by PID.
-//
+// Gets the main image name after resolving the ULONG PID once to a process handle.
+// Requires DomitoInit. Output is NULL on failure; successful result is one Domito
+// allocation containing UNICODE_STRING and its buffer. Use DomitoFreeProcessImageName.
+// Do not change memory callbacks with outstanding allocations. PID reuse can race
+// the initial open; a later path reopen is not proof of original image identity.
+//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
-NTSTATUS
+__declspec(deprecated(
+ "Use DomitoGetProcessImageNameEx with a caller-referenced PEPROCESS to avoid a fresh PID lookup; free the result with DomitoFreeProcessImageName."
+)) NTSTATUS
DomitoGetProcessImageName(
_In_ ULONG ProcessId,
- _Inout_ PUNICODE_STRING* ProcessImageName
+ _Outptr_result_maybenull_ PUNICODE_STRING* ProcessImageName
);
+
+// Additive consumer helpers, included here for existing Domito.h clients.
+#include "Domito.Consumer.h"
diff --git a/src/Domito.Certificate.cpp b/src/Domito.Certificate.cpp
new file mode 100644
index 0000000..e8e692c
--- /dev/null
+++ b/src/Domito.Certificate.cpp
@@ -0,0 +1,396 @@
+#include "Domito.Internal.h"
+
+typedef struct _DOMITO_CERTIFICATE_DER_VIEW
+{
+ const UCHAR* Data;
+ ULONG Size;
+} DOMITO_CERTIFICATE_DER_VIEW;
+
+static BOOLEAN
+DomitoCertificateDerTake(
+ DOMITO_CERTIFICATE_DER_VIEW* Input,
+ UCHAR Tag,
+ DOMITO_CERTIFICATE_DER_VIEW* Content,
+ DOMITO_CERTIFICATE_DER_VIEW* Encoded = nullptr
+)
+{
+ if (Input->Size < 2 || Input->Data[0] != Tag)
+ {
+ return FALSE;
+ }
+
+ ULONG length = Input->Data[1];
+ ULONG header = 2;
+
+ if (length & 0x80)
+ {
+ const ULONG count = length & 0x7f;
+
+ if (!count || count > 4 || count > Input->Size - 2 || !Input->Data[2])
+ {
+ return FALSE;
+ }
+
+ length = 0;
+
+ for (ULONG i = 0; i < count; ++i)
+ {
+ length = (length << 8) | Input->Data[header++];
+ }
+
+ if (length < 128)
+ {
+ return FALSE;
+ }
+ }
+
+ if (length > Input->Size - header)
+ {
+ return FALSE;
+ }
+
+ *Content = {Input->Data + header, length};
+
+ if (Encoded)
+ {
+ *Encoded = {Input->Data, header + length};
+ }
+
+ Input->Data += header + length;
+ Input->Size -= header + length;
+ return TRUE;
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoGetCertificateSpki(
+ const UCHAR* CertData,
+ ULONG CertSize,
+ DOMITO_CERTIFICATE_SPKI* Spki
+)
+{
+ PAGED_CODE();
+
+ if (!Spki)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *Spki = {};
+
+ if (!CertData || !CertSize)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ DOMITO_CERTIFICATE_DER_VIEW certificateInput = {CertData, CertSize};
+ DOMITO_CERTIFICATE_DER_VIEW certificate;
+ DOMITO_CERTIFICATE_DER_VIEW tbsCertificate;
+ DOMITO_CERTIFICATE_DER_VIEW fieldContent;
+ DOMITO_CERTIFICATE_DER_VIEW encodedSpki;
+ DOMITO_CERTIFICATE_DER_VIEW spkiContent;
+ // Validate complete certificate framing, without interpreting trust/algorithms.
+ if (!DomitoCertificateDerTake(&certificateInput, 0x30, &certificate) || certificateInput.Size ||
+ !DomitoCertificateDerTake(&certificate, 0x30, &tbsCertificate) ||
+ !DomitoCertificateDerTake(&certificate, 0x30, &fieldContent) ||
+ !DomitoCertificateDerTake(&certificate, 0x03, &fieldContent) || !fieldContent.Size ||
+ fieldContent.Data[0] > 7 || certificate.Size)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ if (tbsCertificate.Size && tbsCertificate.Data[0] == 0xa0)
+ {
+ DOMITO_CERTIFICATE_DER_VIEW certificateVersion;
+
+ if (!DomitoCertificateDerTake(&tbsCertificate, 0xa0, &fieldContent) ||
+ !DomitoCertificateDerTake(&fieldContent, 0x02, &certificateVersion) || fieldContent.Size ||
+ certificateVersion.Size != 1 || certificateVersion.Data[0] > 2)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+ }
+
+ const UCHAR tbsFieldTags[] = {0x02, 0x30, 0x30, 0x30, 0x30};
+
+ for (ULONG i = 0; i < RTL_NUMBER_OF(tbsFieldTags); ++i)
+ {
+ if (!DomitoCertificateDerTake(&tbsCertificate, tbsFieldTags[i], &fieldContent) ||
+ (i == 0 && !fieldContent.Size))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+ }
+
+ if (!DomitoCertificateDerTake(&tbsCertificate, 0x30, &spkiContent, &encodedSpki))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ DOMITO_CERTIFICATE_DER_VIEW spkiFields = spkiContent;
+ DOMITO_CERTIFICATE_DER_VIEW keyAlgorithm;
+ DOMITO_CERTIFICATE_DER_VIEW algorithmOid;
+ DOMITO_CERTIFICATE_DER_VIEW publicKeyBits;
+
+ if (!DomitoCertificateDerTake(&spkiFields, 0x30, &keyAlgorithm) ||
+ !DomitoCertificateDerTake(&keyAlgorithm, 0x06, &algorithmOid) || !algorithmOid.Size ||
+ !DomitoCertificateDerTake(&spkiFields, 0x03, &publicKeyBits) || publicKeyBits.Size < 2 ||
+ publicKeyBits.Data[0] > 7 || spkiFields.Size)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ Spki->Encoded = {encodedSpki.Data, encodedSpki.Size};
+ Spki->Content = {spkiContent.Data, spkiContent.Size};
+ return STATUS_SUCCESS;
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoHashBuffer(
+ const UCHAR* Bytes,
+ ULONG Size,
+ ALG_ID Algorithm,
+ UCHAR* Hash,
+ ULONG* HashSize
+)
+{
+ PAGED_CODE();
+
+ if (!HashSize)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ const ULONG capacity = *HashSize;
+ ULONG required;
+
+ switch (Algorithm)
+ {
+ case CALG_SHA1:
+ required = 20;
+ break;
+ case CALG_SHA_256:
+ required = 32;
+ break;
+ case CALG_SHA_384:
+ required = 48;
+ break;
+ case CALG_SHA_512:
+ required = 64;
+ break;
+ default:
+ *HashSize = 0;
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ if (!Bytes && Size)
+ {
+ *HashSize = 0;
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (!Hash || capacity < required)
+ {
+ *HashSize = required;
+ return STATUS_BUFFER_TOO_SMALL;
+ }
+
+ RtlZeroMemory(Hash, required);
+ BCRYPT_ALG_HANDLE algorithm = nullptr;
+ BCRYPT_HASH_HANDLE hash = nullptr;
+ NTSTATUS status =
+ BCryptOpenAlgorithmProvider(&algorithm, DOMITO_CALG_TO_BCRYPT_ALGORITHM(Algorithm), MS_PRIMITIVE_PROVIDER, 0);
+
+ if (NT_SUCCESS(status))
+ {
+ status = BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0);
+ }
+
+ if (NT_SUCCESS(status) && Size)
+ {
+ status = BCryptHashData(hash, const_cast(Bytes), Size, 0);
+ }
+
+ if (NT_SUCCESS(status))
+ {
+ status = BCryptFinishHash(hash, Hash, required, 0);
+ }
+
+ if (hash)
+ {
+ BCryptDestroyHash(hash);
+ }
+
+ if (algorithm)
+ {
+ BCryptCloseAlgorithmProvider(algorithm, 0);
+ }
+
+ if (NT_SUCCESS(status))
+ {
+ *HashSize = required;
+ }
+ else
+ {
+ RtlZeroMemory(Hash, required);
+ *HashSize = 0;
+ }
+
+ return status;
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoHashCertificate(
+ const UCHAR* CertData,
+ ULONG CertSize,
+ DOMITO_CERTIFICATE_HASH_KIND Kind,
+ ALG_ID Algorithm,
+ UCHAR* Hash,
+ ULONG* HashSize
+)
+{
+ PAGED_CODE();
+
+ if (!HashSize)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (!CertData || !CertSize)
+ {
+ *HashSize = 0;
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ DOMITO_CERTIFICATE_SPKI spki = {};
+
+ if (Kind == DomitoCertificateHashSpkiDer || Kind == DomitoCertificateHashSpkiContentLegacy)
+ {
+ NTSTATUS status = DomitoGetCertificateSpki(CertData, CertSize, &spki);
+
+ if (!NT_SUCCESS(status))
+ {
+ *HashSize = 0;
+ return status;
+ }
+
+ const DOMITO_BYTE_VIEW selected = Kind == DomitoCertificateHashSpkiDer ? spki.Encoded : spki.Content;
+ return DomitoHashBuffer(selected.Data, selected.Size, Algorithm, Hash, HashSize);
+ }
+
+ if (Kind != DomitoCertificateHashDer)
+ {
+ *HashSize = 0;
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ // Whole-certificate fingerprint: hashes exact input bytes, no trust claim.
+ return DomitoHashBuffer(CertData, CertSize, Algorithm, Hash, HashSize);
+}
+
+static int
+DomitoHexDigit(
+ WCHAR Value
+)
+{
+ if (Value >= L'0' && Value <= L'9')
+ {
+ return Value - L'0';
+ }
+
+ if (Value >= L'A' && Value <= L'F')
+ {
+ return Value - L'A' + 10;
+ }
+
+ if (Value >= L'a' && Value <= L'f')
+ {
+ return Value - L'a' + 10;
+ }
+
+ return -1;
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoDecodeHex(
+ const UNICODE_STRING* Hex,
+ UCHAR* Bytes,
+ ULONG ByteCount
+)
+{
+ PAGED_CODE();
+
+ if (!Bytes && ByteCount)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (Bytes && ByteCount)
+ {
+ RtlZeroMemory(Bytes, ByteCount);
+ }
+
+ if (!Hex || Hex->Length > Hex->MaximumLength || (Hex->Length && !Hex->Buffer) ||
+ ByteCount > MAXUSHORT / (2 * sizeof(WCHAR)) || Hex->Length != ByteCount * 2 * sizeof(WCHAR))
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ for (ULONG i = 0; i < ByteCount * 2; ++i)
+ {
+ if (DomitoHexDigit(Hex->Buffer[i]) < 0)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+ }
+
+ for (ULONG i = 0; i < ByteCount; ++i)
+ {
+ Bytes[i] =
+ static_cast((DomitoHexDigit(Hex->Buffer[2 * i]) << 4) | DomitoHexDigit(Hex->Buffer[2 * i + 1]));
+ }
+
+ return STATUS_SUCCESS;
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoEncodeHex(
+ const UCHAR* Bytes,
+ ULONG ByteCount,
+ WCHAR* Hex,
+ ULONG HexChars
+)
+{
+ PAGED_CODE();
+
+ if (Hex && HexChars)
+ {
+ Hex[0] = 0;
+ }
+
+ if ((!Bytes && ByteCount) || !Hex)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (!HexChars || ByteCount > (HexChars - 1) / 2)
+ {
+ return STATUS_BUFFER_TOO_SMALL;
+ }
+
+ static const WCHAR digits[] = L"0123456789ABCDEF";
+ WCHAR* destination = Hex;
+
+ for (ULONG i = 0; i < ByteCount; ++i)
+ {
+ *destination++ = digits[Bytes[i] >> 4];
+ *destination++ = digits[Bytes[i] & 15];
+ }
+
+ *destination = 0;
+ return STATUS_SUCCESS;
+}
diff --git a/src/Domito.CodeIntegrity.Validation.cpp b/src/Domito.CodeIntegrity.Validation.cpp
new file mode 100644
index 0000000..aeee18c
--- /dev/null
+++ b/src/Domito.CodeIntegrity.Validation.cpp
@@ -0,0 +1,216 @@
+#include "Domito.Internal.h"
+
+#if (NTDDI_VERSION >= NTDDI_WIN10)
+
+static UINT32
+DomitoCiDigestSize(
+ ALG_ID Algorithm
+)
+{
+ switch (Algorithm)
+ {
+ case CALG_SHA1:
+ return MINCRYPT_SHA1_LENGTH;
+ case CALG_SHA_256:
+ return MINCRYPT_SHA256_LENGTH;
+ case CALG_SHA_384:
+ return MINCRYPT_SHA384_LENGTH;
+ case CALG_SHA_512:
+ return MINCRYPT_SHA512_LENGTH;
+ default:
+ return 0;
+ }
+}
+
+static void
+DomitoClearCiDigest(
+ DOMITO_CI_VALIDATION_RESULT* Result
+)
+{
+ RtlZeroMemory(Result->Hash, sizeof(Result->Hash));
+ Result->HashSize = 0;
+ Result->HashAlgId = 0;
+ Result->SigningTime.QuadPart = 0;
+}
+
+_Use_decl_annotations_
+void
+DomitoFreeCiValidationResult(
+ DOMITO_CI_VALIDATION_RESULT* Result
+)
+{
+ PAGED_CODE();
+
+ if (!Result)
+ {
+ return;
+ }
+
+ CiFreePolicyInfo(&Result->PolicyInfo);
+ CiFreePolicyInfo(&Result->TimeStampPolicyInfo);
+ RtlZeroMemory(Result, sizeof(*Result));
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoValidateFileObjectEx(
+ PFILE_OBJECT FileObject,
+ UINT32 ValidationFlags,
+ UINT8 RequestedSigningLevel,
+ DOMITO_CI_SIGNATURE_SELECTION Selection,
+ DOMITO_CI_VALIDATION_RESULT* Result
+)
+{
+ PAGED_CODE();
+
+ if (!Result)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ DomitoFreeCiValidationResult(Result);
+
+ if (!FileObject)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (Selection != DomitoCiSignatureSelectionNative &&
+ Selection != DomitoCiSignatureSelectionFirstSuccessfulExperimental)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (Selection == DomitoCiSignatureSelectionFirstSuccessfulExperimental)
+ {
+#if defined(_M_AMD64) || defined(_AMD64_)
+ ValidationFlags |= DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL;
+#else
+ return STATUS_NOT_SUPPORTED;
+#endif
+ }
+
+ Result->FileObjectValidationFlags = ValidationFlags;
+ Result->RequestedSigningLevel = RequestedSigningLevel;
+ Result->HashSize = sizeof(Result->Hash);
+ NTSTATUS status = CiValidateFileObject(
+ FileObject,
+ ValidationFlags,
+ RequestedSigningLevel,
+ &Result->PolicyInfo,
+ &Result->TimeStampPolicyInfo,
+ &Result->SigningTime,
+ Result->Hash,
+ &Result->HashSize,
+ &Result->HashAlgId
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ const UINT32 expected = DomitoCiDigestSize(Result->HashAlgId);
+
+ if (!expected)
+ {
+ status = STATUS_NOT_SUPPORTED;
+ }
+ else if (Result->HashSize != expected || Result->HashSize > sizeof(Result->Hash))
+ {
+ status = STATUS_INVALID_BUFFER_SIZE;
+ }
+ }
+
+ if (!NT_SUCCESS(status))
+ {
+ DomitoClearCiDigest(Result);
+ }
+ else
+ {
+ RtlZeroMemory(Result->Hash + Result->HashSize, sizeof(Result->Hash) - Result->HashSize);
+ }
+
+ return status;
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoValidatePortableExecutableEmbeddedSignature(
+ const UCHAR* PeBytes,
+ ULONG PeSize,
+ ALG_ID HashAlgId,
+ DOMITO_CI_VALIDATION_RESULT* Result
+)
+{
+ PAGED_CODE();
+
+ if (!Result)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ DomitoFreeCiValidationResult(Result);
+
+ if (!PeBytes)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ const UINT32 expected = DomitoCiDigestSize(HashAlgId);
+
+ if (!expected)
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ PVOID digest = nullptr;
+ ULONG digestSize = 0;
+ ULONG securitySize = 0;
+ LPWIN_CERTIFICATE certificate = nullptr;
+ NTSTATUS status = DomitoCalculatePortableExecutableDigestEx(
+ PeBytes, PeSize, HashAlgId, &digestSize, &digest, &certificate, &securitySize
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ if (!certificate || !securitySize)
+ {
+ status = STATUS_INVALID_IMAGE_HASH;
+ }
+ else if (!digest || digestSize != expected || digestSize > sizeof(Result->Hash))
+ {
+ status = STATUS_INVALID_BUFFER_SIZE;
+ }
+ else
+ {
+ status = CiCheckSignedFile(
+ digest,
+ digestSize,
+ HashAlgId,
+ certificate,
+ securitySize,
+ &Result->PolicyInfo,
+ &Result->SigningTime,
+ &Result->TimeStampPolicyInfo
+ );
+
+ if (NT_SUCCESS(status))
+ {
+ RtlCopyMemory(Result->Hash, digest, digestSize);
+ Result->HashSize = digestSize;
+ Result->HashAlgId = HashAlgId;
+ }
+ }
+ }
+
+ if (digest)
+ {
+ DomitoFreePortableExecutableDigest(digest);
+ }
+
+ if (!NT_SUCCESS(status))
+ {
+ DomitoClearCiDigest(Result);
+ }
+
+ return status;
+}
+#endif // NTDDI_VERSION >= NTDDI_WIN10
diff --git a/src/Domito.CodeIntegrity.cpp b/src/Domito.CodeIntegrity.cpp
index 8a22b33..56ee962 100644
--- a/src/Domito.CodeIntegrity.cpp
+++ b/src/Domito.CodeIntegrity.cpp
@@ -8,7 +8,6 @@
#include "Domito.Internal.h"
#include "Domito.MinCrypt.h"
-
/* _ _ _ _ _ _ _ ___ _ _
* | | | | |_(_) (_) |_ _ _ | __| _ _ _ __| |_(_)___ _ _ ___
* | |_| | _| | | | _| || | | _| || | ' \/ _| _| / _ \ ' \(_-<
@@ -24,43 +23,53 @@ DomitoGetPortableExecutableDigestKind(
_In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory
)
{
- const ULONG securityRva = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress;
-
- if (!securityRva)
+ if (!pPeBytes || !pImgDataDirectory)
{
return CALG_SHA1;
}
- if (securityRva > PeSize || (PeSize - securityRva) < sizeof(WIN_CERTIFICATE))
+ const IMAGE_DATA_DIRECTORY security = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY];
+ const ULONG securityRva = security.VirtualAddress;
+ const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate);
+
+ if (!securityRva || securityRva > PeSize || security.Size > PeSize - securityRva ||
+ security.Size < certificateHeaderSize)
+ {
+ return CALG_SHA1;
+ }
+
+ ULONG certificateSize;
+ memcpy(&certificateSize, pPeBytes + securityRva, sizeof(certificateSize));
+
+ if (certificateSize < certificateHeaderSize || certificateSize > security.Size)
{
return CALG_SHA1;
}
const PVOID pBase = pPeBytes + securityRva;
- const LPWIN_CERTIFICATE pCert = (WIN_CERTIFICATE*)pBase;
- const ULONG searchLen = min(pCert->dwLength, PeSize - securityRva);
+ const ULONG searchLen = certificateSize;
PUCHAR pMatch = NULL;
if (NT_SUCCESS(DomitoMemorySearchPattern(
- (const PUCHAR)"\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14",
- 0x00,
- 15,
- pBase,
- searchLen,
- (PVOID*)&pMatch
- )))
+ (const PUCHAR) "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14",
+ 0x00,
+ 15,
+ pBase,
+ searchLen,
+ (PVOID*)&pMatch
+ )))
{
return CALG_SHA1;
}
if (NT_SUCCESS(DomitoMemorySearchPattern(
- (const PUCHAR)"\x30\xcc\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\xcc\x05\x00\x04\xcc",
- 0xcc,
- 19,
- pBase,
- searchLen,
- (PVOID*)&pMatch
- )))
+ (const PUCHAR) "\x30\xcc\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\xcc\x05\x00\x04\xcc",
+ 0xcc,
+ 19,
+ pBase,
+ searchLen,
+ (PVOID*)&pMatch
+ )))
{
if (pMatch == NULL)
{
@@ -84,308 +93,672 @@ DomitoGetPortableExecutableDigestKind(
return CALG_SHA1;
}
+// Bounded DER navigation for the outer Authenticode content only. This does not
+// validate certificates, signerInfos, nested signatures, or cryptographic trust.
+typedef struct _DOMITO_DER_VIEW
+{
+ const UCHAR* Bytes;
+ ULONG Length;
+} DOMITO_DER_VIEW;
+
+static BOOLEAN
+DomitoDerTake(
+ DOMITO_DER_VIEW* Input,
+ UCHAR Tag,
+ DOMITO_DER_VIEW* Value
+)
+{
+ if (Input->Length < 2 || Input->Bytes[0] != Tag)
+ {
+ return FALSE;
+ }
+
+ ULONG length = Input->Bytes[1];
+ ULONG header = 2;
+
+ if (length & 0x80)
+ {
+ const ULONG count = length & 0x7F;
+
+ if (!count || count > sizeof(ULONG) || count > Input->Length - header || Input->Bytes[header] == 0)
+ {
+ return FALSE; // No indefinite or nonminimal DER lengths.
+ }
+
+ length = 0;
+
+ for (ULONG i = 0; i < count; ++i)
+ {
+ length = (length << 8) | Input->Bytes[header++];
+ }
+
+ if (length < 0x80)
+ {
+ return FALSE;
+ }
+ }
+
+ if (length > Input->Length - header)
+ {
+ return FALSE;
+ }
+
+ Value->Bytes = Input->Bytes + header;
+ Value->Length = length;
+ Input->Bytes += header + length;
+ Input->Length -= header + length;
+ return TRUE;
+}
+
+static BOOLEAN
+DomitoDerOid(
+ DOMITO_DER_VIEW* Input,
+ const UCHAR* Oid,
+ ULONG Size
+)
+{
+ DOMITO_DER_VIEW value;
+ return DomitoDerTake(Input, 0x06, &value) && value.Length == Size && memcmp(value.Bytes, Oid, Size) == 0;
+}
+
+_IRQL_requires_max_(DISPATCH_LEVEL)
+NTSTATUS
+DomitoGetPortableExecutableDigestKindEx(
+ _In_reads_bytes_(PeSize) const UCHAR* PeBytes,
+ _In_ ULONG PeSize,
+ _In_reads_(IMAGE_DIRECTORY_ENTRY_SECURITY + 1) const IMAGE_DATA_DIRECTORY* Directories,
+ _Out_ PUINT32 DigestCalg
+)
+{
+ if (!DigestCalg)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *DigestCalg = 0;
+
+ if (!PeBytes || !Directories)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ const IMAGE_DATA_DIRECTORY security = Directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
+
+ if (!security.VirtualAddress && !security.Size)
+ {
+ return STATUS_NOT_FOUND;
+ }
+
+ if (!security.VirtualAddress || (security.VirtualAddress & 7) || security.VirtualAddress > PeSize ||
+ security.Size > PeSize - security.VirtualAddress || security.Size < 8)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG length;
+ USHORT revision;
+ USHORT type;
+ const UCHAR* certificate = PeBytes + security.VirtualAddress;
+ memcpy(&length, certificate, 4);
+ memcpy(&revision, certificate + 4, 2);
+ memcpy(&type, certificate + 6, 2);
+
+ if (length <= 8 || length > security.Size)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA)
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ const UCHAR signedDataOid[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02};
+ const UCHAR indirectOid[] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x04};
+ const UCHAR sha1Oid[] = {0x2B, 0x0E, 0x03, 0x02, 0x1A};
+ const UCHAR sha2Prefix[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02};
+ DOMITO_DER_VIEW payload = {certificate + 8, length - 8};
+ DOMITO_DER_VIEW content;
+ DOMITO_DER_VIEW wrapped;
+ DOMITO_DER_VIEW signedData;
+ DOMITO_DER_VIEW ignored;
+ DOMITO_DER_VIEW contentInfo;
+ DOMITO_DER_VIEW indirect;
+ DOMITO_DER_VIEW digest;
+ DOMITO_DER_VIEW algorithm;
+ DOMITO_DER_VIEW oid;
+
+ if (!DomitoDerTake(&payload, 0x30, &content) || !DomitoDerOid(&content, signedDataOid, sizeof(signedDataOid)) ||
+ !DomitoDerTake(&content, 0xA0, &wrapped) || content.Length || !DomitoDerTake(&wrapped, 0x30, &signedData) ||
+ wrapped.Length || !DomitoDerTake(&signedData, 0x02, &ignored) || !DomitoDerTake(&signedData, 0x31, &ignored) ||
+ !DomitoDerTake(&signedData, 0x30, &contentInfo) ||
+ !DomitoDerOid(&contentInfo, indirectOid, sizeof(indirectOid)) || !DomitoDerTake(&contentInfo, 0xA0, &wrapped) ||
+ contentInfo.Length || !DomitoDerTake(&wrapped, 0x30, &indirect) || wrapped.Length ||
+ !DomitoDerTake(&indirect, 0x30, &ignored) || !DomitoDerTake(&indirect, 0x30, &digest) || indirect.Length ||
+ !DomitoDerTake(&digest, 0x30, &algorithm) || !DomitoDerTake(&algorithm, 0x06, &oid))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ // Windows-signed entries can include up to seven zero alignment bytes in
+ // dwLength itself. Accept only that bounded, aligned padding after the DER.
+ if (payload.Length > 7 || (payload.Length && (length & 7)))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ for (ULONG i = 0; i < payload.Length; ++i)
+ {
+ if (payload.Bytes[i])
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+ }
+
+ if (algorithm.Length && (!DomitoDerTake(&algorithm, 0x05, &ignored) || ignored.Length || algorithm.Length))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ UINT32 calg = 0;
+ ULONG expectedLength = 0;
+
+ if (oid.Length == sizeof(sha1Oid) && !memcmp(oid.Bytes, sha1Oid, sizeof(sha1Oid)))
+ {
+ calg = CALG_SHA1;
+ expectedLength = 20;
+ }
+ else if (oid.Length == sizeof(sha2Prefix) + 1 && !memcmp(oid.Bytes, sha2Prefix, sizeof(sha2Prefix)))
+ {
+ switch (oid.Bytes[sizeof(sha2Prefix)])
+ {
+ case 1:
+ calg = CALG_SHA256;
+ expectedLength = 32;
+ break;
+ case 2:
+ calg = CALG_SHA384;
+ expectedLength = 48;
+ break;
+ case 3:
+ calg = CALG_SHA512;
+ expectedLength = 64;
+ break;
+ }
+ }
+
+ if (!calg)
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ if (!DomitoDerTake(&digest, 0x04, &ignored) || digest.Length || ignored.Length != expectedLength)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ *DigestCalg = calg;
+ return STATUS_SUCCESS;
+}
+
+// Conservative section-layout boundary: contiguous raw sections (any header
+// order), optional overlay before an EOF certificate table. Gaps, overlaps, and
+// certificates in the middle are rejected rather than assigned an ambiguous hash.
+static NTSTATUS
+DomitoValidateAuthenticodeLayout(
+ const UCHAR* Bytes,
+ ULONG Size
+)
+{
+ if (!Bytes || Size < sizeof(IMAGE_DOS_HEADER))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ IMAGE_DOS_HEADER dos;
+ memcpy(&dos, Bytes, sizeof(dos));
+
+ if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < 0)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ const ULONG nt = (ULONG)dos.e_lfanew;
+ const ULONG prefix = sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER);
+
+ if (nt > Size || prefix > Size - nt)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG signature;
+ IMAGE_FILE_HEADER file;
+ memcpy(&signature, Bytes + nt, sizeof(signature));
+ memcpy(&file, Bytes + nt + sizeof(signature), sizeof(file));
+
+ if (signature != IMAGE_NT_SIGNATURE || !file.NumberOfSections || file.NumberOfSections > 96)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ const ULONG optional = nt + prefix;
+
+ if (file.SizeOfOptionalHeader < 2 || file.SizeOfOptionalHeader > Size - optional)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ USHORT magic;
+ memcpy(&magic, Bytes + optional, sizeof(magic));
+ ULONG directory;
+
+ if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC)
+ {
+ directory = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory);
+ }
+ else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
+ {
+ directory = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory);
+ }
+ else
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ if (file.SizeOfOptionalHeader < directory)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG count;
+ ULONG headers;
+ memcpy(&count, Bytes + optional + directory - 4, sizeof(count));
+ memcpy(&headers, Bytes + optional + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfHeaders), sizeof(headers));
+
+ if (count > (file.SizeOfOptionalHeader - directory) / sizeof(IMAGE_DATA_DIRECTORY))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ if (count <= IMAGE_DIRECTORY_ENTRY_SECURITY)
+ {
+ return STATUS_NOT_SUPPORTED; // SIP exclusion behavior differs for absent slots.
+ }
+
+ const ULONG table = optional + file.SizeOfOptionalHeader;
+ const ULONG tableSize = file.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);
+
+ if (tableSize > Size - table || headers < table + tableSize || headers > Size)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ struct
+ {
+ ULONG Offset;
+ ULONG Length;
+ } sections[96];
+
+ ULONG used = 0;
+
+ for (ULONG i = 0; i < file.NumberOfSections; ++i)
+ {
+ IMAGE_SECTION_HEADER section;
+ memcpy(§ion, Bytes + table + i * sizeof(section), sizeof(section));
+
+ if (!section.SizeOfRawData)
+ {
+ continue;
+ }
+
+ if (section.PointerToRawData < headers || section.PointerToRawData > Size ||
+ section.SizeOfRawData > Size - section.PointerToRawData)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG at = used;
+ while (at && sections[at - 1].Offset > section.PointerToRawData)
+ {
+ sections[at] = sections[at - 1];
+ --at;
+ }
+
+ sections[at].Offset = section.PointerToRawData;
+ sections[at].Length = section.SizeOfRawData;
+ ++used;
+ }
+
+ ULONG end = headers;
+
+ for (ULONG i = 0; i < used; ++i)
+ {
+ if (sections[i].Offset != end)
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ end += sections[i].Length;
+ }
+
+ if (count > IMAGE_DIRECTORY_ENTRY_SECURITY)
+ {
+ IMAGE_DATA_DIRECTORY security;
+ memcpy(
+ &security,
+ Bytes + optional + directory + IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(security),
+ sizeof(security)
+ );
+
+ if (security.VirtualAddress || security.Size)
+ {
+ if (!security.VirtualAddress || !security.Size || (security.VirtualAddress & 7) ||
+ security.VirtualAddress < end || security.VirtualAddress > Size ||
+ security.Size != Size - security.VirtualAddress)
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ // Validate every aligned certificate entry, not just the first.
+ ULONG offset = security.VirtualAddress;
+ while (offset < Size)
+ {
+ if (Size - offset < 8)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG length;
+ USHORT revision;
+ USHORT type;
+ memcpy(&length, Bytes + offset, 4);
+ memcpy(&revision, Bytes + offset + 4, 2);
+ memcpy(&type, Bytes + offset + 6, 2);
+
+ if (length <= 8 || length > Size - offset)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA)
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ const ULONG padding = (8 - (length & 7)) & 7;
+
+ if (padding > Size - offset - length)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ offset += length + padding;
+ }
+ }
+ }
+
+ return STATUS_SUCCESS;
+}
+
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
-NTSTATUS
-DomitoCalculatePortableExecutableDigest(
+static NTSTATUS
+DomitoCalculatePortableExecutableDigestInternal(
+ _In_ UINT32 ExplicitCalg,
_In_ PUCHAR pPeBytes,
_In_ ULONG PeSize,
_Out_ PUINT32 pDigestCalgOut,
_Out_ PULONG pDigestSizeOut,
- _Out_ PVOID * pDigestOut,
- _Outptr_result_maybenull_ LPWIN_CERTIFICATE * pCertOut,
+ _Out_ PVOID* pDigestOut,
+ _Outptr_result_maybenull_ LPWIN_CERTIFICATE* pCertOut,
_Out_ PULONG pSizeOfSecurityDirectory
)
{
- PIMAGE_DATA_DIRECTORY pImgDataDirectory;
- BOOLEAN is64Bit;
- PUCHAR pHash = nullptr;
- NTSTATUS status;
- BCRYPT_ALG_HANDLE hbAlg = nullptr;
- BCRYPT_HASH_HANDLE hbHash = nullptr;
- UINT32 hashLength = 0;
- ULONG resultLength;
- ULONG fileOffset = 0;
+ if (!pDigestCalgOut || !pDigestSizeOut || !pDigestOut || !pCertOut || !pSizeOfSecurityDirectory)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *pDigestCalgOut = 0;
+ *pDigestSizeOut = 0;
+ *pDigestOut = nullptr;
+ *pCertOut = nullptr;
+ *pSizeOfSecurityDirectory = 0;
+
+ if (!pPeBytes)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
if (PeSize < sizeof(IMAGE_DOS_HEADER))
{
return STATUS_INVALID_IMAGE_NOT_MZ;
}
- const PIMAGE_DOS_HEADER phDos = (IMAGE_DOS_HEADER*)pPeBytes;
- if (phDos->e_magic != IMAGE_DOS_SIGNATURE)
+ // Copy fields rather than assuming the caller's buffer is aligned.
+ IMAGE_DOS_HEADER dos = {};
+ memcpy(&dos, pPeBytes, sizeof(dos));
+
+ if (dos.e_magic != IMAGE_DOS_SIGNATURE)
{
- // Not an executable
return STATUS_INVALID_IMAGE_NOT_MZ;
}
- if (phDos->e_lfanew > PeSize - sizeof(IMAGE_NT_HEADERS))
+ if (dos.e_lfanew < 0)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
- const PIMAGE_NT_HEADERS phNt = (PIMAGE_NT_HEADERS)(pPeBytes + phDos->e_lfanew);
- if (phNt->Signature != IMAGE_NT_SIGNATURE)
+ const ULONG ntOffset = (ULONG)dos.e_lfanew;
+ const ULONG ntPrefixSize = sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER);
+
+ if (ntOffset > PeSize || ntPrefixSize > PeSize - ntOffset)
{
- // Not a PE image
return STATUS_INVALID_IMAGE_FORMAT;
}
- //
- // Check if PE is 32 or 64 bits
- //
- switch (phNt->OptionalHeader.Magic)
+ ULONG signature;
+ IMAGE_FILE_HEADER fileHeader;
+ memcpy(&signature, pPeBytes + ntOffset, sizeof(signature));
+ memcpy(&fileHeader, pPeBytes + ntOffset + sizeof(signature), sizeof(fileHeader));
+
+ if (signature != IMAGE_NT_SIGNATURE)
{
- case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
- is64Bit = FALSE;
- break;
- case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
- is64Bit = TRUE;
- break;
- default:
- return STATUS_INVALID_IMAGE_FORMAT; // Unsupported architecture
+ return STATUS_INVALID_IMAGE_FORMAT;
}
- //
- // TODO: Not sure if 16 * 512 * 512 is right. Do something better!
- //
- ULONG copySize = phDos->e_lfanew + sizeof(IMAGE_FILE_HEADER) + 4 + 0x40;
-#pragma warning(disable:4996)
- const PUCHAR pBuf = (PUCHAR)ExAllocatePoolWithTag(
- NonPagedPoolNx,
- 16 * 512 * 512,
- DOMITO_POOL_TAG
- );
-#pragma warning(default:4996)
- if (!pBuf)
+ const ULONG optionalOffset = ntOffset + ntPrefixSize;
+ const ULONG optionalSize = fileHeader.SizeOfOptionalHeader;
+
+ if (optionalSize < sizeof(USHORT) || optionalSize > PeSize - optionalOffset)
{
- return STATUS_INSUFFICIENT_RESOURCES;
+ return STATUS_INVALID_IMAGE_FORMAT;
}
- //
- // Fetch the data directory from the PE.
- // Note that we can also use RtlImageDirectoryEntryToData to fetch
- // only the security directory entry but why not do it manually
- // when we have the full PE image anyway
- //
- if (is64Bit == TRUE)
+ USHORT magic;
+ memcpy(&magic, pPeBytes + optionalOffset, sizeof(magic));
+ ULONG directoryOffset;
+ ULONG checksumOffset;
+ ULONG directoryCountOffset;
+
+ if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC)
{
- pImgDataDirectory = ((PIMAGE_NT_HEADERS64)phNt)->OptionalHeader.DataDirectory;
+ directoryOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory);
+ checksumOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, CheckSum);
+ directoryCountOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, NumberOfRvaAndSizes);
+ }
+ else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
+ {
+ directoryOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory);
+ checksumOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, CheckSum);
+ directoryCountOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, NumberOfRvaAndSizes);
}
else
{
- pImgDataDirectory = ((PIMAGE_NT_HEADERS32)phNt)->OptionalHeader.DataDirectory;
+ return STATUS_INVALID_IMAGE_FORMAT;
}
- //
- // Check if the PE file contains a signature
- //
- const BOOLEAN hasEmbeddedSig = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress != 0;
+ if (optionalSize < directoryOffset)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
- //
- // Boring bcrypt boilerplate code
- //
- const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, pImgDataDirectory);
+ ULONG directoryCount;
+ memcpy(&directoryCount, pPeBytes + optionalOffset + directoryCountOffset, sizeof(directoryCount));
- if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider(
- &hbAlg,
- DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), // bcrypt doesn't understand standard WinCrypt CALG IDs
- MS_PRIMITIVE_PROVIDER,
- 0
- )))
+ if (directoryCount > (optionalSize - directoryOffset) / sizeof(IMAGE_DATA_DIRECTORY))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ checksumOffset += optionalOffset;
+ const BOOLEAN hasSecurityEntry = directoryCount > IMAGE_DIRECTORY_ENTRY_SECURITY;
+ IMAGE_DATA_DIRECTORY directories[IMAGE_DIRECTORY_ENTRY_SECURITY + 1] = {};
+ ULONG securityEntryOffset = PeSize;
+ ULONG dataOffset = PeSize;
+
+ if (hasSecurityEntry)
+ {
+ securityEntryOffset =
+ optionalOffset + directoryOffset + IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(IMAGE_DATA_DIRECTORY);
+ memcpy(
+ &directories[IMAGE_DIRECTORY_ENTRY_SECURITY], pPeBytes + securityEntryOffset, sizeof(IMAGE_DATA_DIRECTORY)
+ );
+ dataOffset = securityEntryOffset + sizeof(IMAGE_DATA_DIRECTORY);
+ }
+
+ const IMAGE_DATA_DIRECTORY security = directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
+ const BOOLEAN hasEmbeddedSig = security.VirtualAddress != 0;
+
+ if (hasEmbeddedSig != (security.Size != 0))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ if (hasEmbeddedSig)
+ {
+ // The security directory contains a FILE OFFSET, not an image RVA.
+ const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate);
+
+ if (security.VirtualAddress < dataOffset || security.VirtualAddress > PeSize ||
+ security.Size > PeSize - security.VirtualAddress || security.Size < certificateHeaderSize)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG certificateSize;
+ memcpy(&certificateSize, pPeBytes + security.VirtualAddress, sizeof(certificateSize));
+
+ if (certificateSize < certificateHeaderSize || certificateSize > security.Size)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+ }
+
+ PUCHAR pHash = nullptr;
+ BCRYPT_ALG_HANDLE hbAlg = nullptr;
+ BCRYPT_HASH_HANDLE hbHash = nullptr;
+ ULONG hashLength = 0;
+ ULONG resultLength = 0;
+ // Intentional compatibility branch; the explicit API never calls the heuristic.
+#pragma warning(push)
+#pragma warning(disable : 4996)
+ const UINT32 peDigestKind =
+ ExplicitCalg ? ExplicitCalg : DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories);
+#pragma warning(pop)
+ NTSTATUS status =
+ BCryptOpenAlgorithmProvider(&hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0);
+
+ if (!NT_SUCCESS(status))
{
goto cleanup;
}
- if (!NT_SUCCESS(status = BCryptGetProperty(
- hbAlg,
- BCRYPT_HASH_LENGTH,
- (PUCHAR)&hashLength, // put the length of the hash into hashLength
- sizeof(hashLength),
- &resultLength,
- 0
- )))
+ status = BCryptGetProperty(hbAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength, sizeof(hashLength), &resultLength, 0);
+
+ if (!NT_SUCCESS(status))
{
goto cleanup;
}
- //
- // Allocate a buffer to store the resulting hash
- //
-#pragma warning(disable:4996)
+#pragma warning(disable : 4996)
pHash = (PUCHAR)ExAllocatePoolWithTag(NonPagedPoolNx, hashLength, DOMITO_POOL_TAG);
-#pragma warning(default:4996)
+#pragma warning(default : 4996)
if (!pHash)
{
status = STATUS_INSUFFICIENT_RESOURCES;
goto cleanup;
}
- RtlZeroMemory(pHash, hashLength);
- //
- // Create a handle to the resulting hash
- //
- if (!NT_SUCCESS(status = BCryptCreateHash(
- hbAlg,
- &hbHash,
- NULL,
- 0,
- NULL,
- 0,
- 0
- )))
- {
- goto cleanup;
- }
+ status = BCryptCreateHash(hbAlg, &hbHash, nullptr, 0, nullptr, 0, 0);
- //
- // Ok, if we haven't BSODed yet, we're ready to continue
- // parsing the PE/COFF and hash the needed values
- //
- memcpy(pBuf, pPeBytes, copySize);
- fileOffset += copySize;
-
- //
- // Hash everything up to the Checksum then skip it
- //
- status = BCryptHashData(
- hbHash,
- pBuf,
- copySize,
- 0
- );
if (!NT_SUCCESS(status))
{
goto cleanup;
}
- fileOffset += 4; // Skipping the checksum field here
+ // Keep the existing linear hashing convention, but hash validated ranges
+ // directly: no fixed-size scratch buffer and no unchecked memcpy lengths.
+ // General section-ordered Authenticode hashing is a separate change.
+ status = BCryptHashData(hbHash, pPeBytes, checksumOffset, 0);
- //
- // Reach the security directory information.
- // For x64 PEs it's 10 bytes further
- //
- copySize = 0x3C;
- if (is64Bit == TRUE)
- {
- copySize += 0x10;
- }
-
- memcpy(pBuf, pPeBytes + fileOffset, copySize);
- fileOffset += copySize;
-
- //
- // Again, hash everything up to here then skip the ignored field.
- //
- status = BCryptHashData(
- hbHash,
- pBuf,
- copySize,
- 0
- );
if (!NT_SUCCESS(status))
{
goto cleanup;
}
- fileOffset += 8; // Skipping an other ignored field here
+ status = BCryptHashData(
+ hbHash, pPeBytes + checksumOffset + sizeof(ULONG), securityEntryOffset - checksumOffset - sizeof(ULONG), 0
+ );
- //
- // Now hash everything else in the file up to the certificate data.
- //
- ULONG remaining;
- if (hasEmbeddedSig)
+ if (!NT_SUCCESS(status))
{
- const ULONG secRva = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress;
- if (secRva < fileOffset)
- {
- status = STATUS_INVALID_IMAGE_FORMAT;
- goto cleanup;
- }
- remaining = secRva - fileOffset;
+ goto cleanup;
}
- else
- {
- remaining = (fileOffset <= PeSize) ? (PeSize - fileOffset) : 0;
- }
- while (remaining > 0)
+ if (hasSecurityEntry)
{
- const ULONG chunkSize = min(remaining, 4096);
- ULONG readBytes;
- memset(pBuf, 0, chunkSize);
-
- if (fileOffset + chunkSize > PeSize)
- {
- readBytes = PeSize - chunkSize;
- memcpy(pBuf, pPeBytes + fileOffset, readBytes);
- fileOffset += readBytes;
- break;
- }
- else
- {
- memcpy(pBuf, pPeBytes + fileOffset, chunkSize);
- readBytes = chunkSize;
- fileOffset += readBytes;
- }
+ status = BCryptHashData(
+ hbHash, pPeBytes + dataOffset, (hasEmbeddedSig ? security.VirtualAddress : PeSize) - dataOffset, 0
+ );
- if (!NT_SUCCESS(status = BCryptHashData(
- hbHash,
- pBuf,
- readBytes,
- 0
- )))
+ if (!NT_SUCCESS(status))
{
goto cleanup;
}
+ }
- remaining -= readBytes;
- }
+ status = BCryptFinishHash(hbHash, pHash, hashLength, 0);
- //
- // Finish up the hash here
- //
- if (!NT_SUCCESS(status = BCryptFinishHash(
- hbHash,
- pHash,
- hashLength,
- 0
- )))
+ if (!NT_SUCCESS(status))
{
goto cleanup;
}
- //
- // Supply the results to the caller
- //
*pDigestCalgOut = peDigestKind;
*pDigestSizeOut = hashLength;
*pDigestOut = pHash;
- if (hasEmbeddedSig == TRUE)
- {
- *pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress);
- *pSizeOfSecurityDirectory = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size;
- }
- else
- {
- *pCertOut = NULL;
- *pSizeOfSecurityDirectory = 0;
- }
+ pHash = nullptr; // Transfer ownership only after all work has succeeded.
- status = STATUS_SUCCESS;
+ if (hasEmbeddedSig)
+ {
+ *pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + security.VirtualAddress);
+ *pSizeOfSecurityDirectory = security.Size;
+ }
cleanup:
- if (pBuf)
- {
- ExFreePoolWithTag(pBuf, DOMITO_POOL_TAG);
- }
if (hbHash)
{
BCryptDestroyHash(hbHash);
}
+ if (pHash)
+ {
+ ExFreePoolWithTag(pHash, DOMITO_POOL_TAG);
+ }
+
if (hbAlg)
{
BCryptCloseAlgorithmProvider(hbAlg, 0);
@@ -394,6 +767,74 @@ cleanup:
return status;
}
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+NTSTATUS
+DomitoCalculatePortableExecutableDigest(
+ PUCHAR PeBytes,
+ ULONG PeSize,
+ PUINT32 DigestCalg,
+ PULONG DigestSize,
+ PVOID* Digest,
+ LPWIN_CERTIFICATE* Certificate,
+ PULONG SecuritySize
+)
+{
+ return DomitoCalculatePortableExecutableDigestInternal(
+ 0, PeBytes, PeSize, DigestCalg, DigestSize, Digest, Certificate, SecuritySize
+ );
+}
+
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+NTSTATUS
+DomitoCalculatePortableExecutableDigestEx(
+ const UCHAR* PeBytes,
+ ULONG PeSize,
+ UINT32 HashAlgId,
+ PULONG DigestSize,
+ PVOID* Digest,
+ LPWIN_CERTIFICATE* Certificate,
+ PULONG SecuritySize
+)
+{
+ if (!DigestSize || !Digest || !Certificate || !SecuritySize)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *DigestSize = 0;
+ *Digest = nullptr;
+ *Certificate = nullptr;
+ *SecuritySize = 0;
+
+ if (!PeBytes)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (HashAlgId != CALG_SHA1 && HashAlgId != CALG_SHA256 && HashAlgId != CALG_SHA384 && HashAlgId != CALG_SHA512)
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ NTSTATUS status = DomitoValidateAuthenticodeLayout(PeBytes, PeSize);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ UINT32 actualCalg = 0;
+ // Contiguous sorted sections make the validated section sequence identical
+ // to linear byte order; overlay precedes an EOF certificate table.
+ return DomitoCalculatePortableExecutableDigestInternal(
+ HashAlgId, (PUCHAR)PeBytes, PeSize, &actualCalg, DigestSize, Digest, Certificate, SecuritySize
+ );
+}
+
_IRQL_requires_max_(DISPATCH_LEVEL)
void
DomitoFreePortableExecutableDigest(
@@ -412,20 +853,36 @@ _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
#pragma code_seg("PAGED")
-NTSTATUS
-DomitoValidateFileLegacyMode(
+static NTSTATUS
+DomitoValidateFileLegacyModeInternal(
_In_ HANDLE FileHandle,
_In_ PVOID Hash,
_In_ UINT32 HashSize,
_In_ ALG_ID HashAlgId,
- _In_ const IMAGE_DATA_DIRECTORY * SecurityDirectory,
- _Inout_ MINCRYPT_POLICY_INFO * PolicyInfo,
- _Out_ LARGE_INTEGER * SigningTime,
- _Inout_ MINCRYPT_POLICY_INFO * TimeStampPolicyInfo
+ _In_ const IMAGE_DATA_DIRECTORY* SecurityDirectory,
+ _Inout_ MINCRYPT_POLICY_INFO* PolicyInfo,
+ _Out_ LARGE_INTEGER* SigningTime,
+ _Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
+ _In_ UINT32 AcceptedCatalogPolicyBits,
+ _In_ BOOLEAN RequireTrustedCatalog,
+ _Out_ PULONG ValidationSource
)
{
PAGED_CODE();
+ if (!ValidationSource)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *ValidationSource = 0;
+
+ if (!Hash || !SecurityDirectory || !PolicyInfo || !SigningTime || !TimeStampPolicyInfo ||
+ PolicyInfo == TimeStampPolicyInfo)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
NTSTATUS status = STATUS_SUCCESS;
PVOID certDirectory = nullptr;
KAPC_STATE systemContext = {};
@@ -437,22 +894,61 @@ DomitoValidateFileLegacyMode(
CiFreePolicyInfo(PolicyInfo);
CiFreePolicyInfo(TimeStampPolicyInfo);
- if (HashSize != MINCRYPT_SHA1_LENGTH && HashSize != MINCRYPT_SHA256_LENGTH)
+ if (HashAlgId != CALG_SHA1 && HashAlgId != CALG_SHA256)
{
- status = STATUS_INVALID_IMAGE_HASH;
+ status = STATUS_NOT_SUPPORTED;
break;
}
- if (SecurityDirectory->Size != 0u &&
- SecurityDirectory->VirtualAddress != 0u)
+ if (HashSize != (UINT32)(HashAlgId == CALG_SHA1 ? MINCRYPT_SHA1_LENGTH : MINCRYPT_SHA256_LENGTH))
{
-#pragma warning(disable:4996)
- certDirectory = ExAllocatePoolWithTag(
- PagedPool,
- SecurityDirectory->Size,
- DOMITO_POOL_TAG
+ status = STATUS_INVALID_PARAMETER;
+ break;
+ }
+
+ if ((SecurityDirectory->Size != 0) != (SecurityDirectory->VirtualAddress != 0))
+ {
+ status = STATUS_INVALID_IMAGE_FORMAT;
+ break;
+ }
+
+ if (SecurityDirectory->Size != 0u && SecurityDirectory->VirtualAddress != 0u)
+ {
+ if ((SecurityDirectory->VirtualAddress & 7) || SecurityDirectory->Size < 12)
+ {
+ status = STATUS_INVALID_IMAGE_FORMAT;
+ break;
+ }
+
+ // Bound the allocation/read against the file before trusting its table.
+ PFILE_OBJECT fileObject = nullptr;
+ status = ObReferenceObjectByHandle(
+ FileHandle, FILE_READ_DATA, *IoFileObjectType, KernelMode, (PVOID*)&fileObject, nullptr
);
-#pragma warning(default:4996)
+
+ if (!NT_SUCCESS(status))
+ {
+ break;
+ }
+
+ LARGE_INTEGER fileSize = {};
+ status = FsRtlGetFileSize(fileObject, &fileSize);
+ ObDereferenceObject(fileObject);
+
+ if (!NT_SUCCESS(status))
+ {
+ break;
+ }
+
+ if (fileSize.QuadPart < 0 || (ULONGLONG)SecurityDirectory->VirtualAddress > (ULONGLONG)fileSize.QuadPart ||
+ SecurityDirectory->Size > (ULONGLONG)fileSize.QuadPart - SecurityDirectory->VirtualAddress)
+ {
+ status = STATUS_INVALID_IMAGE_FORMAT;
+ break;
+ }
+#pragma warning(disable : 4996)
+ certDirectory = ExAllocatePoolWithTag(PagedPool, SecurityDirectory->Size, DOMITO_POOL_TAG);
+#pragma warning(default : 4996)
if (certDirectory == NULL)
{
status = STATUS_INSUFFICIENT_RESOURCES;
@@ -460,33 +956,8 @@ DomitoValidateFileLegacyMode(
}
LARGE_INTEGER offset = {};
- IO_STATUS_BLOCK ioStatusBlock = {};
-
- offset.LowPart = SecurityDirectory->VirtualAddress;
-
- status = ZwReadFile(
- FileHandle,
- NULL,
- NULL,
- NULL,
- &ioStatusBlock,
- certDirectory,
- SecurityDirectory->Size,
- &offset,
- NULL
- );
-
- if (status == STATUS_PENDING)
- {
- ZwWaitForSingleObject(
- FileHandle,
- FALSE,
- NULL
- );
-
- MemoryBarrier();
- status = ioStatusBlock.Status;
- }
+ offset.QuadPart = SecurityDirectory->VirtualAddress;
+ status = DomitoReadFileAt(FileHandle, certDirectory, SecurityDirectory->Size, &offset);
if (!NT_SUCCESS(status))
{
@@ -506,10 +977,12 @@ DomitoValidateFileLegacyMode(
TimeStampPolicyInfo
);
}
+
KeUnstackDetachProcess(&systemContext);
if (NT_SUCCESS(status))
{
+ *ValidationSource = DOMITO_CI_SOURCE_EMBEDDED;
break;
}
@@ -519,6 +992,9 @@ DomitoValidateFileLegacyMode(
}
}
+ CiFreePolicyInfo(PolicyInfo);
+ CiFreePolicyInfo(TimeStampPolicyInfo);
+ SigningTime->QuadPart = 0;
KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
{
status = CiVerifyHashInCatalog(
@@ -526,8 +1002,8 @@ DomitoValidateFileLegacyMode(
HashSize,
HashAlgId,
FALSE,
- 0,
- 0x2007F,
+ RequireTrustedCatalog,
+ AcceptedCatalogPolicyBits,
PolicyInfo,
NULL,
SigningTime,
@@ -536,13 +1012,16 @@ DomitoValidateFileLegacyMode(
if (status == STATUS_INVALID_IMAGE_HASH)
{
+ CiFreePolicyInfo(PolicyInfo);
+ CiFreePolicyInfo(TimeStampPolicyInfo);
+ SigningTime->QuadPart = 0;
status = CiVerifyHashInCatalog(
Hash,
HashSize,
HashAlgId,
TRUE,
- 0,
- 0x2007F,
+ RequireTrustedCatalog,
+ AcceptedCatalogPolicyBits,
PolicyInfo,
NULL,
SigningTime,
@@ -550,8 +1029,14 @@ DomitoValidateFileLegacyMode(
);
}
}
+
KeUnstackDetachProcess(&systemContext);
+ if (NT_SUCCESS(status))
+ {
+ *ValidationSource = DOMITO_CI_SOURCE_CATALOG;
+ }
+
} while (FALSE);
if (certDirectory)
@@ -561,4 +1046,69 @@ DomitoValidateFileLegacyMode(
return status;
}
+
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+NTSTATUS
+DomitoValidateFileLegacyMode(
+ HANDLE FileHandle,
+ PVOID Hash,
+ UINT32 HashSize,
+ ALG_ID HashAlgId,
+ const IMAGE_DATA_DIRECTORY* SecurityDirectory,
+ MINCRYPT_POLICY_INFO* PolicyInfo,
+ LARGE_INTEGER* SigningTime,
+ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo
+)
+{
+ ULONG source;
+ return DomitoValidateFileLegacyModeInternal(
+ FileHandle,
+ Hash,
+ HashSize,
+ HashAlgId,
+ SecurityDirectory,
+ PolicyInfo,
+ SigningTime,
+ TimeStampPolicyInfo,
+ DOMITO_CI_LEGACY_CATALOG_POLICY_BITS,
+ FALSE,
+ &source
+ );
+}
+
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+NTSTATUS
+DomitoValidateFileLegacyModeEx(
+ HANDLE FileHandle,
+ PVOID Hash,
+ UINT32 HashSize,
+ ALG_ID HashAlgId,
+ const IMAGE_DATA_DIRECTORY* SecurityDirectory,
+ MINCRYPT_POLICY_INFO* PolicyInfo,
+ LARGE_INTEGER* SigningTime,
+ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
+ UINT32 AcceptedCatalogPolicyBits,
+ BOOLEAN RequireTrustedCatalog,
+ PULONG ValidationSource
+)
+{
+ return DomitoValidateFileLegacyModeInternal(
+ FileHandle,
+ Hash,
+ HashSize,
+ HashAlgId,
+ SecurityDirectory,
+ PolicyInfo,
+ SigningTime,
+ TimeStampPolicyInfo,
+ AcceptedCatalogPolicyBits,
+ RequireTrustedCatalog,
+ ValidationSource
+ );
+}
+
#pragma code_seg()
diff --git a/src/Domito.Consumer.cpp b/src/Domito.Consumer.cpp
new file mode 100644
index 0000000..c14404b
--- /dev/null
+++ b/src/Domito.Consumer.cpp
@@ -0,0 +1,395 @@
+#include "Domito.Internal.h"
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoFindModuleExportedFunctionAddress(
+ PANSI_STRING ModuleName,
+ PANSI_STRING FunctionName,
+ PVOID* FunctionAddress
+)
+{
+ PAGED_CODE();
+
+ if (!FunctionAddress)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *FunctionAddress = nullptr;
+
+ if (!ModuleName || !ModuleName->Buffer || !ModuleName->Length || ModuleName->Length > ModuleName->MaximumLength ||
+ !FunctionName || !FunctionName->Buffer || !FunctionName->Length ||
+ FunctionName->Length > FunctionName->MaximumLength)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ PVOID moduleBase = nullptr;
+ NTSTATUS status = DomitoFindModuleBaseAddress(ModuleName, &moduleBase);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ return DomitoFindExportedFunctionAddress(moduleBase, FunctionName, FunctionAddress);
+}
+
+_Use_decl_annotations_
+void
+DomitoFreeProcessImageName(
+ PUNICODE_STRING ImageName
+)
+{
+ PAGED_CODE();
+
+ if (ImageName)
+ {
+ G_Memory.Free(ImageName);
+ }
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoGetProcessImageNameEx(
+ PEPROCESS Process,
+ PUNICODE_STRING* ImageName
+)
+{
+ PAGED_CODE();
+
+ if (!ImageName)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *ImageName = nullptr;
+
+ if (!Process)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ PUNICODE_STRING processImageName = nullptr;
+ NTSTATUS status = SeLocateProcessImageName(Process, &processImageName);
+
+ if (NT_SUCCESS(status))
+ {
+ if (!processImageName || !processImageName->Buffer || !processImageName->Length ||
+ (processImageName->Length & 1) || processImageName->Length > processImageName->MaximumLength ||
+ processImageName->Length > MAXUSHORT - sizeof(WCHAR))
+ {
+ status = STATUS_INVALID_BUFFER_SIZE;
+ }
+ else
+ {
+ const SIZE_T size = sizeof(UNICODE_STRING) + processImageName->Length + sizeof(WCHAR);
+ PUNICODE_STRING imageName = static_cast(G_Memory.Allocate(size));
+
+ if (!imageName)
+ {
+ status = STATUS_INSUFFICIENT_RESOURCES;
+ }
+ else
+ {
+ imageName->Buffer = reinterpret_cast(imageName + 1);
+ imageName->Length = processImageName->Length;
+ imageName->MaximumLength = processImageName->Length + sizeof(WCHAR);
+ RtlCopyMemory(imageName->Buffer, processImageName->Buffer, processImageName->Length);
+ imageName->Buffer[imageName->Length / sizeof(WCHAR)] = 0;
+ *ImageName = imageName;
+ }
+ }
+ }
+
+ if (processImageName)
+ {
+ ExFreePool(processImageName);
+ }
+
+ return status;
+}
+
+#if (NTDDI_VERSION >= NTDDI_WIN10)
+_Use_decl_annotations_
+NTSTATUS
+DomitoValidateFileHandleEx(
+ HANDLE FileHandle,
+ UINT32 ValidationFlags,
+ UINT8 RequestedSigningLevel,
+ DOMITO_CI_SIGNATURE_SELECTION Selection,
+ DOMITO_CI_VALIDATION_RESULT* Result
+)
+{
+ PAGED_CODE();
+
+ if (!Result)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ DomitoFreeCiValidationResult(Result);
+
+ if (!FileHandle)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ PFILE_OBJECT fileObject = nullptr;
+ NTSTATUS status = ObReferenceObjectByHandle(
+ FileHandle, FILE_READ_DATA, *IoFileObjectType, KernelMode, reinterpret_cast(&fileObject), nullptr
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = DomitoValidateFileObjectEx(fileObject, ValidationFlags, RequestedSigningLevel, Selection, Result);
+ ObDereferenceObject(fileObject);
+ return status;
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoValidateFilePathEx(
+ const UNICODE_STRING* Path,
+ UINT32 ValidationFlags,
+ UINT8 RequestedSigningLevel,
+ DOMITO_CI_SIGNATURE_SELECTION Selection,
+ DOMITO_CI_VALIDATION_RESULT* Result
+)
+{
+ PAGED_CODE();
+
+ if (!Result)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ DomitoFreeCiValidationResult(Result);
+
+ if (!Path || !Path->Buffer || Path->Length < sizeof(WCHAR) || (Path->Length & 1) ||
+ Path->Length > Path->MaximumLength || Path->Buffer[0] != L'\\')
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ for (USHORT i = 0; i < Path->Length / sizeof(WCHAR); ++i)
+ {
+ if (!Path->Buffer[i])
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+ }
+
+ OBJECT_ATTRIBUTES attributes;
+ InitializeObjectAttributes(
+ &attributes, const_cast(Path), OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, nullptr, nullptr
+ );
+ HANDLE fileHandle = nullptr;
+ IO_STATUS_BLOCK iosb = {};
+ NTSTATUS status = ZwOpenFile(
+ &fileHandle,
+ FILE_READ_DATA | SYNCHRONIZE,
+ &attributes,
+ &iosb,
+ FILE_SHARE_READ,
+ FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT
+ );
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ status = DomitoValidateFileHandleEx(fileHandle, ValidationFlags, RequestedSigningLevel, Selection, Result);
+ ZwClose(fileHandle);
+ return status;
+}
+
+#if defined(_M_AMD64) || defined(_AMD64_)
+static BOOLEAN
+DomitoCiOwnedSpan(
+ const MINCRYPT_CHAIN_INFO* Chain,
+ const void* Data,
+ SIZE_T Size
+)
+{
+ const ULONG_PTR chainBaseAddress = reinterpret_cast(Chain);
+ const ULONG_PTR blobAddress = reinterpret_cast(Data);
+ // Empty blobs may be NULL; they are never dereferenced by this accessor.
+ if (!Size)
+ {
+ return TRUE;
+ }
+
+ return blobAddress >= chainBaseAddress && blobAddress - chainBaseAddress >= 56 &&
+ blobAddress - chainBaseAddress <= Chain->Size && Size <= Chain->Size - (blobAddress - chainBaseAddress);
+}
+#endif
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoGetCiChainView(
+ const MINCRYPT_POLICY_INFO* Policy,
+ DOMITO_CI_CHAIN_LAYOUT Layout,
+ DOMITO_CI_CHAIN_VIEW* View
+)
+{
+ PAGED_CODE();
+
+ if (!View)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *View = {};
+
+ if (!Policy || Layout != DomitoCiChainLayoutObservedX64)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+#if defined(_M_AMD64) || defined(_AMD64_)
+ C_ASSERT(sizeof(MINCRYPT_CHAIN_ELEMENT) == 120);
+ C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, ChainElements) == 40);
+ C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, NumberOfChainElement) == 48);
+
+ if (Policy->Size != sizeof(*Policy))
+ {
+ return STATUS_NOT_SUPPORTED;
+ }
+
+ const MINCRYPT_CHAIN_INFO* chain = Policy->ChainInfo;
+
+ if (!chain)
+ {
+ return STATUS_NOT_FOUND;
+ }
+
+ if (chain->Size < 56)
+ {
+ return STATUS_INVALID_BUFFER_SIZE;
+ }
+
+ const ULONG count = chain->NumberOfChainElement;
+
+ if (!count)
+ {
+ return STATUS_NOT_FOUND;
+ }
+
+ if (count > (chain->Size - 56) / sizeof(MINCRYPT_CHAIN_ELEMENT) ||
+ !DomitoCiOwnedSpan(chain, chain->ChainElements, static_cast(count) * sizeof(MINCRYPT_CHAIN_ELEMENT)))
+ {
+ return STATUS_INVALID_BUFFER_SIZE;
+ }
+
+ for (ULONG i = 0; i < count; ++i)
+ {
+ const MINCRYPT_CHAIN_ELEMENT& element = chain->ChainElements[i];
+
+ if (element.HashSize > sizeof(element.Hash) || !element.Certificate.Size ||
+ !DomitoCiOwnedSpan(chain, element.Certificate.Data, element.Certificate.Size) ||
+ !DomitoCiOwnedSpan(chain, element.Subject.Data, element.Subject.Size) ||
+ !DomitoCiOwnedSpan(chain, element.Issuer.Data, element.Issuer.Size))
+ {
+ return STATUS_INVALID_BUFFER_SIZE;
+ }
+ }
+
+ ULONG rootId;
+ RtlCopyMemory(&rootId, reinterpret_cast(chain) + 52, sizeof(rootId));
+ *View = {chain->ChainElements, count, rootId};
+ return STATUS_SUCCESS;
+#else
+ return STATUS_NOT_SUPPORTED;
+#endif
+}
+
+static MINCRYPT_ALLOCATE_ROUTINE DomitoAllocatePublisher;
+
+static PVOID NTAPI
+DomitoAllocatePublisher(
+ SIZE_T Size
+)
+{
+#pragma warning(suppress : 4996)
+ return ExAllocatePoolWithTag(PagedPool, Size, DOMITO_POOL_TAG);
+}
+
+_Use_decl_annotations_
+void
+DomitoFreeCertificatePublisherName(
+ PUNICODE_STRING PublisherName
+)
+{
+ PAGED_CODE();
+
+ if (!PublisherName)
+ {
+ return;
+ }
+
+ if (PublisherName->Buffer)
+ {
+ ExFreePoolWithTag(PublisherName->Buffer, DOMITO_POOL_TAG);
+ }
+
+ *PublisherName = {};
+}
+
+_Use_decl_annotations_
+NTSTATUS
+DomitoGetCertificatePublisherName(
+ const MINCERT_BLOB* Certificate,
+ PUNICODE_STRING PublisherName
+)
+{
+ PAGED_CODE();
+
+ if (!PublisherName)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ *PublisherName = {};
+
+ if (!Certificate || !Certificate->Data || !Certificate->Size)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ ANSI_STRING moduleName = RTL_CONSTANT_STRING("\\SystemRoot\\system32\\CI.dll");
+ ANSI_STRING exportName = RTL_CONSTANT_STRING("CiGetCertPublisherName");
+ PVOID publisherRoutine = nullptr;
+ NTSTATUS status = DomitoFindModuleExportedFunctionAddress(&moduleName, &exportName, &publisherRoutine);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ if (!publisherRoutine)
+ {
+ return STATUS_PROCEDURE_NOT_FOUND;
+ }
+
+ typedef NTSTATUS(NTAPI * GET_PUBLISHER)(MINCERT_BLOB*, PMINCRYPT_ALLOCATE_ROUTINE, PUNICODE_STRING);
+ const GET_PUBLISHER getPublisher = reinterpret_cast(publisherRoutine);
+ status = getPublisher(const_cast(Certificate), DomitoAllocatePublisher, PublisherName);
+
+ if (NT_SUCCESS(status) && (PublisherName->Length > PublisherName->MaximumLength || (PublisherName->Length & 1) ||
+ !PublisherName->Length || !PublisherName->Buffer))
+ {
+ status = STATUS_INVALID_BUFFER_SIZE;
+ }
+
+ if (!NT_SUCCESS(status))
+ {
+ DomitoFreeCertificatePublisherName(PublisherName);
+ }
+
+ return status;
+}
+#endif
diff --git a/src/Domito.cpp b/src/Domito.cpp
index 41c9dab..4753fdb 100644
--- a/src/Domito.cpp
+++ b/src/Domito.cpp
@@ -7,18 +7,26 @@
#include "Domito.Internal.h"
-
DOMITO_COMMON G_Common = {};
-DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QipRoutineName, L"ZwQueryInformationProcess");
-DECLARE_GLOBAL_CONST_UNICODE_STRING(G_IdetdRoutineName, L"RtlImageDirectoryEntryToData");
-DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QsiRoutineName, L"ZwQuerySystemInformation");
+DECLARE_GLOBAL_CONST_UNICODE_STRING(
+ G_QipRoutineName,
+ L"ZwQueryInformationProcess"
+);
+DECLARE_GLOBAL_CONST_UNICODE_STRING(
+ G_IdetdRoutineName,
+ L"RtlImageDirectoryEntryToData"
+);
+DECLARE_GLOBAL_CONST_UNICODE_STRING(
+ G_QsiRoutineName,
+ L"ZwQuerySystemInformation"
+);
#ifndef LOG
-#define LOG(Format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[Domito][" __FUNCTION__ "] " Format " \n", __VA_ARGS__)
+#define LOG(Format, ...) \
+ DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[Domito][" __FUNCTION__ "] " Format " \n", __VA_ARGS__)
#endif
-
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -27,7 +35,7 @@ DomitoInit()
{
//
// Do those first since the follow-up code depends on them
- //
+ //
G_Common.ZwQueryInformationProcess =
(t_ZwQueryInformationProcess)MmGetSystemRoutineAddress((PUNICODE_STRING)&G_QipRoutineName);
@@ -43,19 +51,27 @@ _IRQL_requires_max_(PASSIVE_LEVEL)
void
DomitoShutdown()
{
-
}
-
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
NTSTATUS
DomitoFindModuleBaseAddress(
_In_ PANSI_STRING ModuleName,
- _Inout_opt_ PVOID * ModuleBase
+ _Inout_opt_ PVOID* ModuleBase
)
{
+ if (ModuleBase)
+ {
+ *ModuleBase = nullptr;
+ }
+
+ if (!ModuleName || !ModuleName->Buffer || !ModuleName->Length || ModuleName->Length > ModuleName->MaximumLength)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
ULONG bufferSize = 0;
PSYSTEM_MODULE_INFORMATION moduleInfo = NULL;
@@ -67,12 +83,7 @@ DomitoFindModuleBaseAddress(
const ULONG SystemModuleInformation = 11;
// Query the required buffer size for module information
- NTSTATUS status = G_Common.ZwQuerySystemInformation(
- SystemModuleInformation,
- &bufferSize,
- 0,
- &bufferSize
- );
+ NTSTATUS status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, &bufferSize, 0, &bufferSize);
if (status != STATUS_INFO_LENGTH_MISMATCH)
{
@@ -80,9 +91,7 @@ DomitoFindModuleBaseAddress(
}
// Allocate memory for the module information
- moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate(
- bufferSize
- );
+ moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate(bufferSize);
if (moduleInfo == NULL)
{
@@ -90,12 +99,7 @@ DomitoFindModuleBaseAddress(
}
// Retrieve the module information
- status = G_Common.ZwQuerySystemInformation(
- SystemModuleInformation,
- moduleInfo,
- bufferSize,
- NULL
- );
+ status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, moduleInfo, bufferSize, NULL);
if (!NT_SUCCESS(status))
{
@@ -120,6 +124,7 @@ DomitoFindModuleBaseAddress(
{
*ModuleBase = moduleInfo->Module[i].Base;
}
+
break;
}
}
@@ -129,6 +134,19 @@ DomitoFindModuleBaseAddress(
return status;
}
+// This helper handles directory RVAs after the loader-mapped header establishes
+// SizeOfImage. ModuleBase is not an arbitrary file buffer (there is no size input).
+static BOOLEAN
+DomitoImageRvaSpan(
+ ULONG ImageSize,
+ ULONG Rva,
+ ULONG Count,
+ ULONG ElementSize
+)
+{
+ return Rva <= ImageSize && ElementSize != 0 && Count <= (ImageSize - Rva) / ElementSize;
+}
+
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -136,59 +154,146 @@ NTSTATUS
DomitoFindExportedFunctionAddress(
_In_ PVOID ModuleBase,
_In_ PANSI_STRING FunctionName,
- _Inout_opt_ PVOID * FunctionAddress
+ _Inout_opt_ PVOID* FunctionAddress
)
{
- NTSTATUS status = STATUS_NOT_FOUND;
- ULONG exportSize;
+ if (FunctionAddress)
+ {
+ *FunctionAddress = nullptr;
+ }
- if (G_Common.RtlImageDirectoryEntryToData == NULL)
+ if (!ModuleBase || !FunctionName || !FunctionName->Buffer || !FunctionName->Length)
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (!G_Common.RtlImageDirectoryEntryToData)
{
return STATUS_NOT_IMPLEMENTED;
}
- // Retrieve the export directory information
- const PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)G_Common.RtlImageDirectoryEntryToData(
- ModuleBase,
- TRUE,
- IMAGE_DIRECTORY_ENTRY_EXPORT,
- &exportSize
- );
+ // Initial headers belong to a valid loaded module, as required by this API.
+ const PUCHAR base = (PUCHAR)ModuleBase;
+ IMAGE_DOS_HEADER dos;
+ memcpy(&dos, base, sizeof(dos));
- if (exportDirectory == NULL)
+ if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < (LONG)sizeof(dos))
{
return STATUS_INVALID_IMAGE_FORMAT;
}
- STRING currentFunctionName;
+ const PUCHAR nt = base + dos.e_lfanew;
+ ULONG signature;
+ memcpy(&signature, nt, sizeof(signature));
+ USHORT magic;
+ memcpy(&magic, nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER), sizeof(magic));
- const PULONG functionAddresses = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfFunctions);
- const PULONG functionNames = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNames);
- const PUSHORT functionOrdinals = (PUSHORT)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNameOrdinals);
-
- for (ULONG i = 0; i < exportDirectory->NumberOfNames; i++)
+ if (signature != IMAGE_NT_SIGNATURE ||
+ (magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC && magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC))
{
- const char* functionName = (const char*)((ULONG_PTR)ModuleBase + functionNames[i]);
- const USHORT functionOrdinal = functionOrdinals[i];
- UNREFERENCED_PARAMETER(functionOrdinal);
-
- const ULONG functionRva = functionAddresses[i];
- const PVOID functionAddress = (PVOID)((ULONG_PTR)ModuleBase + functionRva);
-
- RtlInitAnsiString(¤tFunctionName, functionName);
-
- if (0 == RtlCompareString(FunctionName, ¤tFunctionName, TRUE))
- {
- if (FunctionAddress)
- {
- status = STATUS_SUCCESS;
- *FunctionAddress = functionAddress;
- }
- break;
- }
+ return STATUS_INVALID_IMAGE_FORMAT;
}
- return status;
+ // SizeOfImage is at the same offset in PE32 and PE32+ optional headers.
+ ULONG imageSize;
+ memcpy(
+ &imageSize,
+ nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage),
+ sizeof(imageSize)
+ );
+
+ if (!DomitoImageRvaSpan(
+ imageSize,
+ (ULONG)dos.e_lfanew,
+ sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage) +
+ sizeof(imageSize),
+ 1
+ ))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG exportSize = 0;
+ const PVOID directory =
+ G_Common.RtlImageDirectoryEntryToData(ModuleBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize);
+ const ULONG_PTR directoryOffset = (ULONG_PTR)directory - (ULONG_PTR)ModuleBase;
+
+ if (!directory || directoryOffset > imageSize || exportSize < sizeof(IMAGE_EXPORT_DIRECTORY) ||
+ exportSize > imageSize - directoryOffset)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ IMAGE_EXPORT_DIRECTORY exports;
+ memcpy(&exports, directory, sizeof(exports));
+
+ if (!DomitoImageRvaSpan(imageSize, exports.AddressOfFunctions, exports.NumberOfFunctions, sizeof(ULONG)) ||
+ !DomitoImageRvaSpan(imageSize, exports.AddressOfNames, exports.NumberOfNames, sizeof(ULONG)) ||
+ !DomitoImageRvaSpan(imageSize, exports.AddressOfNameOrdinals, exports.NumberOfNames, sizeof(USHORT)))
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ for (ULONG i = 0; i < exports.NumberOfNames; ++i)
+ {
+ ULONG nameRva;
+ USHORT ordinal;
+ memcpy(&nameRva, base + exports.AddressOfNames + i * sizeof(ULONG), sizeof(nameRva));
+ memcpy(&ordinal, base + exports.AddressOfNameOrdinals + i * sizeof(USHORT), sizeof(ordinal));
+
+ if (nameRva >= imageSize || ordinal >= exports.NumberOfFunctions)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ULONG length = 0;
+ while (length < imageSize - nameRva && base[nameRva + length] != 0)
+ {
+ ++length;
+ }
+
+ if (length == imageSize - nameRva || length > MAXUSHORT)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ ANSI_STRING current;
+ current.Buffer = (PCHAR)(base + nameRva);
+ current.Length = (USHORT)length;
+ current.MaximumLength = (USHORT)length;
+
+ if (RtlCompareString(FunctionName, ¤t, TRUE) != 0)
+ {
+ continue;
+ }
+
+ ULONG functionRva;
+ memcpy(&functionRva, base + exports.AddressOfFunctions + ordinal * sizeof(ULONG), sizeof(functionRva));
+
+ if (!functionRva)
+ {
+ return STATUS_NOT_FOUND; // Empty EAT slot.
+ }
+
+ if (functionRva >= imageSize)
+ {
+ return STATUS_INVALID_IMAGE_FORMAT;
+ }
+
+ if (functionRva >= directoryOffset && functionRva - directoryOffset < exportSize)
+ {
+ return STATUS_NOT_SUPPORTED; // Forwarder string, not a callable address.
+ }
+
+ if (FunctionAddress)
+ {
+ *FunctionAddress = base + functionRva;
+ }
+
+ return STATUS_SUCCESS;
+ }
+
+ return STATUS_NOT_FOUND;
}
_Success_(return == STATUS_SUCCESS)
@@ -201,7 +306,7 @@ DomitoMemorySearchPattern(
_In_ SIZE_T puLen,
_In_ PVOID pcBase,
_In_ SIZE_T puSize,
- _Outptr_result_maybenull_ PVOID * ppMatch
+ _Outptr_result_maybenull_ PVOID* ppMatch
)
{
ASSERT(ppMatch != NULL && pcPattern != NULL && pcBase != NULL);
@@ -221,6 +326,7 @@ DomitoMemorySearchPattern(
for (SIZE_T i = 0; i + puLen <= puSize; i++)
{
BOOLEAN found = TRUE;
+
for (SIZE_T j = 0; j < puLen; j++)
{
if (pcPattern[j] != uWildcard && pcPattern[j] != ((PCUCHAR)pcBase)[i + j])
@@ -240,6 +346,74 @@ DomitoMemorySearchPattern(
return STATUS_NOT_FOUND;
}
+_Success_(return == STATUS_SUCCESS)
+_Must_inspect_result_
+_IRQL_requires_max_(PASSIVE_LEVEL)
+NTSTATUS
+DomitoReadFileAt(
+ _In_ HANDLE FileHandle,
+ _Out_writes_bytes_(BufferSize) PVOID Buffer,
+ _In_ ULONG BufferSize,
+ _In_ const LARGE_INTEGER* ByteOffset
+)
+{
+ if (!FileHandle || !ByteOffset || ByteOffset->QuadPart < 0 || (!Buffer && BufferSize))
+ {
+ return STATUS_INVALID_PARAMETER;
+ }
+
+ if (!BufferSize)
+ {
+ return STATUS_SUCCESS;
+ }
+
+ HANDLE eventHandle = nullptr;
+ OBJECT_ATTRIBUTES attributes;
+ InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr);
+ NTSTATUS status = ZwCreateEvent(&eventHandle, EVENT_ALL_ACCESS, &attributes, NotificationEvent, FALSE);
+
+ if (!NT_SUCCESS(status))
+ {
+ return status;
+ }
+
+ PVOID eventObject = nullptr;
+ status = ObReferenceObjectByHandle(eventHandle, SYNCHRONIZE, *ExEventObjectType, KernelMode, &eventObject, nullptr);
+
+ if (!NT_SUCCESS(status))
+ {
+ ZwClose(eventHandle);
+ return status;
+ }
+
+ IO_STATUS_BLOCK ioStatusBlock = {};
+ LARGE_INTEGER offset = *ByteOffset;
+ status =
+ ZwReadFile(FileHandle, eventHandle, nullptr, nullptr, &ioStatusBlock, Buffer, BufferSize, &offset, nullptr);
+
+ if (status == STATUS_PENDING)
+ {
+ // A private event identifies this request even if the file has other I/O.
+ // KernelMode + nonalertable + no timeout cannot return before completion.
+ KeWaitForSingleObject(eventObject, Executive, KernelMode, FALSE, nullptr);
+ status = ioStatusBlock.Status;
+ }
+
+ if (NT_SUCCESS(status))
+ {
+ status = ioStatusBlock.Status;
+
+ if (NT_SUCCESS(status) && ioStatusBlock.Information != BufferSize)
+ {
+ status = STATUS_END_OF_FILE;
+ }
+ }
+
+ ObDereferenceObject(eventObject);
+ ZwClose(eventHandle);
+ return status;
+}
+
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -250,33 +424,8 @@ DomitoReadFile(
_In_ ULONG BufferSize
)
{
- NTSTATUS status = STATUS_SUCCESS;
- IO_STATUS_BLOCK ioStatusBlock;
-
- // Read the file into memory using ZwReadFile
- if (!NT_SUCCESS(status = ZwReadFile(
- FileHandle,
- NULL,
- NULL,
- NULL,
- &ioStatusBlock,
- Buffer,
- BufferSize,
- NULL,
- NULL
- )))
- {
- return status;
- }
-
- // Check if the file was read successfully
- if (!NT_SUCCESS(ioStatusBlock.Status))
- {
- return ioStatusBlock.Status;
- }
-
- // File read successfully
- return status;
+ const LARGE_INTEGER offset = {};
+ return DomitoReadFileAt(FileHandle, Buffer, BufferSize, &offset);
}
_Success_(return == STATUS_SUCCESS)
@@ -286,95 +435,94 @@ _IRQL_requires_max_(PASSIVE_LEVEL)
NTSTATUS
DomitoGetProcessImageName(
_In_ ULONG ProcessId,
- _Inout_ PUNICODE_STRING * ProcessImageName
+ _Outptr_result_maybenull_ PUNICODE_STRING* ProcessImageName
)
{
- NTSTATUS status = STATUS_UNSUCCESSFUL;
- ULONG returnedLength;
- HANDLE hProcess = NULL;
+ PAGED_CODE();
- PAGED_CODE(); // this eliminates the possibility of the IDLE Thread/Process
-
- if (ProcessId == 0 || ProcessId == 4)
- {
- return STATUS_INVALID_PARAMETER_1;
- }
-
- if (ProcessImageName == NULL)
+ if (!ProcessImageName)
{
return STATUS_INVALID_PARAMETER_2;
}
- CLIENT_ID cid;
- cid.UniqueProcess = (HANDLE)ProcessId;
- cid.UniqueThread = NULL;
+ *ProcessImageName = nullptr;
- OBJECT_ATTRIBUTES objAttr;
- InitializeObjectAttributes(
- &objAttr,
- NULL,
- OBJ_KERNEL_HANDLE,
- NULL,
- NULL
- );
+ if (!ProcessId || ProcessId == 4)
+ {
+ return STATUS_INVALID_PARAMETER_1;
+ }
- if (!NT_SUCCESS(status = ZwOpenProcess(
- &hProcess,
- PROCESS_ALL_ACCESS,
- &objAttr,
- &cid
- )))
+ if (!G_Common.ZwQueryInformationProcess)
+ {
+ return STATUS_NOT_IMPLEMENTED;
+ }
+
+ CLIENT_ID cid = {};
+ cid.UniqueProcess = ULongToHandle(ProcessId);
+ OBJECT_ATTRIBUTES attributes;
+ InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr);
+ HANDLE process = nullptr;
+ // PROCESS_QUERY_LIMITED_INFORMATION (WinNT.h); not named by the WDK headers.
+ const ACCESS_MASK queryLimitedInformation = 0x1000;
+ NTSTATUS status = ZwOpenProcess(&process, queryLimitedInformation, &attributes, &cid);
+
+ if (!NT_SUCCESS(status))
{
return status;
}
- if (G_Common.ZwQueryInformationProcess == NULL)
+ ULONG size = 0;
+ status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, nullptr, 0, &size);
+
+ if (status == STATUS_INFO_LENGTH_MISMATCH)
{
- status = STATUS_NOT_IMPLEMENTED;
- goto cleanUp;
- }
+ if (size < sizeof(UNICODE_STRING))
+ {
+ status = STATUS_INVALID_BUFFER_SIZE;
+ }
+ else
+ {
+ auto image = static_cast(G_Memory.Allocate(size));
- /* Query the actual size of the process path */
- status = G_Common.ZwQueryInformationProcess(
- hProcess,
- ProcessImageFileName,
- NULL, // buffer
- 0, // buffer size
- &returnedLength
- );
+ if (!image)
+ {
+ status = STATUS_INSUFFICIENT_RESOURCES;
+ }
+ else
+ {
+ ULONG returned = 0;
+ status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, image, size, &returned);
- if (STATUS_INFO_LENGTH_MISMATCH != status)
- {
- goto cleanUp;
- }
+ if (NT_SUCCESS(status))
+ {
+ const ULONG_PTR base = reinterpret_cast(image);
+ const ULONG_PTR buffer = reinterpret_cast(image->Buffer);
- *ProcessImageName = (PUNICODE_STRING)G_Memory.Allocate(returnedLength);
+ if (!image->Length || (image->Length & 1) || image->Length > image->MaximumLength ||
+ buffer < base || buffer - base < sizeof(UNICODE_STRING) || buffer - base > size ||
+ image->MaximumLength > size - (buffer - base))
+ {
+ status = STATUS_INVALID_BUFFER_SIZE;
+ }
+ }
- if (*ProcessImageName == NULL)
- {
- status = STATUS_INSUFFICIENT_RESOURCES;
- goto cleanUp;
+ if (NT_SUCCESS(status))
+ {
+ *ProcessImageName = image;
+ }
+ else
+ {
+ G_Memory.Free(image);
+ }
+ }
+ }
}
-
- /* Retrieve the process path from the handle to the process */
- if (!NT_SUCCESS(status = G_Common.ZwQueryInformationProcess(
- hProcess,
- ProcessImageFileName,
- *ProcessImageName,
- returnedLength,
- &returnedLength
- )))
- {
- G_Memory.Free(*ProcessImageName);
- }
-
-cleanUp:
-
- if (hProcess)
+ else if (NT_SUCCESS(status))
{
- ZwClose(hProcess);
+ status = STATUS_INVALID_BUFFER_SIZE;
}
+ ZwClose(process);
return status;
}
#pragma code_seg()
diff --git a/src/Domito.vcxproj b/src/Domito.vcxproj
index 760de2e..173306a 100644
--- a/src/Domito.vcxproj
+++ b/src/Domito.vcxproj
@@ -121,12 +121,18 @@
+
+
+
+
+
+
diff --git a/src/Domito.vcxproj.filters b/src/Domito.vcxproj.filters
index 1b7ac8a..bf45072 100644
--- a/src/Domito.vcxproj.filters
+++ b/src/Domito.vcxproj.filters
@@ -28,6 +28,15 @@
Source Files
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
Source Files
@@ -36,9 +45,18 @@
Header Files
+
+ Header Files
+
Header Files\Public
+
+ Header Files\Public
+
+
+ Header Files\Public
+
Header Files\Public
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..29b3a13
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,109 @@
+# Regression tests
+
+From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
+
+```powershell
+python tests\run-digest-tests.py
+python tests\run-common-tests.py
+python tests\run-ci-probe-tests.py
+python tests\run-ci-validation-tests.py
+```
+
+Build outputs are temporary. No driver is installed or loaded. Test runners
+extract the actual production functions or include the probe body unchanged;
+kernel calls, CI calls, and pool allocation are replaced with test doubles.
+The digest harness uses real BCrypt, Crypt32 and the Windows PE SIP provider.
+
+## Digest and legacy verification tests
+
+Coverage includes PE32/PE32+ files, all four supported hash algorithms, checksum
+and certificate exclusions, overlays, reverse section-header order, multiple
+certificate entries, certificate padding, unsupported section layouts, bounded
+DER algorithm discovery, SHA-1 pattern decoys, truncated ASN.1, explicit catalog
+policy forwarding and cleanup between failed/successful CI attempts.
+
+The test compares digests against `CryptSIPCreateIndirectData` and reads the
+installed system `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` as a
+real-file corpus. For embedded signatures, Crypt32 independently decodes the SPC
+algorithm and expected file digest. These checks do not verify signature trust.
+
+Guard pages cover truncated headers and invalid ranges in both compatibility
+and strict paths. Fault injection covers allocation/hash failures, deterministic
+outputs, short certificate reads and digest ownership. A DOS stub larger than
+4 MiB exercises the removed scratch-buffer limit. Older entry points retain
+legacy selection/layout behavior; strict Ex APIs enforce their documented subset.
+
+## File and export helper tests
+
+The common harness checks explicit and zero offsets, exact/short/failed reads,
+pending completion, event/object cleanup, and error propagation. Export fixtures
+cover nonidentity name-ordinal mappings, forwarders, empty slots, invalid spans,
+unbounded strings, and deterministic failure outputs. The export API requires a
+valid loader-mapped module; it is not an arbitrary file parser.
+
+## CI validation result and selection helpers
+
+`run-ci-validation-tests.py` compiles the production wrapper source unchanged
+(apart from replacing its private include with test dependencies) and extracts
+the public declarations/constants. It checks native flag/level forwarding,
+64-byte capacity on every call, the opt-in x64 experimental bit, non-x64 mode
+rejection, invalid inputs, returned hash invariants, deterministic failure
+scalars, preserved diagnostic policies, and result cleanup/reuse. The embedded
+path covers all four explicit algorithms, unsupported/unsigned/invalid layouts,
+partial digest allocation cleanup, CI failure propagation and no catalog retry.
+CI/hash-layout behavior is injected; real digest behavior is checked by the
+existing Windows SIP/Crypt32 differential harness. A second host build exercises
+the non-x64 conditional branch; real ARM64 compilation is a separate build check.
+
+The first kernel trace used direct CI calls. Probe v2 in CodeIntegrity-RE now
+consumes these library helpers; a returned v2 trace is needed to claim runtime
+verification of the wrappers themselves. The earlier native result is not
+silently relabelled as an API-wrapper test.
+
+## File-object validation flag probe
+
+See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
+for `CiValidateFileObject` flag `0x20`. The host runner tests probe plumbing with
+mock CI. The kernel probe must still run in a suitable VM to verify actual
+policy behavior.
+
+## Kernel compile checks and remaining validation
+
+`mincrypt-layout-tests.cpp` asserts default modern x64/ARM64 and pre-Windows-10
+compile layouts. On x64 it also includes the opt-in observed 26100 header's 14
+layout assertions. Compile it with the same WDK/kernel architecture flags and
+include paths as the library. This checks structure definitions, not runtime
+compatibility on those systems. `ci-file-object-probe.cpp` is separately compiled
+as kernel code for x64/ARM64; its real CI behavior is still untested.
+
+The host tests do not establish CI trust policy, real asynchronous kernel I/O,
+real policy ownership under Driver Verifier, revocation/timestamp behavior, or
+all Windows-version/layout combinations. See the
+[implementation ledger](../docs/implementation-status.md) for the remaining work.
+
+## Consumer helper regression tests
+
+Run `python tests/run-consumer-helper-tests.py` in an x64 Visual Studio developer
+shell. This fifth host runner compiles the actual certificate/common-consumer
+implementations and the existing PID name function, with kernel operations
+replaced by deterministic doubles. Hashing uses real Windows CNG; independent
+Crypt32 decoding/re-encoding/hash calls verify SPKI representation and pins.
+The synthetic v1/v3 certificates are framing fixtures, not trust fixtures.
+
+Coverage includes truncated certificate prefixes at inaccessible guard pages,
+malformed DER lengths and optional read-only comparisons with real system-root
+certificate SPKIs,
+four hash algorithms, short capacities and four injected CNG failures, strict
+hex conversion across all byte values, path/handle ownership, failed references,
+partial publisher allocation, process-name query failures, stale-output cleanup,
+and malformed/overflowing x64 chain spans. A second host build exercises the
+non-x64 rejection branch without claiming ARM64 ABI or runtime emulation.
+No service is started, certificate trusted, or real CI policy evaluated.
+
+## Deprecation compiler checks
+
+Run `python tests/run-deprecation-tests.py --arch x64` or `--arch ARM64` from a
+Visual Studio developer shell with the WDK installed. This compile-only runner
+uses the actual public headers: modern API references must compile under /WX,
+while each of four deprecated references must fail specifically with C4996 and
+its migration message. Both C and C++ are checked. It does not link or load code.
diff --git a/tests/ci-file-object-probe-tests.cpp b/tests/ci-file-object-probe-tests.cpp
new file mode 100644
index 0000000..d7924b4
--- /dev/null
+++ b/tests/ci-file-object-probe-tests.cpp
@@ -0,0 +1,175 @@
+// Host tests validate the probe plumbing, NOT CI trust behavior.
+#define WIN32_NO_STATUS
+#include
+#undef WIN32_NO_STATUS
+#include
+#include
+#include
+#include
+#include "ci-probe-constants.inc"
+#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
+#define DOMITO_CI_PROBE_HOST_TEST
+typedef void* PFILE_OBJECT;
+
+struct MINCRYPT_POLICY_INFO
+{
+ ULONG Size;
+ ULONG VerificationStatus;
+ ULONG PolicyBits;
+ void* ChainInfo;
+};
+
+static unsigned calls;
+static unsigned frees;
+static unsigned outstanding;
+static unsigned checks;
+static bool failureMode;
+static int fileToken;
+static int chainToken;
+
+static void
+Check(
+ bool value,
+ const char* label
+)
+{
+ ++checks;
+
+ if (!value)
+ {
+ std::fprintf(stderr, "FAIL: %s\n", label);
+ std::exit(1);
+ }
+}
+
+static NTSTATUS
+CiValidateFileObject(
+ PFILE_OBJECT file,
+ ULONG flags,
+ ULONG level,
+ MINCRYPT_POLICY_INFO* policy,
+ MINCRYPT_POLICY_INFO* timestamp,
+ LARGE_INTEGER* time,
+ UCHAR* hash,
+ UINT32* size,
+ ALG_ID* algorithm
+)
+{
+ const ULONG expectedFlags[] = {0, 0x20, 0x20, 0, 0, 0x20};
+ Check(calls < 6 && flags == expectedFlags[calls], "paired flags and order");
+ Check(file == &fileToken && level == 0, "same file and level zero");
+ Check(outstanding == 0, "previous outputs freed before next call");
+ Check(policy->Size == sizeof(*policy) && timestamp->Size == sizeof(*timestamp), "policy sizes");
+ Check(
+ !policy->ChainInfo && !timestamp->ChainInfo && !policy->PolicyBits && !timestamp->PolicyBits,
+ "fresh policy objects"
+ );
+ Check(*size == (calls < 4 ? 64u : 63u), "capacity reset for every call");
+ Check(*algorithm == 0 && time->QuadPart == 0, "fresh scalar outputs");
+
+ for (unsigned i = 0; i < 64; ++i)
+ {
+ Check(hash[i] == 0, "fresh hash buffer");
+ }
+
+ ++calls;
+
+ if (*size < 64)
+ {
+ *size = 64;
+ return STATUS_BUFFER_TOO_SMALL;
+ }
+
+ policy->ChainInfo = timestamp->ChainInfo = &chainToken;
+ outstanding = 2;
+ policy->PolicyBits = 0xA5;
+ timestamp->PolicyBits = 0x5A;
+ policy->VerificationStatus = failureMode ? STATUS_INVALID_IMAGE_HASH : STATUS_SUCCESS;
+ timestamp->VerificationStatus = STATUS_SUCCESS;
+ // Also allocate on simulated failure to exercise unconditional cleanup.
+ if (failureMode)
+ {
+ return STATUS_INVALID_IMAGE_HASH;
+ }
+
+ std::memset(hash, 0xCC, 32);
+ *size = 32;
+ *algorithm = 0x800C;
+ time->QuadPart = 123;
+ return STATUS_SUCCESS;
+}
+
+static void
+CiFreePolicyInfo(
+ MINCRYPT_POLICY_INFO* policy
+)
+{
+ ++frees;
+
+ if (policy->ChainInfo)
+ {
+ Check(outstanding > 0, "owned chain");
+ --outstanding;
+ }
+
+ *policy = {};
+}
+
+#include "ci-file-object-probe.cpp"
+
+int
+main()
+{
+ for (unsigned pass = 0; pass < 2; ++pass)
+ {
+ failureMode = pass != 0;
+ calls = frees = outstanding = 0;
+ DOMITO_CI_PROBE_RESULT results[DOMITO_CI_PROBE_RESULT_COUNT];
+ std::memset(results, 0xFF, sizeof(results));
+ DomitoProbeCiValidateFileObject(&fileToken, results);
+ Check(calls == 6 && frees == 12 && outstanding == 0, "all calls and frees");
+
+ for (unsigned i = 0; i < 6; ++i)
+ {
+ const auto& r = results[i];
+ Check(r.HashGuardIntact, "buffer guard");
+ const NTSTATUS expected = i >= 4 ? STATUS_BUFFER_TOO_SMALL
+ : failureMode ? STATUS_INVALID_IMAGE_HASH
+ : STATUS_SUCCESS;
+ Check(r.Status == expected, "status preserved");
+
+ if (i < 4)
+ {
+ Check(r.PolicyBits == 0xA5 && r.TimeStampPolicyBits == 0x5A, "policy capture");
+ Check(r.HasChain && r.HasTimeStampChain, "chain presence captured before free");
+ }
+
+ if (NT_SUCCESS(r.Status))
+ {
+ Check(
+ r.HashSize == 32 && r.HashAlgId == 0x800C && r.SigningTime.QuadPart == 123,
+ "digest metadata and signing time"
+ );
+
+ for (unsigned j = 0; j < 64; ++j)
+ {
+ Check(r.Hash[j] == (j < 32 ? 0xCC : 0), "digest copied and tail cleared");
+ }
+ }
+ else
+ {
+ for (auto byte : r.Hash)
+ {
+ Check(byte == 0, "no digest from failed call");
+ }
+ }
+
+ if (i >= 4)
+ {
+ Check(r.HashSize == 64, "required capacity captured");
+ }
+ }
+ }
+
+ std::printf("PASS: %u probe checks (mock CI; no runtime trust validation)\n", checks);
+}
diff --git a/tests/ci-file-object-probe.cpp b/tests/ci-file-object-probe.cpp
new file mode 100644
index 0000000..41e3b9d
--- /dev/null
+++ b/tests/ci-file-object-probe.cpp
@@ -0,0 +1,72 @@
+// Compile into an existing signed test driver; this file has no DriverEntry.
+#ifndef DOMITO_CI_PROBE_HOST_TEST
+#include
+#include "Domito.MinCrypt.h"
+#endif
+#include "ci-file-object-probe.h"
+
+_IRQL_requires_max_(PASSIVE_LEVEL)
+void
+DomitoProbeCiValidateFileObject(
+ PFILE_OBJECT FileObject,
+ DOMITO_CI_PROBE_RESULT* Results
+)
+{
+ const ULONG flags[DOMITO_CI_PROBE_RESULT_COUNT] = {
+ 0,
+ DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
+ DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
+ 0,
+ 0,
+ DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE
+ };
+
+ for (ULONG i = 0; i < DOMITO_CI_PROBE_RESULT_COUNT; ++i)
+ {
+ MINCRYPT_POLICY_INFO policy = {};
+ MINCRYPT_POLICY_INFO timestamp = {};
+ policy.Size = sizeof(policy);
+ timestamp.Size = sizeof(timestamp);
+
+ struct
+ {
+ UCHAR Bytes[MINCRYPT_MAX_HASH_LENGTH];
+ ULONG Guard;
+ } hash = {};
+
+ hash.Guard = 0xD01170AB;
+ DOMITO_CI_PROBE_RESULT& result = Results[i];
+ result = {};
+ result.Flags = flags[i];
+ result.InputCapacity = i < 4 ? sizeof(hash.Bytes) : sizeof(hash.Bytes) - 1;
+ result.HashSize = result.InputCapacity;
+ result.Status = CiValidateFileObject(
+ FileObject,
+ result.Flags,
+ 0,
+ &policy,
+ ×tamp,
+ &result.SigningTime,
+ hash.Bytes,
+ &result.HashSize,
+ &result.HashAlgId
+ );
+ result.HashGuardIntact = hash.Guard == 0xD01170AB;
+ // Keep failure outputs as raw observations; only successful calls have
+ // a usable digest. Do not dereference the incompletely mapped chains.
+ result.PolicyBits = policy.PolicyBits;
+ result.TimeStampPolicyBits = timestamp.PolicyBits;
+ result.VerificationStatus = (NTSTATUS)policy.VerificationStatus;
+ result.TimeStampVerificationStatus = (NTSTATUS)timestamp.VerificationStatus;
+ result.HasChain = policy.ChainInfo != nullptr;
+ result.HasTimeStampChain = timestamp.ChainInfo != nullptr;
+
+ if (NT_SUCCESS(result.Status) && result.HashSize <= sizeof(result.Hash))
+ {
+ RtlCopyMemory(result.Hash, hash.Bytes, result.HashSize);
+ }
+
+ CiFreePolicyInfo(&policy);
+ CiFreePolicyInfo(×tamp);
+ }
+}
diff --git a/tests/ci-file-object-probe.h b/tests/ci-file-object-probe.h
new file mode 100644
index 0000000..04e4f87
--- /dev/null
+++ b/tests/ci-file-object-probe.h
@@ -0,0 +1,31 @@
+#pragma once
+
+// Include ntifs.h and Domito.MinCrypt.h first. Test support, not a library API.
+typedef struct _DOMITO_CI_PROBE_RESULT
+{
+ ULONG Flags;
+ ULONG InputCapacity;
+ NTSTATUS Status;
+ UINT32 HashSize;
+ ALG_ID HashAlgId;
+ UCHAR Hash[MINCRYPT_MAX_HASH_LENGTH];
+ LARGE_INTEGER SigningTime;
+ ULONG PolicyBits;
+ ULONG TimeStampPolicyBits;
+ NTSTATUS VerificationStatus;
+ NTSTATUS TimeStampVerificationStatus;
+ BOOLEAN HasChain;
+ BOOLEAN HasTimeStampChain;
+ BOOLEAN HashGuardIntact;
+} DOMITO_CI_PROBE_RESULT;
+
+#define DOMITO_CI_PROBE_RESULT_COUNT 6
+
+// PASSIVE_LEVEL; referenced file object; file bytes must remain stable.
+// Writes observations, not an overall trust verdict. Frees both policies per call.
+_IRQL_requires_max_(PASSIVE_LEVEL)
+void
+DomitoProbeCiValidateFileObject(
+ _In_ PFILE_OBJECT FileObject,
+ _Out_writes_(DOMITO_CI_PROBE_RESULT_COUNT) DOMITO_CI_PROBE_RESULT* Results
+);
diff --git a/tests/ci-validation-tests.cpp b/tests/ci-validation-tests.cpp
new file mode 100644
index 0000000..ff21972
--- /dev/null
+++ b/tests/ci-validation-tests.cpp
@@ -0,0 +1,519 @@
+#define WIN32_NO_STATUS
+#include
+#undef WIN32_NO_STATUS
+#include
+#include
+#include
+#include
+#include
+
+#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
+#ifndef _IRQL_requires_max_
+#define _IRQL_requires_max_(x)
+#endif
+#define PASSIVE_LEVEL 0
+#define PAGED_CODE() ((void)0)
+typedef void* PFILE_OBJECT;
+typedef void* LPWIN_CERTIFICATE;
+
+struct MINCRYPT_POLICY_INFO
+{
+ ULONG Size;
+ NTSTATUS VerificationStatus;
+ ULONG PolicyBits;
+ void* ChainInfo;
+};
+
+#include "ci-validation-api.inc"
+
+static unsigned checks;
+static unsigned live;
+static unsigned validationCalls;
+static unsigned embeddedCalls;
+static unsigned digestCalls;
+static unsigned digestFrees;
+static NTSTATUS nativeStatus = STATUS_SUCCESS;
+static NTSTATUS embeddedStatus = STATUS_SUCCESS;
+static NTSTATUS digestStatus = STATUS_SUCCESS;
+static UINT32 nativeAlgorithm = CALG_SHA_256;
+static UINT32 nativeSize = 32;
+static ULONG fakeDigestSize = 32;
+static bool omitCertificate;
+static bool omitDigest;
+static bool partialDigestOnFailure;
+static const UCHAR image[] = {1, 2, 3};
+static ULONG observedFlags;
+static ULONG observedLevel;
+static ULONG observedCapacity;
+static PFILE_OBJECT observedObject;
+static ALG_ID observedEmbeddedAlgorithm;
+static ULONG observedSecuritySize;
+static void* observedCertificate;
+
+static void
+Check(
+ bool value,
+ const char* message
+)
+{
+ ++checks;
+
+ if (!value)
+ {
+ std::fprintf(stderr, "FAIL: %s\n", message);
+ std::exit(1);
+ }
+}
+
+static void*
+Allocate(
+ SIZE_T size
+)
+{
+ void* value = std::malloc(size);
+ Check(value != nullptr, "host allocation");
+ ++live;
+ return value;
+}
+
+static void
+Release(
+ void* value
+)
+{
+ Check(value && live, "single allocation owner");
+ --live;
+ std::free(value);
+}
+
+static void
+CiFreePolicyInfo(
+ MINCRYPT_POLICY_INFO* policy
+)
+{
+ if (policy->ChainInfo)
+ {
+ Release(policy->ChainInfo);
+ }
+
+ *policy = {};
+}
+
+static void
+Populate(
+ MINCRYPT_POLICY_INFO* policy,
+ MINCRYPT_POLICY_INFO* timestamp
+)
+{
+ Check(!policy->ChainInfo && !timestamp->ChainInfo, "old result freed before reuse");
+ policy->Size = timestamp->Size = sizeof(*policy);
+ policy->ChainInfo = Allocate(1);
+ timestamp->ChainInfo = Allocate(1);
+ policy->PolicyBits = 0x123;
+ timestamp->PolicyBits = 0x456;
+ policy->VerificationStatus = STATUS_INVALID_IMAGE_HASH;
+}
+
+static NTSTATUS
+CiValidateFileObject(
+ PFILE_OBJECT object,
+ UINT32 flags,
+ UINT32 level,
+ MINCRYPT_POLICY_INFO* policy,
+ MINCRYPT_POLICY_INFO* timestamp,
+ LARGE_INTEGER* time,
+ UINT8* hash,
+ UINT32* size,
+ ALG_ID* algorithm
+)
+{
+ ++validationCalls;
+ observedObject = object;
+ observedFlags = flags;
+ observedLevel = level;
+ observedCapacity = *size;
+ Check(*size == 64, "fresh native hash capacity");
+ Check(!time->QuadPart && !*algorithm, "native scalar outputs initialized");
+
+ for (unsigned i = 0; i < 64; ++i)
+ {
+ Check(!hash[i], "native digest starts empty");
+ }
+
+ Populate(policy, timestamp);
+ time->QuadPart = 99;
+ std::memset(hash, 0x5a, 64);
+ *size = nativeSize;
+ *algorithm = nativeAlgorithm;
+ return nativeStatus;
+}
+
+static NTSTATUS
+DomitoCalculatePortableExecutableDigestEx(
+ const UCHAR* bytes,
+ ULONG size,
+ UINT32 algorithm,
+ PULONG digestSize,
+ PVOID* digest,
+ LPWIN_CERTIFICATE* certificate,
+ PULONG securitySize
+)
+{
+ ++digestCalls;
+ Check(bytes == image && size == sizeof(image), "same PE snapshot forwarded");
+ Check(
+ algorithm == CALG_SHA1 || algorithm == CALG_SHA_256 || algorithm == CALG_SHA_384 || algorithm == CALG_SHA_512,
+ "supported digest choice forwarded"
+ );
+
+ if (NT_SUCCESS(digestStatus) || partialDigestOnFailure)
+ {
+ *digest = omitDigest ? nullptr : Allocate(64);
+
+ if (*digest)
+ {
+ std::memset(*digest, 0xa5, 64);
+ }
+
+ *digestSize = fakeDigestSize;
+ *certificate = omitCertificate ? nullptr : const_cast(image);
+ *securitySize = omitCertificate ? 0 : 123;
+ }
+
+ return digestStatus;
+}
+
+static void
+DomitoFreePortableExecutableDigest(
+ PVOID digest
+)
+{
+ ++digestFrees;
+ Release(digest);
+}
+
+static NTSTATUS
+CiCheckSignedFile(
+ PVOID hash,
+ UINT32 size,
+ ALG_ID algorithm,
+ LPWIN_CERTIFICATE certificate,
+ ULONG securitySize,
+ MINCRYPT_POLICY_INFO* policy,
+ LARGE_INTEGER* time,
+ MINCRYPT_POLICY_INFO* timestamp
+)
+{
+ ++embeddedCalls;
+ observedEmbeddedAlgorithm = algorithm;
+ observedSecuritySize = securitySize;
+ observedCertificate = certificate;
+
+ for (UINT32 i = 0; i < size; ++i)
+ {
+ Check(static_cast(hash)[i] == 0xa5, "computed digest forwarded unchanged");
+ }
+
+ Populate(policy, timestamp);
+ time->QuadPart = 123;
+ return embeddedStatus;
+}
+
+#ifdef DOMITO_TEST_OTHER_ARCH
+#undef _M_AMD64
+#undef _AMD64_
+#endif
+#include "ci-validation-under-test.inc"
+
+static void
+CheckEmptyDigest(
+ const DOMITO_CI_VALIDATION_RESULT& result
+)
+{
+ Check(!result.HashSize && !result.HashAlgId && !result.SigningTime.QuadPart, "failure digest/time cleared");
+
+ for (auto value : result.Hash)
+ {
+ Check(!value, "failure hash bytes cleared");
+ }
+}
+
+static void
+Clear(
+ DOMITO_CI_VALIDATION_RESULT* result
+)
+{
+ DomitoFreeCiValidationResult(result);
+ DOMITO_CI_VALIDATION_RESULT empty = {};
+ Check(std::memcmp(result, &empty, sizeof(empty)) == 0, "full result cleared by destructor");
+ Check(live == 0, "no leaked policy/digest allocation");
+}
+
+int
+main()
+{
+ DOMITO_CI_VALIDATION_RESULT result = {};
+ PFILE_OBJECT object = reinterpret_cast(static_cast(0x1234));
+ DomitoFreeCiValidationResult(nullptr);
+ Clear(&result);
+ Clear(&result);
+ Check(
+ DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, nullptr) == STATUS_INVALID_PARAMETER,
+ "null native result"
+ );
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, nullptr) ==
+ STATUS_INVALID_PARAMETER,
+ "null embedded result"
+ );
+ Check(!validationCalls && !embeddedCalls && !digestCalls, "null result made no validation calls");
+
+ for (UINT32 flags : {0UL, 0x20UL, 0xa0UL, 0x80000024UL})
+ {
+ for (UINT8 level : {UINT8(0), UINT8(7), UINT8(255)})
+ {
+ result.HashSize = 1;
+ result.HashAlgId = 0xdead;
+ Check(
+ DomitoValidateFileObjectEx(object, flags, level, DomitoCiSignatureSelectionNative, &result) ==
+ STATUS_SUCCESS,
+ "native status"
+ );
+ Check(
+ observedObject == object && observedFlags == flags && observedLevel == level && observedCapacity == 64,
+ "native inputs unchanged"
+ );
+ Check(
+ result.FileObjectValidationFlags == flags && result.RequestedSigningLevel == level,
+ "export input controls recorded"
+ );
+ Check(
+ result.HashSize == 32 && result.HashAlgId == CALG_SHA_256 && result.Hash[0] == 0x5a &&
+ result.SigningTime.QuadPart == 99,
+ "native result"
+ );
+ // API return success does not silently reinterpret a policy status.
+ Check(result.PolicyInfo.VerificationStatus == STATUS_INVALID_IMAGE_HASH, "policy observation retained");
+ Clear(&result);
+ }
+ }
+
+ const unsigned beforeExperimental = validationCalls;
+ NTSTATUS experimental =
+ DomitoValidateFileObjectEx(object, 0x20, 9, DomitoCiSignatureSelectionFirstSuccessfulExperimental, &result);
+#ifdef DOMITO_TEST_OTHER_ARCH
+ Check(
+ experimental == STATUS_NOT_SUPPORTED && validationCalls == beforeExperimental,
+ "experimental mode excluded on non-x64"
+ );
+ CheckEmptyDigest(result);
+#else
+ Check(experimental == STATUS_SUCCESS && validationCalls == beforeExperimental + 1, "experimental native call");
+ Check(observedFlags == 0xa0 && observedLevel == 9, "only experimental bit added");
+#endif
+ Clear(&result);
+ const unsigned beforeInvalid = validationCalls;
+ Check(
+ DomitoValidateFileObjectEx(nullptr, 0x20, 0, DomitoCiSignatureSelectionNative, &result) ==
+ STATUS_INVALID_PARAMETER,
+ "null file object"
+ );
+ Check(
+ DomitoValidateFileObjectEx(object, 0x20, 0, static_cast(-1), &result) ==
+ STATUS_INVALID_PARAMETER,
+ "invalid selection"
+ );
+ Check(validationCalls == beforeInvalid, "invalid requests never call validation");
+ Clear(&result);
+
+ for (NTSTATUS failure : {STATUS_INVALID_IMAGE_HASH, STATUS_BUFFER_TOO_SMALL, STATUS_INSUFFICIENT_RESOURCES})
+ {
+ nativeStatus = failure;
+ Check(
+ DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == failure,
+ "native failure preserved"
+ );
+ CheckEmptyDigest(result);
+ Check(live == 2 && result.PolicyInfo.PolicyBits == 0x123, "failure policy diagnostics remain owned");
+ Clear(&result);
+ }
+
+ nativeStatus = STATUS_SUCCESS;
+ nativeSize = 65;
+ Check(
+ DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) ==
+ STATUS_INVALID_BUFFER_SIZE,
+ "oversized returned hash rejected"
+ );
+ CheckEmptyDigest(result);
+ Clear(&result);
+ nativeSize = 31;
+ Check(
+ DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) ==
+ STATUS_INVALID_BUFFER_SIZE,
+ "algorithm length mismatch rejected"
+ );
+ Clear(&result);
+ nativeSize = 32;
+ nativeAlgorithm = 0xdead;
+ Check(
+ DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_NOT_SUPPORTED,
+ "unknown returned algorithm rejected"
+ );
+ Clear(&result);
+
+ for (ALG_ID algorithm : {CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512})
+ {
+ nativeAlgorithm = algorithm;
+ nativeSize = DomitoCiDigestSize(algorithm);
+ Check(
+ DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS,
+ "known native digest sizes"
+ );
+
+ for (UINT32 i = result.HashSize; i < sizeof(result.Hash); ++i)
+ {
+ Check(!result.Hash[i], "unused native digest bytes cleared");
+ }
+
+ Clear(&result);
+ fakeDigestSize = DomitoCiDigestSize(algorithm);
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), algorithm, &result) ==
+ STATUS_SUCCESS,
+ "embedded success"
+ );
+ Check(
+ observedEmbeddedAlgorithm == algorithm && observedSecuritySize == 123 && observedCertificate == image,
+ "embedded certificate/algorithm forwarded"
+ );
+ Check(
+ result.HashSize == fakeDigestSize && result.HashAlgId == algorithm && result.Hash[0] == 0xa5 &&
+ result.SigningTime.QuadPart == 123,
+ "embedded result owns copied digest"
+ );
+ Check(
+ !result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
+ "embedded metadata and digest cleanup"
+ );
+ Clear(&result);
+ }
+
+ nativeAlgorithm = CALG_SHA_256;
+ nativeSize = 32;
+ fakeDigestSize = 32;
+ nativeStatus = STATUS_MORE_ENTRIES;
+ Check(
+ DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_MORE_ENTRIES,
+ "nonnegative native status preserved"
+ );
+ Clear(&result);
+ nativeStatus = STATUS_SUCCESS;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(nullptr, 0, CALG_SHA_256, &result) ==
+ STATUS_INVALID_PARAMETER,
+ "null PE"
+ );
+ unsigned beforeDigest = digestCalls;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), 0xdead, &result) ==
+ STATUS_NOT_SUPPORTED,
+ "unsupported algorithm"
+ );
+ Check(digestCalls == beforeDigest, "unsupported algorithm does not hash");
+
+ for (NTSTATUS failure : {STATUS_NOT_SUPPORTED, STATUS_INVALID_IMAGE_FORMAT, STATUS_INSUFFICIENT_RESOURCES})
+ {
+ digestStatus = failure;
+ unsigned beforeEmbedded = embeddedCalls;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == failure,
+ "digest failure propagation"
+ );
+ Check(embeddedCalls == beforeEmbedded, "digest failure never calls CI");
+ CheckEmptyDigest(result);
+ Clear(&result);
+ }
+
+ partialDigestOnFailure = true;
+ unsigned beforeFree = digestFrees;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
+ STATUS_INSUFFICIENT_RESOURCES,
+ "partial digest failure"
+ );
+ Check(digestFrees == beforeFree + 1, "partial digest allocation freed");
+ Clear(&result);
+ partialDigestOnFailure = false;
+ digestStatus = STATUS_SUCCESS;
+ omitCertificate = true;
+ unsigned beforeEmbedded = embeddedCalls;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
+ STATUS_INVALID_IMAGE_HASH,
+ "unsigned PE rejected"
+ );
+ Check(embeddedCalls == beforeEmbedded, "unsigned PE does not call CI");
+ Clear(&result);
+ omitCertificate = false;
+ omitDigest = true;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
+ STATUS_INVALID_BUFFER_SIZE,
+ "missing digest rejected"
+ );
+ Clear(&result);
+ omitDigest = false;
+ fakeDigestSize = 31;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
+ STATUS_INVALID_BUFFER_SIZE,
+ "digest helper length mismatch"
+ );
+ Clear(&result);
+ fakeDigestSize = 32;
+ embeddedStatus = STATUS_INVALID_IMAGE_HASH;
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
+ STATUS_INVALID_IMAGE_HASH,
+ "CI embedded failure propagated without fallback"
+ );
+ CheckEmptyDigest(result);
+ Check(live == 2 && result.TimeStampPolicyInfo.PolicyBits == 0x456, "embedded failure diagnostics owned");
+ Clear(&result);
+ embeddedStatus = STATUS_SUCCESS;
+
+ for (unsigned i = 0; i < 16; ++i)
+ {
+ Check(
+ DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS,
+ "repeated native"
+ );
+ Check(
+ DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
+ STATUS_SUCCESS,
+ "reuse native result as embedded result"
+ );
+ Check(
+ !result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
+ "reuse resets metadata without leaking"
+ );
+ }
+
+ // Invalid calls must release a previously valid result, too.
+ Check(
+ DomitoValidateFileObjectEx(nullptr, 0, 0, DomitoCiSignatureSelectionNative, &result) ==
+ STATUS_INVALID_PARAMETER,
+ "invalid reuse cleans old result"
+ );
+ Clear(&result);
+ std::printf(
+ "PASS: %u CI validation checks (%s architecture branch), no live allocations\n",
+ checks,
+#ifdef DOMITO_TEST_OTHER_ARCH
+ "non-x64"
+#else
+ "x64"
+#endif
+ );
+}
diff --git a/tests/common-tests.cpp b/tests/common-tests.cpp
new file mode 100644
index 0000000..ade7b29
--- /dev/null
+++ b/tests/common-tests.cpp
@@ -0,0 +1,395 @@
+// Tests extract the unchanged production bodies. Kernel I/O is simulated.
+#define WIN32_NO_STATUS
+#include
+#undef WIN32_NO_STATUS
+#include
+#include
+#include
+#include
+#include
+#include
+#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
+#define MAXUSHORT 0xffff
+#define Executive 0
+#define KernelMode 0
+#define NotificationEvent 0
+#define OBJ_KERNEL_HANDLE 0x200
+#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f))
+
+struct OBJECT_ATTRIBUTES
+{
+ ULONG Attributes;
+};
+
+struct ANSI_STRING
+{
+ USHORT Length;
+ USHORT MaximumLength;
+ PCHAR Buffer;
+};
+
+typedef ANSI_STRING* PANSI_STRING;
+
+struct IO_STATUS_BLOCK
+{
+ NTSTATUS Status;
+ ULONG_PTR Information;
+};
+
+static int objectType;
+static void* objectTypeValue = &objectType;
+static void** ExEventObjectType = &objectTypeValue;
+static unsigned checks;
+
+static void
+Check(
+ bool value,
+ const char* label
+)
+{
+ ++checks;
+
+ if (!value)
+ {
+ std::fprintf(stderr, "FAIL: %s\n", label);
+ std::exit(1);
+ }
+}
+
+static LONG
+RtlCompareString(
+ PANSI_STRING a,
+ PANSI_STRING b,
+ BOOLEAN insensitive
+)
+{
+ if (a->Length != b->Length)
+ {
+ return 1;
+ }
+
+ for (USHORT i = 0; i < a->Length; ++i)
+ {
+ unsigned char x = (unsigned char)a->Buffer[i], y = (unsigned char)b->Buffer[i];
+
+ if (insensitive)
+ {
+ x = (unsigned char)std::tolower(x);
+ y = (unsigned char)std::tolower(y);
+ }
+
+ if (x != y)
+ {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static void* exportPointer;
+static ULONG exportBytes;
+
+static PVOID
+Directory(
+ PVOID,
+ BOOLEAN mapped,
+ USHORT index,
+ PULONG bytes
+)
+{
+ Check(mapped && index == IMAGE_DIRECTORY_ENTRY_EXPORT, "mapped export lookup");
+ *bytes = exportBytes;
+ return exportPointer;
+}
+
+struct
+{
+ PVOID (*RtlImageDirectoryEntryToData)(
+ PVOID,
+ BOOLEAN,
+ USHORT,
+ PULONG
+ );
+} G_Common = {Directory};
+
+static int eventToken;
+static int fileToken;
+static unsigned created;
+static unsigned referenced;
+static unsigned closed;
+static unsigned dereferenced;
+static unsigned reads;
+static unsigned waits;
+static int ioMode;
+static bool outstanding;
+static IO_STATUS_BLOCK* pendingIosb;
+static ULONG requested;
+static LONGLONG expectedOffset;
+
+static NTSTATUS
+ZwCreateEvent(
+ HANDLE* event,
+ ACCESS_MASK access,
+ OBJECT_ATTRIBUTES* attributes,
+ int type,
+ BOOLEAN signaled
+)
+{
+ Check(
+ access == EVENT_ALL_ACCESS && attributes->Attributes == OBJ_KERNEL_HANDLE && type == NotificationEvent &&
+ !signaled,
+ "private kernel event"
+ );
+
+ if (ioMode == 4)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ ++created;
+ *event = &eventToken;
+ return STATUS_SUCCESS;
+}
+
+static NTSTATUS
+ObReferenceObjectByHandle(
+ HANDLE event,
+ ACCESS_MASK access,
+ void* type,
+ int mode,
+ void** object,
+ void*
+)
+{
+ Check(
+ event == &eventToken && access == SYNCHRONIZE && type == *ExEventObjectType && mode == KernelMode,
+ "reference own event"
+ );
+
+ if (ioMode == 5)
+ {
+ return STATUS_ACCESS_DENIED;
+ }
+
+ ++referenced;
+ *object = &eventToken;
+ return STATUS_SUCCESS;
+}
+
+static void
+ZwClose(
+ HANDLE event
+)
+{
+ Check(event == &eventToken && !outstanding, "close only after completion");
+ ++closed;
+}
+
+static void
+ObDereferenceObject(
+ void* object
+)
+{
+ Check(object == &eventToken && !outstanding, "dereference only after completion");
+ ++dereferenced;
+}
+
+static NTSTATUS
+ZwReadFile(
+ HANDLE file,
+ HANDLE event,
+ void*,
+ void*,
+ IO_STATUS_BLOCK* iosb,
+ void*,
+ ULONG size,
+ LARGE_INTEGER* offset,
+ void*
+)
+{
+ ++reads;
+ Check(
+ file == &fileToken && event == &eventToken && offset && offset->QuadPart == expectedOffset,
+ "file and explicit offset"
+ );
+ Check(iosb->Status == 0 && iosb->Information == 0, "initialized IOSB");
+ requested = size;
+
+ if (ioMode == 6)
+ {
+ return STATUS_ACCESS_DENIED;
+ }
+
+ if (ioMode == 1 || ioMode == 2)
+ {
+ outstanding = true;
+ pendingIosb = iosb;
+ return STATUS_PENDING;
+ }
+
+ iosb->Status = STATUS_SUCCESS;
+ iosb->Information = ioMode == 3 ? size - 1 : size;
+ return STATUS_SUCCESS;
+}
+
+static NTSTATUS
+KeWaitForSingleObject(
+ void* object,
+ int reason,
+ int mode,
+ BOOLEAN alertable,
+ LARGE_INTEGER* timeout
+)
+{
+ ++waits;
+ Check(
+ object == &eventToken && reason == Executive && mode == KernelMode && !alertable && !timeout && outstanding,
+ "noninterruptible per-request wait"
+ );
+ outstanding = false;
+ pendingIosb->Status = ioMode == 2 ? STATUS_END_OF_FILE : STATUS_SUCCESS;
+ pendingIosb->Information = ioMode == 2 ? 0 : requested;
+ return STATUS_SUCCESS;
+}
+
+#include "common-under-test.inc"
+
+static std::vector
+Image()
+{
+ std::vector image(4096);
+ auto dos = (IMAGE_DOS_HEADER*)image.data();
+ dos->e_magic = IMAGE_DOS_SIGNATURE;
+ dos->e_lfanew = 128;
+ auto nt = (IMAGE_NT_HEADERS64*)(image.data() + 128);
+ nt->Signature = IMAGE_NT_SIGNATURE;
+ nt->OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC;
+ nt->OptionalHeader.SizeOfImage = (ULONG)image.size();
+ auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
+ exports->NumberOfFunctions = 3;
+ exports->NumberOfNames = 2;
+ exports->AddressOfFunctions = 801;
+ exports->AddressOfNames = 901;
+ exports->AddressOfNameOrdinals = 1001;
+ const ULONG functions[] = {2000, 2100, 2200};
+ const ULONG names[] = {1100, 1120};
+ const USHORT ordinals[] = {2, 0};
+ std::memcpy(image.data() + 801, functions, sizeof(functions));
+ std::memcpy(image.data() + 901, names, sizeof(names));
+ std::memcpy(image.data() + 1001, ordinals, sizeof(ordinals));
+ std::memcpy(image.data() + 1100, "Beta", 5);
+ std::memcpy(image.data() + 1120, "Alpha", 6);
+ return image;
+}
+
+static NTSTATUS
+Lookup(
+ std::vector& image,
+ const char* name,
+ PVOID* output
+)
+{
+ ANSI_STRING string = {(USHORT)std::strlen(name), (USHORT)std::strlen(name), (PCHAR)name};
+ exportPointer = image.data() + 512;
+ exportBytes = 256;
+ return DomitoFindExportedFunctionAddress(image.data(), &string, output);
+}
+
+int
+main()
+{
+ auto image = Image();
+ PVOID output = nullptr;
+ Check(Lookup(image, "Beta", &output) == STATUS_SUCCESS && output == image.data() + 2200, "ordinal-indexed EAT");
+ Check(Lookup(image, "aLpHa", &output) == STATUS_SUCCESS && output == image.data() + 2000, "case-insensitive name");
+ Check(Lookup(image, "Alpha", nullptr) == STATUS_SUCCESS, "optional output discovery");
+ Check(Lookup(image, "missing", &output) == STATUS_NOT_FOUND && !output, "notfound clears output");
+ auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
+ const USHORT invalidOrdinal = 3;
+ std::memcpy(image.data() + 1001, &invalidOrdinal, 2);
+ Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT && !output, "ordinal bounded");
+ image = Image();
+ const ULONG forwarder = 600;
+ std::memcpy(image.data() + 801 + 8, &forwarder, 4);
+ Check(Lookup(image, "Beta", &output) == STATUS_NOT_SUPPORTED && !output, "forwarder rejected");
+ image = Image();
+ const ULONG zero = 0;
+ std::memcpy(image.data() + 801 + 8, &zero, 4);
+ Check(Lookup(image, "Beta", &output) == STATUS_NOT_FOUND, "empty EAT slot");
+ image = Image();
+ const ULONG outside = 4096;
+ std::memcpy(image.data() + 801 + 8, &outside, 4);
+ Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "function RVA bounded");
+ image = Image();
+ exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
+ exports->NumberOfNames = 0xffffffff;
+ Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "table multiplication bounded");
+ image = Image();
+ exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
+ exports->AddressOfFunctions = 4090;
+ Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "EAT span bounded");
+ image = Image();
+ const ULONG lateName = 4094;
+ std::memcpy(image.data() + 901, &lateName, 4);
+ image[4094] = 'x';
+ image[4095] = 'x';
+ Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "unterminated name bounded");
+ image = Image();
+ std::memcpy(image.data() + 901, &outside, 4);
+ Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "name RVA bounded");
+ image = Image();
+ ANSI_STRING name = {4, 4, (PCHAR) "Beta"};
+ exportPointer = image.data() + 512;
+ exportBytes = 4096;
+ Check(
+ DomitoFindExportedFunctionAddress(image.data(), &name, &output) == STATUS_INVALID_IMAGE_FORMAT,
+ "export directory span bounded"
+ );
+ output = (void*)1;
+ Check(
+ DomitoFindExportedFunctionAddress(nullptr, &name, &output) == STATUS_INVALID_PARAMETER && !output,
+ "invalid arguments clear output"
+ );
+ char buffer[32];
+
+ for (ioMode = 0; ioMode <= 6; ++ioMode)
+ {
+ created = referenced = closed = dereferenced = reads = waits = 0;
+ outstanding = false;
+ expectedOffset = 123;
+ LARGE_INTEGER offset;
+ offset.QuadPart = expectedOffset;
+ NTSTATUS status = DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), &offset);
+ const NTSTATUS expected[] = {
+ STATUS_SUCCESS,
+ STATUS_SUCCESS,
+ STATUS_END_OF_FILE,
+ STATUS_END_OF_FILE,
+ STATUS_INSUFFICIENT_RESOURCES,
+ STATUS_ACCESS_DENIED,
+ STATUS_ACCESS_DENIED
+ };
+ Check(status == expected[ioMode], "read status and exact-byte contract");
+ Check(created == closed && referenced == dereferenced && !outstanding, "event lifetime balanced");
+ Check(waits == ((ioMode == 1 || ioMode == 2) ? 1u : 0u), "wait pending only");
+ }
+
+ ioMode = 0;
+ expectedOffset = 0;
+ Check(DomitoReadFile(&fileToken, buffer, sizeof(buffer)) == STATUS_SUCCESS, "zero-offset wrapper");
+ LARGE_INTEGER offset = {};
+ offset.QuadPart = -1;
+ Check(
+ DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), &offset) == STATUS_INVALID_PARAMETER,
+ "negative offset rejected"
+ );
+ Check(
+ DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), nullptr) == STATUS_INVALID_PARAMETER,
+ "null offset rejected"
+ );
+ offset.QuadPart = 0;
+ Check(DomitoReadFileAt(&fileToken, nullptr, 0, &offset) == STATUS_SUCCESS, "zero byte read");
+ Check(DomitoReadFileAt(&fileToken, nullptr, 1, &offset) == STATUS_INVALID_PARAMETER, "null buffer rejected");
+ std::printf("PASS: %u common-helper checks (mock kernel I/O)\n", checks);
+}
diff --git a/tests/consumer-helper-tests.cpp b/tests/consumer-helper-tests.cpp
new file mode 100644
index 0000000..4cdbb06
--- /dev/null
+++ b/tests/consumer-helper-tests.cpp
@@ -0,0 +1,967 @@
+#define WIN32_NO_STATUS
+#include
+#undef WIN32_NO_STATUS
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
+#define PASSIVE_LEVEL 0
+#define PAGED_CODE() ((void)0)
+#undef NTDDI_VERSION
+#define NTDDI_VERSION 0x0a000008
+#undef NTDDI_WIN10
+#define NTDDI_WIN10 0x0a000000
+#define DOMITO_POOL_TAG 0x446f6d69
+#define MAXUSHORT 0xffff
+#define PagedPool 1
+#define KernelMode 0
+#define ProcessImageFileName 27
+#define OBJ_KERNEL_HANDLE 0x200
+#define OBJ_CASE_INSENSITIVE 0x40
+#define FILE_NON_DIRECTORY_FILE 0x40
+#define FILE_SYNCHRONOUS_IO_NONALERT 0x20
+typedef void* PEPROCESS;
+typedef void* PFILE_OBJECT;
+typedef const UCHAR* PCUCHAR;
+
+typedef struct _UNICODE_STRING
+{
+ USHORT Length;
+ USHORT MaximumLength;
+ PWSTR Buffer;
+} UNICODE_STRING, *PUNICODE_STRING;
+
+typedef struct _ANSI_STRING
+{
+ USHORT Length;
+ USHORT MaximumLength;
+ PCHAR Buffer;
+} ANSI_STRING, *PANSI_STRING;
+
+struct OBJECT_ATTRIBUTES
+{
+ ULONG Attributes;
+ PUNICODE_STRING ObjectName;
+};
+
+struct CLIENT_ID
+{
+ HANDLE UniqueProcess;
+ HANDLE UniqueThread;
+};
+
+struct IO_STATUS_BLOCK
+{
+ NTSTATUS Status;
+ ULONG_PTR Information;
+};
+
+#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f), (a)->ObjectName = (n))
+#define RTL_CONSTANT_STRING(s) {sizeof(s) - sizeof((s)[0]), sizeof(s), const_cast(s)}
+typedef PVOID(NTAPI* PMINCRYPT_ALLOCATE_ROUTINE)(SIZE_T);
+typedef PVOID NTAPI MINCRYPT_ALLOCATE_ROUTINE(SIZE_T);
+#include "consumer-api.inc"
+
+static unsigned checks;
+static unsigned live;
+static unsigned refs;
+static unsigned derefs;
+static unsigned closed;
+static unsigned validated;
+static unsigned opened;
+static unsigned queried;
+static int failure;
+static int cryptoFailure;
+static NTSTATUS moduleStatus = STATUS_SUCCESS;
+static NTSTATUS exportStatus = STATUS_SUCCESS;
+static NTSTATUS validateStatus = STATUS_SUCCESS;
+static int fileToken;
+static int processToken;
+static void* fileTypeValue = &fileToken;
+static void** IoFileObjectType = &fileTypeValue;
+
+static void
+Check(
+ bool value,
+ const char* msg
+)
+{
+ ++checks;
+
+ if (!value)
+ {
+ std::fprintf(stderr, "FAIL: %s\n", msg);
+ std::exit(1);
+ }
+}
+
+static void*
+Allocate(
+ SIZE_T size
+)
+{
+ if (failure == 1)
+ {
+ return nullptr;
+ }
+
+ void* p = std::malloc(size);
+ Check(p != nullptr, "allocation");
+ ++live;
+ std::memset(p, 0xa5, size);
+ return p;
+}
+
+static void
+Release(
+ void* p
+)
+{
+ Check(p && live, "single owner release");
+ --live;
+ std::free(p);
+}
+
+static struct
+{
+ PVOID (*Allocate)(SIZE_T);
+ void (*Free)(PVOID);
+} G_Memory = {Allocate, Release};
+
+static PVOID
+ExAllocatePoolWithTag(
+ int,
+ SIZE_T size,
+ ULONG
+)
+{
+ return Allocate(size);
+}
+
+static void
+ExFreePoolWithTag(
+ void* p,
+ ULONG
+)
+{
+ Release(p);
+}
+
+static void
+ExFreePool(
+ void* p
+)
+{
+ Release(p);
+}
+
+static NTSTATUS
+FakePublisher(
+ MINCERT_BLOB*,
+ PMINCRYPT_ALLOCATE_ROUTINE allocator,
+ PUNICODE_STRING name
+)
+{
+ name->Buffer = static_cast(allocator(12));
+
+ if (!name->Buffer)
+ {
+ return STATUS_INSUFFICIENT_RESOURCES;
+ }
+
+ std::memcpy(name->Buffer, L"hello", 12);
+ name->Length = 10;
+ name->MaximumLength = 12;
+
+ if (failure == 8)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ if (failure == 9)
+ {
+ name->Length = 13;
+ }
+
+ return STATUS_SUCCESS;
+}
+
+static NTSTATUS
+DomitoFindModuleBaseAddress(
+ PANSI_STRING name,
+ PVOID* module
+)
+{
+ Check(name && name->Length, "module name");
+
+ if (NT_SUCCESS(moduleStatus))
+ {
+ *module = &fileToken;
+ }
+
+ return moduleStatus;
+}
+
+static NTSTATUS
+DomitoFindExportedFunctionAddress(
+ PVOID module,
+ PANSI_STRING name,
+ PVOID* routine
+)
+{
+ Check(module == &fileToken && name->Length, "export resolution");
+
+ if (NT_SUCCESS(exportStatus))
+ {
+ *routine = reinterpret_cast(FakePublisher);
+ }
+
+ return exportStatus;
+}
+
+void
+DomitoFreeCiValidationResult(
+ DOMITO_CI_VALIDATION_RESULT* result
+)
+{
+ if (result->PolicyInfo.ChainInfo)
+ {
+ Release(result->PolicyInfo.ChainInfo);
+ }
+
+ *result = {};
+}
+
+NTSTATUS
+DomitoValidateFileObjectEx(
+ PFILE_OBJECT object,
+ UINT32 flags,
+ UINT8 level,
+ DOMITO_CI_SIGNATURE_SELECTION selection,
+ DOMITO_CI_VALIDATION_RESULT* result
+)
+{
+ ++validated;
+ Check(
+ object == &fileToken && flags == 0x20 && level == 7 && selection == DomitoCiSignatureSelectionNative,
+ "CI arguments unchanged"
+ );
+ Check(!result->PolicyInfo.ChainInfo, "result reset before native call");
+ result->PolicyInfo.ChainInfo = static_cast(Allocate(1));
+ return validateStatus;
+}
+
+static NTSTATUS
+ObReferenceObjectByHandle(
+ HANDLE handle,
+ ACCESS_MASK access,
+ void* type,
+ int mode,
+ void** object,
+ void*
+)
+{
+ Check(
+ handle == &fileToken && access == FILE_READ_DATA && type == *IoFileObjectType && mode == KernelMode,
+ "trusted handle reference"
+ );
+
+ if (failure == 2)
+ {
+ return STATUS_ACCESS_DENIED;
+ }
+
+ ++refs;
+ *object = &fileToken;
+ return STATUS_SUCCESS;
+}
+
+static void
+ObDereferenceObject(
+ void* object
+)
+{
+ Check(object == &fileToken, "file dereference");
+ ++derefs;
+}
+
+static NTSTATUS
+ZwOpenFile(
+ HANDLE* handle,
+ ACCESS_MASK access,
+ OBJECT_ATTRIBUTES* a,
+ IO_STATUS_BLOCK*,
+ ULONG share,
+ ULONG options
+)
+{
+ ++opened;
+ Check(
+ access == (FILE_READ_DATA | SYNCHRONIZE) && a->Attributes == (OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE) &&
+ share == FILE_SHARE_READ && options == (FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT),
+ "read-only stable path open"
+ );
+
+ if (failure == 3)
+ {
+ return STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+
+ *handle = &fileToken;
+ return STATUS_SUCCESS;
+}
+
+static void
+ZwClose(
+ HANDLE handle
+)
+{
+ Check(handle == &fileToken || handle == &processToken, "close owned handle");
+ ++closed;
+}
+
+static void
+FillName(
+ PUNICODE_STRING name
+)
+{
+ name->Length = 8;
+ name->MaximumLength = 10;
+ name->Buffer = reinterpret_cast(name + 1);
+ std::memcpy(name->Buffer, L"test", 10);
+}
+
+static NTSTATUS
+SeLocateProcessImageName(
+ PEPROCESS process,
+ PUNICODE_STRING* image
+)
+{
+ Check(process == &processToken, "referenced process passed unchanged");
+
+ if (failure == 4)
+ {
+ return STATUS_NOT_FOUND;
+ }
+
+ // OS allocations are independent of configurable Domito allocator failures.
+ *image = static_cast(std::malloc(sizeof(UNICODE_STRING) + 10));
+ ++live;
+ FillName(*image);
+
+ if (failure == 5)
+ {
+ (*image)->Length = 9;
+ }
+
+ return STATUS_SUCCESS;
+}
+
+static NTSTATUS
+ZwOpenProcess(
+ HANDLE* process,
+ ACCESS_MASK access,
+ OBJECT_ATTRIBUTES* a,
+ CLIENT_ID* cid
+)
+{
+ Check(
+ access == 0x1000 && a->Attributes == OBJ_KERNEL_HANDLE && cid->UniqueProcess == ULongToHandle(123),
+ "PID least privilege kernel open"
+ );
+
+ if (failure == 6)
+ {
+ return STATUS_ACCESS_DENIED;
+ }
+
+ *process = &processToken;
+ return STATUS_SUCCESS;
+}
+
+static NTSTATUS
+QueryProcess(
+ HANDLE process,
+ int info,
+ PVOID buffer,
+ ULONG size,
+ PULONG returned
+)
+{
+ ++queried;
+ Check(process == &processToken && info == ProcessImageFileName, "PID query bound to one handle");
+ *returned = sizeof(UNICODE_STRING) + 10;
+
+ if (!buffer)
+ {
+ return failure == 10 ? STATUS_SUCCESS : STATUS_INFO_LENGTH_MISMATCH;
+ }
+
+ Check(size == *returned, "sized name allocation");
+ FillName(static_cast(buffer));
+
+ if (failure == 7)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ if (failure == 11)
+ {
+ static_cast(buffer)->Buffer = reinterpret_cast(1);
+ }
+
+ return STATUS_SUCCESS;
+}
+
+static struct
+{
+ NTSTATUS (*ZwQueryInformationProcess)(
+ HANDLE,
+ int,
+ PVOID,
+ ULONG,
+ PULONG
+ );
+} G_Common = {QueryProcess};
+
+static NTSTATUS
+OpenAlgorithm(
+ BCRYPT_ALG_HANDLE* a,
+ LPCWSTR n,
+ LPCWSTR p,
+ ULONG f
+)
+{
+ return cryptoFailure == 1 ? STATUS_UNSUCCESSFUL : BCryptOpenAlgorithmProvider(a, n, p, f);
+}
+
+static NTSTATUS
+CreateHash(
+ BCRYPT_ALG_HANDLE a,
+ BCRYPT_HASH_HANDLE* h,
+ PUCHAR o,
+ ULONG os,
+ PUCHAR s,
+ ULONG ss,
+ ULONG f
+)
+{
+ return cryptoFailure == 2 ? STATUS_UNSUCCESSFUL : BCryptCreateHash(a, h, o, os, s, ss, f);
+}
+
+static NTSTATUS
+HashData(
+ BCRYPT_HASH_HANDLE h,
+ PUCHAR p,
+ ULONG n,
+ ULONG f
+)
+{
+ return cryptoFailure == 3 ? STATUS_UNSUCCESSFUL : BCryptHashData(h, p, n, f);
+}
+
+static NTSTATUS
+FinishHash(
+ BCRYPT_HASH_HANDLE h,
+ PUCHAR p,
+ ULONG n,
+ ULONG f
+)
+{
+ if (cryptoFailure == 4)
+ {
+ std::memset(p, 0xff, n);
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ return BCryptFinishHash(h, p, n, f);
+}
+
+#define BCryptOpenAlgorithmProvider OpenAlgorithm
+#define BCryptCreateHash CreateHash
+#define BCryptHashData HashData
+#define BCryptFinishHash FinishHash
+#ifdef DOMITO_TEST_OTHER_ARCH
+#undef _M_AMD64
+#undef _AMD64_
+#endif
+#include "consumer-under-test.inc"
+#undef BCryptOpenAlgorithmProvider
+#undef BCryptCreateHash
+#undef BCryptHashData
+#undef BCryptFinishHash
+
+using Bytes = std::vector;
+
+static Bytes
+Der(
+ UCHAR tag,
+ const Bytes& contents
+)
+{
+ Bytes b = {tag};
+
+ if (contents.size() < 128)
+ {
+ b.push_back(static_cast(contents.size()));
+ }
+ else
+ {
+ Check(contents.size() < 256, "fixture DER bound");
+ b.push_back(0x81);
+ b.push_back(static_cast(contents.size()));
+ }
+
+ b.insert(b.end(), contents.begin(), contents.end());
+ return b;
+}
+
+static Bytes
+Join(
+ std::initializer_list values
+)
+{
+ Bytes b;
+
+ for (const auto& v : values)
+ {
+ b.insert(b.end(), v.begin(), v.end());
+ }
+
+ return b;
+}
+
+static Bytes
+Fixture(
+ bool version
+)
+{
+ Bytes sig = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 11}), Der(5, {})}));
+ Bytes rsa = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 1}), Der(5, {})}));
+ Bytes name = Der(0x30, Der(0x31, Der(0x30, Join({Der(6, {0x55, 4, 3}), Der(0x0c, {'t', 'e', 's', 't'})}))));
+ const std::string date = "260101000000Z";
+ Bytes time = Der(0x17, Bytes(date.begin(), date.end()));
+ Bytes key = Der(0x30, Join({Der(2, {0, 0xab, 0xcd}), Der(2, {1, 0, 1})}));
+ key.insert(key.begin(), 0);
+ Bytes spki = Der(0x30, Join({rsa, Der(3, key)}));
+ Bytes tbs = Join(
+ {version ? Der(0xa0, Der(2, {2})) : Bytes{}, Der(2, {1}), sig, name, Der(0x30, Join({time, time})), name, spki}
+ );
+ return Der(0x30, Join({Der(0x30, tbs), sig, Der(3, {0, 0xab, 0xcd})}));
+}
+
+static void
+TestCertificates()
+{
+ for (bool v : {false, true})
+ {
+ auto cert = Fixture(v);
+ DOMITO_CERTIFICATE_SPKI spki = {};
+ Check(
+ NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast(cert.size()), &spki)),
+ "v1/v3 extraction"
+ );
+ Check(
+ spki.Content.Data > spki.Encoded.Data && spki.Content.Size < spki.Encoded.Size,
+ "content and complete TLV differ"
+ );
+ auto context = CertCreateCertificateContext(X509_ASN_ENCODING, cert.data(), static_cast(cert.size()));
+ Check(context != nullptr, "Crypt32 independently decodes fixture certificate");
+ BYTE* encoded = nullptr;
+ DWORD size = 0;
+ Check(
+ CryptEncodeObjectEx(
+ X509_ASN_ENCODING,
+ X509_PUBLIC_KEY_INFO,
+ &context->pCertInfo->SubjectPublicKeyInfo,
+ CRYPT_ENCODE_ALLOC_FLAG,
+ nullptr,
+ &encoded,
+ &size
+ ) != FALSE,
+ "Crypt32 independent SPKI encoding"
+ );
+ Check(
+ size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size),
+ "extracted complete SPKI equals Crypt32"
+ );
+ LocalFree(encoded);
+ CertFreeCertificateContext(context);
+ UCHAR full[64];
+ UCHAR legacy[64];
+ UCHAR whole[64];
+ UCHAR oracle[64];
+ ULONG a = 64;
+ ULONG b = 64;
+ ULONG c = 64;
+ DWORD d = 64;
+ Check(
+ NT_SUCCESS(DomitoHashCertificate(
+ cert.data(), static_cast(cert.size()), DomitoCertificateHashSpkiDer, CALG_SHA_256, full, &a
+ )),
+ "full SPKI hash"
+ );
+ Check(
+ NT_SUCCESS(DomitoHashCertificate(
+ cert.data(),
+ static_cast(cert.size()),
+ DomitoCertificateHashSpkiContentLegacy,
+ CALG_SHA_256,
+ legacy,
+ &b
+ )),
+ "legacy content SPKI hash"
+ );
+ Check(a == 32 && b == 32 && std::memcmp(full, legacy, 32) != 0, "explicit SPKI conventions differ");
+ Check(
+ CryptHashCertificate2(
+ BCRYPT_SHA256_ALGORITHM, 0, nullptr, spki.Content.Data, spki.Content.Size, oracle, &d
+ ) != FALSE &&
+ d == b && !std::memcmp(legacy, oracle, d),
+ "legacy hash matches exact content-only oracle"
+ );
+ Check(
+ NT_SUCCESS(DomitoHashCertificate(
+ cert.data(), static_cast(cert.size()), DomitoCertificateHashDer, CALG_SHA_256, whole, &c
+ )),
+ "DER thumbprint"
+ );
+ d = 64;
+ Check(
+ CryptHashCertificate2(
+ BCRYPT_SHA256_ALGORITHM, 0, nullptr, cert.data(), static_cast(cert.size()), oracle, &d
+ ) != FALSE &&
+ d == c && !std::memcmp(whole, oracle, d),
+ "thumbprint matches Crypt32"
+ );
+
+ for (ULONG i = 0; i < cert.size(); ++i)
+ {
+ spki.Encoded = {cert.data(), 99};
+ Check(!NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), i, &spki)), "every truncated certificate rejected");
+ Check(!spki.Encoded.Data && !spki.Content.Data, "failed extraction clears borrowed views");
+ }
+
+ cert.push_back(0);
+ Check(
+ !NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast(cert.size()), &spki)),
+ "trailing byte rejected"
+ );
+ }
+
+ for (auto malformed :
+ {Bytes{0x30, 0x80, 0, 0},
+ Bytes{0x30, 0x81, 1, 0},
+ Bytes{0x30, 0x85, 1, 2, 3, 4, 5},
+ Bytes{0x30, 0x84, 0xff, 0xff, 0xff, 0xff}})
+ {
+ DOMITO_CERTIFICATE_SPKI spki = {};
+ Check(
+ !NT_SUCCESS(DomitoGetCertificateSpki(malformed.data(), static_cast(malformed.size()), &spki)),
+ "bad DER lengths rejected"
+ );
+ }
+
+ const UCHAR abc[] = {'a', 'b', 'c'};
+ UCHAR hash[64];
+
+ for (ALG_ID alg : {CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512})
+ {
+ ULONG n = 0;
+ Check(DomitoHashBuffer(abc, 3, alg, nullptr, &n) == STATUS_BUFFER_TOO_SMALL, "hash sizing");
+ Check(n == 20 || n == 32 || n == 48 || n == 64, "known output size");
+ ULONG cap = n - 1;
+ Check(DomitoHashBuffer(abc, 3, alg, hash, &cap) == STATUS_BUFFER_TOO_SMALL && cap == n, "short hash capacity");
+ cap = 64;
+ Check(NT_SUCCESS(DomitoHashBuffer(nullptr, 0, alg, hash, &cap)), "empty hash");
+ }
+
+ for (int fail = 1; fail <= 4; ++fail)
+ {
+ cryptoFailure = fail;
+ ULONG n = 64;
+ std::memset(hash, 0xff, 64);
+ Check(!NT_SUCCESS(DomitoHashBuffer(abc, 3, CALG_SHA_256, hash, &n)) && !n, "CNG failure propagated");
+
+ for (int i = 0; i < 32; ++i)
+ {
+ Check(!hash[i], "CNG failure digest erased");
+ }
+ }
+
+ cryptoFailure = 0;
+
+ for (unsigned byte = 0; byte < 256; ++byte)
+ {
+ UCHAR in = static_cast(byte);
+ UCHAR out = 0;
+ WCHAR hex[3];
+ Check(NT_SUCCESS(DomitoEncodeHex(&in, 1, hex, 3)), "hex encoding");
+ UNICODE_STRING str = {4, 6, hex};
+ Check(NT_SUCCESS(DomitoDecodeHex(&str, &out, 1)) && out == in, "all byte values hex roundtrip");
+ }
+
+ for (WCHAR ch : {L'g', L'/', L':', L' ', L'\0', L'\u1234'})
+ {
+ WCHAR text[] = {L'0', ch};
+ UNICODE_STRING str = {4, 4, text};
+ UCHAR out = 9;
+ Check(
+ DomitoDecodeHex(&str, &out, 1) == STATUS_INVALID_PARAMETER && !out,
+ "invalid hex digit rejected and output zeroed"
+ );
+ }
+
+ WCHAR shortHex[2];
+ Check(DomitoEncodeHex(abc, 3, shortHex, 2) == STATUS_BUFFER_TOO_SMALL && !shortHex[0], "short hex buffer cleared");
+
+ // Put every truncated prefix immediately before an inaccessible page.
+ SYSTEM_INFO system = {};
+ GetSystemInfo(&system);
+ const SIZE_T page = system.dwPageSize;
+ auto guarded = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
+ Check(guarded != nullptr, "guard-page allocation");
+ DWORD old = 0;
+ Check(VirtualProtect(guarded + page, page, PAGE_NOACCESS, &old) != FALSE, "guard page installed");
+ const auto fixture = Fixture(true);
+
+ for (ULONG n = 0; n < fixture.size(); ++n)
+ {
+ UCHAR* input = guarded + page - n;
+ std::memcpy(input, fixture.data(), n);
+ DOMITO_CERTIFICATE_SPKI spki = {};
+ Check(!NT_SUCCESS(DomitoGetCertificateSpki(input, n, &spki)), "truncated DER at guard page rejected");
+ }
+
+ VirtualFree(guarded, 0, MEM_RELEASE);
+
+ // Read-only optional corpus: exercises real long-length RSA/ECC certificates.
+ HCERTSTORE store = CertOpenSystemStoreW(0, L"ROOT");
+
+ if (store)
+ {
+ PCCERT_CONTEXT context = nullptr;
+ unsigned count = 0;
+ while (count < 16 && (context = CertEnumCertificatesInStore(store, context)) != nullptr)
+ {
+ ++count;
+ DOMITO_CERTIFICATE_SPKI spki = {};
+ Check(
+ NT_SUCCESS(DomitoGetCertificateSpki(context->pbCertEncoded, context->cbCertEncoded, &spki)),
+ "real certificate SPKI extraction"
+ );
+ BYTE* encoded = nullptr;
+ DWORD size = 0;
+ Check(
+ CryptEncodeObjectEx(
+ X509_ASN_ENCODING,
+ X509_PUBLIC_KEY_INFO,
+ &context->pCertInfo->SubjectPublicKeyInfo,
+ CRYPT_ENCODE_ALLOC_FLAG,
+ nullptr,
+ &encoded,
+ &size
+ ) != FALSE,
+ "real Crypt32 SPKI encoding"
+ );
+ Check(
+ size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size),
+ "real certificate SPKI equals Crypt32"
+ );
+ LocalFree(encoded);
+ }
+
+ if (context)
+ {
+ CertFreeCertificateContext(context);
+ }
+
+ CertCloseStore(store, 0);
+ }
+}
+
+static void
+TestOwnership()
+{
+ DOMITO_CI_VALIDATION_RESULT result = {};
+ UNICODE_STRING path = {8, 10, const_cast(L"\\foo")};
+
+ for (int fail : {0, 2, 3})
+ {
+ failure = fail;
+ const auto r = refs, d = derefs, c = closed;
+ result.PolicyInfo.ChainInfo = static_cast(Allocate(1));
+ NTSTATUS s = DomitoValidateFilePathEx(&path, 0x20, 7, DomitoCiSignatureSelectionNative, &result);
+ Check(NT_SUCCESS(s) == (fail == 0), "path/reference failure returned");
+ Check(
+ refs - r == derefs - d && closed - c == (fail == 3 ? 0u : 1u),
+ "all references and owned path handles released"
+ );
+ DomitoFreeCiValidationResult(&result);
+ Check(!live, "path no allocations leaked");
+ }
+
+ failure = 0;
+ validateStatus = STATUS_INVALID_IMAGE_HASH;
+ const auto c = closed;
+ Check(
+ DomitoValidateFileHandleEx(&fileToken, 0x20, 7, DomitoCiSignatureSelectionNative, &result) == validateStatus,
+ "CI failure retained"
+ );
+ Check(closed == c && result.PolicyInfo.ChainInfo, "borrowed handle open and diagnostics retained");
+ DomitoFreeCiValidationResult(&result);
+ validateStatus = STATUS_SUCCESS;
+
+ for (int fail : {0, 1, 4, 5})
+ {
+ failure = fail;
+ PUNICODE_STRING name = reinterpret_cast(1);
+ NTSTATUS s = DomitoGetProcessImageNameEx(&processToken, &name);
+ Check(NT_SUCCESS(s) == (fail == 0), "PEPROCESS lookup failure");
+
+ if (NT_SUCCESS(s))
+ {
+ Check(
+ name->Buffer == reinterpret_cast(name + 1) && name->Length == 8,
+ "one contiguous process-name allocation"
+ );
+ }
+ else
+ {
+ Check(!name, "failed process name null");
+ }
+
+ DomitoFreeProcessImageName(name);
+ Check(!live, "process-name allocations released");
+ }
+
+ for (int fail : {0, 1, 6, 7, 10, 11})
+ {
+ failure = fail;
+ PUNICODE_STRING name = reinterpret_cast(1);
+ NTSTATUS s = DomitoGetProcessImageName(123, &name);
+ Check(NT_SUCCESS(s) == (fail == 0), "PID query failure");
+
+ if (!NT_SUCCESS(s))
+ {
+ Check(!name, "PID failure never leaves freed pointer");
+ }
+
+ DomitoFreeProcessImageName(name);
+ Check(!live, "PID query cleanup");
+ }
+
+ failure = 0;
+ MINCERT_BLOB blob = {1, reinterpret_cast(&fileToken)};
+
+ for (int fail : {0, 1, 8, 9})
+ {
+ failure = fail;
+ UNICODE_STRING name = {};
+ NTSTATUS s = DomitoGetCertificatePublisherName(&blob, &name);
+ Check(NT_SUCCESS(s) == (fail == 0), "publisher error propagation");
+ DomitoFreeCertificatePublisherName(&name);
+ DomitoFreeCertificatePublisherName(&name);
+ Check(!live && !name.Buffer, "publisher matching free including partial failure");
+ }
+
+ failure = 0;
+ ANSI_STRING module = RTL_CONSTANT_STRING("module");
+ ANSI_STRING name = RTL_CONSTANT_STRING("export");
+ PVOID address = reinterpret_cast(1);
+ moduleStatus = STATUS_NOT_FOUND;
+ Check(
+ DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_FOUND && !address,
+ "missing module nulls export"
+ );
+ moduleStatus = STATUS_SUCCESS;
+ exportStatus = STATUS_NOT_SUPPORTED;
+ address = reinterpret_cast(1);
+ Check(
+ DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_SUPPORTED && !address,
+ "unsupported forwarder status preserved"
+ );
+ exportStatus = STATUS_SUCCESS;
+}
+
+static void
+TestChains()
+{
+ alignas(8) UCHAR storage[512] = {};
+ auto chain = reinterpret_cast(storage);
+ chain->Size = sizeof(storage);
+ chain->ChainElements = reinterpret_cast(storage + 56);
+ chain->NumberOfChainElement = 2;
+
+ for (ULONG i = 0; i < 2; ++i)
+ {
+ chain->ChainElements[i].Certificate = {1, storage + 400 + i};
+ chain->ChainElements[i].HashSize = 32;
+ }
+
+ MINCRYPT_POLICY_INFO policy = {};
+ policy.Size = sizeof(policy);
+ policy.ChainInfo = chain;
+ DOMITO_CI_CHAIN_VIEW view = {};
+#ifdef DOMITO_TEST_OTHER_ARCH
+ Check(
+ DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view) == STATUS_NOT_SUPPORTED && !view.Count,
+ "non-x64 rejects unverified layout"
+ );
+#else
+ Check(
+ NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && view.Count == 2,
+ "minimal historical 56-byte chain prefix"
+ );
+ // The last certificate is an arbitrary byte, not a proven self-signed root.
+ chain->ChainElements[1].Certificate.Data = storage + 512;
+ Check(
+ !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && !view.Count,
+ "child span past allocation rejected"
+ );
+ chain->ChainElements[1].Certificate.Data = storage + 401;
+ chain->ChainElements[0].Subject = {reinterpret_cast(storage + 500), 13};
+ Check(
+ !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
+ "malformed name span rejected"
+ );
+ chain->ChainElements[0].Subject = {};
+ chain->NumberOfChainElement = 0xffffffff;
+ Check(
+ !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
+ "element multiplication overflow rejected"
+ );
+ chain->NumberOfChainElement = 2;
+ chain->ChainElements[0].HashSize = 65;
+ Check(
+ !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
+ "oversize CI element hash rejected"
+ );
+ chain->ChainElements[0].HashSize = 32;
+
+ for (ULONG n = 0; n < 56; ++n)
+ {
+ chain->Size = n;
+ Check(
+ !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
+ "short historical header rejected"
+ );
+ }
+#endif
+}
+
+int
+main()
+{
+ TestCertificates();
+ TestOwnership();
+ TestChains();
+ Check(!live, "all allocations released");
+ std::printf("PASS: %u consumer-helper checks, no live allocations\n", checks);
+}
diff --git a/tests/digest-tests.cpp b/tests/digest-tests.cpp
new file mode 100644
index 0000000..b425464
--- /dev/null
+++ b/tests/digest-tests.cpp
@@ -0,0 +1,1170 @@
+// User-mode regression harness. The runner extracts the production functions
+// unchanged; pool, file-I/O, and CI calls are test doubles. BCrypt is real CNG.
+#define WIN32_NO_STATUS
+#include
+#undef WIN32_NO_STATUS
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
+#define ASSERT(x) Check(!!(x), #x)
+#define DOMITO_POOL_TAG 0x446f6d69
+#define NonPagedPoolNx 0
+#define PagedPool 1
+#define PAGED_CODE() ((void)0)
+#define MINCRYPT_SHA1_LENGTH 20
+#define MINCRYPT_SHA256_LENGTH 32
+#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
+#define DOMITO_CI_SOURCE_EMBEDDED 1UL
+#define DOMITO_CI_SOURCE_CATALOG 2UL
+typedef const UCHAR* PCUCHAR;
+
+static unsigned checks;
+static unsigned allocations;
+static unsigned ciCalls;
+static unsigned hashCalls;
+static int failOperation;
+static int failHashCall;
+static ULONG_PTR bytesRead;
+static bool allocatePolicies;
+static NTSTATUS embeddedResult = STATUS_SUCCESS;
+static NTSTATUS catalogResults[2] = {STATUS_SUCCESS, STATUS_SUCCESS};
+static unsigned catalogCalls;
+static ULONG expectedCatalogMask = DOMITO_CI_LEGACY_CATALOG_POLICY_BITS;
+static BOOLEAN expectedTrustedMarker;
+static LONGLONG observedFileSize = 1056;
+
+static void
+Check(
+ bool ok,
+ const char* message
+)
+{
+ ++checks;
+
+ if (!ok)
+ {
+ std::fprintf(stderr, "FAIL: %s\n", message);
+ std::exit(1);
+ }
+}
+
+static PVOID
+ExAllocatePoolWithTag(
+ int,
+ SIZE_T size,
+ ULONG
+)
+{
+ if (failOperation == 4)
+ {
+ return nullptr;
+ }
+
+ void* p = HeapAlloc(GetProcessHeap(), 0, size);
+
+ if (p)
+ {
+ ++allocations;
+ }
+
+ return p;
+}
+
+static void
+ExFreePoolWithTag(
+ PVOID p,
+ ULONG
+)
+{
+ Check(p != nullptr && allocations != 0, "allocation ownership");
+ --allocations;
+ Check(HeapFree(GetProcessHeap(), 0, p) != FALSE, "heap free");
+}
+
+static NTSTATUS
+TestCreateHash(
+ BCRYPT_ALG_HANDLE a,
+ BCRYPT_HASH_HANDLE* h,
+ PUCHAR o,
+ ULONG os,
+ PUCHAR s,
+ ULONG ss,
+ ULONG f
+)
+{
+ if (failOperation == 1)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ return BCryptCreateHash(a, h, o, os, s, ss, f);
+}
+
+static NTSTATUS
+TestHashData(
+ BCRYPT_HASH_HANDLE h,
+ PUCHAR b,
+ ULONG n,
+ ULONG f
+)
+{
+ ++hashCalls;
+
+ if (failOperation == 2 && hashCalls == (unsigned)failHashCall)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ return BCryptHashData(h, b, n, f);
+}
+
+static NTSTATUS
+TestFinishHash(
+ BCRYPT_HASH_HANDLE h,
+ PUCHAR b,
+ ULONG n,
+ ULONG f
+)
+{
+ if (failOperation == 3)
+ {
+ return STATUS_UNSUCCESSFUL;
+ }
+
+ return BCryptFinishHash(h, b, n, f);
+}
+
+// Minimal dependencies for the legacy wrapper; no actual CI code is invoked.
+struct MINCRYPT_POLICY_INFO
+{
+ ULONG Size;
+ PVOID ChainInfo;
+};
+
+struct KAPC_STATE
+{
+ ULONG unused;
+};
+
+struct IO_STATUS_BLOCK
+{
+ NTSTATUS Status;
+ ULONG_PTR Information;
+};
+
+static PVOID PsInitialSystemProcess = nullptr;
+
+static void
+CiFreePolicyInfo(
+ MINCRYPT_POLICY_INFO* p
+)
+{
+ if (p->ChainInfo)
+ {
+ ExFreePoolWithTag(p->ChainInfo, DOMITO_POOL_TAG);
+ }
+
+ *p = {};
+}
+
+static void
+PopulatePolicies(
+ MINCRYPT_POLICY_INFO* policy,
+ MINCRYPT_POLICY_INFO* timestamp
+)
+{
+ Check(!policy->ChainInfo && !timestamp->ChainInfo, "previous CI attempt released before reuse");
+
+ if (allocatePolicies)
+ {
+ policy->Size = timestamp->Size = sizeof(*policy);
+ policy->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG);
+ timestamp->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG);
+ }
+}
+
+static void
+KeStackAttachProcess(
+ PVOID,
+ KAPC_STATE*
+)
+{
+}
+
+static void
+KeUnstackDetachProcess(
+ KAPC_STATE*
+)
+{
+}
+
+typedef PVOID PFILE_OBJECT;
+static PVOID fileObjectType;
+static PVOID* IoFileObjectType = &fileObjectType;
+#define KernelMode 0
+
+static NTSTATUS
+ObReferenceObjectByHandle(
+ HANDLE,
+ ULONG,
+ PVOID,
+ int,
+ PVOID* object,
+ PVOID
+)
+{
+ *object = &fileObjectType;
+ return STATUS_SUCCESS;
+}
+
+static void
+ObDereferenceObject(
+ PVOID
+)
+{
+}
+
+static NTSTATUS
+FsRtlGetFileSize(
+ PFILE_OBJECT,
+ LARGE_INTEGER* size
+)
+{
+ size->QuadPart = observedFileSize;
+ return STATUS_SUCCESS;
+}
+
+static NTSTATUS
+DomitoReadFileAt(
+ HANDLE,
+ PVOID buffer,
+ ULONG size,
+ const LARGE_INTEGER* offset
+)
+{
+ Check(offset->QuadPart == 1024, "explicit certificate read offset");
+ std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size));
+ return bytesRead == size ? STATUS_SUCCESS : STATUS_END_OF_FILE;
+}
+
+static NTSTATUS
+CiCheckSignedFile(
+ PVOID,
+ UINT32,
+ ALG_ID,
+ PVOID,
+ UINT32,
+ MINCRYPT_POLICY_INFO* policy,
+ LARGE_INTEGER* time,
+ MINCRYPT_POLICY_INFO* timestamp
+)
+{
+ ++ciCalls;
+ Check(time->QuadPart == 0, "fresh signing time for embedded validation");
+ PopulatePolicies(policy, timestamp);
+ time->QuadPart = 123;
+ return embeddedResult;
+}
+
+static NTSTATUS
+CiVerifyHashInCatalog(
+ PVOID,
+ UINT32,
+ ALG_ID,
+ BOOLEAN reload,
+ UINT32 trusted,
+ UINT32 mask,
+ MINCRYPT_POLICY_INFO* policy,
+ PVOID,
+ LARGE_INTEGER* time,
+ MINCRYPT_POLICY_INFO* timestamp
+)
+{
+ ++ciCalls;
+ Check(mask == expectedCatalogMask && trusted == expectedTrustedMarker, "explicit catalog policy forwarded");
+ Check(reload == (catalogCalls != 0), "catalog reload sequence");
+ Check(time->QuadPart == 0, "fresh signing time for catalog attempt");
+ PopulatePolicies(policy, timestamp);
+ time->QuadPart = 456;
+ Check(catalogCalls < 2, "bounded catalog retries");
+ return catalogResults[catalogCalls++];
+}
+
+#define BCryptCreateHash TestCreateHash
+#define BCryptHashData TestHashData
+#define BCryptFinishHash TestFinishHash
+#include "digest-under-test.inc"
+#undef BCryptCreateHash
+#undef BCryptHashData
+#undef BCryptFinishHash
+
+static std::vector
+Image(
+ bool pe64,
+ ULONG nt = 128,
+ bool certificate = false
+)
+{
+ std::vector b((size_t)nt + 896 + (certificate ? 32 : 0), 0);
+ IMAGE_DOS_HEADER dos = {};
+ dos.e_magic = IMAGE_DOS_SIGNATURE;
+ dos.e_lfanew = (LONG)nt;
+ std::memcpy(b.data(), &dos, sizeof(dos));
+ ULONG signature = IMAGE_NT_SIGNATURE;
+ std::memcpy(b.data() + nt, &signature, sizeof(signature));
+ IMAGE_FILE_HEADER f = {};
+ f.NumberOfSections = 1;
+ f.Machine = pe64 ? IMAGE_FILE_MACHINE_AMD64 : IMAGE_FILE_MACHINE_I386;
+ f.SizeOfOptionalHeader = pe64 ? sizeof(IMAGE_OPTIONAL_HEADER64) : sizeof(IMAGE_OPTIONAL_HEADER32);
+ std::memcpy(b.data() + nt + 4, &f, sizeof(f));
+ IMAGE_OPTIONAL_HEADER64 h64 = {};
+ IMAGE_OPTIONAL_HEADER32 h32 = {};
+ h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC;
+ h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
+ h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16;
+ h64.SectionAlignment = h32.SectionAlignment = 4096;
+ h64.FileAlignment = h32.FileAlignment = 512;
+ h64.SizeOfImage = h32.SizeOfImage = 8192;
+ h64.Subsystem = h32.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI;
+ h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384;
+
+ if (certificate)
+ {
+ h64.DataDirectory[4] = h32.DataDirectory[4] = {nt + 896, 32};
+ ULONG length = 32;
+ std::memcpy(b.data() + nt + 896, &length, sizeof(length));
+ USHORT revision = 0x0200;
+ USHORT type = WIN_CERT_TYPE_PKCS_SIGNED_DATA;
+ std::memcpy(b.data() + nt + 900, &revision, 2);
+ std::memcpy(b.data() + nt + 902, &type, 2);
+ }
+
+ std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader);
+ IMAGE_SECTION_HEADER section = {};
+ section.VirtualAddress = 4096;
+ section.Misc.VirtualSize = 512;
+ section.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ;
+ section.PointerToRawData = nt + 384;
+ section.SizeOfRawData = 512;
+ std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, §ion, sizeof(section));
+
+ for (ULONG i = nt + 384; i < nt + 896; ++i)
+ {
+ b[i] = (UCHAR)(i * 31);
+ }
+
+ return b;
+}
+
+static void
+Set32(
+ std::vector& b,
+ size_t at,
+ ULONG value
+)
+{
+ std::memcpy(b.data() + at, &value, sizeof(value));
+}
+
+static void
+Set16(
+ std::vector& b,
+ size_t at,
+ USHORT value
+)
+{
+ std::memcpy(b.data() + at, &value, sizeof(value));
+}
+
+static std::vector
+Digest(
+ PUCHAR b,
+ ULONG size,
+ NTSTATUS expected = STATUS_SUCCESS
+)
+{
+ UINT32 algorithm = 0xdead;
+ ULONG length = 0xdead;
+ ULONG certSize = 0xdead;
+ PVOID digest = (void*)1;
+ LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1;
+ hashCalls = 0;
+ NTSTATUS s = DomitoCalculatePortableExecutableDigest(b, size, &algorithm, &length, &digest, &cert, &certSize);
+ Check(s == expected, "digest status");
+ std::vector result;
+
+ if (NT_SUCCESS(s))
+ {
+ Check(algorithm == CALG_SHA1 && length == 20 && digest != nullptr, "SHA1 output");
+ Check((cert == nullptr) == (certSize == 0), "certificate outputs agree");
+
+ if (cert)
+ {
+ Check((PUCHAR)cert >= b && (PUCHAR)cert + certSize <= b + size, "borrowed certificate span");
+ }
+
+ result.assign((PUCHAR)digest, (PUCHAR)digest + length);
+ DomitoFreePortableExecutableDigest(digest);
+ }
+ else
+ {
+ Check(!algorithm && !length && !digest && !cert && !certSize, "failure outputs cleared");
+ }
+
+ Check(allocations == 0, "no leaked pool allocations");
+ return result;
+}
+
+static std::vector
+Reference(
+ const std::vector& b,
+ ULONG checksum,
+ ULONG securitySlot,
+ ULONG end
+)
+{
+ // Explicit fixture ranges, independent of the production parser.
+ std::vector bytes(b.begin(), b.begin() + checksum);
+ bytes.insert(bytes.end(), b.begin() + checksum + 4, b.begin() + securitySlot);
+
+ if (securitySlot != end)
+ {
+ bytes.insert(bytes.end(), b.begin() + securitySlot + 8, b.begin() + end);
+ }
+
+ std::vector digest(20);
+ BCRYPT_ALG_HANDLE algorithm = nullptr;
+ Check(
+ NT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA1_ALGORITHM, nullptr, 0)), "reference algorithm"
+ );
+ Check(
+ NT_SUCCESS(BCryptHash(algorithm, nullptr, 0, bytes.data(), (ULONG)bytes.size(), digest.data(), 20)),
+ "reference digest"
+ );
+ BCryptCloseAlgorithmProvider(algorithm, 0);
+ return digest;
+}
+
+static void
+GuardedInvalid(
+ const std::vector& b,
+ NTSTATUS expected
+)
+{
+ SYSTEM_INFO si;
+ GetSystemInfo(&si);
+ const SIZE_T span = ((b.size() + si.dwPageSize - 1) / si.dwPageSize) * si.dwPageSize;
+ UCHAR* region = (UCHAR*)VirtualAlloc(nullptr, span + si.dwPageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
+ Check(region != nullptr, "guard allocation");
+ DWORD old;
+ Check(VirtualProtect(region + span, si.dwPageSize, PAGE_NOACCESS, &old) != FALSE, "guard protection");
+ UCHAR* input = region + span - b.size();
+ std::memcpy(input, b.data(), b.size());
+ Digest(input, (ULONG)b.size(), expected);
+ ULONG strictSize = 0;
+ ULONG securitySize = 0;
+ PVOID strictDigest = nullptr;
+ LPWIN_CERTIFICATE certificate = nullptr;
+ Check(
+ !NT_SUCCESS(DomitoCalculatePortableExecutableDigestEx(
+ input, (ULONG)b.size(), CALG_SHA256, &strictSize, &strictDigest, &certificate, &securitySize
+ )),
+ "strict guard-page malformed input rejected"
+ );
+ Check(!strictSize && !strictDigest && !certificate && !securitySize, "strict malformed outputs cleared");
+ VirtualFree(region, 0, MEM_RELEASE);
+}
+
+static std::vector
+ExplicitDigest(
+ const std::vector& b,
+ UINT32 calg,
+ NTSTATUS expected = STATUS_SUCCESS
+)
+{
+ ULONG length = 0xdead;
+ ULONG certSize = 0xdead;
+ PVOID digest = (PVOID)1;
+ LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1;
+ NTSTATUS status =
+ DomitoCalculatePortableExecutableDigestEx(b.data(), (ULONG)b.size(), calg, &length, &digest, &cert, &certSize);
+ Check(status == expected, "explicit digest status");
+ std::vector result;
+
+ if (NT_SUCCESS(status))
+ {
+ Check(
+ length == (calg == CALG_SHA1 ? 20u
+ : calg == CALG_SHA256 ? 32u
+ : calg == CALG_SHA384 ? 48u
+ : 64u),
+ "explicit algorithm digest length"
+ );
+ result.assign((UCHAR*)digest, (UCHAR*)digest + length);
+ DomitoFreePortableExecutableDigest(digest);
+ }
+ else
+ {
+ Check(!length && !digest && !cert && !certSize, "explicit failure outputs cleared");
+ }
+
+ Check(allocations == 0, "explicit digest allocation cleanup");
+ return result;
+}
+
+static std::vector
+SipDigest(
+ const std::vector& b,
+ UINT32 calg
+)
+{
+ WCHAR tempPath[MAX_PATH];
+ WCHAR tempName[MAX_PATH];
+ Check(GetTempPathW(MAX_PATH, tempPath) != 0, "temp path");
+ Check(GetTempFileNameW(tempPath, L"dci", 0, tempName) != 0, "temp name");
+ HANDLE file = CreateFileW(
+ tempName,
+ GENERIC_READ | GENERIC_WRITE,
+ FILE_SHARE_READ,
+ nullptr,
+ CREATE_ALWAYS,
+ FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,
+ nullptr
+ );
+ Check(file != INVALID_HANDLE_VALUE, "SIP fixture file");
+ DWORD written = 0;
+ Check(WriteFile(file, b.data(), (DWORD)b.size(), &written, nullptr) && written == b.size(), "write SIP fixture");
+ GUID subject;
+ Check(CryptSIPRetrieveSubjectGuid(tempName, file, &subject) != FALSE, "PE SIP subject");
+ SIP_SUBJECTINFO info = {};
+ info.cbSize = sizeof(info);
+ info.pgSubjectType = &subject;
+ info.hFile = file;
+ info.pwsFileName = tempName;
+ info.dwEncodingType = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
+ info.DigestAlgorithm.pszObjId = const_cast(
+ calg == CALG_SHA1 ? szOID_OIWSEC_sha1
+ : calg == CALG_SHA256 ? szOID_NIST_sha256
+ : calg == CALG_SHA384 ? szOID_NIST_sha384
+ : szOID_NIST_sha512
+ );
+ DWORD size = 0;
+ Check(CryptSIPCreateIndirectData(&info, &size, nullptr) != FALSE, "SIP size query");
+ std::vector buffer(size);
+ auto indirect = (SIP_INDIRECT_DATA*)buffer.data();
+ Check(CryptSIPCreateIndirectData(&info, &size, indirect) != FALSE, "Windows SIP digest");
+ std::vector result(indirect->Digest.pbData, indirect->Digest.pbData + indirect->Digest.cbData);
+ CloseHandle(file);
+ return result;
+}
+
+static std::vector
+Der(
+ UCHAR tag,
+ const std::vector& value
+)
+{
+ std::vector out = {tag};
+
+ if (value.size() < 128)
+ {
+ out.push_back((UCHAR)value.size());
+ }
+ else if (value.size() < 256)
+ {
+ out.push_back(0x81);
+ out.push_back((UCHAR)value.size());
+ }
+ else
+ {
+ out.push_back(0x82);
+ out.push_back((UCHAR)(value.size() >> 8));
+ out.push_back((UCHAR)value.size());
+ }
+
+ out.insert(out.end(), value.begin(), value.end());
+ return out;
+}
+
+static std::vector
+Join(
+ std::initializer_list> values
+)
+{
+ std::vector out;
+
+ for (auto& value : values)
+ {
+ out.insert(out.end(), value.begin(), value.end());
+ }
+
+ return out;
+}
+
+static std::vector
+SignedContent(
+ UINT32 calg,
+ bool nullParameters = true
+)
+{
+ std::vector oid = calg == CALG_SHA1 ? std::vector{0x2B, 0x0E, 0x03, 0x02, 0x1A}
+ : std::vector{
+ 0x60,
+ 0x86,
+ 0x48,
+ 0x01,
+ 0x65,
+ 0x03,
+ 0x04,
+ 0x02,
+ (UCHAR)(calg == CALG_SHA256 ? 1
+ : calg == CALG_SHA384 ? 2
+ : 3)
+ };
+ auto algorithm = Der(0x30, Join({Der(0x06, oid), nullParameters ? Der(0x05, {}) : std::vector{}}));
+ auto digest =
+ Der(0x30,
+ Join(
+ {algorithm,
+ Der(0x04,
+ std::vector(
+ calg == CALG_SHA1 ? 20
+ : calg == CALG_SHA256 ? 32
+ : calg == CALG_SHA384 ? 48
+ : 64,
+ 0xAA
+ ))}
+ ));
+ // A complete SHA-1 DigestInfo decoy in the ignored data attribute must not
+ // override the selected SpcIndirectDataContent digest AlgorithmIdentifier.
+ auto data =
+ Der(0x30, Der(0x04, {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x05, 0, 0x04, 0x14}));
+ auto indirect = Der(0x30, Join({data, digest}));
+ auto content =
+ Der(0x30, Join({Der(0x06, {0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x04}), Der(0xA0, indirect)}));
+ auto signedData = Der(0x30, Join({Der(0x02, {1}), Der(0x31, algorithm), content, Der(0x31, {})}));
+ return Der(0x30, Join({Der(0x06, {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02}), Der(0xA0, signedData)}));
+}
+
+static void
+ExtendedTests()
+{
+ for (bool pe64 : {false, true})
+ {
+ for (bool cert : {false, true})
+ {
+ for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
+ {
+ auto b = Image(pe64, 128, cert);
+ Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP differential fixture");
+
+ if (!cert)
+ {
+ b.insert(b.end(), 48, 0x42); // Non-section overlay.
+ Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP overlay differential");
+ }
+ }
+ }
+ }
+
+ for (bool pe64 : {false, true})
+ {
+ for (ULONG count : {0UL, 4UL})
+ {
+ auto absent = Image(pe64);
+ Set32(absent, pe64 ? 260 : 244, count);
+ ExplicitDigest(absent, CALG_SHA256, STATUS_NOT_SUPPORTED);
+ }
+ }
+
+ auto b = Image(true);
+ // Two contiguous sections whose headers are in reverse raw-offset order.
+ Set16(b, 134, 2);
+ IMAGE_SECTION_HEADER first = {};
+ IMAGE_SECTION_HEADER second = {};
+ first.PointerToRawData = 768;
+ first.SizeOfRawData = 256;
+ second.PointerToRawData = 512;
+ second.SizeOfRawData = 256;
+ std::memcpy(b.data() + 392, &first, sizeof(first));
+ std::memcpy(b.data() + 432, &second, sizeof(second));
+ Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "section header order differential");
+ Set32(b, 392 + 20, 800);
+ ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT); // extends beyond EOF
+ Set32(b, 392 + 20, 640);
+ ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // overlap
+ b = Image(true);
+ Set32(b, 392 + 20, 528);
+ Set32(b, 392 + 16, 496);
+ ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // gap
+ b = Image(true, 128, true);
+ b.insert(b.end(), 8, 0);
+ ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // nonterminal certificate
+ b = Image(true);
+ Set32(b, 392 + 20, 0xFFFFFFFF);
+ ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT);
+ ExplicitDigest(Image(true), 0x1234, STATUS_NOT_SUPPORTED);
+ b = Image(true, 128, true);
+ b.resize(1088);
+ Set32(b, 300, 64);
+ Set32(b, 1056, 32);
+ Set16(b, 1060, 0x0200);
+ Set16(b, 1062, 2);
+ Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "multiple certificate entries excluded");
+ Set32(b, 1056, 0);
+ ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT);
+ b = Image(true, 128, true);
+ Set32(b, 1024, 31); // Last entry's alignment padding is outside dwLength.
+ Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "certificate alignment padding");
+
+ for (int failure : {1, 2, 3, 4})
+ {
+ failOperation = failure;
+ failHashCall = 1;
+ hashCalls = 0;
+ ExplicitDigest(Image(true), CALG_SHA256, failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL);
+ }
+
+ failOperation = 0;
+
+ for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
+ {
+ auto content = SignedContent(calg);
+ auto signedImage = Image(true);
+ ULONG certLength = 8 + (ULONG)content.size();
+ ULONG tableSize = (certLength + 7) & ~7u;
+ signedImage.resize(1024 + tableSize);
+ Set32(signedImage, 296, 1024);
+ Set32(signedImage, 300, tableSize);
+ Set32(signedImage, 1024, certLength);
+ Set16(signedImage, 1028, 0x0200);
+ Set16(signedImage, 1030, 2);
+ std::memcpy(signedImage.data() + 1032, content.data(), content.size());
+ IMAGE_DATA_DIRECTORY directories[5] = {};
+ directories[4] = {1024, tableSize};
+ UINT32 actual = 0;
+ Check(
+ DomitoGetPortableExecutableDigestKindEx(
+ signedImage.data(), (ULONG)signedImage.size(), directories, &actual
+ ) == STATUS_SUCCESS &&
+ actual == calg,
+ "bounded SPC digest algorithm, decoy ignored"
+ );
+
+ for (ULONG cut = 0; cut < content.size(); ++cut)
+ {
+ Set32(signedImage, 1024, cut + 8);
+ actual = 0xDEAD;
+ Check(
+ !NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx(
+ signedImage.data(), (ULONG)signedImage.size(), directories, &actual
+ )) &&
+ actual == 0,
+ "truncated DER rejected"
+ );
+ }
+
+ Set32(signedImage, 1024, tableSize);
+ Check(
+ DomitoGetPortableExecutableDigestKindEx(
+ signedImage.data(), (ULONG)signedImage.size(), directories, &actual
+ ) == STATUS_SUCCESS &&
+ actual == calg,
+ "bounded zero padding inside dwLength"
+ );
+
+ if (tableSize > certLength)
+ {
+ signedImage.back() = 0x7F;
+ Check(
+ DomitoGetPortableExecutableDigestKindEx(
+ signedImage.data(), (ULONG)signedImage.size(), directories, &actual
+ ) == STATUS_INVALID_IMAGE_FORMAT &&
+ actual == 0,
+ "nonzero DER trailing bytes rejected"
+ );
+ signedImage.back() = 0;
+ }
+
+ Set32(signedImage, 1024, certLength);
+ signedImage[1033] = 0x80; // Indefinite length forbidden in this DER subset.
+ Check(
+ DomitoGetPortableExecutableDigestKindEx(
+ signedImage.data(), (ULONG)signedImage.size(), directories, &actual
+ ) == STATUS_INVALID_IMAGE_FORMAT,
+ "indefinite DER rejected"
+ );
+ }
+
+ for (bool nullParameters : {false, true})
+ {
+ auto content = SignedContent(CALG_SHA256, nullParameters);
+ auto pe = Image(true);
+ const ULONG length = 8 + (ULONG)content.size();
+ const ULONG tableSize = (length + 7) & ~7u;
+ pe.resize(1024 + tableSize);
+ Set32(pe, 1024, length);
+ Set16(pe, 1028, 0x0200);
+ Set16(pe, 1030, 2);
+ std::memcpy(pe.data() + 1032, content.data(), content.size());
+ IMAGE_DATA_DIRECTORY dirs[5] = {};
+ dirs[4] = {1024, tableSize};
+ UINT32 algorithm = 0;
+ Check(
+ DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) == STATUS_SUCCESS &&
+ algorithm == CALG_SHA256,
+ "AlgorithmIdentifier NULL optional"
+ );
+ // Change both copies of the SHA-256 OID to an unsupported family member.
+ const UCHAR oid[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01};
+
+ for (size_t i = 1032; i + sizeof(oid) <= pe.size(); ++i)
+ {
+ if (!std::memcmp(pe.data() + i, oid, sizeof(oid)))
+ {
+ pe[i + sizeof(oid) - 1] = 0x7F;
+ }
+ }
+
+ Check(
+ DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) ==
+ STATUS_NOT_SUPPORTED &&
+ !algorithm,
+ "unsupported SPC algorithm does not default to SHA1"
+ );
+ }
+
+ IMAGE_DATA_DIRECTORY dirs[5] = {};
+ UINT32 actual = 123;
+ Check(
+ DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual) == STATUS_NOT_FOUND &&
+ actual == 0,
+ "absent DER does not silently select SHA1"
+ );
+}
+
+static void
+SystemImageTests()
+{
+ WCHAR system[MAX_PATH];
+ Check(GetSystemDirectoryW(system, MAX_PATH) != 0, "system directory");
+
+ for (const WCHAR* name : {L"ci.dll", L"ntdll.dll", L"kernel32.dll", L"notepad.exe"})
+ {
+ std::wstring path = std::wstring(system) + L"\\" + name;
+ HANDLE file = CreateFileW(
+ path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr
+ );
+ Check(file != INVALID_HANDLE_VALUE, "system PE corpus file");
+ LARGE_INTEGER size;
+ Check(
+ GetFileSizeEx(file, &size) && size.QuadPart > 0 && size.QuadPart < 32 * 1024 * 1024,
+ "bounded system PE size"
+ );
+ std::vector b((size_t)size.QuadPart);
+ DWORD read;
+ Check(ReadFile(file, b.data(), (DWORD)b.size(), &read, nullptr) && read == b.size(), "read system PE fixture");
+ CloseHandle(file);
+
+ for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
+ {
+ Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "real system PE Windows SIP differential");
+ }
+
+ IMAGE_DOS_HEADER dos;
+ std::memcpy(&dos, b.data(), sizeof(dos));
+ USHORT magic;
+ const ULONG optional = (ULONG)dos.e_lfanew + 24;
+ std::memcpy(&magic, b.data() + optional, 2);
+ const ULONG directory = magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC
+ ? FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory)
+ : FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory);
+ IMAGE_DATA_DIRECTORY dirs[5];
+ std::memcpy(dirs, b.data() + optional + directory, sizeof(dirs));
+
+ if (dirs[4].Size)
+ {
+ UINT32 actual;
+ Check(
+ NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual)),
+ "real embedded SPC digest algorithm"
+ );
+ // Independent Windows ASN.1 decoder: extract signed content with
+ // CryptQueryObject/CryptMsg, then decode SPC_INDIRECT_DATA_OBJID.
+ ULONG certLength;
+ std::memcpy(&certLength, b.data() + dirs[4].VirtualAddress, 4);
+ CRYPT_DATA_BLOB blob = {certLength - 8, b.data() + dirs[4].VirtualAddress + 8};
+ HCRYPTMSG message = nullptr;
+ Check(
+ CryptQueryObject(
+ CERT_QUERY_OBJECT_BLOB,
+ &blob,
+ CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED,
+ CERT_QUERY_FORMAT_FLAG_BINARY,
+ 0,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ &message,
+ nullptr
+ ) != FALSE,
+ "Windows decode PKCS7"
+ );
+ DWORD contentSize = 0;
+ Check(
+ CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, nullptr, &contentSize) != FALSE,
+ "Windows SPC content size"
+ );
+ std::vector content(contentSize);
+ Check(
+ CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, content.data(), &contentSize) != FALSE,
+ "Windows SPC content"
+ );
+ SIP_INDIRECT_DATA* indirect = nullptr;
+ DWORD decodedSize = 0;
+ Check(
+ CryptDecodeObjectEx(
+ X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
+ SPC_INDIRECT_DATA_OBJID,
+ content.data(),
+ contentSize,
+ CRYPT_DECODE_ALLOC_FLAG,
+ nullptr,
+ &indirect,
+ &decodedSize
+ ) != FALSE,
+ "Windows decode indirect data"
+ );
+ const char* oid = indirect->DigestAlgorithm.pszObjId;
+ const UINT32 expected = !std::strcmp(oid, szOID_OIWSEC_sha1) ? CALG_SHA1
+ : !std::strcmp(oid, szOID_NIST_sha256) ? CALG_SHA256
+ : !std::strcmp(oid, szOID_NIST_sha384) ? CALG_SHA384
+ : CALG_SHA512;
+ Check(actual == expected, "SPC digest algorithm matches Windows ASN1 decoder");
+ auto digest = ExplicitDigest(b, actual);
+ Check(
+ digest.size() == indirect->Digest.cbData &&
+ !std::memcmp(digest.data(), indirect->Digest.pbData, digest.size()),
+ "computed digest matches real embedded SPC digest"
+ );
+ LocalFree(indirect);
+ CryptMsgClose(message);
+ }
+ }
+}
+
+int
+main()
+{
+ SystemImageTests();
+ ExtendedTests();
+
+ for (bool pe64 : {false, true})
+ {
+ for (bool certificate : {false, true})
+ {
+ auto b = Image(pe64, 128, certificate);
+ auto result = Digest(b.data(), (ULONG)b.size());
+ Check(
+ result == Reference(b, 216, pe64 ? 296 : 280, 1024), "fixture digest equals independent CNG reference"
+ );
+ Set32(b, 216, 0x12345678);
+ Check(result == Digest(b.data(), (ULONG)b.size()), "checksum excluded");
+
+ if (certificate)
+ {
+ b.back() ^= 0x55;
+ Check(result == Digest(b.data(), (ULONG)b.size()), "certificate contents excluded");
+ }
+ }
+ }
+
+ auto large = Image(true, 4 * 1024 * 1024);
+ Check(
+ Digest(large.data(), (ULONG)large.size()) == Reference(large, 4194392, 4194472, (ULONG)large.size()),
+ "large DOS stub"
+ );
+ auto b = Image(true);
+
+ for (ULONG size = 0; size < 392; ++size)
+ {
+ std::vector prefix(b.begin(), b.begin() + size);
+
+ if (size == 0)
+ {
+ Digest(b.data(), 0, STATUS_INVALID_IMAGE_NOT_MZ);
+ }
+ else
+ {
+ GuardedInvalid(prefix, size < 64 ? STATUS_INVALID_IMAGE_NOT_MZ : STATUS_INVALID_IMAGE_FORMAT);
+ }
+ }
+
+ auto bad = b;
+ Set32(bad, 60, 0xffffffff);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = b;
+ Set32(bad, 60, 0x7fffffff);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = b;
+ Set16(bad, 148, 1);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = b;
+ Set16(bad, 152, 0x999);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = b;
+ Set32(bad, 260, 17);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+
+ for (ULONG offset : {200UL, 2048UL, 0xffffffffUL})
+ {
+ bad = b;
+ Set32(bad, 296, offset);
+ Set32(bad, 300, 32);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ }
+
+ bad = b;
+ Set32(bad, 300, 32);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = b;
+ Set32(bad, 296, 800);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = b;
+ Set32(bad, 296, 800);
+ Set32(bad, 300, 0xffffffff);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = Image(true, 128, true);
+ Set32(bad, 1024, 33);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = Image(true, 128, true);
+ Set32(bad, 1024, 7);
+ GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
+ bad = Image(true);
+ Set32(bad, 260, 0); // No directory entry: don't exclude its physical bytes.
+ Check(Digest(bad.data(), (ULONG)bad.size()) == Reference(bad, 216, 1024, 1024), "absent security directory");
+
+ for (int failure : {1, 2, 3, 4})
+ {
+ failOperation = failure;
+
+ for (int call = 1; call <= (failure == 2 ? 3 : 1); ++call)
+ {
+ failHashCall = call;
+ Digest(b.data(), (ULONG)b.size(), failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL);
+ }
+ }
+
+ failOperation = 0;
+ Digest(nullptr, 1024, STATUS_INVALID_PARAMETER);
+ IMAGE_DATA_DIRECTORY directory = {1024, 32};
+ MINCRYPT_POLICY_INFO policy = {};
+ MINCRYPT_POLICY_INFO timestamp = {};
+ LARGE_INTEGER signing = {};
+ UCHAR hash[20] = {};
+ bytesRead = 16;
+ ciCalls = 0;
+ Check(
+ DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp) ==
+ STATUS_END_OF_FILE,
+ "short certificate read rejected"
+ );
+ Check(ciCalls == 0 && allocations == 0, "short read does not reach CI or leak");
+ bytesRead = 32;
+ Check(
+ NT_SUCCESS(
+ DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp)
+ ),
+ "complete certificate read accepted"
+ );
+ Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once");
+ // Failure paths must release previous attempts before embedded/catalog/reload reuse.
+ allocatePolicies = true;
+ embeddedResult = STATUS_INVALID_IMAGE_HASH;
+ catalogResults[0] = STATUS_INVALID_IMAGE_HASH;
+ catalogResults[1] = STATUS_SUCCESS;
+ catalogCalls = 0;
+ expectedCatalogMask = 0x8000;
+ expectedTrustedMarker = TRUE;
+ ULONG source = 0xDEAD;
+ Check(
+ NT_SUCCESS(DomitoValidateFileLegacyModeEx(
+ (HANDLE)1,
+ hash,
+ 20,
+ CALG_SHA1,
+ &directory,
+ &policy,
+ &signing,
+ ×tamp,
+ expectedCatalogMask,
+ TRUE,
+ &source
+ )),
+ "explicit catalog fallback"
+ );
+ Check(
+ source == DOMITO_CI_SOURCE_CATALOG && catalogCalls == 2 && allocations == 2,
+ "final policy ownership and catalog source"
+ );
+ CiFreePolicyInfo(&policy);
+ CiFreePolicyInfo(×tamp);
+ Check(allocations == 0, "final results freed once");
+ source = 123;
+ Check(
+ DomitoValidateFileLegacyModeEx(
+ (HANDLE)1, hash, 20, CALG_SHA256, &directory, &policy, &signing, ×tamp, 0, FALSE, &source
+ ) == STATUS_INVALID_PARAMETER &&
+ !source,
+ "algorithm and length mismatch rejected"
+ );
+ Check(
+ DomitoValidateFileLegacyModeEx(
+ (HANDLE)1, hash, 20, CALG_SHA384, &directory, &policy, &signing, ×tamp, 0, FALSE, &source
+ ) == STATUS_NOT_SUPPORTED,
+ "legacy catalog algorithm capability explicit"
+ );
+ observedFileSize = 1040;
+ Check(
+ DomitoValidateFileLegacyModeEx(
+ (HANDLE)1, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp, 0, FALSE, &source
+ ) == STATUS_INVALID_IMAGE_FORMAT,
+ "certificate file range checked before allocation"
+ );
+ Check(allocations == 0, "invalid file range never allocated");
+ observedFileSize = 1056;
+ expectedCatalogMask = 0x8000;
+ expectedTrustedMarker = TRUE;
+ catalogCalls = 0;
+ catalogResults[1] = STATUS_INVALID_IMAGE_HASH;
+ Check(
+ DomitoValidateFileLegacyModeEx(
+ (HANDLE)1,
+ hash,
+ 20,
+ CALG_SHA1,
+ &directory,
+ &policy,
+ &signing,
+ ×tamp,
+ expectedCatalogMask,
+ TRUE,
+ &source
+ ) == STATUS_INVALID_IMAGE_HASH,
+ "final catalog failure retained"
+ );
+ Check(!source && allocations == 2, "final failed-call policies remain caller-owned");
+ CiFreePolicyInfo(&policy);
+ CiFreePolicyInfo(×tamp);
+ Check(allocations == 0, "failed final policies freed once");
+ std::printf("PASS: %u digest/legacy checks (Windows SIP, Crypt32, guard pages, failures, ownership).\n", checks);
+}
diff --git a/tests/mincrypt-layout-tests.cpp b/tests/mincrypt-layout-tests.cpp
new file mode 100644
index 0000000..2c674da
--- /dev/null
+++ b/tests/mincrypt-layout-tests.cpp
@@ -0,0 +1,55 @@
+// Compile with the WDK for supported 64-bit targets. No driver is linked/loaded.
+#include
+#include "Domito.MinCrypt.h"
+
+#if NTDDI_VERSION >= NTDDI_WIN10
+C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 48);
+C_ASSERT(
+ FIELD_OFFSET(
+ MINCRYPT_POLICY_INFO,
+ VerificationStatus
+ ) == 4
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ MINCRYPT_POLICY_INFO,
+ ChainInfo
+ ) == 16
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ MINCRYPT_POLICY_INFO,
+ NotBefore
+ ) == 32
+);
+C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 72);
+C_ASSERT(
+ FIELD_OFFSET(
+ MINCRYPT_CHAIN_INFO,
+ ChainElements
+ ) == 40
+);
+C_ASSERT(
+ FIELD_OFFSET(
+ MINCRYPT_CHAIN_INFO,
+ AuthenticodeAttributes
+ ) == 56
+);
+#else
+C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 32);
+C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 40);
+#endif
+C_ASSERT(sizeof(MINCRYPT_NAME_BLOB) == 16);
+C_ASSERT(sizeof(MINCRYPT_CHAIN_ELEMENT) == 120);
+C_ASSERT(
+ FIELD_OFFSET(
+ MINCRYPT_CHAIN_ELEMENT,
+ Certificate
+ ) == 104
+);
+
+#if defined(_M_AMD64)
+// These opt-in declarations establish observations only for the named x64 build.
+#include "Domito.MinCrypt.26100.h"
+#include "Domito.MinCrypt.Historical.h"
+#endif
diff --git a/tests/run-ci-probe-tests.py b/tests/run-ci-probe-tests.py
new file mode 100644
index 0000000..00f0861
--- /dev/null
+++ b/tests/run-ci-probe-tests.py
@@ -0,0 +1,34 @@
+"""Test the actual kernel probe body with mock CI; run in an x64 VS dev shell."""
+
+from pathlib import Path
+import re
+import subprocess
+import tempfile
+
+tests = Path(__file__).resolve().parent
+header = (tests.parent / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
+names = ["MINCRYPT_MAX_HASH_LENGTH", "DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE"]
+constants = "\n".join(
+ re.search(r"^#define\s+" + name + r"\s+[^\r\n]+", header, re.M)[0] for name in names
+)
+with tempfile.TemporaryDirectory(prefix="domito-ci-probe-") as temp:
+ build = Path(temp)
+ (build / "ci-probe-constants.inc").write_text(constants + "\n", encoding="utf-8")
+ exe = build / "ci-probe-tests.exe"
+ subprocess.run(
+ [
+ "cl",
+ "/nologo",
+ "/EHsc",
+ "/W4",
+ "/WX",
+ "/std:c++17",
+ "/I" + str(build),
+ str(tests / "ci-file-object-probe-tests.cpp"),
+ "/Fe" + str(exe),
+ "/Fo" + str(build / "ci-probe-tests.obj"),
+ ],
+ cwd=build,
+ check=True,
+ )
+ subprocess.run([str(exe)], cwd=build, check=True)
diff --git a/tests/run-ci-validation-tests.py b/tests/run-ci-validation-tests.py
new file mode 100644
index 0000000..f535eb7
--- /dev/null
+++ b/tests/run-ci-validation-tests.py
@@ -0,0 +1,53 @@
+"""Compile the actual production CI wrapper body against deterministic CI doubles."""
+
+from pathlib import Path
+import subprocess
+import tempfile
+
+root = Path(__file__).resolve().parents[1]
+header = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
+start = header.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards")
+end = header.index("#define DOMITO_CI_SOURCE_EMBEDDED", start)
+constants_header = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
+constants = "\n".join(
+ line
+ for line in constants_header.splitlines()
+ if line.startswith(
+ (
+ "#define DOMITO_CI_VALIDATE_FLAG_",
+ "#define MINCRYPT_MAX_HASH_LENGTH",
+ "#define MINCRYPT_SHA",
+ )
+ )
+)
+source = (root / "src/Domito.CodeIntegrity.Validation.cpp").read_text(encoding="utf-8-sig")
+source = source.replace('#include "Domito.Internal.h"', "")
+with tempfile.TemporaryDirectory(prefix="domito-ci-validation-") as temporary:
+ directory = Path(temporary)
+ (directory / "ci-validation-api.inc").write_text(
+ constants + "\n" + header[start:end], encoding="utf-8"
+ )
+ (directory / "ci-validation-under-test.inc").write_text(source, encoding="utf-8")
+ for branch in ["x64", "other"]:
+ program = directory / (branch + ".exe")
+ flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else []
+ subprocess.run(
+ [
+ "cl",
+ "/nologo",
+ "/EHsc",
+ "/W4",
+ "/WX",
+ "/std:c++17",
+ "/DNTDDI_VERSION=0x0A000008",
+ "/DNTDDI_WIN10=0x0A000000",
+ "/I" + str(directory),
+ *flags,
+ str(Path(__file__).with_name("ci-validation-tests.cpp")),
+ "/Fe" + str(program),
+ "/Fo" + str(directory / (branch + ".obj")),
+ ],
+ cwd=directory,
+ check=True,
+ )
+ subprocess.run([str(program)], check=True)
diff --git a/tests/run-common-tests.py b/tests/run-common-tests.py
new file mode 100644
index 0000000..cce1bf7
--- /dev/null
+++ b/tests/run-common-tests.py
@@ -0,0 +1,35 @@
+"""Run production export and file-read helpers with deterministic user-mode doubles."""
+
+from pathlib import Path
+import subprocess
+import tempfile
+
+root = Path(__file__).resolve().parents[1]
+source = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
+export_start = source.index("// This helper handles directory RVAs")
+export_end = source.index("_Success_", source.index("DomitoFindExportedFunctionAddress(") + 1)
+read_start = source.rfind("_Success_", 0, source.index("DomitoReadFileAt("))
+read_end = source.index("_Success_", source.index("DomitoReadFile(") + 1)
+with tempfile.TemporaryDirectory(prefix="domito-common-tests-") as temp:
+ build = Path(temp)
+ (build / "common-under-test.inc").write_text(
+ source[export_start:export_end] + "\n" + source[read_start:read_end], encoding="utf-8"
+ )
+ exe = build / "common-tests.exe"
+ subprocess.run(
+ [
+ "cl",
+ "/nologo",
+ "/EHsc",
+ "/W4",
+ "/WX",
+ "/std:c++17",
+ "/I" + str(build),
+ str(root / "tests/common-tests.cpp"),
+ "/Fe" + str(exe),
+ "/Fo" + str(build / "common-tests.obj"),
+ ],
+ cwd=build,
+ check=True,
+ )
+ subprocess.run([str(exe)], cwd=build, check=True)
diff --git a/tests/run-consumer-helper-tests.py b/tests/run-consumer-helper-tests.py
new file mode 100644
index 0000000..1c0952c
--- /dev/null
+++ b/tests/run-consumer-helper-tests.py
@@ -0,0 +1,71 @@
+"""Production consumer helpers with real CNG/Crypt32 and deterministic kernel doubles."""
+
+from pathlib import Path
+import subprocess
+import tempfile
+
+root = Path(__file__).resolve().parents[1]
+public = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
+mincrypt = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
+structures = mincrypt[
+ mincrypt.index("#include ") : mincrypt.index("#include ")
+ + len("#include ")
+]
+validation = public[
+ public.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards") : public.index(
+ "#define DOMITO_CI_SOURCE_EMBEDDED"
+ )
+]
+helpers = (
+ (root / "include/Domito.Consumer.h")
+ .read_text(encoding="utf-8-sig")
+ .replace('#include "Domito.h"', "")
+)
+algorithm = public[
+ public.index("#if !defined(CALG_SHA1)") : public.index(
+ "/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM(")
+ )
+]
+common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
+process_start = common.rfind("_Success_", 0, common.index("DomitoGetProcessImageName("))
+process = (
+ common[process_start:]
+ .replace('#pragma code_seg("PAGED")', "")
+ .replace("#pragma code_seg()", "")
+)
+with tempfile.TemporaryDirectory(prefix="domito-consumer-tests-") as temp:
+ build = Path(temp)
+ (build / "consumer-api.inc").write_text(
+ algorithm + "\n" + structures + "\n" + validation + "\n" + helpers
+ )
+ bodies = "\n".join(
+ (root / ("src/" + name))
+ .read_text(encoding="utf-8-sig")
+ .replace('#include "Domito.Internal.h"', "")
+ for name in ["Domito.Certificate.cpp", "Domito.Consumer.cpp"]
+ )
+ (build / "consumer-under-test.inc").write_text(bodies + "\n" + process)
+ for branch in ["x64", "other"]:
+ exe = build / (branch + ".exe")
+ flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else []
+ subprocess.run(
+ [
+ "cl",
+ "/nologo",
+ "/EHsc",
+ "/W4",
+ "/WX",
+ "/std:c++17",
+ "/I" + str(build),
+ *flags,
+ str(root / "tests/consumer-helper-tests.cpp"),
+ "/Fe" + str(exe),
+ "/Fo" + str(build / (branch + ".obj")),
+ "/link",
+ "bcrypt.lib",
+ "crypt32.lib",
+ ],
+ cwd=build,
+ check=True,
+ )
+ subprocess.run([str(exe)], check=True)
diff --git a/tests/run-deprecation-tests.py b/tests/run-deprecation-tests.py
new file mode 100644
index 0000000..7e9e620
--- /dev/null
+++ b/tests/run-deprecation-tests.py
@@ -0,0 +1,94 @@
+"""Compile actual public headers and verify MSVC C4996 migration diagnostics."""
+
+import argparse
+import os
+from pathlib import Path
+import subprocess
+import tempfile
+
+parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument("--arch", choices=["x64", "ARM64"], default="x64")
+args = parser.parse_args()
+root = Path(__file__).resolve().parents[1]
+vc = Path(os.environ["VCToolsInstallDir"])
+kit = Path(os.environ["WindowsSdkDir"]) / "Include" / os.environ["WindowsSDKVersion"].strip("\\/")
+compiler = vc / "bin/Hostx64" / args.arch.lower() / "cl.exe"
+architecture = ["/D_AMD64_", "/D_AMD64", "/Gz"] if args.arch == "x64" else ["/D_ARM64_", "/DARM64"]
+common = [
+ "/nologo",
+ "/c",
+ "/kernel",
+ "/W4",
+ "/WX",
+ "/we4996",
+ "/D_WIN64",
+ "/DWINNT=1",
+ "/DNTDDI_VERSION=0x0A000008",
+ "/D_WIN32_WINNT=0x0A00",
+ *architecture,
+ "/I" + str(vc / "include"),
+ "/I" + str(kit / "km"),
+ "/I" + str(kit / "shared"),
+ "/I" + str(kit / "ucrt"),
+ "/I" + str(root / "include"),
+]
+old = {
+ "DomitoGetPortableExecutableDigestKind": "DomitoGetPortableExecutableDigestKindEx",
+ "DomitoCalculatePortableExecutableDigest": "DomitoCalculatePortableExecutableDigestEx",
+ "DomitoValidateFileLegacyMode": "DomitoValidateFileLegacyModeEx",
+ "DomitoGetProcessImageName": "DomitoGetProcessImageNameEx",
+}
+modern = [
+ *old.values(),
+ "DomitoFreePortableExecutableDigest",
+ "DomitoFreeProcessImageName",
+ "DomitoValidateFileObjectEx",
+ "DomitoValidateFileHandleEx",
+ "DomitoValidateFilePathEx",
+ "DomitoGetCiChainView",
+ "DomitoGetCertificatePublisherName",
+ "DomitoFindModuleBaseAddress",
+ "DomitoFindExportedFunctionAddress",
+ "DomitoReadFile",
+ "DomitoReadFileAt",
+ "CiValidateFileObject",
+]
+prefix = "#include \n#include \n#include \n#include \n#include \n"
+checks = 0
+with tempfile.TemporaryDirectory(prefix="domito-deprecation-") as temp:
+ build = Path(temp)
+ for language in ["c", "cpp"]:
+ for index, (name, replacement) in enumerate([("modern", None), *old.items()]):
+ source = build / (f"check-{index}." + language)
+ refs = modern if replacement is None else [name]
+ source.write_text(
+ prefix
+ + "void Check(void) {\n"
+ + "".join(f" (void)&{ref};\n" for ref in refs)
+ + "}\n"
+ )
+ result = subprocess.run(
+ [
+ str(compiler),
+ *common,
+ "/TC" if language == "c" else "/TP",
+ "/Fo" + str(build / "check.obj"),
+ str(source),
+ ],
+ text=True,
+ capture_output=True,
+ )
+ output = result.stdout + result.stderr
+ if replacement is None:
+ assert result.returncode == 0, output
+ else:
+ assert (
+ result.returncode != 0
+ and "C4996" in output
+ and name in output
+ and replacement in output
+ ), output
+ errors = [line for line in output.splitlines() if "error C" in line]
+ assert errors and all("C4996" in line for line in errors), output
+ checks += 1
+print(f"PASS: {checks} {args.arch} C/C++ public-header deprecation checks")
diff --git a/tests/run-digest-tests.py b/tests/run-digest-tests.py
new file mode 100644
index 0000000..b59e062
--- /dev/null
+++ b/tests/run-digest-tests.py
@@ -0,0 +1,57 @@
+"""Run in an x64 VS developer shell with Windows SDK and Python 3 installed.
+
+Extracts the actual production functions without changing their bodies, replacing
+only kernel dependencies with the test harness. No driver is loaded. This tests
+parsing, hashing and cleanup, not real CI policy or kernel I/O synchronization.
+"""
+
+import argparse
+from pathlib import Path
+import subprocess
+import tempfile
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--source-root", type=Path, default=Path(__file__).resolve().parents[1])
+parser.add_argument("--digest-source", type=Path)
+args = parser.parse_args()
+root = args.source_root
+digest = (args.digest_source or root / "src/Domito.CodeIntegrity.cpp").read_text(
+ encoding="utf-8-sig"
+)
+digest = digest.replace('#include "Domito.Internal.h"', "").replace(
+ '#include "Domito.MinCrypt.h"', ""
+)
+common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
+start = common.rfind("_Success_", 0, common.index("DomitoMemorySearchPattern("))
+end = common.index("_Success_", common.index("DomitoMemorySearchPattern("))
+search = common[start:end]
+public = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
+start = public.index("#if !defined(CALG_SHA1)")
+end = public.index("/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM("))
+algorithm = public[start:end]
+with tempfile.TemporaryDirectory(prefix="domito-digest-tests-") as temp:
+ build = Path(temp)
+ (build / "digest-under-test.inc").write_text(
+ algorithm + "\n" + search + "\n" + digest, encoding="utf-8"
+ )
+ test_source = Path(__file__).with_name("digest-tests.cpp")
+ subprocess.run(
+ [
+ "cl",
+ "/nologo",
+ "/EHsc",
+ "/W4",
+ "/WX",
+ "/std:c++17",
+ "/I" + str(build),
+ str(test_source),
+ "/Fe" + str(build / "digest-tests.exe"),
+ "/Fo" + str(build / "digest-tests.obj"),
+ "/link",
+ "bcrypt.lib",
+ "crypt32.lib",
+ ],
+ cwd=build,
+ check=True,
+ )
+ subprocess.run([str(build / "digest-tests.exe")], cwd=build, check=True)