Add CI validation helpers with explicit signature selection and owned results

This commit is contained in:
nefarius committed 2026-10-26 23:45:12 +01:00
1 parent 28f284be8f
commit 1f4d62ae95
13 files changed
+596 -3

No files matched your search

+8
View File
@@ -30,6 +30,14 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
- Reliable SAL annotations. - 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 😎 - 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 ## How to use
- Add the `include` directory to your project's headers search path. - Add the `include` directory to your project's headers search path.
+5 -2
View File
@@ -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 of the company certificate. First successful does not necessarily mean first
listed: CI may reject the primary and continue. listed: CI may reject the primary and continue.
`0x80` has additional policy and fallback effects. It has not been published as `0x80` has additional policy and fallback effects. Domito exposes it only as
a supported Domito constant or a production default. The companion research `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 `SIGNATURE-SELECTION.md` provides the full static evidence and an alternative
primary-signature path using `CiCheckSignedFile`. primary-signature path using `CiCheckSignedFile`.
+116
View File
@@ -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.
+8 -1
View File
@@ -6,6 +6,13 @@ it should not be read as describing the corrected source unchanged.
## Implemented with host tests or compilation ## 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 - **S01–S04:** subtraction-safe PE ranges, no fixed 4 MiB copy, bounded certificate
intervals, deterministic failure outputs, and digest cleanup. Compatibility intervals, deterministic failure outputs, and digest cleanup. Compatibility
and explicit paths share the bounded hashing core. and explicit paths share the bounded hashing core.
@@ -95,7 +102,7 @@ and compilation do not close the remaining work:
## Checks ## 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, 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, 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. not a claim that historical or ARM64 CI runtime ABI has been verified.
+4
View File
@@ -158,6 +158,10 @@ later conversion failure. It is not owned by either policy object.
Evidence: `MincryptFreePolicyInfo`, `MincryptCopyPolicyInfo`, Evidence: `MincryptFreePolicyInfo`, `MincryptCopyPolicyInfo`,
`I_MincryptAddChainInfo`, `CiVerifyHashInCatalog`, and `CiGetCertPublisherName`. `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 ## Multiple signatures and selected results
`CiValidateFileObject` returns one selected signature result and its timestamp `CiValidateFileObject` returns one selected signature result and its timestamp
+7
View File
@@ -565,6 +565,13 @@ CiSetTrustedOriginClaimId(
// See docs/ci-validate-file-object.md for evidence and version limitations. // See docs/ci-validate-file-object.md for evidence and version limitations.
#define DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE 0x00000020UL #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 * Given a file object, verify the signature and provide information regarding
* the certificates that was used for signing (the entire certificate chain) * the certificates that was used for signing (the entire certificate chain)
+73
View File
@@ -254,6 +254,79 @@ DomitoFreePortableExecutableDigest(
_In_ PVOID pDigestOut _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_EMBEDDED 1UL
#define DOMITO_CI_SOURCE_CATALOG 2UL #define DOMITO_CI_SOURCE_CATALOG 2UL
+109
View File
@@ -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
+1
View File
@@ -122,6 +122,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="Domito.CodeIntegrity.cpp" /> <ClCompile Include="Domito.CodeIntegrity.cpp" />
<ClCompile Include="Domito.CodeIntegrity.Validation.cpp" />
<ClCompile Include="Domito.cpp" /> <ClCompile Include="Domito.cpp" />
<ClCompile Include="Domito.Memory.cpp" /> <ClCompile Include="Domito.Memory.cpp" />
</ItemGroup> </ItemGroup>
+3
View File
@@ -28,6 +28,9 @@
<ClCompile Include="Domito.Memory.cpp"> <ClCompile Include="Domito.Memory.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="Domito.CodeIntegrity.Validation.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Domito.CodeIntegrity.cpp"> <ClCompile Include="Domito.CodeIntegrity.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
+20
View File
@@ -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-digest-tests.py
python tests\run-common-tests.py python tests\run-common-tests.py
python tests\run-ci-probe-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 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 unbounded strings, and deterministic failure outputs. The export API requires a
valid loader-mapped module; it is not an arbitrary file parser. 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 ## File-object validation flag probe
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md) See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
+215
View File
@@ -0,0 +1,215 @@
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
#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<UCHAR*>(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<UCHAR*>(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<PFILE_OBJECT>(static_cast<ULONG_PTR>(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<DOMITO_CI_SIGNATURE_SELECTION>(-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
);
}
+27
View File
@@ -0,0 +1,27 @@
"""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)