#!/usr/bin/env python3 """Verify the signed AppRelease/BOM -> L2 metadata tree chain. The serviceified deployment has three independent release authorities: * the L0 Engine release owns the generic runtime; * the signed AppRelease owns L1 CRM metadata provenance; and * the signed BOM owns the exact L2 metadata checkout and tree. Build provenance is read from the final-pack contract. It does not duplicate the signed authorities or make an L2 release constrain an independently released L0 service commit. """ from __future__ import annotations import argparse import hashlib import json import re import subprocess from pathlib import Path from typing import Any try: from .lib.final_pack_contract import metadata_tree_sha256 from . import validate_serviceification_bom except ImportError: # pragma: no cover - direct script execution from lib.final_pack_contract import metadata_tree_sha256 import validate_serviceification_bom class VerificationFailure(RuntimeError): pass def load_object(path: Path, label: str) -> dict[str, Any]: try: value = json.loads( path.read_text(), object_pairs_hook=validate_serviceification_bom.reject_duplicate_keys, ) except (OSError, json.JSONDecodeError, validate_serviceification_bom.ValidationFailure) as exc: raise VerificationFailure(f"cannot read {label} {path}: {exc}") from exc if not isinstance(value, dict): raise VerificationFailure(f"{label} must be an object: {path}") return value def require_object(value: Any, label: str) -> dict[str, Any]: if not isinstance(value, dict): raise VerificationFailure(f"{label} must be an object") return value def require_equal(actual: Any, expected: Any, label: str) -> None: if actual != expected: raise VerificationFailure(f"{label} mismatch: got {actual!r}, want {expected!r}") def require_relative_path(value: Any, label: str) -> Path: path = Path(str(value or "")) if not path.parts or path.is_absolute() or ".." in path.parts: raise VerificationFailure(f"{label} must stay inside its exact checkout") return path def require_matching_string(value: Any, pattern: str, label: str) -> str: if not isinstance(value, str) or re.fullmatch(pattern, value) is None: raise VerificationFailure(f"{label} is malformed") return value def require_clean_checkout(root: Path, expected_commit: Any, label: str) -> None: checkout = subprocess.run( ["git", "-C", str(root), "rev-parse", "HEAD"], check=True, capture_output=True, text=True, ).stdout.strip() require_equal(checkout, expected_commit, f"{label} checkout") dirty = subprocess.run( ["git", "-C", str(root), "status", "--porcelain", "--untracked-files=all"], check=True, capture_output=True, text=True, ).stdout if dirty: raise VerificationFailure(f"exact {label} checkout is dirty") def verify_metadata_authority( bom: dict[str, Any], l2_root: Path, app_packs_root: Path, ) -> None: if bom.get("schema_version") == validate_serviceification_bom.LOCAL_TEST3_DEPLOYMENT_SCHEMA_VERSION: errors = validate_serviceification_bom.validate_deployment_bom_view(bom) if errors: raise VerificationFailure("invalid local Test3 deployment: " + "; ".join(errors)) bom = validate_serviceification_bom.deployment_bom_view(bom) l2_root = l2_root.resolve() app_packs_root = app_packs_root.resolve() metadata = require_object(bom.get("l2_metadata"), "l2_metadata") installation = require_object(bom.get("app_installation"), "app_installation") artifact_locator = require_object( installation.get("artifact"), "app_installation.artifact", ) attestation_locator = require_object( installation.get("publisher_attestation"), "app_installation.publisher_attestation", ) require_clean_checkout(l2_root, metadata.get("commit"), "L2 metadata") require_clean_checkout( app_packs_root, artifact_locator.get("commit"), "AppRelease publication", ) require_equal( attestation_locator.get("commit"), artifact_locator.get("commit"), "AppRelease locator commit", ) pack_path = require_relative_path(metadata.get("path"), "l2_metadata.path") pack_root = l2_root / pack_path lineage_path = pack_root / ".build-lineage.json" lineage = load_object(lineage_path, "final-pack build lineage") lineage_official = require_object(lineage.get("officialPack"), "lineage officialPack") lineage_downstream = require_object(lineage.get("downstream"), "lineage downstream") lineage_assembly = require_object(lineage.get("assembly"), "lineage assembly") attestation_path = require_relative_path( attestation_locator.get("path"), "app_installation.publisher_attestation.path", ) attestation_file = app_packs_root / attestation_path attestation_digest = "sha256:" + hashlib.sha256(attestation_file.read_bytes()).hexdigest() require_equal( attestation_digest, attestation_locator.get("sha256"), "signed AppRelease publisher attestation", ) attestation = load_object(attestation_file, "AppRelease publisher attestation") provenance = require_object(attestation.get("provenance"), "AppRelease provenance") require_equal( provenance.get("schema_version"), "nexuscrm.app-package-provenance/v1", "AppRelease provenance schema", ) require_matching_string( provenance.get("source_repository"), r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", "AppRelease provenance source_repository", ) require_matching_string( provenance.get("source_revision"), r"[0-9a-f]{40}", "AppRelease provenance source_revision", ) require_relative_path( provenance.get("source_path"), "AppRelease provenance source_path", ) require_matching_string( provenance.get("source_tree_digest"), r"sha256:[0-9a-f]{64}", "AppRelease provenance source_tree_digest", ) require_matching_string( provenance.get("builder_id"), r"\S+", "AppRelease provenance builder_id", ) require_matching_string( provenance.get("contract_module_version"), r"v\S+", "AppRelease provenance contract_module_version", ) actual_tree = metadata_tree_sha256(pack_root) require_equal(actual_tree, metadata.get("tree_sha256"), "signed metadata tree") require_equal(lineage_assembly.get("finalPackPath"), pack_path.as_posix(), "lineage final-pack path") require_equal( lineage_downstream.get("repo"), metadata.get("repository"), "lineage L2 repository", ) if bom.get("schema_version") in { "nexuscrm.serviceification-bom/v10", "nexuscrm.serviceification-resolved-bom/v10", validate_serviceification_bom.LOCAL_TEST3_DEPLOYMENT_SCHEMA_VERSION, }: # The v10 L2 metadata artifact is a final merged pack, not an # overlay-only payload: it physically republishes the selected L1 # standard pack. The AppRelease and that embedded L1 source must # therefore come from the same exact publication checkout. Allowing # independent revisions creates a half-upgrade in which the AppRelease # requires components absent from the final pack; final distribution # verification then fails only after Catalog mutation. Keep L0 release # provenance independent, but bind this duplicated L1 payload at the # v10 producer and pre-mutation deployment boundaries. Historical # schemas retain their established independent-revision compatibility # contract. require_equal( lineage_official.get("repo"), artifact_locator.get("repository"), "lineage official pack repository", ) require_equal( lineage_official.get("ref"), artifact_locator.get("commit"), "lineage official pack revision", ) require_equal( lineage_official.get("packPath"), provenance.get("source_path"), "lineage official pack path", ) else: for field in ("repo", "ref", "packPath"): value = lineage_official.get(field) if not isinstance(value, str) or not value: raise VerificationFailure( f"lineage officialPack.{field} must be non-empty" ) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--bom", required=True) parser.add_argument("--l2-root", required=True) parser.add_argument("--app-packs-root", required=True) parser.add_argument( "--require-desired", action="store_true", help="verify a checked-in desired BOM instead of a deployable resolved BOM", ) args = parser.parse_args() try: bom = validate_serviceification_bom.load_bom(Path(args.bom).resolve()) if args.require_desired: errors = validate_serviceification_bom.validate_bom( bom, require_desired=True, ) if errors: raise VerificationFailure("invalid desired BOM: " + "; ".join(errors)) verify_metadata_authority( bom, Path(args.l2_root), Path(args.app_packs_root), ) except ( OSError, subprocess.CalledProcessError, validate_serviceification_bom.ValidationFailure, VerificationFailure, ) as exc: print(f"D1 metadata authority verification failed: {exc}") return 1 print("D1 metadata authority verified") return 0 if __name__ == "__main__": raise SystemExit(main())