121 lines
4.6 KiB
Python
121 lines
4.6 KiB
Python
"""Verify a packaged no-device checkpoint without executing its binaries."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import zipfile
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UPSTREAM = json.loads(
|
|
(ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8")
|
|
)
|
|
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
|
|
|
|
def _digest(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def _artifact_path(root: Path, value: str) -> Path:
|
|
relative = PurePosixPath(value)
|
|
if relative.is_absolute() or ".." in relative.parts or len(relative.parts) != 1:
|
|
raise ValueError(f"artifact path must be one filename: {value!r}")
|
|
return root / relative.name
|
|
|
|
|
|
def _check_file(root: Path, record: dict, label: str) -> Path:
|
|
path = _artifact_path(root, record["path"])
|
|
data = path.read_bytes()
|
|
if record.get("bytes") != len(data):
|
|
raise ValueError(f"{label} size mismatch")
|
|
digest = record.get("sha256", "")
|
|
if not SHA256.fullmatch(digest) or digest != _digest(data):
|
|
raise ValueError(f"{label} SHA-256 mismatch")
|
|
return path
|
|
|
|
|
|
def verify(manifest_path: Path) -> dict:
|
|
manifest_path = manifest_path.resolve()
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
|
if manifest.get("schema_version") != 1:
|
|
raise ValueError("unsupported checkpoint schema")
|
|
if manifest.get("rayforge_commit") != UPSTREAM["commit"]:
|
|
raise ValueError("Rayforge commit does not match the repository pin")
|
|
if manifest.get("hardware_access") is not False or manifest.get("transport") is not None:
|
|
raise ValueError("checkpoint does not declare a no-device boundary")
|
|
if manifest.get("manual_interaction_required") is not True:
|
|
raise ValueError("checkpoint must retain the manual interaction gate")
|
|
|
|
artifact_root = manifest_path.parent
|
|
bundle = _check_file(artifact_root, manifest["bundle"], "bundle")
|
|
installer = _check_file(artifact_root, manifest["installer"], "installer")
|
|
packaged_root = PurePosixPath(manifest["addon_packaged_root"])
|
|
if packaged_root.is_absolute() or ".." in packaged_root.parts:
|
|
raise ValueError("unsafe packaged addon root")
|
|
|
|
with zipfile.ZipFile(bundle) as archive:
|
|
files = {
|
|
PurePosixPath(info.filename): info
|
|
for info in archive.infolist()
|
|
if not info.is_dir()
|
|
}
|
|
for path in files:
|
|
if path.is_absolute() or ".." in path.parts:
|
|
raise ValueError("bundle contains an unsafe path")
|
|
executable = manifest["bundle"]["path"].removesuffix(".zip") + ".exe"
|
|
if not any(path.name == executable for path in files):
|
|
raise ValueError("bundle application executable is missing")
|
|
|
|
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:
|
|
raise ValueError(f"packaged addon SHA-256 mismatch: {relative}")
|
|
verified_addon_files += 1
|
|
|
|
return {
|
|
"status": "passed",
|
|
"rayforge_commit": manifest["rayforge_commit"],
|
|
"hardware_access": False,
|
|
"executed_binaries": False,
|
|
"bundle_sha256": manifest["bundle"]["sha256"],
|
|
"installer_sha256": manifest["installer"]["sha256"],
|
|
"verified_addon_files": verified_addon_files,
|
|
"manual_interaction_required": True,
|
|
"installer_name": installer.name,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("manifest", type=Path)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
result = verify(args.manifest)
|
|
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
|
|
if args.output:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(rendered, encoding="utf-8", newline="\n")
|
|
print(rendered, end="")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|