Add explicit Authenticode workflows and correct CI helper contracts

This commit is contained in:
nefarius committed 2026-10-26 17:55:58 +01:00
1 parent 1026c48324
commit 7aa23f50be
18 files changed
+1952 -185

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
# Authenticode digests and legacy verification
Prefer `DomitoCalculatePortableExecutableDigestEx` when computing a PE digest.
It takes an explicit SHA-1/256/384/512 algorithm, validates its supported PE layout,
and returns a digest independently of whether the file has an embedded signature.
This allows SHA-256 catalog lookup for a file without an embedded certificate.
Hash calculation is separate from signature verification and signer acceptance.
The older `DomitoCalculatePortableExecutableDigest` and
`DomitoGetPortableExecutableDigestKind` retain their compatibility behavior:
linear byte hashing and heuristic pattern discovery with a SHA-1 fallback. They
remain bounded, but should not be used to discover a general signature policy.
The new API is additive so existing callers do not silently acquire different
selection or rejection rules.
## Supported PE layout
The explicit API validates DOS/NT/optional-header spans, PE32 and PE32+ magic,
declared data-directory count, the complete section table, `SizeOfHeaders`, every
nonempty section's raw interval, and the certificate table. It sorts section
descriptors by raw file offset before validating their sequence. At most 96
sections are accepted, keeping stack storage and sorting work bounded.
Nonempty sections must form a contiguous sequence starting at `SizeOfHeaders`.
Section-header order can differ from raw-file order. Zero-raw-size sections
contribute no bytes. An overlay between the raw sections and the certificate
table, or EOF for an unsigned file, is included. The checksum and physical
certificate-directory slot are excluded. A present certificate table must be
8-byte aligned and end at EOF; every entry must have a bounded length, version
`0x200`, type PKCS#7 SignedData, and complete 8-byte alignment storage.
For this subset, sorted section bytes coincide with linear file order, so the
implementation can stream validated ranges directly to BCrypt without copying
or allocating a whole-file scratch buffer. Digest allocation is the only pool
allocation in the hashing path and is released on every failure.
Gaps, overlapping raw sections, certificates in the middle of a file, and absent
certificate-directory slots are unsupported. Some malformed inputs return a
format error; ambiguous or excluded layouts return `STATUS_NOT_SUPPORTED`.
The missing-slot boundary is deliberate: Windows SIP produced a different hash
than the old linear helper on a fixture with zero declared directories. Do not
infer Windows parity from the older helper's successful return on that input.
This is a validated subset, not a claim that every PE accepted by every Windows
SIP is supported. Microsoft's [PE format documentation](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format)
describes section ordering and exclusions, but unusual overlay behavior requires
measurements against the relevant Windows implementation. The local tests use
the actual [CryptSIPCreateIndirectData](https://learn.microsoft.com/en-us/windows/win32/api/mssip/nf-mssip-cryptsipcreateindirectdata)
provider as a differential oracle, rather than treating a prose summary as the
complete algorithm. SIP hash agreement does not establish CI trust-policy parity.
## Discovering the first embedded content's digest algorithm
`DomitoGetPortableExecutableDigestKindEx` navigates a bounded DER subset:
PKCS#7 ContentInfo / SignedData / encapsulated `SpcIndirectDataContent` /
`DigestInfo.AlgorithmIdentifier`. It reads the algorithm associated with the
file digest, avoiding unrelated algorithm patterns in certificates, attributes,
timestamps, or signer information. SHA-1/256/384/512 are recognized. Algorithm
parameters may be absent or ASN.1 NULL; the digest OCTET STRING must have the
corresponding length. DER lengths must be bounded, definite, and minimally
encoded. Up to seven zero bytes of aligned padding after the outer DER are
accepted when included in `WIN_CERTIFICATE.dwLength`, as observed on Microsoft
system binaries. Other trailing bytes are rejected.
An absent certificate table returns `STATUS_NOT_FOUND`, an unsupported algorithm
or certificate type returns `STATUS_NOT_SUPPORTED`, and malformed supported
structure returns a format error. The output algorithm is zero on failure; no
default algorithm is silently manufactured.
The caller supplies an array containing the security-directory entry and a
stable input buffer. The helper inspects the first table entry. It does not
enumerate nested/multiple signatures, validate the entire ASN.1 document,
validate signerInfos, verify cryptographic signatures, or establish trust.
Indefinite-length BER is outside this supported DER subset. Choose explicit
algorithms when handling catalog-only files or application-defined lookup order.
## Explicit legacy catalog policy
`DomitoValidateFileLegacyModeEx` takes a caller-selected
`AcceptedCatalogPolicyBits` mask, a `RequireTrustedCatalog` marker gate, and a
`ValidationSource` output. Source is `DOMITO_CI_SOURCE_EMBEDDED` or
`DOMITO_CI_SOURCE_CATALOG` on success and zero on failure.
This helper accepts SHA-1 with exactly 20 bytes or SHA-256 with exactly 32 bytes.
Unsupported algorithms return `STATUS_NOT_SUPPORTED`; mismatched lengths return
`STATUS_INVALID_PARAMETER`. The underlying observed embedded API supports more
algorithms than this helper's catalog workflow, as documented in
[the mincrypt contracts](mincrypt-contracts.md).
Both policy objects must be zero-initialized before first use, or own valid CI
results. The helper frees previous contents on entry and before each retry and
clears signing time between attempts. The caller owns both final policy results,
including any returned on failure, and frees them with `CiFreePolicyInfo`.
Embedded-to-catalog fallback and catalog reload occur only for
`STATUS_INVALID_IMAGE_HASH`; resource, I/O, and other policy errors propagate.
Before certificate allocation, the helper references the file object and checks
the supplied table interval against the current file size. It then uses the
exact-read `DomitoReadFileAt`, which waits on a private completion event. An EOF
short read fails before CI sees any uninitialized bytes. The caller must keep
file identity/bytes stable between hashing and consumption; a size check cannot
prevent concurrent mutation. Handles must be valid in the current process
context and satisfy the documented kernel I/O/APC requirements.
The older `DomitoValidateFileLegacyMode` delegates with the existing
`DOMITO_CI_LEGACY_CATALOG_POLICY_BITS` (`0x2007F`) and a false marker gate. That
value is a compatibility default with broad categories, not an application trust
recommendation. The explicit mask uses **any-overlap** catalog filtering.
For a catalog-only file, choose an allowed algorithm sequence as application
policy (for example SHA-256 followed by SHA-1 when legacy acceptance is intended).
For each algorithm, call `DomitoCalculatePortableExecutableDigestEx`, then
`DomitoValidateFileLegacyModeEx` with a zero security directory. Free the digest
and both policy objects before retrying. Continue to another algorithm only on
the specifically intended lookup/signature-mismatch result, not on all failures.
Preserve the actual algorithm and validation source in the application's result.
This supports an explicit search strategy; it does not reproduce every native
CI scenario-selection rule or WinVerifyTrust policy.
## Validation performed
On the local Windows 26100 system, host tests compare all four digest algorithms
against Windows SIP for PE32/PE32+, unsigned/signed fixtures, overlays, reverse
section-header order, multiple certificate entries, and certificate alignment.
The installed `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` form a
real-file corpus. Embedded digest algorithms are independently extracted with
Crypt32, and computed file digests are compared with the signed
`SpcIndirectDataContent` values where an embedded signature exists. This reads
files and hashes bytes; it does not load or execute them as code.
Guard-page truncations, malformed offsets/lengths, unsupported layouts,
algorithm selection, injected CNG/allocation failures, retry ownership, exact
read failures, and explicit catalog controls are also covered. Actual CI calls
are mocked in those host tests. The kernel translation units and layout
assertions compile for x64/ARM64 with warnings treated as errors; the opt-in
26100 runtime-layout observations remain x64-specific.
Still pending are historical/ARM64 CI runtime compatibility, Driver Verifier,
real catalog-only acceptance across algorithms, nested/multiple-signature
selection, weak-crypto/revocation/timestamp policy, and extension of supported PE
layouts. Use the [remaining-work ledger](implementation-status.md) and the
[flag probe](ci-validate-file-object.md) to plan the VM run.
+79
View File
@@ -0,0 +1,79 @@
# CI SDK implementation status
This ledger tracks the source audit S01–S18 and the remaining binary research.
The original CodeIntegrity-RE audit remains a snapshot of commit `7985b23`;
it should not be read as describing the corrected source unchanged.
## Implemented with host tests or compilation
- **S01–S04:** subtraction-safe PE ranges, no fixed 4 MiB copy, bounded certificate
intervals, deterministic failure outputs, and digest cleanup. Compatibility
and explicit paths share the bounded hashing core.
- **S05:** actual certificate read length checked; allocation bounded against
file size; per-operation completion waits through the exact-read helper.
- **S06:** `DomitoReadFileAt` uses an explicit offset and private kernel event;
`DomitoReadFile` reads exactly from offset zero. Pending completion cannot
outlive its stack state or caller buffer. Short reads return EOF.
- **S10:** export lookup uses the name ordinal to index the EAT, bounds tables,
names and resolved RVAs, and rejects forwarders. A valid loader-mapped module
remains a precondition because the API has no externally supplied image size.
- **S11–S12:** corrected policy pointer typedef and publisher return/ownership docs.
- **S14:** policy cleanup before embedded/catalog/reload retries, with host tests
allocating results even on failures. Real CI failure ownership still needs
the VM tests below.
- **S17:** legacy hash algorithm/length combinations validated and calculation
versus verification capabilities documented.
- **S18:** byte-count SAL, buffer-capacity rules, scalar input annotations, and
kernel read context/APC requirements documented.
## Implemented within an explicit support boundary
- **S07:** new bounded DER discovery reads the first outer SPC file-digest
AlgorithmIdentifier; new explicit digest API needs no discovery. The old
heuristic entry point retains compatibility behavior. Arbitrary BER, nested
signatures, and full signature enumeration remain outside this parser.
- **S08:** unsigned/catalog-only files can be hashed with a caller-chosen
SHA-1 or SHA-256 strategy. The policy-explicit legacy helper forwards catalog
controls and reports the successful source. Real catalog lookup comparisons
across OS versions are pending; no implicit algorithm retry policy is imposed.
- **S09:** the explicit digest path enforces a contiguous raw-section subset and
EOF certificate tables, with Windows SIP and real embedded-digest comparisons.
Gaps, overlaps, missing directory slots and nonterminal certificates are
rejected. The old linear compatibility entry point is not a universal hasher.
- **S13/S15/S16:** recovered fields, parameter meanings, policy categories, sizes,
name encoding, borrowed lifetimes, and allocator ownership are documented.
The opt-in x64 26100 header provides distinct observed layouts and assertions.
Default layouts remain compatible; runtime layout identity cannot be inferred
from `NTDDI_VERSION` or `ChainInfo->Size` alone.
## Requires the test VM or additional matching binaries
The VM is not available yet. Host mocks and compilation do not close these:
1. Run the paired `CiValidateFileObject` flag `0`/`0x20` probe on 19041/19045
with TESTSIGNING off and a loadable signed test driver. Preserve complete
statuses, policies and fixture/environment identities (issue #3).
2. Verify historical and ARM64 ABI, structure evolution and first-available
export versions using matching binaries/symbols and runtime tests. Preserve
default layouts until that evidence supports changing them.
3. Test real CI policy ownership on every failure/retry path under Driver
Verifier, including allocation failures and initialized outputs.
4. Compare real embedded/catalog-only SHA-1/SHA-256 results, catalog cache/reload
behavior, trust-mask choices, requested signing levels and flag combinations.
5. Measure signing-time propagation, zero/sentinel values, timestamp expiry,
revocation freshness/network policy and weak-crypto policy. Record OS policy,
Secure Boot/HVCI/WDAC and exact files, not just a boolean result.
6. Establish multiple/nested signature-selection behavior with a signed corpus;
extend supported PE gaps/overlays only after differential evidence.
7. Resolve page-hash allocation destruction, normalized-entry/truncation meaning,
terminal-entry handling and cross-version layouts before publishing a general
page-hash API. `CiFreePolicyInfo` is not an established destructor for it.
8. Investigate protected-media/initialization and ordinal-only exports separately
if they are needed. Recovered export names alone are insufficient contracts.
## Checks
Run the three host runners in `tests/README.md`. Kernel compilation also covers
the default modern x64/ARM64 structures, the pre-Windows-10 x64 compile variant,
and 14 assertions in the opt-in observed-build header. Those are build checks,
not a claim that historical or ARM64 CI runtime ABI has been verified.
+211
View File
@@ -0,0 +1,211 @@
# 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. These observations do not establish
older-Windows or ARM64 ABI compatibility. Compile-target `NTDDI_VERSION` is not
runtime layout negotiation.
## 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 this build 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 remain implemented on the analyzed Windows 11 build. The older
header's “Win7–8.1 only” comments described a workflow, not verified export
availability. Exact first-available builds and pre/post-KB3033929 ABI changes
still require matching historical binaries. Import-library symbol names and
stdcall stack-byte counts alone do not settle parameter 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: historical/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.