diff --git a/scripts/verify-reconstruction-readiness.mjs b/scripts/verify-reconstruction-readiness.mjs index 1de268ae8..940a39fdd 100644 --- a/scripts/verify-reconstruction-readiness.mjs +++ b/scripts/verify-reconstruction-readiness.mjs @@ -1,7 +1,6 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; -import { access, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { access, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { promisify } from "node:util"; @@ -25,8 +24,6 @@ const electronPath = join(fixtureRoot, "electron"); const environment = { ...process.env, }; -const temporaryRoot = await mkdtemp(join(tmpdir(), "rea-readiness-")); - await access(nativePath); const nativeEvidence = await runCli(["inspect-artifact", nativePath, "--json"]); const javascriptCliEvidence = await runCli([ @@ -135,7 +132,6 @@ try { ); } finally { await client.close(); - await rm(temporaryRoot, { recursive: true, force: true }); } async function runCli(arguments_) { diff --git a/src/domain/evidenceBundle.ts b/src/domain/evidenceBundle.ts index 217b44fee..4deceb0c4 100644 --- a/src/domain/evidenceBundle.ts +++ b/src/domain/evidenceBundle.ts @@ -238,7 +238,13 @@ export const parseEvidenceBundle = (input: unknown): EvidenceBundle => { const owned = immutableBundles.get(input); if (owned !== undefined) return owned; } - const parsed = evidenceBundleSchema.parse(input); + return authenticateParsedEvidenceBundle(evidenceBundleSchema.parse(input)); +}; + +/** Authenticate evidence and bundle invariants after the bundle shape is parsed. */ +export const authenticateParsedEvidenceBundle = ( + parsed: EvidenceBundle, +): EvidenceBundle => { const recordIds = parsed.records.map(({ evidence_id: id }) => id); if (new Set(recordIds).size !== recordIds.length) throw new TypeError("Evidence bundle contains duplicate record IDs"); diff --git a/src/domain/reconstructionReadiness.test.ts b/src/domain/reconstructionReadiness.test.ts index 93f827c85..df0d5e2a0 100644 --- a/src/domain/reconstructionReadiness.test.ts +++ b/src/domain/reconstructionReadiness.test.ts @@ -14,6 +14,45 @@ const input = ( ...overrides, }); +describe("readiness bundle authentication", () => { + it("authenticates each Evidence record after parsing the readiness contract", () => { + const value = input(); + const record = value.evidence_bundle.records[0]; + if (record === undefined) throw new TypeError("Missing Evidence record"); + value.evidence_bundle.records[0] = { + ...record, + evidence_id: `ev_${"0".repeat(64)}`, + }; + + expect(() => createReconstructionReadinessReport(value)).toThrow( + /Evidence semantic identifier does not match its record/u, + ); + }); + + it("rejects a noncanonical bundle manifest after readiness parsing", () => { + const value = input(); + const provider = value.evidence_bundle.providers[0]; + if (provider === undefined) + throw new TypeError("Missing provider manifest"); + value.evidence_bundle.providers.push(provider); + + expect(() => createReconstructionReadinessReport(value)).toThrow( + /Evidence bundle manifests are not canonical/u, + ); + }); + + it("rejects duplicate Evidence record IDs after readiness parsing", () => { + const value = input(); + const record = value.evidence_bundle.records[0]; + if (record === undefined) throw new TypeError("Missing Evidence record"); + value.evidence_bundle.records.push(record); + + expect(() => createReconstructionReadinessReport(value)).toThrow( + /Evidence bundle contains duplicate record IDs/u, + ); + }); +}); + describe("reconstruction readiness report", () => { it("accepts every caller supplied capability", () => { const capability = RECONSTRUCTION_READINESS_EXAMPLE.capabilities[0]; diff --git a/src/domain/reconstructionReadiness.ts b/src/domain/reconstructionReadiness.ts index f7553b75f..13a51e67c 100644 --- a/src/domain/reconstructionReadiness.ts +++ b/src/domain/reconstructionReadiness.ts @@ -1,8 +1,5 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; - -import { parseEvidenceBundle } from "./evidenceBundle.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; +import { authenticateParsedEvidenceBundle } from "./evidenceBundle.js"; import { readinessFindingOrder } from "./reconstructionReadinessFindingHelpers.js"; import { READINESS_STAGE_IDS, @@ -24,7 +21,7 @@ export const createReconstructionReadinessReport = ( const input = reconstructionReadinessInputSchema.parse(inputValue); const sourceDigest = digest(sourceValue(input)); const evidenceIds = new Set( - parseEvidenceBundle(input.evidence_bundle).records.map( + authenticateParsedEvidenceBundle(input.evidence_bundle).records.map( ({ evidence_id: evidenceId }) => evidenceId, ), ); @@ -185,8 +182,5 @@ const ratio = (numerator: number, denominator: number): number => denominator === 0 ? 1 : numerator / denominator; const digest = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Reconstruction readiness value is not canonical JSON"); - return createHash("sha256").update(encoded).digest("hex"); + return digestCanonicalValue(value, "Reconstruction readiness"); };