Files
Domito/docs/consumer-helpers.md
nefarius 3fbfe31d15 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
2026-10-27 00:17:25 +01:00

15 KiB

Migrating consumer Authenticode helpers

These helpers centralize file validation, certificate inspection, hashing and allocation cleanup. The contracts keep application identity and trust policy explicit. Host tests and kernel compilation do not establish new CI runtime guarantees.

Verified fixes and existing coverage

  • Implementation defect fixed: DomitoGetProcessImageName freed its allocation on a failed second query but left that freed address in the caller's output. It also left old caller output untouched on earlier failures. It now clears the output first, publishes only a successful bounded result, and frees its local allocation on all other paths. The process handle uses limited-query access (0x1000) instead of PROCESS_ALL_ACCESS. A PID is resolved once to a process handle; no later query re-resolves that PID.
  • Output-contract defect fixed: DomitoFindModuleBaseAddress could preserve an old address when lookup failed. It now clears the optional output first and validates the counted module name. The export-by-module wrapper preserves the underlying status, including missing exports and unsupported forwarders.
  • Annotation defect fixed: the DomitoReadFileAt definition's single-object output annotation disagreed with its public byte-count annotation. Both now describe BufferSize writable bytes.
  • Already addressed before this review: exact file reads return STATUS_END_OF_FILE on short reads and wait for their own pending request. Explicit digest helpers bound PE32/PE32+ headers, sections, certificate spans and DER navigation. Certificate pointers borrow the stable input buffer. CI validation results initialize outputs and own both policies, including diagnostic allocations on failure. The current raw CI declaration names the flags and byte-sized signing level; 0x20 broadens root acceptance in examined builds, rather than declaring a file to be PE.

Applications should avoid truncating file lengths to EndOfFile.LowPart, reparsing fixed-kind PE headers, consuming uninitialized CI policies, skipping cleanup on failure, or accepting invalid hexadecimal digits. The helpers below provide reusable primitives without defining application authorization policy.

File object, handle and path validation

DomitoValidateFileObjectEx, DomitoValidateFileHandleEx and DomitoValidateFilePathEx expose the same explicit flags, requested signing level, selection mode and owning DOMITO_CI_VALIDATION_RESULT. None adds flags or falls back to a different validation policy. They require PASSIVE_LEVEL and Windows 10 compile targets; the CI imports must exist on the target OS. They do not require DomitoInit.

The handle wrapper requires a trusted kernel handle with FILE_READ_DATA. It references its FILE_OBJECT, releases that reference on every exit and leaves the caller's handle open. Never pass an unchecked user handle: the reference is made using KernelMode. The caller must keep the handle valid during validation.

The path wrapper accepts a counted absolute native path, for example \??\C:\Program Files\Example\app.exe. It creates a read-only kernel handle, shares only reads, references that same opened file for CI, and closes its own handle. Existing incompatible writers cause an open failure. Special kernel APCs must be enabled for synchronous I/O. Reparse points may redirect the open; validation describes the opened object. It does not establish the identity of a previously looked-up process image or certify in-memory process contents. File-object/handle callers must independently provide stable file bytes.

DOMITO_CI_VALIDATION_RESULT result = {0};
NTSTATUS status = DomitoValidateFilePathEx(&nativePath, selectedCiFlags, 0, DomitoCiSignatureSelectionNative, &result);
if (NT_SUCCESS(status) && NT_SUCCESS(result.PolicyInfo.VerificationStatus))
{
    // Apply caller-provided certificate/publisher/pin policy here.
    // A nonzero SigningTime is a separate consumer requirement, if desired.
}
DomitoFreeCiValidationResult(&result); // Also on CI or application-policy failure.

Modern file-object verification can select a preferred Microsoft nested signer where direct embedded verification selected the company signer. Changing paths therefore requires an explicit migration decision. Keep DomitoValidatePortableExecutableEmbeddedSignature for the stable PE-buffer, explicit-algorithm route. Its supported PE subset and nested-selection limitations still apply. It verifies the original first WIN_CERTIFICATE through CI, with no algorithm retry or catalog fallback. Existing legacy/catalog APIs remain available with explicit catalog policy; new convenience wrappers do not invoke them implicitly. An absent optional API is not permission to relax trust policy.

Certificate fingerprints and SPKI pins

