build: validate packaged artifact identity

This commit is contained in:
Codex 2026-10-21 18:29:36 +02:00
parent 9230ec514b
commit bf53e4f2fc
7 changed files with 93 additions and 16 deletions

View file

@ -19,7 +19,8 @@ The repository now provides:
- a non-executing verifier for externally produced Windows artifacts; - a non-executing verifier for externally produced Windows artifacts;
- a checksummed acceptance-kit generator containing only allowlisted public - a checksummed acceptance-kit generator containing only allowlisted public
synthetic inputs, with provenance binding to the repository commit and 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; - a fail-closed manual desktop-acceptance report and validator;
- pinned upstream contract/license manifests and a drift checker; - pinned upstream contract/license manifests and a drift checker;
- a fail-closed API-23 adapter isolating Rayforge's internal driver registry; - a fail-closed API-23 adapter isolating Rayforge's internal driver registry;

View file

@ -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 LightBurn evidence or any machine data. Acceptance-kit schema 2 binds the
generated manual report to the kit manifest, repository commit and copied generated manual report to the kit manifest, repository commit and copied
artifact-verification report by SHA-256. Editing, replacing or referring to an 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. 1. Pass the non-executing artifact verifier above.
2. Launch the portable build with a fresh application-data directory. 2. Launch the portable build with a fresh application-data directory.

View file

@ -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 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 test run. Its schema-2 provenance chain binds the editable manual report to the
exact kit manifest, repository commit and non-executing artifact-verification 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. The pinned upstream contracts and license notices are now machine-verifiable.
Rayforge's documented addon lifecycle is viable, but production driver Rayforge's documented addon lifecycle is viable, but production driver
registration still needs a supported symmetric registry API. The current registration still needs a supported symmetric registry API. The current

View file

@ -5,12 +5,17 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import json import json
import re
import shutil import shutil
import subprocess import subprocess
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] 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" REPORT_TEMPLATE = "tests/fixtures/m1-manual-acceptance-template.json"
KIT_FILES = { KIT_FILES = {
"inputs/synthetic-mopa-import.lbrn2": "tests/fixtures/synthetic-mopa-import.lbrn2", "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("hardware_access") is not False
or verification.get("executed_binaries") is not False or verification.get("executed_binaries") is not False
or verification.get("manual_interaction_required") is not True 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") raise ValueError("artifact-verification report did not pass the no-device gate")

View file

@ -10,6 +10,9 @@ from pathlib import Path, PurePosixPath
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
RAYFORGE_COMMIT = json.loads(
(ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8")
)["commit"]
STEP_IDS = ( STEP_IDS = (
"artifact_verified", "artifact_verified",
"isolated_portable_launch", "isolated_portable_launch",
@ -111,6 +114,17 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]:
errors.append("artifact verification executed binaries") errors.append("artifact verification executed binaries")
if verification.get("manual_interaction_required") is not True: if verification.get("manual_interaction_required") is not True:
errors.append("artifact verification dropped the manual interaction gate") 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 return errors

View file

@ -6,6 +6,7 @@ from pathlib import Path
from scripts.prepare_m1_acceptance_kit import ( from scripts.prepare_m1_acceptance_kit import (
KIT_FILES, KIT_FILES,
RAYFORGE_COMMIT,
REPORT_TEMPLATE, REPORT_TEMPLATE,
ROOT, ROOT,
create_kit, create_kit,
@ -13,6 +14,19 @@ from scripts.prepare_m1_acceptance_kit import (
class M1AcceptanceKitTests(unittest.TestCase): 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): def test_kit_contains_only_allowlisted_public_inputs(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
output = Path(directory) / "kit" output = Path(directory) / "kit"
@ -70,12 +84,9 @@ class M1AcceptanceKitTests(unittest.TestCase):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory) root = Path(directory)
verification = root / "verification.json" verification = root / "verification.json"
verification.write_text(json.dumps({ verification.write_text(
"status": "passed", json.dumps(self._verification()), encoding="utf-8"
"hardware_access": False, )
"executed_binaries": False,
"manual_interaction_required": True,
}), encoding="utf-8")
output = root / "kit" output = root / "kit"
create_kit( create_kit(
output, output,
@ -95,12 +106,24 @@ class M1AcceptanceKitTests(unittest.TestCase):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory) root = Path(directory)
verification = root / "verification.json" verification = root / "verification.json"
verification.write_text(json.dumps({ verification.write_text(
"status": "failed", json.dumps(self._verification(status="failed")), encoding="utf-8"
"hardware_access": False, )
"executed_binaries": False, with self.assertRaisesRegex(ValueError, "did not pass"):
"manual_interaction_required": True, create_kit(
}), encoding="utf-8") 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"): with self.assertRaisesRegex(ValueError, "did not pass"):
create_kit( create_kit(
root / "kit", root / "kit",

View file

@ -5,7 +5,12 @@ import tempfile
import unittest import unittest
from pathlib import Path 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" TEMPLATE = ROOT / "tests" / "fixtures" / "m1-manual-acceptance-template.json"
@ -41,6 +46,10 @@ class M1ManualAcceptanceTests(unittest.TestCase):
"hardware_access": False, "hardware_access": False,
"executed_binaries": False, "executed_binaries": False,
"manual_interaction_required": True, "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 = root / "artifact-verification.json"
verification_path.write_text(json.dumps(verification), encoding="utf-8") verification_path.write_text(json.dumps(verification), encoding="utf-8")
@ -136,6 +145,23 @@ class M1ManualAcceptanceTests(unittest.TestCase):
self.assertEqual(result["status"], "failed") self.assertEqual(result["status"], "failed")
self.assertIn("acceptance-kit repository commit mismatch", result["errors"]) 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__": if __name__ == "__main__":
unittest.main() unittest.main()