diff --git a/src/application/ReconstructionObligationAuthority.ts b/src/application/ReconstructionObligationAuthority.ts index f8c7b0bda..4ebed5f81 100644 --- a/src/application/ReconstructionObligationAuthority.ts +++ b/src/application/ReconstructionObligationAuthority.ts @@ -4,6 +4,50 @@ import type { ReconstructionObligation } from "../domain/reconstructionObligatio type ProofAuthority = ReconstructionObligation["required_fixture_authority"]; type OriginalAuthority = ReconstructionObligation["required_original_authority"]; +type ProofEvidenceShape = readonly [predicate: string, operation: string]; + +const proofEvidenceShapes: Readonly< + Record +> = { + unit: [["rea.fixture-verification/v1", "run_fixture_verifier"]], + integration: [["rea.fixture-verification/v1", "run_fixture_verifier"]], + protocol: [["rea.protocol-observation/v1", "observe_protocol"]], + renderer: [ + ["rea.browser-scenario-capture/v1", "capture_browser_scenario"], + ["rea.electron-page-inspection/v1", "inspect_electron_page"], + ], + "packaged-process": [ + ["rea.process-capture/v4", "capture_process_scenario"], + [ + "rea.reconstruction-readiness-fixture/v1", + "run_reconstruction_readiness_fixture", + ], + ], + "native-abi": [ + ["rea.managed-native-verification/v1", "verify_managed_native_boundaries"], + ], + "live-observation": [ + ["javascript-controlled-replay-observation", "run_controlled_replay"], + ["rea.browser-scenario-capture/v1", "capture_browser_scenario"], + ["rea.electron-page-inspection/v1", "inspect_electron_page"], + ["rea.javascript-runtime-observation/v1", "observe_javascript_runtime"], + ["rea.runtime-characterization/v1", "execute_node_characterization"], + ], + external: [["rea.protocol-observation/v1", "observe_protocol"]], +}; + +const proofEvidenceAuthorities: Readonly< + Record +> = { + unit: ["shipped-artifact", "controlled-replay"], + integration: ["controlled-replay"], + protocol: ["controlled-replay", "external-service"], + renderer: ["controlled-replay", "external-service"], + "packaged-process": ["controlled-replay"], + "native-abi": ["shipped-artifact", "controlled-replay"], + "live-observation": ["controlled-replay", "external-service"], + external: ["external-service"], +}; /** Return whether an actual reconstruction proof is comparable to a requirement. */ export const proofAuthoritySatisfies = ( @@ -46,9 +90,11 @@ export const evidenceAuthoritySupportsProof = ( authority: ProofAuthority, ): boolean => evidence.confidence === "observed" && - (authority === "external" - ? evidence.authority === "external-service" - : ["shipped-artifact", "controlled-replay"].includes(evidence.authority)); + proofEvidenceAuthorities[authority].includes(evidence.authority) && + proofEvidenceShapes[authority].some( + ([predicate, operation]) => + evidence.predicate_type === predicate && evidence.operation === operation, + ); /** Check that Evidence observes the original at the required boundary. */ export const evidenceSupportsOriginalAuthority = ( diff --git a/src/application/ReconstructionObligationLedgerEvaluation.ts b/src/application/ReconstructionObligationLedgerEvaluation.ts index 82035c4d4..384be39cf 100644 --- a/src/application/ReconstructionObligationLedgerEvaluation.ts +++ b/src/application/ReconstructionObligationLedgerEvaluation.ts @@ -454,35 +454,31 @@ const applyDependencyDiagnostics = ( obligations: readonly ReconstructionObligation[], context: EvaluationContext, ): ReconstructionObligation[] => { - const statuses = new Map( - obligations.map((obligation) => [ - obligation.obligation_id, - obligation.status, - ]), - ); - return obligations.map((obligation) => { - const diagnostics = [...obligation.diagnostics]; - for (const dependencyId of obligation.dependency_obligation_ids) { - if (!context.candidates.has(dependencyId)) - addDiagnostic( - diagnostics, - "dependency-missing", - `Dependency obligation is missing: ${dependencyId}.`, - ); - else if (statuses.get(dependencyId) !== "verified") - addDiagnostic( - diagnostics, - "dependency-open", - `Dependency obligation is not verified: ${dependencyId}.`, - ); - } - const normalized = uniqueDiagnostics(diagnostics); - return { - ...obligation, - status: dependencyStatus(obligation.status, normalized), - diagnostics: normalized, - }; - }); + let evaluated = [...obligations]; + for (let pass = 0; pass < obligations.length; pass += 1) { + const statuses = new Map( + evaluated.map(({ obligation_id: id, status }) => [id, status]), + ); + let changed = false; + evaluated = evaluated.map((obligation) => { + const diagnostics = [...obligation.diagnostics]; + for (const dependencyId of obligation.dependency_obligation_ids) { + const missing = !context.candidates.has(dependencyId); + if (missing || statuses.get(dependencyId) !== "verified") + addDiagnostic( + diagnostics, + missing ? "dependency-missing" : "dependency-open", + `Dependency obligation is ${missing ? "missing" : "not verified"}: ${dependencyId}.`, + ); + } + const normalized = uniqueDiagnostics(diagnostics); + const status = dependencyStatus(obligation.status, normalized); + changed ||= status !== obligation.status; + return { ...obligation, status, diagnostics: normalized }; + }); + if (!changed) break; + } + return evaluated; }; const obligationStatus = ( diff --git a/tests/reconstructionObligationLedger.test.ts b/tests/reconstructionObligationLedger.test.ts index cbcc43333..e269d838c 100644 --- a/tests/reconstructionObligationLedger.test.ts +++ b/tests/reconstructionObligationLedger.test.ts @@ -256,13 +256,12 @@ describe("reconstruction obligation ledger", () => { it("closes only when every required case and verifier has comparable authority", () => { const capture = processEvidence(); - const proof = proofEvidence("packaged"); - const initial = build(request([capture, proof])); + const initial = build(request([capture])); const obligation = initial.obligations[0]; if (obligation === undefined) throw new Error("Expected process obligation"); const result = build( - request([capture, proof], { + request([capture], { manifest: { schema_version: 1, bindings: [ @@ -279,7 +278,7 @@ describe("reconstruction obligation ledger", () => { fixture_id: `packaged.${caseKind}`, case_kind: caseKind, authority: "packaged-process", - evidence_ids: [proof.evidence_id], + evidence_ids: [capture.evidence_id], })), verifier: { verifier_id: "verifier.packaged", @@ -287,7 +286,7 @@ describe("reconstruction obligation ledger", () => { command: "npm run verify:package -- process", authority: "packaged-process", status: "pass", - result_evidence_id: proof.evidence_id, + result_evidence_id: capture.evidence_id, enumerated_obligation_ids: [obligation.obligation_id], nondeterminism: { mode: "partial-order", @@ -310,6 +309,140 @@ describe("reconstruction obligation ledger", () => { expect(result.summary.required_open).toBe(0); }); + it("does not authenticate packaged-process proof with generic replay evidence", () => { + const capture = processEvidence(); + const proof = proofEvidence("generic-packaged"); + const initial = build(request([capture, proof])); + const obligation = initial.obligations[0]; + if (obligation === undefined) + throw new Error("Expected process obligation"); + const result = build( + request([capture, proof], { + manifest: { + schema_version: 1, + bindings: [ + { + obligation_id: obligation.obligation_id, + owner: { + module_path: "src/processOwner.ts", + symbol: "runProcess", + owner_sha256: "5".repeat(64), + }, + parser_type: null, + original_cases: originalCases(obligation, capture.evidence_id), + fixtures: obligation.required_case_kinds.map((caseKind) => ({ + fixture_id: `packaged.${caseKind}`, + case_kind: caseKind, + authority: "packaged-process", + evidence_ids: [proof.evidence_id], + })), + verifier: { + verifier_id: "verifier.packaged", + claim_id: "claim.process", + command: "npm run verify:package -- process", + authority: "packaged-process", + status: "pass", + result_evidence_id: proof.evidence_id, + enumerated_obligation_ids: [obligation.obligation_id], + nondeterminism: { + mode: "partial-order", + specification: "Fixture scheduling is partially ordered.", + }, + }, + }, + ], + contradictions: [], + }, + }), + ); + + expect(result.obligations[0]?.diagnostics.map(({ code }) => code)).toEqual( + expect.arrayContaining([ + "weak-fixture-authority", + "weak-verifier-authority", + ]), + ); + }); + + it("propagates an open dependency through the full obligation chain", () => { + const evidence = createEvidence( + undefined, + { id: "fixture-chain", name: "Fixture chain", version: "1" }, + { + predicateType: "rea.fixture-verification/v1", + operation: "run_fixture_verifier", + parameters: {}, + result: { passed: true }, + confidence: "observed", + authority: "shipped-artifact", + }, + ); + const ids = ["obl.chain.a", "obl.chain.b", "obl.chain.c"] as const; + const reviewed = ids.map((id, index) => ({ + ...reviewedObligation(id, evidence.evidence_id), + dependency_obligation_ids: + index === 0 ? [ids[1]] : index === 1 ? [ids[2]] : ["obl.chain.missing"], + })); + const bindings = reviewed.map((obligation) => ({ + obligation_id: obligation.obligation_id, + owner: { + module_path: `src/${obligation.obligation_id}.ts`, + symbol: "verify", + owner_sha256: "6".repeat(64), + }, + parser_type: null, + original_cases: [ + { + kind: "positive" as const, + evidence_id: evidence.evidence_id, + location: "/fixture/positive", + }, + ], + fixtures: [ + { + fixture_id: `fixture.${obligation.obligation_id}`, + case_kind: "positive" as const, + authority: "unit" as const, + evidence_ids: [evidence.evidence_id], + }, + ], + verifier: { + verifier_id: `verifier.${obligation.obligation_id}`, + claim_id: `claim.${obligation.obligation_id}`, + command: "npm test -- fixture-chain", + authority: "unit" as const, + status: "pass" as const, + result_evidence_id: evidence.evidence_id, + enumerated_obligation_ids: [obligation.obligation_id], + nondeterminism: { + mode: "total-order" as const, + specification: "Fixture execution is ordered.", + }, + }, + })); + const result = build( + request([evidence], { + reviewed_obligations: reviewed, + manifest: { schema_version: 1, bindings, contradictions: [] }, + }), + ); + + expect(result.obligations.map(({ status }) => status)).toEqual([ + "blocked", + "blocked", + "blocked", + ]); + expect( + result.obligations.map(({ diagnostics }) => + diagnostics.map(({ code }) => code), + ), + ).toEqual([ + ["dependency-open"], + ["dependency-open"], + ["dependency-missing"], + ]); + }); + it("fails closed on parser gaps, ambiguous ownership, and contradiction", () => { const original = createEvidence( undefined,