DomitoGetCertificateSpki extracts two borrowed views from stable certificate DER: Encoded contains the complete SPKI SEQUENCE TLV; Content omits that sequence's tag and length. It handles optional X.509 version fields and bounded, minimal definite DER lengths. It validates navigation/framing and the SPKI field shape, rather than every X.509 field, extension, name, algorithm parameter or signature. It does not verify trust. Failed extraction clears both views.

DomitoHashBuffer hashes exact bytes using SHA-1/256/384/512 and Windows CNG. HashSize is capacity on entry, actual length on success, required length on STATUS_BUFFER_TOO_SMALL, and zero on other failures. A NULL hash buffer queries the required size. Operational CNG failures erase the selected digest bytes. Input/output buffers must not overlap. Only consume a digest after success.

DomitoHashCertificate takes an explicit DOMITO_CERTIFICATE_HASH_KIND:

  • DomitoCertificateHashDer: exact complete certificate bytes, suitable for a DER thumbprint. This mode hashes bytes; it does not parse or validate them.
  • DomitoCertificateHashSpkiDer: complete SPKI encoding, including outer TLV.
  • DomitoCertificateHashSpkiContentLegacy: preserves the consumers' existing content-only convention. Existing pins must use this mode. Switching to the complete encoding requires recomputing pins from the intended certificates; do not silently treat hashes from the two representations as interchangeable.
UCHAR pin[MINCRYPT_SHA256_LENGTH];
ULONG pinSize = sizeof(pin);
status =
    DomitoHashCertificate(cert.Data, cert.Size, DomitoCertificateHashSpkiContentLegacy, CALG_SHA_256, pin, &pinSize);

DomitoDecodeHex accepts an exact counted Unicode hex string for a caller-selected binary length, rejects invalid digits/odd or mismatched lengths, and zeroes its destination on malformed input. DomitoEncodeHex writes uppercase Unicode hex with a NUL terminator; its capacity is in WCHARs including the terminator. Decode configuration pins once into binary and compare like representations. The library embeds no publisher names, root pins, or certificate allowlists. Hashing does not prove that a certificate is a root or trusted issuer.

Chain inspection and publisher ownership

DomitoGetCiChainView requires a known, readable, initialized CI-owned policy and an explicit DomitoCiChainLayoutObservedX64 argument. This asserts the sampled x64 common prefix (+0x28 elements, +0x30 count, +0x34 opaque RootId, minimum 56-byte header, 120-byte elements). It is not automatic OS/layout detection. Non-x64 builds return STATUS_NOT_SUPPORTED; allocation size and compile-target NTDDI cannot establish runtime layout compatibility.

The accessor bounds the element array and every exposed certificate/name blob within CI's owned allocation before returning a view. It rejects oversized hash lengths and absent certificate bytes. It exposes neither trailing attributes nor EKU/public-key arrays. It cannot make a forged or freed kernel pointer safe; allocation extent/readability and the asserted layout remain caller preconditions. All returned pointers expire when the policy is freed or its result reused.

Sampled CI results are leaf-first. Inspect Elements[0] for that selected leaf. Elements[Count-1] is the last represented certificate; this API does not promise it is self-signed, the configured trust anchor, or that the chain includes every certificate through a root. RootId is opaque and does not establish those properties. A consumer requiring a specific root must establish its intended CI policy and represented-anchor assumptions instead of inferring trust from position.

The accessor deliberately imposes no bytewise issuer/next-subject equality. CI name blobs are not an independent chain verifier; their encoding and representation are version-sensitive. Current evidence does not establish universal byte equality as a CI API contract. Preserve such a check as an explicit downstream restriction if compatibility demands it, and assess its false-rejection risk. CI return status, policy verification status and application identity/pin requirements remain separate checks.

DomitoGetCertificatePublisherName dynamically resolves the optional CI export instead of adding a static import. It requires DomitoInit; missing modules, exports and unsupported export forwarders propagate their lookup status. The caller passes an empty UNICODE_STRING, and uses DomitoFreeCertificatePublisherName to release/reset it. Partial allocations on failure are cleaned internally; the destructor is safe again afterward. Its private pool allocator is independent of configurable Domito callbacks. Never free this string through G_Memory, RtlFreeUnicodeString or the process-name free API. Display-name comparison remains caller policy, not certificate identity.

Process names, allocators and optional exports

