From bf53e4f2fc2ce2581566e3de691b418aa0e3198d Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 21 Oct 2026 18:29:36 +0200 Subject: [PATCH] build: validate packaged artifact identity --- docs/CODEX_HANDOFF.md | 3 +- docs/research/m1-no-device-acceptance.md | 4 +- docs/roadmap.md | 3 +- scripts/prepare_m1_acceptance_kit.py | 10 +++++ scripts/validate_m1_manual_acceptance.py | 14 +++++++ tests/test_m1_acceptance_kit.py | 47 ++++++++++++++++++------ tests/test_m1_manual_acceptance.py | 28 +++++++++++++- 7 files changed, 93 insertions(+), 16 deletions(-) diff --git a/docs/CODEX_HANDOFF.md b/docs/CODEX_HANDOFF.md index b08c801..c66772d 100644 --- a/docs/CODEX_HANDOFF.md +++ b/docs/CODEX_HANDOFF.md @@ -19,7 +19,8 @@ The repository now provides: - a non-executing verifier for externally produced Windows artifacts; - a checksummed acceptance-kit generator containing only allowlisted public synthetic inputs, with provenance binding to the repository commit and - non-executing artifact-verification report; + non-executing artifact-verification report, including the Rayforge pin and + bundle, installer and addon-verification fields; - a fail-closed manual desktop-acceptance report and validator; - pinned upstream contract/license manifests and a drift checker; - a fail-closed API-23 adapter isolating Rayforge's internal driver registry; diff --git a/docs/research/m1-no-device-acceptance.md b/docs/research/m1-no-device-acceptance.md index a1499ef..04477c4 100644 --- a/docs/research/m1-no-device-acceptance.md +++ b/docs/research/m1-no-device-acceptance.md @@ -97,7 +97,9 @@ non-empty directory. It does not include the application bundle, private LightBurn evidence or any machine data. Acceptance-kit schema 2 binds the generated manual report to the kit manifest, repository commit and copied artifact-verification report by SHA-256. Editing, replacing or referring to an -external provenance report therefore fails validation. +external provenance report therefore fails validation. The copied report must +also name the pinned Rayforge commit, contain valid bundle and installer +SHA-256 values, and confirm that at least one packaged addon file was verified. 1. Pass the non-executing artifact verifier above. 2. Launch the portable build with a fresh application-data directory. diff --git a/docs/roadmap.md b/docs/roadmap.md index a6dc7e7..de3e94c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -332,7 +332,8 @@ A reproducible kit generator copies only allowlisted synthetic inputs into the ignored build area, records their hashes and refuses to overwrite an existing test run. Its schema-2 provenance chain binds the editable manual report to the exact kit manifest, repository commit and non-executing artifact-verification -report; substituted or changed inputs fail validation. +report; substituted or changed inputs, a different Rayforge pin, malformed +artifact hashes or an unverified addon fail validation. The pinned upstream contracts and license notices are now machine-verifiable. Rayforge's documented addon lifecycle is viable, but production driver registration still needs a supported symmetric registry API. The current diff --git a/scripts/prepare_m1_acceptance_kit.py b/scripts/prepare_m1_acceptance_kit.py index 2f34e05..dd3895e 100644 --- a/scripts/prepare_m1_acceptance_kit.py +++ b/scripts/prepare_m1_acceptance_kit.py @@ -5,12 +5,17 @@ from __future__ import annotations import argparse import hashlib import json +import re import shutil import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] +RAYFORGE_COMMIT = json.loads( + (ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8") +)["commit"] +SHA256 = re.compile(r"^[0-9a-f]{64}$") REPORT_TEMPLATE = "tests/fixtures/m1-manual-acceptance-template.json" KIT_FILES = { "inputs/synthetic-mopa-import.lbrn2": "tests/fixtures/synthetic-mopa-import.lbrn2", @@ -52,6 +57,11 @@ def create_kit( or verification.get("hardware_access") is not False or verification.get("executed_binaries") is not False or verification.get("manual_interaction_required") is not True + or verification.get("rayforge_commit") != RAYFORGE_COMMIT + or SHA256.fullmatch(verification.get("bundle_sha256", "")) is None + or SHA256.fullmatch(verification.get("installer_sha256", "")) is None + or not isinstance(verification.get("verified_addon_files"), int) + or verification.get("verified_addon_files", 0) <= 0 ): raise ValueError("artifact-verification report did not pass the no-device gate") diff --git a/scripts/validate_m1_manual_acceptance.py b/scripts/validate_m1_manual_acceptance.py index 6d4d164..1b1767b 100644 --- a/scripts/validate_m1_manual_acceptance.py +++ b/scripts/validate_m1_manual_acceptance.py @@ -10,6 +10,9 @@ from pathlib import Path, PurePosixPath ROOT = Path(__file__).resolve().parents[1] +RAYFORGE_COMMIT = json.loads( + (ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8") +)["commit"] STEP_IDS = ( "artifact_verified", "isolated_portable_launch", @@ -111,6 +114,17 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]: errors.append("artifact verification executed binaries") if verification.get("manual_interaction_required") is not True: errors.append("artifact verification dropped the manual interaction gate") + if verification.get("rayforge_commit") != RAYFORGE_COMMIT: + errors.append("artifact verification uses the wrong Rayforge commit") + if SHA256.fullmatch(verification.get("bundle_sha256", "")) is None: + errors.append("artifact verification has an invalid bundle SHA-256") + if SHA256.fullmatch(verification.get("installer_sha256", "")) is None: + errors.append("artifact verification has an invalid installer SHA-256") + if ( + not isinstance(verification.get("verified_addon_files"), int) + or verification.get("verified_addon_files", 0) <= 0 + ): + errors.append("artifact verification did not verify addon files") return errors diff --git a/tests/test_m1_acceptance_kit.py b/tests/test_m1_acceptance_kit.py index 0c7c61a..65fdddf 100644 --- a/tests/test_m1_acceptance_kit.py +++ b/tests/test_m1_acceptance_kit.py @@ -6,6 +6,7 @@ from pathlib import Path from scripts.prepare_m1_acceptance_kit import ( KIT_FILES, + RAYFORGE_COMMIT, REPORT_TEMPLATE, ROOT, create_kit, @@ -13,6 +14,19 @@ from scripts.prepare_m1_acceptance_kit import ( class M1AcceptanceKitTests(unittest.TestCase): + @staticmethod + def _verification(status="passed"): + return { + "status": status, + "hardware_access": False, + "executed_binaries": False, + "manual_interaction_required": True, + "rayforge_commit": RAYFORGE_COMMIT, + "bundle_sha256": "1" * 64, + "installer_sha256": "2" * 64, + "verified_addon_files": 7, + } + def test_kit_contains_only_allowlisted_public_inputs(self): with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: output = Path(directory) / "kit" @@ -70,12 +84,9 @@ class M1AcceptanceKitTests(unittest.TestCase): with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: root = Path(directory) verification = root / "verification.json" - verification.write_text(json.dumps({ - "status": "passed", - "hardware_access": False, - "executed_binaries": False, - "manual_interaction_required": True, - }), encoding="utf-8") + verification.write_text( + json.dumps(self._verification()), encoding="utf-8" + ) output = root / "kit" create_kit( output, @@ -95,12 +106,24 @@ class M1AcceptanceKitTests(unittest.TestCase): with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: root = Path(directory) verification = root / "verification.json" - verification.write_text(json.dumps({ - "status": "failed", - "hardware_access": False, - "executed_binaries": False, - "manual_interaction_required": True, - }), encoding="utf-8") + verification.write_text( + json.dumps(self._verification(status="failed")), encoding="utf-8" + ) + with self.assertRaisesRegex(ValueError, "did not pass"): + create_kit( + root / "kit", + repository_commit="f" * 40, + artifact_verification=verification, + ) + + def test_wrong_rayforge_pin_or_missing_hashes_are_rejected(self): + with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: + root = Path(directory) + verification = root / "verification.json" + result = self._verification() + result["rayforge_commit"] = "0" * 40 + result["bundle_sha256"] = "not-a-hash" + verification.write_text(json.dumps(result), encoding="utf-8") with self.assertRaisesRegex(ValueError, "did not pass"): create_kit( root / "kit", diff --git a/tests/test_m1_manual_acceptance.py b/tests/test_m1_manual_acceptance.py index 4c6287f..48f639e 100644 --- a/tests/test_m1_manual_acceptance.py +++ b/tests/test_m1_manual_acceptance.py @@ -5,7 +5,12 @@ import tempfile import unittest from pathlib import Path -from scripts.validate_m1_manual_acceptance import ROOT, STEP_IDS, validate +from scripts.validate_m1_manual_acceptance import ( + RAYFORGE_COMMIT, + ROOT, + STEP_IDS, + validate, +) TEMPLATE = ROOT / "tests" / "fixtures" / "m1-manual-acceptance-template.json" @@ -41,6 +46,10 @@ class M1ManualAcceptanceTests(unittest.TestCase): "hardware_access": False, "executed_binaries": False, "manual_interaction_required": True, + "rayforge_commit": RAYFORGE_COMMIT, + "bundle_sha256": "1" * 64, + "installer_sha256": "2" * 64, + "verified_addon_files": 7, } verification_path = root / "artifact-verification.json" verification_path.write_text(json.dumps(verification), encoding="utf-8") @@ -136,6 +145,23 @@ class M1ManualAcceptanceTests(unittest.TestCase): self.assertEqual(result["status"], "failed") self.assertIn("acceptance-kit repository commit mismatch", result["errors"]) + def test_wrong_artifact_pin_and_invalid_artifact_hashes_are_rejected(self): + with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: + root = Path(directory) + report = self._passing(root) + verification_path = root / report["artifact_verification"]["report_path"] + verification = json.loads(verification_path.read_text(encoding="utf-8")) + verification["rayforge_commit"] = "0" * 40 + verification["installer_sha256"] = "invalid" + verification_path.write_text(json.dumps(verification), encoding="utf-8") + report["artifact_verification"]["report_sha256"] = hashlib.sha256( + verification_path.read_bytes() + ).hexdigest() + result = validate(self._write(root, report)) + self.assertEqual("failed", result["status"]) + self.assertIn("artifact verification uses the wrong Rayforge commit", result["errors"]) + self.assertIn("artifact verification has an invalid installer SHA-256", result["errors"]) + if __name__ == "__main__": unittest.main()