622 lines
25 KiB
Python
622 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Validate the host-only Phase-0.9E-R official Y2JB correlation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
from typing import Any
|
|
import zipfile
|
|
|
|
|
|
BASELINE = "3d3151bd12ff0f786c1e1b9af75d7174408e3d2f"
|
|
BRANCH = "codex/chimera-gfx-phase09e-r-y2jb-correlation"
|
|
PHASE = "PHASE_0_9E_R_OFFICIAL_Y2JB_CORRELATION"
|
|
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"
|
|
DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783"
|
|
UPSTREAM_COMMIT = "0dbbf4e7e0203af7e5d101a3256c634edf4e3ba2"
|
|
UPSTREAM_TREE = "c4344f43af7c268337437e6419548dba6f6bc211"
|
|
UPSTREAM_REMOTE = "https://github.com/Gezine/Y2JB.git"
|
|
PORT_9020_CLASSIFICATION = "PORT_9020_REFERENCE_ONLY"
|
|
FINAL_CLASSIFICATION = "LOCAL_BACKUP_NOT_CORRELATED"
|
|
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",
|
|
)
|
|
|
|
AUTHORIZATION_FIELDS = (
|
|
"device_action_authorized",
|
|
"target_build_authorized",
|
|
"transfer_authorized",
|
|
"execution_authorized",
|
|
"installation_authorized",
|
|
"lifecycle_authorized",
|
|
"autoload_authorized",
|
|
"device_write_authorized",
|
|
"automatic_retry",
|
|
)
|
|
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",
|
|
"manifests/runtime/phase-0.9e-bootstrap-provenance.json":
|
|
"5dfa9bfe2ae751b2f0ea0e03c60c1a4471a35452389cf471456e6c54eb4601cf",
|
|
"manifests/runtime/phase-0.9e-loader-protocol.json":
|
|
"a7ca8b4e8072cb60ad4cd4869f6c508e8014059c8d81ad3b94a8d9e8cd0db4cb",
|
|
}
|
|
UPSTREAM_FILE_HASHES = {
|
|
"README.md": (7500, "16bfdaa624b8b04f4a6a4a7d512ca8473ad2db73e80df39a974267403e34f751"),
|
|
"payload_sender.py": (1064, "8c87920c41dbdbd66b9f36ca9509f0d6bef9170f351dd97ff831cfb98e642ec6"),
|
|
"log_server.py": (929, "463114fd46479a7286706de13beb3f52221f36f3cbe37dc5a27bdb6104787a98"),
|
|
"appinfo_editor.py": (2008, "c1bcb453660f597cbc9026dba76519a4929fc3e967c183deec8a1ca56912e2e8"),
|
|
"download0/cache/splash_screen/aHR0cHM6Ly93d3cueW91dHViZS5jb20vdHY=/remotejsloader.js":
|
|
(7132, "30cc6d1535549b2a49b47a9e0c85a3444cf84177be54398691694b7c6505f38e"),
|
|
"download0/cache/splash_screen/aHR0cHM6Ly93d3cueW91dHViZS5jb20vdHY=/elfldr-ps5-1340.elf":
|
|
(397000, "30478bcadb6439e1247451c4ac706b6e1385044dd0f12d6486b6d7057929453b"),
|
|
}
|
|
DELIVERABLES = (
|
|
"docs/runtime/phase-0.9e-r-official-release-correlation.md",
|
|
"docs/runtime/phase-0.9e-r-release-source-binding.md",
|
|
"docs/runtime/phase-0.9e-r-port9020-source-audit.md",
|
|
"docs/runtime/phase-0.9e-r-official-hostsender-audit.md",
|
|
"docs/runtime/phase-0.9e-r-provenance-gaps.md",
|
|
"docs/approvals/phase-0.9e-r-y2jb-deployed-use-attestation.md",
|
|
"manifests/runtime/phase-0.9e-r-release-correlation.json",
|
|
"manifests/runtime/phase-0.9e-r-release-correlation.schema.json",
|
|
"manifests/runtime/phase-0.9e-r-port9020-audit.json",
|
|
"manifests/runtime/phase-0.9e-r-port9020-audit.schema.json",
|
|
"tools/validate_phase09er_provenance.py",
|
|
"tests/test_phase09er_provenance.py",
|
|
"packaging/phase09er/SHA256SUMS.txt",
|
|
)
|
|
FORBIDDEN_SUFFIXES = {
|
|
".c", ".cc", ".cpp", ".s", ".asm", ".ld", ".elf", ".self", ".sprx",
|
|
".pkg", ".bin", ".wasm",
|
|
}
|
|
FORBIDDEN_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 a JSON 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 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 candidate_archive_path(root: Path) -> Path:
|
|
direct = Path.home() / "Downloads" / OUTER_NAME
|
|
if direct.is_file():
|
|
return direct
|
|
for ancestor in (root, *root.parents):
|
|
if ancestor.parent.name.lower() == "users":
|
|
return ancestor / "Downloads" / OUTER_NAME
|
|
return direct
|
|
|
|
|
|
def official_byte_match(
|
|
*,
|
|
official_source: bool,
|
|
local_name: str,
|
|
local_size: int,
|
|
local_sha256: str,
|
|
asset_name: str,
|
|
asset_size: int,
|
|
asset_sha256: str,
|
|
) -> bool:
|
|
"""Only exact official name, byte count, and hash establish a match."""
|
|
return (
|
|
official_source
|
|
and local_name == asset_name
|
|
and local_size == asset_size
|
|
and local_sha256 == asset_sha256
|
|
)
|
|
|
|
|
|
def source_binding(
|
|
*, release_associated: bool, inner_bytes_matched: bool, inner_opaque: bool
|
|
) -> str:
|
|
if inner_bytes_matched:
|
|
return "REPRODUCIBLE_CONTENT_BINDING"
|
|
if release_associated and inner_opaque:
|
|
return "SOURCE_ONLY_ASSOCIATION"
|
|
if release_associated:
|
|
return "OFFICIAL_RELEASE_ASSOCIATION"
|
|
return "NO_LOCAL_RELEASE_ASSOCIATION"
|
|
|
|
|
|
def port_implementation_sufficient(classification: str) -> bool:
|
|
return classification in {
|
|
"PORT_9020_IMPLEMENTATION_FOUND",
|
|
"PORT_9020_IMPLEMENTATION_PARTIAL",
|
|
}
|
|
|
|
|
|
def phase09f_design_allowed(
|
|
*,
|
|
correlation: str,
|
|
release_commit_known: bool,
|
|
upstream_clean: bool,
|
|
port_classification: str,
|
|
sender_identified: bool,
|
|
attestation_available: bool,
|
|
all_authorizations_false: bool,
|
|
) -> bool:
|
|
return all(
|
|
(
|
|
correlation == "OFFICIAL_RELEASE_BYTE_MATCH",
|
|
release_commit_known,
|
|
upstream_clean,
|
|
port_implementation_sufficient(port_classification),
|
|
sender_identified,
|
|
attestation_available,
|
|
all_authorizations_false,
|
|
)
|
|
)
|
|
|
|
|
|
def sender_duplex(*, sends: bool, receives: bool) -> bool:
|
|
return sends and receives
|
|
|
|
|
|
def sender_short_send_deficiency(*, uses_sendall: bool, explicit_send_loop: bool) -> bool:
|
|
return not (uses_sendall or explicit_send_loop)
|
|
|
|
|
|
def attestation_is_runtime_proof(*, attested: bool, hardware_observed: bool) -> bool:
|
|
return attested and hardware_observed
|
|
|
|
|
|
def _type_matches(value: Any, expected: str | list[str]) -> bool:
|
|
if isinstance(expected, list):
|
|
return any(_type_matches(value, item) for item in expected)
|
|
checks = {
|
|
"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),
|
|
"null": value is None,
|
|
}
|
|
return checks.get(expected, True)
|
|
|
|
|
|
def validate_schema_instance(
|
|
schema: dict[str, Any], instance: Any, path: str = "$"
|
|
) -> list[str]:
|
|
errors: list[str] = []
|
|
expected_type = schema.get("type")
|
|
if expected_type is not None and not _type_matches(instance, expected_type):
|
|
return [f"{path}: type mismatch"]
|
|
if "const" in schema and instance != schema["const"]:
|
|
errors.append(f"{path}: const mismatch")
|
|
if "enum" in schema and instance not in schema["enum"]:
|
|
errors.append(f"{path}: outside enum")
|
|
if isinstance(instance, dict):
|
|
for required in schema.get("required", []):
|
|
if required not in instance:
|
|
errors.append(f"{path}: missing {required}")
|
|
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 isinstance(schema.get("additionalProperties"), dict):
|
|
errors.extend(
|
|
validate_schema_instance(
|
|
schema["additionalProperties"], value, f"{path}.{key}"
|
|
)
|
|
)
|
|
if isinstance(instance, list) and len(instance) < schema.get("minItems", 0):
|
|
errors.append(f"{path}: too few items")
|
|
return errors
|
|
|
|
|
|
def validate_release_manifest(manifest: dict[str, Any]) -> list[str]:
|
|
errors: list[str] = []
|
|
expected = {
|
|
"phase": PHASE,
|
|
"status": "DESIGN_ONLY",
|
|
"baseline_commit": BASELINE,
|
|
"release_correlation": "OFFICIAL_RELEASE_NO_MATCH",
|
|
"final_classification": FINAL_CLASSIFICATION,
|
|
"identified_release": None,
|
|
"identified_release_asset": None,
|
|
"inner_content_binding": "SIECAF_OPAQUE_UNBOUND",
|
|
"phase09f_offline_design_allowed": False,
|
|
}
|
|
for field, value in expected.items():
|
|
if manifest.get(field) != value:
|
|
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")
|
|
local = manifest.get("local_candidate", {})
|
|
if (
|
|
local.get("file_name") != OUTER_NAME
|
|
or local.get("size") != OUTER_SIZE
|
|
or local.get("sha256") != OUTER_SHA256
|
|
):
|
|
errors.append("local candidate identity changed")
|
|
inner = manifest.get("inner_archive", {})
|
|
if (
|
|
inner.get("size") != INNER_SIZE
|
|
or inner.get("sha256") != INNER_SHA256
|
|
or inner.get("classification") != "OPAQUE_UNBOUND"
|
|
or inner.get("further_reverse_engineering_performed") is not False
|
|
):
|
|
errors.append("inner SIECAF boundary changed")
|
|
assets = manifest.get("assets", [])
|
|
if len(assets) != 7:
|
|
errors.append("official asset inventory must contain seven assets")
|
|
for asset in assets:
|
|
if not re.fullmatch(r"[0-9a-f]{64}", str(asset.get("sha256", ""))):
|
|
errors.append(f"official asset digest malformed: {asset.get('name')}")
|
|
if asset.get("classification") != "NO_MATCH" or asset.get("downloaded"):
|
|
errors.append(f"asset was not metadata-excluded: {asset.get('name')}")
|
|
if official_byte_match(
|
|
official_source=True,
|
|
local_name=OUTER_NAME,
|
|
local_size=OUTER_SIZE,
|
|
local_sha256=OUTER_SHA256,
|
|
asset_name=str(asset.get("name")),
|
|
asset_size=int(asset.get("size", -1)),
|
|
asset_sha256=str(asset.get("sha256")),
|
|
):
|
|
errors.append(f"unrecorded official byte match: {asset.get('name')}")
|
|
tags = manifest.get("tags", [])
|
|
if len(tags) != 5:
|
|
errors.append("official tag inventory must contain five tags")
|
|
for tag in tags:
|
|
if not re.fullmatch(r"[0-9a-f]{40}", str(tag.get("commit", ""))):
|
|
errors.append(f"tag commit is not exact: {tag.get('tag')}")
|
|
if not re.fullmatch(r"[0-9a-f]{64}", str(tag.get("source_archive_sha256", ""))):
|
|
errors.append(f"source archive hash missing: {tag.get('tag')}")
|
|
if len(manifest.get("release_inventory", [])) != 6:
|
|
errors.append("release inventory must cover versions 1.2 through 1.6")
|
|
provenance = manifest.get("artifact_provenance", [])
|
|
if len(provenance) < 6:
|
|
errors.append("artifact provenance inventory is incomplete")
|
|
for artifact in provenance:
|
|
missing = [field for field in PROVENANCE_FIELDS if field not in artifact]
|
|
if missing:
|
|
errors.append(
|
|
f"{artifact.get('logical_name')}: missing provenance fields {missing}"
|
|
)
|
|
if artifact.get("confidence") == "EXACT_USED":
|
|
errors.append(
|
|
f"{artifact.get('logical_name')}: deployed-use provenance overclaimed"
|
|
)
|
|
if artifact.get("logical_name") == "official_embedded_elfldr_1_6":
|
|
if artifact.get("source_commit") is not None:
|
|
errors.append("embedded elfldr received an invented source commit")
|
|
worktree = manifest.get("upstream_worktree", {})
|
|
if (
|
|
worktree.get("commit") != UPSTREAM_COMMIT
|
|
or worktree.get("tree") != UPSTREAM_TREE
|
|
or worktree.get("clean") is not True
|
|
or worktree.get("submodules") != 0
|
|
or worktree.get("git_lfs_pointers") != 0
|
|
):
|
|
errors.append("official upstream worktree record changed")
|
|
sender = manifest.get("official_host_sender", {})
|
|
if (
|
|
sender.get("response_read") is not False
|
|
or sender.get("duplex") is not False
|
|
or sender.get("deployed_use_attested") is not False
|
|
):
|
|
errors.append("official sender was overclaimed")
|
|
if manifest.get("operator_attestation", {}).get("attested") is not False:
|
|
errors.append("operator attestation must remain empty")
|
|
actions = manifest.get("actions", {})
|
|
expected_false_actions = (
|
|
"downloaded_code_executed",
|
|
"dependency_installed",
|
|
"ps5_connected",
|
|
"ps5_ip_used",
|
|
"device_request_performed",
|
|
"files_transferred_to_or_from_ps5",
|
|
"target_build_performed",
|
|
"target_code_created",
|
|
"target_artifact_created",
|
|
"payload_created",
|
|
"device_client_created",
|
|
)
|
|
for field in expected_false_actions:
|
|
if actions.get(field) is not False:
|
|
errors.append(f"actions.{field} must be false")
|
|
if (
|
|
actions.get("large_release_assets_downloaded") != 0
|
|
or actions.get("large_release_asset_bytes_downloaded") != 0
|
|
):
|
|
errors.append("large release asset download was recorded")
|
|
if manifest.get("blocking_state", {}).get("payload_manager_backup") != (
|
|
"HARD_BLOCKER_FOR_INSTALLATION"
|
|
):
|
|
errors.append("Payload Manager installation blocker changed")
|
|
if phase09f_design_allowed(
|
|
correlation=str(manifest.get("release_correlation")),
|
|
release_commit_known=False,
|
|
upstream_clean=True,
|
|
port_classification=PORT_9020_CLASSIFICATION,
|
|
sender_identified=True,
|
|
attestation_available=True,
|
|
all_authorizations_false=True,
|
|
):
|
|
errors.append("Phase 0.9F incorrectly passed")
|
|
return errors
|
|
|
|
|
|
def validate_port_manifest(manifest: dict[str, Any]) -> list[str]:
|
|
errors: list[str] = []
|
|
if manifest.get("phase") != PHASE or manifest.get("status") != "DESIGN_ONLY":
|
|
errors.append("port manifest phase/status changed")
|
|
if manifest.get("release_commit") != UPSTREAM_COMMIT:
|
|
errors.append("port manifest release commit changed")
|
|
if manifest.get("port_9020_classification") != PORT_9020_CLASSIFICATION:
|
|
errors.append("port-9020 implementation was overclaimed")
|
|
if (
|
|
manifest.get("port_9020_listener_found") is not False
|
|
or manifest.get("port_9020_parser_found") is not False
|
|
or manifest.get("port_9020_mapping_found") is not False
|
|
):
|
|
errors.append("port-9020 source was invented")
|
|
loader = manifest.get("official_remote_js_loader", {})
|
|
if (
|
|
loader.get("desired_dynamic_port") != 50000
|
|
or loader.get("maximum_receive_bytes") != 512000
|
|
or loader.get("framing") != "CONNECTION_EOF_OR_BUFFER_LIMIT"
|
|
or loader.get("declared_length") is not False
|
|
or loader.get("timeout") is not False
|
|
or loader.get("native_elf_mapping") is not False
|
|
):
|
|
errors.append("Remote JS Loader contract changed")
|
|
if loader.get("automatic_retry_authorized") is not False:
|
|
errors.append("automatic retry was authorized")
|
|
relation = manifest.get("port_9021_relation", {})
|
|
if relation.get("listener_source_in_y2jb") is not False:
|
|
errors.append("embedded 9021 listener source was invented")
|
|
if relation.get("source_commit") is not None:
|
|
errors.append("embedded elfldr received an invented source commit")
|
|
sender = manifest.get("official_sender", {})
|
|
if sender.get("response_read") is not False or sender.get("duplex") is not False:
|
|
errors.append("one-way sender was classified duplex")
|
|
for field in AUTHORIZATION_FIELDS:
|
|
if manifest.get("authorization", {}).get(field) is not False:
|
|
errors.append(f"port authorization.{field} must be false")
|
|
if manifest.get("phase09f_offline_design_allowed") is not False:
|
|
errors.append("Phase 0.9F was authorized")
|
|
return errors
|
|
|
|
|
|
def changed_paths(root: Path) -> set[str]:
|
|
paths = set(git(root, "diff", "--name-only", BASELINE).splitlines())
|
|
for line in git(root, "status", "--porcelain=v1", "--untracked-files=all").splitlines():
|
|
path = line[3:]
|
|
if " -> " in path:
|
|
path = path.split(" -> ", 1)[1]
|
|
paths.add(path.replace("\\", "/"))
|
|
return {path for path in paths if path}
|
|
|
|
|
|
def phase09er_path_errors(paths: set[str]) -> list[str]:
|
|
errors: list[str] = []
|
|
for path in paths:
|
|
normalized = path.replace("\\", "/")
|
|
lowered = normalized.lower()
|
|
if normalized.startswith(FORBIDDEN_PREFIXES):
|
|
errors.append(f"target/production path changed: {normalized}")
|
|
if Path(normalized).suffix.lower() in FORBIDDEN_SUFFIXES:
|
|
errors.append(f"target artifact/source added: {normalized}")
|
|
if any(
|
|
token in lowered
|
|
for token in (
|
|
"rescue.elf", "observer.elf", "readback.elf", "executionpackage",
|
|
"installpackage", "transferpackage",
|
|
)
|
|
):
|
|
errors.append(f"forbidden Phase-0.9E-R output: {normalized}")
|
|
return errors
|
|
|
|
|
|
def validate_checksum_file(root: Path) -> list[str]:
|
|
checksum_path = root / "packaging/phase09er/SHA256SUMS.txt"
|
|
if not checksum_path.is_file():
|
|
return ["missing packaging/phase09er/SHA256SUMS.txt"]
|
|
errors: list[str] = []
|
|
for line in checksum_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, relative = parts
|
|
path = root / relative
|
|
if not path.is_file() or sha256_file(path) != expected:
|
|
errors.append(f"checksum mismatch or missing: {relative}")
|
|
return errors
|
|
|
|
|
|
def collect_errors(root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
release_path = root / "manifests/runtime/phase-0.9e-r-release-correlation.json"
|
|
port_path = root / "manifests/runtime/phase-0.9e-r-port9020-audit.json"
|
|
release_schema_path = (
|
|
root / "manifests/runtime/phase-0.9e-r-release-correlation.schema.json"
|
|
)
|
|
port_schema_path = (
|
|
root / "manifests/runtime/phase-0.9e-r-port9020-audit.schema.json"
|
|
)
|
|
try:
|
|
release = load_json(release_path)
|
|
port = load_json(port_path)
|
|
release_schema = load_json(release_schema_path)
|
|
port_schema = load_json(port_schema_path)
|
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
return [str(error)]
|
|
errors.extend(validate_schema_instance(release_schema, release))
|
|
errors.extend(validate_schema_instance(port_schema, port))
|
|
errors.extend(validate_release_manifest(release))
|
|
errors.extend(validate_port_manifest(port))
|
|
|
|
for relative in DELIVERABLES:
|
|
if not (root / relative).is_file():
|
|
errors.append(f"missing deliverable: {relative}")
|
|
if git(root, "rev-parse", "--abbrev-ref", "HEAD") != BRANCH:
|
|
errors.append("current branch is not the Phase-0.9E-R branch")
|
|
if git(root, "merge-base", BASELINE, "HEAD") != BASELINE:
|
|
errors.append("branch no longer descends from the canonical baseline")
|
|
if sha256_file(root / "manifests/artifact-denylist.json") != DENYLIST_SHA256:
|
|
errors.append("permanent artifact 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}")
|
|
|
|
outer = candidate_archive_path(root)
|
|
if not outer.is_file():
|
|
errors.append(f"local Y2JB candidate missing: {outer}")
|
|
elif outer.stat().st_size != OUTER_SIZE or sha256_file(outer) != OUTER_SHA256:
|
|
errors.append("local Y2JB outer identity changed")
|
|
else:
|
|
try:
|
|
with zipfile.ZipFile(outer) as archive:
|
|
info = archive.getinfo(INNER_NAME)
|
|
if (
|
|
len(archive.infolist()) != 5
|
|
or archive.comment != b""
|
|
or info.file_size != INNER_SIZE
|
|
or info.compress_size != 504158629
|
|
or info.CRC != int("522808c8", 16)
|
|
):
|
|
errors.append("local ZIP metadata changed")
|
|
digest = hashlib.sha256()
|
|
magic = b""
|
|
with archive.open(info) as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
if not magic:
|
|
magic = chunk[:6]
|
|
digest.update(chunk)
|
|
if magic != b"SIECAF" or digest.hexdigest() != INNER_SHA256:
|
|
errors.append("inner SIECAF identity changed")
|
|
except (OSError, KeyError, zipfile.BadZipFile) as error:
|
|
errors.append(f"local ZIP validation failed: {error}")
|
|
|
|
upstream = root / "work/upstream/Y2JB-official"
|
|
if not upstream.is_dir():
|
|
errors.append("official immutable upstream worktree missing")
|
|
else:
|
|
if git(upstream, "rev-parse", "HEAD") != UPSTREAM_COMMIT:
|
|
errors.append("official upstream HEAD changed")
|
|
if git(upstream, "rev-parse", "HEAD^{tree}") != UPSTREAM_TREE:
|
|
errors.append("official upstream tree changed")
|
|
if git(upstream, "status", "--porcelain=v1"):
|
|
errors.append("official upstream worktree is dirty")
|
|
remotes = git(upstream, "remote", "-v")
|
|
if UPSTREAM_REMOTE not in remotes:
|
|
errors.append("official upstream remote changed")
|
|
if git(upstream, "submodule", "status"):
|
|
errors.append("unexpected official upstream submodule")
|
|
if len(git(upstream, "ls-files").splitlines()) != 19:
|
|
errors.append("official upstream tracked file inventory changed")
|
|
for relative, (expected_size, expected_hash) in UPSTREAM_FILE_HASHES.items():
|
|
path = upstream / relative
|
|
if (
|
|
not path.is_file()
|
|
or path.stat().st_size != expected_size
|
|
or sha256_file(path) != expected_hash
|
|
):
|
|
errors.append(f"official upstream file identity changed: {relative}")
|
|
|
|
errors.extend(phase09er_path_errors(changed_paths(root)))
|
|
for path in root.rglob("*"):
|
|
if (
|
|
path.is_file()
|
|
and ".git" not in path.parts
|
|
and "work" not in path.parts
|
|
and path.stat().st_size > 50 * 1024 * 1024
|
|
):
|
|
errors.append(f"large release-like object present in repository: {path}")
|
|
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-R official Y2JB provenance validation: PASS")
|
|
print(f"Release correlation: OFFICIAL_RELEASE_NO_MATCH")
|
|
print(f"Port 9020: {PORT_9020_CLASSIFICATION}")
|
|
print(f"Decision: {FINAL_CLASSIFICATION}")
|
|
print("Device actions: NONE")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|