From 1f4d62ae95c4038578ed04836b0cc0292d9aabfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=B6glinger-Stelzer?= Date: Mon, 26 Oct 2026 23:45:12 +0100 Subject: [PATCH] Add CI validation helpers with explicit signature selection and owned results --- README.md | 8 + docs/ci-validate-file-object.md | 7 +- docs/ci-validation-helpers.md | 116 +++++++++++++ docs/implementation-status.md | 9 +- docs/mincrypt-contracts.md | 4 + include/Domito.MinCrypt.h | 7 + include/Domito.h | 73 ++++++++ src/Domito.CodeIntegrity.Validation.cpp | 109 ++++++++++++ src/Domito.vcxproj | 1 + src/Domito.vcxproj.filters | 3 + tests/README.md | 20 +++ tests/ci-validation-tests.cpp | 215 ++++++++++++++++++++++++ tests/run-ci-validation-tests.py | 27 +++ 13 files changed, 596 insertions(+), 3 deletions(-) create mode 100644 docs/ci-validation-helpers.md create mode 100644 src/Domito.CodeIntegrity.Validation.cpp create mode 100644 tests/ci-validation-tests.cpp create mode 100644 tests/run-ci-validation-tests.py 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)