[PR #4359] [CLOSED] fix(native): report every slice of a signed universal Mach-O signature #6202

Closed
opened 2026-10-31 01:12:39 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/4359
Author: @Ambar-13
Created: 10/16/2026
Status: ❌ Closed

Base: main ← Head: fix/signature-universal-slices


📝 Commits (10+)

  • 24e5c08 fix(native): report every slice of a signed universal Mach-O signature
  • b160d44 fix(native): inspect slices behind an unsigned default and keep slice entitlement facts
  • 7802a07 fix(native): name exact slices behind an unsigned default and keep unread entitlements unobserved
  • c836573 fix(native): keep signature facts when lipo is unavailable
  • 6986254 fix(native): report slice signing identity and entitlement values in full
  • ed75ca7 fix(native): read entitlement keys as own properties when comparing slices
  • 52bea8b fix(native): compare slice entitlements as they arrive and keep exact integers
  • e17beb5 Merge origin/main into fix/signature-universal-slices
  • 9308314 Merge origin/main into fix/signature-universal-slices
  • adae09e fix(native): quote slice identity values and keep cut-off entitlement plists unknown

📊 Changes

15 files changed (+1861 additions, -75 deletions)

View changed files

📝 docs/native-investigation.md (+30 -4)
📝 src/native/NativeMacOSProvider.ts (+334 -53)
➕ src/native/signatureSlices.test.ts (+331 -0)
➕ src/native/signatureSlices.ts (+344 -0)
📝 tests/boundary/cli/nativeSignatureCli.test.ts (+16 -4)
📝 tests/boundary/providers/native/nativeInventory.golden.test.ts (+649 -1)
📝 tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts (+29 -13)
➕ tests/fixtures/native-macos/codesign-universal-signed/README.md (+47 -0)
➕ tests/fixtures/native-macos/codesign-universal-signed/aggregate.txt (+26 -0)
➕ tests/fixtures/native-macos/codesign-universal-signed/entitlements-aggregate.xml (+1 -0)
➕ tests/fixtures/native-macos/codesign-universal-signed/entitlements-arm64.xml (+1 -0)
➕ tests/fixtures/native-macos/codesign-universal-signed/entitlements-x86_64.xml (+1 -0)
➕ tests/fixtures/native-macos/codesign-universal-signed/requirements.txt (+1 -0)
➕ tests/fixtures/native-macos/codesign-universal-signed/slice-arm64.txt (+26 -0)
➕ tests/fixtures/native-macos/codesign-universal-signed/slice-x86_64.txt (+25 -0)

📄 Description

Summary

Fixes #4293.

On a universal Mach-O where every slice is signed, inspect_signature returned the identity of one slice and said nothing about the others. Now the fields describe the requested architecture (main's -a binding, since the 2026-10-24 merge below), and the result lists the other slices and their CDHashes, says which slices are signed differently and how, and explains why designated_requirement can list CDHashes that cdhashes doesn't. No schema change; the new facts are limitations plus per-slice provenance entries.

Problem and expected behavior

Build a two-slice file where the arm64 slice is ad hoc signed as com.example.armslice with the hardened runtime and disable-library-validation, and the x86_64 slice as com.example.x86slice with no hardened runtime and get-task-allow (commands in tests/fixtures/native-macos/codesign-universal-signed/README.md). On main, rea inspect-signature fixture --json reports com.example.armslice, one CDHash, hardened_runtime: true and only disable-library-validation, with no mention of slices, while designated_requirement lists both CDHashes.

Each slice of a universal binary carries its own signature (Apple TN3126), and codesign -d without -a describes one slice. Presenting one slice's facts as the file's facts gets the x86_64 slice exactly wrong on the two facts that decide whether it can be debugged.

Change and scope

src/native/NativeMacOSProvider.ts: when the aggregate display reports a signed Mach-O universal (…) with more than one slice, run codesign -d -a <slice> --verbose=4 for each slice (before, this only happened once an aggregate query had already hit an unsigned slice). Parse each signed slice with the existing parseCodeSignature. A slice whose CDHash differs from the aggregate is also asked for its entitlements, since they're sealed per code directory. Unsigned slices keep the Unsigned Mach-O slices: … limitation. Slice names come from codesign's own Format= line with no allow-list, so arm64e.x1 on macOS 27 is queried too.

src/native/signatureSlices.ts (new, pure): finds the described slice by CDHash, lists the others, names the differing fields (identifier, team, authorities, hardened runtime, entitlement keys), and reports CDHashes that appear in the requirement but not in cdhashes. An unmatched default slice is reported as unmatched, not guessed. Empty and absent entitlements compare equal, since codesign prints nothing for either.

docs/native-investigation.md: a paragraph on what the fields describe for universal files.

Not included: a structured per-slice field (agents that want the full record still run codesign -d -a themselves; the limitation tells them which slices and what differs), and the case where codesign's default slice is the unsigned one, which stays signed: false as in #3583.

Overlap with #3583: it also queries every slice and renames #inspectMixedSignatureSlices to #inspectSignatureSlices, but only to report unsigned slices; its own test asserts no limitation when all slices are signed. This PR adds the comparison on top. If #3583 lands first, I'll rebase.

Ordinary fix inside the existing inspect_signature boundary. All added commands are read-only codesign -d runs on the registered snapshot.

Review follow-up (second commit)

Codex and Greptile raised four points; all are in the second commit.

  • Default slice unsigned: codesign's display then says "not signed at all" and nothing queried the other slices. The slices are now queried from the bound target's architecture list, and a limitation names the signed ones with identifier, CDHash and hardened-runtime state. signed stays false for the default slice, as #3583 left it. Verified on a real file (arm64 slice unsigned, x86_64 ad hoc signed) on this Mac.
  • Slice entitlements with <data> or <date>: jsonValueSchema rejected them and the exception took the whole result down. parseEntitlements now falls back to the plist projection inspect_plist uses (typed $plist_type objects) with a limitation, and reports an undecodable plist as null instead of throwing. This also hardens the aggregate path, which had the same gap.
  • Omitted __proto__ keys in a slice's entitlements are now reported as Entitlements of the <slice> slice: …, same as the aggregate.
  • Requirement CDHashes are matched against the queried slices: names CDHashes of other slices (x86_64 2696…), and any hash matching no slice is listed separately as unmatched rather than attributed.
  • Also: the two "mixed signing states" limitations now need a signed slice to have been observed, so a fully unsigned universal file no longer calls itself mixed.

Tests added: unit tests for the unmatched-hash case and the unsigned-default description; golden tests for an unsigned default slice with a signed sibling (exact provenance) and for a non-default slice whose entitlements hold a data value and a __proto__ key. Re-ran: the signature test set (111 passed), every native test file (49 files), npm run check, npm run docs:check.

Review follow-up, round two (third commit)

  • Exact slice names for an unsigned default: the bound target names slices by CPU family, which merges arm64, arm64e and arm64e.x1. The unsigned path now asks lipo -archs for the names codesign accepts and falls back to the target's families only if lipo cannot answer. The golden test covers a signed arm64e.x1 sibling behind an unsigned x86_64 default; the real-file run above still works and gains the lipo -archs provenance entry.
  • Unreadable or undecodable slice entitlements are no longer compared. parseEntitlements reports whether it decoded anything; a slice whose plist could not be decoded keeps its entitlements unobserved and is named in Entitlements could not be read for Mach-O slices, so the comparison can no longer say entitlements none for it. The same flag stops slice entitlement queries when the aggregate plist itself did not decode. New golden test with a malformed plist on the non-default slice.
  • Projection notes are carried: non-finite reals, unknown dates and exact-integer notes from the plist projection now reach the result, prefixed with the slice; the plutil framing sentence and the __proto__ count are replaced by the entitlement-specific ones.
  • Round five (seventh commit): each slice's entitlements are compared as soon as they are decoded and only the outcome is kept, so parsed trees from many slices are never live together; entitlement plists holding integers beyond the exact range go through the XML projection so 9007199254740992 and 9007199254740993 compare as different (golden test); long values are cut at 80 characters but carry their length and a SHA-256 prefix so two long values that differ past the cut still read as different (unit test).
  • Sixth commit: entitlement keys are read as own properties when comparing slices, so a key named constructor or toString cannot reach a prototype property and throw inside the rendering; unit test added.
  • Round four (fifth commit): lipo is only consulted for mach-o-universal targets, so unsigned thin files never depend on it; the missing-lipo fallback is narrowed to the exact lipo is unavailable through xcrun reason with complete cleanup, so a closed client or cleanup failure still propagates; signed slices behind an unsigned default now report team identifier and authorities too; and entitlement differences carry values (get-task-allow: true (described slice false), … absent) instead of key names only, with a unit test for the same-key-different-value case.
  • Missing lipo (fourth commit): if lipo is unavailable through xcrun, the unsigned path falls back to the target's CPU families with a limitation instead of failing the result; cancellation, target-change and cleanup errors still propagate. Golden test added.
  • Memory: slice captures are projected to provenance entries as they arrive instead of being held until the end, so a 128-slice file no longer retains every raw codesign output at once.

Re-ran: signature test set (7 files, 130 passed), every native test file (49 files), npm run check, npm run docs:check, and the three CLI runs (fixture, /bin/ls, the real default-unsigned file).

Merge with main (2026-10-18)

Main rewrote parseEntitlements to project entitlement scalars through the plist owners (shared value and exact-number projections). The merge takes that version, which also covers the integer and data cases earlier commits here handled separately, and keeps only the decoded flag so an undecodable slice plist stays unobserved. After the merge: signature test set 122 passed, every native test file (60 files, 785 passed), npm run check and npm run docs:check ok.

Review follow-up after CassieFox23's triage (2026-10-19)

One commit (adae09e) for the five items marked real: a plist with an XML header but no closing tag is now undecodable rather than "no entitlements"; the echoed snapshot path is stripped before the per-slice not-signed verdict is matched, as the main parser already does; identifiers, team, authorities and entitlement keys are JSON-quoted in the limitation sentences; and entitlements are compared on a type-tagged form of the decoded plist as well as the projected JSON, so a <data> value and a dictionary shaped like its wrapper read as different. Each has a test. Left alone with a note on the thread: 21 (retained text size, unmeasured) and 28 (carrying codesign's diagnostic for a failed per-slice query, a design call); 20 is outdated and 22 resolved by the merge with main. Re-ran: signature test set 128 passed, every native test file (62 files, 809 passed), npm run check, npm run docs:check, and the CLI on the two real fixtures.

Merge with main (2026-10-22)

Main threaded a per-command deadline through the native provider; the slice and lipo captures now pass it along. Main also added a real-codesign CLI test that runs inspect-signature on a universal copy of /usr/bin/true with forged text in the file name and asserted the limitations list exactly; it now accepts the two slice sentences this PR adds beside the snapshot note, and additionally checks that none of the forged text (authority, identifier, entitlement, requirement) reaches any limitation. On the merged tree: signature test set 143 passed, every native test file (75 files, 987 passed), npm run check and npm run docs:check ok.

Merges with main (2026-10-21)

Two more merges. Main adopted an exact snapshot-framed unsigned verdict (isNonzeroUnsignedObservation now requires codesign's diagnostic for the bound snapshot and nothing else), which supersedes the echoed-path handling from adae09e; both the aggregate and the per-slice checks use it. Main's #5765 then added partial-observation retention for slice inspection: this merge keeps that, so provenance and limitations accumulate on the shared observation as slices arrive, a slice query that fails for a reason other than being unsigned is a failure that retains the facts collected so far (the earlier "could not be classified" limitation is gone), and the mixed-state notes are recorded before the slice loop. The echoed-file-name test now asserts that failure path. After the merge: signature test set 142 passed, every native test file (71 files, 913 passed), npm run docs:check ok; npm run check fails only on knip's unlisted g++ binary in src/windows/WindowsHandleClose.test.ts, a file from main (43ac3d1) that this branch does not touch.

Merge with main (2026-10-24)

Main's 27216508 now asks codesign for the selected architecture with -a on all three inspect_signature queries and probes the other slices only for their signing state, with a limitation saying their metadata is not returned. The merge keeps the -a binding, so the result's fields describe the requested slice by name instead of codesign's host default, and runs this PR's per-slice comparison on top; the limitation now ends "other slices are queried separately" and is followed by Other signed Mach-O slices, the differing fields, and the requirement-hash note. Main's two "unavailable because codesign reported it unsigned" notes replace this branch's mixed-signing notes.

One thing surfaced while merging: with -a, codesign reports the requested slice as Format=Mach-O thin (<arch>) even for a universal file (checked on /usr/bin/true, macOS 27), so main's probe loop, which takes slice names from the Format line and treats a thin format as no slices, finds nothing on a real universal file; its tests substitute a universal Format line. Here a universal target's slice names come from lipo -archs (the path this branch already had for the unsigned case), with the target's CPU families as the fallback when lipo is unavailable. Main's real-codesign binding test, which expected the probes to follow the target's CPU families, now expects lipo's exact slice names. On the merged tree: signature test set 91 passed, every native test file (87 files, 1239 passed), npm run check and npm run docs:check ok, with Node 26.12.1 as main now requires.

Contract and boundary impact

  • Semantic owner and earliest changed stage: #inspectBoundSignature / #inspectSignatureSlices in the native macOS provider, plus the new describeSignatureSlices.
  • Contribution scope fit and linked maintainer decision: not applicable.
  • CLI and MCP/tool-catalog contract: no schema change. New limitation sentences for signed universal files; provenance gains one -d -a <slice> --verbose=4 entry per slice and one --entitlements :- per differing slice.
  • Provider, bridge, target-format, or platform compatibility: native macOS provider only. Thin and fully unsigned files are unchanged.
  • Evidence, artifact, provenance, or reconstruction contract: every per-slice query is in provenance with its exit status; described slice, differing slices, unmatched default and unreadable slice entitlements are all stated as limitations.
  • Target selection, authority, upstream behavior, and cleanup details: each extra call goes through #captureSignature, so it reads the digest-verified snapshot and re-verifies the target before and after, like the existing calls.
  • Generated metadata, package, or installation impact: none.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated:

    • src/native/signatureSlices.test.ts (new, 7 tests): differing slices, slices signed alike, unmatched default slice, slices not asked for entitlements, empty vs absent entitlements, case-insensitive CDHash match, no slices.
    • tests/boundary/providers/native/nativeInventory.golden.test.ts: a real-capture case from the issue's fixture asserting the three new limitations and the exact six-command provenance; a signed-alike control (described-slice note only); a thin control (three commands, no slice note, runner throws on any -a). Existing mixed-signature tests pass unchanged.
    • tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts: the real-codesign swap test copies /usr/bin/true, which is universal, so it now checks that every call, including per-slice ones, reads $ARTIFACT instead of expecting exactly three.
  • Base reproduction: built main 42fc6c5 and ran the CLI on the fixture; output as described above, three provenance entries, no slice limitation. The new golden test fails on main.

  • CLI output on this branch (macOS 27.0.1 arm64):

    fixture (x86_64 + arm64, signed differently)
     - Signature fields describe the arm64 slice that codesign displayed by default; other signed slices: x86_64 (CDHash 269615ed3412eb7a8c0945e51c76355412db4b12).
     - Mach-O slices signed differently from the arm64 slice: x86_64 (identifier com.example.x86slice; hardened runtime off; entitlements com.apple.security.get-task-allow).
     - designated_requirement covers the whole file and names CDHashes of other slices (269615ed3412eb7a8c0945e51c76355412db4b12); cdhashes lists only the described slice.
     provenance: -d --verbose=4 · -d -r- · -d --entitlements :- · -d -a x86_64 --verbose=4 · -d -a x86_64 --entitlements :- · -d -a arm64 --verbose=4 (all exit 0)
    
    /bin/ls (x86_64 arm64e arm64e.x1, Apple-signed alike)
     - Signature fields describe the arm64e.x1 slice that codesign displayed by default; other signed slices: x86_64 (CDHash 61afc1ca…), arm64e (CDHash 54097a16…).
     (no "signed differently" line; 8 codesign runs)
    
  • Remaining proof gaps: one Apple Silicon Mac. On Intel the default slice differs, so the fields describe a different slice (the limitation names it). Slice names beyond x86_64, arm64, arm64e, arm64e.x1 weren't tried against real codesign. Extra run time not measured: one codesign -d -a per slice and one --entitlements per differing slice, only for signed universal files.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

macOS 27.0.1 arm64, Node 26.7.0, npm 11.19.0, on top of main 42fc6c5.

  • npm ci --ignore-scripts — ok
  • npm run test:focused on the 7 signature-related files (new unit test, golden, target binding, nativeProvider, nativeSignatureCli, nativeSignatureTargetBindingMcp, appleApplication) — 127 passed
  • npm run test:focused on every test file under tests/boundary/providers/native and src/native (49 files) — 625 passed
  • npm run check:fast — ok
  • npm run check — ok
  • npm run docs:check — ok
  • npm run verify:test-discovery — all 1401 test files found once
  • npm run build:cached, then the CLI runs above before and after
  • Not run: the full suite (test:fast), Linux, Intel macOS

Compatibility, safety, and release

  • Breaking changes or migration steps: none.
  • Real Hopper/Ghidra, browser, or OS coverage: real codesign on macOS 27.0.1 arm64 (CLI runs and the /usr/bin/true test).
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: added commands are read-only codesign -d on the registered snapshot; no target execution.

Author readiness

Human owner: @Ambar-13

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/4359 **Author:** [@Ambar-13](https://github.com/Ambar-13) **Created:** 10/16/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/signature-universal-slices` --- ### 📝 Commits (10+) - [`24e5c08`](https://github.com/morluto/rea/commit/24e5c08974d1de3ac5a6926c53e66e5f8a57ffbc) fix(native): report every slice of a signed universal Mach-O signature - [`b160d44`](https://github.com/morluto/rea/commit/b160d44f64ce2e02d9b6da305b351a5ee2248e69) fix(native): inspect slices behind an unsigned default and keep slice entitlement facts - [`7802a07`](https://github.com/morluto/rea/commit/7802a0777cd7dcb617098e4445b036789c9eba5f) fix(native): name exact slices behind an unsigned default and keep unread entitlements unobserved - [`c836573`](https://github.com/morluto/rea/commit/c8365731b33fd4799f45969606710eea7d6a140b) fix(native): keep signature facts when lipo is unavailable - [`6986254`](https://github.com/morluto/rea/commit/698625408f5bc47779624a39bf180506b2413331) fix(native): report slice signing identity and entitlement values in full - [`ed75ca7`](https://github.com/morluto/rea/commit/ed75ca7afe8d575fc3fdc1d7a1f7f4974691950d) fix(native): read entitlement keys as own properties when comparing slices - [`52bea8b`](https://github.com/morluto/rea/commit/52bea8b1d03602f4420d6c2b894438d6542183da) fix(native): compare slice entitlements as they arrive and keep exact integers - [`e17beb5`](https://github.com/morluto/rea/commit/e17beb5212e6a6c5e840ac5674fa4dbe0051fbb5) Merge origin/main into fix/signature-universal-slices - [`9308314`](https://github.com/morluto/rea/commit/930831444114ec1606643004f07b90703cc96b29) Merge origin/main into fix/signature-universal-slices - [`adae09e`](https://github.com/morluto/rea/commit/adae09ed084d478ef55842d19a86d122bf66e284) fix(native): quote slice identity values and keep cut-off entitlement plists unknown ### 📊 Changes **15 files changed** (+1861 additions, -75 deletions) <details> <summary>View changed files</summary> 📝 `docs/native-investigation.md` (+30 -4) 📝 `src/native/NativeMacOSProvider.ts` (+334 -53) ➕ `src/native/signatureSlices.test.ts` (+331 -0) ➕ `src/native/signatureSlices.ts` (+344 -0) 📝 `tests/boundary/cli/nativeSignatureCli.test.ts` (+16 -4) 📝 `tests/boundary/providers/native/nativeInventory.golden.test.ts` (+649 -1) 📝 `tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts` (+29 -13) ➕ `tests/fixtures/native-macos/codesign-universal-signed/README.md` (+47 -0) ➕ `tests/fixtures/native-macos/codesign-universal-signed/aggregate.txt` (+26 -0) ➕ `tests/fixtures/native-macos/codesign-universal-signed/entitlements-aggregate.xml` (+1 -0) ➕ `tests/fixtures/native-macos/codesign-universal-signed/entitlements-arm64.xml` (+1 -0) ➕ `tests/fixtures/native-macos/codesign-universal-signed/entitlements-x86_64.xml` (+1 -0) ➕ `tests/fixtures/native-macos/codesign-universal-signed/requirements.txt` (+1 -0) ➕ `tests/fixtures/native-macos/codesign-universal-signed/slice-arm64.txt` (+26 -0) ➕ `tests/fixtures/native-macos/codesign-universal-signed/slice-x86_64.txt` (+25 -0) </details> ### 📄 Description ## Summary Fixes #4293. On a universal Mach-O where every slice is signed, `inspect_signature` returned the identity of one slice and said nothing about the others. Now the fields describe the requested architecture (main's `-a` binding, since the 2026-10-24 merge below), and the result lists the other slices and their CDHashes, says which slices are signed differently and how, and explains why `designated_requirement` can list CDHashes that `cdhashes` doesn't. No schema change; the new facts are limitations plus per-slice `provenance` entries. ## Problem and expected behavior Build a two-slice file where the arm64 slice is ad hoc signed as `com.example.armslice` with the hardened runtime and `disable-library-validation`, and the x86_64 slice as `com.example.x86slice` with no hardened runtime and `get-task-allow` (commands in `tests/fixtures/native-macos/codesign-universal-signed/README.md`). On main, `rea inspect-signature fixture --json` reports `com.example.armslice`, one CDHash, `hardened_runtime: true` and only `disable-library-validation`, with no mention of slices, while `designated_requirement` lists both CDHashes. Each slice of a universal binary carries its own signature (Apple TN3126), and `codesign -d` without `-a` describes one slice. Presenting one slice's facts as the file's facts gets the x86_64 slice exactly wrong on the two facts that decide whether it can be debugged. ## Change and scope `src/native/NativeMacOSProvider.ts`: when the aggregate display reports a signed `Mach-O universal (…)` with more than one slice, run `codesign -d -a <slice> --verbose=4` for each slice (before, this only happened once an aggregate query had already hit an unsigned slice). Parse each signed slice with the existing `parseCodeSignature`. A slice whose CDHash differs from the aggregate is also asked for its entitlements, since they're sealed per code directory. Unsigned slices keep the `Unsigned Mach-O slices: …` limitation. Slice names come from codesign's own `Format=` line with no allow-list, so `arm64e.x1` on macOS 27 is queried too. `src/native/signatureSlices.ts` (new, pure): finds the described slice by CDHash, lists the others, names the differing fields (identifier, team, authorities, hardened runtime, entitlement keys), and reports CDHashes that appear in the requirement but not in `cdhashes`. An unmatched default slice is reported as unmatched, not guessed. Empty and absent entitlements compare equal, since codesign prints nothing for either. `docs/native-investigation.md`: a paragraph on what the fields describe for universal files. Not included: a structured per-slice field (agents that want the full record still run `codesign -d -a` themselves; the limitation tells them which slices and what differs), and the case where codesign's default slice is the unsigned one, which stays `signed: false` as in #3583. Overlap with #3583: it also queries every slice and renames `#inspectMixedSignatureSlices` to `#inspectSignatureSlices`, but only to report unsigned slices; its own test asserts no limitation when all slices are signed. This PR adds the comparison on top. If #3583 lands first, I'll rebase. Ordinary fix inside the existing `inspect_signature` boundary. All added commands are read-only `codesign -d` runs on the registered snapshot. ## Review follow-up (second commit) Codex and Greptile raised four points; all are in the second commit. - Default slice unsigned: codesign's display then says "not signed at all" and nothing queried the other slices. The slices are now queried from the bound target's architecture list, and a limitation names the signed ones with identifier, CDHash and hardened-runtime state. `signed` stays false for the default slice, as #3583 left it. Verified on a real file (arm64 slice unsigned, x86_64 ad hoc signed) on this Mac. - Slice entitlements with `<data>` or `<date>`: `jsonValueSchema` rejected them and the exception took the whole result down. `parseEntitlements` now falls back to the plist projection `inspect_plist` uses (typed `$plist_type` objects) with a limitation, and reports an undecodable plist as null instead of throwing. This also hardens the aggregate path, which had the same gap. - Omitted `__proto__` keys in a slice's entitlements are now reported as `Entitlements of the <slice> slice: …`, same as the aggregate. - Requirement CDHashes are matched against the queried slices: `names CDHashes of other slices (x86_64 2696…)`, and any hash matching no slice is listed separately as unmatched rather than attributed. - Also: the two "mixed signing states" limitations now need a signed slice to have been observed, so a fully unsigned universal file no longer calls itself mixed. Tests added: unit tests for the unmatched-hash case and the unsigned-default description; golden tests for an unsigned default slice with a signed sibling (exact provenance) and for a non-default slice whose entitlements hold a data value and a `__proto__` key. Re-ran: the signature test set (111 passed), every native test file (49 files), `npm run check`, `npm run docs:check`. ## Review follow-up, round two (third commit) - Exact slice names for an unsigned default: the bound target names slices by CPU family, which merges `arm64`, `arm64e` and `arm64e.x1`. The unsigned path now asks `lipo -archs` for the names codesign accepts and falls back to the target's families only if lipo cannot answer. The golden test covers a signed `arm64e.x1` sibling behind an unsigned `x86_64` default; the real-file run above still works and gains the `lipo -archs` provenance entry. - Unreadable or undecodable slice entitlements are no longer compared. `parseEntitlements` reports whether it decoded anything; a slice whose plist could not be decoded keeps its entitlements unobserved and is named in `Entitlements could not be read for Mach-O slices`, so the comparison can no longer say `entitlements none` for it. The same flag stops slice entitlement queries when the aggregate plist itself did not decode. New golden test with a malformed plist on the non-default slice. - Projection notes are carried: non-finite reals, unknown dates and exact-integer notes from the plist projection now reach the result, prefixed with the slice; the plutil framing sentence and the `__proto__` count are replaced by the entitlement-specific ones. - Round five (seventh commit): each slice's entitlements are compared as soon as they are decoded and only the outcome is kept, so parsed trees from many slices are never live together; entitlement plists holding integers beyond the exact range go through the XML projection so `9007199254740992` and `9007199254740993` compare as different (golden test); long values are cut at 80 characters but carry their length and a SHA-256 prefix so two long values that differ past the cut still read as different (unit test). - Sixth commit: entitlement keys are read as own properties when comparing slices, so a key named `constructor` or `toString` cannot reach a prototype property and throw inside the rendering; unit test added. - Round four (fifth commit): lipo is only consulted for `mach-o-universal` targets, so unsigned thin files never depend on it; the missing-lipo fallback is narrowed to the exact `lipo is unavailable through xcrun` reason with complete cleanup, so a closed client or cleanup failure still propagates; signed slices behind an unsigned default now report team identifier and authorities too; and entitlement differences carry values (`get-task-allow: true (described slice false)`, `… absent`) instead of key names only, with a unit test for the same-key-different-value case. - Missing lipo (fourth commit): if lipo is unavailable through xcrun, the unsigned path falls back to the target's CPU families with a limitation instead of failing the result; cancellation, target-change and cleanup errors still propagate. Golden test added. - Memory: slice captures are projected to provenance entries as they arrive instead of being held until the end, so a 128-slice file no longer retains every raw codesign output at once. Re-ran: signature test set (7 files, 130 passed), every native test file (49 files), `npm run check`, `npm run docs:check`, and the three CLI runs (fixture, `/bin/ls`, the real default-unsigned file). ## Merge with main (2026-10-18) Main rewrote `parseEntitlements` to project entitlement scalars through the plist owners (shared value and exact-number projections). The merge takes that version, which also covers the integer and data cases earlier commits here handled separately, and keeps only the `decoded` flag so an undecodable slice plist stays unobserved. After the merge: signature test set 122 passed, every native test file (60 files, 785 passed), `npm run check` and `npm run docs:check` ok. ## Review follow-up after CassieFox23's triage (2026-10-19) One commit (adae09e) for the five items marked real: a plist with an XML header but no closing tag is now undecodable rather than "no entitlements"; the echoed snapshot path is stripped before the per-slice not-signed verdict is matched, as the main parser already does; identifiers, team, authorities and entitlement keys are JSON-quoted in the limitation sentences; and entitlements are compared on a type-tagged form of the decoded plist as well as the projected JSON, so a `<data>` value and a dictionary shaped like its wrapper read as different. Each has a test. Left alone with a note on the thread: 21 (retained text size, unmeasured) and 28 (carrying codesign's diagnostic for a failed per-slice query, a design call); 20 is outdated and 22 resolved by the merge with main. Re-ran: signature test set 128 passed, every native test file (62 files, 809 passed), `npm run check`, `npm run docs:check`, and the CLI on the two real fixtures. ## Merge with main (2026-10-22) Main threaded a per-command deadline through the native provider; the slice and lipo captures now pass it along. Main also added a real-codesign CLI test that runs `inspect-signature` on a universal copy of `/usr/bin/true` with forged text in the file name and asserted the limitations list exactly; it now accepts the two slice sentences this PR adds beside the snapshot note, and additionally checks that none of the forged text (authority, identifier, entitlement, requirement) reaches any limitation. On the merged tree: signature test set 143 passed, every native test file (75 files, 987 passed), `npm run check` and `npm run docs:check` ok. ## Merges with main (2026-10-21) Two more merges. Main adopted an exact snapshot-framed unsigned verdict (`isNonzeroUnsignedObservation` now requires codesign's diagnostic for the bound snapshot and nothing else), which supersedes the echoed-path handling from adae09e; both the aggregate and the per-slice checks use it. Main's #5765 then added partial-observation retention for slice inspection: this merge keeps that, so provenance and limitations accumulate on the shared observation as slices arrive, a slice query that fails for a reason other than being unsigned is a failure that retains the facts collected so far (the earlier "could not be classified" limitation is gone), and the mixed-state notes are recorded before the slice loop. The echoed-file-name test now asserts that failure path. After the merge: signature test set 142 passed, every native test file (71 files, 913 passed), `npm run docs:check` ok; `npm run check` fails only on knip's unlisted `g++` binary in `src/windows/WindowsHandleClose.test.ts`, a file from main (43ac3d1) that this branch does not touch. ## Merge with main (2026-10-24) Main's 27216508 now asks codesign for the selected architecture with `-a` on all three `inspect_signature` queries and probes the other slices only for their signing state, with a limitation saying their metadata is not returned. The merge keeps the `-a` binding, so the result's fields describe the requested slice by name instead of codesign's host default, and runs this PR's per-slice comparison on top; the limitation now ends "other slices are queried separately" and is followed by `Other signed Mach-O slices`, the differing fields, and the requirement-hash note. Main's two "unavailable because codesign reported it unsigned" notes replace this branch's mixed-signing notes. One thing surfaced while merging: with `-a`, codesign reports the requested slice as `Format=Mach-O thin (<arch>)` even for a universal file (checked on `/usr/bin/true`, macOS 27), so main's probe loop, which takes slice names from the Format line and treats a thin format as no slices, finds nothing on a real universal file; its tests substitute a universal Format line. Here a universal target's slice names come from `lipo -archs` (the path this branch already had for the unsigned case), with the target's CPU families as the fallback when lipo is unavailable. Main's real-codesign binding test, which expected the probes to follow the target's CPU families, now expects lipo's exact slice names. On the merged tree: signature test set 91 passed, every native test file (87 files, 1239 passed), `npm run check` and `npm run docs:check` ok, with Node 26.12.1 as main now requires. ## Contract and boundary impact - Semantic owner and earliest changed stage: `#inspectBoundSignature` / `#inspectSignatureSlices` in the native macOS provider, plus the new `describeSignatureSlices`. - Contribution scope fit and linked maintainer decision: not applicable. - CLI and MCP/tool-catalog contract: no schema change. New limitation sentences for signed universal files; `provenance` gains one `-d -a <slice> --verbose=4` entry per slice and one `--entitlements :-` per differing slice. - Provider, bridge, target-format, or platform compatibility: native macOS provider only. Thin and fully unsigned files are unchanged. - Evidence, artifact, provenance, or reconstruction contract: every per-slice query is in provenance with its exit status; described slice, differing slices, unmatched default and unreadable slice entitlements are all stated as limitations. - Target selection, authority, upstream behavior, and cleanup details: each extra call goes through `#captureSignature`, so it reads the digest-verified snapshot and re-verifies the target before and after, like the existing calls. - Generated metadata, package, or installation impact: none. Expected effects: - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: - `src/native/signatureSlices.test.ts` (new, 7 tests): differing slices, slices signed alike, unmatched default slice, slices not asked for entitlements, empty vs absent entitlements, case-insensitive CDHash match, no slices. - `tests/boundary/providers/native/nativeInventory.golden.test.ts`: a real-capture case from the issue's fixture asserting the three new limitations and the exact six-command provenance; a signed-alike control (described-slice note only); a thin control (three commands, no slice note, runner throws on any `-a`). Existing mixed-signature tests pass unchanged. - `tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts`: the real-codesign swap test copies `/usr/bin/true`, which is universal, so it now checks that every call, including per-slice ones, reads `$ARTIFACT` instead of expecting exactly three. - Base reproduction: built main `42fc6c5` and ran the CLI on the fixture; output as described above, three provenance entries, no slice limitation. The new golden test fails on main. - CLI output on this branch (macOS 27.0.1 arm64): ```text fixture (x86_64 + arm64, signed differently) - Signature fields describe the arm64 slice that codesign displayed by default; other signed slices: x86_64 (CDHash 269615ed3412eb7a8c0945e51c76355412db4b12). - Mach-O slices signed differently from the arm64 slice: x86_64 (identifier com.example.x86slice; hardened runtime off; entitlements com.apple.security.get-task-allow). - designated_requirement covers the whole file and names CDHashes of other slices (269615ed3412eb7a8c0945e51c76355412db4b12); cdhashes lists only the described slice. provenance: -d --verbose=4 · -d -r- · -d --entitlements :- · -d -a x86_64 --verbose=4 · -d -a x86_64 --entitlements :- · -d -a arm64 --verbose=4 (all exit 0) /bin/ls (x86_64 arm64e arm64e.x1, Apple-signed alike) - Signature fields describe the arm64e.x1 slice that codesign displayed by default; other signed slices: x86_64 (CDHash 61afc1ca…), arm64e (CDHash 54097a16…). (no "signed differently" line; 8 codesign runs) ``` - Remaining proof gaps: one Apple Silicon Mac. On Intel the default slice differs, so the fields describe a different slice (the limitation names it). Slice names beyond `x86_64`, `arm64`, `arm64e`, `arm64e.x1` weren't tried against real codesign. Extra run time not measured: one `codesign -d -a` per slice and one `--entitlements` per differing slice, only for signed universal files. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed macOS 27.0.1 arm64, Node 26.7.0, npm 11.19.0, on top of main `42fc6c5`. - `npm ci --ignore-scripts` — ok - `npm run test:focused` on the 7 signature-related files (new unit test, golden, target binding, nativeProvider, nativeSignatureCli, nativeSignatureTargetBindingMcp, appleApplication) — 127 passed - `npm run test:focused` on every test file under `tests/boundary/providers/native` and `src/native` (49 files) — 625 passed - `npm run check:fast` — ok - `npm run check` — ok - `npm run docs:check` — ok - `npm run verify:test-discovery` — all 1401 test files found once - `npm run build:cached`, then the CLI runs above before and after - Not run: the full suite (`test:fast`), Linux, Intel macOS ## Compatibility, safety, and release - Breaking changes or migration steps: none. - Real Hopper/Ghidra, browser, or OS coverage: real `codesign` on macOS 27.0.1 arm64 (CLI runs and the `/usr/bin/true` test). - Package or release metadata impact: none. - Security, privacy, process, or containment review: added commands are read-only `codesign -d` on the registered snapshot; no target execution. ## Author readiness Human owner: @Ambar-13 - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:12:39 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#6202
No description provided.