diff --git a/tests/helpers/update-capability-policy.ts b/tests/helpers/update-capability-policy.ts index eab57a1509d..bf0e195affd 100644 --- a/tests/helpers/update-capability-policy.ts +++ b/tests/helpers/update-capability-policy.ts @@ -21,7 +21,8 @@ const readsFor = (module: string): Set | undefined => { module = module.replace(/^node:/, ""); if (module === "fs" || module === "fs/promises") return fsReads; if (module === "bun") return bunReads; - if (module === "crypto") return new Set(["randomBytes", "randomUUID", "createHash"]); + // createPublicKey and verify are pure: they build a key object from bytes and check a signature. + if (module === "crypto") return new Set(["randomBytes", "randomUUID", "createHash", "createPublicKey", "verify"]); if (module === "os") return new Set(["homedir", "tmpdir", "platform", "arch", "type", "release"]); if (module === "tty") return new Set(["isatty"]); // process imports are disallowed: the global view below is deliberately narrow. diff --git a/tests/helpers/update-helper-control-contracts.ts b/tests/helpers/update-helper-control-contracts.ts index 2dbd4cff383..91a6bc100b1 100644 --- a/tests/helpers/update-helper-control-contracts.ts +++ b/tests/helpers/update-helper-control-contracts.ts @@ -4,6 +4,7 @@ type Contract = readonly [file: string, context: string, call: string, count: number, contract: string]; import { orchestratedChildReview } from "./update-review-orchestrated-child"; import { orchestratedEntryReview } from "./update-review-orchestrated-entry"; +import { signedSupplyReview } from "./update-review-signed-supply"; const baseHelperControlContracts: readonly Contract[] = [ ["src/lib/desktop-cli-handoff.mjs","runDesktopCliHandoff","launch.onSpawn(child.pid)",1,"pre-mutation: publish the sync lease delegate for the launched child"], @@ -123,4 +124,4 @@ const baseHelperControlContracts: readonly Contract[] = [ ["src/update/mutation-guard.mjs","guardedRun","operation(...data)",1,"materialized data then authorizer then direct primitive dispatch"], ["src/update/mutation-guard.mjs","ownedArtifactRm","native(...data)",1,"bounded private transaction artifact cleanup"] ]; -export const helperControlContracts: readonly Contract[] = [...baseHelperControlContracts, ...orchestratedEntryReview.contracts, ...orchestratedChildReview.contracts]; +export const helperControlContracts: readonly Contract[] = [...baseHelperControlContracts, ...orchestratedEntryReview.contracts, ...orchestratedChildReview.contracts, ...signedSupplyReview.contracts]; diff --git a/tests/helpers/update-import-policy.ts b/tests/helpers/update-import-policy.ts index 9cd1c9e461a..e919539fe73 100644 --- a/tests/helpers/update-import-policy.ts +++ b/tests/helpers/update-import-policy.ts @@ -2,6 +2,7 @@ // security boundary or completeness proof. Runtime mutation guards enforce authority. import { orchestratedChildReview } from "./update-review-orchestrated-child"; import { orchestratedEntryReview } from "./update-review-orchestrated-entry"; +import { signedSupplyReview } from "./update-review-signed-supply"; import { mergeStaticImports } from "./update-review-table"; const baseStaticImports: Readonly> = { @@ -366,7 +367,7 @@ const baseStaticImports: Readonly> = { "import { dirname, isAbsolute } from \"node:path\";" ] }; -export const updateStaticImports = mergeStaticImports(baseStaticImports, orchestratedEntryReview.staticImports, orchestratedChildReview.staticImports); +export const updateStaticImports = mergeStaticImports(baseStaticImports, orchestratedEntryReview.staticImports, orchestratedChildReview.staticImports, signedSupplyReview.staticImports); const baseDeferredImports: readonly string[] = [ // Read-only identity verification before the child enters mutation. 'src/update/index.ts:runUpdate:session = createOrchestratedChildSession({ home: getConfigDir(), resultPath: child.resultPath, resultCapability: child.resultCapability, expect, notAfter: child.notAfter, verifyIdentity: async () => (await import("./orchestrated-entry")).verifyOrchestratedChildIdentity(expect, child.version) })', @@ -386,7 +387,7 @@ const baseDeferredImports: readonly string[] = [ "src/update/job-update-tray.ts:defaultWorkerTrayHandoff:{ getWindowsTrayStatus, guardedStartWindowsTray, guardedStopWindowsTray } = await import(\"./guarded-tray-ops\")", "src/update/job.ts:restartAfterUpdate:{ serviceReinstallArgs } = await import(\"../service\")" ]; -export const updateDeferredImports: readonly string[] = [...baseDeferredImports, ...orchestratedEntryReview.deferredImports, ...orchestratedChildReview.deferredImports]; +export const updateDeferredImports: readonly string[] = [...baseDeferredImports, ...orchestratedEntryReview.deferredImports, ...orchestratedChildReview.deferredImports, ...signedSupplyReview.deferredImports]; // Existing trusted wrapper internals: each complete expression is reviewed. // Import permission does not grant a wrapper arbitrary loader/global access. export const updateBoundaryRootSites = new Map([ diff --git a/tests/helpers/update-primitive-allowlist.ts b/tests/helpers/update-primitive-allowlist.ts index 3d71f8819f8..b5b97c3a48f 100644 --- a/tests/helpers/update-primitive-allowlist.ts +++ b/tests/helpers/update-primitive-allowlist.ts @@ -4,6 +4,7 @@ // Probe rows confer no update authority. Child rows retain only their own child. import { orchestratedChildReview } from "./update-review-orchestrated-child"; import { orchestratedEntryReview } from "./update-review-orchestrated-entry"; +import { signedSupplyReview } from "./update-review-signed-supply"; const basePrimitiveAllowlist: readonly (readonly [string, string])[] = [ ["src/update/config-preflight.ts::import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from \"node:fs\";", "read-only config preflight: open with O_RDONLY | O_NONBLOCK, inspect and read the fd, then close it"], @@ -216,4 +217,4 @@ const basePrimitiveAllowlist: readonly (readonly [string, string])[] = [ ["src/update/npm-install.mjs:runNpmInstall:process.stderr.write(chunk)","bounded helper protocol output; no successful update publication"], ["src/update/refresh-scheduler.ts:createRefreshScheduler:deps.write(tag, latest, deps.now())","version badge cache only; no runtime or install authority"] ]; -export const updatePrimitiveAllowlist: readonly (readonly [string, string])[] = [...basePrimitiveAllowlist, ...orchestratedEntryReview.primitive, ...orchestratedChildReview.primitive]; +export const updatePrimitiveAllowlist: readonly (readonly [string, string])[] = [...basePrimitiveAllowlist, ...orchestratedEntryReview.primitive, ...orchestratedChildReview.primitive, ...signedSupplyReview.primitive]; diff --git a/tests/helpers/update-process-allowlist.ts b/tests/helpers/update-process-allowlist.ts index dddfb8e1d87..05ce6a17154 100644 --- a/tests/helpers/update-process-allowlist.ts +++ b/tests/helpers/update-process-allowlist.ts @@ -3,6 +3,7 @@ // Exact existing terminal/probe lifecycle sites. No arbitrary process API is admitted. import { orchestratedChildReview } from "./update-review-orchestrated-child"; import { orchestratedEntryReview } from "./update-review-orchestrated-entry"; +import { signedSupplyReview } from "./update-review-signed-supply"; const baseProcessAllowlist: readonly (readonly [string, string, number])[] = [ ["bin/ocx.mjs:runPackageManagerSelfUpdate:process.on(signal, () => {})","pre-mutation: keep the update session owner alive during installer supervision",1], @@ -35,4 +36,4 @@ const baseProcessAllowlist: readonly (readonly [string, string, number])[] = [ ["src/update/notify.ts:maybeShowUpdatePrompt:process.exit(0)","ordinary launcher/help/probe or terminal prompt outcome; update success remains guarded",1], ["src/update/service-home-probe-entry.ts::process.exit(1)","terminal failure only; cannot publish successful update",1], ]; -export const updateProcessAllowlist: readonly (readonly [string, string, number])[] = [...baseProcessAllowlist, ...orchestratedEntryReview.process, ...orchestratedChildReview.process]; +export const updateProcessAllowlist: readonly (readonly [string, string, number])[] = [...baseProcessAllowlist, ...orchestratedEntryReview.process, ...orchestratedChildReview.process, ...signedSupplyReview.process]; diff --git a/tests/helpers/update-review-signed-supply.ts b/tests/helpers/update-review-signed-supply.ts new file mode 100644 index 00000000000..f1ef880e172 --- /dev/null +++ b/tests/helpers/update-review-signed-supply.ts @@ -0,0 +1,57 @@ +// Best-effort regression lint data, not a security boundary or completeness proof. +// Review rows for the #8294 offline signed-update verification modules (minisign.mjs, signed-supply.mjs, +// update-bounded-json.mjs, update-checkpoint-codec.mjs, update-contracts.mjs, +// update-signature-threshold.mjs, update-signed-version.mjs), kept apart from the base tables so the +// group is reviewed on its own. Every module here is pure verification over caller-supplied bytes: no +// filesystem, process, network or runtime capability, and no update authority. A file key may appear in +// only one table. +import type { UpdateReviewTable } from "./update-review-table"; + +export const signedSupplyReview: UpdateReviewTable = { + staticImports: { + "src/update/minisign.mjs": [ + "import { snapshotUpdateBytes } from \"./update-bytes.mjs\";", // pure: bounded copy of caller bytes + ], + "src/update/signed-supply.mjs": [ + "import { parseRootTransitionEnvelope, parseSignatureEnvelope, parseUpdateDocument } from \"./update-contracts.mjs\";", // pure: parse signed documents + "import { EMBEDDED_ROOT_SHA256 } from \"./update-trust-anchor.mjs\";", // pure: embedded trust-anchor constant + "import { snapshotUpdateBytes } from \"./update-bytes.mjs\";", // pure: bounded copy of caller bytes + "import { verifySignatureThreshold } from \"./update-signature-threshold.mjs\";", // pure: count valid signatures + "import { deriveCandidateId } from \"./update-candidate-id.mjs\";", // pure: digest-derived identifier + "import { decodePublicKey, verifyMinisignBytes } from \"./minisign.mjs\";", // pure: signature verification + "import { trustedCommentMatchesVersion } from \"./update-signed-version.mjs\";", // pure: version comparison + ], + "src/update/update-bounded-json.mjs": [ + "import { snapshotUpdateBytes } from \"./update-bytes.mjs\";", // pure: bounded copy of caller bytes + ], + "src/update/update-checkpoint-codec.mjs": [ + "import { snapshotUpdateBytes } from \"./update-bytes.mjs\";", // pure: bounded copy of caller bytes + "import { parseRootTransitionEnvelope, parseUpdateDocument } from \"./update-contracts.mjs\";", // pure: parse signed documents + "import { sameSignedVersion } from \"./update-signed-version.mjs\";", // pure: version comparison + ], + "src/update/update-contracts.mjs": [ + "import { parseBoundedJson } from \"./update-bounded-json.mjs\";", // pure: bounded JSON parse + "import { decodePublicKey } from \"./minisign.mjs\";", // pure: key decoding + "import { sameSignedVersion } from \"./update-signed-version.mjs\";", // pure: version comparison + ], + "src/update/update-signature-threshold.mjs": [ + "import { decodePublicKey, verifyMinisignBytes } from \"./minisign.mjs\";", // pure: signature verification + ], + }, + deferredImports: [], + primitive: [ + ["src/update/update-signed-version.mjs:validVersion:VERSION.exec", "pure: RegExp#exec on a version string, not a process exec"], + ["src/update/update-bounded-json.mjs:parseBoundedJson:NUMBER.exec", "pure: RegExp#exec on JSON number text, not a process exec"], + // Keyed on the complete expression: any other Reflect member at these sites is still flagged. + ["src/update/signed-supply.mjs:verifyExternalMetadataEvidence:Reflect.ownKeys(descriptors)", "pure: counts a descriptor snapshot's own keys; no capability acquisition"], + ["src/update/update-checkpoint-codec.mjs:record:Reflect.ownKeys(value)", "pure: checks a decoded record's exact key set; no capability acquisition"], + ["src/update/update-checkpoint-codec.mjs:list:Reflect.ownKeys(value)", "pure: checks a decoded list's exact index keys; no capability acquisition"], + ], + process: [], + contracts: [ + ["src/update/signed-supply.mjs", "verifyExternalMetadataEvidence", "Reflect.ownKeys(descriptors)", 1, "read-only: exact own-key count of a caller descriptor snapshot before any field is trusted"], + ["src/update/update-checkpoint-codec.mjs", "record", "Reflect.ownKeys(value)", 1, "read-only: exact key set of a decoded checkpoint record"], + ["src/update/update-checkpoint-codec.mjs", "list", "Reflect.ownKeys(value)", 1, "read-only: exact index keys of a decoded checkpoint list"], + ], +}; +