build: validate packaged artifact identity
This commit is contained in:
parent
9230ec514b
commit
bf53e4f2fc
7 changed files with 93 additions and 16 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue