build: verify exact packaged addon contents

This commit is contained in:
Codex 2026-10-21 18:31:54 +02:00
parent bf53e4f2fc
commit 644c99a9ca
9 changed files with 122 additions and 35 deletions

View file

@ -17,6 +17,7 @@ The repository now provides:
- an on-demand source/build entry point that never installs the Windows
toolchain;
- a non-executing verifier for externally produced Windows artifacts;
it requires an exact repository-to-manifest-to-ZIP addon file set;
- a checksummed acceptance-kit generator containing only allowlisted public
synthetic inputs, with provenance binding to the repository commit and
non-executing artifact-verification report, including the Rayforge pin and

View file

@ -53,8 +53,10 @@ python scripts/verify_rayforge_windows_checkpoint.py `
The verifier requires checkpoint schema 2 and portable artifact names, checks
sizes and SHA-256 digests, rejects unsafe ZIP paths, confirms the pinned
Rayforge commit, compares every packaged addon file with its recorded digest,
and requires the pinned Rayforge license both inside the ZIP and as a sidecar.
Rayforge commit, requires the manifest and bundle to contain exactly the
versioned repository addon file set with matching digests, and requires the
pinned Rayforge license both inside the ZIP and as a sidecar. Missing, changed
or additional addon files fail verification.
It never executes the application or installer, and a passing result retains
the manual GUI interaction gate. The installer, manifest and license sidecar
form one distribution set.
@ -99,7 +101,8 @@ 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. 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.
SHA-256 values, and confirm that the exact current addon file count was
verified.
1. Pass the non-executing artifact verifier above.
2. Launch the portable build with a fresh application-data directory.

View file

@ -324,7 +324,9 @@ reports. The on-demand build entry point fetches pinned source and stages the
addon but deliberately refuses to install a missing Windows toolchain.
An artifact produced elsewhere can be checked locally without execution: the
verifier rejects unsafe paths or changed bytes and confirms the pin plus every
packaged addon source before the manual GUI checklist may begin.
packaged addon source before the manual GUI checklist may begin. It also
rejects omitted manifest entries and extra files hidden in the packaged addon
directory.
The manual checklist now also has a versioned, fail-closed report template and
validator. Missing steps remain incomplete, stop conditions fail the run, and
hashed screenshots stay outside the public repository pending sanitization.

View file

@ -16,6 +16,10 @@ RAYFORGE_COMMIT = json.loads(
(ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8")
)["commit"]
SHA256 = re.compile(r"^[0-9a-f]{64}$")
EXPECTED_ADDON_FILES = sum(
path.is_file() and "__pycache__" not in path.parts
for path in (ROOT / "spikes" / "rayforge-jcz-no-device-addon").rglob("*")
)
REPORT_TEMPLATE = "tests/fixtures/m1-manual-acceptance-template.json"
KIT_FILES = {
"inputs/synthetic-mopa-import.lbrn2": "tests/fixtures/synthetic-mopa-import.lbrn2",
@ -60,8 +64,7 @@ def create_kit(
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
or verification.get("verified_addon_files") != EXPECTED_ADDON_FILES
):
raise ValueError("artifact-verification report did not pass the no-device gate")

View file

@ -36,6 +36,10 @@ STOP_CONDITIONS = (
STEP_STATUSES = {"passed", "failed", "blocked", "not_run"}
SHA256 = re.compile(r"^[0-9a-f]{64}$")
GIT_COMMIT = re.compile(r"^[0-9a-f]{40}$")
EXPECTED_ADDON_FILES = sum(
path.is_file() and "__pycache__" not in path.parts
for path in (ROOT / "spikes" / "rayforge-jcz-no-device-addon").rglob("*")
)
def _validate_evidence(report_root: Path, records: list[dict]) -> list[str]:
@ -120,11 +124,8 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]:
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")
if verification.get("verified_addon_files") != EXPECTED_ADDON_FILES:
errors.append("artifact verification did not verify the exact addon file set")
return errors

View file

@ -11,6 +11,7 @@ from pathlib import Path, PurePosixPath
ROOT = Path(__file__).resolve().parents[1]
ADDON_SOURCE = ROOT / "spikes" / "rayforge-jcz-no-device-addon"
UPSTREAM = json.loads(
(ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8")
)
@ -44,6 +45,24 @@ def _check_file(root: Path, record: dict, label: str) -> Path:
return path
def _expected_addon_files() -> dict[PurePosixPath, str]:
return {
PurePosixPath(path.relative_to(ADDON_SOURCE).as_posix()): _digest(
path.read_bytes()
)
for path in sorted(ADDON_SOURCE.rglob("*"))
if path.is_file() and "__pycache__" not in path.parts
}
def _relative_below(path: PurePosixPath, root: PurePosixPath) -> PurePosixPath | None:
root_parts = root.parts
for index in range(len(path.parts) - len(root_parts)):
if path.parts[index : index + len(root_parts)] == root_parts:
return PurePosixPath(*path.parts[index + len(root_parts) :])
return None
def verify(manifest_path: Path) -> dict:
manifest_path = manifest_path.resolve()
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
@ -69,6 +88,23 @@ def verify(manifest_path: Path) -> dict:
packaged_root = PurePosixPath(manifest["addon_packaged_root"])
if packaged_root.is_absolute() or ".." in packaged_root.parts:
raise ValueError("unsafe packaged addon root")
expected_addon = _expected_addon_files()
manifest_addon = {}
for record in manifest.get("addon_source_files", []):
relative = PurePosixPath(record.get("path", ""))
if (
not relative.parts
or relative.is_absolute()
or ".." in relative.parts
or relative in manifest_addon
):
raise ValueError(f"unsafe or duplicate addon source path: {relative}")
manifest_addon[relative] = record.get("sha256", "")
if set(manifest_addon) != set(expected_addon):
raise ValueError("checkpoint addon file list does not match the repository")
for relative, digest in manifest_addon.items():
if digest != expected_addon[relative]:
raise ValueError(f"checkpoint addon source differs from repository: {relative}")
with zipfile.ZipFile(bundle) as archive:
files = {
@ -92,24 +128,19 @@ def verify(manifest_path: Path) -> dict:
if _text_digest_lf(archive.read(files[license_matches[0]])) != expected_license_hash:
raise ValueError("bundled Rayforge license does not match the pinned upstream")
verified_addon_files = 0
for record in manifest["addon_source_files"]:
relative = PurePosixPath(record["path"])
if relative.is_absolute() or ".." in relative.parts:
raise ValueError("unsafe addon source path")
matches = [
path for path in files
if tuple(path.parts[-len((packaged_root / relative).parts):])
== (packaged_root / relative).parts
]
if len(matches) != 1:
raise ValueError(f"packaged addon file missing or ambiguous: {relative}")
digest = record.get("sha256", "")
if not SHA256.fullmatch(digest):
raise ValueError(f"invalid addon SHA-256: {relative}")
if _digest(archive.read(files[matches[0]])) != digest:
packaged_addon = {}
for path in files:
relative = _relative_below(path, packaged_root)
if relative is not None:
if relative in packaged_addon:
raise ValueError(f"packaged addon file is ambiguous: {relative}")
packaged_addon[relative] = path
if set(packaged_addon) != set(expected_addon):
raise ValueError("packaged addon file set does not match the repository")
for relative, digest in expected_addon.items():
if _digest(archive.read(files[packaged_addon[relative]])) != digest:
raise ValueError(f"packaged addon SHA-256 mismatch: {relative}")
verified_addon_files += 1
verified_addon_files = len(packaged_addon)
return {
"status": "passed",

View file

@ -6,6 +6,7 @@ from pathlib import Path
from scripts.prepare_m1_acceptance_kit import (
KIT_FILES,
EXPECTED_ADDON_FILES,
RAYFORGE_COMMIT,
REPORT_TEMPLATE,
ROOT,
@ -24,7 +25,7 @@ class M1AcceptanceKitTests(unittest.TestCase):
"rayforge_commit": RAYFORGE_COMMIT,
"bundle_sha256": "1" * 64,
"installer_sha256": "2" * 64,
"verified_addon_files": 7,
"verified_addon_files": EXPECTED_ADDON_FILES,
}
def test_kit_contains_only_allowlisted_public_inputs(self):

View file

@ -6,6 +6,7 @@ import unittest
from pathlib import Path
from scripts.validate_m1_manual_acceptance import (
EXPECTED_ADDON_FILES,
RAYFORGE_COMMIT,
ROOT,
STEP_IDS,
@ -49,7 +50,7 @@ class M1ManualAcceptanceTests(unittest.TestCase):
"rayforge_commit": RAYFORGE_COMMIT,
"bundle_sha256": "1" * 64,
"installer_sha256": "2" * 64,
"verified_addon_files": 7,
"verified_addon_files": EXPECTED_ADDON_FILES,
}
verification_path = root / "artifact-verification.json"
verification_path.write_text(json.dumps(verification), encoding="utf-8")

View file

@ -5,19 +5,30 @@ import unittest
import zipfile
from pathlib import Path
from scripts.verify_rayforge_windows_checkpoint import ROOT, UPSTREAM, verify
from scripts.verify_rayforge_windows_checkpoint import (
ADDON_SOURCE,
ROOT,
UPSTREAM,
verify,
)
ADDON_ROOT = "rayforge/builtin_addons/rayforge-addon-the-pirate-ray-jcz"
class WindowsCheckpointVerifierTests(unittest.TestCase):
def _checkpoint(self, root: Path, unsafe_member: str | None = None) -> Path:
def _checkpoint(
self,
root: Path,
unsafe_member: str | None = None,
extra_addon_file: str | None = None,
) -> Path:
bundle_name = f"rayforge-vpirateray-{UPSTREAM['commit'][:12]}.zip"
executable = bundle_name.removesuffix(".zip") + ".exe"
addon_files = {
"rayforge-addon.yaml": b"name: the_pirate_ray_jcz\n",
"pirate_ray_jcz/worker.py": b"TRANSPORT = None\n",
path.relative_to(ADDON_SOURCE).as_posix(): path.read_bytes()
for path in sorted(ADDON_SOURCE.rglob("*"))
if path.is_file() and "__pycache__" not in path.parts
}
upstream = json.loads(
(ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8")
@ -29,6 +40,11 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
archive.writestr(f"{bundle_name[:-4]}/{executable}", b"synthetic")
for relative, data in addon_files.items():
archive.writestr(f"{bundle_name[:-4]}/_internal/{ADDON_ROOT}/{relative}", data)
if extra_addon_file:
archive.writestr(
f"{bundle_name[:-4]}/_internal/{ADDON_ROOT}/{extra_addon_file}",
b"unexpected",
)
archive.writestr(
f"{bundle_name[:-4]}/THIRD_PARTY_LICENSES/Rayforge-LICENSE.txt",
license_data,
@ -79,7 +95,11 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
result = verify(self._checkpoint(Path(directory)))
self.assertEqual(result["status"], "passed")
self.assertFalse(result["executed_binaries"])
self.assertEqual(result["verified_addon_files"], 2)
expected_count = sum(
path.is_file() and "__pycache__" not in path.parts
for path in ADDON_SOURCE.rglob("*")
)
self.assertEqual(result["verified_addon_files"], expected_count)
def test_modified_installer_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
@ -112,6 +132,30 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
with self.assertRaises(FileNotFoundError):
verify(manifest)
def test_manifest_cannot_omit_or_change_repository_addon_source(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
manifest = self._checkpoint(Path(directory))
data = json.loads(manifest.read_text(encoding="utf-8"))
data["addon_source_files"].pop()
manifest.write_text(json.dumps(data), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "file list does not match"):
verify(manifest)
manifest = self._checkpoint(Path(directory))
data = json.loads(manifest.read_text(encoding="utf-8"))
data["addon_source_files"][0]["sha256"] = "0" * 64
manifest.write_text(json.dumps(data), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "differs from repository"):
verify(manifest)
def test_unlisted_packaged_addon_file_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
manifest = self._checkpoint(
Path(directory), extra_addon_file="pirate_ray_jcz/transport.py"
)
with self.assertRaisesRegex(ValueError, "file set does not match"):
verify(manifest)
if __name__ == "__main__":
unittest.main()