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 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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue