799 lines
29 KiB
Python
799 lines
29 KiB
Python
#!/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())
|