Add reusable certificate and file validation helpers

Fix process-name ownership and module lookup outputs, add explicit SPKI hashing and bounded chain access, and improve WDM integration. Deprecate superseded entry points with migration guidance and align branch code and tests with the library style.

Add CI validation helpers with explicit signature selection and owned results

Document runtime-confirmed CI signer selection and crypto reuse

Expand CI compatibility matrix to twenty verified samples

Extend CI export matrix with Windows 11 x64 and ARM64 samples

Document CI API availability and historical mincrypt layouts

Add explicit Authenticode workflows and correct CI helper contracts

Document CI validation flag 0x20 and add kernel probe

Harden CI digest parsing and failure cleanup
This commit is contained in:
nefarius committed 2026-10-27 00:17:25 +01:00
1 parent 7985b23be5
commit 3fbfe31d15
38 files changed
+9201 -751

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
# Regression tests
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
```powershell
python tests\run-digest-tests.py
python tests\run-common-tests.py
python tests\run-ci-probe-tests.py
python tests\run-ci-validation-tests.py
```
Build outputs are temporary. No driver is installed or loaded. Test runners
extract the actual production functions or include the probe body unchanged;
kernel calls, CI calls, and pool allocation are replaced with test doubles.
The digest harness uses real BCrypt, Crypt32 and the Windows PE SIP provider.
## Digest and legacy verification tests
Coverage includes PE32/PE32+ files, all four supported hash algorithms, checksum
and certificate exclusions, overlays, reverse section-header order, multiple
certificate entries, certificate padding, unsupported section layouts, bounded
DER algorithm discovery, SHA-1 pattern decoys, truncated ASN.1, explicit catalog
policy forwarding and cleanup between failed/successful CI attempts.
The test compares digests against `CryptSIPCreateIndirectData` and reads the
installed system `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` as a
real-file corpus. For embedded signatures, Crypt32 independently decodes the SPC
algorithm and expected file digest. These checks do not verify signature trust.
Guard pages cover truncated headers and invalid ranges in both compatibility
and strict paths. Fault injection covers allocation/hash failures, deterministic
outputs, short certificate reads and digest ownership. A DOS stub larger than
4 MiB exercises the removed scratch-buffer limit. Older entry points retain
legacy selection/layout behavior; strict Ex APIs enforce their documented subset.
## File and export helper tests
The common harness checks explicit and zero offsets, exact/short/failed reads,
pending completion, event/object cleanup, and error propagation. Export fixtures
cover nonidentity name-ordinal mappings, forwarders, empty slots, invalid spans,
unbounded strings, and deterministic failure outputs. The export API requires a
valid loader-mapped module; it is not an arbitrary file parser.
## CI validation result and selection helpers
`run-ci-validation-tests.py` compiles the production wrapper source unchanged
(apart from replacing its private include with test dependencies) and extracts
the public declarations/constants. It checks native flag/level forwarding,
64-byte capacity on every call, the opt-in x64 experimental bit, non-x64 mode
rejection, invalid inputs, returned hash invariants, deterministic failure
scalars, preserved diagnostic policies, and result cleanup/reuse. The embedded
path covers all four explicit algorithms, unsupported/unsigned/invalid layouts,
partial digest allocation cleanup, CI failure propagation and no catalog retry.
CI/hash-layout behavior is injected; real digest behavior is checked by the
existing Windows SIP/Crypt32 differential harness. A second host build exercises
the non-x64 conditional branch; real ARM64 compilation is a separate build check.
The first kernel trace used direct CI calls. Probe v2 in CodeIntegrity-RE now
consumes these library helpers; a returned v2 trace is needed to claim runtime
verification of the wrappers themselves. The earlier native result is not
silently relabelled as an API-wrapper test.
## File-object validation flag probe
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
for `CiValidateFileObject` flag `0x20`. The host runner tests probe plumbing with
mock CI. The kernel probe must still run in a suitable VM to verify actual
policy behavior.
## Kernel compile checks and remaining validation
`mincrypt-layout-tests.cpp` asserts default modern x64/ARM64 and pre-Windows-10
compile layouts. On x64 it also includes the opt-in observed 26100 header's 14
layout assertions. Compile it with the same WDK/kernel architecture flags and
include paths as the library. This checks structure definitions, not runtime
compatibility on those systems. `ci-file-object-probe.cpp` is separately compiled
as kernel code for x64/ARM64; its real CI behavior is still untested.
The host tests do not establish CI trust policy, real asynchronous kernel I/O,
real policy ownership under Driver Verifier, revocation/timestamp behavior, or
all Windows-version/layout combinations. See the
[implementation ledger](../docs/implementation-status.md) for the remaining work.
## Consumer helper regression tests
Run `python tests/run-consumer-helper-tests.py` in an x64 Visual Studio developer
shell. This fifth host runner compiles the actual certificate/common-consumer
implementations and the existing PID name function, with kernel operations
replaced by deterministic doubles. Hashing uses real Windows CNG; independent
Crypt32 decoding/re-encoding/hash calls verify SPKI representation and pins.
The synthetic v1/v3 certificates are framing fixtures, not trust fixtures.
Coverage includes truncated certificate prefixes at inaccessible guard pages,
malformed DER lengths and optional read-only comparisons with real system-root
certificate SPKIs,
four hash algorithms, short capacities and four injected CNG failures, strict
hex conversion across all byte values, path/handle ownership, failed references,
partial publisher allocation, process-name query failures, stale-output cleanup,
and malformed/overflowing x64 chain spans. A second host build exercises the
non-x64 rejection branch without claiming ARM64 ABI or runtime emulation.
No service is started, certificate trusted, or real CI policy evaluated.
## Deprecation compiler checks
Run `python tests/run-deprecation-tests.py --arch x64` or `--arch ARM64` from a
Visual Studio developer shell with the WDK installed. This compile-only runner
uses the actual public headers: modern API references must compile under /WX,
while each of four deprecated references must fail specifically with C4996 and
its migration message. Both C and C++ are checked. It does not link or load code.
+175
View File
@@ -0,0 +1,175 @@
// Host tests validate the probe plumbing, NOT CI trust behavior.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "ci-probe-constants.inc"
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define DOMITO_CI_PROBE_HOST_TEST
typedef void* PFILE_OBJECT;
struct MINCRYPT_POLICY_INFO
{
ULONG Size;
ULONG VerificationStatus;
ULONG PolicyBits;
void* ChainInfo;
};
static unsigned calls;
static unsigned frees;
static unsigned outstanding;
static unsigned checks;
static bool failureMode;
static int fileToken;
static int chainToken;
static void
Check(
bool value,
const char* label
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", label);
std::exit(1);
}
}
static NTSTATUS
CiValidateFileObject(
PFILE_OBJECT file,
ULONG flags,
ULONG level,
MINCRYPT_POLICY_INFO* policy,
MINCRYPT_POLICY_INFO* timestamp,
LARGE_INTEGER* time,
UCHAR* hash,
UINT32* size,
ALG_ID* algorithm
)
{
const ULONG expectedFlags[] = {0, 0x20, 0x20, 0, 0, 0x20};
Check(calls < 6 && flags == expectedFlags[calls], "paired flags and order");
Check(file == &fileToken && level == 0, "same file and level zero");
Check(outstanding == 0, "previous outputs freed before next call");
Check(policy->Size == sizeof(*policy) && timestamp->Size == sizeof(*timestamp), "policy sizes");
Check(
!policy->ChainInfo && !timestamp->ChainInfo && !policy->PolicyBits && !timestamp->PolicyBits,
"fresh policy objects"
);
Check(*size == (calls < 4 ? 64u : 63u), "capacity reset for every call");
Check(*algorithm == 0 && time->QuadPart == 0, "fresh scalar outputs");
for (unsigned i = 0; i < 64; ++i)
{
Check(hash[i] == 0, "fresh hash buffer");
}
++calls;
if (*size < 64)
{
*size = 64;
return STATUS_BUFFER_TOO_SMALL;
}
policy->ChainInfo = timestamp->ChainInfo = &chainToken;
outstanding = 2;
policy->PolicyBits = 0xA5;
timestamp->PolicyBits = 0x5A;
policy->VerificationStatus = failureMode ? STATUS_INVALID_IMAGE_HASH : STATUS_SUCCESS;
timestamp->VerificationStatus = STATUS_SUCCESS;
// Also allocate on simulated failure to exercise unconditional cleanup.
if (failureMode)
{
return STATUS_INVALID_IMAGE_HASH;
}
std::memset(hash, 0xCC, 32);
*size = 32;
*algorithm = 0x800C;
time->QuadPart = 123;
return STATUS_SUCCESS;
}
static void
CiFreePolicyInfo(
MINCRYPT_POLICY_INFO* policy
)
{
++frees;
if (policy->ChainInfo)
{
Check(outstanding > 0, "owned chain");
--outstanding;
}
*policy = {};
}
#include "ci-file-object-probe.cpp"
int
main()
{
for (unsigned pass = 0; pass < 2; ++pass)
{
failureMode = pass != 0;
calls = frees = outstanding = 0;
DOMITO_CI_PROBE_RESULT results[DOMITO_CI_PROBE_RESULT_COUNT];
std::memset(results, 0xFF, sizeof(results));
DomitoProbeCiValidateFileObject(&fileToken, results);
Check(calls == 6 && frees == 12 && outstanding == 0, "all calls and frees");
for (unsigned i = 0; i < 6; ++i)
{
const auto& r = results[i];
Check(r.HashGuardIntact, "buffer guard");
const NTSTATUS expected = i >= 4 ? STATUS_BUFFER_TOO_SMALL
: failureMode ? STATUS_INVALID_IMAGE_HASH
: STATUS_SUCCESS;
Check(r.Status == expected, "status preserved");
if (i < 4)
{
Check(r.PolicyBits == 0xA5 && r.TimeStampPolicyBits == 0x5A, "policy capture");
Check(r.HasChain && r.HasTimeStampChain, "chain presence captured before free");
}
if (NT_SUCCESS(r.Status))
{
Check(
r.HashSize == 32 && r.HashAlgId == 0x800C && r.SigningTime.QuadPart == 123,
"digest metadata and signing time"
);
for (unsigned j = 0; j < 64; ++j)
{
Check(r.Hash[j] == (j < 32 ? 0xCC : 0), "digest copied and tail cleared");
}
}
else
{
for (auto byte : r.Hash)
{
Check(byte == 0, "no digest from failed call");
}
}
if (i >= 4)
{
Check(r.HashSize == 64, "required capacity captured");
}
}
}
std::printf("PASS: %u probe checks (mock CI; no runtime trust validation)\n", checks);
}
+72
View File
@@ -0,0 +1,72 @@
// Compile into an existing signed test driver; this file has no DriverEntry.
#ifndef DOMITO_CI_PROBE_HOST_TEST
#include <ntifs.h>
#include "Domito.MinCrypt.h"
#endif
#include "ci-file-object-probe.h"
_IRQL_requires_max_(PASSIVE_LEVEL)
void
DomitoProbeCiValidateFileObject(
PFILE_OBJECT FileObject,
DOMITO_CI_PROBE_RESULT* Results
)
{
const ULONG flags[DOMITO_CI_PROBE_RESULT_COUNT] = {
0,
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
0,
0,
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE
};
for (ULONG i = 0; i < DOMITO_CI_PROBE_RESULT_COUNT; ++i)
{
MINCRYPT_POLICY_INFO policy = {};
MINCRYPT_POLICY_INFO timestamp = {};
policy.Size = sizeof(policy);
timestamp.Size = sizeof(timestamp);
struct
{
UCHAR Bytes[MINCRYPT_MAX_HASH_LENGTH];
ULONG Guard;
} hash = {};
hash.Guard = 0xD01170AB;
DOMITO_CI_PROBE_RESULT& result = Results[i];
result = {};
result.Flags = flags[i];
result.InputCapacity = i < 4 ? sizeof(hash.Bytes) : sizeof(hash.Bytes) - 1;
result.HashSize = result.InputCapacity;
result.Status = CiValidateFileObject(
FileObject,
result.Flags,
0,
&policy,
&timestamp,
&result.SigningTime,
hash.Bytes,
&result.HashSize,
&result.HashAlgId
);
result.HashGuardIntact = hash.Guard == 0xD01170AB;
// Keep failure outputs as raw observations; only successful calls have
// a usable digest. Do not dereference the incompletely mapped chains.
result.PolicyBits = policy.PolicyBits;
result.TimeStampPolicyBits = timestamp.PolicyBits;
result.VerificationStatus = (NTSTATUS)policy.VerificationStatus;
result.TimeStampVerificationStatus = (NTSTATUS)timestamp.VerificationStatus;
result.HasChain = policy.ChainInfo != nullptr;
result.HasTimeStampChain = timestamp.ChainInfo != nullptr;
if (NT_SUCCESS(result.Status) && result.HashSize <= sizeof(result.Hash))
{
RtlCopyMemory(result.Hash, hash.Bytes, result.HashSize);
}
CiFreePolicyInfo(&policy);
CiFreePolicyInfo(&timestamp);
}
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
// Include ntifs.h and Domito.MinCrypt.h first. Test support, not a library API.
typedef struct _DOMITO_CI_PROBE_RESULT
{
ULONG Flags;
ULONG InputCapacity;
NTSTATUS Status;
UINT32 HashSize;
ALG_ID HashAlgId;
UCHAR Hash[MINCRYPT_MAX_HASH_LENGTH];
LARGE_INTEGER SigningTime;
ULONG PolicyBits;
ULONG TimeStampPolicyBits;
NTSTATUS VerificationStatus;
NTSTATUS TimeStampVerificationStatus;
BOOLEAN HasChain;
BOOLEAN HasTimeStampChain;
BOOLEAN HashGuardIntact;
} DOMITO_CI_PROBE_RESULT;
#define DOMITO_CI_PROBE_RESULT_COUNT 6
// PASSIVE_LEVEL; referenced file object; file bytes must remain stable.
// Writes observations, not an overall trust verdict. Frees both policies per call.
_IRQL_requires_max_(PASSIVE_LEVEL)
void
DomitoProbeCiValidateFileObject(
_In_ PFILE_OBJECT FileObject,
_Out_writes_(DOMITO_CI_PROBE_RESULT_COUNT) DOMITO_CI_PROBE_RESULT* Results
);
+519
View File
@@ -0,0 +1,519 @@
#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;
static unsigned live;
static unsigned validationCalls;
static unsigned embeddedCalls;
static unsigned digestCalls;
static unsigned digestFrees;
static NTSTATUS nativeStatus = STATUS_SUCCESS;
static NTSTATUS embeddedStatus = STATUS_SUCCESS;
static NTSTATUS digestStatus = STATUS_SUCCESS;
static UINT32 nativeAlgorithm = CALG_SHA_256;
static UINT32 nativeSize = 32;
static ULONG fakeDigestSize = 32;
static bool omitCertificate;
static bool omitDigest;
static bool partialDigestOnFailure;
static const UCHAR image[] = {1, 2, 3};
static ULONG observedFlags;
static ULONG observedLevel;
static ULONG observedCapacity;
static PFILE_OBJECT observedObject;
static ALG_ID observedEmbeddedAlgorithm;
static ULONG observedSecuritySize;
static void* observedCertificate;
static void
Check(
bool value,
const char* message
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", message);
std::exit(1);
}
}
static void*
Allocate(
SIZE_T size
)
{
void* value = std::malloc(size);
Check(value != nullptr, "host allocation");
++live;
return value;
}
static void
Release(
void* value
)
{
Check(value && live, "single allocation owner");
--live;
std::free(value);
}
static void
CiFreePolicyInfo(
MINCRYPT_POLICY_INFO* policy
)
{
if (policy->ChainInfo)
{
Release(policy->ChainInfo);
}
*policy = {};
}
static void
Populate(
MINCRYPT_POLICY_INFO* policy,
MINCRYPT_POLICY_INFO* timestamp
)
{
Check(!policy->ChainInfo && !timestamp->ChainInfo, "old result freed before reuse");
policy->Size = timestamp->Size = sizeof(*policy);
policy->ChainInfo = Allocate(1);
timestamp->ChainInfo = Allocate(1);
policy->PolicyBits = 0x123;
timestamp->PolicyBits = 0x456;
policy->VerificationStatus = STATUS_INVALID_IMAGE_HASH;
}
static NTSTATUS
CiValidateFileObject(
PFILE_OBJECT object,
UINT32 flags,
UINT32 level,
MINCRYPT_POLICY_INFO* policy,
MINCRYPT_POLICY_INFO* timestamp,
LARGE_INTEGER* time,
UINT8* hash,
UINT32* size,
ALG_ID* algorithm
)
{
++validationCalls;
observedObject = object;
observedFlags = flags;
observedLevel = level;
observedCapacity = *size;
Check(*size == 64, "fresh native hash capacity");
Check(!time->QuadPart && !*algorithm, "native scalar outputs initialized");
for (unsigned i = 0; i < 64; ++i)
{
Check(!hash[i], "native digest starts empty");
}
Populate(policy, timestamp);
time->QuadPart = 99;
std::memset(hash, 0x5a, 64);
*size = nativeSize;
*algorithm = nativeAlgorithm;
return nativeStatus;
}
static NTSTATUS
DomitoCalculatePortableExecutableDigestEx(
const UCHAR* bytes,
ULONG size,
UINT32 algorithm,
PULONG digestSize,
PVOID* digest,
LPWIN_CERTIFICATE* certificate,
PULONG securitySize
)
{
++digestCalls;
Check(bytes == image && size == sizeof(image), "same PE snapshot forwarded");
Check(
algorithm == CALG_SHA1 || algorithm == CALG_SHA_256 || algorithm == CALG_SHA_384 || algorithm == CALG_SHA_512,
"supported digest choice forwarded"
);
if (NT_SUCCESS(digestStatus) || partialDigestOnFailure)
{
*digest = omitDigest ? nullptr : Allocate(64);
if (*digest)
{
std::memset(*digest, 0xa5, 64);
}
*digestSize = fakeDigestSize;
*certificate = omitCertificate ? nullptr : const_cast<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
);
}
+395
View File
@@ -0,0 +1,395 @@
// Tests extract the unchanged production bodies. Kernel I/O is simulated.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <cctype>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define MAXUSHORT 0xffff
#define Executive 0
#define KernelMode 0
#define NotificationEvent 0
#define OBJ_KERNEL_HANDLE 0x200
#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f))
struct OBJECT_ATTRIBUTES
{
ULONG Attributes;
};
struct ANSI_STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
};
typedef ANSI_STRING* PANSI_STRING;
struct IO_STATUS_BLOCK
{
NTSTATUS Status;
ULONG_PTR Information;
};
static int objectType;
static void* objectTypeValue = &objectType;
static void** ExEventObjectType = &objectTypeValue;
static unsigned checks;
static void
Check(
bool value,
const char* label
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", label);
std::exit(1);
}
}
static LONG
RtlCompareString(
PANSI_STRING a,
PANSI_STRING b,
BOOLEAN insensitive
)
{
if (a->Length != b->Length)
{
return 1;
}
for (USHORT i = 0; i < a->Length; ++i)
{
unsigned char x = (unsigned char)a->Buffer[i], y = (unsigned char)b->Buffer[i];
if (insensitive)
{
x = (unsigned char)std::tolower(x);
y = (unsigned char)std::tolower(y);
}
if (x != y)
{
return 1;
}
}
return 0;
}
static void* exportPointer;
static ULONG exportBytes;
static PVOID
Directory(
PVOID,
BOOLEAN mapped,
USHORT index,
PULONG bytes
)
{
Check(mapped && index == IMAGE_DIRECTORY_ENTRY_EXPORT, "mapped export lookup");
*bytes = exportBytes;
return exportPointer;
}
struct
{
PVOID (*RtlImageDirectoryEntryToData)(
PVOID,
BOOLEAN,
USHORT,
PULONG
);
} G_Common = {Directory};
static int eventToken;
static int fileToken;
static unsigned created;
static unsigned referenced;
static unsigned closed;
static unsigned dereferenced;
static unsigned reads;
static unsigned waits;
static int ioMode;
static bool outstanding;
static IO_STATUS_BLOCK* pendingIosb;
static ULONG requested;
static LONGLONG expectedOffset;
static NTSTATUS
ZwCreateEvent(
HANDLE* event,
ACCESS_MASK access,
OBJECT_ATTRIBUTES* attributes,
int type,
BOOLEAN signaled
)
{
Check(
access == EVENT_ALL_ACCESS && attributes->Attributes == OBJ_KERNEL_HANDLE && type == NotificationEvent &&
!signaled,
"private kernel event"
);
if (ioMode == 4)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
++created;
*event = &eventToken;
return STATUS_SUCCESS;
}
static NTSTATUS
ObReferenceObjectByHandle(
HANDLE event,
ACCESS_MASK access,
void* type,
int mode,
void** object,
void*
)
{
Check(
event == &eventToken && access == SYNCHRONIZE && type == *ExEventObjectType && mode == KernelMode,
"reference own event"
);
if (ioMode == 5)
{
return STATUS_ACCESS_DENIED;
}
++referenced;
*object = &eventToken;
return STATUS_SUCCESS;
}
static void
ZwClose(
HANDLE event
)
{
Check(event == &eventToken && !outstanding, "close only after completion");
++closed;
}
static void
ObDereferenceObject(
void* object
)
{
Check(object == &eventToken && !outstanding, "dereference only after completion");
++dereferenced;
}
static NTSTATUS
ZwReadFile(
HANDLE file,
HANDLE event,
void*,
void*,
IO_STATUS_BLOCK* iosb,
void*,
ULONG size,
LARGE_INTEGER* offset,
void*
)
{
++reads;
Check(
file == &fileToken && event == &eventToken && offset && offset->QuadPart == expectedOffset,
"file and explicit offset"
);
Check(iosb->Status == 0 && iosb->Information == 0, "initialized IOSB");
requested = size;
if (ioMode == 6)
{
return STATUS_ACCESS_DENIED;
}
if (ioMode == 1 || ioMode == 2)
{
outstanding = true;
pendingIosb = iosb;
return STATUS_PENDING;
}
iosb->Status = STATUS_SUCCESS;
iosb->Information = ioMode == 3 ? size - 1 : size;
return STATUS_SUCCESS;
}
static NTSTATUS
KeWaitForSingleObject(
void* object,
int reason,
int mode,
BOOLEAN alertable,
LARGE_INTEGER* timeout
)
{
++waits;
Check(
object == &eventToken && reason == Executive && mode == KernelMode && !alertable && !timeout && outstanding,
"noninterruptible per-request wait"
);
outstanding = false;
pendingIosb->Status = ioMode == 2 ? STATUS_END_OF_FILE : STATUS_SUCCESS;
pendingIosb->Information = ioMode == 2 ? 0 : requested;
return STATUS_SUCCESS;
}
#include "common-under-test.inc"
static std::vector<UCHAR>
Image()
{
std::vector<UCHAR> image(4096);
auto dos = (IMAGE_DOS_HEADER*)image.data();
dos->e_magic = IMAGE_DOS_SIGNATURE;
dos->e_lfanew = 128;
auto nt = (IMAGE_NT_HEADERS64*)(image.data() + 128);
nt->Signature = IMAGE_NT_SIGNATURE;
nt->OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC;
nt->OptionalHeader.SizeOfImage = (ULONG)image.size();
auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
exports->NumberOfFunctions = 3;
exports->NumberOfNames = 2;
exports->AddressOfFunctions = 801;
exports->AddressOfNames = 901;
exports->AddressOfNameOrdinals = 1001;
const ULONG functions[] = {2000, 2100, 2200};
const ULONG names[] = {1100, 1120};
const USHORT ordinals[] = {2, 0};
std::memcpy(image.data() + 801, functions, sizeof(functions));
std::memcpy(image.data() + 901, names, sizeof(names));
std::memcpy(image.data() + 1001, ordinals, sizeof(ordinals));
std::memcpy(image.data() + 1100, "Beta", 5);
std::memcpy(image.data() + 1120, "Alpha", 6);
return image;
}
static NTSTATUS
Lookup(
std::vector<UCHAR>& image,
const char* name,
PVOID* output
)
{
ANSI_STRING string = {(USHORT)std::strlen(name), (USHORT)std::strlen(name), (PCHAR)name};
exportPointer = image.data() + 512;
exportBytes = 256;
return DomitoFindExportedFunctionAddress(image.data(), &string, output);
}
int
main()
{
auto image = Image();
PVOID output = nullptr;
Check(Lookup(image, "Beta", &output) == STATUS_SUCCESS && output == image.data() + 2200, "ordinal-indexed EAT");
Check(Lookup(image, "aLpHa", &output) == STATUS_SUCCESS && output == image.data() + 2000, "case-insensitive name");
Check(Lookup(image, "Alpha", nullptr) == STATUS_SUCCESS, "optional output discovery");
Check(Lookup(image, "missing", &output) == STATUS_NOT_FOUND && !output, "notfound clears output");
auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
const USHORT invalidOrdinal = 3;
std::memcpy(image.data() + 1001, &invalidOrdinal, 2);
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT && !output, "ordinal bounded");
image = Image();
const ULONG forwarder = 600;
std::memcpy(image.data() + 801 + 8, &forwarder, 4);
Check(Lookup(image, "Beta", &output) == STATUS_NOT_SUPPORTED && !output, "forwarder rejected");
image = Image();
const ULONG zero = 0;
std::memcpy(image.data() + 801 + 8, &zero, 4);
Check(Lookup(image, "Beta", &output) == STATUS_NOT_FOUND, "empty EAT slot");
image = Image();
const ULONG outside = 4096;
std::memcpy(image.data() + 801 + 8, &outside, 4);
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "function RVA bounded");
image = Image();
exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
exports->NumberOfNames = 0xffffffff;
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "table multiplication bounded");
image = Image();
exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
exports->AddressOfFunctions = 4090;
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "EAT span bounded");
image = Image();
const ULONG lateName = 4094;
std::memcpy(image.data() + 901, &lateName, 4);
image[4094] = 'x';
image[4095] = 'x';
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "unterminated name bounded");
image = Image();
std::memcpy(image.data() + 901, &outside, 4);
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "name RVA bounded");
image = Image();
ANSI_STRING name = {4, 4, (PCHAR) "Beta"};
exportPointer = image.data() + 512;
exportBytes = 4096;
Check(
DomitoFindExportedFunctionAddress(image.data(), &name, &output) == STATUS_INVALID_IMAGE_FORMAT,
"export directory span bounded"
);
output = (void*)1;
Check(
DomitoFindExportedFunctionAddress(nullptr, &name, &output) == STATUS_INVALID_PARAMETER && !output,
"invalid arguments clear output"
);
char buffer[32];
for (ioMode = 0; ioMode <= 6; ++ioMode)
{
created = referenced = closed = dereferenced = reads = waits = 0;
outstanding = false;
expectedOffset = 123;
LARGE_INTEGER offset;
offset.QuadPart = expectedOffset;
NTSTATUS status = DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), &offset);
const NTSTATUS expected[] = {
STATUS_SUCCESS,
STATUS_SUCCESS,
STATUS_END_OF_FILE,
STATUS_END_OF_FILE,
STATUS_INSUFFICIENT_RESOURCES,
STATUS_ACCESS_DENIED,
STATUS_ACCESS_DENIED
};
Check(status == expected[ioMode], "read status and exact-byte contract");
Check(created == closed && referenced == dereferenced && !outstanding, "event lifetime balanced");
Check(waits == ((ioMode == 1 || ioMode == 2) ? 1u : 0u), "wait pending only");
}
ioMode = 0;
expectedOffset = 0;
Check(DomitoReadFile(&fileToken, buffer, sizeof(buffer)) == STATUS_SUCCESS, "zero-offset wrapper");
LARGE_INTEGER offset = {};
offset.QuadPart = -1;
Check(
DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), &offset) == STATUS_INVALID_PARAMETER,
"negative offset rejected"
);
Check(
DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), nullptr) == STATUS_INVALID_PARAMETER,
"null offset rejected"
);
offset.QuadPart = 0;
Check(DomitoReadFileAt(&fileToken, nullptr, 0, &offset) == STATUS_SUCCESS, "zero byte read");
Check(DomitoReadFileAt(&fileToken, nullptr, 1, &offset) == STATUS_INVALID_PARAMETER, "null buffer rejected");
std::printf("PASS: %u common-helper checks (mock kernel I/O)\n", checks);
}
+967
View File
@@ -0,0 +1,967 @@
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <bcrypt.h>
#include <wincrypt.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <initializer_list>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define PASSIVE_LEVEL 0
#define PAGED_CODE() ((void)0)
#undef NTDDI_VERSION
#define NTDDI_VERSION 0x0a000008
#undef NTDDI_WIN10
#define NTDDI_WIN10 0x0a000000
#define DOMITO_POOL_TAG 0x446f6d69
#define MAXUSHORT 0xffff
#define PagedPool 1
#define KernelMode 0
#define ProcessImageFileName 27
#define OBJ_KERNEL_HANDLE 0x200
#define OBJ_CASE_INSENSITIVE 0x40
#define FILE_NON_DIRECTORY_FILE 0x40
#define FILE_SYNCHRONOUS_IO_NONALERT 0x20
typedef void* PEPROCESS;
typedef void* PFILE_OBJECT;
typedef const UCHAR* PCUCHAR;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _ANSI_STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} ANSI_STRING, *PANSI_STRING;
struct OBJECT_ATTRIBUTES
{
ULONG Attributes;
PUNICODE_STRING ObjectName;
};
struct CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
};
struct IO_STATUS_BLOCK
{
NTSTATUS Status;
ULONG_PTR Information;
};
#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f), (a)->ObjectName = (n))
#define RTL_CONSTANT_STRING(s) {sizeof(s) - sizeof((s)[0]), sizeof(s), const_cast<char*>(s)}
typedef PVOID(NTAPI* PMINCRYPT_ALLOCATE_ROUTINE)(SIZE_T);
typedef PVOID NTAPI MINCRYPT_ALLOCATE_ROUTINE(SIZE_T);
#include "consumer-api.inc"
static unsigned checks;
static unsigned live;
static unsigned refs;
static unsigned derefs;
static unsigned closed;
static unsigned validated;
static unsigned opened;
static unsigned queried;
static int failure;
static int cryptoFailure;
static NTSTATUS moduleStatus = STATUS_SUCCESS;
static NTSTATUS exportStatus = STATUS_SUCCESS;
static NTSTATUS validateStatus = STATUS_SUCCESS;
static int fileToken;
static int processToken;
static void* fileTypeValue = &fileToken;
static void** IoFileObjectType = &fileTypeValue;
static void
Check(
bool value,
const char* msg
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", msg);
std::exit(1);
}
}
static void*
Allocate(
SIZE_T size
)
{
if (failure == 1)
{
return nullptr;
}
void* p = std::malloc(size);
Check(p != nullptr, "allocation");
++live;
std::memset(p, 0xa5, size);
return p;
}
static void
Release(
void* p
)
{
Check(p && live, "single owner release");
--live;
std::free(p);
}
static struct
{
PVOID (*Allocate)(SIZE_T);
void (*Free)(PVOID);
} G_Memory = {Allocate, Release};
static PVOID
ExAllocatePoolWithTag(
int,
SIZE_T size,
ULONG
)
{
return Allocate(size);
}
static void
ExFreePoolWithTag(
void* p,
ULONG
)
{
Release(p);
}
static void
ExFreePool(
void* p
)
{
Release(p);
}
static NTSTATUS
FakePublisher(
MINCERT_BLOB*,
PMINCRYPT_ALLOCATE_ROUTINE allocator,
PUNICODE_STRING name
)
{
name->Buffer = static_cast<PWCHAR>(allocator(12));
if (!name->Buffer)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
std::memcpy(name->Buffer, L"hello", 12);
name->Length = 10;
name->MaximumLength = 12;
if (failure == 8)
{
return STATUS_UNSUCCESSFUL;
}
if (failure == 9)
{
name->Length = 13;
}
return STATUS_SUCCESS;
}
static NTSTATUS
DomitoFindModuleBaseAddress(
PANSI_STRING name,
PVOID* module
)
{
Check(name && name->Length, "module name");
if (NT_SUCCESS(moduleStatus))
{
*module = &fileToken;
}
return moduleStatus;
}
static NTSTATUS
DomitoFindExportedFunctionAddress(
PVOID module,
PANSI_STRING name,
PVOID* routine
)
{
Check(module == &fileToken && name->Length, "export resolution");
if (NT_SUCCESS(exportStatus))
{
*routine = reinterpret_cast<PVOID>(FakePublisher);
}
return exportStatus;
}
void
DomitoFreeCiValidationResult(
DOMITO_CI_VALIDATION_RESULT* result
)
{
if (result->PolicyInfo.ChainInfo)
{
Release(result->PolicyInfo.ChainInfo);
}
*result = {};
}
NTSTATUS
DomitoValidateFileObjectEx(
PFILE_OBJECT object,
UINT32 flags,
UINT8 level,
DOMITO_CI_SIGNATURE_SELECTION selection,
DOMITO_CI_VALIDATION_RESULT* result
)
{
++validated;
Check(
object == &fileToken && flags == 0x20 && level == 7 && selection == DomitoCiSignatureSelectionNative,
"CI arguments unchanged"
);
Check(!result->PolicyInfo.ChainInfo, "result reset before native call");
result->PolicyInfo.ChainInfo = static_cast<MINCRYPT_CHAIN_INFO*>(Allocate(1));
return validateStatus;
}
static NTSTATUS
ObReferenceObjectByHandle(
HANDLE handle,
ACCESS_MASK access,
void* type,
int mode,
void** object,
void*
)
{
Check(
handle == &fileToken && access == FILE_READ_DATA && type == *IoFileObjectType && mode == KernelMode,
"trusted handle reference"
);
if (failure == 2)
{
return STATUS_ACCESS_DENIED;
}
++refs;
*object = &fileToken;
return STATUS_SUCCESS;
}
static void
ObDereferenceObject(
void* object
)
{
Check(object == &fileToken, "file dereference");
++derefs;
}
static NTSTATUS
ZwOpenFile(
HANDLE* handle,
ACCESS_MASK access,
OBJECT_ATTRIBUTES* a,
IO_STATUS_BLOCK*,
ULONG share,
ULONG options
)
{
++opened;
Check(
access == (FILE_READ_DATA | SYNCHRONIZE) && a->Attributes == (OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE) &&
share == FILE_SHARE_READ && options == (FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT),
"read-only stable path open"
);
if (failure == 3)
{
return STATUS_OBJECT_NAME_NOT_FOUND;
}
*handle = &fileToken;
return STATUS_SUCCESS;
}
static void
ZwClose(
HANDLE handle
)
{
Check(handle == &fileToken || handle == &processToken, "close owned handle");
++closed;
}
static void
FillName(
PUNICODE_STRING name
)
{
name->Length = 8;
name->MaximumLength = 10;
name->Buffer = reinterpret_cast<PWCHAR>(name + 1);
std::memcpy(name->Buffer, L"test", 10);
}
static NTSTATUS
SeLocateProcessImageName(
PEPROCESS process,
PUNICODE_STRING* image
)
{
Check(process == &processToken, "referenced process passed unchanged");
if (failure == 4)
{
return STATUS_NOT_FOUND;
}
// OS allocations are independent of configurable Domito allocator failures.
*image = static_cast<PUNICODE_STRING>(std::malloc(sizeof(UNICODE_STRING) + 10));
++live;
FillName(*image);
if (failure == 5)
{
(*image)->Length = 9;
}
return STATUS_SUCCESS;
}
static NTSTATUS
ZwOpenProcess(
HANDLE* process,
ACCESS_MASK access,
OBJECT_ATTRIBUTES* a,
CLIENT_ID* cid
)
{
Check(
access == 0x1000 && a->Attributes == OBJ_KERNEL_HANDLE && cid->UniqueProcess == ULongToHandle(123),
"PID least privilege kernel open"
);
if (failure == 6)
{
return STATUS_ACCESS_DENIED;
}
*process = &processToken;
return STATUS_SUCCESS;
}
static NTSTATUS
QueryProcess(
HANDLE process,
int info,
PVOID buffer,
ULONG size,
PULONG returned
)
{
++queried;
Check(process == &processToken && info == ProcessImageFileName, "PID query bound to one handle");
*returned = sizeof(UNICODE_STRING) + 10;
if (!buffer)
{
return failure == 10 ? STATUS_SUCCESS : STATUS_INFO_LENGTH_MISMATCH;
}
Check(size == *returned, "sized name allocation");
FillName(static_cast<PUNICODE_STRING>(buffer));
if (failure == 7)
{
return STATUS_UNSUCCESSFUL;
}
if (failure == 11)
{
static_cast<PUNICODE_STRING>(buffer)->Buffer = reinterpret_cast<PWCHAR>(1);
}
return STATUS_SUCCESS;
}
static struct
{
NTSTATUS (*ZwQueryInformationProcess)(
HANDLE,
int,
PVOID,
ULONG,
PULONG
);
} G_Common = {QueryProcess};
static NTSTATUS
OpenAlgorithm(
BCRYPT_ALG_HANDLE* a,
LPCWSTR n,
LPCWSTR p,
ULONG f
)
{
return cryptoFailure == 1 ? STATUS_UNSUCCESSFUL : BCryptOpenAlgorithmProvider(a, n, p, f);
}
static NTSTATUS
CreateHash(
BCRYPT_ALG_HANDLE a,
BCRYPT_HASH_HANDLE* h,
PUCHAR o,
ULONG os,
PUCHAR s,
ULONG ss,
ULONG f
)
{
return cryptoFailure == 2 ? STATUS_UNSUCCESSFUL : BCryptCreateHash(a, h, o, os, s, ss, f);
}
static NTSTATUS
HashData(
BCRYPT_HASH_HANDLE h,
PUCHAR p,
ULONG n,
ULONG f
)
{
return cryptoFailure == 3 ? STATUS_UNSUCCESSFUL : BCryptHashData(h, p, n, f);
}
static NTSTATUS
FinishHash(
BCRYPT_HASH_HANDLE h,
PUCHAR p,
ULONG n,
ULONG f
)
{
if (cryptoFailure == 4)
{
std::memset(p, 0xff, n);
return STATUS_UNSUCCESSFUL;
}
return BCryptFinishHash(h, p, n, f);
}
#define BCryptOpenAlgorithmProvider OpenAlgorithm
#define BCryptCreateHash CreateHash
#define BCryptHashData HashData
#define BCryptFinishHash FinishHash
#ifdef DOMITO_TEST_OTHER_ARCH
#undef _M_AMD64
#undef _AMD64_
#endif
#include "consumer-under-test.inc"
#undef BCryptOpenAlgorithmProvider
#undef BCryptCreateHash
#undef BCryptHashData
#undef BCryptFinishHash
using Bytes = std::vector<UCHAR>;
static Bytes
Der(
UCHAR tag,
const Bytes& contents
)
{
Bytes b = {tag};
if (contents.size() < 128)
{
b.push_back(static_cast<UCHAR>(contents.size()));
}
else
{
Check(contents.size() < 256, "fixture DER bound");
b.push_back(0x81);
b.push_back(static_cast<UCHAR>(contents.size()));
}
b.insert(b.end(), contents.begin(), contents.end());
return b;
}
static Bytes
Join(
std::initializer_list<Bytes> values
)
{
Bytes b;
for (const auto& v : values)
{
b.insert(b.end(), v.begin(), v.end());
}
return b;
}
static Bytes
Fixture(
bool version
)
{
Bytes sig = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 11}), Der(5, {})}));
Bytes rsa = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 1}), Der(5, {})}));
Bytes name = Der(0x30, Der(0x31, Der(0x30, Join({Der(6, {0x55, 4, 3}), Der(0x0c, {'t', 'e', 's', 't'})}))));
const std::string date = "260101000000Z";
Bytes time = Der(0x17, Bytes(date.begin(), date.end()));
Bytes key = Der(0x30, Join({Der(2, {0, 0xab, 0xcd}), Der(2, {1, 0, 1})}));
key.insert(key.begin(), 0);
Bytes spki = Der(0x30, Join({rsa, Der(3, key)}));
Bytes tbs = Join(
{version ? Der(0xa0, Der(2, {2})) : Bytes{}, Der(2, {1}), sig, name, Der(0x30, Join({time, time})), name, spki}
);
return Der(0x30, Join({Der(0x30, tbs), sig, Der(3, {0, 0xab, 0xcd})}));
}
static void
TestCertificates()
{
for (bool v : {false, true})
{
auto cert = Fixture(v);
DOMITO_CERTIFICATE_SPKI spki = {};
Check(
NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast<ULONG>(cert.size()), &spki)),
"v1/v3 extraction"
);
Check(
spki.Content.Data > spki.Encoded.Data && spki.Content.Size < spki.Encoded.Size,
"content and complete TLV differ"
);
auto context = CertCreateCertificateContext(X509_ASN_ENCODING, cert.data(), static_cast<DWORD>(cert.size()));
Check(context != nullptr, "Crypt32 independently decodes fixture certificate");
BYTE* encoded = nullptr;
DWORD size = 0;
Check(
CryptEncodeObjectEx(
X509_ASN_ENCODING,
X509_PUBLIC_KEY_INFO,
&context->pCertInfo->SubjectPublicKeyInfo,
CRYPT_ENCODE_ALLOC_FLAG,
nullptr,
&encoded,
&size
) != FALSE,
"Crypt32 independent SPKI encoding"
);
Check(
size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size),
"extracted complete SPKI equals Crypt32"
);
LocalFree(encoded);
CertFreeCertificateContext(context);
UCHAR full[64];
UCHAR legacy[64];
UCHAR whole[64];
UCHAR oracle[64];
ULONG a = 64;
ULONG b = 64;
ULONG c = 64;
DWORD d = 64;
Check(
NT_SUCCESS(DomitoHashCertificate(
cert.data(), static_cast<ULONG>(cert.size()), DomitoCertificateHashSpkiDer, CALG_SHA_256, full, &a
)),
"full SPKI hash"
);
Check(
NT_SUCCESS(DomitoHashCertificate(
cert.data(),
static_cast<ULONG>(cert.size()),
DomitoCertificateHashSpkiContentLegacy,
CALG_SHA_256,
legacy,
&b
)),
"legacy content SPKI hash"
);
Check(a == 32 && b == 32 && std::memcmp(full, legacy, 32) != 0, "explicit SPKI conventions differ");
Check(
CryptHashCertificate2(
BCRYPT_SHA256_ALGORITHM, 0, nullptr, spki.Content.Data, spki.Content.Size, oracle, &d
) != FALSE &&
d == b && !std::memcmp(legacy, oracle, d),
"legacy hash matches exact content-only oracle"
);
Check(
NT_SUCCESS(DomitoHashCertificate(
cert.data(), static_cast<ULONG>(cert.size()), DomitoCertificateHashDer, CALG_SHA_256, whole, &c
)),
"DER thumbprint"
);
d = 64;
Check(
CryptHashCertificate2(
BCRYPT_SHA256_ALGORITHM, 0, nullptr, cert.data(), static_cast<DWORD>(cert.size()), oracle, &d
) != FALSE &&
d == c && !std::memcmp(whole, oracle, d),
"thumbprint matches Crypt32"
);
for (ULONG i = 0; i < cert.size(); ++i)
{
spki.Encoded = {cert.data(), 99};
Check(!NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), i, &spki)), "every truncated certificate rejected");
Check(!spki.Encoded.Data && !spki.Content.Data, "failed extraction clears borrowed views");
}
cert.push_back(0);
Check(
!NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast<ULONG>(cert.size()), &spki)),
"trailing byte rejected"
);
}
for (auto malformed :
{Bytes{0x30, 0x80, 0, 0},
Bytes{0x30, 0x81, 1, 0},
Bytes{0x30, 0x85, 1, 2, 3, 4, 5},
Bytes{0x30, 0x84, 0xff, 0xff, 0xff, 0xff}})
{
DOMITO_CERTIFICATE_SPKI spki = {};
Check(
!NT_SUCCESS(DomitoGetCertificateSpki(malformed.data(), static_cast<ULONG>(malformed.size()), &spki)),
"bad DER lengths rejected"
);
}
const UCHAR abc[] = {'a', 'b', 'c'};
UCHAR hash[64];
for (ALG_ID alg : {CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512})
{
ULONG n = 0;
Check(DomitoHashBuffer(abc, 3, alg, nullptr, &n) == STATUS_BUFFER_TOO_SMALL, "hash sizing");
Check(n == 20 || n == 32 || n == 48 || n == 64, "known output size");
ULONG cap = n - 1;
Check(DomitoHashBuffer(abc, 3, alg, hash, &cap) == STATUS_BUFFER_TOO_SMALL && cap == n, "short hash capacity");
cap = 64;
Check(NT_SUCCESS(DomitoHashBuffer(nullptr, 0, alg, hash, &cap)), "empty hash");
}
for (int fail = 1; fail <= 4; ++fail)
{
cryptoFailure = fail;
ULONG n = 64;
std::memset(hash, 0xff, 64);
Check(!NT_SUCCESS(DomitoHashBuffer(abc, 3, CALG_SHA_256, hash, &n)) && !n, "CNG failure propagated");
for (int i = 0; i < 32; ++i)
{
Check(!hash[i], "CNG failure digest erased");
}
}
cryptoFailure = 0;
for (unsigned byte = 0; byte < 256; ++byte)
{
UCHAR in = static_cast<UCHAR>(byte);
UCHAR out = 0;
WCHAR hex[3];
Check(NT_SUCCESS(DomitoEncodeHex(&in, 1, hex, 3)), "hex encoding");
UNICODE_STRING str = {4, 6, hex};
Check(NT_SUCCESS(DomitoDecodeHex(&str, &out, 1)) && out == in, "all byte values hex roundtrip");
}
for (WCHAR ch : {L'g', L'/', L':', L' ', L'\0', L'\u1234'})
{
WCHAR text[] = {L'0', ch};
UNICODE_STRING str = {4, 4, text};
UCHAR out = 9;
Check(
DomitoDecodeHex(&str, &out, 1) == STATUS_INVALID_PARAMETER && !out,
"invalid hex digit rejected and output zeroed"
);
}
WCHAR shortHex[2];
Check(DomitoEncodeHex(abc, 3, shortHex, 2) == STATUS_BUFFER_TOO_SMALL && !shortHex[0], "short hex buffer cleared");
// Put every truncated prefix immediately before an inaccessible page.
SYSTEM_INFO system = {};
GetSystemInfo(&system);
const SIZE_T page = system.dwPageSize;
auto guarded = static_cast<PUCHAR>(VirtualAlloc(nullptr, 2 * page, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
Check(guarded != nullptr, "guard-page allocation");
DWORD old = 0;
Check(VirtualProtect(guarded + page, page, PAGE_NOACCESS, &old) != FALSE, "guard page installed");
const auto fixture = Fixture(true);
for (ULONG n = 0; n < fixture.size(); ++n)
{
UCHAR* input = guarded + page - n;
std::memcpy(input, fixture.data(), n);
DOMITO_CERTIFICATE_SPKI spki = {};
Check(!NT_SUCCESS(DomitoGetCertificateSpki(input, n, &spki)), "truncated DER at guard page rejected");
}
VirtualFree(guarded, 0, MEM_RELEASE);
// Read-only optional corpus: exercises real long-length RSA/ECC certificates.
HCERTSTORE store = CertOpenSystemStoreW(0, L"ROOT");
if (store)
{
PCCERT_CONTEXT context = nullptr;
unsigned count = 0;
while (count < 16 && (context = CertEnumCertificatesInStore(store, context)) != nullptr)
{
++count;
DOMITO_CERTIFICATE_SPKI spki = {};
Check(
NT_SUCCESS(DomitoGetCertificateSpki(context->pbCertEncoded, context->cbCertEncoded, &spki)),
"real certificate SPKI extraction"
);
BYTE* encoded = nullptr;
DWORD size = 0;
Check(
CryptEncodeObjectEx(
X509_ASN_ENCODING,
X509_PUBLIC_KEY_INFO,
&context->pCertInfo->SubjectPublicKeyInfo,
CRYPT_ENCODE_ALLOC_FLAG,
nullptr,
&encoded,
&size
) != FALSE,
"real Crypt32 SPKI encoding"
);
Check(
size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size),
"real certificate SPKI equals Crypt32"
);
LocalFree(encoded);
}
if (context)
{
CertFreeCertificateContext(context);
}
CertCloseStore(store, 0);
}
}
static void
TestOwnership()
{
DOMITO_CI_VALIDATION_RESULT result = {};
UNICODE_STRING path = {8, 10, const_cast<PWCHAR>(L"\\foo")};
for (int fail : {0, 2, 3})
{
failure = fail;
const auto r = refs, d = derefs, c = closed;
result.PolicyInfo.ChainInfo = static_cast<MINCRYPT_CHAIN_INFO*>(Allocate(1));
NTSTATUS s = DomitoValidateFilePathEx(&path, 0x20, 7, DomitoCiSignatureSelectionNative, &result);
Check(NT_SUCCESS(s) == (fail == 0), "path/reference failure returned");
Check(
refs - r == derefs - d && closed - c == (fail == 3 ? 0u : 1u),
"all references and owned path handles released"
);
DomitoFreeCiValidationResult(&result);
Check(!live, "path no allocations leaked");
}
failure = 0;
validateStatus = STATUS_INVALID_IMAGE_HASH;
const auto c = closed;
Check(
DomitoValidateFileHandleEx(&fileToken, 0x20, 7, DomitoCiSignatureSelectionNative, &result) == validateStatus,
"CI failure retained"
);
Check(closed == c && result.PolicyInfo.ChainInfo, "borrowed handle open and diagnostics retained");
DomitoFreeCiValidationResult(&result);
validateStatus = STATUS_SUCCESS;
for (int fail : {0, 1, 4, 5})
{
failure = fail;
PUNICODE_STRING name = reinterpret_cast<PUNICODE_STRING>(1);
NTSTATUS s = DomitoGetProcessImageNameEx(&processToken, &name);
Check(NT_SUCCESS(s) == (fail == 0), "PEPROCESS lookup failure");
if (NT_SUCCESS(s))
{
Check(
name->Buffer == reinterpret_cast<PWCHAR>(name + 1) && name->Length == 8,
"one contiguous process-name allocation"
);
}
else
{
Check(!name, "failed process name null");
}
DomitoFreeProcessImageName(name);
Check(!live, "process-name allocations released");
}
for (int fail : {0, 1, 6, 7, 10, 11})
{
failure = fail;
PUNICODE_STRING name = reinterpret_cast<PUNICODE_STRING>(1);
NTSTATUS s = DomitoGetProcessImageName(123, &name);
Check(NT_SUCCESS(s) == (fail == 0), "PID query failure");
if (!NT_SUCCESS(s))
{
Check(!name, "PID failure never leaves freed pointer");
}
DomitoFreeProcessImageName(name);
Check(!live, "PID query cleanup");
}
failure = 0;
MINCERT_BLOB blob = {1, reinterpret_cast<PUCHAR>(&fileToken)};
for (int fail : {0, 1, 8, 9})
{
failure = fail;
UNICODE_STRING name = {};
NTSTATUS s = DomitoGetCertificatePublisherName(&blob, &name);
Check(NT_SUCCESS(s) == (fail == 0), "publisher error propagation");
DomitoFreeCertificatePublisherName(&name);
DomitoFreeCertificatePublisherName(&name);
Check(!live && !name.Buffer, "publisher matching free including partial failure");
}
failure = 0;
ANSI_STRING module = RTL_CONSTANT_STRING("module");
ANSI_STRING name = RTL_CONSTANT_STRING("export");
PVOID address = reinterpret_cast<PVOID>(1);
moduleStatus = STATUS_NOT_FOUND;
Check(
DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_FOUND && !address,
"missing module nulls export"
);
moduleStatus = STATUS_SUCCESS;
exportStatus = STATUS_NOT_SUPPORTED;
address = reinterpret_cast<PVOID>(1);
Check(
DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_SUPPORTED && !address,
"unsupported forwarder status preserved"
);
exportStatus = STATUS_SUCCESS;
}
static void
TestChains()
{
alignas(8) UCHAR storage[512] = {};
auto chain = reinterpret_cast<MINCRYPT_CHAIN_INFO*>(storage);
chain->Size = sizeof(storage);
chain->ChainElements = reinterpret_cast<MINCRYPT_CHAIN_ELEMENT*>(storage + 56);
chain->NumberOfChainElement = 2;
for (ULONG i = 0; i < 2; ++i)
{
chain->ChainElements[i].Certificate = {1, storage + 400 + i};
chain->ChainElements[i].HashSize = 32;
}
MINCRYPT_POLICY_INFO policy = {};
policy.Size = sizeof(policy);
policy.ChainInfo = chain;
DOMITO_CI_CHAIN_VIEW view = {};
#ifdef DOMITO_TEST_OTHER_ARCH
Check(
DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view) == STATUS_NOT_SUPPORTED && !view.Count,
"non-x64 rejects unverified layout"
);
#else
Check(
NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && view.Count == 2,
"minimal historical 56-byte chain prefix"
);
// The last certificate is an arbitrary byte, not a proven self-signed root.
chain->ChainElements[1].Certificate.Data = storage + 512;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && !view.Count,
"child span past allocation rejected"
);
chain->ChainElements[1].Certificate.Data = storage + 401;
chain->ChainElements[0].Subject = {reinterpret_cast<PCHAR>(storage + 500), 13};
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"malformed name span rejected"
);
chain->ChainElements[0].Subject = {};
chain->NumberOfChainElement = 0xffffffff;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"element multiplication overflow rejected"
);
chain->NumberOfChainElement = 2;
chain->ChainElements[0].HashSize = 65;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"oversize CI element hash rejected"
);
chain->ChainElements[0].HashSize = 32;
for (ULONG n = 0; n < 56; ++n)
{
chain->Size = n;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"short historical header rejected"
);
}
#endif
}
int
main()
{
TestCertificates();
TestOwnership();
TestChains();
Check(!live, "all allocations released");
std::printf("PASS: %u consumer-helper checks, no live allocations\n", checks);
}
File diff suppressed because it is too large. Load diff
+55
View File
@@ -0,0 +1,55 @@
// Compile with the WDK for supported 64-bit targets. No driver is linked/loaded.
#include <ntifs.h>
#include "Domito.MinCrypt.h"
#if NTDDI_VERSION >= NTDDI_WIN10
C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 48);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_POLICY_INFO,
VerificationStatus
) == 4
);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_POLICY_INFO,
ChainInfo
) == 16
);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_POLICY_INFO,
NotBefore
) == 32
);
C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 72);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_CHAIN_INFO,
ChainElements
) == 40
);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_CHAIN_INFO,
AuthenticodeAttributes
) == 56
);
#else
C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 32);
C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 40);
#endif
C_ASSERT(sizeof(MINCRYPT_NAME_BLOB) == 16);
C_ASSERT(sizeof(MINCRYPT_CHAIN_ELEMENT) == 120);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_CHAIN_ELEMENT,
Certificate
) == 104
);
#if defined(_M_AMD64)
// These opt-in declarations establish observations only for the named x64 build.
#include "Domito.MinCrypt.26100.h"
#include "Domito.MinCrypt.Historical.h"
#endif
+34
View File
@@ -0,0 +1,34 @@
"""Test the actual kernel probe body with mock CI; run in an x64 VS dev shell."""
from pathlib import Path
import re
import subprocess
import tempfile
tests = Path(__file__).resolve().parent
header = (tests.parent / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
names = ["MINCRYPT_MAX_HASH_LENGTH", "DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE"]
constants = "\n".join(
re.search(r"^#define\s+" + name + r"\s+[^\r\n]+", header, re.M)[0] for name in names
)
with tempfile.TemporaryDirectory(prefix="domito-ci-probe-") as temp:
build = Path(temp)
(build / "ci-probe-constants.inc").write_text(constants + "\n", encoding="utf-8")
exe = build / "ci-probe-tests.exe"
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
str(tests / "ci-file-object-probe-tests.cpp"),
"/Fe" + str(exe),
"/Fo" + str(build / "ci-probe-tests.obj"),
],
cwd=build,
check=True,
)
subprocess.run([str(exe)], cwd=build, check=True)
+53
View File
@@ -0,0 +1,53 @@
"""Compile the actual production CI wrapper body against deterministic CI doubles."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
header = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
start = header.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards")
end = header.index("#define DOMITO_CI_SOURCE_EMBEDDED", start)
constants_header = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
constants = "\n".join(
line
for line in constants_header.splitlines()
if line.startswith(
(
"#define DOMITO_CI_VALIDATE_FLAG_",
"#define MINCRYPT_MAX_HASH_LENGTH",
"#define MINCRYPT_SHA",
)
)
)
source = (root / "src/Domito.CodeIntegrity.Validation.cpp").read_text(encoding="utf-8-sig")
source = source.replace('#include "Domito.Internal.h"', "")
with tempfile.TemporaryDirectory(prefix="domito-ci-validation-") as temporary:
directory = Path(temporary)
(directory / "ci-validation-api.inc").write_text(
constants + "\n" + header[start:end], encoding="utf-8"
)
(directory / "ci-validation-under-test.inc").write_text(source, encoding="utf-8")
for branch in ["x64", "other"]:
program = directory / (branch + ".exe")
flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else []
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/DNTDDI_VERSION=0x0A000008",
"/DNTDDI_WIN10=0x0A000000",
"/I" + str(directory),
*flags,
str(Path(__file__).with_name("ci-validation-tests.cpp")),
"/Fe" + str(program),
"/Fo" + str(directory / (branch + ".obj")),
],
cwd=directory,
check=True,
)
subprocess.run([str(program)], check=True)
+35
View File
@@ -0,0 +1,35 @@
"""Run production export and file-read helpers with deterministic user-mode doubles."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
source = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
export_start = source.index("// This helper handles directory RVAs")
export_end = source.index("_Success_", source.index("DomitoFindExportedFunctionAddress(") + 1)
read_start = source.rfind("_Success_", 0, source.index("DomitoReadFileAt("))
read_end = source.index("_Success_", source.index("DomitoReadFile(") + 1)
with tempfile.TemporaryDirectory(prefix="domito-common-tests-") as temp:
build = Path(temp)
(build / "common-under-test.inc").write_text(
source[export_start:export_end] + "\n" + source[read_start:read_end], encoding="utf-8"
)
exe = build / "common-tests.exe"
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
str(root / "tests/common-tests.cpp"),
"/Fe" + str(exe),
"/Fo" + str(build / "common-tests.obj"),
],
cwd=build,
check=True,
)
subprocess.run([str(exe)], cwd=build, check=True)
+71
View File
@@ -0,0 +1,71 @@
"""Production consumer helpers with real CNG/Crypt32 and deterministic kernel doubles."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
public = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
mincrypt = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
structures = mincrypt[
mincrypt.index("#include <pshpack8.h>") : mincrypt.index("#include <poppack.h>")
+ len("#include <poppack.h>")
]
validation = public[
public.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards") : public.index(
"#define DOMITO_CI_SOURCE_EMBEDDED"
)
]
helpers = (
(root / "include/Domito.Consumer.h")
.read_text(encoding="utf-8-sig")
.replace('#include "Domito.h"', "")
)
algorithm = public[
public.index("#if !defined(CALG_SHA1)") : public.index(
"/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM(")
)
]
common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
process_start = common.rfind("_Success_", 0, common.index("DomitoGetProcessImageName("))
process = (
common[process_start:]
.replace('#pragma code_seg("PAGED")', "")
.replace("#pragma code_seg()", "")
)
with tempfile.TemporaryDirectory(prefix="domito-consumer-tests-") as temp:
build = Path(temp)
(build / "consumer-api.inc").write_text(
algorithm + "\n" + structures + "\n" + validation + "\n" + helpers
)
bodies = "\n".join(
(root / ("src/" + name))
.read_text(encoding="utf-8-sig")
.replace('#include "Domito.Internal.h"', "")
for name in ["Domito.Certificate.cpp", "Domito.Consumer.cpp"]
)
(build / "consumer-under-test.inc").write_text(bodies + "\n" + process)
for branch in ["x64", "other"]:
exe = build / (branch + ".exe")
flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else []
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
*flags,
str(root / "tests/consumer-helper-tests.cpp"),
"/Fe" + str(exe),
"/Fo" + str(build / (branch + ".obj")),
"/link",
"bcrypt.lib",
"crypt32.lib",
],
cwd=build,
check=True,
)
subprocess.run([str(exe)], check=True)
+94
View File
@@ -0,0 +1,94 @@
"""Compile actual public headers and verify MSVC C4996 migration diagnostics."""
import argparse
import os
from pathlib import Path
import subprocess
import tempfile
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--arch", choices=["x64", "ARM64"], default="x64")
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
vc = Path(os.environ["VCToolsInstallDir"])
kit = Path(os.environ["WindowsSdkDir"]) / "Include" / os.environ["WindowsSDKVersion"].strip("\\/")
compiler = vc / "bin/Hostx64" / args.arch.lower() / "cl.exe"
architecture = ["/D_AMD64_", "/D_AMD64", "/Gz"] if args.arch == "x64" else ["/D_ARM64_", "/DARM64"]
common = [
"/nologo",
"/c",
"/kernel",
"/W4",
"/WX",
"/we4996",
"/D_WIN64",
"/DWINNT=1",
"/DNTDDI_VERSION=0x0A000008",
"/D_WIN32_WINNT=0x0A00",
*architecture,
"/I" + str(vc / "include"),
"/I" + str(kit / "km"),
"/I" + str(kit / "shared"),
"/I" + str(kit / "ucrt"),
"/I" + str(root / "include"),
]
old = {
"DomitoGetPortableExecutableDigestKind": "DomitoGetPortableExecutableDigestKindEx",
"DomitoCalculatePortableExecutableDigest": "DomitoCalculatePortableExecutableDigestEx",
"DomitoValidateFileLegacyMode": "DomitoValidateFileLegacyModeEx",
"DomitoGetProcessImageName": "DomitoGetProcessImageNameEx",
}
modern = [
*old.values(),
"DomitoFreePortableExecutableDigest",
"DomitoFreeProcessImageName",
"DomitoValidateFileObjectEx",
"DomitoValidateFileHandleEx",
"DomitoValidateFilePathEx",
"DomitoGetCiChainView",
"DomitoGetCertificatePublisherName",
"DomitoFindModuleBaseAddress",
"DomitoFindExportedFunctionAddress",
"DomitoReadFile",
"DomitoReadFileAt",
"CiValidateFileObject",
]
prefix = "#include <ntifs.h>\n#include <ntintsafe.h>\n#include <ntimage.h>\n#include <bcrypt.h>\n#include <Domito.h>\n"
checks = 0
with tempfile.TemporaryDirectory(prefix="domito-deprecation-") as temp:
build = Path(temp)
for language in ["c", "cpp"]:
for index, (name, replacement) in enumerate([("modern", None), *old.items()]):
source = build / (f"check-{index}." + language)
refs = modern if replacement is None else [name]
source.write_text(
prefix
+ "void Check(void) {\n"
+ "".join(f" (void)&{ref};\n" for ref in refs)
+ "}\n"
)
result = subprocess.run(
[
str(compiler),
*common,
"/TC" if language == "c" else "/TP",
"/Fo" + str(build / "check.obj"),
str(source),
],
text=True,
capture_output=True,
)
output = result.stdout + result.stderr
if replacement is None:
assert result.returncode == 0, output
else:
assert (
result.returncode != 0
and "C4996" in output
and name in output
and replacement in output
), output
errors = [line for line in output.splitlines() if "error C" in line]
assert errors and all("C4996" in line for line in errors), output
checks += 1
print(f"PASS: {checks} {args.arch} C/C++ public-header deprecation checks")
+57
View File
@@ -0,0 +1,57 @@
"""Run in an x64 VS developer shell with Windows SDK and Python 3 installed.
Extracts the actual production functions without changing their bodies, replacing
only kernel dependencies with the test harness. No driver is loaded. This tests
parsing, hashing and cleanup, not real CI policy or kernel I/O synchronization.
"""
import argparse
from pathlib import Path
import subprocess
import tempfile
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--digest-source", type=Path)
args = parser.parse_args()
root = args.source_root
digest = (args.digest_source or root / "src/Domito.CodeIntegrity.cpp").read_text(
encoding="utf-8-sig"
)
digest = digest.replace('#include "Domito.Internal.h"', "").replace(
'#include "Domito.MinCrypt.h"', ""
)
common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
start = common.rfind("_Success_", 0, common.index("DomitoMemorySearchPattern("))
end = common.index("_Success_", common.index("DomitoMemorySearchPattern("))
search = common[start:end]
public = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
start = public.index("#if !defined(CALG_SHA1)")
end = public.index("/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM("))
algorithm = public[start:end]
with tempfile.TemporaryDirectory(prefix="domito-digest-tests-") as temp:
build = Path(temp)
(build / "digest-under-test.inc").write_text(
algorithm + "\n" + search + "\n" + digest, encoding="utf-8"
)
test_source = Path(__file__).with_name("digest-tests.cpp")
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
str(test_source),
"/Fe" + str(build / "digest-tests.exe"),
"/Fo" + str(build / "digest-tests.obj"),
"/link",
"bcrypt.lib",
"crypt32.lib",
],
cwd=build,
check=True,
)
subprocess.run([str(build / "digest-tests.exe")], cwd=build, check=True)