#!/usr/bin/env python3 """Verify that a signed L2 image embeds the binding it claims.""" from __future__ import annotations import argparse import hashlib import json import re import sys from pathlib import Path SCRIPT_DIRECTORY = Path(__file__).resolve().parent if str(SCRIPT_DIRECTORY) not in sys.path: sys.path.insert(0, str(SCRIPT_DIRECTORY)) from validate_serviceification_bom import ( # noqa: E402 ValidationFailure, canonical_json_bytes, load_bom, ) SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$") def file_digest(path: Path) -> str: return "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest() def verify(role: str, binding: str, label: str, artifact: Path) -> dict[str, str]: errors: list[str] = [] if role not in {"frontend", "resolved_bom"}: errors.append("role must be frontend or resolved_bom") if not SHA256.fullmatch(binding): errors.append("binding must be a canonical sha256 digest") if label != binding: errors.append("artifact label disagrees with the signed binding") try: value = load_bom(artifact) except ValidationFailure as exc: errors.append(str(exc)) value = {} if role == "frontend": if not isinstance(value, dict) or set(value) != { "nexuscrm_repository", "nexuscrm_commit", "app_packs_repository", "app_packs_commit", "l2_repository", "l2_commit", }: errors.append("embedded frontend source tuple has the wrong shape") if file_digest(artifact) != binding: errors.append("embedded frontend source tuple disagrees with the signed binding") elif role == "resolved_bom": if value.get("desired_bom_sha256") != binding: errors.append("embedded resolved BOM disagrees with the signed binding") if errors: raise ValidationFailure("; ".join(errors)) canonical = canonical_json_bytes(value) if artifact.read_bytes() != canonical: raise ValidationFailure("embedded artifact JSON is not canonical") return { "schema_version": "nexuscrm.l2-artifact-binding-evidence/v1", "subject_role": role, "binding_sha256": binding, "artifact_file_sha256": file_digest(artifact), } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--role", required=True, choices=("frontend", "resolved_bom")) parser.add_argument("--binding", required=True) parser.add_argument("--label", required=True) parser.add_argument("--artifact", required=True) parser.add_argument("--output", required=True) args = parser.parse_args() try: result = verify(args.role, args.binding, args.label, Path(args.artifact).resolve()) except (OSError, ValidationFailure) as exc: print(f"serviceification L2 artifact verification: {exc}", file=sys.stderr) return 1 output = Path(args.output).resolve() output.parent.mkdir(parents=True, exist_ok=True) output.write_bytes(canonical_json_bytes(result)) return 0 if __name__ == "__main__": raise SystemExit(main())