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

+327
View File
@@ -0,0 +1,327 @@
# Code Integrity and mincrypt contracts
The default header retains the existing structure layouts and calling shapes.
Parameter names and ownership comments now distinguish the known controls from
the verification result. `VerificationStatus` is an `NTSTATUS` with the same
32-bit storage. The additional `Domito.MinCrypt.26100.h` header is an **opt-in
description of ci.dll 10.0.26100.8328 x64**. It neither adds imports nor extends
the default structures on every Windows target.
The evidence is static REA/Ghidra analysis of SHA-256
`c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b`, with
Microsoft ci.pdb GUID `1CAE574B-CF1F-3564-5F74-49567A128629`, age 1 supplying the
address/name map. The companion CodeIntegrity-RE repository retains the full
records and readable `evidence/` files. The historical corpus below adds
sampled x64 Windows 10 observations; ARM64 and unsampled-build compatibility
remain open. Compile-target `NTDDI_VERSION` is not runtime layout negotiation.
The original modern research and `Domito.MinCrypt.26100.h` use the string
`FileVersion` label **26100.8328**. That same exact binary has numeric
`VS_FIXEDFILEINFO` version **26100.9444**, which the export matrix uses. Its
SHA-256 and recovered contracts are unchanged; these labels identify one file.
## Historical x64 corpus: availability and structure evolution
The [CI API compatibility matrix](ci-api-compatibility.md) lists observed named
exports for 20 exact x64/ARM64 CI files, including the later ISO samples. Consult it
for import availability, and the contracts below for layout and ownership limits.
[NOTES.md](../NOTES.md) lists the exact identities of six sampled Windows 10
files: `10.0.10240.16463`, `10.0.10586.0`, `10.0.14393.2189`, `10.0.15063.0`,
`10.0.16299.125`, and `10.0.17134.1130`. All export `CiValidateFileObject` and
the digest/catalog functions. The global setter is already present in sampled
1703, correcting the old 1709 note. The publisher-name export is absent from
all six. ISO samples additionally show absence in 19041.1 and presence in
19041.1865 and 22000.318 x64; the original 26100 file also exports it. The first
Windows 10 servicing update adding it remains unresolved. See CodeIntegrity-RE
`iso-corpus-index.json`. Header guards do not negotiate runtime import availability.
All six sampled x64 file-object exports transfer 48 bytes into each caller
policy, with the owned chain pointer at `+0x10`; cleanup zeros `0x30` bytes. Their
file-object helpers require hash capacity >=64, return `STATUS_BUFFER_TOO_SMALL`
and required length 64 for a smaller buffer, and consume the third input as a
byte. This supports the stable policy storage prefix on these exact artifacts;
it does not establish all field semantics or every servicing build's behavior.
**The chain header evolves within Windows 10. The default header's
`NTDDI_VERSION >= NTDDI_WIN10` condition cannot safely expose every later field
on Windows 10 1507.** Constructor evidence establishes:
- `10.0.10240.16463` (1507): fixed header 56 bytes (`0x38`), root ID at `+0x34`,
variable payload beginning at `+0x38`. There is **no AuthenticodeAttributes
header field at `+0x38`**. Accessing the default structure's member on this
layout would interpret variable payload bytes as a blob descriptor.
- `10.0.10586.0` (1511): fixed header 72 bytes (`0x48`), attributes descriptor at
`+0x38`, payload beginning at `+0x48`.
- Sampled 1607/1703/1709/1803: fixed header 104 bytes (`0x68`), root ID still at
`+0x34`, attributes at `+0x38`, payload beginning at `+0x68`. Constructor evidence
establishes space for the 32-byte extension at `+0x48`, not its semantic
contents on every build. The historical view leaves this region reserved.
`Domito.MinCrypt.Historical.h` supplies distinct opt-in x64 views for those
observed profiles with 12 compile-time assertions. The common prefix ends after
root ID at `+0x34`; chain elements use 120-byte strides in these constructors,
but the prefix views expose their storage as opaque. Do not assume every element
field is populated on every version. The 26100-specific header separately names
its recovered binary-ID field.
Choose a view only after establishing the runtime artifact/layout. `Size` is
**total allocation bytes including payload**: a 1507 allocation can be larger
than 72 or 104 bytes, so `Size >= sizeof(later_header)` is not version detection.
Once layout identity is established, a size check is an additional field-read
bound, not a substitute for identity. The default declarations remain unchanged
for source compatibility; their later fields require this explicit runtime scope.
Successful exported/helper records in CodeIntegrity-RE retain the evidence:
- `historical/1507-export-contracts.json`:
`ev_489c3b57057b0bc5c86950d7bb4ea25108bbc7762abc363e1be7eb5cca034796`.
- `historical/ci-10240-helpers.json`:
`ev_6d602c57de9614e7c5c28b9532c139f750f0e014b151000ecb41ad36f54bf764`.
- `historical/ci-10586-exports.json`:
`ev_a09e04976cfd90e9ba69cc6618892f15c3da166c64fa9241efae0db85234fd6e`.
- `historical/ci-10586-helpers.json`:
`ev_3403d5b8bf7c322fd924880d4cd9b296b98e94238cac2e1c1e9605c4b4d27255`.
- `historical/ci-14393-contracts.json`:
`ev_e63a4c4b7613e231795051b784b9571605b9bf81258b5dcd04d32ddc681abb41`.
- `historical/ci-15063-contracts.json`:
`ev_338658fb82559fa6daa8ae5487b90fe84b8e408ea7578feed6d8dff5d590e114`.
- `historical/ci-16299-contracts.json`:
`ev_fe5731175f0726f0d46af2f78ca615d284f8598939296815e26b5589db3001cf`.
- `historical/ci-17134-contracts.json`:
`ev_0c3e3359b8b7486318006afa6b95eb2257a18abeec9041a86a32db487bcbe996`.
## Results and trust policy
Check the API's returned `NTSTATUS` first. `PolicyInfo.VerificationStatus`,
`PolicyBits`, signer details, and timestamps supply additional observations.
Successful chain processing does not imply that the caller's file digest matched
the signed digest; the comparison can occur afterward. Diagnostic policy data
may exist on failure. An extracted publisher name is display information and
does not establish publisher identity or signer trust.
Define the policy the application needs: embedded signature validation,
membership in an accepted installed catalog, an expected publisher/key, or a
Windows signing scenario. Preserve which path produced a result. A nonzero
`PolicyBits` value is not automatically failure, and any one root bit is not a
complete trust decision. File bytes and object identity must remain tied to the
bytes the application will subsequently use.
The [file-object flag document](ci-validate-file-object.md) covers `0x20`, hash
capacity, signing level, and its pending VM experiment. Other observed flag
effects on 26100.8328 x64 are:
- `0x1` selects hash scenario 5.
- Otherwise `0x2` selects scenario 4 for signing levels <=4; higher levels use
the level mapping.
- Otherwise `0x80000000` selects scenario 5 for levels <=4; higher levels use
the level mapping. It takes precedence over `0x4`.
- Otherwise `0x4` selects scenario 1; without these choices the signing level
maps to a scenario.
- `0x00400000` suppresses accepted-root bit `0x8` when the relevant helper mode
is enabled; CI can add this flag internally. Bits `0x2` and `0x8` also affect
later signing-level/EKU restrictions.
The scenarios resolve through initialized signing-policy state. Static defaults
are not a guarantee of a running system's algorithm, and the flags should not
be named as universal fixed SHA selectors. Unknown bits remain unknown. Evidence:
`CipGetHashAlgorithmForLegacyScenario`, `CipGetAcceptedRootKeys`, and
`CipMinCryptToSigningLevel` in the research bundle.
## Ownership and initialization
Zero-initialize both `MINCRYPT_POLICY_INFO` output objects before their first
use. Free both with `CiFreePolicyInfo` after use and before reuse, including
failure paths. Never call the destructor on arbitrary uninitialized stack data.
Do not clear a nonzero `Size` while a chain is still owned: the observed cleanup
helper skips an object whose `Size` is zero.
`ChainInfo` owns one allocation. Public-key blobs, names, encoded certificates,
EKUs, attributes, and chain elements are borrowed views into it. Do not free
children separately or retain them after releasing the owner. Copying the policy
structure with `memcpy` creates two aliases to one allocation, not independent
ownership. CI's internal deep-copy helper uses the total allocation size and
rebases interior pointers; Domito does not expose that helper.
The default `CiFreePolicyInfo` declaration retains its legacy `PVOID` return to
avoid a broad source compatibility change. **Ignore that value**: observed x64
assembly provides no defined pointer result. The opt-in build-specific function
pointer type uses `VOID`.
`CatalogName` is a separate `RtlDuplicateUnicodeString` allocation in the observed
catalog export. Initialize it empty, release a returned buffer with
`RtlFreeUnicodeString`, and release both policies separately. Publisher-name
storage is allocated through the caller's callback and must be released with
that allocator's matching free operation, including an allocated buffer on a
later conversion failure. It is not owned by either policy object.
Evidence: `MincryptFreePolicyInfo`, `MincryptCopyPolicyInfo`,
`I_MincryptAddChainInfo`, `CiVerifyHashInCatalog`, and `CiGetCertPublisherName`.
The additive [validation helpers](ci-validation-helpers.md) package safe result
ownership, explicit file-object selection and embedded CI verification. They do
not change raw CI declarations or existing legacy-helper behavior.
## Multiple signatures and selected results
`CiValidateFileObject` returns one selected signature result and its timestamp
policy. Those two policy outputs do not enumerate two file signatures. The
output file digest and algorithm are not expected-certificate selectors.
REA tracing of x64 CI 22621.2792 and the original 26100 sample found a
flag-dependent distinction: ordinary `0x20` can continue nested-signature
verification and retain a policy-preferred signing level. Adding `0x80` enters
an early-exit path after the first successful full candidate verification.
`0xA0` is therefore a concrete kernel experiment, not a proved stable
"select primary signature" flag: `0x80` also affects other policy/fallback paths.
An arbitrary signer index, expected-certificate predicate and continuation
interface are not exposed by these examined wrappers.
For the supplied BthPS3 fixture, host SignTool verifies a primary Nefarius
SHA-256 signature and a nested Microsoft SHA-256 signature. On both examined CI
builds, `CiCheckSignedFile` with the matching SHA-256 digest follows the primary
signature directly. This gives a CI-based alternative worth testing without
implementing RSA/ECDSA, chain or timestamp verification. A subsequent user-run
probe on Windows 11 22631 x64 with TESTSIGNING enabled confirms that `0x20`
returns Microsoft while `0xA0` and direct SHA-256 `CiCheckSignedFile` return the
Nefarius leaf and its signing time. Every top-level and signer/timestamp policy
status succeeds; the two company-signer routes have matching policy observations.
The companion `signature-selection/runtime-22631-bthps3.json` retains exact
fixture/trace identities. This does not establish TESTSIGNING-off or general
cross-build behavior. A different layout may still require bounded signature
framing/enumeration.
The companion CodeIntegrity-RE `SIGNATURE-SELECTION.md` retains fixture hashes,
56 procedure bodies, assembly, flag side effects and the kernel test plan.
The 23H2 batch is
`ev_049bd7d3aa492a9e3e4d8fcbf365ae00a6057751b26f93f3c1663c9436952ff7`;
its primary-selection helper is in
`ev_c867c73ca7eae7e4fbca2f44c6f6fb5904fb00459d0b84f2da8294b5b05a3866`.
## Digest-based verification inputs
`CiCheckSignedFile` consumes a caller-computed file digest. It does not reopen
the file or compute its Authenticode hash. On the observed build it accepts
SHA-1/256/384/512 with exactly 20/32/48/64 bytes respectively. A wrong algorithm,
length, or digest fails the final comparison.
Its signature input points to a `WIN_CERTIFICATE` header. The observed wrapper
requires 4-byte pointer alignment, supplied size >=12, revision `0x200`, type 2,
and `8 < dwLength <= supplied size`. The PKCS#7 payload begins at header+8.
The wrapper selects the first certificate-table entry. Multiple table entries
and nested signatures are separate concepts; this is not a complete signature
enumeration interface.
These exports are present in all six sampled Windows 10 x64 files and the
analyzed Windows 11 file. The older header's “Win7–8.1 only” comments described
a workflow, not verified export availability. Earlier first-available builds
and pre/post-KB3033929 ABI changes still require matching Windows 7/8 artifacts.
Import-library names and stdcall stack-byte counts alone do not settle semantics.
## Catalog controls
`ReloadCatalogs` controls catalog-store reload and related context setup.
`RequireTrustedCatalog` is a nonzero gate requiring an observed catalog extension
marker: its matching bytes include `PE`, integer `0x10001`, and `TRUSTED`.
The marker is an additional catalog condition, not proof of a Microsoft signer.
`AcceptedCatalogPolicyBits` accepts a candidate when
`(RequestedMask & Catalog.PolicyInfo.PolicyBits) != 0`: **any overlap** is enough.
It neither enables policy flags nor requires every requested bit. The existing
Domito compatibility value is named `DOMITO_CI_LEGACY_CATALOG_POLICY_BITS`
(`0x2007F`). It includes test/DMD categories and diagnostic bit `0x20000`, while
omitting other categories. It is retained as an existing behavior, not promoted
as a universally safe trust mask. Establish and test the application's accepted
signer policy before changing it.
Observed file-hash lookup supports SHA-1 and SHA-256; the internal path has no
SHA-384/512 file-hash case. Embedded-signature algorithm support and catalog
lookup support are different capabilities. Catalog-only files need an explicit,
tested algorithm strategy rather than assuming SHA-1 from an absent embedded
table. Catalog stores, caches, silo/process context, and signing policy affect
results. The new `DomitoValidateFileLegacyModeEx` forwards a caller-selected mask and
marker gate and reports embedded/catalog origin. Its compatibility wrapper retains
the existing mask. See [digest and workflow details](authenticode-digests.md).
Domito's older-Windows helper keeps its system-process attachment until
historical runtime evidence supports changing it.
Evidence: `I_FindFileOrHeaderHashInLoadedCatalogs` and its assembly
`ev_c42e7ee76099d55e7cf7cdd3d9f79e99030ba637680365612afb4906694ae3a8`,
`I_ParseCatalog`, and marker matcher
`ev_a1d3a5844a2b6be6c16175e307956c9819d0b64a4fe70216f292c12e4ea38d82`.
## Opt-in x64 26100 structure observations
The version header has distinct type names and 14 compile-time layout assertions.
Use it only after independently establishing the matching runtime build and
architecture. A large `ChainInfo->Size` alone does not identify the header: it
includes variable trailing data, so an older allocation could also be large.
Only after layout identity is established may a size check protect a field read.
- `DOMITO_CI26100_NAME_BLOB`: pointer at +0, byte length at +8, ASN.1 string tag
at +10. Names are not necessarily NUL-terminated. Observed conversion handles
UTF8String tag `0x0C` and BMPString `0x1E`; `PCHAR` in the default declaration
must not be read as a guarantee of ANSI encoding.
- `DOMITO_CI26100_CHAIN_INFO`: `Size` is total allocation bytes; fixed header is
104 bytes. Root ID at +0x34 is an opaque integer, not policy bits. The 32 bytes
at +0x48 are a platform-manifest binary ID from an authenticated attribute,
not a file digest or certificate thumbprint. Zero/nonzero does not by itself
prove trust. Require the matching layout and `Size >=104` before reading it.
- `DOMITO_CI26100_POLICY_INFO`: 48 bytes; `NotBefore`/`NotAfter` come from the
signer certificate validity fields, not an intersection of the whole chain.
Interpret `RevocationTime` only with the relevant result. A zero signing time
is not a real timestamp to display as a signing date.
Construction/copy evidence: `ev_d7110cfc711074a4af1874b5c8e701c13bcc3dc58a1596c64dca4ca98966c79e`,
`ev_858afecf2170484325379b6a8bc307fb72dad2f9a5a765169192c7363a3386af`,
`ev_f82cbee977715399323bb398781c125a9d19b4ea29acd05063ec55b711cfc670`.
## Partial policy-bit dictionary
Names in the opt-in header are Domito descriptions of observed 26100 branches,
not official Microsoft constants or a universal trusted-root enumeration.
Root/provenance categories include `0x1` self-issued terminal path, `0x2` selected
built-in Microsoft roots, `0x4` test roots, `0x8` the built-in root associated with
embedded picroot handling, `0x10` tolerated partial/unresolved chain, `0x20` DMD,
`0x40` DMD test, `0x80` another built-in Microsoft root, `0x400` flight root,
`0x2000` third-party marketplace, `0x4000` explicit supplied certificate/key
acceptance, and `0x8000` Microsoft Identity Root 2020. `0x800` and `0x1000` are
additional discriminators on some root entries; their independent meanings
remain unresolved. These categories do not establish a full trust result.
Observed diagnostic branches set `0x20000` chain failure, `0x40000` invalid
cryptographic/signature data, `0x80000` unmet EKU, `0x100000` page-hash failure,
`0x200000` revocation, `0x400000` certificate validity failure (including
not-yet-valid), `0x08000000` path-length constraints, `0x10000000` generic fallback,
and `0x20000000` CA/BasicConstraints failure. `0x04000000` marks a no-usable-
timestamp branch during revoked-certificate recovery, not universal timestamp
absence. `0x50000` and `0x60000` are observed composites; do not infer a unique
meaning for every constituent bit. Timestamp-before-revocation processing can
clear a revocation bit. Internal checks of high bits are not a replacement for
the API status and application policy.
## Remaining runtime and API coverage
The missing page-hash exports have recovered argument shapes in the research
report. Their normalized allocation uses a 24-byte x64 header and 24-byte entries
even for stronger source algorithms. Field meaning, truncation intent, source
terminal-entry handling, and a supported destructor remain open. `CiFreePolicyInfo`
is not a demonstrated destructor for that allocation. No universal page-hash SDK
or static import is added until those ownership and cross-version contracts can
be tested.
`CiGetPEInformation` is a protected-media dispatcher, not a general PE parser.
`CiInitialize` is initialization machinery. `CiSetTrustedOriginClaimId` writes a
CI global; it is not an ordinary per-file trust selector. Ordinal-only exports
and other initialization interfaces require their own evidence and callers.
Still required: complete sampled-build/ARM64 layout and parameter verification, real CI
output lifetimes and retry behavior under failures, timestamp propagation,
revocation freshness/network policy, weak-crypto selection, multiple/nested
signatures, catalog-only algorithms, and extension of the bounded digest subset to additional PE layouts.
The explicit digest helper now has local Windows SIP and real embedded-digest
comparisons; this does not close those historical or unusual-layout cases. Run the prepared probes in disposable VMs with exact OS,
CI binary, configuration, and fixture identities. Compilation and host mocks do
not close those runtime questions.