DomitoGetProcessImageName returns one Domito allocation containing the UNICODE_STRING and its buffer. DomitoFreeProcessImageName frees the entire allocation once, without querying callbacks in consumer code. Its output is NULL on failure. The existing ULONG PID interface remains; a PID lookup can race process exit/reuse before the initial open and is not a durable process identity.

DomitoGetProcessImageNameEx instead accepts a caller-referenced PEPROCESS. It uses SeLocateProcessImageName, copies the name into the same Domito-owned representation and frees the OS allocation internally. The caller must keep the process referenced for the call. No Domito initialization is needed for this Ex API. The PID API still requires DomitoInit for its query routine. Neither name API proves that a subsequent path open addresses the original image file. WDF/requestor-process lifetime and authorization remain downstream.

Allocator contract: the existing callback API does not attach allocator provenance to each result and is not synchronized for concurrent replacement. Configure a compatible allocate/free pair during initialization, before concurrent calls, and keep it unchanged until all callback-backed allocations and in-flight calls are released. A dedicated free name does not make changing callback pairs with outstanding allocations safe. Do not retrofit a hidden prefix onto existing allocations: callers may already free the public base address with their callback. CI policies and the publisher helper use their dedicated free APIs instead.

DomitoFindModuleExportedFunctionAddress combines the existing full-module-path and export-name lookups, clears its output and preserves useful failure status. It requires DomitoInit, and does not pin the module. The caller must guarantee that the module remains mapped throughout lookup and use of its returned address.

Integration and consumer boundary

Import Domito.props from the Domito package location or override DomitoRootPath. The default root is the sheet's own directory. x64/ARM64 and Debug/Release remain selected by the project; DomitoLibraryPlatform can override the library platform directory. Both WDM and KMDF final driver projects receive Domito, CI and CNG dependencies. Importing a sheet into a static library does not make link dependencies transitive: import it into the final driver as well.

WDF caches, PID caches, administrator checks, developer bypasses, request allowlists, build expiry and selected-signing-time requirements stay in consumer code. No default trust evaluator is added: that would silently choose policy absent from the generic SDK. WPP diagnostics can enumerate the bounded chain view without fixed-depth indentation arrays. Report the returned NTSTATUS and local policy failure reason separately rather than mapping all failures inside the SDK.

The new host suite compiles the production code unchanged apart from its kernel include, uses deterministic kernel doubles for ownership/failure paths, compares SPKI bytes and fingerprints with Crypt32, and uses real CNG for hashing. It tests both architecture conditional branches, which is not ARM64 runtime emulation. Real WDK compilation separately covers x64 and ARM64. These checks do not certify kernel CI behavior, every chain layout, or full X.509 validation.

References: SeLocateProcessImageName ownership, BCryptCreateHash memory management and kernel requirements.

Deprecated compatibility entry points

The public header uses MSVC __declspec(deprecated("migration guidance")) for four older functions. Calling or taking their address produces C4996 in C and C++; declarations and binary entry points remain available. Projects treating warnings as errors must migrate, or suppress C4996 locally around a deliberate compatibility call. Do not disable the warning globally to conceal accidental use.

  • DomitoGetPortableExecutableDigestKind → DomitoGetPortableExecutableDigestKindEx: check NTSTATUS; unrecognized input no longer silently chooses SHA-1. Neither function validates signature trust.
  • DomitoCalculatePortableExecutableDigest → DomitoCalculatePortableExecutableDigestEx: select the algorithm explicitly and review the supported layout/rejection rules. This is not a drop-in semantic change for arbitrary files previously accepted by linear hashing.
  • DomitoValidateFileLegacyMode → DomitoValidateFileLegacyModeEx: select catalog policy and trusted-catalog requirements explicitly, and consume the validation source. The Ex legacy path remains available where catalog/older-OS behavior is required; migration does not implicitly switch to modern signer selection.
  • DomitoGetProcessImageName → DomitoGetProcessImageNameEx: acquire and hold the intended PEPROCESS reference, then use the dedicated name destructor. Looking up a PID immediately before the Ex call cannot eliminate an earlier PID-reuse race; obtain the process reference from the operation's intended identity. This migration does not solve subsequent path-reopen identity races.

No old type has an unconditional replacement. Raw CI/mincrypt types still describe the native ABI; the owning validation result and borrowed chain view serve different purposes. Raw Windows CI exports, allocator callbacks, module lookup primitives, exact file reads and digest cleanup remain supported.