#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Validate the offline-only Phase-0.9E bootstrap provenance audit.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path import re import subprocess from typing import Any import zipfile EXPECTED_BASELINE = "e5304e394efb0790e1939bef87404f19b58caa43" EXPECTED_BRANCH = "codex/chimera-gfx-phase09e-bootstrap-rescue-provenance" EXPECTED_PHASE = "PHASE_0_9E_BOOTSTRAP_RESCUE_PROVENANCE" EXPECTED_STATUS = "DESIGN_ONLY" EXPECTED_CLASSIFICATION = "BOOTSTRAP_IMPLEMENTATION_MISSING" BLOCKED_HASH = ( "4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63" ) DENYLIST_HASH = ( "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783" ) OUTER_NAME = "Y2JB-Autoloader-403-1240.zip" OUTER_SIZE = 504159435 OUTER_SHA256 = ( "805e3f87f0c371223619ffc7d3a7b3c0d41a1fae8a8b1171d9e2f162659e8291" ) INNER_NAME = "PS5/EXPORT/BACKUP/202606102126_00/archive.dat" INNER_SIZE = 504365056 INNER_SHA256 = ( "6439834e8856d45b6d6fe699b74c35ca6985a199ea8ecf3e398c018d37be2d55" ) ZIP_ENTRIES = ( "PS5/", "PS5/EXPORT/", "PS5/EXPORT/BACKUP/", "PS5/EXPORT/BACKUP/202606102126_00/", INNER_NAME, ) AUTHORIZATION_FIELDS = ( "device_action_authorized", "execution_authorized", "installation_authorized", "lifecycle_authorized", "autoload_authorized", "device_write_authorized", "transfer_authorized", "automatic_retry", "target_build_authorized", "rescue_payload_design_authorized", ) ACTION_FIELDS = ( "internet_used", "fetch_performed", "clone_performed", "package_installed", "ps5_connected", "device_request_performed", "files_transferred", "device_write_performed", "target_execution_performed", "target_build_performed", "target_artifact_created", "rescue_payload_created", "readback_payload_created", "device_client_created", "backup_created", "staging_performed", ) PROVENANCE_FIELDS = ( "logical_name", "artifact_role", "local_relative_path", "size", "sha256", "file_type", "source_repository", "source_commit", "build_identity", "version", "obtained_from", "evidence_that_it_is_deployed_or_used", "confidence", "immutable", "executable", "persistent_on_device", "transferred_per_session", "required_for_bootstrap", "required_for_recovery", ) SOURCE_COMMITS = { "hardened_elfldr": ( "../chimera-elfldr", "197623058f509eddde18868dafcb92fdcac66464", ), "controlled_payload_manager": ( "../chimera-ps5-payload-manager", "e23d94ff91233aa770e2342800c1467875bdef44", ), "elfldr_public_base": ( "work/upstream/elfldr-v0.23", "699e8bcff03e91e8d6ca6eba281af25c5a58d8c2", ), "payload_manager_public_base": ( "work/upstream/pldmgr-v0.3.1", "cfbc70f30f419b09bf2b52283f7409e2d3117ee1", ), "ps5_payload_sdk_v0_41": ( "work/upstream/sdk", "d2e2e585740362976a39fdd5ccf390f199a7bc37", ), } SOURCE_HASHES = { "../chimera-elfldr/elfldr-ps5.elf": ( 397000, "63e810982471eb40cae3a20aa9df9a0a02892f420e429874fae4e99aa400b561", ), "../chimera-elfldr/elfldr-ps5.elf.map": ( 142905, "2ea5ff73299db6e61d5fd998c358ee910bc4e8fa1e6e7c73dbaa842bc813124f", ), "../chimera-elfldr/bootstrap.elf.map": ( 143081, "ec4ae17abeb8270948d8a7d4196593ba94cbdb69376b8b9f7f009b25ab5b3323", ), "../chimera-elfldr/socksrv.elf.map": ( 145072, "bf74db6ee68a60652426936e7037bb5a143078f44cc7774fa04c9bc4bc5447c1", ), "../chimera-elfldr/Makefile": ( 1902, "3df93b48fc61ec67907b136e44ea2085baacf08347864d32befc072a7a783384", ), "../chimera-elfldr/README.md": ( 2821, "372aeb28dc971b2bd98093a47fdaf77c32f75bbdc3b3d7e8678900744b91eadb", ), "../chimera-elfldr/main.c": ( 2473, "876389a26999073994e63ca29926982280d9594a1ee941244205b54f57e2b4d1", ), "../chimera-elfldr/bootstrap.c": ( 2081, "5a8072ec0d6db919cb3a81a7028dc91fd8e2c3a0d69b0fa7e84836fa93b45381", ), "../chimera-elfldr/socksrv.c": ( 11556, "d642ced3e9b4a296dd15e355050ebe956f53a6dfdaa6ac10109cd067a3bba3d7", ), "work/upstream/release-assets/elfldr-ps5-v0.23.elf": ( 397000, "092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8", ), "../chimera-ps5-payload-manager/pldmgr-controlled.elf": ( 99560, "8fecf8241a46246eddbd21e8bb4d875f5d76f1f4f4c6a11384df1f131aa5e5b1", ), } IMMUTABLE_HASHES = { "manifests/runtime/phase-0.8-read-only-preflight.json": ( "47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322" ), "manifests/runtime/phase-0.8-remediation.json": ( "a9dafed8c83722c43709dcf90ea117c21d6f996ff8fe233810ed08aa20cbe071" ), "manifests/runtime/phase-0.9-anti-brick-design.json": ( "39fd7c70cae998d9d74e7caf1ff3c19f9f76f7de2e5a50bc09baede4ba53e9e9" ), "manifests/runtime/phase-0.9b-observer.json": ( "104c4a667ad17f9827fc7276852c6faeef96effb8e3561a5ebc19a62c7d51634" ), "manifests/runtime/phase-0.9c-feasibility.json": ( "84eb737ff3486d0c5d8b5ecf06809f93bc573497da4919595b4c1817fd34247c" ), "manifests/runtime/phase-0.9d-existing-stack-readback.json": ( "86e5aaf034685dbe058b71ffeec645b682f0a8cc7d249e8ac0397155233991de" ), } DELIVERABLES = ( "docs/runtime/phase-0.9e-bootstrap-provenance.md", "docs/runtime/phase-0.9e-independent-rescue-chain.md", "docs/runtime/phase-0.9e-loader-9020-protocol.md", "docs/runtime/phase-0.9e-reboot-and-crash-model.md", "docs/runtime/phase-0.9e-future-rescue-payload-contract.md", "docs/runtime/phase-0.9e-output-architecture-options.md", "manifests/runtime/phase-0.9e-bootstrap-provenance.json", "manifests/runtime/phase-0.9e-bootstrap-provenance.schema.json", "manifests/runtime/phase-0.9e-loader-protocol.json", "manifests/runtime/phase-0.9e-loader-protocol.schema.json", "tools/validate_phase09e_bootstrap.py", "tests/test_phase09e_bootstrap.py", "packaging/phase09e/SHA256SUMS.txt", ) FORBIDDEN_NEW_SUFFIXES = { ".c", ".cc", ".cpp", ".s", ".asm", ".ld", ".elf", ".self", ".sprx", ".pkg", ".bin", } FORBIDDEN_PRODUCTION_PREFIXES = ( "include/", "src/", "adapters/", "samples/", ) def load_json(path: Path) -> dict[str, Any]: with path.open("r", encoding="utf-8") as stream: value = json.load(stream) if not isinstance(value, dict): raise ValueError(f"{path} does not contain an object") return value def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def candidate_archive_path(root: Path) -> Path: """Resolve the same Windows user Downloads path from Windows or WSL.""" windows_home_candidate = Path.home() / "Downloads" / OUTER_NAME if windows_home_candidate.is_file(): return windows_home_candidate for ancestor in (root, *root.parents): if ancestor.parent.name.lower() == "users": return ancestor / "Downloads" / OUTER_NAME return windows_home_candidate def git(root: Path, *args: str) -> str: result = subprocess.run( ["git", *args], cwd=root, check=False, capture_output=True, text=True, ) if result.returncode != 0: raise RuntimeError(result.stderr.strip() or "git command failed") return result.stdout.strip() def provenance_errors(artifact: dict[str, Any]) -> list[str]: errors: list[str] = [] for field in PROVENANCE_FIELDS: if field not in artifact: errors.append(f"missing provenance field {field}") confidence = artifact.get("confidence") if confidence not in { "EXACT_USED", "STRONG_MATCH", "POSSIBLE", "REFERENCE_ONLY", "UNKNOWN", }: errors.append("invalid confidence") obtained = str(artifact.get("obtained_from", "")).lower() repository = str(artifact.get("source_repository") or "").lower() if ( "public" in obtained or "upstream" in obtained or "upstream" in repository ) and confidence == "EXACT_USED": errors.append("public upstream cannot be exact-used without provenance") if ( artifact.get("source_repository") is None and artifact.get("build_identity") is None and artifact.get("source_commit") is not None ): errors.append("opaque binary cannot carry an invented source commit") if not re.fullmatch(r"[0-9a-f]{64}", str(artifact.get("sha256", ""))): errors.append("invalid SHA-256") if not isinstance(artifact.get("size"), int) or artifact.get("size", -1) < 0: errors.append("invalid artifact size") return errors def rescue_classification( *, actual_package_available: bool, requires_elfldr: bool = False, requires_payload_manager: bool = False, replaces_live_component: bool = False, all_required_properties_proven: bool = False, ) -> str: if not actual_package_available: return "BOOTSTRAP_IMPLEMENTATION_MISSING" if requires_elfldr or requires_payload_manager: return "SELF_OR_CROSS_DEPENDENT" if replaces_live_component: return "NO_INDEPENDENT_RESCUE_PATH" if all_required_properties_proven: return "INDEPENDENT_RESCUE_EXECUTOR_CANDIDATE" return "PARTIAL_RESCUE_EXECUTOR" def host_to_memory_classification( *, receive_code: bool, mapping_code: bool, entrypoint_code: bool, device_file_only: bool = False, ) -> str: if device_file_only: return "DEVICE_FILE_ONLY" if receive_code and mapping_code and entrypoint_code: return "PROVEN_FROM_SOURCE" if receive_code or mapping_code or entrypoint_code: return "PARTIAL" return "UNKNOWN" def protocol_model_complete(protocol: dict[str, Any]) -> bool: required = ( "maximum_payload_size", "headers", "length_fields", "short_read_detection", "short_write_detection", "bounds_checks", ) return all(protocol.get(field) not in {None, "", "UNKNOWN"} for field in required) def risk_classification( *, temporary_socket: bool = False, live_filesystem_write: bool = False, autoload_activation: bool = False, ) -> str: if live_filesystem_write or autoload_activation: return "BRICK_RELEVANT" if temporary_socket: return "LOW_TECHNICAL" return "UNKNOWN" def reboot_classification( *, exact_package: bool, source_design_restartable: bool, hardware_observed: bool, ) -> str: if exact_package and source_design_restartable and hardware_observed: return "REBOOT_RECOVERY_SUPPORTED_BY_DESIGN" if exact_package and source_design_restartable: return "REBOOT_RECOVERY_PLAUSIBLE" return "REBOOT_RECOVERY_UNPROVEN" def phase09f_design_allowed( *, actual_package_available: bool, independent_from_elfldr: bool, independent_from_payload_manager: bool, no_live_replacement: bool, ) -> bool: return all( ( actual_package_available, independent_from_elfldr, independent_from_payload_manager, no_live_replacement, ) ) def _type_matches(value: Any, expected: str) -> bool: return { "object": isinstance(value, dict), "array": isinstance(value, list), "string": isinstance(value, str), "boolean": isinstance(value, bool), "integer": isinstance(value, int) and not isinstance(value, bool), "number": isinstance(value, (int, float)) and not isinstance(value, bool), "null": value is None, }.get(expected, True) def validate_schema_instance( schema: dict[str, Any], instance: Any, path: str = "$" ) -> list[str]: """Small offline validator for the schema features used by this phase.""" errors: list[str] = [] expected_type = schema.get("type") if expected_type and not _type_matches(instance, expected_type): return [f"{path}: expected {expected_type}"] if "const" in schema and instance != schema["const"]: errors.append(f"{path}: expected constant {schema['const']!r}") if "enum" in schema and instance not in schema["enum"]: errors.append(f"{path}: value is outside enum") pattern = schema.get("pattern") if pattern and isinstance(instance, str) and not re.fullmatch(pattern, instance): errors.append(f"{path}: pattern mismatch") if isinstance(instance, dict): for key in schema.get("required", []): if key not in instance: errors.append(f"{path}: missing {key}") properties = schema.get("properties", {}) for key, value in instance.items(): if key in properties: errors.extend( validate_schema_instance(properties[key], value, f"{path}.{key}") ) elif schema.get("additionalProperties") is False: errors.append(f"{path}: unexpected property {key}") elif isinstance(schema.get("additionalProperties"), dict): errors.extend( validate_schema_instance( schema["additionalProperties"], value, f"{path}.{key}" ) ) if isinstance(instance, list): if "maxItems" in schema and len(instance) > schema["maxItems"]: errors.append(f"{path}: too many items") item_schema = schema.get("items") if isinstance(item_schema, dict): for index, item in enumerate(instance): errors.extend( validate_schema_instance(item_schema, item, f"{path}[{index}]") ) return errors def validate_manifest(manifest: dict[str, Any]) -> list[str]: errors: list[str] = [] expected_top = { "phase": EXPECTED_PHASE, "status": EXPECTED_STATUS, "baseline_commit": EXPECTED_BASELINE, "branch": EXPECTED_BRANCH, } for field, expected in expected_top.items(): if manifest.get(field) != expected: errors.append(f"{field} changed") for field in AUTHORIZATION_FIELDS: if manifest.get("authorization", {}).get(field) is not False: errors.append(f"authorization.{field} must be false") for field in ACTION_FIELDS: if manifest.get("actions", {}).get(field) is not False: errors.append(f"actions.{field} must be false") canonical = manifest.get("canonical_state", {}) expected_canonical = { "phase09d_decision": "BLOCKED_NO_READBACK_PATH", "phase09c_new_observer": "BLOCKED_MULTIPLE_FOUNDATIONAL_CONTRACTS", "payload_manager_backup": "HARD_BLOCKER_FOR_INSTALLATION", "independent_recovery": "UNPROVEN", "firmware_runtime_behavior": "UNPROVEN", "permanent_denylist_sha256": DENYLIST_HASH, "permanently_blocked_artifact_sha256": BLOCKED_HASH, } for field, expected in expected_canonical.items(): if canonical.get(field) != expected: errors.append(f"canonical_state.{field} changed") decisions = manifest.get("decisions", {}) expected_decisions = { "actual_bootstrap_package_available": False, "actual_bootstrap_identity": None, "bootstrap_provenance": "POSSIBLE", "independent_from_elfldr": "unproven", "independent_from_payload_manager": "unproven", "restartable_after_reboot": "unproven", "host_to_memory": "UNKNOWN", "output_channel_for_future_rescue": "UNKNOWN", "independent_rescue_classification": EXPECTED_CLASSIFICATION, "phase09f_rescue_payload_design_allowed": False, "device_action_authorized": False, "execution_authorized": False, "installation_authorized": False, "automatic_retry": False, } if decisions != expected_decisions: errors.append("decision matrix changed") if rescue_classification(actual_package_available=False) != ( EXPECTED_CLASSIFICATION ): errors.append("missing implementation is misclassified") for name, (_, expected) in SOURCE_COMMITS.items(): if manifest.get("source_commits", {}).get(name) != expected: errors.append(f"source commit changed: {name}") if manifest.get("source_tree_status", {}).get(name) != "clean": errors.append(f"source tree not recorded clean: {name}") audit = manifest.get("candidate_archive_audit", {}) expected_audit = { "outer_size": OUTER_SIZE, "outer_sha256": OUTER_SHA256, "zip_entry_count": len(ZIP_ENTRIES), "inner_path": INNER_NAME, "inner_size": INNER_SIZE, "inner_sha256": INNER_SHA256, "inner_magic_ascii": "SIECAF", "acceptable_local_parser_present": False, "decryption_attempted": False, "source_identity_proven": False, "deployed_use_proven": False, "classification": "POSSIBLE", } for field, expected in expected_audit.items(): if audit.get(field) != expected: errors.append(f"candidate_archive_audit.{field} changed") artifacts = manifest.get("artifacts", []) if len(artifacts) != 13: errors.append("artifact inventory must contain 13 records") seen: set[str] = set() for artifact in artifacts: name = str(artifact.get("logical_name")) if name in seen: errors.append(f"duplicate artifact: {name}") seen.add(name) errors.extend(f"{name}: {error}" for error in provenance_errors(artifact)) if artifact.get("confidence") == "EXACT_USED": errors.append(f"{name}: no exact-used artifact is proven") if manifest.get("exact_used_artifacts") != []: errors.append("exact_used_artifacts must remain empty") host_memory = manifest.get("host_to_memory_analysis", {}) if ( host_memory.get("classification") != "UNKNOWN" or host_memory.get("receive_code") != "MISSING" or host_memory.get("mapping_code") != "MISSING" or host_memory.get("entrypoint_selection") != "UNKNOWN" ): errors.append("host-to-memory is overclaimed") reboot = manifest.get("reboot_and_crash", {}) if ( reboot.get("classification") != "REBOOT_RECOVERY_UNPROVEN" or reboot.get("automatic_retry") is not False ): errors.append("reboot recovery is overclaimed or retry enabled") finding_classes = { item.get("id"): item.get("classification") for item in manifest.get("security_findings", []) } if finding_classes.get("unknown_live_filesystem_effect") != "BRICK_RELEVANT": errors.append("unknown live filesystem effect is not brick relevant") if finding_classes.get("unknown_autoload_effect") != "BRICK_RELEVANT": errors.append("unknown autoload effect is not brick relevant") if finding_classes.get( "temporary_socket_not_automatically_brick_relevant" ) == "BRICK_RELEVANT": errors.append("temporary socket is automatically overclassified") contract = manifest.get("future_rescue_contract", {}) if ( contract.get("requirements_total") != 20 or contract.get("bootstrap_supported_now") != [] or contract.get("target_source_allowed") is not False ): errors.append("future rescue contract is not fully blocked") if len(manifest.get("output_options", [])) != 5: errors.append("output architecture options are incomplete") if len(manifest.get("missing_actual_files", [])) < 5: errors.append("missing actual bootstrap inputs are incomplete") final = manifest.get("final_decision", {}) if final.get("classification") != EXPECTED_CLASSIFICATION: errors.append("final decision changed") return errors def validate_protocol(protocol_manifest: dict[str, Any]) -> list[str]: errors: list[str] = [] expected = { "phase": "PHASE_0_9E_LOADER_9020_PROTOCOL", "status": "UNPROVEN_IMPLEMENTATION_MISSING", "exact_server_source_present": False, "exact_server_binary_present": False, "exact_host_client_present": False, "protocol_identity": None, "classification": "CONCEPTUAL_9020_DESCRIPTION_IS_NOT_PROTOCOL_PROOF", } for field, value in expected.items(): if protocol_manifest.get(field) != value: errors.append(f"protocol {field} changed") protocol = protocol_manifest.get("protocol", {}) if protocol.get("port") != 9020: errors.append("protocol port changed") if protocol_model_complete(protocol): errors.append("missing implementation cannot yield a complete protocol") for field in ( "handshake", "length_fields", "maximum_payload_size", "timeout", "retry", "response_direction", "parser", "mappings", "entrypoint_validation", "filesystem_staging", "cleanup", ): if protocol.get(field) not in {"UNKNOWN", "MISSING"}: errors.append(f"protocol.{field} is overclaimed") host_model = protocol_manifest.get("host_model", {}) for field in ( "allowed", "created", "network_port_opened", "device_connection_performed", "payload_sent", "binary_executed", ): if host_model.get(field) is not False: errors.append(f"host_model.{field} must be false") return errors def _changed_paths(root: Path) -> set[str]: paths = { path for path in git(root, "diff", "--name-only", EXPECTED_BASELINE).splitlines() if path } status = git(root, "status", "--porcelain=v1", "--untracked-files=all") for line in status.splitlines(): if not line: continue path = line[3:] if " -> " in path: path = path.split(" -> ", 1)[1] paths.add(path.replace("\\", "/")) return paths def phase09e_path_errors(paths: set[str]) -> list[str]: errors: list[str] = [] for path in paths: normalized = path.replace("\\", "/") suffix = Path(normalized).suffix.lower() if normalized.startswith(FORBIDDEN_PRODUCTION_PREFIXES): errors.append(f"production/target path changed: {normalized}") if suffix in FORBIDDEN_NEW_SUFFIXES: errors.append(f"forbidden target artifact/source added: {normalized}") lowered = normalized.lower() if any( token in lowered for token in ("rescue.elf", "readback.elf", "executionpackage", "installpackage") ): errors.append(f"forbidden Phase-0.9E output added: {normalized}") if "tests/host/phase09e_protocol_model.py" in paths: errors.append("protocol model created without exact protocol source") return errors def _validate_checksum_file(root: Path) -> list[str]: path = root / "packaging/phase09e/SHA256SUMS.txt" if not path.is_file(): return ["missing packaging/phase09e/SHA256SUMS.txt"] errors: list[str] = [] for line in path.read_text(encoding="utf-8").splitlines(): if not line or line.startswith("#"): continue parts = line.split(" ", 1) if len(parts) != 2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]): errors.append(f"invalid checksum line: {line}") continue expected, label = parts if label == f"~/Downloads/{OUTER_NAME}": actual = sha256_file(candidate_archive_path(root)) elif label == f"~/Downloads/{OUTER_NAME}::{INNER_NAME}": with zipfile.ZipFile(candidate_archive_path(root)) as archive: digest = hashlib.sha256() with archive.open(INNER_NAME) as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) actual = digest.hexdigest() else: candidate = root / label if not candidate.is_file(): errors.append(f"checksum target missing: {label}") continue actual = sha256_file(candidate) if actual != expected: errors.append(f"checksum mismatch: {label}") return errors def collect_errors(root: Path) -> list[str]: errors: list[str] = [] manifest_path = ( root / "manifests/runtime/phase-0.9e-bootstrap-provenance.json" ) protocol_path = root / "manifests/runtime/phase-0.9e-loader-protocol.json" schema_path = ( root / "manifests/runtime/phase-0.9e-bootstrap-provenance.schema.json" ) protocol_schema_path = ( root / "manifests/runtime/phase-0.9e-loader-protocol.schema.json" ) try: manifest = load_json(manifest_path) protocol_manifest = load_json(protocol_path) schema = load_json(schema_path) protocol_schema = load_json(protocol_schema_path) except (OSError, ValueError, json.JSONDecodeError) as error: return [str(error)] errors.extend(validate_schema_instance(schema, manifest)) errors.extend(validate_schema_instance(protocol_schema, protocol_manifest)) errors.extend(validate_manifest(manifest)) errors.extend(validate_protocol(protocol_manifest)) for relative in DELIVERABLES: if not (root / relative).is_file(): errors.append(f"missing deliverable: {relative}") if git(root, "rev-parse", "--abbrev-ref", "HEAD") != EXPECTED_BRANCH: errors.append("current branch is not the Phase-0.9E branch") if git(root, "merge-base", EXPECTED_BASELINE, "HEAD") != EXPECTED_BASELINE: errors.append("Phase-0.9E branch no longer descends from baseline") if sha256_file(root / "manifests/artifact-denylist.json") != DENYLIST_HASH: errors.append("permanent denylist changed") for relative, expected in IMMUTABLE_HASHES.items(): path = root / relative if not path.is_file() or sha256_file(path) != expected: errors.append(f"immutable evidence changed: {relative}") for name, (relative, expected) in SOURCE_COMMITS.items(): source_root = (root / relative).resolve() if git(source_root, "rev-parse", "HEAD") != expected: errors.append(f"source HEAD changed: {name}") if git(source_root, "status", "--porcelain"): errors.append(f"source tree dirty: {name}") for relative, (expected_size, expected_hash) in SOURCE_HASHES.items(): path = (root / relative).resolve() if not path.is_file(): errors.append(f"source artifact missing: {relative}") continue if path.stat().st_size != expected_size or sha256_file(path) != expected_hash: errors.append(f"source artifact identity changed: {relative}") outer = candidate_archive_path(root) if not outer.is_file(): errors.append(f"candidate archive missing: {outer}") else: if outer.stat().st_size != OUTER_SIZE or sha256_file(outer) != OUTER_SHA256: errors.append("candidate outer archive identity changed") try: with zipfile.ZipFile(outer) as archive: if tuple(item.filename for item in archive.infolist()) != ZIP_ENTRIES: errors.append("candidate ZIP inventory changed") info = archive.getinfo(INNER_NAME) if info.file_size != INNER_SIZE: errors.append("candidate inner size changed") digest = hashlib.sha256() magic = b"" with archive.open(INNER_NAME) as stream: while True: chunk = stream.read(1024 * 1024) if not chunk: break if not magic: magic = chunk[:6] digest.update(chunk) if magic != b"SIECAF" or digest.hexdigest() != INNER_SHA256: errors.append("candidate inner identity changed") except (OSError, KeyError, zipfile.BadZipFile) as error: errors.append(f"candidate ZIP audit failed: {error}") errors.extend(phase09e_path_errors(_changed_paths(root))) errors.extend(_validate_checksum_file(root)) return errors def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", type=Path, required=True) args = parser.parse_args() errors = collect_errors(args.root.resolve()) if errors: for error in errors: print(f"ERROR: {error}") return 1 print("Phase-0.9E bootstrap provenance validation: PASS") print(f"Decision: {EXPECTED_CLASSIFICATION}") print("Device actions: NONE") return 0 if __name__ == "__main__": raise SystemExit(main())