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
248 lines
15 KiB
Markdown
248 lines
15 KiB
Markdown
# 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.
|