283 lines
16 KiB
Markdown
283 lines
16 KiB
Markdown
# 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.
|
||
|
||
## Historical x64 corpus: availability and structure evolution
|
||
|
||
The [CI API compatibility matrix](ci-api-compatibility.md) lists observed named
|
||
exports for ten exact x64 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`.
|
||
|
||
## 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.
|