diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eafda0d..ce3aac2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,16 +67,38 @@ jobs:
- name: Build and verify ad-hoc macOS package
run: bun run --cwd apps/desktop package:macos:adhoc
+ codex_signature_macos_26:
+ name: Codex signature normalization (macOS 26)
+ runs-on: macos-26
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
+ with:
+ bun-version: 1.3.14
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version-file: .node-version
+ - name: Install exact dependencies
+ run: bun install --frozen-lockfile
+ - name: Reproduce the Codex signature contract
+ run: bun test ./apps/desktop/runtime/test/codex-signature-normalization.macos.test.ts
+
required:
name: Required
if: ${{ always() }}
- needs: [verify]
+ needs: [verify, codex_signature_macos_26]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every blocking job
shell: bash
env:
+ CODEX_SIGNATURE_MACOS_26_RESULT: ${{ needs.codex_signature_macos_26.result }}
VERIFY_RESULT: ${{ needs.verify.result }}
run: |
set -euo pipefail
@@ -84,3 +106,7 @@ jobs:
echo "::error::verify finished with $VERIFY_RESULT"
exit 1
fi
+ if [[ "$CODEX_SIGNATURE_MACOS_26_RESULT" != "success" ]]; then
+ echo "::error::codex_signature_macos_26 finished with $CODEX_SIGNATURE_MACOS_26_RESULT"
+ exit 1
+ fi
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 884bfc7..2df8e07 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -37,9 +37,9 @@
"build:macos": "bun run ../../scripts/check-resource-scheduler.ts --mode=exclusive --label=HRA-desktop-macOS-build -- bun run build:macos:uncoordinated",
"build:macos:uncoordinated": "bun run check:generated:macos && bun run build:frontend && bun run build:runtime && bun run runtime/run-zig.ts build -Dplatform=macos -Doptimize=ReleaseFast && bun run check:direct-boundary",
"package:macos": "bun run ../../scripts/check-resource-scheduler.ts --mode=exclusive --label=HRA-desktop-macOS-package -- bun run package:macos:uncoordinated",
- "package:macos:uncoordinated": "bun run check:generated:macos && bun run build:runtime && bun run runtime/run-zig.ts build package -Dplatform=macos -Doptimize=ReleaseFast && bun run runtime/package-macos.ts && bun run runtime/verify-macos-package.ts && bun run runtime/create-dmg.ts",
+ "package:macos:uncoordinated": "bun run check:generated:macos && bun run build:runtime && bun run runtime/run-zig.ts build package -Dplatform=macos -Doptimize=ReleaseFast && bun run runtime/package-macos.ts && bun run runtime/verify-macos-package.ts && bun run runtime/verify-codex-signature-tamper.ts && bun run runtime/create-dmg.ts",
"package:macos:adhoc": "bun run ../../scripts/check-resource-scheduler.ts --mode=exclusive --label=HRA-desktop-macOS-ad-hoc-package -- bun run package:macos:adhoc:uncoordinated",
- "package:macos:adhoc:uncoordinated": "bun run check:generated:macos && bun run build:runtime && bun run runtime/run-zig.ts build package -Dplatform=macos -Doptimize=ReleaseFast && bun run runtime/package-macos.ts && bun run runtime/verify-macos-package.ts && bun run runtime/create-dmg.ts --core-only",
+ "package:macos:adhoc:uncoordinated": "bun run check:generated:macos && bun run build:runtime && bun run runtime/run-zig.ts build package -Dplatform=macos -Doptimize=ReleaseFast && bun run runtime/package-macos.ts && bun run runtime/verify-macos-package.ts && bun run runtime/verify-codex-signature-tamper.ts && bun run runtime/create-dmg.ts --core-only",
"verify:package:macos": "bun run ../../scripts/check-resource-scheduler.ts --mode=exclusive --label=HRA-desktop-macOS-package-verification -- bun run verify:package:macos:uncoordinated",
"verify:package:macos:uncoordinated": "bun run runtime/verify-macos-package.ts --release-directory zig-out/release/macos/arm64",
"verify:package:macos:adhoc": "bun run ../../scripts/check-resource-scheduler.ts --mode=exclusive --label=HRA-desktop-macOS-ad-hoc-package-verification -- bun run runtime/verify-macos-package.ts --core-release-directory zig-out/release/macos/arm64",
@@ -82,7 +82,7 @@
"baseline:reactive": "bun run runtime/reactive-baseline.ts",
"test:property": "bun test ./frontend/src/features/chat/model.property.test.ts ./frontend/src/runtime/*.property.test.ts ./frontend/direct/*.property.test.ts ./contracts/*.property.test.ts ./runtime/test/*.property.test.ts",
"test:macos": "bun run ../../scripts/check-resource-scheduler.ts --mode=exclusive --label=HRA-desktop-macOS-tests -- bun run test:macos:uncoordinated",
- "test:macos:uncoordinated": "bun run runtime/run-zig.ts build test -Dplatform=macos && bun test ./runtime/test/image-normalizer.macos.test.ts"
+ "test:macos:uncoordinated": "bun run runtime/run-zig.ts build test -Dplatform=macos && bun test ./runtime/test/image-normalizer.macos.test.ts ./runtime/test/codex-signature-normalization.macos.test.ts"
},
"dependencies": {
"@hraness/agent-tasks-protocol": "workspace:*",
diff --git a/apps/desktop/runtime/AGENTS.md b/apps/desktop/runtime/AGENTS.md
index 5759e12..fa482cd 100644
--- a/apps/desktop/runtime/AGENTS.md
+++ b/apps/desktop/runtime/AGENTS.md
@@ -11,8 +11,9 @@
- `shipped-javascript-licenses.ts` – fail-closed, overinclusive production dependency inventory with nested license text and reviewed package exceptions.
- `generate-codex-schema.ts` – pinned Codex schema generation and checked-source verification.
- `frontend-package-integrity.ts` and `prepare-package-output.ts` – deterministic frontend asset validation used by the native build graph.
-- `package-macos.ts`, `verify-macos-package.ts`, `create-dmg.ts`, and `corresponding-sources.ts` – credential-free runtime staging, inside-out ad-hoc signing, package verification, DMG assembly, and full-commit GPL/LGPL source archives.
-- `test/image-normalizer.macos.test.ts` – macOS-only container, orientation, metadata, bounds, filesystem-race, deterministic-output, and code-signing regressions for the bundled image normalizer.
+- `package-macos.ts`, `verify-macos-package.ts`, `verify-codex-signature-tamper.ts`, `create-dmg.ts`, and `corresponding-sources.ts` – credential-free runtime staging, inside-out ad-hoc signing, package verification and tamper regressions, DMG assembly, and full-commit GPL/LGPL source archives.
+- `codex-signature-normalization.ts`, `codex-signature-normalization.entitlements.plist`, and `CODEX-SIGNATURE-NORMALIZATION.md` – exact, reversible policy, JIT entitlement allowlist, and evidence for the two pinned Codex payloads whose upstream Developer ID signatures fail strict validation on supported macOS.
+- `test/image-normalizer.macos.test.ts` and `test/codex-signature-normalization.macos.test.ts` – macOS-only image-normalizer regressions and cross-host Codex signing-page determinism evidence.
- `control-plane-maintenance.ts` – app-stopped health checks plus encrypted backup, inspection, verification, and restore.
- `installation-handoff.ts`, `installation-path-authority.ts`, and `installation-process-authority.ts` – the fail-closed OPRTE-to-HRA application handoff, resumable committed cleanup, exact filesystem authority, ordered native-root shutdown, and unchanged-state rollback.
- `release-download-contract.ts`, `release-provenance.ts`, and `release-download.json` at the repository root – strict candidate/publication evidence and hermetic canonical Git provenance for downloadable releases.
@@ -33,7 +34,7 @@
- Page immutable snapshots that exceed Native's response limit. Replace oversized recoverable events with `snapshot.invalidated`; never discard terminal or human-in-the-loop events to make them fit.
- Keep the gateway as the semantic proxy. Native owns launch, lifecycle, trusted directory selection, and transport plumbing.
- Recovery-only local-data removal startup may resume only its strict recovery state machine. It must not open normal application writers.
-- Keep Developer ID signing, notarization, provider writes, and publication code outside the public source workspace. Ad-hoc packaging must remain credential-free, preserve trusted upstream signatures, sign HRA code inside-out, and keep Sparkle disabled.
+- Keep Developer ID signing, notarization, provider writes, and publication code outside the public source workspace. Ad-hoc packaging must remain credential-free, preserve strict-valid trusted upstream signatures except for the exact pinned Codex normalization policy, sign HRA code inside-out, and keep Sparkle disabled. Bind each Codex exception to exact package and source identity; the exact two-key JIT entitlement allowlist; explicit digest, timestamp, hardened-runtime version, and 16 KiB page-size inputs; structural non-signature equivalence; exact packaged identity; and an exact reversible source delta. Require the unchanged native payload verifier to accept each owner-private reconstruction and the signed code-mode host to complete a framed-protocol V8 JIT smoke.
- Stage `hra-image-normalizer` as entitlement-free HRA-owned code in every macOS package shape. Bind its SHA-256 and CodeDirectory hash into the runtime manifest, verify its exact identifier and empty entitlement set in both the app and mounted DMG, and never weaken nested verification to accommodate another runtime.
- Reconcile image-normalizer residue before opening attachment state: remove only exact `.hra-image-normalizer-<32 lowercase hex>.tmp` sibling directories through no-follow descriptor-relative traversal. Never promote temp residue. Treat a missing final generation as uncommitted, and accept an existing final generation only when its exact two-file inventory, identities, sizes, and SHA-256 values match the durable receipt.
- Inventory the installed frontend and gateway production dependency closure, including nested license files. Fail packaging on missing identity, version, license metadata, text, unexpected file types, UTF-8 BOMs, or hash drift; every defective upstream tarball needs an exact version-bound reviewed exception and provenance.
diff --git a/apps/desktop/runtime/CODEX-SIGNATURE-NORMALIZATION.md b/apps/desktop/runtime/CODEX-SIGNATURE-NORMALIZATION.md
new file mode 100644
index 0000000..8a9846c
--- /dev/null
+++ b/apps/desktop/runtime/CODEX-SIGNATURE-NORMALIZATION.md
@@ -0,0 +1,66 @@
+# Codex signature normalization
+
+HRA packages the exact official `@openai/codex@0.144.6-darwin-arm64`
+payload identified by npm integrity
+`sha512-6zgvh70MzBNSeT17HEhSOrmmGGZGAKzSC7x6JAq+edkJkdPYA9P0I1tG7aJ49GlBkBxuC+MKBH1qm6+2Cghcww==`.
+Its `codex` and `codex-code-mode-host` Developer ID signatures validate on
+some supported macOS builders but fail strict validation on macOS 26.5.2.
+The same failure is reproducible from fresh official npm downloads, follows
+the bytes across copies, and is not caused by quarantine or path metadata.
+
+The package build verifies the official source package and both source files
+before changing the staged copy. It requires these exact source identities:
+
+| Payload | Source SHA-256 | Size | Identifier | Team | CDHash |
+| --- | --- | ---: | --- | --- | --- |
+| `bin/codex` | `80a3933d11a9d13ef806aa24f7bb8afc9169cfe4e9b09d6da6a92922cbde9cff` | 260472144 | `codex` | `2DC432GLL2` | `14fe9fce7d47a8c12e42094e5cc90ff97b2cf627` |
+| `bin/codex-code-mode-host` | `de329ec247b5ebbdf796b5888a7c2a9d731e221321584c5abdcc686c70b2db81` | 46374288 | `codex-code-mode-host` | `2DC432GLL2` | `d4a7d8e1af4b06413ef43fa933d983c3db019e8f` |
+
+Only those two exact staged files are re-signed. The command fixes every input
+that otherwise varies across supported builders: ad-hoc identity, the original
+identifier, hardened-runtime flags and version `15.5.0`, SHA-256 digest, no
+timestamp, 16 KiB signing pages, and DER entitlement generation. In particular,
+the default signing page size differs between supported macOS builders.
+
+The canonical
+`codex-signature-normalization.entitlements.plist` has SHA-256
+`a2f94dda68da5a6d994132cfc3ee49f07b83bccc5c1b9d5653e2e5fdb228ff41`
+and contains exactly two true keys:
+
+- `com.apple.security.cs.allow-jit`
+- `com.apple.security.cs.allow-unsigned-executable-memory`
+
+Those permissions reproduce the two permissions on the official payload and
+are required by the V8 code-mode host. No Info.plist, sealed resources,
+timestamp, or custom requirement is bound. The build and verifier require
+these exact packaged identities:
+
+| Payload | Packaged SHA-256 | Size | Identifier | Team | CDHash | Page size | Runtime |
+| --- | --- | ---: | --- | --- | --- | ---: | --- |
+| `bin/codex` | `055f18d2a33a719a2fab08e0a8326d950fa733340c596bb3df0d8dc94f85a96e` | 258960048 | `codex` | unset | `d5a8decaaecc44cd318c818f9ad794083570a812` | 16384 | `15.5.0` |
+| `bin/codex-code-mode-host` | `7f622f21007acac2780b0e9e39822ba493425366fc1cf996c24adafc9c0a6e08` | 46107184 | `codex-code-mode-host` | unset | `62c42f5ea878b3d0cf931a993216a4034cd8e91f` | 16384 | `15.5.0` |
+
+Both normalized files must pass strict code-signature verification before the
+outer app is signed. The runtime manifest records the source and packaged
+identity and signing contract for each file. Structural verification proves
+that every byte outside the Mach-O signature envelope is unchanged. A
+deterministic, bounded source delta is also packaged for each normalized file.
+Verification reconstructs the exact official source bytes in an owner-private
+temporary directory and runs the unchanged native payload verifier against the
+reconstructed vendor tree. The deltas are pinned to SHA-256
+`b0b05a7e03adf00fc1293b3e2679464cd8ec63024ca0ab5448915b5c33a1dadd`
+and `5952f9bc32083e1f62e1cc13c55b5b50145f8f7e4df56dd89c2d8d5267d9c2c2`.
+
+All other reviewed third-party signatures remain unchanged. Any upstream
+version, package integrity, source hash, size, identifier, team, CDHash, or
+source delta, entitlement, signing page, or runtime-version change requires a
+new reviewed policy and evidence. macOS 15 and macOS 26 CI both reconstruct
+the exact normalized identities, and the signed code-mode host must complete a
+framed-protocol V8 JIT execution.
+
+Every macOS package build runs destructive-in-fixture regressions against the
+exact staged app. They alter each normalized payload and source delta, alter a
+normalized manifest path, and re-sign one payload without hardened-runtime
+flags. Each mutation must make the full app verifier fail. The runner restores
+the exact original bytes, reruns the full verifier, and finishes with deep,
+strict verification of the restored outer app.
diff --git a/apps/desktop/runtime/codex-signature-normalization.entitlements.plist b/apps/desktop/runtime/codex-signature-normalization.entitlements.plist
new file mode 100644
index 0000000..9ab52e1
--- /dev/null
+++ b/apps/desktop/runtime/codex-signature-normalization.entitlements.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
diff --git a/apps/desktop/runtime/codex-signature-normalization.ts b/apps/desktop/runtime/codex-signature-normalization.ts
new file mode 100644
index 0000000..9196dba
--- /dev/null
+++ b/apps/desktop/runtime/codex-signature-normalization.ts
@@ -0,0 +1,738 @@
+import { constants } from "node:fs";
+import { copyFile, lstat, open, readFile, rm } from "node:fs/promises";
+
+import type { CodexNativeLicenseInventory } from "./codex-native-licenses";
+
+const deltaMagic = Buffer.from("HRACSD01", "ascii");
+const deltaHeaderBytes = 28;
+const deltaSegmentHeaderBytes = 16;
+const maximumSourceBytes = 350_000_000;
+const maximumDeltaBytes = 50_000_000;
+const maximumDeltaSegments = 100_000;
+const maximumMachOLoadCommandBytes = 16 * 1_024 * 1_024;
+
+export type CodeSignatureMetadata = Readonly<{
+ cdHash: string | null;
+ identifier: string | null;
+ teamIdentifier: string | null;
+}>;
+
+export const CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE = 16_384 as const;
+export const CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION = "15.5.0" as const;
+export const CODEX_SIGNATURE_NORMALIZATION_ENTITLEMENTS_FILE =
+ "codex-signature-normalization.entitlements.plist" as const;
+
+export const codexSignatureNormalizationEntitlements = Object.freeze({
+ "com.apple.security.cs.allow-jit": true,
+ "com.apple.security.cs.allow-unsigned-executable-memory": true,
+});
+
+export const codexSignatureNormalizationSigning = Object.freeze({
+ digestAlgorithm: "sha256" as const,
+ entitlementsFile: CODEX_SIGNATURE_NORMALIZATION_ENTITLEMENTS_FILE,
+ entitlementsSha256:
+ "a2f94dda68da5a6d994132cfc3ee49f07b83bccc5c1b9d5653e2e5fdb228ff41",
+ generateEntitlementDer: true as const,
+ pageSize: CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE,
+ runtimeVersion: CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION,
+ timestamp: "none" as const,
+});
+
+export type CodexSignatureNormalizationEntry = Readonly<{
+ appRelativePath: string;
+ packaged: Readonly<{
+ cdHash: string;
+ identifier: string;
+ pageSize: typeof CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE;
+ runtimeVersion: typeof CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION;
+ sha256: string;
+ size: number;
+ teamIdentifier: null;
+ }>;
+ payloadPath: string;
+ source: Readonly<{
+ cdHash: string;
+ identifier: string;
+ sha256: string;
+ size: number;
+ teamIdentifier: string;
+ }>;
+ sourceDelta: Readonly<{
+ format: "hra-source-delta-v1";
+ path: string;
+ sha256: string;
+ size: number;
+ }>;
+}>;
+
+export type CodexSignatureNormalizationManifestEntry = Readonly<{
+ normalization: "adhoc-runtime-v1";
+ packaged: CodexSignatureNormalizationEntry["packaged"];
+ path: string;
+ signing: typeof codexSignatureNormalizationSigning;
+ source: CodexSignatureNormalizationEntry["source"];
+ sourceDelta: CodexSignatureNormalizationEntry["sourceDelta"];
+}>;
+
+const entries = Object.freeze([
+ Object.freeze({
+ appRelativePath: "Contents/Resources/runtime/codex/bin/codex",
+ packaged: Object.freeze({
+ cdHash: "d5a8decaaecc44cd318c818f9ad794083570a812",
+ identifier: "codex",
+ pageSize: CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE,
+ runtimeVersion: CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION,
+ sha256: "055f18d2a33a719a2fab08e0a8326d950fa733340c596bb3df0d8dc94f85a96e",
+ size: 258_960_048,
+ teamIdentifier: null,
+ }),
+ payloadPath: "bin/codex",
+ source: Object.freeze({
+ cdHash: "14fe9fce7d47a8c12e42094e5cc90ff97b2cf627",
+ identifier: "codex",
+ sha256: "80a3933d11a9d13ef806aa24f7bb8afc9169cfe4e9b09d6da6a92922cbde9cff",
+ size: 260_472_144,
+ teamIdentifier: "2DC432GLL2",
+ }),
+ sourceDelta: Object.freeze({
+ format: "hra-source-delta-v1",
+ path: "Contents/Resources/runtime/provenance/codex-signatures/codex.source-delta",
+ sha256: "b0b05a7e03adf00fc1293b3e2679464cd8ec63024ca0ab5448915b5c33a1dadd",
+ size: 2_046_810,
+ }),
+ }),
+ Object.freeze({
+ appRelativePath: "Contents/Resources/runtime/codex/bin/codex-code-mode-host",
+ packaged: Object.freeze({
+ cdHash: "62c42f5ea878b3d0cf931a993216a4034cd8e91f",
+ identifier: "codex-code-mode-host",
+ pageSize: CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE,
+ runtimeVersion: CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION,
+ sha256: "7f622f21007acac2780b0e9e39822ba493425366fc1cf996c24adafc9c0a6e08",
+ size: 46_107_184,
+ teamIdentifier: null,
+ }),
+ payloadPath: "bin/codex-code-mode-host",
+ source: Object.freeze({
+ cdHash: "d4a7d8e1af4b06413ef43fa933d983c3db019e8f",
+ identifier: "codex-code-mode-host",
+ sha256: "de329ec247b5ebbdf796b5888a7c2a9d731e221321584c5abdcc686c70b2db81",
+ size: 46_374_288,
+ teamIdentifier: "2DC432GLL2",
+ }),
+ sourceDelta: Object.freeze({
+ format: "hra-source-delta-v1",
+ path:
+ "Contents/Resources/runtime/provenance/codex-signatures/codex-code-mode-host.source-delta",
+ sha256: "5952f9bc32083e1f62e1cc13c55b5b50145f8f7e4df56dd89c2d8d5267d9c2c2",
+ size: 363_584,
+ }),
+ }),
+] satisfies readonly CodexSignatureNormalizationEntry[]);
+
+export const codexSignatureNormalizationPolicy = Object.freeze({
+ entries,
+ packageIntegrity:
+ "sha512-6zgvh70MzBNSeT17HEhSOrmmGGZGAKzSC7x6JAq+edkJkdPYA9P0I1tG7aJ49GlBkBxuC+MKBH1qm6+2Cghcww==",
+ packageManifestSha256:
+ "051cbc20f48e7bd20b89e301ffc8f60af890a1da3815e5e700f11ada41c3b445",
+ packageName: "@openai/codex",
+ packageTarget: "aarch64-apple-darwin",
+ packageVersion: "0.144.6-darwin-arm64",
+} as const);
+
+export function codexSignatureNormalizationEntry(
+ payloadPath: string,
+): CodexSignatureNormalizationEntry {
+ const entry = codexSignatureNormalizationPolicy.entries.find(
+ (candidate) => candidate.payloadPath === payloadPath,
+ );
+ if (entry === undefined) {
+ throw new Error(`Codex signature normalization policy is absent: ${payloadPath}`);
+ }
+ return entry;
+}
+
+function signatureEquals(
+ actual: CodeSignatureMetadata,
+ expected: CodeSignatureMetadata,
+): boolean {
+ return actual.cdHash === expected.cdHash
+ && actual.identifier === expected.identifier
+ && actual.teamIdentifier === expected.teamIdentifier;
+}
+
+export function codexSignatureNormalizationCodesignArguments(
+ entry: CodexSignatureNormalizationEntry,
+ entitlementsPath: string,
+ path: string,
+): readonly string[] {
+ if (entitlementsPath.length === 0 || path.length === 0) {
+ throw new Error("Codex signature normalization paths must not be empty.");
+ }
+ return Object.freeze([
+ "/usr/bin/codesign",
+ "--force",
+ "--sign",
+ "-",
+ "--options",
+ "runtime",
+ "--entitlements",
+ entitlementsPath,
+ "--generate-entitlement-der",
+ "--timestamp=none",
+ "--digest-algorithm=sha256",
+ "--runtime-version",
+ CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION,
+ "--pagesize",
+ String(CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE),
+ "--identifier",
+ entry.source.identifier,
+ path,
+ ]);
+}
+
+export function parseCodexSignatureNormalizationEntitlements(
+ codesignOutput: string,
+): Readonly> {
+ const dictionary = /([\s\S]*?)<\/dict>/u.exec(codesignOutput)?.[1];
+ if (dictionary === undefined) {
+ throw new Error("Normalized Codex entitlements are absent.");
+ }
+ const compact = dictionary.replace(/\s+/gu, "");
+ const entries: Record = {};
+ const entryPattern = /([^<]+)<\/key><(true|false)\/>/gyu;
+ let cursor = 0;
+ while (cursor < compact.length) {
+ entryPattern.lastIndex = cursor;
+ const match = entryPattern.exec(compact);
+ if (match === null || match.index !== cursor) {
+ throw new Error("Normalized Codex entitlements are malformed.");
+ }
+ const [, key, boolean] = match;
+ if (key === undefined || boolean === undefined || key in entries) {
+ throw new Error("Normalized Codex entitlements are malformed.");
+ }
+ entries[key] = boolean === "true";
+ cursor = entryPattern.lastIndex;
+ }
+ return Object.freeze(entries);
+}
+
+export function verifyCodexSignatureNormalizationInventory(
+ inventory: CodexNativeLicenseInventory,
+): void {
+ const policy = codexSignatureNormalizationPolicy;
+ if (
+ inventory.platformPackage.integrity !== policy.packageIntegrity
+ || inventory.platformPackage.manifestSha256 !== policy.packageManifestSha256
+ || inventory.platformPackage.name !== policy.packageName
+ || inventory.platformPackage.target !== policy.packageTarget
+ || inventory.platformPackage.version !== policy.packageVersion
+ ) {
+ throw new Error("Codex signature normalization package provenance differs.");
+ }
+ const payloads = new Map(
+ inventory.platformPackage.payloads.map((payload) => [payload.path, payload]),
+ );
+ for (const entry of policy.entries) {
+ const payload = payloads.get(entry.payloadPath);
+ if (
+ payload === undefined
+ || payload.sha256 !== entry.source.sha256
+ || payload.size !== entry.source.size
+ ) {
+ throw new Error(
+ `Codex signature normalization source payload differs: ${entry.payloadPath}`,
+ );
+ }
+ }
+}
+
+export function verifyCodexSignatureNormalizationSource(
+ entry: CodexSignatureNormalizationEntry,
+ actual: Readonly<{
+ sha256: string;
+ signature: CodeSignatureMetadata;
+ size: number;
+ }>,
+): void {
+ if (
+ actual.sha256 !== entry.source.sha256
+ || actual.size !== entry.source.size
+ || !signatureEquals(actual.signature, entry.source)
+ ) {
+ throw new Error(
+ `Codex normalization source identity differs: ${entry.payloadPath}`,
+ );
+ }
+}
+
+export function verifyCodexSignatureNormalizationPackaged(
+ entry: CodexSignatureNormalizationEntry,
+ actual: Readonly<{
+ sha256: string;
+ signature: CodeSignatureMetadata & Readonly<{
+ entitlements: Readonly>;
+ flags: readonly string[];
+ hashChoices: readonly string[];
+ hashType: string | null;
+ infoPlistBound: boolean | null;
+ internalRequirementsCount: number | null;
+ pageSize: number | null;
+ runtimeVersion: string | null;
+ sealedResources: string | null;
+ signatureKind: string | null;
+ timestamp: string | null;
+ }>;
+ size: number;
+ }>,
+): void {
+ const flags = [...actual.signature.flags];
+ const entitlementKeys = Object.keys(actual.signature.entitlements).sort();
+ const expectedEntitlementKeys = Object.keys(
+ codexSignatureNormalizationEntitlements,
+ ).sort();
+ if (
+ actual.sha256 !== entry.packaged.sha256
+ || actual.size !== entry.packaged.size
+ || !signatureEquals(actual.signature, entry.packaged)
+ || actual.signature.pageSize !== entry.packaged.pageSize
+ || actual.signature.runtimeVersion !== entry.packaged.runtimeVersion
+ || actual.signature.signatureKind !== "adhoc"
+ || actual.signature.hashType !== codexSignatureNormalizationSigning.digestAlgorithm
+ || actual.signature.hashChoices.length !== 1
+ || actual.signature.hashChoices[0]
+ !== codexSignatureNormalizationSigning.digestAlgorithm
+ || actual.signature.infoPlistBound !== false
+ || actual.signature.internalRequirementsCount !== 0
+ || actual.signature.sealedResources !== "none"
+ || actual.signature.timestamp !== null
+ || entitlementKeys.length !== expectedEntitlementKeys.length
+ || entitlementKeys.some((key, index) => key !== expectedEntitlementKeys[index])
+ || expectedEntitlementKeys.some(
+ (key) => actual.signature.entitlements[key] !== true,
+ )
+ || flags.length !== 2
+ || new Set(flags).size !== 2
+ || !flags.includes("adhoc")
+ || !flags.includes("runtime")
+ ) {
+ throw new Error(
+ `Normalized Codex package identity differs: ${entry.payloadPath}`,
+ );
+ }
+}
+
+export function codexSignatureNormalizationManifestEntries(): readonly CodexSignatureNormalizationManifestEntry[] {
+ return codexSignatureNormalizationPolicy.entries.map((entry) => Object.freeze({
+ normalization: "adhoc-runtime-v1" as const,
+ packaged: entry.packaged,
+ path: entry.appRelativePath,
+ signing: codexSignatureNormalizationSigning,
+ source: entry.source,
+ sourceDelta: entry.sourceDelta,
+ }));
+}
+
+async function readExactChunk(
+ handle: Awaited>,
+ length: number,
+ position: number,
+): Promise {
+ const bytes = Buffer.allocUnsafe(length);
+ let offset = 0;
+ while (offset < length) {
+ const result = await handle.read(bytes, offset, length - offset, position + offset);
+ if (result.bytesRead === 0) {
+ throw new Error("Codex signature delta source changed while read.");
+ }
+ offset += result.bytesRead;
+ }
+ return bytes;
+}
+
+type MachOSignatureEnvelope = Readonly<{
+ codeSignatureCommandOffset: number;
+ codeSignatureDataOffset: number;
+ headerAndCommands: Buffer;
+ linkeditCommandOffset: number;
+ linkeditFileOffset: number;
+}>;
+
+async function readMachOSignatureEnvelope(
+ path: string,
+): Promise {
+ const status = await lstat(path);
+ if (
+ !status.isFile()
+ || status.isSymbolicLink()
+ || status.nlink !== 1
+ || status.size <= 0
+ || status.size > maximumSourceBytes
+ ) {
+ throw new Error(
+ "Codex normalization content must be a bounded regular single-link file.",
+ );
+ }
+ const handle = await open(path, "r");
+ try {
+ const header = await readExactChunk(handle, 32, 0);
+ const commandCount = header.readUInt32LE(16);
+ const commandBytes = header.readUInt32LE(20);
+ if (
+ header.readUInt32LE(0) !== 0xfeedfacf
+ || commandCount === 0
+ || commandCount > 4_096
+ || commandBytes === 0
+ || commandBytes > maximumMachOLoadCommandBytes
+ || 32 + commandBytes > status.size
+ ) {
+ throw new Error("Codex normalization content has an invalid Mach-O header.");
+ }
+ const headerAndCommands = await readExactChunk(handle, 32 + commandBytes, 0);
+ let cursor = 32;
+ let codeSignature: Readonly<{
+ commandOffset: number;
+ dataOffset: number;
+ dataSize: number;
+ }> | undefined;
+ let linkedit: Readonly<{
+ commandOffset: number;
+ fileOffset: number;
+ fileSize: number;
+ vmSize: number;
+ }> | undefined;
+ for (let index = 0; index < commandCount; index += 1) {
+ if (cursor + 8 > headerAndCommands.byteLength) {
+ throw new Error("Codex normalization content has truncated Mach-O commands.");
+ }
+ const command = headerAndCommands.readUInt32LE(cursor);
+ const commandSize = headerAndCommands.readUInt32LE(cursor + 4);
+ if (
+ commandSize < 8
+ || commandSize % 4 !== 0
+ || cursor + commandSize > headerAndCommands.byteLength
+ ) {
+ throw new Error("Codex normalization content has an invalid Mach-O command.");
+ }
+ if (command === 0x19 && commandSize >= 72) {
+ const segmentName = headerAndCommands
+ .subarray(cursor + 8, cursor + 24)
+ .toString("utf8")
+ .replace(/\0+$/u, "");
+ if (segmentName === "__LINKEDIT") {
+ if (linkedit !== undefined) {
+ throw new Error(
+ "Codex normalization content has duplicate __LINKEDIT segments.",
+ );
+ }
+ linkedit = {
+ commandOffset: cursor,
+ fileOffset: safeBigUInt(
+ headerAndCommands.readBigUInt64LE(cursor + 40),
+ "Mach-O __LINKEDIT file offset",
+ ),
+ fileSize: safeBigUInt(
+ headerAndCommands.readBigUInt64LE(cursor + 48),
+ "Mach-O __LINKEDIT file size",
+ ),
+ vmSize: safeBigUInt(
+ headerAndCommands.readBigUInt64LE(cursor + 32),
+ "Mach-O __LINKEDIT VM size",
+ ),
+ };
+ }
+ } else if (command === 0x1d && commandSize === 16) {
+ if (codeSignature !== undefined) {
+ throw new Error("Codex normalization content has duplicate code signatures.");
+ }
+ codeSignature = {
+ commandOffset: cursor,
+ dataOffset: headerAndCommands.readUInt32LE(cursor + 8),
+ dataSize: headerAndCommands.readUInt32LE(cursor + 12),
+ };
+ }
+ cursor += commandSize;
+ }
+ if (
+ cursor !== headerAndCommands.byteLength
+ || codeSignature === undefined
+ || linkedit === undefined
+ || codeSignature.dataOffset < headerAndCommands.byteLength
+ || codeSignature.dataSize <= 0
+ || codeSignature.dataOffset + codeSignature.dataSize !== status.size
+ || linkedit.fileOffset > codeSignature.dataOffset
+ || linkedit.fileSize <= 0
+ || linkedit.fileOffset + linkedit.fileSize !== status.size
+ || linkedit.vmSize < linkedit.fileSize
+ || linkedit.vmSize - linkedit.fileSize >= CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE
+ || linkedit.vmSize % CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE !== 0
+ ) {
+ throw new Error("Codex normalization content has an invalid signature envelope.");
+ }
+ return {
+ codeSignatureCommandOffset: codeSignature.commandOffset,
+ codeSignatureDataOffset: codeSignature.dataOffset,
+ headerAndCommands,
+ linkeditCommandOffset: linkedit.commandOffset,
+ linkeditFileOffset: linkedit.fileOffset,
+ };
+ } finally {
+ await handle.close();
+ }
+}
+
+function sameMachOHeaderOutsideSignatureEnvelope(
+ source: MachOSignatureEnvelope,
+ packaged: MachOSignatureEnvelope,
+): boolean {
+ if (source.headerAndCommands.byteLength !== packaged.headerAndCommands.byteLength) {
+ return false;
+ }
+ const mutableRanges = [
+ [source.linkeditCommandOffset + 32, source.linkeditCommandOffset + 40],
+ [source.linkeditCommandOffset + 48, source.linkeditCommandOffset + 56],
+ [source.codeSignatureCommandOffset + 12, source.codeSignatureCommandOffset + 16],
+ ] as const;
+ for (let index = 0; index < source.headerAndCommands.byteLength; index += 1) {
+ if (mutableRanges.some(([start, end]) => index >= start && index < end)) continue;
+ if (source.headerAndCommands[index] !== packaged.headerAndCommands[index]) return false;
+ }
+ return true;
+}
+
+export async function verifyCodexSignatureNormalizationContent(
+ sourcePath: string,
+ packagedPath: string,
+): Promise {
+ const [source, packaged] = await Promise.all([
+ readMachOSignatureEnvelope(sourcePath),
+ readMachOSignatureEnvelope(packagedPath),
+ ]);
+ if (
+ source.codeSignatureCommandOffset !== packaged.codeSignatureCommandOffset
+ || source.codeSignatureDataOffset !== packaged.codeSignatureDataOffset
+ || source.linkeditCommandOffset !== packaged.linkeditCommandOffset
+ || source.linkeditFileOffset !== packaged.linkeditFileOffset
+ || !sameMachOHeaderOutsideSignatureEnvelope(source, packaged)
+ ) {
+ throw new Error(
+ "Normalized Codex content changed outside its code-signature envelope.",
+ );
+ }
+ const sourceHandle = await open(sourcePath, "r");
+ const packagedHandle = await open(packagedPath, "r");
+ try {
+ const chunkBytes = 1024 * 1024;
+ for (
+ let position = source.headerAndCommands.byteLength;
+ position < source.codeSignatureDataOffset;
+ position += chunkBytes
+ ) {
+ const length = Math.min(chunkBytes, source.codeSignatureDataOffset - position);
+ const [sourceChunk, packagedChunk] = await Promise.all([
+ readExactChunk(sourceHandle, length, position),
+ readExactChunk(packagedHandle, length, position),
+ ]);
+ if (!sourceChunk.equals(packagedChunk)) {
+ throw new Error(
+ "Normalized Codex content changed outside its code-signature envelope.",
+ );
+ }
+ }
+ } finally {
+ await Promise.all([sourceHandle.close(), packagedHandle.close()]);
+ }
+}
+
+export async function createCodexSignatureSourceDelta(
+ sourcePath: string,
+ packagedPath: string,
+): Promise {
+ const [sourceStatus, packagedStatus] = await Promise.all([
+ lstat(sourcePath),
+ lstat(packagedPath),
+ ]);
+ for (const [label, status] of [["source", sourceStatus], ["packaged", packagedStatus]] as const) {
+ if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1) {
+ throw new Error(`Codex signature delta ${label} must be a regular single-link file.`);
+ }
+ }
+ if (
+ sourceStatus.size <= 0
+ || sourceStatus.size > maximumSourceBytes
+ || packagedStatus.size <= 0
+ || packagedStatus.size > maximumSourceBytes
+ ) {
+ throw new Error("Codex signature delta input size is invalid.");
+ }
+
+ const source = await open(sourcePath, "r");
+ const packaged = await open(packagedPath, "r");
+ const segments: Array> = [];
+ try {
+ const chunkBytes = 1024 * 1024;
+ for (let position = 0; position < sourceStatus.size; position += chunkBytes) {
+ const sourceLength = Math.min(chunkBytes, sourceStatus.size - position);
+ const packagedLength = Math.max(
+ 0,
+ Math.min(sourceLength, packagedStatus.size - position),
+ );
+ const sourceChunk = await readExactChunk(source, sourceLength, position);
+ const packagedChunk = packagedLength === 0
+ ? Buffer.alloc(0)
+ : await readExactChunk(packaged, packagedLength, position);
+ if (sourceLength === packagedLength && sourceChunk.equals(packagedChunk)) continue;
+
+ let cursor = 0;
+ while (cursor < sourceLength) {
+ while (
+ cursor < sourceLength
+ && cursor < packagedLength
+ && sourceChunk[cursor] === packagedChunk[cursor]
+ ) cursor += 1;
+ if (cursor === sourceLength) break;
+ const start = cursor;
+ while (
+ cursor < sourceLength
+ && (
+ cursor >= packagedLength
+ || sourceChunk[cursor] !== packagedChunk[cursor]
+ )
+ ) cursor += 1;
+ segments.push({
+ bytes: Buffer.from(sourceChunk.subarray(start, cursor)),
+ offset: position + start,
+ });
+ if (segments.length > maximumDeltaSegments) {
+ throw new Error("Codex signature delta has too many changed segments.");
+ }
+ }
+ }
+ } finally {
+ await Promise.all([source.close(), packaged.close()]);
+ }
+
+ const header = Buffer.alloc(deltaHeaderBytes);
+ deltaMagic.copy(header, 0);
+ header.writeBigUInt64BE(BigInt(sourceStatus.size), 8);
+ header.writeBigUInt64BE(BigInt(packagedStatus.size), 16);
+ header.writeUInt32BE(segments.length, 24);
+ const parts: Buffer[] = [header];
+ for (const segment of segments) {
+ const segmentHeader = Buffer.alloc(deltaSegmentHeaderBytes);
+ segmentHeader.writeBigUInt64BE(BigInt(segment.offset), 0);
+ segmentHeader.writeBigUInt64BE(BigInt(segment.bytes.byteLength), 8);
+ parts.push(segmentHeader, segment.bytes);
+ }
+ const delta = Buffer.concat(parts);
+ if (delta.byteLength > maximumDeltaBytes) {
+ throw new Error("Codex signature delta is unexpectedly large.");
+ }
+ return delta;
+}
+
+function safeBigUInt(value: bigint, label: string): number {
+ const number = Number(value);
+ if (!Number.isSafeInteger(number) || number < 0) {
+ throw new Error(`Codex signature delta ${label} is invalid.`);
+ }
+ return number;
+}
+
+export async function reconstructCodexSignatureSource(
+ packagedPath: string,
+ deltaPath: string,
+ destinationPath: string,
+): Promise {
+ const [packagedStatus, deltaStatus] = await Promise.all([
+ lstat(packagedPath),
+ lstat(deltaPath),
+ ]);
+ if (
+ !packagedStatus.isFile()
+ || packagedStatus.isSymbolicLink()
+ || packagedStatus.nlink !== 1
+ || !deltaStatus.isFile()
+ || deltaStatus.isSymbolicLink()
+ || deltaStatus.nlink !== 1
+ || deltaStatus.size < deltaHeaderBytes
+ || deltaStatus.size > maximumDeltaBytes
+ ) {
+ throw new Error("Codex signature source reconstruction input is invalid.");
+ }
+ const delta = await readFile(deltaPath);
+ if (!delta.subarray(0, deltaMagic.byteLength).equals(deltaMagic)) {
+ throw new Error("Codex signature delta magic differs.");
+ }
+ const sourceSize = safeBigUInt(delta.readBigUInt64BE(8), "source size");
+ const packagedSize = safeBigUInt(delta.readBigUInt64BE(16), "packaged size");
+ const segmentCount = delta.readUInt32BE(24);
+ if (
+ sourceSize <= 0
+ || sourceSize > maximumSourceBytes
+ || packagedSize !== packagedStatus.size
+ || segmentCount > maximumDeltaSegments
+ ) {
+ throw new Error("Codex signature delta identity differs.");
+ }
+
+ let cursor = deltaHeaderBytes;
+ let priorEnd = 0;
+ const segments: Array> = [];
+ for (let index = 0; index < segmentCount; index += 1) {
+ if (cursor + deltaSegmentHeaderBytes > delta.byteLength) {
+ throw new Error("Codex signature delta segment header is truncated.");
+ }
+ const offset = safeBigUInt(delta.readBigUInt64BE(cursor), "segment offset");
+ const length = safeBigUInt(
+ delta.readBigUInt64BE(cursor + 8),
+ "segment length",
+ );
+ cursor += deltaSegmentHeaderBytes;
+ if (
+ length <= 0
+ || offset < priorEnd
+ || offset + length > sourceSize
+ || cursor + length > delta.byteLength
+ ) {
+ throw new Error("Codex signature delta segment is invalid.");
+ }
+ segments.push({
+ bytes: delta.subarray(cursor, cursor + length),
+ offset,
+ });
+ cursor += length;
+ priorEnd = offset + length;
+ }
+ if (cursor !== delta.byteLength) {
+ throw new Error("Codex signature delta has trailing bytes.");
+ }
+
+ await copyFile(packagedPath, destinationPath, constants.COPYFILE_EXCL);
+ const destination = await open(destinationPath, "r+");
+ try {
+ await destination.truncate(sourceSize);
+ for (const segment of segments) {
+ let written = 0;
+ while (written < segment.bytes.byteLength) {
+ const result = await destination.write(
+ segment.bytes,
+ written,
+ segment.bytes.byteLength - written,
+ segment.offset + written,
+ );
+ if (result.bytesWritten === 0) {
+ throw new Error("Codex signature source reconstruction stopped early.");
+ }
+ written += result.bytesWritten;
+ }
+ }
+ } catch (error) {
+ await destination.close();
+ await rm(destinationPath, { force: true });
+ throw error;
+ }
+ await destination.close();
+}
diff --git a/apps/desktop/runtime/macos-package-config.ts b/apps/desktop/runtime/macos-package-config.ts
index 1b9da0f..d989798 100644
--- a/apps/desktop/runtime/macos-package-config.ts
+++ b/apps/desktop/runtime/macos-package-config.ts
@@ -57,6 +57,7 @@ export const requiredLicenseFileNames = Object.freeze([
"CODEX-NATIVE-LICENSES.json",
"CODEX-NATIVE-LICENSES.txt",
"CODEX-NOTICE.txt",
+ "CODEX-SIGNATURE-NORMALIZATION.md",
"CODEX-package.json",
"CODEX-platform-package.json",
"DESKTOP-THIRD-PARTY-NOTICES.md",
diff --git a/apps/desktop/runtime/package-macos.ts b/apps/desktop/runtime/package-macos.ts
index 91cf66a..da9d0fe 100644
--- a/apps/desktop/runtime/package-macos.ts
+++ b/apps/desktop/runtime/package-macos.ts
@@ -21,6 +21,20 @@ import {
loadCodexNativeLicenseInventory,
verifyInstalledCodexNativePayloads,
} from "./codex-native-licenses";
+import {
+ CODEX_SIGNATURE_NORMALIZATION_ENTITLEMENTS_FILE,
+ codexSignatureNormalizationEntry,
+ codexSignatureNormalizationCodesignArguments,
+ codexSignatureNormalizationManifestEntries,
+ codexSignatureNormalizationPolicy,
+ codexSignatureNormalizationSigning,
+ createCodexSignatureSourceDelta,
+ parseCodexSignatureNormalizationEntitlements,
+ verifyCodexSignatureNormalizationContent,
+ verifyCodexSignatureNormalizationInventory,
+ verifyCodexSignatureNormalizationPackaged,
+ verifyCodexSignatureNormalizationSource,
+} from "./codex-signature-normalization";
import { verifyPackagedFrontend } from "./frontend-package-integrity";
import { loadGcmDependencyLicenseInventory } from "./gcm-dependency-licenses";
import {
@@ -48,8 +62,18 @@ type CommandResult = Readonly<{
type CodeSignature = Readonly<{
cdHash: string | null;
+ flags: readonly string[];
+ hashChoices: readonly string[];
+ hashType: string | null;
identifier: string | null;
+ infoPlistBound: boolean | null;
+ internalRequirementsCount: number | null;
+ pageSize: number | null;
+ runtimeVersion: string | null;
+ sealedResources: string | null;
+ signatureKind: string | null;
teamIdentifier: string | null;
+ timestamp: string | null;
}>;
type RuntimeTreeEntry = Readonly<{
@@ -234,6 +258,7 @@ async function stageLicenseFiles(options: Readonly<{
["CODEX-NATIVE-LICENSES.json", join(macosPackage.desktopRoot, "runtime/CODEX-NATIVE-LICENSES.json")],
["CODEX-NATIVE-LICENSES.txt", join(macosPackage.desktopRoot, "runtime/CODEX-NATIVE-LICENSES.txt")],
["CODEX-NOTICE.txt", join(macosPackage.desktopRoot, "runtime/CODEX-NOTICE.txt")],
+ ["CODEX-SIGNATURE-NORMALIZATION.md", join(macosPackage.desktopRoot, "runtime/CODEX-SIGNATURE-NORMALIZATION.md")],
["CODEX-package.json", join(options.codexPackageRoot, "package.json")],
["CODEX-platform-package.json", options.codexPlatformPackageJson],
["DESKTOP-THIRD-PARTY-NOTICES.md", join(macosPackage.desktopRoot, "runtime/THIRD_PARTY_NOTICES.md")],
@@ -356,13 +381,46 @@ async function codeSignature(path: string): Promise {
const value = (pattern: RegExp): string | null =>
pattern.exec(details)?.[1]?.trim() ?? null;
const rawTeam = value(/^TeamIdentifier=(.+)$/mu);
+ const rawFlags = value(/^CodeDirectory .* flags=0x[0-9a-fA-F]+\(([^)]*)\)/mu);
+ const rawHashChoices = value(/^Hash choices=(.+)$/mu);
+ const rawInfoPlist = value(/^Info\.plist=(.+)$/mu);
+ const rawRequirementsCount = value(/^Internal requirements count=([0-9]+) size=/mu);
+ const rawPageSize = value(/^Page size=([0-9]+)$/mu);
return {
cdHash: value(/^CDHash=([0-9a-fA-F]+)$/mu)?.toLowerCase() ?? null,
+ flags: rawFlags === null || rawFlags.length === 0 ? [] : rawFlags.split(","),
+ hashChoices: rawHashChoices === null || rawHashChoices.length === 0
+ ? []
+ : rawHashChoices.split(","),
+ hashType: value(/^Hash type=([^ ]+) size=/mu),
identifier: value(/^Identifier=(.+)$/mu),
+ infoPlistBound: rawInfoPlist === null ? null : rawInfoPlist !== "not bound",
+ internalRequirementsCount:
+ rawRequirementsCount === null ? null : Number(rawRequirementsCount),
+ pageSize: rawPageSize === null ? null : Number(rawPageSize),
+ runtimeVersion: value(/^Runtime Version=(.+)$/mu),
+ sealedResources: value(/^Sealed Resources=(.+)$/mu),
+ signatureKind: value(/^Signature=(.+)$/mu),
teamIdentifier: rawTeam === "not set" ? null : rawTeam,
+ timestamp: value(/^Timestamp=(.+)$/mu),
};
}
+async function codeSignatureEntitlements(
+ path: string,
+): Promise>> {
+ const result = await run([
+ "/usr/bin/codesign",
+ "--display",
+ "--entitlements",
+ ":-",
+ path,
+ ]);
+ return parseCodexSignatureNormalizationEntitlements(
+ `${result.stdout}\n${result.stderr}`,
+ );
+}
+
async function signAdHoc(
path: string,
options: Readonly<{ entitlements?: string; identifier?: string }> = {},
@@ -384,6 +442,92 @@ async function signAdHoc(
]);
}
+async function normalizeCodexSignatures(
+ inventory: CodexNativeLicenseInventory,
+ sourceVendorRoot: string,
+): Promise> {
+ verifyCodexSignatureNormalizationInventory(inventory);
+ const entitlementsPath = join(
+ import.meta.dir,
+ CODEX_SIGNATURE_NORMALIZATION_ENTITLEMENTS_FILE,
+ );
+ const entitlementsStatus = await lstat(entitlementsPath);
+ if (
+ !entitlementsStatus.isFile()
+ || entitlementsStatus.isSymbolicLink()
+ || entitlementsStatus.nlink !== 1
+ || await sha256(entitlementsPath)
+ !== codexSignatureNormalizationSigning.entitlementsSha256
+ ) {
+ throw new Error("Codex signature normalization entitlements differ from policy.");
+ }
+ for (const entry of codexSignatureNormalizationPolicy.entries) {
+ const path = join(runtimeRoot, "codex", entry.payloadPath);
+ const status = await lstat(path);
+ if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1) {
+ throw new Error(
+ `Codex normalization source must be a regular single-link file: ${entry.payloadPath}`,
+ );
+ }
+ verifyCodexSignatureNormalizationSource(entry, {
+ sha256: await sha256(path),
+ signature: await codeSignature(path),
+ size: status.size,
+ });
+ const sourceStrict = await run([
+ "/usr/bin/codesign",
+ "--verify",
+ "--strict",
+ "--verbose=6",
+ path,
+ ], { allowFailure: true });
+ process.stdout.write(
+ `Codex source signature ${entry.payloadPath}: strict ${sourceStrict.exitCode === 0 ? "accepted" : "rejected"}; applying reviewed deterministic normalization.\n`,
+ );
+ await run(codexSignatureNormalizationCodesignArguments(
+ entry,
+ entitlementsPath,
+ path,
+ ));
+ const packagedStatus = await lstat(path);
+ verifyCodexSignatureNormalizationPackaged(entry, {
+ sha256: await sha256(path),
+ signature: {
+ ...await codeSignature(path),
+ entitlements: await codeSignatureEntitlements(path),
+ },
+ size: packagedStatus.size,
+ });
+ const sourcePath = join(sourceVendorRoot, entry.payloadPath);
+ await verifyCodexSignatureNormalizationContent(sourcePath, path);
+ await run([
+ "/usr/bin/codesign",
+ "--verify",
+ "--strict",
+ "--verbose=6",
+ path,
+ ]);
+ const delta = await createCodexSignatureSourceDelta(
+ sourcePath,
+ path,
+ );
+ const deltaSha256 = createHash("sha256").update(delta).digest("hex");
+ if (
+ delta.byteLength !== entry.sourceDelta.size
+ || deltaSha256 !== entry.sourceDelta.sha256
+ ) {
+ throw new Error(`Codex signature source delta differs: ${entry.payloadPath}`);
+ }
+ const deltaPath = resolve(appRoot, entry.sourceDelta.path);
+ if (!inside(appRoot, deltaPath)) {
+ throw new Error(`Codex signature source delta escaped the app: ${entry.payloadPath}`);
+ }
+ await mkdir(dirname(deltaPath), { recursive: true, mode: 0o755 });
+ await writeFile(deltaPath, delta, { flag: "wx", mode: 0o644 });
+ }
+ return codexSignatureNormalizationManifestEntries();
+}
+
async function signRuntimeTree(
preserveExactSignedPaths: ReadonlySet,
): Promise {
codexNativeInventory.platformPackage.payloads.map((payload) =>
join(runtimeRoot, "codex", payload.path)),
);
+ const normalizedSignatures = await normalizeCodexSignatures(
+ codexNativeInventory,
+ pins.codexVendorRoot,
+ );
const preservedSignatures = await signRuntimeTree(exactCodexPayloadPaths);
const dataRemoverSignature = await codeSignature(join(binRoot, "oprte-data-remover"));
if (!/^[0-9a-f]{40,64}$/u.test(dataRemoverSignature.cdHash ?? "")) {
@@ -558,6 +706,8 @@ async function main(): Promise {
runtimeVersions.codex.dependencyLicenseInventorySha256,
dependencyLicenseNoticesSha256:
runtimeVersions.codex.dependencyLicenseNoticesSha256,
+ sourceBinarySha256:
+ codexSignatureNormalizationEntry("bin/codex").source.sha256,
sourceCommit: runtimeVersions.codex.sourceCommit,
version: runtimeVersions.codex.version,
},
@@ -618,6 +768,7 @@ async function main(): Promise {
sourceCommit: runtimeVersions.gitLfs.sourceCommit,
version: runtimeVersions.gitLfs.version,
},
+ normalizedSignatures,
preservedSignatures,
ripgrep: {
binarySha256: await sha256(join(runtimeRoot, "codex/codex-path/rg")),
diff --git a/apps/desktop/runtime/test/codex-signature-normalization.macos.test.ts b/apps/desktop/runtime/test/codex-signature-normalization.macos.test.ts
new file mode 100644
index 0000000..c1ef96b
--- /dev/null
+++ b/apps/desktop/runtime/test/codex-signature-normalization.macos.test.ts
@@ -0,0 +1,373 @@
+import { describe, expect, test } from "bun:test";
+import { createHash } from "node:crypto";
+import {
+ copyFile,
+ lstat,
+ mkdtemp,
+ rm,
+ writeFile,
+} from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { basename, join } from "node:path";
+
+import {
+ CODEX_SIGNATURE_NORMALIZATION_ENTITLEMENTS_FILE,
+ codexSignatureNormalizationCodesignArguments,
+ codexSignatureNormalizationPolicy,
+ codexSignatureNormalizationSigning,
+ createCodexSignatureSourceDelta,
+ parseCodexSignatureNormalizationEntitlements,
+ reconstructCodexSignatureSource,
+ verifyCodexSignatureNormalizationContent,
+ verifyCodexSignatureNormalizationPackaged,
+} from "../codex-signature-normalization";
+import { sha256File } from "../verify-macos-package";
+import { verifyRuntimePins } from "../verify-runtime-pins";
+
+type CommandResult = Readonly<{
+ stderr: string;
+ stdout: string;
+}>;
+
+async function run(argv: readonly string[]): Promise {
+ const child = Bun.spawn([...argv], {
+ stderr: "pipe",
+ stdout: "pipe",
+ });
+ const [stdout, stderr, exitCode] = await Promise.all([
+ new Response(child.stdout).text(),
+ new Response(child.stderr).text(),
+ child.exited,
+ ]);
+ if (exitCode !== 0) {
+ throw new Error(`${argv.join(" ")} failed with exit code ${exitCode}: ${stderr.trim()}`);
+ }
+ return { stderr, stdout };
+}
+
+async function packagedIdentity(path: string) {
+ const [status, sha256, signatureResult, entitlementResult] = await Promise.all([
+ lstat(path),
+ sha256File(path),
+ run(["/usr/bin/codesign", "--display", "--verbose=4", path]),
+ run(["/usr/bin/codesign", "--display", "--entitlements", ":-", path]),
+ ]);
+ const details = `${signatureResult.stdout}\n${signatureResult.stderr}`;
+ const value = (pattern: RegExp): string | null =>
+ pattern.exec(details)?.[1]?.trim() ?? null;
+ const rawFlags = value(/^CodeDirectory .* flags=0x[0-9a-fA-F]+\(([^)]*)\)/mu);
+ const rawHashChoices = value(/^Hash choices=(.+)$/mu);
+ const rawInfoPlist = value(/^Info\.plist=(.+)$/mu);
+ const rawRequirementsCount = value(/^Internal requirements count=([0-9]+) size=/mu);
+ const rawPageSize = value(/^Page size=([0-9]+)$/mu);
+ const rawTeam = value(/^TeamIdentifier=(.+)$/mu);
+ return {
+ sha256,
+ signature: {
+ cdHash: value(/^CDHash=([0-9a-fA-F]+)$/mu)?.toLowerCase() ?? null,
+ entitlements: parseCodexSignatureNormalizationEntitlements(
+ `${entitlementResult.stdout}\n${entitlementResult.stderr}`,
+ ),
+ flags: rawFlags === null || rawFlags.length === 0 ? [] : rawFlags.split(","),
+ hashChoices: rawHashChoices === null || rawHashChoices.length === 0
+ ? []
+ : rawHashChoices.split(","),
+ hashType: value(/^Hash type=([^ ]+) size=/mu),
+ identifier: value(/^Identifier=(.+)$/mu),
+ infoPlistBound: rawInfoPlist === null ? null : rawInfoPlist !== "not bound",
+ internalRequirementsCount:
+ rawRequirementsCount === null ? null : Number(rawRequirementsCount),
+ pageSize: rawPageSize === null ? null : Number(rawPageSize),
+ runtimeVersion: value(/^Runtime Version=(.+)$/mu),
+ sealedResources: value(/^Sealed Resources=(.+)$/mu),
+ signatureKind: value(/^Signature=(.+)$/mu),
+ teamIdentifier: rawTeam === "not set" ? null : rawTeam,
+ timestamp: value(/^Timestamp=(.+)$/mu),
+ },
+ size: status.size,
+ } as const;
+}
+
+function withPageSize(argv: readonly string[], pageSize: number): readonly string[] {
+ const result = [...argv];
+ const optionIndex = result.indexOf("--pagesize");
+ if (optionIndex < 0 || result[optionIndex + 1] === undefined) {
+ throw new Error("Codex signing arguments omit --pagesize.");
+ }
+ result[optionIndex + 1] = String(pageSize);
+ return result;
+}
+
+async function deadline(promise: Promise, label: string): Promise {
+ return await Promise.race([
+ promise,
+ Bun.sleep(10_000).then(() => {
+ throw new Error(`Timed out waiting for ${label}.`);
+ }),
+ ]);
+}
+
+class FrameReader {
+ readonly #reader: ReadableStreamDefaultReader;
+ #buffer = Buffer.alloc(0);
+
+ constructor(stream: ReadableStream) {
+ this.#reader = stream.getReader();
+ }
+
+ async #readExact(length: number): Promise {
+ while (this.#buffer.byteLength < length) {
+ const chunk = await this.#reader.read();
+ if (chunk.done) throw new Error("Code-mode host closed its framed output early.");
+ this.#buffer = Buffer.concat([this.#buffer, Buffer.from(chunk.value)]);
+ }
+ const result = this.#buffer.subarray(0, length);
+ this.#buffer = this.#buffer.subarray(length);
+ return result;
+ }
+
+ async read(): Promise {
+ const length = (await this.#readExact(4)).readUInt32LE(0);
+ if (length === 0 || length > 64 * 1024 * 1024) {
+ throw new Error(`Code-mode host returned invalid frame length ${length}.`);
+ }
+ return JSON.parse((await this.#readExact(length)).toString("utf8")) as unknown;
+ }
+}
+
+async function writeFrame(
+ stdin: Bun.FileSink,
+ value: unknown,
+): Promise {
+ const payload = Buffer.from(JSON.stringify(value), "utf8");
+ const frame = Buffer.allocUnsafe(4 + payload.byteLength);
+ frame.writeUInt32LE(payload.byteLength, 0);
+ payload.copy(frame, 4);
+ await stdin.write(frame);
+ await stdin.flush();
+}
+
+function object(value: unknown, label: string): Record {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ throw new Error(`${label} must be an object.`);
+ }
+ return value as Record;
+}
+
+async function readUntil(
+ reader: FrameReader,
+ predicate: (message: Record) => boolean,
+ label: string,
+): Promise> {
+ for (let index = 0; index < 8; index += 1) {
+ const message = object(
+ await deadline(reader.read(), label),
+ `code-mode ${label}`,
+ );
+ if (predicate(message)) return message;
+ }
+ throw new Error(`Code-mode host did not return ${label}.`);
+}
+
+async function verifyCodeModeJit(path: string): Promise {
+ const child = Bun.spawn([path], {
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ });
+ const stderr = new Response(child.stderr).text();
+ const reader = new FrameReader(child.stdout);
+ try {
+ await writeFrame(child.stdin, {
+ optionalCapabilities: [],
+ requiredCapabilities: [],
+ supportedVersions: [1],
+ type: "connection/hello",
+ });
+ expect(await deadline(reader.read(), "code-mode handshake")).toEqual({
+ capabilities: [],
+ selectedVersion: 1,
+ type: "connection/ready",
+ });
+
+ await writeFrame(child.stdin, {
+ id: 1,
+ request: { method: "session/open", sessionId: "hra-jit-smoke" },
+ type: "operation/request",
+ });
+ expect(await deadline(reader.read(), "code-mode session open")).toEqual({
+ id: 1,
+ result: {
+ status: "ok",
+ value: { sessionId: "hra-jit-smoke", type: "session/ready" },
+ },
+ type: "operation/response",
+ });
+
+ await writeFrame(child.stdin, {
+ id: 2,
+ request: {
+ method: "session/execute",
+ request: {
+ enabled_tools: [],
+ max_output_tokens: 16,
+ source: `
+function hot(value) {
+ return ((value * 17) ^ (value >>> 3)) & 0xffff;
+}
+let total = 0;
+for (let index = 0; index < 1_000_000; index += 1) {
+ total = (total + hot(index)) >>> 0;
+}
+text(String(total));
+`,
+ tool_call_id: "hra-jit-smoke",
+ yield_time_ms: 60_000,
+ },
+ sessionId: "hra-jit-smoke",
+ },
+ type: "operation/request",
+ });
+ const started = await readUntil(
+ reader,
+ (message) => message.type === "operation/response" && message.id === 2,
+ "code-mode execution start",
+ );
+ expect(started).toEqual({
+ id: 2,
+ result: {
+ status: "ok",
+ value: { cellId: "1", type: "execution/started" },
+ },
+ type: "operation/response",
+ });
+ const initial = await readUntil(
+ reader,
+ (message) => message.type === "execute/initialResponse" && message.id === 2,
+ "code-mode JIT result",
+ );
+ const result = object(initial.result, "code-mode JIT result envelope");
+ expect(result.status).toBe("ok");
+ const value = object(result.value, "code-mode JIT result value");
+ const terminal = object(value.Result, "code-mode JIT terminal result");
+ expect(terminal).toMatchObject({
+ cell_id: "1",
+ error_text: null,
+ });
+ expect(terminal.content_items).toEqual([
+ { text: "2732512480", type: "input_text" },
+ ]);
+
+ await writeFrame(child.stdin, {
+ id: 3,
+ request: { method: "session/shutdown", sessionId: "hra-jit-smoke" },
+ type: "operation/request",
+ });
+ const closed = await readUntil(
+ reader,
+ (message) => message.type === "operation/response" && message.id === 3,
+ "code-mode session shutdown",
+ );
+ expect(closed).toEqual({
+ id: 3,
+ result: {
+ status: "ok",
+ value: { sessionId: "hra-jit-smoke", type: "session/closed" },
+ },
+ type: "operation/response",
+ });
+ await child.stdin.end();
+ expect(await deadline(child.exited, "code-mode host exit")).toBe(0);
+ expect(await stderr).toBe("");
+ } catch (error) {
+ child.kill("SIGKILL");
+ await child.stdin.end();
+ await child.exited;
+ const details = (await stderr).trim();
+ throw new Error(
+ `${error instanceof Error ? error.message : String(error)}${details.length === 0 ? "" : `: ${details}`}`,
+ );
+ }
+}
+
+describe("Codex signature normalization on macOS", () => {
+ test("reproduces pinned identities and executes the entitled V8 JIT", async () => {
+ expect(process.platform).toBe("darwin");
+ expect(process.arch).toBe("arm64");
+ const pins = await verifyRuntimePins();
+ const entitlementsPath = join(
+ import.meta.dir,
+ `../${CODEX_SIGNATURE_NORMALIZATION_ENTITLEMENTS_FILE}`,
+ );
+ expect(await sha256File(entitlementsPath))
+ .toBe(codexSignatureNormalizationSigning.entitlementsSha256);
+ const root = await mkdtemp(join(tmpdir(), "hra-codex-signature-contract-"));
+ try {
+ for (const entry of codexSignatureNormalizationPolicy.entries) {
+ const name = basename(entry.payloadPath);
+ const sourcePath = join(pins.codexVendorRoot, entry.payloadPath);
+ const legacyPath = join(root, `${name}-4096`);
+ const pinnedPath = join(root, `${name}-16384`);
+ const deltaPath = join(root, `${name}.source-delta`);
+ const reconstructedPath = join(root, `${name}-reconstructed`);
+ await Promise.all([
+ copyFile(sourcePath, legacyPath),
+ copyFile(sourcePath, pinnedPath),
+ ]);
+ const pinnedArguments = codexSignatureNormalizationCodesignArguments(
+ entry,
+ entitlementsPath,
+ pinnedPath,
+ );
+ await Promise.all([
+ run(withPageSize(
+ codexSignatureNormalizationCodesignArguments(
+ entry,
+ entitlementsPath,
+ legacyPath,
+ ),
+ 4_096,
+ )),
+ run(pinnedArguments),
+ ]);
+
+ const [legacyIdentity, pinnedIdentity] = await Promise.all([
+ packagedIdentity(legacyPath),
+ packagedIdentity(pinnedPath),
+ ]);
+ expect(() => verifyCodexSignatureNormalizationPackaged(entry, legacyIdentity))
+ .toThrow("package identity differs");
+ expect(() => verifyCodexSignatureNormalizationPackaged(entry, pinnedIdentity))
+ .not.toThrow();
+ expect(pinnedIdentity).toMatchObject({
+ sha256: entry.packaged.sha256,
+ signature: {
+ cdHash: entry.packaged.cdHash,
+ pageSize: 16_384,
+ runtimeVersion: "15.5.0",
+ },
+ size: entry.packaged.size,
+ });
+ await Promise.all([
+ verifyCodexSignatureNormalizationContent(sourcePath, legacyPath),
+ verifyCodexSignatureNormalizationContent(sourcePath, pinnedPath),
+ run(["/usr/bin/codesign", "--verify", "--strict", pinnedPath]),
+ ]);
+
+ const delta = await createCodexSignatureSourceDelta(sourcePath, pinnedPath);
+ expect(delta.byteLength).toBe(entry.sourceDelta.size);
+ expect(createHash("sha256").update(delta).digest("hex"))
+ .toBe(entry.sourceDelta.sha256);
+ await writeFile(deltaPath, delta, { flag: "wx", mode: 0o600 });
+ await reconstructCodexSignatureSource(pinnedPath, deltaPath, reconstructedPath);
+ expect(await sha256File(reconstructedPath)).toBe(entry.source.sha256);
+
+ if (entry.payloadPath === "bin/codex-code-mode-host") {
+ await verifyCodeModeJit(pinnedPath);
+ }
+ }
+ } finally {
+ await rm(root, { force: true, recursive: true });
+ }
+ }, 180_000);
+});
diff --git a/apps/desktop/runtime/test/codex-signature-normalization.test.ts b/apps/desktop/runtime/test/codex-signature-normalization.test.ts
new file mode 100644
index 0000000..a4eb456
--- /dev/null
+++ b/apps/desktop/runtime/test/codex-signature-normalization.test.ts
@@ -0,0 +1,369 @@
+import { afterEach, describe, expect, test } from "bun:test";
+import { createHash } from "node:crypto";
+import { link, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+import { loadCodexNativeLicenseInventory } from "../codex-native-licenses";
+import {
+ CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE,
+ CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION,
+ codexSignatureNormalizationEntry,
+ codexSignatureNormalizationCodesignArguments,
+ codexSignatureNormalizationEntitlements,
+ codexSignatureNormalizationManifestEntries,
+ codexSignatureNormalizationPolicy,
+ codexSignatureNormalizationSigning,
+ createCodexSignatureSourceDelta,
+ parseCodexSignatureNormalizationEntitlements,
+ reconstructCodexSignatureSource,
+ verifyCodexSignatureNormalizationContent,
+ verifyCodexSignatureNormalizationInventory,
+ verifyCodexSignatureNormalizationPackaged,
+ verifyCodexSignatureNormalizationSource,
+} from "../codex-signature-normalization";
+
+const temporaryRoots: string[] = [];
+
+function machOSignatureFixture(signatureBytes: number): Buffer {
+ const signatureOffset = 256;
+ const linkeditOffset = 192;
+ const bytes = Buffer.alloc(signatureOffset + signatureBytes);
+ bytes.writeUInt32LE(0xfeedfacf, 0);
+ bytes.writeUInt32LE(0x0100000c, 4);
+ bytes.writeUInt32LE(2, 16);
+ bytes.writeUInt32LE(88, 20);
+ bytes.writeUInt32LE(0x19, 32);
+ bytes.writeUInt32LE(72, 36);
+ bytes.write("__LINKEDIT", 40, "ascii");
+ bytes.writeBigUInt64LE(BigInt(CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE), 64);
+ bytes.writeBigUInt64LE(BigInt(linkeditOffset), 72);
+ bytes.writeBigUInt64LE(BigInt(bytes.byteLength - linkeditOffset), 80);
+ bytes.writeUInt32LE(0x1d, 104);
+ bytes.writeUInt32LE(16, 108);
+ bytes.writeUInt32LE(signatureOffset, 112);
+ bytes.writeUInt32LE(signatureBytes, 116);
+ for (let index = 120; index < signatureOffset; index += 1) {
+ bytes[index] = (index * 13 + 17) % 251;
+ }
+ bytes.fill(signatureBytes % 251, signatureOffset);
+ return bytes;
+}
+
+function packagedSignature(
+ entry: ReturnType,
+) {
+ return {
+ ...entry.packaged,
+ entitlements: codexSignatureNormalizationEntitlements,
+ flags: ["runtime", "adhoc"],
+ hashChoices: ["sha256"],
+ hashType: "sha256",
+ infoPlistBound: false,
+ internalRequirementsCount: 0,
+ sealedResources: "none",
+ signatureKind: "adhoc",
+ timestamp: null,
+ } as const;
+}
+
+async function temporaryRoot(): Promise {
+ const root = await mkdtemp(join(tmpdir(), "hra-codex-signature-normalization-"));
+ temporaryRoots.push(root);
+ return root;
+}
+
+async function expectFailure(
+ action: () => Promise,
+ message: string,
+): Promise {
+ try {
+ await action();
+ throw new Error(`Expected failure containing: ${message}`);
+ } catch (error) {
+ expect(error).toBeInstanceOf(Error);
+ expect((error as Error).message).toContain(message);
+ }
+}
+
+afterEach(async () => {
+ await Promise.all(
+ temporaryRoots.splice(0).map((root) => rm(root, { force: true, recursive: true })),
+ );
+});
+
+describe("Codex signature normalization", () => {
+ test("binds the exception to the exact official package and source payloads", async () => {
+ const inventory = await loadCodexNativeLicenseInventory();
+ expect(() => verifyCodexSignatureNormalizationInventory(inventory)).not.toThrow();
+ expect(codexSignatureNormalizationPolicy.entries.map((entry) => entry.payloadPath)).toEqual([
+ "bin/codex",
+ "bin/codex-code-mode-host",
+ ]);
+ expect(codexSignatureNormalizationPolicy.entries.map((entry) => entry.source.sha256))
+ .toEqual([
+ "80a3933d11a9d13ef806aa24f7bb8afc9169cfe4e9b09d6da6a92922cbde9cff",
+ "de329ec247b5ebbdf796b5888a7c2a9d731e221321584c5abdcc686c70b2db81",
+ ]);
+ expect(codexSignatureNormalizationPolicy.entries.map((entry) => entry.packaged.sha256))
+ .toEqual([
+ "055f18d2a33a719a2fab08e0a8326d950fa733340c596bb3df0d8dc94f85a96e",
+ "7f622f21007acac2780b0e9e39822ba493425366fc1cf996c24adafc9c0a6e08",
+ ]);
+ expect(codexSignatureNormalizationPolicy.entries.every(
+ (entry) => entry.source.teamIdentifier === "2DC432GLL2",
+ )).toBe(true);
+ expect(codexSignatureNormalizationPolicy.entries.every(
+ (entry) => entry.packaged.teamIdentifier === null,
+ )).toBe(true);
+ });
+
+ test("records exact source, packaged, and reversible-delta evidence", () => {
+ expect(codexSignatureNormalizationManifestEntries()).toEqual(
+ codexSignatureNormalizationPolicy.entries.map((entry) => ({
+ normalization: "adhoc-runtime-v1",
+ packaged: entry.packaged,
+ path: entry.appRelativePath,
+ signing: codexSignatureNormalizationSigning,
+ source: entry.source,
+ sourceDelta: entry.sourceDelta,
+ })),
+ );
+ expect(codexSignatureNormalizationPolicy.entries.map((entry) => [
+ entry.sourceDelta.sha256,
+ entry.sourceDelta.size,
+ ])).toEqual([
+ ["b0b05a7e03adf00fc1293b3e2679464cd8ec63024ca0ab5448915b5c33a1dadd", 2_046_810],
+ ["5952f9bc32083e1f62e1cc13c55b5b50145f8f7e4df56dd89c2d8d5267d9c2c2", 363_584],
+ ]);
+ });
+
+ test("pins every cross-host codesign input in the signing command", async () => {
+ const entry = codexSignatureNormalizationEntry("bin/codex");
+ const entitlementsPath = join(
+ import.meta.dir,
+ "../codex-signature-normalization.entitlements.plist",
+ );
+ expect(CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE).toBe(16_384);
+ expect(CODEX_SIGNATURE_NORMALIZATION_RUNTIME_VERSION).toBe("15.5.0");
+ expect(entry.packaged.pageSize).toBe(CODEX_SIGNATURE_NORMALIZATION_PAGE_SIZE);
+ expect(createHash("sha256").update(await readFile(entitlementsPath)).digest("hex"))
+ .toBe(codexSignatureNormalizationSigning.entitlementsSha256);
+ expect(codexSignatureNormalizationCodesignArguments(
+ entry,
+ entitlementsPath,
+ "/tmp/codex",
+ )).toEqual([
+ "/usr/bin/codesign",
+ "--force",
+ "--sign",
+ "-",
+ "--options",
+ "runtime",
+ "--entitlements",
+ entitlementsPath,
+ "--generate-entitlement-der",
+ "--timestamp=none",
+ "--digest-algorithm=sha256",
+ "--runtime-version",
+ "15.5.0",
+ "--pagesize",
+ "16384",
+ "--identifier",
+ "codex",
+ "/tmp/codex",
+ ]);
+ });
+
+ test("parses only the two canonical true JIT entitlements", () => {
+ const xml = `
+ com.apple.security.cs.allow-jit
+ com.apple.security.cs.allow-unsigned-executable-memory
+ `;
+ expect(parseCodexSignatureNormalizationEntitlements(xml)).toEqual(
+ codexSignatureNormalizationEntitlements,
+ );
+ expect(() => parseCodexSignatureNormalizationEntitlements(
+ xml.replace("", "unexpected"),
+ )).not.toThrow();
+ expect(() => verifyCodexSignatureNormalizationPackaged(
+ codexSignatureNormalizationEntry("bin/codex"),
+ {
+ sha256: codexSignatureNormalizationEntry("bin/codex").packaged.sha256,
+ signature: {
+ ...packagedSignature(codexSignatureNormalizationEntry("bin/codex")),
+ entitlements: parseCodexSignatureNormalizationEntitlements(
+ xml.replace("", "unexpected"),
+ ),
+ },
+ size: codexSignatureNormalizationEntry("bin/codex").packaged.size,
+ },
+ )).toThrow("package identity differs");
+ });
+
+ test("rejects source and packaged signature identity drift", () => {
+ const entry = codexSignatureNormalizationEntry("bin/codex");
+ expect(() => verifyCodexSignatureNormalizationSource(entry, {
+ sha256: entry.source.sha256,
+ signature: entry.source,
+ size: entry.source.size,
+ })).not.toThrow();
+ expect(() => verifyCodexSignatureNormalizationSource(entry, {
+ sha256: entry.source.sha256,
+ signature: { ...entry.source, teamIdentifier: "unexpected" },
+ size: entry.source.size,
+ })).toThrow("source identity differs");
+
+ expect(() => verifyCodexSignatureNormalizationPackaged(entry, {
+ sha256: entry.packaged.sha256,
+ signature: packagedSignature(entry),
+ size: entry.packaged.size,
+ })).not.toThrow();
+ expect(() => verifyCodexSignatureNormalizationPackaged(entry, {
+ sha256: "0".repeat(64),
+ signature: packagedSignature(entry),
+ size: entry.packaged.size,
+ })).toThrow("package identity differs");
+ const signatureContract = packagedSignature(entry);
+ for (const signature of [
+ { ...signatureContract, cdHash: "0".repeat(40) },
+ { ...signatureContract, identifier: "unreviewed" },
+ { ...signatureContract, teamIdentifier: "2DC432GLL2" },
+ { ...signatureContract, flags: ["adhoc"] },
+ { ...signatureContract, hashChoices: ["sha1", "sha256"] },
+ { ...signatureContract, hashType: "sha1" },
+ { ...signatureContract, infoPlistBound: true },
+ { ...signatureContract, internalRequirementsCount: 1 },
+ { ...signatureContract, pageSize: 4_096 },
+ { ...signatureContract, runtimeVersion: "26.0.0" },
+ { ...signatureContract, sealedResources: "yes" },
+ { ...signatureContract, signatureKind: null },
+ { ...signatureContract, timestamp: "Aug 20, 2026" },
+ {
+ ...signatureContract,
+ entitlements: { "com.apple.security.cs.allow-jit": true },
+ },
+ ]) {
+ expect(() => verifyCodexSignatureNormalizationPackaged(entry, {
+ sha256: entry.packaged.sha256,
+ signature,
+ size: entry.packaged.size,
+ })).toThrow("package identity differs");
+ }
+ expect(() => codexSignatureNormalizationEntry("../bin/codex"))
+ .toThrow("policy is absent");
+ });
+
+ test("allows changes only inside the Mach-O signature envelope", async () => {
+ const root = await temporaryRoot();
+ const sourcePath = join(root, "source-macho");
+ const packagedPath = join(root, "packaged-macho");
+ const source = machOSignatureFixture(64);
+ const packaged = machOSignatureFixture(32);
+ await Promise.all([
+ writeFile(sourcePath, source, { mode: 0o755 }),
+ writeFile(packagedPath, packaged, { mode: 0o755 }),
+ ]);
+ expect(await verifyCodexSignatureNormalizationContent(sourcePath, packagedPath))
+ .toBeUndefined();
+ packaged[200] = packaged[200]! ^ 0xff;
+ await writeFile(packagedPath, packaged, { mode: 0o755 });
+ await expectFailure(
+ () => verifyCodexSignatureNormalizationContent(sourcePath, packagedPath),
+ "changed outside its code-signature envelope",
+ );
+ });
+
+ test("creates a deterministic bounded delta that restores exact source bytes", async () => {
+ const root = await temporaryRoot();
+ const sourcePath = join(root, "source");
+ const packagedPath = join(root, "packaged");
+ const deltaPath = join(root, "source.delta");
+ const reconstructedPath = join(root, "reconstructed");
+ const source = Buffer.alloc(32_000);
+ for (let index = 0; index < source.byteLength; index += 1) {
+ source[index] = (index * 17 + 29) % 251;
+ }
+ const packaged = Buffer.from(source.subarray(0, 30_000));
+ packaged[17] = packaged[17]! ^ 0xff;
+ packaged[2_049] = packaged[2_049]! ^ 0xff;
+ packaged.fill(7, 27_000, 27_100);
+ await Promise.all([
+ writeFile(sourcePath, source, { mode: 0o755 }),
+ writeFile(packagedPath, packaged, { mode: 0o755 }),
+ ]);
+
+ const first = await createCodexSignatureSourceDelta(sourcePath, packagedPath);
+ const second = await createCodexSignatureSourceDelta(sourcePath, packagedPath);
+ expect(first).toEqual(second);
+ await writeFile(deltaPath, first, { flag: "wx", mode: 0o600 });
+ await reconstructCodexSignatureSource(
+ packagedPath,
+ deltaPath,
+ reconstructedPath,
+ );
+ expect(await readFile(reconstructedPath)).toEqual(source);
+ });
+
+ test("rejects malformed source deltas before reconstruction", async () => {
+ const root = await temporaryRoot();
+ const sourcePath = join(root, "source");
+ const packagedPath = join(root, "packaged");
+ const deltaPath = join(root, "source.delta");
+ const destinationPath = join(root, "reconstructed");
+ await Promise.all([
+ writeFile(sourcePath, "source bytes", { mode: 0o755 }),
+ writeFile(packagedPath, "packaged", { mode: 0o755 }),
+ ]);
+ const delta = await createCodexSignatureSourceDelta(sourcePath, packagedPath);
+ delta[0] = delta[0]! ^ 0xff;
+ await writeFile(deltaPath, delta, { mode: 0o600 });
+ await expectFailure(
+ () => reconstructCodexSignatureSource(packagedPath, deltaPath, destinationPath),
+ "delta magic differs",
+ );
+ });
+
+ test("rejects truncated, symlinked, and hard-linked delta custody", async () => {
+ const root = await temporaryRoot();
+ const sourcePath = join(root, "source");
+ const packagedPath = join(root, "packaged");
+ const deltaPath = join(root, "source.delta");
+ const truncatedPath = join(root, "truncated.delta");
+ const deltaLinkPath = join(root, "delta-link");
+ const packagedLinkPath = join(root, "packaged-hard-link");
+ await Promise.all([
+ writeFile(sourcePath, "source bytes extended", { mode: 0o755 }),
+ writeFile(packagedPath, "packaged", { mode: 0o755 }),
+ ]);
+ const delta = await createCodexSignatureSourceDelta(sourcePath, packagedPath);
+ await Promise.all([
+ writeFile(deltaPath, delta, { mode: 0o600 }),
+ writeFile(truncatedPath, delta.subarray(0, delta.byteLength - 1), { mode: 0o600 }),
+ ]);
+ await expectFailure(
+ () => reconstructCodexSignatureSource(
+ packagedPath,
+ truncatedPath,
+ join(root, "truncated-output"),
+ ),
+ "delta segment is invalid",
+ );
+
+ await symlink(deltaPath, deltaLinkPath);
+ await expectFailure(
+ () => reconstructCodexSignatureSource(
+ packagedPath,
+ deltaLinkPath,
+ join(root, "symlink-output"),
+ ),
+ "reconstruction input is invalid",
+ );
+
+ await link(packagedPath, packagedLinkPath);
+ await expectFailure(
+ () => createCodexSignatureSourceDelta(sourcePath, packagedPath),
+ "packaged must be a regular single-link file",
+ );
+ });
+});
diff --git a/apps/desktop/runtime/test/macos-package-config.test.ts b/apps/desktop/runtime/test/macos-package-config.test.ts
index bff2c82..a51fe5d 100644
--- a/apps/desktop/runtime/test/macos-package-config.test.ts
+++ b/apps/desktop/runtime/test/macos-package-config.test.ts
@@ -110,6 +110,7 @@ describe("macOS ad-hoc package contract", () => {
expect(requiredLicenseFileNames).toContain("BUN-DEPENDENCY-LICENSES.txt");
expect(requiredLicenseFileNames).toContain("CODEX-NATIVE-LICENSES.json");
expect(requiredLicenseFileNames).toContain("CODEX-NATIVE-LICENSES.txt");
+ expect(requiredLicenseFileNames).toContain("CODEX-SIGNATURE-NORMALIZATION.md");
expect(requiredLicenseFileNames).toContain("GIT-COPYING.txt");
expect(requiredLicenseFileNames).toContain("GIT-LFS-LICENSE.md");
expect(requiredLicenseFileNames).toContain("GIT-CREDENTIAL-MANAGER-LICENSE.txt");
diff --git a/apps/desktop/runtime/verify-codex-signature-tamper.ts b/apps/desktop/runtime/verify-codex-signature-tamper.ts
new file mode 100644
index 0000000..91a4d7e
--- /dev/null
+++ b/apps/desktop/runtime/verify-codex-signature-tamper.ts
@@ -0,0 +1,214 @@
+import { constants } from "node:fs";
+import {
+ copyFile,
+ lstat,
+ mkdtemp,
+ open,
+ readFile,
+ realpath,
+ rm,
+ writeFile,
+} from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
+
+import {
+ codexSignatureNormalizationEntry,
+ codexSignatureNormalizationPolicy,
+} from "./codex-signature-normalization";
+import { macosPackage } from "./macos-package-config";
+import { sha256File, verifyMacOSApp } from "./verify-macos-package";
+
+async function privateTemporaryRoot(): Promise {
+ const root = await realpath(
+ await mkdtemp(join(tmpdir(), "hra-codex-signature-tamper-")),
+ );
+ const status = await lstat(root);
+ if (
+ !status.isDirectory()
+ || status.isSymbolicLink()
+ || status.uid !== process.getuid?.()
+ || (status.mode & 0o777) !== 0o700
+ ) {
+ await rm(root, { force: true, recursive: true });
+ throw new Error("Codex signature tamper root is not an owner-private directory.");
+ }
+ return root;
+}
+
+async function expectAppRejection(
+ appPath: string,
+ label: string,
+ expectedMessage: string,
+): Promise {
+ try {
+ await verifyMacOSApp(appPath);
+ } catch (error) {
+ if (error instanceof Error && error.message.includes(expectedMessage)) {
+ process.stdout.write(`Codex signature tamper rejected: ${label}.\n`);
+ return;
+ }
+ throw error;
+ }
+ throw new Error(`Codex signature tamper was accepted: ${label}.`);
+}
+
+async function mutateLastByte(path: string): Promise {
+ const status = await lstat(path);
+ if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1 || status.size < 1) {
+ throw new Error(`Tamper target must be a nonempty regular single-link file: ${path}`);
+ }
+ const handle = await open(path, "r+");
+ try {
+ const byte = Buffer.alloc(1);
+ const read = await handle.read(byte, 0, 1, status.size - 1);
+ if (read.bytesRead !== 1) throw new Error(`Could not read tamper target: ${path}`);
+ byte[0] = byte[0]! ^ 0xff;
+ const written = await handle.write(byte, 0, 1, status.size - 1);
+ if (written.bytesWritten !== 1) throw new Error(`Could not write tamper target: ${path}`);
+ } finally {
+ await handle.close();
+ }
+}
+
+async function runCodesign(argv: readonly string[]): Promise {
+ const child = Bun.spawn([...argv], {
+ cwd: macosPackage.desktopRoot,
+ env: process.env,
+ stderr: "pipe",
+ stdout: "pipe",
+ });
+ const [, stderr, exitCode] = await Promise.all([
+ new Response(child.stdout).text(),
+ new Response(child.stderr).text(),
+ child.exited,
+ ]);
+ if (exitCode !== 0) {
+ throw new Error(`${argv.join(" ")} failed with exit code ${exitCode}: ${stderr.trim()}`);
+ }
+}
+
+async function withRestoredFile(
+ temporaryRoot: string,
+ target: string,
+ backupName: string,
+ action: () => Promise,
+): Promise {
+ const backup = join(temporaryRoot, backupName);
+ await copyFile(target, backup, constants.COPYFILE_EXCL);
+ const expectedSha256 = await sha256File(backup);
+ try {
+ await action();
+ } finally {
+ await copyFile(backup, target);
+ }
+ if (await sha256File(target) !== expectedSha256) {
+ throw new Error(`Tamper regression did not restore its target: ${target}`);
+ }
+}
+
+function runtimeManifest(value: unknown): Record {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ throw new Error("Runtime manifest tamper fixture must be an object.");
+ }
+ return value as Record;
+}
+
+async function main(): Promise {
+ if (process.platform !== "darwin" || process.arch !== "arm64") {
+ throw new Error("Codex signature tamper regression requires Apple Silicon macOS.");
+ }
+ const appPath = await realpath(macosPackage.appBundlePath);
+ if (appPath !== macosPackage.appBundlePath || !appPath.endsWith(".app")) {
+ throw new Error("Codex signature tamper regression requires the exact package app.");
+ }
+ await verifyMacOSApp(appPath);
+ const temporaryRoot = await privateTemporaryRoot();
+ try {
+ for (const [index, entry] of codexSignatureNormalizationPolicy.entries.entries()) {
+ const packagedPath = resolve(appPath, entry.appRelativePath);
+ await withRestoredFile(
+ temporaryRoot,
+ packagedPath,
+ `packaged-${index}`,
+ async () => {
+ await mutateLastByte(packagedPath);
+ await expectAppRejection(
+ appPath,
+ `${entry.payloadPath} packaged bytes`,
+ entry.payloadPath === "bin/codex"
+ ? "Runtime hash differs: codex/bin/codex"
+ : "Normalized Codex package identity differs",
+ );
+ },
+ );
+
+ const deltaPath = resolve(appPath, entry.sourceDelta.path);
+ await withRestoredFile(
+ temporaryRoot,
+ deltaPath,
+ `delta-${index}`,
+ async () => {
+ await mutateLastByte(deltaPath);
+ await expectAppRejection(
+ appPath,
+ `${entry.payloadPath} source delta`,
+ "Normalized Codex evidence differs",
+ );
+ },
+ );
+ }
+
+ const manifestPath = join(appPath, "Contents/Resources/runtime/manifest.json");
+ await withRestoredFile(temporaryRoot, manifestPath, "manifest", async () => {
+ const manifest = runtimeManifest(JSON.parse(await readFile(manifestPath, "utf8")));
+ const runtime = runtimeManifest(manifest["runtime"]);
+ const normalized = runtime["normalizedSignatures"];
+ if (!Array.isArray(normalized) || normalized.length === 0) {
+ throw new Error("Runtime manifest has no normalized signature tamper target.");
+ }
+ const first = runtimeManifest(normalized[0]);
+ first["path"] = "Contents/Resources/runtime/codex/bin/unreviewed";
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
+ await expectAppRejection(
+ appPath,
+ "normalized manifest path",
+ "normalized Codex signatures differ from policy",
+ );
+ });
+
+ const codex = codexSignatureNormalizationEntry("bin/codex");
+ const codexPath = resolve(appPath, codex.appRelativePath);
+ await withRestoredFile(temporaryRoot, codexPath, "codex-flags", async () => {
+ await runCodesign([
+ "/usr/bin/codesign",
+ "--force",
+ "--sign",
+ "-",
+ "--identifier",
+ codex.packaged.identifier,
+ codexPath,
+ ]);
+ await expectAppRejection(
+ appPath,
+ "normalized signature without hardened-runtime flags",
+ "Runtime hash differs: codex/bin/codex",
+ );
+ });
+
+ await verifyMacOSApp(appPath);
+ await runCodesign([
+ "/usr/bin/codesign",
+ "--verify",
+ "--deep",
+ "--strict",
+ "--verbose=4",
+ appPath,
+ ]);
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+ process.stdout.write("Codex signature tamper regressions passed.\n");
+}
+
+if (import.meta.main) await main();
diff --git a/apps/desktop/runtime/verify-macos-package.ts b/apps/desktop/runtime/verify-macos-package.ts
index 7bc6ef1..c8e72f8 100644
--- a/apps/desktop/runtime/verify-macos-package.ts
+++ b/apps/desktop/runtime/verify-macos-package.ts
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import {
+ cp,
lstat,
mkdtemp,
open,
@@ -11,14 +12,26 @@ import {
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
+import { isDeepStrictEqual } from "node:util";
import { loadBunNativeLicenseInventory } from "./bun-native-licenses";
import {
+ type CodexNativeLicenseInventory,
renderCodexNativeLicenseNotices,
serializeCodexNativeLicenseInventory,
verifyCodexNativeLicenseInventory,
verifyCodexNativePayloadsAtPaths,
} from "./codex-native-licenses";
+import {
+ codexSignatureNormalizationEntry,
+ codexSignatureNormalizationManifestEntries,
+ codexSignatureNormalizationPolicy,
+ parseCodexSignatureNormalizationEntitlements,
+ reconstructCodexSignatureSource,
+ verifyCodexSignatureNormalizationContent,
+ verifyCodexSignatureNormalizationPackaged,
+ verifyCodexSignatureNormalizationSource,
+} from "./codex-signature-normalization";
import {
correspondingSourceSpecs,
verifyCorrespondingSourceArchive,
@@ -194,8 +207,18 @@ function number(value: unknown, label: string): number {
async function codeSignature(path: string): Promise> {
const result = await run([
"/usr/bin/codesign",
@@ -209,16 +232,49 @@ async function codeSignature(path: string): Promise>> {
+ const result = await run([
+ "/usr/bin/codesign",
+ "--display",
+ "--entitlements",
+ ":-",
+ path,
+ ]);
+ return parseCodexSignatureNormalizationEntitlements(
+ `${result.stdout}\n${result.stderr}`,
+ );
+}
+
async function verifyRuntimeManifest(
appPath: string,
): Promise {
@@ -300,6 +356,8 @@ async function verifyRuntimeManifest(
!== runtimeVersions.codex.dependencyLicenseInventorySha256
|| codex["dependencyLicenseNoticesSha256"]
!== runtimeVersions.codex.dependencyLicenseNoticesSha256
+ || codex["sourceBinarySha256"]
+ !== codexSignatureNormalizationEntry("bin/codex").source.sha256
|| git["version"] !== runtimeVersions.git.version
|| git["assetSha256"] !== runtimeVersions.git.assetSha256
|| gitCredentialManager["version"] !== runtimeVersions.gitCredentialManager.version
@@ -336,6 +394,51 @@ async function verifyRuntimeManifest(
if (imageNormalizer["cdHash"] !== imageNormalizerSignature.cdHash) {
throw new Error("Image normalizer CodeDirectory hash differs from the manifest.");
}
+ const expectedNormalized = codexSignatureNormalizationManifestEntries();
+ const normalized = runtime["normalizedSignatures"];
+ if (!isDeepStrictEqual(normalized, expectedNormalized)) {
+ throw new Error("Runtime manifest normalized Codex signatures differ from policy.");
+ }
+ const normalizedPaths = new Set();
+ for (const entry of codexSignatureNormalizationPolicy.entries) {
+ const absolute = resolve(appPath, entry.appRelativePath);
+ const deltaPath = resolve(appPath, entry.sourceDelta.path);
+ if (!inside(appPath, absolute) || !inside(appPath, deltaPath)) {
+ throw new Error(`Normalized Codex evidence escaped the app: ${entry.payloadPath}`);
+ }
+ const [status, deltaStatus] = await Promise.all([
+ lstat(absolute),
+ lstat(deltaPath),
+ ]);
+ if (
+ !status.isFile()
+ || status.isSymbolicLink()
+ || status.nlink !== 1
+ || !deltaStatus.isFile()
+ || deltaStatus.isSymbolicLink()
+ || deltaStatus.nlink !== 1
+ || deltaStatus.size !== entry.sourceDelta.size
+ || await sha256File(deltaPath) !== entry.sourceDelta.sha256
+ ) {
+ throw new Error(`Normalized Codex evidence differs: ${entry.payloadPath}`);
+ }
+ verifyCodexSignatureNormalizationPackaged(entry, {
+ sha256: await sha256File(absolute),
+ signature: {
+ ...await codeSignature(absolute),
+ entitlements: await codeSignatureEntitlements(absolute),
+ },
+ size: status.size,
+ });
+ await run([
+ "/usr/bin/codesign",
+ "--verify",
+ "--strict",
+ "--verbose=6",
+ absolute,
+ ]);
+ normalizedPaths.add(entry.appRelativePath);
+ }
const preserved = runtime["preservedSignatures"];
if (!Array.isArray(preserved) || preserved.length === 0) {
throw new Error("Runtime manifest has no preserved third-party signatures.");
@@ -347,6 +450,9 @@ async function verifyRuntimeManifest(
if (!trustedThirdPartyTeams.has(team)) {
throw new Error(`Untrusted preserved signature team: ${team}`);
}
+ if (normalizedPaths.has(path)) {
+ throw new Error(`Normalized signature cannot also be preserved: ${path}`);
+ }
const absolute = resolve(appPath, path);
if (!inside(appPath, absolute)) {
throw new Error(`Preserved signature escaped the app: ${path}`);
@@ -370,6 +476,76 @@ async function verifyRuntimeManifest(
return { commit, runtimeManifest: manifest, treeSha256 };
}
+async function verifyReconstructedCodexSourcePayloads(
+ appPath: string,
+ inventory: CodexNativeLicenseInventory,
+ manifestPath: string,
+): Promise {
+ const temporaryRoot = await realpath(
+ await mkdtemp(join(tmpdir(), "hra-codex-source-recovery-")),
+ );
+ const temporaryStatus = await lstat(temporaryRoot);
+ if (
+ !temporaryStatus.isDirectory()
+ || temporaryStatus.isSymbolicLink()
+ || temporaryStatus.uid !== process.getuid?.()
+ || (temporaryStatus.mode & 0o777) !== 0o700
+ ) {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ throw new Error("Codex source recovery root is not an owner-private directory.");
+ }
+ const vendorRoot = join(temporaryRoot, "vendor");
+ try {
+ await cp(join(appPath, "Contents/Resources/runtime/codex"), vendorRoot, {
+ force: false,
+ recursive: true,
+ verbatimSymlinks: true,
+ });
+ for (const entry of codexSignatureNormalizationPolicy.entries) {
+ const packagedPath = resolve(appPath, entry.appRelativePath);
+ const deltaPath = resolve(appPath, entry.sourceDelta.path);
+ const reconstructedPath = join(vendorRoot, entry.payloadPath);
+ await rm(reconstructedPath, { force: true });
+ await reconstructCodexSignatureSource(
+ packagedPath,
+ deltaPath,
+ reconstructedPath,
+ );
+ await verifyCodexSignatureNormalizationContent(
+ reconstructedPath,
+ packagedPath,
+ );
+ const status = await lstat(reconstructedPath);
+ if (!status.isFile() || status.isSymbolicLink() || status.nlink !== 1) {
+ throw new Error(
+ `Reconstructed Codex source is not a regular single-link file: ${entry.payloadPath}`,
+ );
+ }
+ verifyCodexSignatureNormalizationSource(entry, {
+ sha256: await sha256File(reconstructedPath),
+ signature: await codeSignature(reconstructedPath),
+ size: status.size,
+ });
+ const sourceStrict = await run([
+ "/usr/bin/codesign",
+ "--verify",
+ "--strict",
+ "--verbose=6",
+ reconstructedPath,
+ ], { allowFailure: true });
+ process.stdout.write(
+ `Reconstructed Codex source signature ${entry.payloadPath}: strict ${sourceStrict.exitCode === 0 ? "accepted" : "rejected"}; source provenance remains exact.\n`,
+ );
+ }
+ await verifyCodexNativePayloadsAtPaths(inventory, {
+ manifestPath,
+ vendorRoot,
+ });
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+}
+
export async function verifyMacOSApp(
appPath = macosPackage.appBundlePath,
): Promise {
@@ -480,10 +656,12 @@ export async function verifyMacOSApp(
if (stagedCodexNotices !== renderCodexNativeLicenseNotices(stagedCodexInventory)) {
throw new Error("Staged Codex native license notices differ from their inventory.");
}
- await verifyCodexNativePayloadsAtPaths(stagedCodexInventory, {
- manifestPath: join(licenseRoot, "CODEX-platform-package.json"),
- vendorRoot: join(runtimeRoot, "codex"),
- });
+ const release = await verifyRuntimeManifest(canonical);
+ await verifyReconstructedCodexSourcePayloads(
+ canonical,
+ stagedCodexInventory,
+ join(licenseRoot, "CODEX-platform-package.json"),
+ );
const [stagedRuntimeVersions, sourceRuntimeVersions] = await Promise.all([
readFile(join(licenseRoot, "RUNTIME-VERSIONS.json"), "utf8"),
readFile(join(import.meta.dir, "runtime-versions.json"), "utf8"),
@@ -492,7 +670,6 @@ export async function verifyMacOSApp(
throw new Error("Staged runtime version pins differ from source.");
}
- const release = await verifyRuntimeManifest(canonical);
const dataRemover = await codeSignature(join(runtimeRoot, "bin/oprte-data-remover"));
if (dataRemover.identifier !== "oprte-data-remover") {
throw new Error("Data remover code identifier differs.");
diff --git a/hra-legacy-identifiers.manifest.json b/hra-legacy-identifiers.manifest.json
index a58722e..06d28db 100644
--- a/hra-legacy-identifiers.manifest.json
+++ b/hra-legacy-identifiers.manifest.json
@@ -102,7 +102,7 @@
},
{
"category": "compatibility",
- "matchingLinesSha256": "75281cf5966253ab3fa5ef8734799c61ee1c0a55f71f0ec3b45b437961f8901e",
+ "matchingLinesSha256": "ffe8477dfef08b4d2a5bb67997f559840f4cdc5b962599e9b4f513ee933aaf1f",
"occurrences": {
"kitchen": 0,
"operateStylized": 0,
@@ -152,7 +152,7 @@
},
{
"category": "compatibility",
- "matchingLinesSha256": "0326e9761e256da77d2ace9fd3f08bc4a6fae11cc100f15b4ca9b34c165f0074",
+ "matchingLinesSha256": "6e9254f9daf76aca9f9c6174f7827e4e160f8cfabff7ce5d10e0c5716651a880",
"occurrences": {
"kitchen": 1,
"operateStylized": 0,
@@ -162,7 +162,7 @@
},
{
"category": "compatibility",
- "matchingLinesSha256": "8282e79df1d60dcef50b9d7f98811de5f20ca0c458ae8c4950660c5a086332b4",
+ "matchingLinesSha256": "9a96d5c718ca96009d517504a8c79e47895a97f8dc28f022df5522f968f5a278",
"occurrences": {
"kitchen": 1,
"operateStylized": 0,
@@ -1592,7 +1592,7 @@
},
{
"category": "compatibility",
- "matchingLinesSha256": "e33570c46c15518c9ccecd09ca8caf4fc588b44e362dbbfe361851335ccdecfc",
+ "matchingLinesSha256": "160d6226a851d16b24195f4a75babeadf4d2a412e2765a1b24b60b0f43ce2158",
"occurrences": {
"kitchen": 1,
"operateStylized": 0,
@@ -1772,7 +1772,7 @@
},
{
"category": "compatibility",
- "matchingLinesSha256": "105de496a92c387f990cc1de811061f7457c07aa726b77529abd6a013f8e6e16",
+ "matchingLinesSha256": "2284296e2a788b581938be2c73bf83113aacdc37ce50b7c8a6af81a863718a22",
"occurrences": {
"kitchen": 1,
"operateStylized": 0,
diff --git a/scripts/public-tree.manifest.json b/scripts/public-tree.manifest.json
index 193d825..a80ab8f 100644
--- a/scripts/public-tree.manifest.json
+++ b/scripts/public-tree.manifest.json
@@ -1225,6 +1225,7 @@
"apps/desktop/runtime/CODEX-NATIVE-LICENSES.json",
"apps/desktop/runtime/CODEX-NATIVE-LICENSES.txt",
"apps/desktop/runtime/CODEX-NOTICE.txt",
+ "apps/desktop/runtime/CODEX-SIGNATURE-NORMALIZATION.md",
"apps/desktop/runtime/GCM-DEPENDENCY-LICENSES.json",
"apps/desktop/runtime/GCM-DEPENDENCY-LICENSES.txt",
"apps/desktop/runtime/GIT-COPYING.txt",
@@ -1248,6 +1249,8 @@
"apps/desktop/runtime/codex-native-licenses-reviewed.json",
"apps/desktop/runtime/codex-native-licenses-update.ts",
"apps/desktop/runtime/codex-native-licenses.ts",
+ "apps/desktop/runtime/codex-signature-normalization.entitlements.plist",
+ "apps/desktop/runtime/codex-signature-normalization.ts",
"apps/desktop/runtime/control-plane-maintenance.ts",
"apps/desktop/runtime/corresponding-sources.ts",
"apps/desktop/runtime/create-dmg.ts",
@@ -1584,6 +1587,8 @@
"apps/desktop/runtime/test/codex-reconciliation.property.test.ts",
"apps/desktop/runtime/test/codex-reconciliation.test.ts",
"apps/desktop/runtime/test/codex-rpc-core.test.ts",
+ "apps/desktop/runtime/test/codex-signature-normalization.macos.test.ts",
+ "apps/desktop/runtime/test/codex-signature-normalization.test.ts",
"apps/desktop/runtime/test/codex-stream-position.test.ts",
"apps/desktop/runtime/test/codex-supervisor.property.test.ts",
"apps/desktop/runtime/test/codex-supervisor.test.ts",
@@ -1798,6 +1803,7 @@
"apps/desktop/runtime/test/zig-toolchain.test.ts",
"apps/desktop/runtime/update-bun-native-licenses.ts",
"apps/desktop/runtime/update-gcm-dependency-licenses.ts",
+ "apps/desktop/runtime/verify-codex-signature-tamper.ts",
"apps/desktop/runtime/verify-macos-package.ts",
"apps/desktop/runtime/verify-runtime-pins.ts",
"apps/desktop/runtime/zig-toolchain.ts",