diff --git a/CHANGELOG.md b/CHANGELOG.md index de49dbe01..89ed9decc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning ### Changed +- Release codesign now preserves an existing valid ad-hoc signature and refuses publication when + post-install signing changes the retained wheel's native bytes, so the published wheel and the + runtime exercised by conformance and final examples are byte-identical. - Strict AMR checkpoint payload v7 now persists the accepted shared-interface flux audit together with Program clocks, histories, tagging state, conservative ledger and synchronization report. Restart validates every fragment's topology epoch, level pair, exact clock window, resolved @@ -116,6 +119,8 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning native ABI, and checkpoint envelopes independently in one generated Python/C++ release contract; declares the exact source/wheel matrix; and adds a fail-closed release preflight requiring exact tag, installed native identity, clean tree, generated products, and authenticated final-gate evidence. + The retained wheel filename and its internal `WHEEL` record must also identify exactly the promised + native macOS/arm64/cp312 lane; Python/ABI, platform, purelib, build-tag, or metadata drift is refused. - ADC-633 Compiled condensed-implicit time Program (std.condensed_schur, theta=1) on the AMR hierarchy: the condensed operators run per level through AmrProgramContext::grid_context / assembly_target / assembly_source (matrix-free coefficiented apply, reconstruct, energy), so a flat hierarchy is bit-identical to the uniform Program (the emitted matrix-free BiCGStab runs on level 0 through ctx.solve_linear_matfree) and a refined hierarchy solves the tensor elliptic by the composite FAC (CompositeFacPoisson in amr_condensed_elliptic.hpp), matching the native source-stage route; the vestigial AMR deferral stubs are removed and the Spec 6 sec.20 clean_schur_program.amr.mono cell flips to green. - ADC-640 One spatial-reconstruction dispatch generator (include/pops/runtime/builders/scheme_dispatch.hpp): dispatch_limiter binds a typed LimiterRouteId to its compile-time reconstruction policy behind an X-macro plus a count-lock static_assert, so the 17 hand-written limiter ladders across the System, polar, AMR multi-block, AMR compiled and external-brick builders collapse to one dispatch_limiter call each. A forgotten limiter is now a build error (the -Werror-free tree could only warn on a missing switch arm). Same template instantiations, bit-identical. - ADC-637 condensed_schur gained a generic lowering route: the electrostatic-Lorentz linearization J = [[0, B_z], [-B_z, 0]] is authored in the DSL (pops.lib.physics.author_electrostatic_lorentz, an m.local_linear_map on the momentum subset) and the macro (route="generic") lowers the condensed tensor coefficient A = I + c*rho*(I - theta*dt*J)^-1, the fused RHS and the velocity reconstruction through the closed-form block_inverse codegen, with no coupling/schur vocabulary. Bit-identical to the retiring hand-written Schur brick over a multi-step trajectory at theta == 1 and theta == 0.5 (golden, np.array_equal): the coefficient tensor reuses block_inverse<2> (== LorentzEliminator's binv entries) and the flux/reconstruct vector applies reuse a new factored block_apply_inverse intrinsic reproducing apply_Binv's operation order. The brick route stays the default until it is retired. diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index d3673f179..3790bd07a 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -86,5 +86,9 @@ before the official build begins. `## [x.y.z] - YYYY-MM-DD` section. 3. Run `python scripts/generate_release_contract.py --check` and the release preflight; a missing build/codesign/example/conformance evidence record blocks tagging. + The Darwin gate first preserves an already-valid ad-hoc signature and requires the post-codesign + native digest to remain byte-identical to the retained wheel member. A repair confined to the + installed copy therefore blocks publication: the wheel users receive must itself contain the + exact signed runtime exercised by conformance and the final examples. 4. Merge, then `git tag vx.y.z` on master and `git push --tags`. The `release.yml` workflow turns the tag into a GitHub Release built from that CHANGELOG section. diff --git a/scripts/codesign_pops_extensions.py b/scripts/codesign_pops_extensions.py index 78113fe54..6681f47f1 100755 --- a/scripts/codesign_pops_extensions.py +++ b/scripts/codesign_pops_extensions.py @@ -7,13 +7,19 @@ from __future__ import annotations import argparse +from collections.abc import Sequence +import hashlib import importlib.machinery import importlib.util +import json from pathlib import Path import shutil import subprocess import sys -from typing import Sequence +from typing import Any + + +CODESIGN_EVIDENCE_SCHEMA_VERSION = 1 class CodesignError(RuntimeError): @@ -66,6 +72,33 @@ def _checked_codesign(command: Sequence[str], *, action: str) -> subprocess.Comp return result +def _has_valid_adhoc_signature(codesign: str, extension: Path) -> bool: + """Return whether ``extension`` already carries the release signature policy. + + Release validation must not rewrite bytes which came from the retained wheel: those are the + bytes eventually published. Probe first and only repair an absent/invalid signature. The + release preflight separately refuses a repair which changes the retained native-member digest. + """ + + verification = subprocess.run( + (codesign, "--verify", "--strict", "--verbose=2", str(extension)), + text=True, + capture_output=True, + check=False, + ) + if verification.returncode != 0: + return False + inspection = subprocess.run( + (codesign, "--display", "--verbose=4", str(extension)), + text=True, + capture_output=True, + check=False, + ) + if inspection.returncode != 0: + return False + return "Signature=adhoc" in "%s\n%s" % (inspection.stdout, inspection.stderr) + + def codesign_imported_extensions(*, if_present: bool = False) -> tuple[Path, ...]: """Sign and verify every extension a clean ``import pops`` will load on Darwin.""" if sys.platform != "darwin": @@ -80,6 +113,8 @@ def codesign_imported_extensions(*, if_present: bool = False) -> tuple[Path, ... if not codesign: raise CodesignError("Darwin requires 'codesign', but it is not available on PATH") for extension in extensions: + if _has_valid_adhoc_signature(codesign, extension): + continue _checked_codesign( (codesign, "--force", "--sign", "-", str(extension)), action="ad-hoc signing %s" % extension) @@ -97,17 +132,40 @@ def codesign_imported_extensions(*, if_present: bool = False) -> tuple[Path, ... return extensions +def codesign_evidence(extensions: Sequence[Path]) -> dict[str, Any]: + """Describe the exact post-sign extension bytes authenticated by this process.""" + return { + "schema_version": CODESIGN_EVIDENCE_SCHEMA_VERSION, + "platform": sys.platform, + "extensions": [ + { + "path": str(extension.resolve()), + "sha256": hashlib.sha256(extension.read_bytes()).hexdigest(), + "signature": "adhoc", + } + for extension in extensions + ], + } + + def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--if-present", action="store_true", help="skip only when the pops package is absent (a present package without _pops fails)") + parser.add_argument( + "--json", action="store_true", + help="print machine-authenticated post-sign paths and hashes") args = parser.parse_args(argv) try: extensions = codesign_imported_extensions(if_present=args.if_present) - except CodesignError as error: + evidence = codesign_evidence(extensions) + except (CodesignError, OSError) as error: print("ERROR: %s" % error, file=sys.stderr) return 1 + if args.json: + print(json.dumps(evidence, sort_keys=True)) + return 0 if sys.platform == "darwin": if extensions: for extension in extensions: diff --git a/scripts/final_release_contract.py b/scripts/final_release_contract.py index 692536074..39658bcde 100644 --- a/scripts/final_release_contract.py +++ b/scripts/final_release_contract.py @@ -37,8 +37,9 @@ PYTHON_REQUIRED_SELECTION = "not mpi and not hdf5" REQUIRED_RELEASE_GATES = ( "official_build", - "doctor", + "installed_wheel", "codesign", + "doctor", "native_conformance", "python_conformance", "examples", diff --git a/scripts/prove_installed_wheel.py b/scripts/prove_installed_wheel.py new file mode 100644 index 000000000..7d5d2f7fc --- /dev/null +++ b/scripts/prove_installed_wheel.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Prove that the imported PoPS package is the exact retained release wheel.""" + +from __future__ import annotations + +import argparse +from collections.abc import Sequence +import hashlib +import importlib.machinery +import importlib.metadata +import json +from pathlib import Path +import sys +from typing import Any +from urllib.parse import unquote, urlparse +import zipfile + + +ROOT = Path(__file__).resolve().parents[1] +PROOF_SCHEMA_VERSION = 2 + + +class InstalledWheelProofError(RuntimeError): + """The retained wheel and the imported installation are not byte-identical.""" + + +def _sha256_bytes(payload: bytes) -> str: + return hashlib.sha256(payload).hexdigest() + + +def _sha256(path: Path) -> str: + return _sha256_bytes(path.read_bytes()) + + +def _outside_checkout(path: Path, *, label: str) -> Path: + resolved = path.resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + return resolved + raise InstalledWheelProofError("%s must be outside the checkout: %s" % (label, resolved)) + + +def _direct_url_path(payload: Any) -> tuple[Path, str]: + if not isinstance(payload, dict) or set(payload) != {"archive_info", "url"}: + raise InstalledWheelProofError("installed distribution direct_url.json is malformed") + archive = payload["archive_info"] + if not isinstance(archive, dict): + raise InstalledWheelProofError("installed distribution archive_info is malformed") + hashes = archive.get("hashes") + if not isinstance(hashes, dict) or set(hashes) != {"sha256"}: + raise InstalledWheelProofError("installed distribution lacks one exact sha256 archive hash") + digest = hashes["sha256"] + if not isinstance(digest, str) or len(digest) != 64: + raise InstalledWheelProofError("installed distribution archive sha256 is malformed") + parsed = urlparse(payload["url"]) + if parsed.scheme != "file" or parsed.netloc not in {"", "localhost"}: + raise InstalledWheelProofError("installed distribution did not originate from a local wheel") + return Path(unquote(parsed.path)).resolve(), digest + + +def _wheel_payload_proof( + archive: zipfile.ZipFile, + *, + distribution_root: Path, +) -> tuple[int, str]: + """Authenticate every directly installed wheel member except mutable ``RECORD``.""" + + rows: list[str] = [] + for name in sorted(archive.namelist()): + if name.endswith("/") or name.endswith(".dist-info/RECORD"): + continue + if ".data/" in name: + raise InstalledWheelProofError( + "retained wheel uses an unsupported .data installation scheme" + ) + relative = Path(name) + installed = (distribution_root / relative).resolve() + try: + installed.relative_to(distribution_root) + except ValueError as exc: + raise InstalledWheelProofError( + "wheel member escapes the installed distribution root: %s" % name + ) from exc + if not installed.is_file(): + raise InstalledWheelProofError( + "installed distribution is missing wheel member %s" % name + ) + wheel_digest = _sha256_bytes(archive.read(name)) + if _sha256(installed) != wheel_digest: + raise InstalledWheelProofError( + "installed wheel member is not byte-identical: %s" % name + ) + rows.append("%s\0%s\n" % (name, wheel_digest)) + if not rows: + raise InstalledWheelProofError("retained wheel has no immutable payload members") + return len(rows), _sha256_bytes("".join(rows).encode("utf-8")) + + +def _installed_distribution_paths( + distribution: importlib.metadata.Distribution, +) -> tuple[Path, Path, Path]: + """Resolve package/native paths from distribution metadata without importing PoPS.""" + + members = tuple(distribution.files or ()) + package_members = [ + member for member in members if member.as_posix() == "pops/__init__.py" + ] + native_members = [ + member + for member in members + if member.parent.as_posix() == "pops" + and member.name.startswith("_pops.") + and any(member.name.endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES) + ] + if len(package_members) != 1 or len(native_members) != 1: + raise InstalledWheelProofError( + "installed distribution lacks one unique pops package and native extension" + ) + return ( + Path(distribution.locate_file(package_members[0])).resolve(), + Path(distribution.locate_file(native_members[0])).resolve(), + Path(distribution.locate_file("")).resolve(), + ) + + +def build_proof( + wheel: Path, + *, + package_file: Path, + native_extension: Path, + distribution_root: Path, + python_executable: Path, + installed_version: str, + direct_url: Any, +) -> dict[str, Any]: + """Authenticate one installed distribution against one exact wheel archive.""" + + retained = _outside_checkout(wheel, label="retained wheel") + package = _outside_checkout(package_file, label="installed package") + extension = _outside_checkout(native_extension, label="installed native extension") + distribution = _outside_checkout(distribution_root, label="installed distribution") + if retained.suffix != ".whl" or not retained.is_file(): + raise InstalledWheelProofError("retained wheel is not a readable .whl file") + for label, path in (("installed package", package), ("installed native extension", extension)): + if not path.is_file(): + raise InstalledWheelProofError("%s is not a readable file: %s" % (label, path)) + if not distribution.is_dir(): + raise InstalledWheelProofError( + "installed distribution root is not a directory: %s" % distribution + ) + if not isinstance(installed_version, str) or not installed_version: + raise InstalledWheelProofError("installed distribution version is empty") + + wheel_digest = _sha256(retained) + direct_path, direct_digest = _direct_url_path(direct_url) + if direct_path != retained or direct_digest != wheel_digest: + raise InstalledWheelProofError( + "installed distribution direct URL does not authenticate the retained wheel" + ) + + try: + with zipfile.ZipFile(retained) as archive: + names = archive.namelist() + native_members = [ + name + for name in names + if name.startswith("pops/") and Path(name).name.startswith("_pops.") + and name.endswith((".so", ".pyd")) + ] + metadata_members = [name for name in names if name.endswith(".dist-info/METADATA")] + if len(native_members) != 1: + raise InstalledWheelProofError( + "retained wheel must contain exactly one pops._pops extension" + ) + if len(metadata_members) != 1: + raise InstalledWheelProofError( + "retained wheel must contain exactly one METADATA record" + ) + native_member = native_members[0] + native_digest = _sha256_bytes(archive.read(native_member)) + metadata = archive.read(metadata_members[0]).decode("utf-8") + installed_member_count, installed_tree_sha256 = _wheel_payload_proof( + archive, + distribution_root=distribution, + ) + except (OSError, UnicodeDecodeError, zipfile.BadZipFile) as exc: + raise InstalledWheelProofError("retained wheel is unreadable: %s" % exc) from exc + + metadata_version = next( + ( + line.split(": ", 1)[1] + for line in metadata.splitlines() + if line.startswith("Version: ") + ), + None, + ) + if metadata_version != installed_version: + raise InstalledWheelProofError( + "installed distribution version disagrees with retained wheel metadata" + ) + installed_native_digest = _sha256(extension) + if installed_native_digest != native_digest: + raise InstalledWheelProofError( + "installed native extension is not byte-identical to the retained wheel member" + ) + + return { + "schema_version": PROOF_SCHEMA_VERSION, + "python_executable": str(python_executable.resolve()), + "distribution_root": str(distribution), + "package_file": str(package), + "native_extension": str(extension), + "native_member": native_member, + "native_sha256": native_digest, + "installed_member_count": installed_member_count, + "installed_tree_sha256": installed_tree_sha256, + "proof_script_sha256": _sha256(Path(__file__).resolve()), + "version": installed_version, + "wheel_path": str(retained), + "wheel_sha256": wheel_digest, + } + + +def installed_wheel_proof(wheel: Path) -> dict[str, Any]: + """Resolve the live imported distribution and authenticate it against ``wheel``.""" + + distribution = importlib.metadata.distribution("pops") + package_file, native_extension, distribution_root = _installed_distribution_paths( + distribution + ) + direct_url_text = distribution.read_text("direct_url.json") + if direct_url_text is None: + raise InstalledWheelProofError( + "installed distribution has no direct_url.json for the retained wheel" + ) + try: + direct_url = json.loads(direct_url_text) + except json.JSONDecodeError as exc: + raise InstalledWheelProofError("installed direct_url.json is invalid JSON") from exc + return build_proof( + wheel, + package_file=package_file, + native_extension=native_extension, + distribution_root=distribution_root, + python_executable=Path(sys.executable), + installed_version=distribution.version, + direct_url=direct_url, + ) + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--wheel", required=True, type=Path) + args = parser.parse_args(argv) + try: + proof = installed_wheel_proof(args.wheel) + except (InstalledWheelProofError, OSError, ValueError) as exc: + print("installed wheel proof failed: %s" % exc, file=sys.stderr) + return 1 + print(json.dumps(proof, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/release_preflight.py b/scripts/release_preflight.py index d9401c8fd..f11a73784 100644 --- a/scripts/release_preflight.py +++ b/scripts/release_preflight.py @@ -32,7 +32,7 @@ ROOT = Path(__file__).resolve().parents[1] GENERATED = ROOT / "python" / "pops" / "_generated_release_contract.py" REQUIRED_GATES = REQUIRED_RELEASE_GATES -EVIDENCE_SCHEMA_VERSION = 4 +EVIDENCE_SCHEMA_VERSION = 7 class PreflightError(RuntimeError): @@ -190,6 +190,54 @@ def _artifact_file(root: Path, relative: Any, digest: Any, *, label: str) -> Non raise PreflightError("release evidence %s hash drifted" % label) +def _wheel_lane_contract(path: Path, archive: zipfile.ZipFile, contract: Any) -> None: + """Require one native wheel whose filename and WHEEL tags match the promised lane.""" + + lanes = contract.SUPPORTED_MATRIX["wheels"] + if len(lanes) != 1: + raise PreflightError("release contract must promise exactly one wheel lane") + lane = lanes[0] + if set(lane) != {"os", "arch", "python", "backend"}: + raise PreflightError("promised wheel lane is malformed") + if lane["os"] != "macos" or lane["arch"] != "arm64" \ + or lane["backend"] != "Kokkos Serial": + raise PreflightError("promised wheel lane has no release tag verifier") + + if path.suffix != ".whl": + raise PreflightError("release artifact is not a wheel") + parts = path.name[:-4].split("-") + if len(parts) != 5: + raise PreflightError("release wheel filename must not contain a build tag") + distribution, version, python_tag, abi_tag, platform_tag = parts + expected_python = lane["python"] + if distribution.lower().replace("_", "-") != "pops" \ + or version != contract.PACKAGE_VERSION: + raise PreflightError("release wheel filename name/version disagrees with the contract") + if python_tag != expected_python or abi_tag != expected_python: + raise PreflightError("release wheel Python/ABI tags disagree with the promised lane") + if re.fullmatch(r"macosx_\d+_\d+_arm64", platform_tag) is None: + raise PreflightError("release wheel platform tag disagrees with the promised lane") + + dist_info = "%s-%s.dist-info" % (distribution, version) + wheel_names = [name for name in archive.namelist() if name.endswith(".dist-info/WHEEL")] + if wheel_names != [dist_info + "/WHEEL"]: + raise PreflightError("release wheel has no unique lane-bound WHEEL record") + try: + wheel_metadata = archive.read(wheel_names[0]).decode("utf-8") + except UnicodeDecodeError as exc: + raise PreflightError("release wheel WHEEL record is not UTF-8") from exc + fields: dict[str, list[str]] = {} + for line in wheel_metadata.splitlines(): + if ": " in line: + key, value = line.split(": ", 1) + fields.setdefault(key, []).append(value) + expected_tag = "%s-%s-%s" % (python_tag, abi_tag, platform_tag) + if fields.get("Wheel-Version") != ["1.0"] \ + or fields.get("Root-Is-Purelib") != ["false"] \ + or fields.get("Tag") != [expected_tag]: + raise PreflightError("release WHEEL metadata disagrees with the promised native lane") + + def _checkpoint_tree(path: Path) -> str: if path.is_file(): return hashlib.sha256(path.read_bytes()).hexdigest() @@ -219,6 +267,7 @@ def _wheel_evidence(directory: Path, gates: dict[str, Any], contract: Any) -> No raise PreflightError("official build wheel size drifted") try: with zipfile.ZipFile(path) as archive: + _wheel_lane_contract(path, archive, contract) metadata_names = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")] if len(metadata_names) != 1: @@ -236,7 +285,137 @@ def _wheel_evidence(directory: Path, gates: dict[str, Any], contract: Any) -> No raise PreflightError("release wheel name/version disagrees with the release contract") -def _examples_evidence(directory: Path, gates: dict[str, Any]) -> None: +def _installed_wheel_evidence( + directory: Path, + gates: dict[str, Any], + contract: Any, + runtime: dict[str, str], +) -> None: + wheel = gates["official_build"]["evidence"]["wheel"] + retained = (directory / wheel["path"]).resolve() + row = gates["installed_wheel"] + evidence = row["evidence"] + expected = { + "schema_version", + "python_executable", + "distribution_root", + "package_file", + "native_extension", + "native_member", + "native_sha256", + "installed_member_count", + "installed_tree_sha256", + "proof_script_sha256", + "version", + "wheel_path", + "wheel_sha256", + } + if not isinstance(evidence, dict) or set(evidence) != expected: + raise PreflightError("installed wheel evidence is malformed") + if evidence["schema_version"] != 2: + raise PreflightError("installed wheel evidence schema is unsupported") + if evidence["version"] != contract.PACKAGE_VERSION: + raise PreflightError("installed wheel evidence version disagrees with release contract") + if Path(evidence["wheel_path"]).resolve() != retained \ + or evidence["wheel_sha256"] != wheel["sha256"]: + raise PreflightError("installed wheel evidence does not authenticate the retained wheel") + if evidence["python_executable"] != runtime["python_executable"] \ + or evidence["package_file"] != runtime["pops_file"] \ + or evidence["native_extension"] != runtime["native_extension"]: + raise PreflightError("installed wheel evidence belongs to another runtime") + + commands = row["commands"] + logs = _command_evidence(directory, commands, gate="installed_wheel") + if len(logs) != 2: + raise PreflightError("installed wheel gate requires reinstall and proof transcripts") + install_suffix = [ + "python", + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + str(retained), + ] + proof_suffix = [ + "python", + "scripts/prove_installed_wheel.py", + "--wheel", + str(retained), + ] + if commands[0]["argv"][-len(install_suffix):] != install_suffix \ + or commands[1]["argv"][-len(proof_suffix):] != proof_suffix: + raise PreflightError("installed wheel gate did not reinstall and prove the retained wheel") + try: + with zipfile.ZipFile(retained) as archive: + member = evidence["native_member"] + member_digest = hashlib.sha256(archive.read(member)).hexdigest() + rows = [] + for name in sorted(archive.namelist()): + if name.endswith("/") or name.endswith(".dist-info/RECORD"): + continue + if ".data/" in name: + raise PreflightError( + "release wheel uses an unsupported .data installation scheme" + ) + digest = hashlib.sha256(archive.read(name)).hexdigest() + rows.append("%s\0%s\n" % (name, digest)) + except (KeyError, OSError, zipfile.BadZipFile) as exc: + raise PreflightError("installed wheel native member is unreadable: %s" % exc) from exc + if member_digest != evidence["native_sha256"]: + raise PreflightError("installed wheel native member hash drifted") + expected_tree = hashlib.sha256("".join(rows).encode("utf-8")).hexdigest() + if evidence["installed_member_count"] != len(rows) \ + or evidence["installed_tree_sha256"] != expected_tree: + raise PreflightError("installed wheel payload proof drifted") + proof_script = ROOT / "scripts" / "prove_installed_wheel.py" + if evidence["proof_script_sha256"] != hashlib.sha256(proof_script.read_bytes()).hexdigest(): + raise PreflightError("installed wheel proof script drifted") + + +def _codesign_evidence( + directory: Path, + gates: dict[str, Any], + runtime: dict[str, str], +) -> None: + row = gates["codesign"] + evidence = row["evidence"] + if not isinstance(evidence, dict) or set(evidence) != { + "schema_version", "platform", "extensions"}: + raise PreflightError("codesign evidence is malformed") + if evidence["schema_version"] != 1 or evidence["platform"] != "darwin": + raise PreflightError("codesign evidence must authenticate the Darwin release lane") + extensions = evidence["extensions"] + if not isinstance(extensions, list) or len(extensions) != 1: + raise PreflightError("codesign evidence must authenticate exactly one extension") + extension = extensions[0] + if not isinstance(extension, dict) or set(extension) != { + "path", "sha256", "signature"}: + raise PreflightError("codesign extension evidence is malformed") + if extension != { + "path": runtime["native_extension"], + "sha256": runtime["native_sha256"], + "signature": "adhoc", + }: + raise PreflightError("codesign evidence does not authenticate the live native extension") + retained_native_sha256 = gates["installed_wheel"]["evidence"]["native_sha256"] + if extension["sha256"] != retained_native_sha256: + raise PreflightError( + "codesign changed the retained wheel native bytes; the published wheel " + "would differ from the validated runtime" + ) + commands = row["commands"] + logs = _command_evidence(directory, commands, gate="codesign") + suffix = ["python", "scripts/codesign_pops_extensions.py", "--json"] + if len(logs) != 1 or commands[0]["argv"][-len(suffix):] != suffix: + raise PreflightError("codesign gate did not run the exact structured verifier") + + +def _examples_evidence( + directory: Path, + gates: dict[str, Any], + runtime: dict[str, str], +) -> None: examples = gates["examples"]["evidence"] reopen = gates["artifact_reopen"]["evidence"] restart = gates["strict_restart"]["evidence"] @@ -250,25 +429,41 @@ def _examples_evidence(directory: Path, gates: dict[str, Any]) -> None: logs = _command_evidence(directory, command_rows, gate="examples") if len(logs) != len(FINAL_EXAMPLES): raise PreflightError("final examples must have one execution transcript each") - for example in FINAL_EXAMPLES: + for index, example in enumerate(FINAL_EXAMPLES): key = example.as_posix() row = examples["examples"][key] - if not isinstance(row, dict) or set(row) != {"source_sha256", "stdout_sha256", "output_root"}: + if not isinstance(row, dict) or set(row) != { + "source_sha256", "stdout_sha256", "output_root", "runtime_sha256"}: raise PreflightError("release evidence %s is malformed" % key) if row["source_sha256"] != hashlib.sha256((ROOT / example).read_bytes()).hexdigest(): raise PreflightError("release evidence source drifted for %s" % key) if not isinstance(row["output_root"], str): raise PreflightError("release evidence output root is invalid for %s" % key) - matching = [log for log, command in zip(logs, command_rows, strict=True) - if key in " ".join(command["argv"])] - if len(matching) != 1: - raise PreflightError("release evidence has no unique command transcript for %s" % key) - transcript = matching[0].read_text(encoding="utf-8") + if row["runtime_sha256"] != runtime["native_sha256"]: + raise PreflightError("release evidence runtime digest drifted for %s" % key) + output_root = (directory / row["output_root"]).resolve() + expected_suffix = [ + "python", + "scripts/run_installed_example.py", + "--runtime-sha256", + runtime["native_sha256"], + "--example", + key, + "--", + "--output-dir", + str(output_root), + ] + command = command_rows[index]["argv"] + if command[-len(expected_suffix):] != expected_suffix: + raise PreflightError("release evidence command drifted for %s" % key) + transcript = logs[index].read_text(encoding="utf-8") if row["stdout_sha256"] != hashlib.sha256(transcript.encode("utf-8")).hexdigest(): raise PreflightError("release evidence stdout hash drifted for %s" % key) if any(marker not in transcript for marker in REQUIRED_PROOF_MARKERS): raise PreflightError("release evidence lacks restart/reopen proof output for %s" % key) - output_root = (directory / row["output_root"]).resolve() + runtime_marker = "PoPS release runtime | native_sha256=" + runtime["native_sha256"] + if transcript.count(runtime_marker) != 1: + raise PreflightError("release evidence runtime binding drifted for %s" % key) if not _inside(directory, output_root) or not output_root.is_dir(): raise PreflightError("release evidence output root is absent for %s" % key) reopened = reopen["examples"][key] @@ -340,6 +535,8 @@ def _evidence(path: Path, contract: Any, commit: str, runtime: dict[str, str]) - else: _command_evidence(directory, commands, gate=name) _wheel_evidence(directory, gates, contract) + _installed_wheel_evidence(directory, gates, contract, runtime) + _codesign_evidence(directory, gates, runtime) for name in ("native_conformance", "python_conformance"): evidence = gates[name]["evidence"] expected = {"required_lane"} if name == "native_conformance" \ @@ -360,7 +557,7 @@ def _evidence(path: Path, contract: Any, commit: str, runtime: dict[str, str]) - label="%s JUnit" % name) if gates["python_conformance"]["evidence"]["selection"] != PYTHON_REQUIRED_SELECTION: raise PreflightError("release evidence Python required-lane selection drifted") - _examples_evidence(directory, gates) + _examples_evidence(directory, gates, runtime) def main() -> int: diff --git a/scripts/run_final_gate.py b/scripts/run_final_gate.py index efddb3463..4980627b5 100644 --- a/scripts/run_final_gate.py +++ b/scripts/run_final_gate.py @@ -38,7 +38,7 @@ ROOT = Path(__file__).resolve().parents[1] -EVIDENCE_SCHEMA_VERSION = 4 +EVIDENCE_SCHEMA_VERSION = 7 REQUIRED_GATES = REQUIRED_RELEASE_GATES @@ -190,6 +190,41 @@ def _runtime_provenance() -> dict[str, str]: return payload +def _json_evidence(stdout: str, *, gate: str) -> dict[str, Any]: + try: + payload = json.loads(stdout) + except json.JSONDecodeError as exc: + raise FinalGateError("%s evidence was not JSON: %s" % (gate, stdout[-4000:])) from exc + if not isinstance(payload, dict): + raise FinalGateError("%s evidence must be a JSON object" % gate) + return payload + + +def _signed_runtime_sha256( + evidence: dict[str, Any], *, retained_native_sha256: str +) -> str: + if set(evidence) != {"schema_version", "platform", "extensions"} \ + or evidence["schema_version"] != 1 or evidence["platform"] != "darwin": + raise FinalGateError("codesign evidence is not the Darwin release proof") + extensions = evidence["extensions"] + if not isinstance(extensions, list) or len(extensions) != 1: + raise FinalGateError("codesign evidence must authenticate exactly one extension") + extension = extensions[0] + if not isinstance(extension, dict) or set(extension) != { + "path", "sha256", "signature"} or extension["signature"] != "adhoc": + raise FinalGateError("codesign extension evidence is malformed") + digest = extension["sha256"] + if not isinstance(digest, str) or len(digest) != 64 \ + or any(character not in "0123456789abcdef" for character in digest): + raise FinalGateError("codesign extension sha256 is malformed") + if digest != retained_native_sha256: + raise FinalGateError( + "codesign changed the retained wheel native bytes; refusing to publish " + "an artifact different from the validated runtime" + ) + return digest + + def _contract() -> tuple[str, str]: generated = ROOT / "python" / "pops" / "_generated_release_contract.py" specification = importlib.util.spec_from_file_location("_final_release_contract", generated) @@ -363,7 +398,11 @@ def _reopen_npz_with_installed_runtime(recorder: Recorder, paths: Sequence[Path] ["python", "-c", code, *(str(path) for path in paths)])) -def _run_examples(recorder: Recorder) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: +def _run_examples( + recorder: Recorder, + *, + runtime_sha256: str, +) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: results: dict[str, Any] = {} reopened: dict[str, Any] = {} restarted: dict[str, Any] = {} @@ -372,7 +411,17 @@ def _run_examples(recorder: Recorder) -> tuple[dict[str, Any], dict[str, Any], d destination = examples_root / example.stem stdout = recorder.run( "examples", - _conda_command(["python", str(example), "--output-dir", str(destination)]), + _conda_command([ + "python", + "scripts/run_installed_example.py", + "--runtime-sha256", + runtime_sha256, + "--example", + str(example), + "--", + "--output-dir", + str(destination), + ]), ) missing = [marker for marker in REQUIRED_PROOF_MARKERS if marker not in stdout] if missing: @@ -391,6 +440,7 @@ def _run_examples(recorder: Recorder) -> tuple[dict[str, Any], dict[str, Any], d "source_sha256": _sha256(ROOT / example), "stdout_sha256": hashlib.sha256(stdout.encode("utf-8")).hexdigest(), "output_root": str(destination.relative_to(recorder.root)), + "runtime_sha256": runtime_sha256, } return results, reopened, restarted @@ -465,6 +515,27 @@ def main(argv: Sequence[str] | None = None) -> int: "size": wheel.stat().st_size, }, } + recorder.run("installed_wheel", _conda_command([ + "python", "-m", "pip", "install", "--force-reinstall", "--no-deps", str(wheel), + ])) + installed_wheel_stdout = recorder.run( + "installed_wheel", + _conda_command([ + "python", "scripts/prove_installed_wheel.py", "--wheel", str(wheel), + ]), + ) + recorder.rows["installed_wheel"]["evidence"] = _json_evidence( + installed_wheel_stdout, gate="installed_wheel" + ) + codesign_stdout = recorder.run( + "codesign", + _conda_command([ + "python", "scripts/codesign_pops_extensions.py", "--json", + ]), + ) + recorder.rows["codesign"]["evidence"] = _json_evidence( + codesign_stdout, gate="codesign" + ) recorder.run("official_build", _conda_command(["cmake", "--preset", "serial"])) recorder.run("official_build", _conda_command(["cmake", "--build", "--preset", "serial"])) doctor_code = ( @@ -475,8 +546,6 @@ def main(argv: Sequence[str] | None = None) -> int: "print('doctor package=' + pops.__version__)" ) recorder.run("doctor", _conda_command(["python", "-c", doctor_code])) - recorder.run("codesign", _conda_command( - ["python", "scripts/codesign_pops_extensions.py"])) ctest_dir = _resolve_ctest_dir(args.ctest_dir) native_junit = evidence_root / "reports" / "native-conformance.xml" @@ -500,7 +569,14 @@ def main(argv: Sequence[str] | None = None) -> int: "required_lane": _junit_summary(python_junit), "selection": PYTHON_REQUIRED_SELECTION, } - examples, reopened, restarted = _run_examples(recorder) + signed_runtime_sha256 = _signed_runtime_sha256( + recorder.rows["codesign"]["evidence"], + retained_native_sha256=( + recorder.rows["installed_wheel"]["evidence"]["native_sha256"] + ), + ) + examples, reopened, restarted = _run_examples( + recorder, runtime_sha256=signed_runtime_sha256) recorder.rows["examples"]["evidence"] = {"examples": examples} recorder.rows["artifact_reopen"]["evidence"] = {"examples": reopened} recorder.derived("strict_restart", {"examples": restarted}) diff --git a/scripts/run_installed_example.py b/scripts/run_installed_example.py new file mode 100644 index 000000000..67191b6a7 --- /dev/null +++ b/scripts/run_installed_example.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run one release example only after authenticating its installed PoPS runtime.""" + +from __future__ import annotations + +import argparse +from collections.abc import Sequence +import hashlib +from pathlib import Path +import runpy +import sys + + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_MARKER = "PoPS release runtime | native_sha256=" + + +class InstalledExampleError(RuntimeError): + """The example is not bound to the expected installed native runtime.""" + + +def _outside_checkout(path: Path, *, label: str) -> Path: + resolved = path.resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + return resolved + raise InstalledExampleError("%s must be outside the checkout: %s" % (label, resolved)) + + +def verify_installed_runtime(expected_sha256: str) -> str: + """Import PoPS once and return the authenticated native extension digest.""" + if len(expected_sha256) != 64 or any( + character not in "0123456789abcdef" for character in expected_sha256): + raise InstalledExampleError("expected native sha256 is malformed") + + import pops + from pops import _pops + + _outside_checkout(Path(pops.__file__), label="installed PoPS package") + extension = _outside_checkout( + Path(_pops.__file__), label="installed PoPS native extension") + digest = hashlib.sha256(extension.read_bytes()).hexdigest() + if digest != expected_sha256: + raise InstalledExampleError( + "installed native extension does not match signed release runtime") + if pops.__version__ != _pops.__version__: + raise InstalledExampleError("installed Python and native versions disagree") + return digest + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runtime-sha256", required=True) + parser.add_argument("--example", required=True, type=Path) + parser.add_argument("example_args", nargs=argparse.REMAINDER) + args = parser.parse_args(argv) + try: + digest = verify_installed_runtime(args.runtime_sha256) + example = args.example.resolve() + if not example.is_file(): + raise InstalledExampleError("release example is not a readable file: %s" % example) + try: + example.relative_to(ROOT) + except ValueError as exc: + raise InstalledExampleError( + "release example must belong to this checkout: %s" % example) from exc + forwarded = list(args.example_args) + if forwarded[:1] == ["--"]: + forwarded.pop(0) + print(RUNTIME_MARKER + digest, flush=True) + sys.argv = [str(example), *forwarded] + runpy.run_path(str(example), run_name="__main__") + return 0 + except (InstalledExampleError, OSError, ValueError) as exc: + print("installed example failed: %s" % exc, file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/python/architecture/test_codesign_build_scripts.py b/tests/python/architecture/test_codesign_build_scripts.py index 2045e8fbb..93066e027 100644 --- a/tests/python/architecture/test_codesign_build_scripts.py +++ b/tests/python/architecture/test_codesign_build_scripts.py @@ -1,6 +1,7 @@ """ADC-647 source-only tests for post-install Darwin code-signing.""" from __future__ import annotations +import hashlib import importlib.util from pathlib import Path import subprocess @@ -55,7 +56,7 @@ def test_non_darwin_never_locates_or_invokes_codesign(monkeypatch): assert helper.codesign_imported_extensions() == () -def test_darwin_signs_then_verifies_and_authenticates_ad_hoc_signature(tmp_path, monkeypatch): +def test_darwin_preserves_an_existing_valid_ad_hoc_signature(tmp_path, monkeypatch): helper = _helper() extension = tmp_path / "_pops.so" extension.touch() @@ -73,13 +74,60 @@ def run(command, **kwargs): assert helper.codesign_imported_extensions() == (extension,) assert calls == [ + ("/usr/bin/codesign", "--verify", "--strict", "--verbose=2", str(extension)), + ("/usr/bin/codesign", "--display", "--verbose=4", str(extension)), + ] + + +def test_darwin_repairs_then_verifies_a_missing_signature(tmp_path, monkeypatch): + helper = _helper() + extension = tmp_path / "_pops.so" + extension.touch() + calls = [] + + def run(command, **kwargs): + calls.append(tuple(command)) + if len(calls) == 1: + return subprocess.CompletedProcess(command, 1, "", "unsigned") + evidence = "Signature=adhoc\n" if "--display" in command else "" + return subprocess.CompletedProcess(command, 0, "", evidence) + + monkeypatch.setattr(helper.sys, "platform", "darwin") + monkeypatch.setattr(helper, "locate_imported_pops_extensions", lambda: (extension,)) + monkeypatch.setattr(helper.shutil, "which", lambda command: "/usr/bin/codesign") + monkeypatch.setattr(helper.subprocess, "run", run) + + assert helper.codesign_imported_extensions() == (extension,) + assert calls == [ + ("/usr/bin/codesign", "--verify", "--strict", "--verbose=2", str(extension)), ("/usr/bin/codesign", "--force", "--sign", "-", str(extension)), ("/usr/bin/codesign", "--verify", "--strict", "--verbose=2", str(extension)), ("/usr/bin/codesign", "--display", "--verbose=4", str(extension)), ] -@pytest.mark.parametrize("failure_call", [0, 1]) +def test_structured_evidence_binds_the_post_sign_extension_bytes(tmp_path, monkeypatch): + helper = _helper() + extension = tmp_path / "_pops.so" + extension.write_bytes(b"signed extension") + monkeypatch.setattr(helper.sys, "platform", "darwin") + + evidence = helper.codesign_evidence((extension,)) + + assert evidence == { + "schema_version": 1, + "platform": "darwin", + "extensions": [ + { + "path": str(extension.resolve()), + "sha256": hashlib.sha256(extension.read_bytes()).hexdigest(), + "signature": "adhoc", + } + ], + } + + +@pytest.mark.parametrize("failure_call", [1, 2, 3]) def test_darwin_codesign_or_verification_failure_is_explicit( tmp_path, monkeypatch, failure_call, ): @@ -91,9 +139,12 @@ def test_darwin_codesign_or_verification_failure_is_explicit( def run(command, **kwargs): call = len(calls) calls.append(tuple(command)) + if call == 0: + return subprocess.CompletedProcess(command, 1, "", "unsigned") if call == failure_call: return subprocess.CompletedProcess(command, 9, "", "signature failure") - return subprocess.CompletedProcess(command, 0, "", "") + evidence = "Signature=adhoc\n" if "--display" in command else "" + return subprocess.CompletedProcess(command, 0, "", evidence) monkeypatch.setattr(helper.sys, "platform", "darwin") monkeypatch.setattr(helper, "locate_imported_pops_extensions", lambda: (extension,)) diff --git a/tests/python/architecture/test_final_release_gate.py b/tests/python/architecture/test_final_release_gate.py index afb28b249..be66df947 100644 --- a/tests/python/architecture/test_final_release_gate.py +++ b/tests/python/architecture/test_final_release_gate.py @@ -1,9 +1,12 @@ """Source-only contract checks for the final release gate (ADC-695).""" from __future__ import annotations +import hashlib import importlib.util +import json from pathlib import Path import sys +import types import zipfile import pytest @@ -25,6 +28,50 @@ def _load(name: str, path: Path): contract = _load("final_release_contract", SCRIPTS / "final_release_contract.py") gate = _load("_final_release_gate_test", SCRIPTS / "run_final_gate.py") preflight = _load("_release_preflight_test", SCRIPTS / "release_preflight.py") +installed = _load("_installed_wheel_proof_test", SCRIPTS / "prove_installed_wheel.py") +example_runner = _load("_installed_example_test", SCRIPTS / "run_installed_example.py") + + +def _release_contract(version: str = "0.3.0"): + return type( + "ReleaseContract", + (), + { + "PACKAGE_VERSION": version, + "SUPPORTED_MATRIX": { + "wheels": ( + { + "os": "macos", + "arch": "arm64", + "python": "cp312", + "backend": "Kokkos Serial", + }, + ), + }, + }, + ) + + +def _write_release_wheel( + path: Path, + *, + version: str = "0.3.0", + tag: str = "cp312-cp312-macosx_11_0_arm64", + purelib: str = "false", +) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(path, "w") as archive: + archive.writestr( + f"pops-{version}.dist-info/METADATA", + f"Metadata-Version: 2.3\nName: PoPS\nVersion: {version}\n", + ) + archive.writestr( + f"pops-{version}.dist-info/WHEEL", + "Wheel-Version: 1.0\n" + "Generator: ADC-688 test\n" + f"Root-Is-Purelib: {purelib}\n" + f"Tag: {tag}\n", + ) def _write_final_source_tree(root: Path) -> None: @@ -165,30 +212,470 @@ def test_artifact_reopen_requires_and_records_npz(tmp_path): def test_release_evidence_authenticates_the_exact_retained_wheel(tmp_path): + wheel = tmp_path / "wheels" / "pops-0.3.0-cp312-cp312-macosx_11_0_arm64.whl" + _write_release_wheel(wheel) + gates = { + "official_build": { + "evidence": { + "wheel": { + "path": str(wheel.relative_to(tmp_path)), + "sha256": gate._sha256(wheel), + "size": wheel.stat().st_size, + }, + }, + }, + } + release = _release_contract() + + preflight._wheel_evidence(tmp_path, gates, release) + gates["official_build"]["evidence"]["wheel"]["size"] += 1 + with pytest.raises(preflight.PreflightError, match="size drifted"): + preflight._wheel_evidence(tmp_path, gates, release) + + +@pytest.mark.parametrize( + ("filename", "tag", "purelib", "message"), + ( + ( + "pops-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", + "cp311-cp311-macosx_11_0_arm64", + "false", + "Python/ABI tags", + ), + ( + "pops-0.3.0-cp312-cp312-macosx_11_0_universal2.whl", + "cp312-cp312-macosx_11_0_universal2", + "false", + "platform tag", + ), + ( + "pops-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", + "cp311-cp311-macosx_11_0_arm64", + "false", + "WHEEL metadata", + ), + ( + "pops-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", + "cp312-cp312-macosx_11_0_arm64", + "true", + "WHEEL metadata", + ), + ( + "pops-0.3.0-1-cp312-cp312-macosx_11_0_arm64.whl", + "cp312-cp312-macosx_11_0_arm64", + "false", + "build tag", + ), + ), +) +def test_release_evidence_refuses_wheel_lane_drift( + tmp_path, filename, tag, purelib, message, +): + wheel = tmp_path / "wheels" / filename + _write_release_wheel(wheel, tag=tag, purelib=purelib) + gates = { + "official_build": { + "evidence": { + "wheel": { + "path": str(wheel.relative_to(tmp_path)), + "sha256": gate._sha256(wheel), + "size": wheel.stat().st_size, + }, + }, + }, + } + + with pytest.raises(preflight.PreflightError, match=message): + preflight._wheel_evidence(tmp_path, gates, _release_contract()) + + +def test_installed_wheel_proof_requires_exact_native_member_and_direct_url(tmp_path): + wheel = tmp_path / "pops-0.3.0-cp312-cp312-macosx_11_0_arm64.whl" + native_bytes = b"exact wheel extension" + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr("pops/__init__.py", "__version__ = '0.3.0'\n") + archive.writestr("pops/_pops.cpython-312-darwin.so", native_bytes) + archive.writestr( + "pops-0.3.0.dist-info/METADATA", + "Metadata-Version: 2.3\nName: PoPS\nVersion: 0.3.0\n", + ) + package = tmp_path / "site-packages" / "pops" / "__init__.py" + extension = package.parent / "_pops.cpython-312-darwin.so" + distribution = package.parents[1] + package.parent.mkdir(parents=True) + package.write_text("__version__ = '0.3.0'\n", encoding="utf-8") + extension.write_bytes(native_bytes) + metadata = distribution / "pops-0.3.0.dist-info" / "METADATA" + metadata.parent.mkdir() + metadata.write_text( + "Metadata-Version: 2.3\nName: PoPS\nVersion: 0.3.0\n", + encoding="utf-8", + ) + wheel_sha256 = hashlib.sha256(wheel.read_bytes()).hexdigest() + direct_url = { + "archive_info": {"hashes": {"sha256": wheel_sha256}}, + "url": wheel.as_uri(), + } + + proof = installed.build_proof( + wheel, + package_file=package, + native_extension=extension, + distribution_root=distribution, + python_executable=Path(sys.executable), + installed_version="0.3.0", + direct_url=direct_url, + ) + + assert proof["wheel_sha256"] == wheel_sha256 + assert proof["native_sha256"] == hashlib.sha256(native_bytes).hexdigest() + assert proof["installed_member_count"] == 3 + extension.write_bytes(b"not the retained wheel") + with pytest.raises(installed.InstalledWheelProofError, match="not byte-identical"): + installed.build_proof( + wheel, + package_file=package, + native_extension=extension, + distribution_root=distribution, + python_executable=Path(sys.executable), + installed_version="0.3.0", + direct_url=direct_url, + ) + + +def test_installed_wheel_resolver_never_imports_unsigned_native_extension() -> None: + source = (SCRIPTS / "prove_installed_wheel.py").read_text(encoding="utf-8") + resolver = source.split("def installed_wheel_proof(", 1)[1].split( + "\ndef main(", 1 + )[0] + + assert "import pops" not in resolver + assert "from pops import" not in resolver + assert "_installed_distribution_paths(" in resolver + + +def test_release_preflight_authenticates_installed_wheel_proof_and_transcripts(tmp_path): wheel = tmp_path / "wheels" / "pops-0.3.0-cp312-cp312-macosx_11_0_arm64.whl" wheel.parent.mkdir() + native_member = "pops/_pops.cpython-312-darwin.so" + native_bytes = b"exact wheel extension" with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr("pops/__init__.py", "__version__ = '0.3.0'\n") + archive.writestr(native_member, native_bytes) archive.writestr( "pops-0.3.0.dist-info/METADATA", "Metadata-Version: 2.3\nName: PoPS\nVersion: 0.3.0\n", ) + runtime = { + "python_executable": "/proof/bin/python", + "pops_file": "/proof/site-packages/pops/__init__.py", + "native_extension": "/proof/site-packages/pops/_pops.so", + "native_sha256": "post-sign-runtime-digest", + } + wheel_sha256 = hashlib.sha256(wheel.read_bytes()).hexdigest() + with zipfile.ZipFile(wheel) as archive: + rows = [ + "%s\0%s\n" + % (name, hashlib.sha256(archive.read(name)).hexdigest()) + for name in sorted(archive.namelist()) + if not name.endswith("/") and not name.endswith(".dist-info/RECORD") + ] + commands = [] + command_argvs = ( + [ + "/proof/conda", + "run", + "python", + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + str(wheel), + ], + [ + "/proof/conda", + "run", + "python", + "scripts/prove_installed_wheel.py", + "--wheel", + str(wheel), + ], + ) + for index, argv in enumerate(command_argvs, 1): + log = tmp_path / "logs" / f"{index:02d}_installed_wheel.log" + log.parent.mkdir(exist_ok=True) + log.write_text(json.dumps({"ok": True}), encoding="utf-8") + commands.append( + { + "argv": argv, + "log": str(log.relative_to(tmp_path)), + "sha256": hashlib.sha256(log.read_bytes()).hexdigest(), + } + ) gates = { "official_build": { "evidence": { "wheel": { "path": str(wheel.relative_to(tmp_path)), - "sha256": gate._sha256(wheel), + "sha256": wheel_sha256, "size": wheel.stat().st_size, }, }, }, + "installed_wheel": { + "commands": commands, + "evidence": { + "schema_version": 2, + "python_executable": runtime["python_executable"], + "distribution_root": "/proof/site-packages", + "package_file": runtime["pops_file"], + "native_extension": runtime["native_extension"], + "native_member": native_member, + "native_sha256": hashlib.sha256(native_bytes).hexdigest(), + "installed_member_count": len(rows), + "installed_tree_sha256": hashlib.sha256( + "".join(rows).encode("utf-8") + ).hexdigest(), + "proof_script_sha256": hashlib.sha256( + (SCRIPTS / "prove_installed_wheel.py").read_bytes() + ).hexdigest(), + "version": "0.3.0", + "wheel_path": str(wheel), + "wheel_sha256": wheel_sha256, + }, + }, } release = type("ReleaseContract", (), {"PACKAGE_VERSION": "0.3.0"}) - preflight._wheel_evidence(tmp_path, gates, release) - gates["official_build"]["evidence"]["wheel"]["size"] += 1 - with pytest.raises(preflight.PreflightError, match="size drifted"): - preflight._wheel_evidence(tmp_path, gates, release) + preflight._installed_wheel_evidence(tmp_path, gates, release, runtime) + gates["installed_wheel"]["evidence"]["native_sha256"] = "0" * 64 + with pytest.raises(preflight.PreflightError, match="native member hash drifted"): + preflight._installed_wheel_evidence(tmp_path, gates, release, runtime) + + +def test_installed_wheel_gate_precedes_codesign_and_conformance(): + gates = contract.REQUIRED_RELEASE_GATES + + assert gates.index("official_build") < gates.index("installed_wheel") + assert gates.index("installed_wheel") < gates.index("codesign") + assert gates.index("codesign") < gates.index("doctor") + assert gates.index("codesign") < gates.index("native_conformance") + + +def test_release_preflight_binds_codesign_to_live_runtime(tmp_path): + log = tmp_path / "logs" / "codesign.log" + log.parent.mkdir() + log.write_text('{"platform": "darwin"}\n', encoding="utf-8") + runtime = { + "python_executable": "/proof/bin/python", + "pops_file": "/proof/site-packages/pops/__init__.py", + "native_extension": "/proof/site-packages/pops/_pops.so", + "native_sha256": "a" * 64, + } + gates = { + "installed_wheel": { + "evidence": {"native_sha256": runtime["native_sha256"]}, + }, + "codesign": { + "commands": [ + { + "argv": [ + "/proof/conda", + "run", + "python", + "scripts/codesign_pops_extensions.py", + "--json", + ], + "log": str(log.relative_to(tmp_path)), + "sha256": hashlib.sha256(log.read_bytes()).hexdigest(), + } + ], + "evidence": { + "schema_version": 1, + "platform": "darwin", + "extensions": [ + { + "path": runtime["native_extension"], + "sha256": runtime["native_sha256"], + "signature": "adhoc", + } + ], + }, + }, + } + + preflight._codesign_evidence(tmp_path, gates, runtime) + gates["codesign"]["evidence"]["extensions"][0]["sha256"] = "b" * 64 + with pytest.raises(preflight.PreflightError, match="live native extension"): + preflight._codesign_evidence(tmp_path, gates, runtime) + + gates["codesign"]["evidence"]["extensions"][0]["sha256"] = runtime["native_sha256"] + gates["installed_wheel"]["evidence"]["native_sha256"] = "b" * 64 + with pytest.raises(preflight.PreflightError, match="published wheel"): + preflight._codesign_evidence(tmp_path, gates, runtime) + + +def test_installed_example_authenticates_native_bytes_before_execution( + monkeypatch, tmp_path, capsys, +): + package = tmp_path / "site-packages" / "pops" / "__init__.py" + extension = package.parent / "_pops.so" + package.parent.mkdir(parents=True) + package.write_text("", encoding="utf-8") + extension.write_bytes(b"signed release runtime") + native = types.ModuleType("pops._pops") + native.__file__ = str(extension) + native.__version__ = "1.0.0" + pops = types.ModuleType("pops") + pops.__file__ = str(package) + pops.__version__ = "1.0.0" + pops._pops = native + monkeypatch.setitem(sys.modules, "pops", pops) + monkeypatch.setitem(sys.modules, "pops._pops", native) + digest = hashlib.sha256(extension.read_bytes()).hexdigest() + + assert example_runner.verify_installed_runtime(digest) == digest + with pytest.raises(example_runner.InstalledExampleError, match="does not match"): + example_runner.verify_installed_runtime("0" * 64) + + monkeypatch.setattr(example_runner, "ROOT", tmp_path) + example = tmp_path / "example.py" + example.write_text( + "import sys\nprint('example_args=' + '|'.join(sys.argv[1:]))\n", + encoding="utf-8", + ) + monkeypatch.setattr(example_runner, "verify_installed_runtime", lambda expected: expected) + assert example_runner.main([ + "--runtime-sha256", + digest, + "--example", + str(example), + "--", + "--output-dir", + "/proof/output", + ]) == 0 + output = capsys.readouterr().out + assert example_runner.RUNTIME_MARKER + digest in output + assert "example_args=--output-dir|/proof/output" in output + + +def test_final_gate_rejects_incomplete_non_darwin_or_rewritten_codesign_runtime(): + evidence = { + "schema_version": 1, + "platform": "darwin", + "extensions": [ + { + "path": "/proof/pops/_pops.so", + "sha256": "a" * 64, + "signature": "adhoc", + } + ], + } + + assert gate._signed_runtime_sha256( + evidence, retained_native_sha256="a" * 64 + ) == "a" * 64 + with pytest.raises(gate.FinalGateError, match="different from the validated runtime"): + gate._signed_runtime_sha256(evidence, retained_native_sha256="b" * 64) + evidence["platform"] = "linux" + with pytest.raises(gate.FinalGateError, match="Darwin release proof"): + gate._signed_runtime_sha256(evidence, retained_native_sha256="a" * 64) + + +def test_release_preflight_requires_exact_runtime_bound_example_commands(tmp_path): + runtime = { + "python_executable": "/proof/bin/python", + "pops_file": "/proof/site-packages/pops/__init__.py", + "native_extension": "/proof/site-packages/pops/_pops.so", + "native_sha256": "c" * 64, + } + examples = {} + reopened = {} + restarted = {} + commands = [] + for index, example in enumerate(contract.FINAL_EXAMPLES, 1): + key = example.as_posix() + output_root = tmp_path / "examples" / example.stem + output_root.mkdir(parents=True) + hdf5 = output_root / "state.h5" + hdf5.write_bytes(b"\x89HDF\r\n\x1a\npayload") + npz = output_root / "state.npz" + with zipfile.ZipFile(npz, "w") as archive: + archive.writestr("state.npy", b"payload") + paraview = output_root / "state.vtu" + paraview.write_text("", encoding="utf-8") + checkpoint = output_root / "checkpoint.bin" + checkpoint.write_bytes(b"restart") + transcript = "\n".join( + [ + example_runner.RUNTIME_MARKER + runtime["native_sha256"], + *contract.REQUIRED_PROOF_MARKERS, + ] + ) + "\n" + log = tmp_path / "logs" / f"{index:02d}_examples.log" + log.parent.mkdir(exist_ok=True) + log.write_text(transcript, encoding="utf-8") + commands.append( + { + "argv": [ + "/proof/conda", + "run", + "python", + "scripts/run_installed_example.py", + "--runtime-sha256", + runtime["native_sha256"], + "--example", + key, + "--", + "--output-dir", + str(output_root), + ], + "log": str(log.relative_to(tmp_path)), + "sha256": hashlib.sha256(log.read_bytes()).hexdigest(), + } + ) + examples[key] = { + "source_sha256": hashlib.sha256((ROOT / example).read_bytes()).hexdigest(), + "stdout_sha256": hashlib.sha256(transcript.encode("utf-8")).hexdigest(), + "output_root": str(output_root.relative_to(tmp_path)), + "runtime_sha256": runtime["native_sha256"], + } + reopened[key] = { + "hdf5": [ + { + "path": hdf5.name, + "sha256": hashlib.sha256(hdf5.read_bytes()).hexdigest(), + } + ], + "npz": [ + { + "path": npz.name, + "sha256": hashlib.sha256(npz.read_bytes()).hexdigest(), + } + ], + "paraview": [ + { + "path": paraview.name, + "sha256": hashlib.sha256(paraview.read_bytes()).hexdigest(), + } + ], + } + restarted[key] = { + "checkpoint": str(checkpoint), + "tree_sha256": hashlib.sha256(checkpoint.read_bytes()).hexdigest(), + "proof_markers": list(contract.REQUIRED_PROOF_MARKERS), + } + gates = { + "examples": {"commands": commands, "evidence": {"examples": examples}}, + "artifact_reopen": {"evidence": {"examples": reopened}}, + "strict_restart": {"evidence": {"examples": restarted}}, + } + + preflight._examples_evidence(tmp_path, gates, runtime) + commands[0]["argv"][commands[0]["argv"].index(runtime["native_sha256"])] = "d" * 64 + with pytest.raises(preflight.PreflightError, match="command drifted"): + preflight._examples_evidence(tmp_path, gates, runtime) def test_tag_release_cannot_race_or_bypass_supported_matrix_wheel_and_final_gate():