Files
Domito/docs/mincrypt-contracts.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

19 KiB
Raw Permalink Blame History

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 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 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 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 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.

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.