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.
This commit is contained in:
nefarius committed 2026-10-27 00:16:42 +01:00
1 parent 1f4d62ae95
commit a491bbd3f8
32 files changed
+5404 -1136

No files matched your search

+247
View File
@@ -0,0 +1,247 @@
# 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](ci-validation-helpers.md) 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.
```c
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.
```c
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](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-selocateprocessimagename),
[BCryptCreateHash memory management and kernel requirements](https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash).
## 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.