324 lines
11 KiB
Python
324 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Twenty-two host-only guardrails for Phase 0.9E-R."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import copy
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import sys
|
|
from types import ModuleType
|
|
from typing import Callable
|
|
|
|
|
|
def load_module(name: str, path: Path) -> ModuleType:
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError(f"could not load {path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise RuntimeError(message)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
validator = load_module(
|
|
"phase09er_validator", root / "tools/validate_phase09er_provenance.py"
|
|
)
|
|
manifest = validator.load_json(
|
|
root / "manifests/runtime/phase-0.9e-r-release-correlation.json"
|
|
)
|
|
port_manifest = validator.load_json(
|
|
root / "manifests/runtime/phase-0.9e-r-port9020-audit.json"
|
|
)
|
|
cases: list[tuple[str, Callable[[], None]]] = []
|
|
|
|
def case(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]:
|
|
def register(function: Callable[[], None]) -> Callable[[], None]:
|
|
cases.append((name, function))
|
|
return function
|
|
|
|
return register
|
|
|
|
@case("01 name match alone is not a byte match")
|
|
def _() -> None:
|
|
require(
|
|
not validator.official_byte_match(
|
|
official_source=True,
|
|
local_name="same.zip",
|
|
local_size=1,
|
|
local_sha256="a" * 64,
|
|
asset_name="same.zip",
|
|
asset_size=2,
|
|
asset_sha256="b" * 64,
|
|
),
|
|
"name-only candidate became a byte match",
|
|
)
|
|
|
|
@case("02 size match alone is not a byte match")
|
|
def _() -> None:
|
|
require(
|
|
not validator.official_byte_match(
|
|
official_source=True,
|
|
local_name="local.zip",
|
|
local_size=10,
|
|
local_sha256="a" * 64,
|
|
asset_name="asset.zip",
|
|
asset_size=10,
|
|
asset_sha256="b" * 64,
|
|
),
|
|
"size-only candidate became a byte match",
|
|
)
|
|
|
|
@case("03 hash match also requires exact byte count")
|
|
def _() -> None:
|
|
require(
|
|
not validator.official_byte_match(
|
|
official_source=True,
|
|
local_name="same.zip",
|
|
local_size=10,
|
|
local_sha256="a" * 64,
|
|
asset_name="same.zip",
|
|
asset_size=11,
|
|
asset_sha256="a" * 64,
|
|
),
|
|
"hash with a different byte count became a match",
|
|
)
|
|
|
|
@case("04 a mirror cannot establish an official byte match")
|
|
def _() -> None:
|
|
require(
|
|
not validator.official_byte_match(
|
|
official_source=False,
|
|
local_name="same.zip",
|
|
local_size=10,
|
|
local_sha256="a" * 64,
|
|
asset_name="same.zip",
|
|
asset_size=10,
|
|
asset_sha256="a" * 64,
|
|
),
|
|
"non-official mirror established an official match",
|
|
)
|
|
|
|
@case("05 release association is not inner-content binding")
|
|
def _() -> None:
|
|
require(
|
|
validator.source_binding(
|
|
release_associated=True,
|
|
inner_bytes_matched=False,
|
|
inner_opaque=True,
|
|
)
|
|
== "SOURCE_ONLY_ASSOCIATION",
|
|
"release association was promoted to reproducible content",
|
|
)
|
|
|
|
@case("06 opaque SIECAF receives no invented provenance")
|
|
def _() -> None:
|
|
require(
|
|
manifest["inner_archive"]["classification"] == "OPAQUE_UNBOUND"
|
|
and manifest["inner_archive"]["further_reverse_engineering_performed"]
|
|
is False,
|
|
"opaque inner archive provenance was overclaimed",
|
|
)
|
|
|
|
@case("07 public release commits are exact")
|
|
def _() -> None:
|
|
for tag in manifest["tags"]:
|
|
require(
|
|
len(tag["commit"]) == 40
|
|
and all(character in "0123456789abcdef" for character in tag["commit"]),
|
|
f"release commit is not exact: {tag['tag']}",
|
|
)
|
|
|
|
@case("08 each source archive is hashed")
|
|
def _() -> None:
|
|
for tag in manifest["tags"]:
|
|
require(
|
|
len(tag["source_archive_sha256"]) == 64,
|
|
f"source archive hash missing: {tag['tag']}",
|
|
)
|
|
|
|
@case("09 upstream worktree must be clean")
|
|
def _() -> None:
|
|
changed = copy.deepcopy(manifest)
|
|
changed["upstream_worktree"]["clean"] = False
|
|
require(
|
|
bool(validator.validate_release_manifest(changed)),
|
|
"dirty upstream worktree passed",
|
|
)
|
|
|
|
@case("10 port reference is not an implementation")
|
|
def _() -> None:
|
|
require(
|
|
not validator.port_implementation_sufficient("PORT_9020_REFERENCE_ONLY"),
|
|
"reference-only text was accepted as implementation",
|
|
)
|
|
|
|
@case("11 embedded bytes receive no invented source commit")
|
|
def _() -> None:
|
|
changed = copy.deepcopy(port_manifest)
|
|
changed["port_9021_relation"]["source_commit"] = "0" * 40
|
|
require(
|
|
bool(validator.validate_port_manifest(changed)),
|
|
"embedded loader bytes accepted an invented source commit",
|
|
)
|
|
|
|
@case("12 a sender without receive is not duplex")
|
|
def _() -> None:
|
|
require(
|
|
not validator.sender_duplex(sends=True, receives=False),
|
|
"one-way sender became duplex",
|
|
)
|
|
|
|
@case("13 missing short-send handling is a deficiency")
|
|
def _() -> None:
|
|
require(
|
|
validator.sender_short_send_deficiency(
|
|
uses_sendall=False, explicit_send_loop=False
|
|
),
|
|
"missing short-send handling was accepted",
|
|
)
|
|
require(
|
|
not validator.sender_short_send_deficiency(
|
|
uses_sendall=True, explicit_send_loop=False
|
|
),
|
|
"sendall was incorrectly marked as lacking short-send handling",
|
|
)
|
|
|
|
@case("14 operator attestation is not runtime evidence")
|
|
def _() -> None:
|
|
require(
|
|
not validator.attestation_is_runtime_proof(
|
|
attested=True, hardware_observed=False
|
|
),
|
|
"operator attestation became runtime proof",
|
|
)
|
|
|
|
@case("15 empty attestation authorizes nothing")
|
|
def _() -> None:
|
|
text = (
|
|
root
|
|
/ "docs/approvals/phase-0.9e-r-y2jb-deployed-use-attestation.md"
|
|
).read_text(encoding="utf-8")
|
|
require("attested: false" in text, "template became attested")
|
|
for field in validator.AUTHORIZATION_FIELDS:
|
|
require(f"{field}: false" in text, f"template omits false {field}")
|
|
|
|
@case("16 Phase 0.9F blocks without official match")
|
|
def _() -> None:
|
|
require(
|
|
not validator.phase09f_design_allowed(
|
|
correlation="OFFICIAL_RELEASE_NO_MATCH",
|
|
release_commit_known=True,
|
|
upstream_clean=True,
|
|
port_classification="PORT_9020_IMPLEMENTATION_FOUND",
|
|
sender_identified=True,
|
|
attestation_available=True,
|
|
all_authorizations_false=True,
|
|
),
|
|
"Phase 0.9F passed without an official asset match",
|
|
)
|
|
|
|
@case("17 Phase 0.9F blocks without found or partial loader")
|
|
def _() -> None:
|
|
require(
|
|
not validator.phase09f_design_allowed(
|
|
correlation="OFFICIAL_RELEASE_BYTE_MATCH",
|
|
release_commit_known=True,
|
|
upstream_clean=True,
|
|
port_classification="PORT_9020_REFERENCE_ONLY",
|
|
sender_identified=True,
|
|
attestation_available=True,
|
|
all_authorizations_false=True,
|
|
),
|
|
"Phase 0.9F passed with a reference-only loader",
|
|
)
|
|
|
|
@case("18 no target source appears")
|
|
def _() -> None:
|
|
errors = validator.phase09er_path_errors(
|
|
{"docs/runtime/phase-0.9e-r-note.md", "src/backends/ps5/rescue.c"}
|
|
)
|
|
require(any("target" in error for error in errors), "target source guard failed")
|
|
require(
|
|
not validator.phase09er_path_errors(
|
|
{"docs/runtime/phase-0.9e-r-note.md"}
|
|
),
|
|
"documentation was rejected as target source",
|
|
)
|
|
|
|
@case("19 no target artifact appears")
|
|
def _() -> None:
|
|
require(
|
|
bool(validator.phase09er_path_errors({"packaging/rescue.elf"})),
|
|
"target artifact guard failed",
|
|
)
|
|
|
|
@case("20 all authorization fields remain false")
|
|
def _() -> None:
|
|
changed = copy.deepcopy(manifest)
|
|
changed["authorization"]["execution_authorized"] = True
|
|
require(
|
|
bool(validator.validate_release_manifest(changed)),
|
|
"true authorization passed",
|
|
)
|
|
|
|
@case("21 automatic retry remains false")
|
|
def _() -> None:
|
|
require(
|
|
manifest["authorization"]["automatic_retry"] is False
|
|
and port_manifest["authorization"]["automatic_retry"] is False
|
|
and port_manifest["official_remote_js_loader"][
|
|
"automatic_retry_authorized"
|
|
]
|
|
is False,
|
|
"automatic retry was authorized",
|
|
)
|
|
|
|
@case("22 no large release asset enters Git")
|
|
def _() -> None:
|
|
actions = manifest["actions"]
|
|
require(
|
|
actions["large_release_assets_downloaded"] == 0
|
|
and actions["large_release_asset_bytes_downloaded"] == 0,
|
|
"a large official release asset was recorded as downloaded",
|
|
)
|
|
for path in root.rglob("*"):
|
|
if (
|
|
path.is_file()
|
|
and ".git" not in path.parts
|
|
and "work" not in path.parts
|
|
):
|
|
require(
|
|
path.stat().st_size <= 50 * 1024 * 1024,
|
|
f"large release-like file entered repository: {path}",
|
|
)
|
|
|
|
failures: list[str] = []
|
|
for name, function in cases:
|
|
try:
|
|
function()
|
|
except Exception as error: # guardrail harness reports all failures
|
|
failures.append(f"{name}: {error}")
|
|
if failures:
|
|
for failure in failures:
|
|
print(f"FAIL: {failure}")
|
|
return 1
|
|
print(f"Phase-0.9E-R provenance guardrails: {len(cases)}/{len(cases)} PASS")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|