diff --git a/README.md b/README.md
index 5d0f829..348eea1 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,14 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
- 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.
+
## How to use
- Add the `include` directory to your project's headers search path.
diff --git a/docs/ci-validate-file-object.md b/docs/ci-validate-file-object.md
index 62be7f6..dedfd6e 100644
--- a/docs/ci-validate-file-object.md
+++ b/docs/ci-validate-file-object.md
@@ -197,8 +197,11 @@ 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. It has not been published as
-a supported Domito constant or a production default. The companion research
+`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`.
diff --git a/docs/ci-validation-helpers.md b/docs/ci-validation-helpers.md
new file mode 100644
index 0000000..22c3e91
--- /dev/null
+++ b/docs/ci-validation-helpers.md
@@ -0,0 +1,116 @@
+# 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/implementation-status.md b/docs/implementation-status.md
index 8f8db0c..ec170d4 100644
--- a/docs/implementation-status.md
+++ b/docs/implementation-status.md
@@ -6,6 +6,13 @@ 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.
@@ -95,7 +102,7 @@ and compilation do not close the remaining work:
## Checks
-Run the three host runners in `tests/README.md`. Kernel compilation also covers
+Run the four 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.
diff --git a/docs/mincrypt-contracts.md b/docs/mincrypt-contracts.md
index 438dea2..e9da9a9 100644
--- a/docs/mincrypt-contracts.md
+++ b/docs/mincrypt-contracts.md
@@ -158,6 +158,10 @@ 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
diff --git a/include/Domito.MinCrypt.h b/include/Domito.MinCrypt.h
index 7a879ff..5e0c37c 100644
--- a/include/Domito.MinCrypt.h
+++ b/include/Domito.MinCrypt.h
@@ -565,6 +565,13 @@ CiSetTrustedOriginClaimId(
// 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)
diff --git a/include/Domito.h b/include/Domito.h
index b1a820f..c64dc92 100644
--- a/include/Domito.h
+++ b/include/Domito.h
@@ -254,6 +254,79 @@ DomitoFreePortableExecutableDigest(
_In_ PVOID pDigestOut
);
+#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
diff --git a/src/Domito.CodeIntegrity.Validation.cpp b/src/Domito.CodeIntegrity.Validation.cpp
new file mode 100644
index 0000000..2599e06
--- /dev/null
+++ b/src/Domito.CodeIntegrity.Validation.cpp
@@ -0,0 +1,109 @@
+#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, 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.vcxproj b/src/Domito.vcxproj
index b46a46c..74f0803 100644
--- a/src/Domito.vcxproj
+++ b/src/Domito.vcxproj
@@ -122,6 +122,7 @@
+
diff --git a/src/Domito.vcxproj.filters b/src/Domito.vcxproj.filters
index 3483c2f..18bd842 100644
--- a/src/Domito.vcxproj.filters
+++ b/src/Domito.vcxproj.filters
@@ -28,6 +28,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/tests/README.md b/tests/README.md
index e639c3f..87c926e 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -6,6 +6,7 @@ From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3
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
@@ -40,6 +41,25 @@ 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)
diff --git a/tests/ci-validation-tests.cpp b/tests/ci-validation-tests.cpp
new file mode 100644
index 0000000..f0a0645
--- /dev/null
+++ b/tests/ci-validation-tests.cpp
@@ -0,0 +1,215 @@
+#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, live, validationCalls, embeddedCalls, digestCalls, digestFrees;
+static NTSTATUS nativeStatus = STATUS_SUCCESS, embeddedStatus = STATUS_SUCCESS, digestStatus = STATUS_SUCCESS;
+static UINT32 nativeAlgorithm = CALG_SHA_256, nativeSize = 32;
+static ULONG fakeDigestSize = 32;
+static bool omitCertificate, omitDigest, partialDigestOnFailure;
+static const UCHAR image[] = {1,2,3};
+static ULONG observedFlags, observedLevel, 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(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= 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)