From 8fd45623868c50dd515ccdd048a1d187c82da9e9 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:43:42 +0300 Subject: [PATCH 1/9] =?UTF-8?q?Proof:=20=D0=B7=D0=B0=D0=BF=D0=B5=D1=87?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D1=82=D1=8C=20MPFI=20source=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/PROTOCOL.md | 28 +- proof/region/v1/arb/tests/test_transport.py | 8 + proof/region/v1/build/input.py | 5 +- proof/region/v1/mpfi/__init__.py | 1 + proof/region/v1/mpfi/input.py | 239 +++++++++ proof/region/v1/tests/test_mpfi_input.py | 554 ++++++++++++++++++++ 6 files changed, 829 insertions(+), 6 deletions(-) create mode 100644 proof/region/v1/mpfi/__init__.py create mode 100644 proof/region/v1/mpfi/input.py create mode 100644 proof/region/v1/tests/test_mpfi_input.py diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index fcdd4595..6b966f70 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -12,9 +12,10 @@ protocol fixtures и не является evaluator runner. `arb/evaluator` в Arb-enclosures и выпускает связанные transcript bytes; `SourceBoundArbControllerV1` заново собирает evaluator, запускает его и создаёт только provenance receipt. Ни один из этих путей не выполняет независимый -semantic replay и не создаёт mathematical proof type. MPFI source lock и -archive admission уже представлены, но MPFI evaluator/source-bound receipt и -semantic verifier в текущем release отсутствуют. +semantic replay и не создаёт mathematical proof type. MPFI source lock, +archive admission и sealed source input уже представлены, но MPFI +evaluator/source-bound receipt и semantic verifier в текущем release +отсутствуют. Structural protocol/admission сам не является математическим proof. `DualComparisonCandidateV1` кодирует только structural agreement и не создаёт @@ -245,6 +246,22 @@ lane entries, кодирует один канонический USTAR и вла caller digest и не утверждает recipe либо engine semantics. Resource bounds передаёт lane: общий encoder не вводит собственный fixture-specific cap. +`mpfi/input.py` строит `SealedInputV1` только из заново допущенной пары +`MpfiSourceLockV1` и `AdmittedMpfiSourcesV1`. Он повторно материализует exact +regular files, помещает их в versioned MPFI-only namespace +`sources//` и связывает свежую aggregate source capability с +exact USTAR bytes. Роль, а не archive root, разделяет три source trees: lock +не требует уникальности root. Целостность `SealedInputV1` сама по себе не +доказывает принадлежность MPFI closure; это отдельно перепроверяет MPFI +binding. Caller передаёт canonical `CanonicalInputLimitsV1`: lane сверяет +declared exact file count и payload closure до replay, а общий encoder сверяет +все final USTAR bounds после materialization. Limits — operational boundary, не +координата MPFI source binding и не build policy. Для неверного public +capability boundary возвращается `MpfiSourceInputErrorV1`; failure exact source +replay остаётся `ProvenanceErrorV1`, а limits/USTAR rejection — `InputErrorV1`. +Эта ступень не вводит recipe, Docker policy, BUILD/RUN authority, executable, +comparator, receipt или semantic verifier. + `proof/region/v1/build/transport.py` владеет immutable Docker policy, одноразовым probe→build lease, bounded stdin/stdout observation, cleanup и двумя свежими попытками. Доказательные координаты разделены по причинам: @@ -283,8 +300,9 @@ cleanup, без ложного заявления о reap CLI. `TwoBuildObservat контракта, выявленное до неё, может не иметь ни session, ни process prefix. Transport не знает formula, ELF, comparator или source provenance: lane отдельно перепроверяет semantic input binding перед -каждым process и передаёт output admission. Arb объявляет собственную exact -policy; MPFI обязан объявить другую, а не заимствовать Arb semantics. +каждым process и передаёт output admission. MPFI sealed source input ещё не +является MPFI build policy; будущая policy должна быть объявлена отдельно и не +может заимствовать Arb semantics. ## Воспроизведение Arb, связанное с источником diff --git a/proof/region/v1/arb/tests/test_transport.py b/proof/region/v1/arb/tests/test_transport.py index e794da38..4ec1aae0 100644 --- a/proof/region/v1/arb/tests/test_transport.py +++ b/proof/region/v1/arb/tests/test_transport.py @@ -369,6 +369,14 @@ def reject( hashlib.sha256(encoded).hexdigest(), "11bc313cba907e89535876eb8ce46194472367007053ab58b723338676f99427", ) + private_mode = build_input.canonical_ustar_v1( + (("private", 0o700, b"x"),), + _TEST_CANONICAL_LIMITS, + ) + with tarfile.open(fileobj=io.BytesIO(private_mode), mode="r:") as archive: + member = archive.getmember("private") + self.assertTrue(member.isreg()) + self.assertEqual(member.mode, 0o700) for hostile, reason, field in ( ( tuple(reversed(entries)), diff --git a/proof/region/v1/build/input.py b/proof/region/v1/build/input.py index f09cf886..8352792e 100644 --- a/proof/region/v1/build/input.py +++ b/proof/region/v1/build/input.py @@ -15,6 +15,9 @@ _USTAR_BLOCK_BYTES = 512 _USTAR_RECORD_BYTES = 20 * _USTAR_BLOCK_BYTES _USTAR_EOF_BLOCKS = 2 +# Канонический input сохраняет privacy bit допущенного regular file: замена +# 0700 на 0755 незаметно меняла бы owned bytes и смысл сборки. +_REGULAR_FILE_MODES_V1 = frozenset((0o644, 0o700, 0o755)) def _valid_digest(value: object) -> bool: @@ -270,7 +273,7 @@ def canonical_ustar_v1( path = _logical_path(path) if not _ustar_path_is_encodable(path): _fail(InputReasonV1.INVALID_PATH, path) - if type(mode) is not int or mode not in (0o644, 0o755): + if type(mode) is not int or mode not in _REGULAR_FILE_MODES_V1: _fail(InputReasonV1.INVALID_MODE, path) if type(contents) is not bytes: _fail(InputReasonV1.WRONG_TYPE, path) diff --git a/proof/region/v1/mpfi/__init__.py b/proof/region/v1/mpfi/__init__.py new file mode 100644 index 00000000..82942371 --- /dev/null +++ b/proof/region/v1/mpfi/__init__.py @@ -0,0 +1 @@ +"""MPFI-специфичные границы proof V1.""" diff --git a/proof/region/v1/mpfi/input.py b/proof/region/v1/mpfi/input.py new file mode 100644 index 00000000..91e52869 --- /dev/null +++ b/proof/region/v1/mpfi/input.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""MPFI-owned source closure, materialized как один sealed generic input.""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from enum import StrEnum +from typing import NoReturn + +import provenance +from build import input as build_input + + +_MPFI_SOURCE_INPUT_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-source-input.v1\0" +# Это release layout, не MPFI build recipe: source role — единственная +# инъективная namespace coordinate, гарантированная locked closure. +_MPFI_SOURCE_INPUT_LAYOUT_V1 = b"sources//" +_SOURCE_NAMESPACE_V1 = { + provenance.SourceRoleV1.GMP: "gmp", + provenance.SourceRoleV1.MPFR: "mpfr", + provenance.SourceRoleV1.MPFI: "mpfi", +} + + +class MpfiSourceInputReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + FOREIGN_SOURCE_CAPABILITY = "foreign_source_capability" + + +@dataclass(frozen=True) +class MpfiSourceInputErrorV1(ValueError): + reason: MpfiSourceInputReasonV1 + field: str + + def __str__(self) -> str: + return f"{self.reason.value}: {self.field}" + + +def _fail(reason: MpfiSourceInputReasonV1, field_name: str) -> NoReturn: + raise MpfiSourceInputErrorV1(reason, field_name) + + +def _canonical_lock_v1( + source_lock: provenance.MpfiSourceLockV1, +) -> provenance.MpfiSourceLockV1: + """Отбрасывает cached hostile state до именования input capability.""" + + if type(source_lock) is not provenance.MpfiSourceLockV1: + _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "source_lock") + try: + return provenance.MpfiSourceLockV1.parse(source_lock.encode()) + except provenance.ProvenanceErrorV1: + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "source_lock") + except (AttributeError, TypeError, ValueError, OverflowError): + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "source_lock") + + +def _fresh_admitted_sources_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, +) -> provenance.AdmittedMpfiSourcesV1: + if type(admitted_sources) is not provenance.AdmittedMpfiSourcesV1: + _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "admitted_sources") + try: + source_lock_identity = admitted_sources.source_lock_identity + if ( + type(source_lock_identity) is not bytes + or len(source_lock_identity) != 32 + or source_lock_identity == bytes(32) + or source_lock_identity != source_lock.identity + ): + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + # Re-admission сохраняет semantic slot order: сортировка выдала бы + # forged GMP/MPFR exchange за легитимный closure. + return provenance.admit_mpfi_sources(source_lock, admitted_sources.sources) + except provenance.ProvenanceErrorV1 as error: + if error.reason is provenance.ProvenanceReasonV1.FOREIGN_BINDING: + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + raise + except (AttributeError, TypeError, ValueError, OverflowError): + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + + +def _canonical_limits_v1( + limits: build_input.CanonicalInputLimitsV1, +) -> build_input.CanonicalInputLimitsV1: + if type(limits) is not build_input.CanonicalInputLimitsV1: + _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "limits") + try: + canonical = build_input.CanonicalInputLimitsV1(*tuple(limits)) + except build_input.InputErrorV1: + raise + except (AttributeError, TypeError, ValueError, OverflowError): + raise build_input.InputErrorV1( + build_input.InputReasonV1.NONCANONICAL_SET, + "limits", + ) + if tuple(canonical) != tuple(limits): + raise build_input.InputErrorV1( + build_input.InputReasonV1.NONCANONICAL_SET, + "limits", + ) + return canonical + + +def _preflight_declared_resource_bounds_v1( + source_lock: provenance.MpfiSourceLockV1, + limits: build_input.CanonicalInputLimitsV1, +) -> None: + """Отклоняет declared totals до allocation file bodies во время replay.""" + + declared_file_count = sum( + item.regular_file_count for item in source_lock.sources + ) + declared_payload_bytes = sum( + item.regular_file_bytes for item in source_lock.sources + ) + if declared_file_count > limits.max_members: + raise build_input.InputErrorV1( + build_input.InputReasonV1.RESOURCE_LIMIT, + "max_members", + ) + if declared_payload_bytes > limits.max_payload_bytes: + raise build_input.InputErrorV1( + build_input.InputReasonV1.RESOURCE_LIMIT, + "max_payload_bytes", + ) + + +def _source_entries_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, +) -> tuple[tuple[str, int, bytes], ...]: + entries = tuple( + ( + f"sources/{_SOURCE_NAMESPACE_V1[lock.role]}/{relative}", + mode, + contents, + ) + for lock, admitted in zip( + source_lock.sources, + admitted_sources.sources, + strict=True, + ) + for relative, mode, contents in provenance.materialize_admitted_source_files_v1( + lock, + admitted, + ) + ) + if not entries: + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources") + return tuple(sorted(entries)) + + +def _binding_identity_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, + contents: bytes, +) -> bytes: + if type(contents) is not bytes or not contents: + raise TypeError("MPFI source input contents must be exact nonempty bytes") + coordinates = ( + _MPFI_SOURCE_INPUT_LAYOUT_V1, + source_lock.identity, + admitted_sources.identity, + len(contents).to_bytes(8, "big"), + hashlib.sha256(contents).digest(), + ) + preimage = b"".join(len(value).to_bytes(8, "big") + value for value in coordinates) + return hashlib.sha256( + _MPFI_SOURCE_INPUT_ID_LABEL_V1 + + len(preimage).to_bytes(8, "big") + + preimage + ).digest() + + +def seal_mpfi_source_input_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, + limits: build_input.CanonicalInputLimitsV1, +) -> build_input.SealedInputV1: + """Запечатывает MPFI source closure в caller-owned resource bounds. + + `MpfiSourceInputErrorV1` означает invalid public capability boundary, + `ProvenanceErrorV1` — failure exact source replay, а `InputErrorV1` — + canonical USTAR или resource-bound rejection. + """ + + canonical_lock = _canonical_lock_v1(source_lock) + canonical_limits = _canonical_limits_v1(limits) + canonical_admitted = _fresh_admitted_sources_v1(canonical_lock, admitted_sources) + _preflight_declared_resource_bounds_v1(canonical_lock, canonical_limits) + entries = _source_entries_v1(canonical_lock, canonical_admitted) + contents = build_input.canonical_ustar_v1(entries, canonical_limits) + return build_input.seal_input_v1( + _binding_identity_v1(canonical_lock, canonical_admitted, contents), + contents, + ) + + +def mpfi_source_input_is_bound_v1( + source_lock: object, + admitted_sources: object, + limits: object, + value: object, +) -> bool: + """Независимо пересобирает MPFI input, а не доверяет одному seal.""" + + if ( + type(value) is not build_input.SealedInputV1 + or not build_input.sealed_input_is_intact_v1(value) + ): + return False + try: + expected = seal_mpfi_source_input_v1(source_lock, admitted_sources, limits) + except ( + MpfiSourceInputErrorV1, + provenance.ProvenanceErrorV1, + build_input.InputErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + ): + return False + return ( + value.binding_identity == expected.binding_identity + and value.contents == expected.contents + ) diff --git a/proof/region/v1/tests/test_mpfi_input.py b/proof/region/v1/tests/test_mpfi_input.py new file mode 100644 index 00000000..a095409d --- /dev/null +++ b/proof/region/v1/tests/test_mpfi_input.py @@ -0,0 +1,554 @@ +#!/usr/bin/env python3 +"""RED-контракт границы MPFI admitted-source → sealed-input.""" + +from __future__ import annotations + +import ast +import hashlib +import io +import lzma +import sys +import tarfile +import unittest +from pathlib import Path +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import provenance # noqa: E402 +from build import input as build_input # noqa: E402 +from mpfi import input as mpfi_input # noqa: E402 + + +# Characterization-pins маленького three-source closure. Versioned layout +# меняет их только явным решением, а не silent drift. +SYNTHETIC_MPFI_INPUT_LENGTH_V1 = 10_240 +SYNTHETIC_MPFI_INPUT_SHA256_V1 = ( + "fac4761a9018ca55f467328dae238ccea0a280c08277e533a7bbef696eea567f" +) +SYNTHETIC_MPFI_INPUT_BINDING_V1 = ( + "d0adc51b30e68b672efcf7f3a4be4a6ec4171b9a1f053ef52787adeb713b6653" +) + + +def _sha256(value: bytes) -> bytes: + return hashlib.sha256(value).digest() + + +def _detached_policy() -> provenance.DetachedSignaturePolicyV1: + return provenance.DetachedSignaturePolicyV1( + "https://example.invalid/source.tar.xz.sig", + 3, + _sha256(b"signature"), + _sha256(b"packets"), + bytes.fromhex("00112233445566778899aabbccddeeff00112233"), + ) + + +def _fixture_archive( + *, + license_body: bytes, + value_body: bytes, + value_mode: int = 0o644, +) -> bytes: + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + root = tarfile.TarInfo("shared/") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + root.uid = 0 + root.gid = 0 + root.mtime = 0 + archive.addfile(root) + for name, body, mode in ( + ("LICENSE", license_body, 0o644), + ("value", value_body, value_mode), + ): + member = tarfile.TarInfo(f"shared/{name}") + member.mode = mode + member.uid = 0 + member.gid = 0 + member.mtime = 0 + member.size = len(body) + archive.addfile(member, io.BytesIO(body)) + return lzma.compress(raw.getvalue(), format=lzma.FORMAT_XZ) + + +def _fixture_release( + role: provenance.SourceRoleV1, + archive: bytes, + *, + license_body: bytes, + value_body: bytes, + value_mode: int = 0o644, +) -> provenance.SourceReleaseLockV1: + raw_tar = lzma.decompress(archive) + integrity: ( + provenance.DetachedSignaturePolicyV1 + | provenance.ProjectPinnedArchiveDigestPolicyV1 + ) + if role is provenance.SourceRoleV1.MPFI: + integrity = provenance.ProjectPinnedArchiveDigestPolicyV1() + else: + integrity = _detached_policy() + return provenance.SourceReleaseLockV1( + role, + "1", + f"https://example.invalid/{role.name.lower()}.tar.xz", + provenance.ArchiveFormatV1.TAR_XZ, + len(archive), + _sha256(archive), + len(raw_tar), + "shared/", + 2, + len(license_body) + len(value_body), + (provenance.LegalFileV1("LICENSE", len(license_body), _sha256(license_body)),), + integrity, + ) + + +def _admitted_closure( + *, + mpfr_value_mode: int = 0o755, +) -> tuple[ + provenance.MpfiSourceLockV1, + provenance.AdmittedMpfiSourcesV1, + tuple[tuple[str, int, bytes], ...], +]: + specifications = ( + (provenance.SourceRoleV1.GMP, b"gmp-license", b"gmp-source", 0o644), + (provenance.SourceRoleV1.MPFR, b"mpfr-license", b"mpfr-source", mpfr_value_mode), + (provenance.SourceRoleV1.MPFI, b"mpfi-license", b"mpfi-source", 0o644), + ) + releases: list[provenance.SourceReleaseLockV1] = [] + archives: list[bytes] = [] + expected_entries: list[tuple[str, int, bytes]] = [] + for role, license_body, value_body, value_mode in specifications: + archive = _fixture_archive( + license_body=license_body, + value_body=value_body, + value_mode=value_mode, + ) + archives.append(archive) + releases.append( + _fixture_release( + role, + archive, + license_body=license_body, + value_body=value_body, + value_mode=value_mode, + ) + ) + namespace = role.name.lower() + expected_entries.extend( + ( + (f"sources/{namespace}/LICENSE", 0o644, license_body), + (f"sources/{namespace}/value", value_mode, value_body), + ) + ) + lock = provenance.MpfiSourceLockV1(tuple(releases)) + sources = tuple( + provenance.admit_source_archive(release, archive) + for release, archive in zip(lock.sources, archives, strict=True) + ) + return ( + lock, + provenance.admit_mpfi_sources(lock, sources), + tuple(sorted(expected_entries)), + ) + + +def _limits_for_entries( + entries: tuple[tuple[str, int, bytes], ...], +) -> build_input.CanonicalInputLimitsV1: + directories = { + "/".join(path.split("/")[:length]) + for path, _mode, _contents in entries + for length in range(1, len(path.split("/"))) + } + return build_input.CanonicalInputLimitsV1( + len(entries) + len(directories), + max(len(contents) for _path, _mode, contents in entries), + sum(len(contents) for _path, _mode, contents in entries), + ) + + +def _regular_ustar_entries(value: build_input.SealedInputV1) -> tuple[tuple[str, int, bytes], ...]: + with tarfile.open(fileobj=io.BytesIO(value.contents), mode="r:") as archive: + return tuple( + (member.name, member.mode, archive.extractfile(member).read()) + for member in archive.getmembers() + if member.isreg() + ) + + +def _imported_module_names(source: str) -> tuple[str, ...]: + modules: list[str] = [] + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Import): + modules.extend(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom): + prefix = node.module or "" + modules.extend( + ".".join(part for part in (prefix, alias.name) if part) + for alias in node.names + ) + return tuple(modules) + + +class MpfiSourceInputTests(unittest.TestCase): + def test_three_same_root_archives_become_one_deterministic_lane_input(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + + first = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + second = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + + self.assertIs(type(first), build_input.SealedInputV1) + self.assertTrue(build_input.sealed_input_is_intact_v1(first)) + self.assertEqual(first, second) + self.assertEqual(first.length, SYNTHETIC_MPFI_INPUT_LENGTH_V1) + self.assertEqual(first.sha256.hex(), SYNTHETIC_MPFI_INPUT_SHA256_V1) + self.assertEqual( + first.binding_identity.hex(), + SYNTHETIC_MPFI_INPUT_BINDING_V1, + ) + self.assertEqual(_regular_ustar_entries(first), expected_entries) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1(lock, admitted, limits, first) + ) + relaxed_limits = build_input.CanonicalInputLimitsV1( + limits.max_members + 1, + limits.max_file_bytes + 1, + limits.max_payload_bytes + 1, + ) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + relaxed_limits, + first, + ) + ) + with tarfile.open(fileobj=io.BytesIO(first.contents), mode="r:") as archive: + members = archive.getmembers() + self.assertEqual( + tuple(sorted(member.name for member in members if member.isdir())), + ("sources", "sources/gmp", "sources/mpfi", "sources/mpfr"), + ) + self.assertTrue(all(member.uid == 0 and member.gid == 0 for member in members)) + self.assertTrue(all(member.mtime == 0 for member in members)) + + def test_binding_rechecks_the_closure_and_exact_ustar_bytes(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + + foreign_binding = build_input.seal_input_v1(_sha256(b"foreign"), sealed.contents) + changed_entries = list(expected_entries) + path, mode, contents = changed_entries[0] + changed_entries[0] = (path, mode, contents + b"!") + changed_contents = build_input.canonical_ustar_v1( + tuple(changed_entries), + _limits_for_entries(tuple(changed_entries)), + ) + stale_binding = build_input.seal_input_v1(sealed.binding_identity, changed_contents) + + self.assertTrue(build_input.sealed_input_is_intact_v1(foreign_binding)) + self.assertTrue(build_input.sealed_input_is_intact_v1(stale_binding)) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + foreign_binding, + ) + ) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + stale_binding, + ) + ) + + def test_reordered_or_foreign_closure_is_rejected_before_ustar_encoding( + self, + ) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + _ = admitted.identity + original_sources = admitted.sources + object.__setattr__( + admitted, + "sources", + (original_sources[1], original_sources[0], original_sources[2]), + ) + try: + with mock.patch.object(mpfi_input.build_input, "canonical_ustar_v1") as encoder: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + encoder.assert_not_called() + finally: + object.__setattr__(admitted, "sources", original_sources) + admitted.__dict__.pop("identity", None) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + def test_cached_lock_identity_cannot_hide_source_or_capability_drift(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + cached_lock_identity = lock.identity + original_version = lock.sources[0].version + object.__setattr__(lock.sources[0], "version", "2") + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1(lock, admitted, limits, sealed) + ) + finally: + object.__setattr__(lock.sources[0], "version", original_version) + lock.__dict__.pop("identity", None) + lock.__dict__["identity"] = cached_lock_identity + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + source = admitted.sources[2] + original_tree_identity = source.tree_identity + object.__setattr__(source, "tree_identity", _sha256(b"foreign tree")) + try: + with self.assertRaises(provenance.ProvenanceErrorV1): + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1(lock, admitted, limits, sealed) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + + def test_wrong_public_capability_types_are_typed_rejections(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + for hostile_lock, hostile_admitted, hostile_limits, field_name in ( + (object(), admitted, limits, "source_lock"), + (lock, object(), limits, "admitted_sources"), + (lock, admitted, object(), "limits"), + ): + with self.subTest(field=field_name): + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + hostile_lock, + hostile_admitted, + hostile_limits, + ) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.WRONG_TYPE, + ) + self.assertEqual(caught.exception.field, field_name) + + def test_locked_mode_is_preserved_without_silent_normalization(self) -> None: + lock, admitted, expected_entries = _admitted_closure(mpfr_value_mode=0o700) + + sealed = mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + _limits_for_entries(expected_entries), + ) + + self.assertEqual(_regular_ustar_entries(sealed), expected_entries) + + def test_limits_reject_declared_closure_before_archive_materialization(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + total_files = len(expected_entries) + total_payload = sum(len(contents) for _path, _mode, contents in expected_entries) + max_file = max(len(contents) for _path, _mode, contents in expected_entries) + cases = ( + ( + build_input.CanonicalInputLimitsV1(total_files - 1, max_file, total_payload), + "max_members", + ), + ( + build_input.CanonicalInputLimitsV1(total_files + 4, max_file, total_payload - 1), + "max_payload_bytes", + ), + ) + for limits, field in cases: + with self.subTest(limit=field): + with mock.patch.object( + mpfi_input.provenance, + "materialize_admitted_source_files_v1", + ) as materialize: + with self.assertRaises(build_input.InputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + materialize.assert_not_called() + self.assertEqual(caught.exception.reason, build_input.InputReasonV1.RESOURCE_LIMIT) + self.assertEqual(caught.exception.field, field) + + def test_final_ustar_limits_remain_typed_rejections(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + cases = ( + ( + build_input.CanonicalInputLimitsV1( + limits.max_members, + limits.max_file_bytes - 1, + limits.max_payload_bytes, + ), + "max_file_bytes", + ), + ( + build_input.CanonicalInputLimitsV1( + limits.max_members, + limits.max_file_bytes, + limits.max_payload_bytes, + sealed.length - 1, + ), + "max_encoded_bytes", + ), + ) + for constrained, field in cases: + with self.subTest(limit=field): + with self.assertRaises(build_input.InputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, constrained) + self.assertEqual(caught.exception.reason, build_input.InputReasonV1.RESOURCE_LIMIT) + self.assertEqual(caught.exception.field, field) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + constrained, + sealed, + ) + ) + + def test_missing_capability_field_is_a_typed_rejection(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + original = admitted.source_lock_identity + object.__delattr__(admitted, "source_lock_identity") + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + limits, + ) + finally: + object.__setattr__(admitted, "source_lock_identity", original) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + class ExplodesOnComparison: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("comparison ran") + + object.__setattr__(admitted, "source_lock_identity", ExplodesOnComparison()) + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + finally: + object.__setattr__(admitted, "source_lock_identity", original) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + def test_noncanonical_exact_type_lock_is_a_typed_rejection(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + original_version = lock.sources[0].version + object.__setattr__(lock.sources[0], "version", "\0") + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + _limits_for_entries(expected_entries), + ) + finally: + object.__setattr__(lock.sources[0], "version", original_version) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + def test_source_input_owner_has_no_engine_dependency(self) -> None: + source_path = ROOT / "mpfi" / "input.py" + self.assertTrue(source_path.is_file()) + tree = ast.parse(source_path.read_text(encoding="utf-8")) + imported_modules = _imported_module_names(source_path.read_text(encoding="utf-8")) + forbidden = ( + "arb", + "pipeline", + "receipt", + "executor", + "transport", + "formula", + "controller", + "region_proof_protocol", + ) + self.assertFalse( + any( + name in module.split(".") + for module in imported_modules + for name in forbidden + ), + ) + self.assertFalse( + any( + isinstance(node, ast.Call) + and ( + isinstance(node.func, ast.Name) + and node.func.id == "__import__" + or isinstance(node.func, ast.Attribute) + and node.func.attr == "import_module" + ) + for node in ast.walk(tree) + ), + ) + self.assertFalse((ROOT / "mpfi" / "build").exists()) + + def test_import_guard_resolves_from_import_targets(self) -> None: + self.assertEqual( + _imported_module_names( + "from build import transport\n" + "from proof.region.v1.arb import pipeline\n" + ), + ("build.transport", "proof.region.v1.arb.pipeline"), + ) + + def test_protocol_keeps_the_source_input_boundary_below_build_authority(self) -> None: + reference = " ".join( + (ROOT / "PROTOCOL.md").read_text(encoding="utf-8").split() + ) + + self.assertIn("`mpfi/input.py`", reference) + self.assertIn("`sources//`", reference) + self.assertIn("не требует уникальности root", reference) + self.assertIn("Caller передаёт canonical `CanonicalInputLimitsV1`", reference) + self.assertIn("`MpfiSourceInputErrorV1`", reference) + self.assertIn("`ProvenanceErrorV1`", reference) + self.assertIn("`InputErrorV1`", reference) + self.assertIn( + "не вводит recipe, Docker policy, BUILD/RUN authority", + reference, + ) + self.assertIn( + "MPFI sealed source input ещё не является MPFI build policy", + reference, + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 173c4efff91f29afc275434e1f7d3d79c9df72e3 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:01:20 +0300 Subject: [PATCH 2/9] =?UTF-8?q?Test:=20=D0=B7=D0=B0=D0=BF=D0=B5=D1=87?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D1=82=D1=8C=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9?= =?UTF-8?q?=20proof=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/tests/gate.py | 54 ++++++++++++++++++++ proof/region/v1/tests/test_build.py | 77 +++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 proof/region/v1/tests/gate.py diff --git a/proof/region/v1/tests/gate.py b/proof/region/v1/tests/gate.py new file mode 100644 index 00000000..c96588d0 --- /dev/null +++ b/proof/region/v1/tests/gate.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Точный состав общего fast proof-suite без engine-specific зависимости.""" + +from __future__ import annotations + +import hashlib +import unittest +from collections.abc import Iterator +from pathlib import Path + + +TEST_DIRECTORY = Path(__file__).resolve().parent +EXPECTED_TEST_COUNT_V1 = 166 +EXPECTED_TEST_INVENTORY_SHA256_V1 = ( + "1690f11e76b57e532f9ca5fa7cccd298438b72fb9a5327192e57c3d84c899715" +) + + +def _iter_tests_v1(suite: unittest.TestSuite) -> Iterator[unittest.TestCase]: + for item in suite: + if isinstance(item, unittest.TestSuite): + yield from _iter_tests_v1(item) + elif isinstance(item, unittest.TestCase): + yield item + else: + raise TypeError("suite contains a non-test object") + + +def _inventory_preimage_v1(test_ids: tuple[str, ...]) -> bytes: + return b"".join(test_id.encode("utf-8") + b"\n" for test_id in sorted(test_ids)) + + +def test_count_v1(suite: unittest.TestSuite) -> int: + return sum(1 for _test in _iter_tests_v1(suite)) + + +def test_inventory_sha256_v1(suite: unittest.TestSuite) -> str: + test_ids = tuple(test.id() for test in _iter_tests_v1(suite)) + return hashlib.sha256(_inventory_preimage_v1(test_ids)).hexdigest() + + +def full_suite_v1() -> unittest.TestSuite: + """Один engine-neutral suite, который CI уже запускает целиком.""" + + return unittest.defaultTestLoader.discover(str(TEST_DIRECTORY), pattern="test_*.py") + + +def inventory_is_exact_v1(suite: unittest.TestSuite) -> bool: + """Не даёт исчезнуть contract-тесту за общим minimum-count порогом CI.""" + + return ( + test_count_v1(suite) == EXPECTED_TEST_COUNT_V1 + and test_inventory_sha256_v1(suite) == EXPECTED_TEST_INVENTORY_SHA256_V1 + ) diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index 3a076234..7b67d58f 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -26,6 +26,7 @@ import pipeline # noqa: E402 from proof.region.v1.arb.tests import gate as arb_gate # noqa: E402 +from proof.region.v1.tests import gate as proof_gate # noqa: E402 from test_pipeline import ( # noqa: E402 _docker_capability, _probe_native_backend, @@ -45,6 +46,14 @@ ) ARB_TEST_COUNT_V1 = 184 +# Независимый внешний oracle общего proof-suite. Он живёт в уже обязательном +# discovery leaf, поэтому добавление/удаление MPFI-contract теста нельзя +# незаметно прикрыть только редактированием будущего общего gate. +SHARED_PROOF_TEST_INVENTORY_SHA256_V1 = ( + "1690f11e76b57e532f9ca5fa7cccd298438b72fb9a5327192e57c3d84c899715" +) +SHARED_PROOF_TEST_COUNT_V1 = 166 + MOVED_INPUT_SURFACE_V1 = ( "CanonicalInputLimitsV1", "SealedInputV1", @@ -284,6 +293,74 @@ def test_existing_arb_suite_keeps_exact_count_order_and_inventory(self) -> None: ) +class ExistingSharedProofGateTests(unittest.TestCase): + def test_mandatory_shared_proof_suite_keeps_exact_inventory(self) -> None: + suite = proof_gate.full_suite_v1() + + self.assertEqual( + proof_gate.test_count_v1(suite), + SHARED_PROOF_TEST_COUNT_V1, + ) + self.assertEqual( + proof_gate.test_inventory_sha256_v1(suite), + SHARED_PROOF_TEST_INVENTORY_SHA256_V1, + ) + self.assertTrue(proof_gate.inventory_is_exact_v1(suite)) + + def test_exact_inventory_rejects_a_missing_mpfi_input_contract(self) -> None: + suite = proof_gate.full_suite_v1() + without_mpfi_input = unittest.TestSuite( + test + for test in proof_gate._iter_tests_v1(suite) + if not test.id().startswith("test_mpfi_input.") + ) + + self.assertLess( + proof_gate.test_count_v1(without_mpfi_input), + proof_gate.test_count_v1(suite), + ) + self.assertFalse(proof_gate.inventory_is_exact_v1(without_mpfi_input)) + + def test_exact_inventory_rejects_same_count_contract_replacement(self) -> None: + class ReplacementTest(unittest.TestCase): + def test_replacement(self) -> None: + pass + + suite = proof_gate.full_suite_v1() + tests = tuple(proof_gate._iter_tests_v1(suite)) + replaced = next( + test for test in tests if test.id().startswith("test_mpfi_input.") + ) + replacement = unittest.defaultTestLoader.loadTestsFromTestCase(ReplacementTest) + same_count_replacement = unittest.TestSuite( + (*tuple(test for test in tests if test is not replaced), replacement) + ) + + self.assertEqual( + proof_gate.test_count_v1(same_count_replacement), + proof_gate.test_count_v1(suite), + ) + self.assertFalse(proof_gate.inventory_is_exact_v1(same_count_replacement)) + + def test_mpfi_input_contract_cannot_green_by_skipping(self) -> None: + suite = unittest.defaultTestLoader.discover( + str(proof_gate.TEST_DIRECTORY), + pattern="test_mpfi_input.py", + ) + test_ids = tuple(test.id() for test in proof_gate._iter_tests_v1(suite)) + result = unittest.TestResult() + + suite.run(result) + + self.assertTrue(test_ids) + self.assertEqual(result.testsRun, len(test_ids)) + self.assertFalse(result.skipped) + self.assertFalse(result.failures) + self.assertFalse(result.errors) + self.assertFalse(result.expectedFailures) + self.assertFalse(result.unexpectedSuccesses) + + class ArbBuildIdentityCharacterizationTests(unittest.TestCase): def test_arb_input_and_process_stay_exact_while_capability_identities_move_to_v2(self) -> None: transport = importlib.import_module("build.transport") From 4c78b4e98f2c51138676b3549480b3875b76c897 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:23:21 +0300 Subject: [PATCH 3/9] =?UTF-8?q?Test:=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=B8=D1=82=D1=8C=20MPFI=20input=20=D0=B2=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8F=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=BC=20?= =?UTF-8?q?proof=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/PROTOCOL.md | 15 ++-- proof/region/v1/arb/tests/gate.py | 21 +++-- .../region/v1/arb/tests/test_build_recipe.py | 60 ++++++++++--- proof/region/v1/mpfi/input.py | 4 +- proof/region/v1/tests/gate.py | 54 ------------ proof/region/v1/tests/test_build.py | 85 +------------------ proof/region/v1/tests/test_mpfi_input.py | 39 +++++++-- 7 files changed, 108 insertions(+), 170 deletions(-) delete mode 100644 proof/region/v1/tests/gate.py diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index 6b966f70..462e15d3 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -13,7 +13,7 @@ Arb-enclosures и выпускает связанные transcript bytes; `SourceBoundArbControllerV1` заново собирает evaluator, запускает его и создаёт только provenance receipt. Ни один из этих путей не выполняет независимый semantic replay и не создаёт mathematical proof type. MPFI source lock, -archive admission и sealed source input уже представлены, но MPFI +archive admission и sealed source input (не evaluator replay) уже представлены, но MPFI evaluator/source-bound receipt и semantic verifier в текущем release отсутствуют. @@ -253,11 +253,12 @@ regular files, помещает их в versioned MPFI-only namespace exact USTAR bytes. Роль, а не archive root, разделяет три source trees: lock не требует уникальности root. Целостность `SealedInputV1` сама по себе не доказывает принадлежность MPFI closure; это отдельно перепроверяет MPFI -binding. Caller передаёт canonical `CanonicalInputLimitsV1`: lane сверяет -declared exact file count и payload closure до replay, а общий encoder сверяет -все final USTAR bounds после materialization. Limits — operational boundary, не -координата MPFI source binding и не build policy. Для неверного public -capability boundary возвращается `MpfiSourceInputErrorV1`; failure exact source +source-input binding. Caller передаёт canonical `CanonicalInputLimitsV1`: lane сверяет +declared exact file count и payload closure до повторной materialization archive +bytes, а общий encoder сверяет все final USTAR bounds после materialization. +Limits — operational boundary, не +координата MPFI source-input binding и не build policy. Для неверного public +capability boundary возвращается `MpfiSourceInputErrorV1`; failure exact archive replay остаётся `ProvenanceErrorV1`, а limits/USTAR rejection — `InputErrorV1`. Эта ступень не вводит recipe, Docker policy, BUILD/RUN authority, executable, comparator, receipt или semantic verifier. @@ -299,7 +300,7 @@ cleanup, без ложного заявления о reap CLI. `TwoBuildObservat валидной session сохраняется весь уже завершённый causal prefix; нарушение контракта, выявленное до неё, может не иметь ни session, ни process prefix. Transport не знает formula, ELF, comparator или -source provenance: lane отдельно перепроверяет semantic input binding перед +source provenance: engine lane отдельно перепроверяет свой engine-owned input binding перед каждым process и передаёт output admission. MPFI sealed source input ещё не является MPFI build policy; будущая policy должна быть объявлена отдельно и не может заимствовать Arb semantics. diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py index cbb658ec..3df4660b 100644 --- a/proof/region/v1/arb/tests/gate.py +++ b/proof/region/v1/arb/tests/gate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Run the complete fast Arb contract with an exact skip manifest.""" +"""Запускает обязательные быстрые proof-контракты с точным manifest skips.""" from __future__ import annotations @@ -14,8 +14,12 @@ SHARED_TEST_DIRECTORY = TEST_DIRECTORY.parents[1] / "tests" REPO = Path(__file__).resolve().parents[5] sys.path.insert(0, str(REPO)) +SHARED_FAST_TEST_PATTERNS_V1 = ( + "test_executor.py", + "test_mpfi_input.py", +) EXPECTED_TEST_INVENTORY_SHA256 = ( - "721fcceb07c3d73e30032814a181e9c3d86f2185cfb3382cc79b34e05618fa48" + "c74942a9240b68327921160f86fd948532849234bb6da00a0075a137fef098cc" ) _EVALUATOR_REASON = "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary" EXPECTED_SKIPS = frozenset( @@ -71,14 +75,17 @@ def test_inventory_sha256_v1(suite: unittest.TestSuite) -> str: def full_suite_v1() -> unittest.TestSuite: - """Compose the shared execution contract and Arb-only contract once each.""" + """Собирает обязательные общие proof-контракты и Arb-only contract.""" return unittest.TestSuite( - ( + tuple( unittest.defaultTestLoader.discover( str(SHARED_TEST_DIRECTORY), - pattern="test_executor.py", - ), + pattern=pattern, + ) + for pattern in SHARED_FAST_TEST_PATTERNS_V1 + ) + + ( unittest.defaultTestLoader.discover( str(TEST_DIRECTORY), pattern="test_*.py", @@ -131,7 +138,7 @@ def run_exact_suite_v1( ) return 1 print( - f"Arb fast gate: {len(tests)} tests, " + f"Proof fast gate: {len(tests)} tests, " f"inventory {actual_inventory_sha256}, " f"exact {len(actual_skips)}-skip manifest" ) diff --git a/proof/region/v1/arb/tests/test_build_recipe.py b/proof/region/v1/arb/tests/test_build_recipe.py index 98ea1e03..2cfe4838 100644 --- a/proof/region/v1/arb/tests/test_build_recipe.py +++ b/proof/region/v1/arb/tests/test_build_recipe.py @@ -19,26 +19,58 @@ class ArbBuildRecipeTests(unittest.TestCase): - def test_fast_gate_includes_the_shared_executor_suite_exactly_once(self) -> None: + def test_fast_gate_includes_each_shared_contract_suite_exactly_once(self) -> None: tests = tuple(arb_gate._iter_tests_v1(arb_gate.full_suite_v1())) identifiers = tuple(test.id() for test in tests) - executor_identifiers = tuple( - identifier for identifier in identifiers if identifier.startswith("test_executor.") - ) - expected = tuple( - test.id() - for test in arb_gate._iter_tests_v1( - unittest.defaultTestLoader.discover( - str(arb_gate.SHARED_TEST_DIRECTORY), - pattern="test_executor.py", + for pattern, module_prefix in ( + ("test_executor.py", "test_executor."), + ("test_mpfi_input.py", "test_mpfi_input."), + ): + with self.subTest(pattern=pattern): + included = tuple( + identifier + for identifier in identifiers + if identifier.startswith(module_prefix) + ) + expected = tuple( + test.id() + for test in arb_gate._iter_tests_v1( + unittest.defaultTestLoader.discover( + str(arb_gate.SHARED_TEST_DIRECTORY), + pattern=pattern, + ) + ) ) - ) - ) - self.assertTrue(executor_identifiers) - self.assertEqual(executor_identifiers, expected) + self.assertTrue(expected) + self.assertEqual(included, expected) self.assertEqual(len(identifiers), len(set(identifiers))) + def test_mpfi_input_contract_cannot_green_by_skipping(self) -> None: + suite = unittest.defaultTestLoader.discover( + str(arb_gate.SHARED_TEST_DIRECTORY), + pattern="test_mpfi_input.py", + ) + test_ids = tuple(test.id() for test in arb_gate._iter_tests_v1(suite)) + result = unittest.TestResult() + + suite.run(result) + + self.assertTrue(test_ids) + self.assertTrue( + all(test_id.startswith("test_mpfi_input.") for test_id in test_ids) + ) + expected_skip_ids = { + test_id for test_id, _reason in arb_gate.EXPECTED_SKIPS + } + self.assertTrue(set(test_ids).isdisjoint(expected_skip_ids)) + self.assertEqual(result.testsRun, len(test_ids)) + self.assertFalse(result.skipped) + self.assertFalse(result.expectedFailures) + self.assertFalse(result.unexpectedSuccesses) + self.assertFalse(result.failures) + self.assertFalse(result.errors) + def test_pr_gate_requires_a_disposable_exact_workflow_runner(self) -> None: source = WORKFLOW.read_text(encoding="utf-8") runner_contracts = [ diff --git a/proof/region/v1/mpfi/input.py b/proof/region/v1/mpfi/input.py index 91e52869..e1caa8b5 100644 --- a/proof/region/v1/mpfi/input.py +++ b/proof/region/v1/mpfi/input.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""MPFI-owned source closure, materialized как один sealed generic input.""" +"""MPFI-замыкание исходников, материализуемое в единый sealed input.""" from __future__ import annotations @@ -192,7 +192,7 @@ def seal_mpfi_source_input_v1( """Запечатывает MPFI source closure в caller-owned resource bounds. `MpfiSourceInputErrorV1` означает invalid public capability boundary, - `ProvenanceErrorV1` — failure exact source replay, а `InputErrorV1` — + `ProvenanceErrorV1` — failure exact archive replay, а `InputErrorV1` — canonical USTAR или resource-bound rejection. """ diff --git a/proof/region/v1/tests/gate.py b/proof/region/v1/tests/gate.py deleted file mode 100644 index c96588d0..00000000 --- a/proof/region/v1/tests/gate.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -"""Точный состав общего fast proof-suite без engine-specific зависимости.""" - -from __future__ import annotations - -import hashlib -import unittest -from collections.abc import Iterator -from pathlib import Path - - -TEST_DIRECTORY = Path(__file__).resolve().parent -EXPECTED_TEST_COUNT_V1 = 166 -EXPECTED_TEST_INVENTORY_SHA256_V1 = ( - "1690f11e76b57e532f9ca5fa7cccd298438b72fb9a5327192e57c3d84c899715" -) - - -def _iter_tests_v1(suite: unittest.TestSuite) -> Iterator[unittest.TestCase]: - for item in suite: - if isinstance(item, unittest.TestSuite): - yield from _iter_tests_v1(item) - elif isinstance(item, unittest.TestCase): - yield item - else: - raise TypeError("suite contains a non-test object") - - -def _inventory_preimage_v1(test_ids: tuple[str, ...]) -> bytes: - return b"".join(test_id.encode("utf-8") + b"\n" for test_id in sorted(test_ids)) - - -def test_count_v1(suite: unittest.TestSuite) -> int: - return sum(1 for _test in _iter_tests_v1(suite)) - - -def test_inventory_sha256_v1(suite: unittest.TestSuite) -> str: - test_ids = tuple(test.id() for test in _iter_tests_v1(suite)) - return hashlib.sha256(_inventory_preimage_v1(test_ids)).hexdigest() - - -def full_suite_v1() -> unittest.TestSuite: - """Один engine-neutral suite, который CI уже запускает целиком.""" - - return unittest.defaultTestLoader.discover(str(TEST_DIRECTORY), pattern="test_*.py") - - -def inventory_is_exact_v1(suite: unittest.TestSuite) -> bool: - """Не даёт исчезнуть contract-тесту за общим minimum-count порогом CI.""" - - return ( - test_count_v1(suite) == EXPECTED_TEST_COUNT_V1 - and test_inventory_sha256_v1(suite) == EXPECTED_TEST_INVENTORY_SHA256_V1 - ) diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index 7b67d58f..abe02d5c 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""RED contract for an identity-preserving engine-neutral BUILD leaf.""" +"""Контракт нейтрального к движку BUILD-слоя с сохранением идентичности.""" from __future__ import annotations @@ -26,7 +26,6 @@ import pipeline # noqa: E402 from proof.region.v1.arb.tests import gate as arb_gate # noqa: E402 -from proof.region.v1.tests import gate as proof_gate # noqa: E402 from test_pipeline import ( # noqa: E402 _docker_capability, _probe_native_backend, @@ -39,20 +38,12 @@ # its expected hash here would let a coordinated gate edit hide inventory drift. # A deliberate test-set change updates both values from fresh enumeration. ARB_INVENTORY_SHA256_V1 = ( - "721fcceb07c3d73e30032814a181e9c3d86f2185cfb3382cc79b34e05618fa48" + "c74942a9240b68327921160f86fd948532849234bb6da00a0075a137fef098cc" ) ARB_ORDER_SHA256_V1 = ( - "ad40ffaf023f70b347c1ad691e0ebfa9e0dbfb53cdda45aacd86f1dbb2c5c999" + "bbf8711108939c4658e0b17bc037c5b592499fdf73c67121c492e5edea4635e9" ) -ARB_TEST_COUNT_V1 = 184 - -# Независимый внешний oracle общего proof-suite. Он живёт в уже обязательном -# discovery leaf, поэтому добавление/удаление MPFI-contract теста нельзя -# незаметно прикрыть только редактированием будущего общего gate. -SHARED_PROOF_TEST_INVENTORY_SHA256_V1 = ( - "1690f11e76b57e532f9ca5fa7cccd298438b72fb9a5327192e57c3d84c899715" -) -SHARED_PROOF_TEST_COUNT_V1 = 166 +ARB_TEST_COUNT_V1 = 198 MOVED_INPUT_SURFACE_V1 = ( "CanonicalInputLimitsV1", @@ -293,74 +284,6 @@ def test_existing_arb_suite_keeps_exact_count_order_and_inventory(self) -> None: ) -class ExistingSharedProofGateTests(unittest.TestCase): - def test_mandatory_shared_proof_suite_keeps_exact_inventory(self) -> None: - suite = proof_gate.full_suite_v1() - - self.assertEqual( - proof_gate.test_count_v1(suite), - SHARED_PROOF_TEST_COUNT_V1, - ) - self.assertEqual( - proof_gate.test_inventory_sha256_v1(suite), - SHARED_PROOF_TEST_INVENTORY_SHA256_V1, - ) - self.assertTrue(proof_gate.inventory_is_exact_v1(suite)) - - def test_exact_inventory_rejects_a_missing_mpfi_input_contract(self) -> None: - suite = proof_gate.full_suite_v1() - without_mpfi_input = unittest.TestSuite( - test - for test in proof_gate._iter_tests_v1(suite) - if not test.id().startswith("test_mpfi_input.") - ) - - self.assertLess( - proof_gate.test_count_v1(without_mpfi_input), - proof_gate.test_count_v1(suite), - ) - self.assertFalse(proof_gate.inventory_is_exact_v1(without_mpfi_input)) - - def test_exact_inventory_rejects_same_count_contract_replacement(self) -> None: - class ReplacementTest(unittest.TestCase): - def test_replacement(self) -> None: - pass - - suite = proof_gate.full_suite_v1() - tests = tuple(proof_gate._iter_tests_v1(suite)) - replaced = next( - test for test in tests if test.id().startswith("test_mpfi_input.") - ) - replacement = unittest.defaultTestLoader.loadTestsFromTestCase(ReplacementTest) - same_count_replacement = unittest.TestSuite( - (*tuple(test for test in tests if test is not replaced), replacement) - ) - - self.assertEqual( - proof_gate.test_count_v1(same_count_replacement), - proof_gate.test_count_v1(suite), - ) - self.assertFalse(proof_gate.inventory_is_exact_v1(same_count_replacement)) - - def test_mpfi_input_contract_cannot_green_by_skipping(self) -> None: - suite = unittest.defaultTestLoader.discover( - str(proof_gate.TEST_DIRECTORY), - pattern="test_mpfi_input.py", - ) - test_ids = tuple(test.id() for test in proof_gate._iter_tests_v1(suite)) - result = unittest.TestResult() - - suite.run(result) - - self.assertTrue(test_ids) - self.assertEqual(result.testsRun, len(test_ids)) - self.assertFalse(result.skipped) - self.assertFalse(result.failures) - self.assertFalse(result.errors) - self.assertFalse(result.expectedFailures) - self.assertFalse(result.unexpectedSuccesses) - - class ArbBuildIdentityCharacterizationTests(unittest.TestCase): def test_arb_input_and_process_stay_exact_while_capability_identities_move_to_v2(self) -> None: transport = importlib.import_module("build.transport") diff --git a/proof/region/v1/tests/test_mpfi_input.py b/proof/region/v1/tests/test_mpfi_input.py index a095409d..953b8463 100644 --- a/proof/region/v1/tests/test_mpfi_input.py +++ b/proof/region/v1/tests/test_mpfi_input.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""RED-контракт границы MPFI admitted-source → sealed-input.""" +"""Контракт границы MPFI admitted-source → sealed input.""" from __future__ import annotations @@ -529,25 +529,54 @@ def test_import_guard_resolves_from_import_targets(self) -> None: ) def test_protocol_keeps_the_source_input_boundary_below_build_authority(self) -> None: - reference = " ".join( - (ROOT / "PROTOCOL.md").read_text(encoding="utf-8").split() + protocol = (ROOT / "PROTOCOL.md").read_text(encoding="utf-8") + source_input_start = protocol.index("`mpfi/input.py`") + transport_start = protocol.index( + "`proof/region/v1/build/transport.py`", + source_input_start, ) + arb_replay_start = protocol.index("## Воспроизведение Arb", transport_start) + reference = " ".join(protocol[source_input_start:transport_start].split()) + transport_reference = " ".join(protocol[transport_start:arb_replay_start].split()) + source_path = ROOT / "mpfi" / "input.py" + source_text = source_path.read_text(encoding="utf-8") + # Тест намеренно запускается с ``-OO``. Явно выключаем оптимизацию + # parser-а, чтобы контракт документации наблюдался по исходнику, а не + # случайно зависел от сохранения runtime ``__doc__``. + source_tree = compile( + source_text, + str(source_path), + "exec", + flags=ast.PyCF_ONLY_AST, + optimize=0, + ) + seal_function = next( + node + for node in source_tree.body + if isinstance(node, ast.FunctionDef) + and node.name == "seal_mpfi_source_input_v1" + ) + seal_reference = ast.get_docstring(seal_function) - self.assertIn("`mpfi/input.py`", reference) self.assertIn("`sources//`", reference) self.assertIn("не требует уникальности root", reference) self.assertIn("Caller передаёт canonical `CanonicalInputLimitsV1`", reference) self.assertIn("`MpfiSourceInputErrorV1`", reference) self.assertIn("`ProvenanceErrorV1`", reference) self.assertIn("`InputErrorV1`", reference) + self.assertIn("MPFI source-input binding", reference) + self.assertIn("materialization archive", reference) self.assertIn( "не вводит recipe, Docker policy, BUILD/RUN authority", reference, ) self.assertIn( "MPFI sealed source input ещё не является MPFI build policy", - reference, + transport_reference, ) + self.assertIn("engine-owned input binding", transport_reference) + self.assertIsNotNone(seal_reference) + self.assertIn("failure exact archive replay", seal_reference) if __name__ == "__main__": From e588d43bb5cd84616b6160c20000a9fa700a4044 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:30:58 +0300 Subject: [PATCH 4/9] =?UTF-8?q?Test:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20MPFI=20fixture=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/tests/test_mpfi_input.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/proof/region/v1/tests/test_mpfi_input.py b/proof/region/v1/tests/test_mpfi_input.py index 953b8463..8a7a4cdc 100644 --- a/proof/region/v1/tests/test_mpfi_input.py +++ b/proof/region/v1/tests/test_mpfi_input.py @@ -82,7 +82,6 @@ def _fixture_release( *, license_body: bytes, value_body: bytes, - value_mode: int = 0o644, ) -> provenance.SourceReleaseLockV1: raw_tar = lzma.decompress(archive) integrity: ( @@ -138,7 +137,6 @@ def _admitted_closure( archive, license_body=license_body, value_body=value_body, - value_mode=value_mode, ) ) namespace = role.name.lower() @@ -486,8 +484,9 @@ def test_noncanonical_exact_type_lock_is_a_typed_rejection(self) -> None: def test_source_input_owner_has_no_engine_dependency(self) -> None: source_path = ROOT / "mpfi" / "input.py" self.assertTrue(source_path.is_file()) - tree = ast.parse(source_path.read_text(encoding="utf-8")) - imported_modules = _imported_module_names(source_path.read_text(encoding="utf-8")) + source = source_path.read_text(encoding="utf-8") + tree = ast.parse(source) + imported_modules = _imported_module_names(source) forbidden = ( "arb", "pipeline", @@ -509,10 +508,14 @@ def test_source_input_owner_has_no_engine_dependency(self) -> None: any( isinstance(node, ast.Call) and ( - isinstance(node.func, ast.Name) - and node.func.id == "__import__" - or isinstance(node.func, ast.Attribute) - and node.func.attr == "import_module" + ( + isinstance(node.func, ast.Name) + and node.func.id == "__import__" + ) + or ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "import_module" + ) ) for node in ast.walk(tree) ), From ec13060d380d7e322db69ff31461902fe7f8f690 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 20:12:52 +0300 Subject: [PATCH 5/9] =?UTF-8?q?Proof:=20=D0=B7=D0=B0=D0=BC=D0=BA=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=8C=20single-operation=20replay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/PROTOCOL.md | 52 +- proof/region/v1/arb/pipeline.py | 582 +++++++++++-- proof/region/v1/arb/receipt.py | 748 +++++++++++++---- proof/region/v1/arb/tests/gate.py | 4 +- .../v1/arb/tests/test_build_identity_v2.py | 12 +- proof/region/v1/arb/tests/test_pipeline.py | 519 ++++++++++++ proof/region/v1/arb/tests/test_receipt.py | 406 ++++++++- proof/region/v1/arb/tests/test_transport.py | 47 +- proof/region/v1/build/transport.py | 16 +- proof/region/v1/mpfi/input.py | 69 +- proof/region/v1/provenance.py | 771 ++++++++++++++++-- proof/region/v1/region_proof_protocol.py | 65 ++ proof/region/v1/tests/test_build.py | 12 +- proof/region/v1/tests/test_mpfi_input.py | 193 ++++- .../region/v1/tests/test_mpfi_source_lock.py | 3 +- proof/region/v1/tests/test_source_lock.py | 423 +++++++++- 16 files changed, 3530 insertions(+), 392 deletions(-) diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index 462e15d3..cd5b095a 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -236,19 +236,25 @@ observation. Право на Arb receipt получает не executor, а от ## Общая граница BUILD -`provenance.materialize_admitted_source_files_v1` повторно допускает один -admitted archive и выдаёт только exact relative regular files. Он не вводит -USTAR namespace, recipe или engine semantics. Lane выбирает layout и связывает -собственный aggregate source capability; общий materializer не создаёт generic -source closure. `proof/region/v1/build/input.py` принимает уже нормализованные -lane entries, кодирует один канонический USTAR и владеет точными input bytes. +Source replay имеет две намеренно разные стадии. Сначала provenance канонически +перепарсивает source lock, bounded-decompresses и сканирует archive, чтобы +сверить lock, manifest, tree и compressed bytes. Эта metadata replay не создаёт +отдельные file-byte buffers. Затем +`provenance.replay_materialize_admitted_source_v1` из одного такого replay +создаёт token-closed снимок lock, archive и exact relative regular files. +Aggregate Arb/MPFI admission владеет только свежими replayed archives; runtime +получает все три file-byte materializations только через один +`replay_admitted_source_closure_v1`. Общий leaf не вводит USTAR namespace, +recipe или engine semantics. Lane выбирает layout и связывает собственный +aggregate source capability. `proof/region/v1/build/input.py` принимает уже +нормализованные lane entries, кодирует один канонический USTAR и владеет точными input bytes. `SealedInputV1` структурно неизменяем, связывает целостность байтов с opaque caller digest и не утверждает recipe либо engine semantics. Resource bounds передаёт lane: общий encoder не вводит собственный fixture-specific cap. -`mpfi/input.py` строит `SealedInputV1` только из заново допущенной пары -`MpfiSourceLockV1` и `AdmittedMpfiSourcesV1`. Он повторно материализует exact -regular files, помещает их в versioned MPFI-only namespace +`mpfi/input.py` строит `SealedInputV1` только из одного owned replay snapshot +пары `MpfiSourceLockV1` и `AdmittedMpfiSourcesV1`. Тот же снимок даёт exact +regular files, aggregate identity и versioned MPFI-only namespace `sources//` и связывает свежую aggregate source capability с exact USTAR bytes. Роль, а не archive root, разделяет три source trees: lock не требует уникальности root. Целостность `SealedInputV1` сама по себе не @@ -307,10 +313,23 @@ source provenance: engine lane отдельно перепроверяет св ## Воспроизведение Arb, связанное с источником -`SourceBoundArbControllerV1` сначала повторно парсит source lock и job, -повторно допускает exact owned archive/build-input bytes и строит из regular -files один canonical USTAR с нормализованными metadata. Один immutable bundle -object дважды передаётся через bounded stdin; каждый свежий контейнер до +`PipelineRequestV1` до операции отдельно перепроверяет и владеет metadata-only +source closure: это ранняя integrity boundary для public input, не shared cache +операции. Затем `SourceBoundArbControllerV1` получает один detached operation +snapshot: канонический source lock, owned replayed archives и единственные для +этой операции file-byte materializations, заново допущенные копии build files, +job и limits. Он передаёт этот же private snapshot в `ControlledPipelineV1`; +самостоятельный BUILD создаёт snapshot сам до probe/spawn. Внутренний transport +recheck сверяет только owned snapshot, а public verifier независимо строит +новый snapshot из request, сохранённого внутри evidence, а не из исходного +объекта вызывающего. До replay он фиксирует structural projection всех +evidence coordinates и сверяет каждый используемый protocol identity cache с +независимо восстановленным canonical wire. Он принимает результат только если +та же projection на входе, после source replay и после edge replay совпадает. +Projection сверяет retained bytes, manifests и protocol wire, но не открывает +вторую source materialization; она доказывает стабильность значения в пределах +одного вызова, а не неизменность объекта после возврата. Один +immutable bundle object дважды передаётся через bounded stdin; каждый свежий контейнер до распаковки сверяет exact length и SHA-256, распаковывает только в private bounded tmpfs, а executable возвращает через stdout. Semantic host bind mounts, host output path и повторное открытие результата отсутствуют. Эта граница @@ -328,9 +347,10 @@ identity без зеркальных промежуточных dataclass: 2. build identity связывает source identity, versioned Docker capability, pipeline policy, trust boundary, один sealed bundle object, два exact transfer и два byte-identical executable stdout. Comparator verifier - строит свежий canonical manifest из SHA-256 retained preimage bytes и - сверяет все его поля и identity с build observation; это проверка retained - причинных данных, а не заявление о независимом втором выводе preimages; + заново выводит все десять preimage bytes и canonical manifest из того же + operation snapshot и retained BUILD observation, затем сверяет все поля и + identity с build observation. Это не независимая реализация или semantic + replay, а exact re-derivation тех же причинных координат; 3. run identity впервые связывает canonical job с тем же retained executable bytes object, exact argv/env/cwd/stdin/limits, единственной допустимой `linux-x86_64` sandbox platform, typed child exit, stdout, canonical diff --git a/proof/region/v1/arb/pipeline.py b/proof/region/v1/arb/pipeline.py index 26364ea0..7dc99987 100644 --- a/proof/region/v1/arb/pipeline.py +++ b/proof/region/v1/arb/pipeline.py @@ -272,14 +272,20 @@ def build_source_manifest_bytes_v1(sources: AdmittedBuildSourcesV1) -> bytes: if type(sources) is not AdmittedBuildSourcesV1: raise TypeError("sources must be AdmittedBuildSourcesV1") replayed = admit_build_sources_v1(sources.files) + retained_identity = sources.identity + retained_build_input_identity = sources.build_input_identity + retained_formula_support_identity = sources.formula_support_identity if ( - replayed.identity != sources.identity - or replayed.build_input_identity != sources.build_input_identity - or replayed.formula_support_identity != sources.formula_support_identity + not _valid_digest(retained_identity) + or not _valid_digest(retained_build_input_identity) + or not _valid_digest(retained_formula_support_identity) + or retained_identity != replayed.identity + or retained_build_input_identity != replayed.build_input_identity + or retained_formula_support_identity != replayed.formula_support_identity ): raise TypeError("retained build-source coordinates changed") - chunks: list[bytes] = [len(sources.files).to_bytes(4, "big")] - for item in sources.files: + chunks: list[bytes] = [len(replayed.files).to_bytes(4, "big")] + for item in replayed.files: chunks.extend( ( item.path.encode("ascii"), @@ -312,13 +318,30 @@ def _build_sources_identity(files_value: tuple[BuildSourceFileV1, ...]) -> bytes return _source_subset_identity(_BUILD_SOURCES_ID_LABEL_V1, files_value) -def admit_build_sources_v1( +def _snapshot_build_source_files_v1( files_value: tuple[BuildSourceFileV1, ...], -) -> AdmittedBuildSourcesV1: +) -> tuple[BuildSourceFileV1, ...]: + """Copies exact primitives before admission derives any retained identity.""" + if type(files_value) is not tuple or any( type(item) is not BuildSourceFileV1 for item in files_value ): _source_fail(BuildSourceReasonV1.WRONG_TYPE, "files") + try: + return tuple( + BuildSourceFileV1(item.path, item.mode, item.contents) + for item in files_value + ) + except BuildSourceAdmissionErrorV1: + raise + except Exception: + _source_fail(BuildSourceReasonV1.WRONG_TYPE, "files") + + +def admit_build_sources_v1( + files_value: tuple[BuildSourceFileV1, ...], +) -> AdmittedBuildSourcesV1: + files_value = _snapshot_build_source_files_v1(files_value) actual = tuple((item.path, item.mode) for item in files_value) if actual != REQUIRED_BUILD_SOURCE_MODES_V1: _source_fail(BuildSourceReasonV1.NONCANONICAL_SET, "files") @@ -388,48 +411,65 @@ def arb_input_is_bound_v1( exact_policy: object, value: object, ) -> bool: - """Recompute Arb semantics independently of generic byte integrity.""" + """Independently replay public input; never trust its retained identities.""" + + if type(value) is not build_input.SealedInputV1: + return False + try: + snapshot = _snapshot_pipeline_operation_v1(request) + expected = _seal_build_input_from_snapshot_v1( + snapshot, + exact_policy, + ) + return _owned_arb_input_is_bound_v1(value, expected) + except Exception: + return False + + +def _owned_arb_input_is_bound_v1( + value: object, + expected: build_input.SealedInputV1, +) -> bool: + """Cheap transport recheck against one private operation snapshot.""" if ( - type(request) is not PipelineRequestV1 - or type(value) is not build_input.SealedInputV1 + type(value) is not build_input.SealedInputV1 or not build_input.sealed_input_is_intact_v1(value) ): return False try: - return value.binding_identity == _arb_input_binding_identity_v1( - request.admitted_sources.identity, - request.build_sources.build_input_identity, - value.contents, - exact_policy, + return ( + value.binding_identity == expected.binding_identity + and value.sha256 == expected.sha256 + and value.length == expected.length + and value.contents == expected.contents ) except Exception: return False -def _seal_build_input_bundle_v1( - request: "PipelineRequestV1", +def _seal_build_input_from_snapshot_v1( + snapshot: _PipelineOperationSnapshotV1, exact_policy: build_transport.DockerBuildPolicyV1, ) -> build_input.SealedInputV1: - if type(request) is not PipelineRequestV1: - raise TypeError("request must be PipelineRequestV1") + if type(snapshot) is not _PipelineOperationSnapshotV1: + raise TypeError("snapshot must be _PipelineOperationSnapshotV1") if not build_transport.docker_policy_is_valid_v1(exact_policy): raise TypeError("exact_policy must be canonical DockerBuildPolicyV1") + request = snapshot.request + source_closure = snapshot.source_closure source_entries = tuple( ( f"inputs/{lock.root_prefix[:-1]}/{relative}", mode, contents, ) - for lock, admitted in zip( - request.source_lock.sources, - request.admitted_sources.sources, + for lock, materialized in zip( + source_closure.source_lock.sources, + source_closure.sources, strict=True, ) - for relative, mode, contents in provenance.materialize_admitted_source_files_v1( - lock, - admitted, - ) + for relative, mode, contents in materialized.files ) workspace_entries = tuple( ( @@ -454,11 +494,14 @@ def _seal_build_input_bundle_v1( MAX_BUILD_SOURCE_FILE_BYTES_V1, *( lock.regular_file_bytes - for lock in request.source_lock.sources + for lock in source_closure.source_lock.sources ), ), MAX_BUILD_SOURCE_TOTAL_BYTES_V1 - + sum(lock.regular_file_bytes for lock in request.source_lock.sources), + + sum( + lock.regular_file_bytes + for lock in source_closure.source_lock.sources + ), ), ) return build_input.seal_input_v1( @@ -472,16 +515,37 @@ def _seal_build_input_bundle_v1( ) +def _seal_build_input_bundle_v1( + request: "PipelineRequestV1", + exact_policy: build_transport.DockerBuildPolicyV1, +) -> build_input.SealedInputV1: + """Seal one public request through a detached operation snapshot.""" + + return _seal_build_input_from_snapshot_v1( + _snapshot_pipeline_operation_v1(request), + exact_policy, + ) + + class HostTrustBoundaryV1(StrEnum): UNSEALED_LINUX_X64_DOCKER_HOST = "unsealed-linux-x64-docker-host" +_UNSEALED_LINUX_X64_DOCKER_HOST_WIRE_V1 = b"unsealed-linux-x64-docker-host" + + +def _host_trust_wire_v1(value: object) -> bytes: + """Own the sole V1 host declaration without reading mutable enum storage.""" + + if value is not HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST: + raise TypeError("unknown host trust boundary") + return _UNSEALED_LINUX_X64_DOCKER_HOST_WIRE_V1 + + def pipeline_policy_identity_v2( host_trust: HostTrustBoundaryV1, exact_policy: build_transport.DockerBuildPolicyV1, ) -> bytes: - if type(host_trust) is not HostTrustBoundaryV1: - raise TypeError("host_trust must be HostTrustBoundaryV1") if not build_transport.docker_policy_is_valid_v1(exact_policy): raise TypeError("exact_policy must be canonical DockerBuildPolicyV1") return _identity( @@ -489,7 +553,7 @@ def pipeline_policy_identity_v2( ( build_transport.transport_policy_identity_v1(exact_policy), build_transport.native_command_contract_identity_v1(), - host_trust.value.encode("ascii"), + _host_trust_wire_v1(host_trust), b"build-observation=diagnostic-unsealed-v1", b"inputs=one-controller-sealed-normalized-tree-ustar", b"container-admission=exact-length-and-sha256-before-extraction", @@ -501,6 +565,7 @@ def pipeline_policy_identity_v2( class PipelineInputReasonV1(StrEnum): WRONG_TYPE = "wrong_type" FOREIGN_SOURCE_CAPABILITY = "foreign_source_capability" + INVALID_RETAINED_INPUT = "invalid_retained_input" FORMULA_MISMATCH = "formula_mismatch" EXECUTION_LIMIT_MISMATCH = "execution_limit_mismatch" @@ -564,27 +629,62 @@ def _require_bound_source_capability_v1( source_lock: provenance.ArbSourceLockV1, admitted_sources: provenance.AdmittedArbSourcesV1, ) -> None: - if source_lock.identity != admitted_sources.source_lock_identity: + if ( + type(source_lock) is not provenance.ArbSourceLockV1 + or type(admitted_sources) is not provenance.AdmittedArbSourcesV1 + ): + raise PipelineInputErrorV1( + PipelineInputReasonV1.WRONG_TYPE, + "source_lock/admitted_sources", + ) + try: + closure_identity = admitted_sources.source_lock_identity + sources = admitted_sources.sources + if ( + not _valid_digest(closure_identity) + or type(sources) is not tuple + or len(sources) != provenance.SOURCE_CLOSURE_COUNT_V1 + or any(type(source) is not provenance.SafeSourceArchiveV1 for source in sources) + ): + raise TypeError("invalid retained source closure") + except (AttributeError, TypeError, ValueError, OverflowError) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + if source_lock.identity != closure_identity: raise PipelineInputErrorV1( PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources", ) for lock, admitted in zip( source_lock.sources, - admitted_sources.sources, + sources, strict=True, ): - if lock.identity != admitted.source_lock_identity: + try: + admitted_lock_identity = admitted.source_lock_identity + except AttributeError as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + if ( + not _valid_digest(admitted_lock_identity) + or lock.identity != admitted_lock_identity + ): raise PipelineInputErrorV1( PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources", ) -def flint_source_content_partition_v1( +def _owned_flint_source_content_partition_v1( source_lock: provenance.ArbSourceLockV1, admitted_sources: provenance.AdmittedArbSourcesV1, ) -> FlintSourceContentPartitionV1: + """Derive FLINT partitions from one already-detached source closure.""" + if type(source_lock) is not provenance.ArbSourceLockV1: raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "source_lock") if type(admitted_sources) is not provenance.AdmittedArbSourcesV1: @@ -637,6 +737,49 @@ def flint_source_content_partition_v1( ) +def flint_source_content_partition_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, +) -> FlintSourceContentPartitionV1: + """Independently derive FLINT partitions from a public retained closure.""" + + if type(source_lock) is not provenance.ArbSourceLockV1: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "source_lock") + if type(admitted_sources) is not provenance.AdmittedArbSourcesV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.WRONG_TYPE, + "admitted_sources", + ) + try: + canonical_lock = provenance.snapshot_source_closure_lock_v1(source_lock) + canonical_sources = provenance.snapshot_admitted_source_closure_v1( + canonical_lock, + admitted_sources, + ) + if type(canonical_lock) is not provenance.ArbSourceLockV1 or type( + canonical_sources + ) is not provenance.AdmittedArbSourcesV1: + raise TypeError("FLINT requires an Arb source closure") + return _owned_flint_source_content_partition_v1( + canonical_lock, + canonical_sources, + ) + except PipelineInputErrorV1: + raise + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + + def _comparator_preimage_v1(label: bytes, chunks: tuple[bytes, ...]) -> bytes: """Encode one independently versioned, ordered comparator preimage.""" @@ -671,7 +814,7 @@ def _comparator_preimage_v2(label: bytes, chunks: tuple[bytes, ...]) -> bytes: ) -def comparator_build_preimage_v2( +def _comparator_build_preimage_v2( build_sources: AdmittedBuildSourcesV1, docker_capability_identity: bytes, pipeline_policy_identity: bytes, @@ -911,37 +1054,309 @@ def __post_init__(self) -> None: for field_name, value, expected_type in expected_types: if type(value) is not expected_type: raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, field_name) - _require_bound_source_capability_v1(self.source_lock, self.admitted_sources) - flint_source_content_partition_v1(self.source_lock, self.admitted_sources) - if self.build_sources.formula_spec != self.job.formula_spec: - raise PipelineInputErrorV1(PipelineInputReasonV1.FORMULA_MISMATCH, "job") - job_bytes = self.job.encode() - invocation_bytes = sum( - len(value) + 1 - for value in ( - b"arb-evaluator", - b"--manifest-identity", - bytes(32).hex().encode("ascii"), - b"--job", - b"/dev/stdin", + try: + source_lock = provenance.snapshot_source_closure_lock_v1(self.source_lock) + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "source_lock", + ) from error + if type(source_lock) is not provenance.ArbSourceLockV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "source_lock", + ) + try: + admitted_sources = provenance.snapshot_admitted_source_closure_v1( + source_lock, + self.admitted_sources, ) - ) + sum( - len(key) + len(value) + 2 - for key, value in ((b"LC_ALL", b"C"), (b"TZ", b"UTC")) + if type(admitted_sources) is not provenance.AdmittedArbSourcesV1: + raise TypeError("Arb request retained a non-Arb source closure") + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + try: + build_sources = admit_build_sources_v1(self.build_sources.files) + except ( + BuildSourceAdmissionErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "build_sources", + ) from error + try: + job = protocol.snapshot_proof_job_v1(self.job) + except ( + protocol.ProtocolErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "job", + ) from error + try: + execution_limits = executor.ExecutionLimitsV1(*tuple(self.execution_limits)) + except ( + executor.ExecutionRequestErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "execution_limits", + ) from error + try: + _host_trust_wire_v1(self.host_trust) + except TypeError as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "host_trust", + ) from error + _validate_pipeline_request_coordinates_v1( + source_lock, + admitted_sources, + build_sources, + job, + execution_limits, + self.host_trust, + ) + object.__setattr__(self, "source_lock", source_lock) + object.__setattr__(self, "admitted_sources", admitted_sources) + object.__setattr__(self, "build_sources", build_sources) + object.__setattr__(self, "job", job) + object.__setattr__(self, "execution_limits", execution_limits) + + +_PIPELINE_OWNED_REQUEST_TOKEN = object() + + +def _validate_pipeline_request_coordinates_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + build_sources: AdmittedBuildSourcesV1, + job: protocol.ProofJobV1, + execution_limits: executor.ExecutionLimitsV1, + host_trust: HostTrustBoundaryV1, +) -> None: + """Check a detached request without reopening its already-owned archives.""" + + expected_types = ( + ("source_lock", source_lock, provenance.ArbSourceLockV1), + ("admitted_sources", admitted_sources, provenance.AdmittedArbSourcesV1), + ("build_sources", build_sources, AdmittedBuildSourcesV1), + ("job", job, protocol.ProofJobV1), + ("execution_limits", execution_limits, executor.ExecutionLimitsV1), + ("host_trust", host_trust, HostTrustBoundaryV1), + ) + for field_name, value, expected_type in expected_types: + if type(value) is not expected_type: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, field_name) + try: + _require_bound_source_capability_v1(source_lock, admitted_sources) + _owned_flint_source_content_partition_v1(source_lock, admitted_sources) + except PipelineInputErrorV1: + raise + except (AttributeError, TypeError, ValueError, OverflowError) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + if build_sources.formula_spec != job.formula_spec: + raise PipelineInputErrorV1(PipelineInputReasonV1.FORMULA_MISMATCH, "job") + job_bytes = protocol.ProofJobV1.encode(job) + invocation_bytes = sum( + len(value) + 1 + for value in ( + b"arb-evaluator", + b"--manifest-identity", + bytes(32).hex().encode("ascii"), + b"--job", + b"/dev/stdin", ) + ) + sum( + len(key) + len(value) + 2 + for key, value in ((b"LC_ALL", b"C"), (b"TZ", b"UTC")) + ) + if ( + execution_limits.max_executable_bytes > BUILD_STDOUT_LIMIT_V1 + or len(job_bytes) > execution_limits.max_stdin_bytes + or invocation_bytes > execution_limits.max_argument_bytes + ): + raise PipelineInputErrorV1( + PipelineInputReasonV1.EXECUTION_LIMIT_MISMATCH, + "execution_limits", + ) + + +def _owned_pipeline_request_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + build_sources: AdmittedBuildSourcesV1, + job: protocol.ProofJobV1, + execution_limits: executor.ExecutionLimitsV1, + host_trust: HostTrustBoundaryV1, + *, + _token: object, +) -> PipelineRequestV1: + """Mint the request half of a private operation from already-owned values.""" + + if _token is not _PIPELINE_OWNED_REQUEST_TOKEN: + raise TypeError("owned pipeline requests are created only by operation replay") + _validate_pipeline_request_coordinates_v1( + source_lock, + admitted_sources, + build_sources, + job, + execution_limits, + host_trust, + ) + request = object.__new__(PipelineRequestV1) + for field_name, value in ( + ("source_lock", source_lock), + ("admitted_sources", admitted_sources), + ("build_sources", build_sources), + ("job", job), + ("execution_limits", execution_limits), + ("host_trust", host_trust), + ): + object.__setattr__(request, field_name, value) + return request + + +_PIPELINE_OPERATION_SNAPSHOT_TOKEN = object() + + +@dataclass(frozen=True, init=False) +class _PipelineOperationSnapshotV1: + """One private operation capability; its contents never alias caller input.""" + + request: PipelineRequestV1 + source_closure: provenance.ReplayedSourceClosureV1 + + def __init__( + self, + request: PipelineRequestV1, + source_closure: provenance.ReplayedSourceClosureV1, + *, + _token: object, + ) -> None: + if _token is not _PIPELINE_OPERATION_SNAPSHOT_TOKEN: + raise TypeError("PipelineOperationSnapshotV1 is created only by pipeline replay") if ( - self.execution_limits.max_executable_bytes > BUILD_STDOUT_LIMIT_V1 - or len(job_bytes) > self.execution_limits.max_stdin_bytes - or invocation_bytes > self.execution_limits.max_argument_bytes + type(request) is not PipelineRequestV1 + or type(source_closure) is not provenance.ReplayedSourceClosureV1 + or type(source_closure.source_lock) is not provenance.ArbSourceLockV1 + or type(source_closure.admitted_sources) is not provenance.AdmittedArbSourcesV1 + or request.source_lock.identity != source_closure.source_lock.identity + or request.admitted_sources.identity != source_closure.admitted_sources.identity + ): + raise TypeError("operation snapshot must retain one coherent Arb closure") + object.__setattr__(self, "request", request) + object.__setattr__(self, "source_closure", source_closure) + + +def _snapshot_pipeline_operation_v1( + request: object, +) -> _PipelineOperationSnapshotV1: + """Rebuild every authority-bearing request coordinate before any probe/spawn.""" + + if type(request) is not PipelineRequestV1: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "request") + try: + source_lock = request.source_lock + admitted_sources = request.admitted_sources + build_sources = request.build_sources + job = request.job + execution_limits = request.execution_limits + host_trust = request.host_trust + if ( + type(source_lock) is not provenance.ArbSourceLockV1 + or type(admitted_sources) is not provenance.AdmittedArbSourcesV1 + or type(build_sources) is not AdmittedBuildSourcesV1 + or type(job) is not protocol.ProofJobV1 + or type(execution_limits) is not executor.ExecutionLimitsV1 + or type(host_trust) is not HostTrustBoundaryV1 ): raise PipelineInputErrorV1( - PipelineInputReasonV1.EXECUTION_LIMIT_MISMATCH, - "execution_limits", + PipelineInputReasonV1.WRONG_TYPE, + "request", + ) + # Copy all non-source coordinates before archive replay can do work or + # trigger a reentrant hostile fixture. The protocol-owned copier reads + # raw fields rather than a mutable instance ``encode`` or cached digest. + canonical_job = protocol.snapshot_proof_job_v1(job) + canonical_build_sources = admit_build_sources_v1(build_sources.files) + canonical_execution_limits = executor.ExecutionLimitsV1( + *tuple(execution_limits) + ) + _host_trust_wire_v1(host_trust) + source_closure = provenance.replay_admitted_source_closure_v1( + source_lock, + admitted_sources, + ) + if type(source_closure.source_lock) is not provenance.ArbSourceLockV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", ) + if type(source_closure.admitted_sources) is not provenance.AdmittedArbSourcesV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + canonical_request = _owned_pipeline_request_v1( + source_closure.source_lock, + source_closure.admitted_sources, + canonical_build_sources, + canonical_job, + canonical_execution_limits, + host_trust, + _token=_PIPELINE_OWNED_REQUEST_TOKEN, + ) + except PipelineInputErrorV1: + raise + except Exception as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "request", + ) from error + return _PipelineOperationSnapshotV1( + canonical_request, + source_closure, + _token=_PIPELINE_OPERATION_SNAPSHOT_TOKEN, + ) def _derive_arb_comparator_for_build_v1( - request: PipelineRequestV1, + snapshot: _PipelineOperationSnapshotV1, docker_capability: build_transport.DockerSupportedV1, binary: bytes, rebuild_sha256s: tuple[bytes, bytes], @@ -952,8 +1367,9 @@ def _derive_arb_comparator_for_build_v1( ) -> DiagnosticArbComparatorV1: """Derive all ten coordinates without accepting a caller digest/resolver.""" - if type(request) is not PipelineRequestV1: - raise TypeError("request must be PipelineRequestV1") + if type(snapshot) is not _PipelineOperationSnapshotV1: + raise TypeError("snapshot must be _PipelineOperationSnapshotV1") + request = snapshot.request if type(docker_capability) is not build_transport.DockerSupportedV1: raise TypeError("docker_capability must be DockerSupportedV1") if type(binary) is not bytes or not binary: @@ -991,7 +1407,7 @@ def _derive_arb_comparator_for_build_v1( b"gap:no-per-test-result-records", b"gap:no-git-derivation-for-project-pinned-release-only-files", b"gap:no-origin-authority-reverification", - request.host_trust.value.encode("ascii"), + _host_trust_wire_v1(request.host_trust), b"build-observation=diagnostic-unsealed-v1", len(flint_lock.integrity.omitted_paths).to_bytes(4, "big"), *( @@ -1013,13 +1429,9 @@ def _derive_arb_comparator_for_build_v1( request.admitted_sources.source_lock_identity, len(request.source_lock.sources).to_bytes(4, "big"), ] - for lock, source in zip( - request.source_lock.sources, - request.admitted_sources.sources, - strict=True, - ): + for materialized in snapshot.source_closure.sources: upstream_chunks.extend( - provenance.source_archive_replay_coordinates_v1(lock, source) + provenance._materialized_source_coordinates_v1(materialized) ) upstream_source = _comparator_preimage_v1( b"labcolors.proof-region.arb-comparator.upstream-source.v1\0", @@ -1091,7 +1503,7 @@ def _derive_arb_comparator_for_build_v1( process_bytes = tuple( build_transport.build_process_bytes_v1(item) for item in build_processes ) - build_identity = comparator_build_preimage_v2( + build_identity = _comparator_build_preimage_v2( request.build_sources, docker_capability_identity, pipeline_policy_identity, @@ -1324,8 +1736,7 @@ def __init__( or rebuild_sha256s != (binary_sha256, binary_sha256) ): raise TypeError("invalid observed two-build digests") - if type(host_trust) is not HostTrustBoundaryV1: - raise TypeError("invalid host trust boundary") + _host_trust_wire_v1(host_trust) if type(input_bundle_length) is not int or input_bundle_length <= 0: raise TypeError("invalid build input bundle length") if ( @@ -1484,6 +1895,24 @@ def build(self, request: PipelineRequestV1) -> BuildResultV1: if type(request) is not PipelineRequestV1: raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "request") + try: + snapshot = _snapshot_pipeline_operation_v1(request) + except Exception: + return build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + return self._build_snapshot_v1(snapshot) + + def _build_snapshot_v1( + self, + snapshot: _PipelineOperationSnapshotV1, + ) -> BuildResultV1: + """Consume one controller-owned snapshot without replaying its closure.""" + + if type(snapshot) is not _PipelineOperationSnapshotV1: + raise TypeError("snapshot must be _PipelineOperationSnapshotV1") + request = snapshot.request probe_result = self._transport.probe() if type(probe_result) is build_transport.DockerUnsupportedV1: return PipelineBlockedV1(probe_result.reason, probe_result.detail) @@ -1494,8 +1923,8 @@ def build(self, request: PipelineRequestV1) -> BuildResultV1: ) docker_capability = probe_result try: - input_bundle = _seal_build_input_bundle_v1( - request, + input_bundle = _seal_build_input_from_snapshot_v1( + snapshot, docker_capability.policy, ) except ( @@ -1514,10 +1943,9 @@ def build(self, request: PipelineRequestV1) -> BuildResultV1: docker_capability, input_bundle, request.execution_limits.max_executable_bytes, - input_admission=lambda value: arb_input_is_bound_v1( - request, - docker_capability.policy, + input_admission=lambda value: _owned_arb_input_is_bound_v1( value, + input_bundle, ), output_admission=self._admit_arb_output_v1, ) @@ -1550,13 +1978,13 @@ def build(self, request: PipelineRequestV1) -> BuildResultV1: ) build_processes = built.processes comparator = _derive_arb_comparator_for_build_v1( - request, + snapshot, docker_capability, binary, rebuild_sha256s, build_processes, ) - flint_partition = flint_source_content_partition_v1( + flint_partition = _owned_flint_source_content_partition_v1( request.source_lock, request.admitted_sources, ) diff --git a/proof/region/v1/arb/receipt.py b/proof/region/v1/arb/receipt.py index 61ee8f92..6b4f69e3 100644 --- a/proof/region/v1/arb/receipt.py +++ b/proof/region/v1/arb/receipt.py @@ -34,10 +34,45 @@ _BUILD_ID_LABEL_V2 = b"labcolors.proof-region.arb-build-replay.v2\0" _RUN_ID_LABEL_V1 = b"labcolors.proof-region.arb-run-replay.v1\0" _EVIDENCE_ID_LABEL_V1 = b"labcolors.proof-region.arb-evaluator-replay.v1\0" +_EVIDENCE_STABILITY_LABEL_V1 = ( + b"labcolors.proof-region.arb-evaluator-stability.v1\0" +) _SOURCE_BOUND_POLICY_ID_LABEL_V2 = ( b"labcolors.proof-region.arb-source-bound-policy.v2\0" ) +_DIAGNOSTIC_BUILD_FIELDS_V1 = ( + "structural_source_identity", + "flint_commit_content_identity", + "flint_commit_content_file_count", + "flint_project_pinned_release_only_identity", + "flint_project_pinned_release_only_file_count", + "build_input_identity", + "formula_support_identity", + "pipeline_policy_identity", + "docker_capability", + "binary_sha256", + "rebuild_sha256s", + "host_trust", + "input_bundle_identity", + "input_bundle_sha256", + "input_bundle_length", + "build_processes", + "comparator", + "_binary", + "_rebuild_binaries", + "_input_bundle", +) +_DIAGNOSTIC_COMPARATOR_FIELDS_V1 = ( + "preimages", + "manifest", + "structural_source_identity", + "build_input_identity", + "pipeline_policy_identity", + "binary_sha256", + "rebuild_sha256s", +) + def _blob(value: bytes) -> bytes: return len(value).to_bytes(8, "big") + value @@ -76,28 +111,21 @@ def source_bound_policy_identity_v2( ) -def _source_identity_v1(request: pipeline.PipelineRequestV1) -> bytes: - if type(request) is not pipeline.PipelineRequestV1: - raise TypeError("source replay requires PipelineRequestV1") +def _source_identity_from_operation_v1( + snapshot: pipeline._PipelineOperationSnapshotV1, +) -> bytes: + """Derive source identity from one operation-owned materialization.""" + + if type(snapshot) is not pipeline._PipelineOperationSnapshotV1: + raise TypeError("source identity requires a pipeline operation snapshot") + request = snapshot.request chunks: list[bytes] = [ request.source_lock.encode(), request.source_lock.identity, request.admitted_sources.identity, ] - replayed_sources = provenance.admit_arb_sources( - request.source_lock, - request.admitted_sources.sources, - ) - for lock, source in zip( - request.source_lock.sources, - request.admitted_sources.sources, - strict=True, - ): - chunks.extend( - provenance.source_archive_replay_coordinates_v1(lock, source) - ) - if replayed_sources.identity != request.admitted_sources.identity: - raise TypeError("source closure did not replay") + for materialized in snapshot.source_closure.sources: + chunks.extend(provenance._materialized_source_coordinates_v1(materialized)) chunks.extend( ( request.build_sources.identity, @@ -109,93 +137,60 @@ def _source_identity_v1(request: pipeline.PipelineRequestV1) -> bytes: return _identity(_SOURCE_ID_LABEL_V1, tuple(chunks)) -def _comparator_replays_v1( - request: pipeline.PipelineRequestV1, +def _source_identity_v1(request: pipeline.PipelineRequestV1) -> bytes: + """Independently derive source identity from a public request.""" + + return _source_identity_from_operation_v1( + pipeline._snapshot_pipeline_operation_v1(request) + ) + + +def _comparator_replays_from_operation_v1( + snapshot: pipeline._PipelineOperationSnapshotV1, build: pipeline.DiagnosticBuildObservationV1, ) -> bool: try: - comparator = build.comparator - capability_identity = build_transport.docker_capability_identity_v1( - build.docker_capability - ) - expected_pipeline_policy = pipeline.pipeline_policy_identity_v2( - request.host_trust, - build.docker_capability.policy, - ) - expected_build_preimage = pipeline.comparator_build_preimage_v2( - request.build_sources, - capability_identity, - expected_pipeline_policy, - build.build_processes, - build.binary_sha256, - build.rebuild_sha256s, - len(build.binary), - ) if ( - type(comparator) is not pipeline.DiagnosticArbComparatorV1 - or comparator.structural_source_identity - != request.admitted_sources.identity - or comparator.build_input_identity - != request.build_sources.build_input_identity - or comparator.pipeline_policy_identity != build.pipeline_policy_identity - or comparator.pipeline_policy_identity != expected_pipeline_policy - or comparator.preimages.build_identity != expected_build_preimage - or comparator.binary_sha256 != build.binary_sha256 - or comparator.rebuild_sha256s != build.rebuild_sha256s + type(snapshot) is not pipeline._PipelineOperationSnapshotV1 + or type(build) is not pipeline.DiagnosticBuildObservationV1 ): return False - names = tuple(item.name for item in fields(comparator.preimages)) - manifest_names = tuple( - item.name - for item in fields(comparator.manifest.manifest) - if item.name != "kind" - ) - if names != manifest_names: - return False - coordinates = tuple( - hashlib.sha256(getattr(comparator.preimages, name)).digest() - for name in names - ) - fresh_manifest = protocol.ComparatorManifestV2( - comparator.manifest.manifest.kind, - *coordinates, - ) - by_digest = { - coordinate: getattr(comparator.preimages, name) - for name, coordinate in zip(names, coordinates, strict=True) - } - replayed = protocol.ContentResolvedComparatorManifestV2.admit( - fresh_manifest, - by_digest.get, - ) - return ( - comparator.manifest.manifest == fresh_manifest - and replayed.manifest == fresh_manifest - and replayed.identity == fresh_manifest.identity - and comparator.manifest.identity == fresh_manifest.identity - and comparator.identity == fresh_manifest.identity + expected = pipeline._derive_arb_comparator_for_build_v1( + snapshot, + build.docker_capability, + build.binary, + build.rebuild_sha256s, + build.build_processes, ) + return build.comparator == expected except Exception: return False -def _build_identity_v2( - request: pipeline.PipelineRequestV1, +def _build_identity_from_operation_v2( + snapshot: pipeline._PipelineOperationSnapshotV1, source_identity: bytes, build: pipeline.DiagnosticBuildObservationV1, ) -> bytes: + if type(snapshot) is not pipeline._PipelineOperationSnapshotV1: + raise TypeError("build identity requires a pipeline operation snapshot") if type(build) is not pipeline.DiagnosticBuildObservationV1: raise TypeError("build replay requires DiagnosticBuildObservationV1") + request = snapshot.request bundle = build.input_bundle processes = build.build_processes binaries = build.rebuild_binaries capability_identity = build_transport.docker_capability_identity_v1( build.docker_capability ) - flint_partition = pipeline.flint_source_content_partition_v1( + flint_partition = pipeline._owned_flint_source_content_partition_v1( request.source_lock, request.admitted_sources, ) + expected_bundle = pipeline._seal_build_input_from_snapshot_v1( + snapshot, + build.docker_capability.policy, + ) if ( build.structural_source_identity != request.admitted_sources.identity or build.flint_commit_content_identity @@ -215,11 +210,7 @@ def _build_identity_v2( build.docker_capability.policy, ) or build.host_trust is not request.host_trust - or not pipeline.arb_input_is_bound_v1( - request, - build.docker_capability.policy, - bundle, - ) + or not pipeline._owned_arb_input_is_bound_v1(bundle, expected_bundle) or build.input_bundle_identity != bundle.binding_identity or build.input_bundle_sha256 != bundle.sha256 or build.input_bundle_length != bundle.length @@ -237,7 +228,7 @@ def _build_identity_v2( or build.binary is not binaries[0] or build.binary_sha256 != hashlib.sha256(build.binary).digest() or build.rebuild_sha256s != (build.binary_sha256, build.binary_sha256) - or not _comparator_replays_v1(request, build) + or not _comparator_replays_from_operation_v1(snapshot, build) ): raise TypeError("controller-observed BUILD did not replay") for process in processes: @@ -257,7 +248,7 @@ def _build_identity_v2( ( source_identity, build.pipeline_policy_identity, - build.host_trust.value.encode("ascii"), + pipeline._host_trust_wire_v1(build.host_trust), capability_identity, bundle.binding_identity, bundle.sha256, @@ -271,6 +262,20 @@ def _build_identity_v2( ) +def _build_identity_v2( + request: pipeline.PipelineRequestV1, + source_identity: bytes, + build: pipeline.DiagnosticBuildObservationV1, +) -> bytes: + """Independently derive BUILD identity from a public request.""" + + return _build_identity_from_operation_v2( + pipeline._snapshot_pipeline_operation_v1(request), + source_identity, + build, + ) + + def _run_identity_v1( request: pipeline.PipelineRequestV1, build: pipeline.DiagnosticBuildObservationV1, @@ -398,12 +403,21 @@ def __init__( transcript: protocol.DecisionTranscriptV1, run_claim: protocol.RunClaimV1, *, + _operation: pipeline._PipelineOperationSnapshotV1, _token: object, ) -> None: - if _token is not _EVIDENCE_TOKEN: + if ( + _token is not _EVIDENCE_TOKEN + or type(_operation) is not pipeline._PipelineOperationSnapshotV1 + or _operation.request is not request + ): raise TypeError("ContentResolvedEvaluatorReplayV1 is controller-derived") - source_identity = _source_identity_v1(request) - build_identity = _build_identity_v2(request, source_identity, build) + source_identity = _source_identity_from_operation_v1(_operation) + build_identity = _build_identity_from_operation_v2( + _operation, + source_identity, + build, + ) run_identity = _run_identity_v1( request, build, @@ -442,35 +456,510 @@ def identity(self) -> bytes: return self._identity +@dataclass(frozen=True) +class _EvidenceFieldsV1: + """One non-reentrant observation of every public evidence coordinate.""" + + request: pipeline.PipelineRequestV1 + build: pipeline.DiagnosticBuildObservationV1 + invocation: executor.ExecutionRequestV1 + platform: executor.SupportedV1 + process: executor.CompletedV1 + transcript: protocol.DecisionTranscriptV1 + run_claim: protocol.RunClaimV1 + source_identity: bytes + build_identity: bytes + run_identity: bytes + identity: bytes + + +def _capture_evidence_fields_v1(value: object) -> _EvidenceFieldsV1: + """Read all public fields before replay can re-enter a hostile fixture.""" + + if type(value) is not ContentResolvedEvaluatorReplayV1: + raise TypeError("evidence must be ContentResolvedEvaluatorReplayV1") + return _EvidenceFieldsV1( + value.request, + value.build, + value.invocation, + value.platform, + value.process, + value.transcript, + value.run_claim, + value.source_identity, + value.build_identity, + value.run_identity, + value._identity, + ) + + +def _same_evidence_references_v1( + first: _EvidenceFieldsV1, + second: _EvidenceFieldsV1, +) -> bool: + """Reject replacement even when an attacker chooses equal-looking values.""" + + return ( + first.request is second.request + and first.build is second.build + and first.invocation is second.invocation + and first.platform is second.platform + and first.process is second.process + and first.transcript is second.transcript + and first.run_claim is second.run_claim + and first.source_identity is second.source_identity + and first.build_identity is second.build_identity + and first.run_identity is second.run_identity + and first.identity is second.identity + ) + + +def _request_replay_coordinates_v1( + request: pipeline.PipelineRequestV1, +) -> tuple[bytes, ...]: + """Project a request without reopening a second materialized source closure.""" + + if type(request) is not pipeline.PipelineRequestV1: + raise TypeError("request must be PipelineRequestV1") + source_lock = request.source_lock + admitted_sources = request.admitted_sources + build_sources = request.build_sources + job = request.job + execution_limits = request.execution_limits + if ( + type(source_lock) is not provenance.ArbSourceLockV1 + or type(admitted_sources) is not provenance.AdmittedArbSourcesV1 + or type(build_sources) is not pipeline.AdmittedBuildSourcesV1 + or type(job) is not protocol.ProofJobV1 + or type(execution_limits) is not executor.ExecutionLimitsV1 + or admitted_sources.source_lock_identity != source_lock.identity + or type(source_lock.sources) is not tuple + or type(admitted_sources.sources) is not tuple + or len(source_lock.sources) != provenance.SOURCE_CLOSURE_COUNT_V1 + or len(admitted_sources.sources) != provenance.SOURCE_CLOSURE_COUNT_V1 + ): + raise TypeError("request coordinates are not canonical") + source_coordinates: list[bytes] = [] + for lock, source in zip( + source_lock.sources, + admitted_sources.sources, + strict=True, + ): + if ( + type(lock) is not provenance.SourceReleaseLockV1 + or type(source) is not provenance.SafeSourceArchiveV1 + ): + raise TypeError("request source coordinates are not canonical") + archive = source.archive_bytes + if ( + type(archive) is not bytes + or type(source.source_lock_identity) is not bytes + or type(source.archive_sha256) is not bytes + or type(source.tree_identity) is not bytes + or type(source.regular_file_count) is not int + or type(source.regular_file_bytes) is not int + or source.source_lock_identity != lock.identity + or source.archive_sha256 != lock.archive_sha256 + or source.regular_file_count != lock.regular_file_count + or source.regular_file_bytes != lock.regular_file_bytes + or source.archive_sha256 != hashlib.sha256(archive).digest() + ): + raise TypeError("retained source coordinates changed") + source_coordinates.extend( + provenance._source_archive_coordinates_from_replayed_v1(lock, source) + ) + canonical_job = _canonical_proof_job_with_coherent_identities_v1(job) + canonical_limits = executor.ExecutionLimitsV1(*tuple(execution_limits)) + return ( + source_lock.encode(), + source_lock.identity, + admitted_sources.identity, + *source_coordinates, + build_sources.identity, + build_sources.build_input_identity, + build_sources.formula_support_identity, + pipeline.build_source_manifest_bytes_v1(build_sources), + canonical_job.encode(), + canonical_job.identity, + *( + value.to_bytes(8, "big") + for value in canonical_limits + ), + pipeline._host_trust_wire_v1(request.host_trust), + ) + + +def _exact_digest_v1(value: object, field_name: str) -> bytes: + if type(value) is not bytes or len(value) != 32 or value == bytes(32): + raise TypeError(f"invalid {field_name}") + return value + + +def _require_canonical_digest_v1( + retained: object, + canonical: object, + field_name: str, +) -> None: + """Reject an observable identity cache that disagrees with fresh wire state.""" + + if _exact_digest_v1(retained, field_name) != _exact_digest_v1( + canonical, + f"canonical {field_name}", + ): + raise TypeError(f"{field_name} does not match canonical wire state") + + +def _canonical_proof_job_with_coherent_identities_v1( + value: object, +) -> protocol.ProofJobV1: + """Detach a job and require every identity cache used by its wire to agree. + + ``cached_property`` is a performance detail, not an authority boundary: + frozen public protocol values still expose a writable ``__dict__`` to + hostile callers. The detached snapshot supplies the cache-free oracle. + """ + + if type(value) is not protocol.ProofJobV1: + raise TypeError("request job must be ProofJobV1") + canonical = protocol.snapshot_proof_job_v1(value) + _require_canonical_digest_v1( + value.definition.definition_digest, + canonical.definition.definition_digest, + "request definition digest", + ) + _require_canonical_digest_v1( + value.domain.identity, + canonical.domain.identity, + "request domain identity", + ) + _require_canonical_digest_v1( + value.policy.identity, + canonical.policy.identity, + "request policy identity", + ) + _require_canonical_digest_v1( + value.identity, + canonical.identity, + "request job identity", + ) + return canonical + + +def _bytes_stability_coordinate_v1(value: object, field_name: str) -> bytes: + if type(value) is not bytes: + raise TypeError(f"invalid {field_name}") + return len(value).to_bytes(8, "big") + hashlib.sha256(value).digest() + + +def _build_stability_coordinates_v1( + build: pipeline.DiagnosticBuildObservationV1, +) -> tuple[bytes, ...]: + """Capture every mutable BUILD observation coordinate without source replay.""" + + if ( + type(build) is not pipeline.DiagnosticBuildObservationV1 + or tuple(field.name for field in fields(build)) + != _DIAGNOSTIC_BUILD_FIELDS_V1 + ): + raise TypeError("diagnostic BUILD schema is not canonical V1") + scalar_digests = tuple( + _exact_digest_v1(getattr(build, name), name) + for name in ( + "structural_source_identity", + "flint_commit_content_identity", + "flint_project_pinned_release_only_identity", + "build_input_identity", + "formula_support_identity", + "pipeline_policy_identity", + "binary_sha256", + "input_bundle_identity", + "input_bundle_sha256", + ) + ) + counts = ( + build.flint_commit_content_file_count, + build.flint_project_pinned_release_only_file_count, + build.input_bundle_length, + ) + if any(type(value) is not int or value <= 0 for value in counts): + raise TypeError("invalid diagnostic BUILD count") + rebuild_sha256s = build.rebuild_sha256s + binary = build.binary + input_bundle = build.input_bundle + processes = build.build_processes + comparator = build.comparator + if ( + type(binary) is not bytes + or type(rebuild_sha256s) is not tuple + or len(rebuild_sha256s) != 2 + or any( + _exact_digest_v1(value, "rebuild_sha256") != build.binary_sha256 + for value in rebuild_sha256s + ) + or type(processes) is not tuple + or len(processes) != 2 + or any( + type(process) is not build_transport.DockerBuildExitedV1 + for process in processes + ) + or type(comparator) is not pipeline.DiagnosticArbComparatorV1 + or tuple(field.name for field in fields(comparator)) + != _DIAGNOSTIC_COMPARATOR_FIELDS_V1 + ): + raise TypeError("invalid diagnostic BUILD observation") + rebuild_binaries = build.rebuild_binaries + if ( + type(rebuild_binaries) is not tuple + or len(rebuild_binaries) != 2 + or any(type(value) is not bytes for value in rebuild_binaries) + ): + raise TypeError("diagnostic BUILD executable binding changed") + if ( + input_bundle.binding_identity != build.input_bundle_identity + or input_bundle.sha256 != build.input_bundle_sha256 + or input_bundle.length != build.input_bundle_length + or type(input_bundle.contents) is not bytes + or hashlib.sha256(input_bundle.contents).digest() != input_bundle.sha256 + ): + raise TypeError("diagnostic BUILD input bundle changed") + preimages = comparator.preimages + manifest = comparator.manifest + if ( + type(preimages) is not pipeline.ArbComparatorPreimagesV1 + or type(manifest) is not protocol.ContentResolvedComparatorManifestV2 + or type(manifest.manifest) is not protocol.ComparatorManifestV2 + or comparator.structural_source_identity != build.structural_source_identity + or comparator.build_input_identity != build.build_input_identity + or comparator.pipeline_policy_identity != build.pipeline_policy_identity + or comparator.binary_sha256 != build.binary_sha256 + or comparator.rebuild_sha256s != rebuild_sha256s + ): + raise TypeError("diagnostic BUILD comparator binding changed") + manifest_bytes = manifest.manifest.encode() + parsed_manifest = protocol.ComparatorManifestV2.parse(manifest_bytes) + preimage_coordinates = tuple( + _bytes_stability_coordinate_v1( + getattr(preimages, field.name), + f"comparator preimage {field.name}", + ) + for field in fields(preimages) + ) + resolved_manifest = protocol.ContentResolvedComparatorManifestV2.admit( + parsed_manifest, + { + hashlib.sha256(getattr(preimages, field.name)).digest(): getattr( + preimages, + field.name, + ) + for field in fields(preimages) + }.get, + ) + if resolved_manifest.manifest.encode() != manifest_bytes: + raise TypeError("diagnostic BUILD manifest changed") + _require_canonical_digest_v1( + manifest.manifest.identity, + parsed_manifest.identity, + "diagnostic BUILD manifest identity", + ) + _require_canonical_digest_v1( + manifest.identity, + resolved_manifest.identity, + "diagnostic BUILD resolved manifest identity", + ) + _require_canonical_digest_v1( + comparator.identity, + resolved_manifest.identity, + "diagnostic BUILD comparator identity", + ) + return ( + *scalar_digests, + *(value.to_bytes(8, "big") for value in counts), + build_transport.docker_capability_identity_v1(build.docker_capability), + pipeline._host_trust_wire_v1(build.host_trust), + _bytes_stability_coordinate_v1(binary, "diagnostic BUILD binary"), + bytes( + ( + binary is rebuild_binaries[0], + binary is processes[0].stdout, + rebuild_binaries[1] is processes[1].stdout, + hashlib.sha256(binary).digest() == build.binary_sha256, + ) + ), + *( + _bytes_stability_coordinate_v1(value, "diagnostic rebuild binary") + for value in rebuild_binaries + ), + input_bundle.binding_identity, + input_bundle.sha256, + input_bundle.length.to_bytes(8, "big"), + _bytes_stability_coordinate_v1( + input_bundle.contents, + "diagnostic BUILD input bytes", + ), + build_transport.build_process_bytes_v1(processes[0]), + build_transport.build_process_bytes_v1(processes[1]), + manifest_bytes, + *preimage_coordinates, + comparator.structural_source_identity, + comparator.build_input_identity, + comparator.pipeline_policy_identity, + comparator.binary_sha256, + *comparator.rebuild_sha256s, + ) + + +def _evidence_stability_coordinates_v1(fields_value: _EvidenceFieldsV1) -> bytes: + """Bind the entry-to-exit value state; this is not a receipt identity.""" + + invocation_identity = executor.invocation_identity_v1(fields_value.invocation) + platform_identity = executor.platform_identity_v1(fields_value.platform) + if type(invocation_identity) is not bytes or type(platform_identity) is not bytes: + raise TypeError("execution coordinates did not replay") + process = fields_value.process + if ( + type(process) is not executor.CompletedV1 + or type(process.stdout) is not bytes + or type(process.stderr) is not bytes + ): + raise TypeError("RUN observation is not structurally bound") + transcript = fields_value.transcript + run_claim = fields_value.run_claim + if ( + type(transcript) is not protocol.DecisionTranscriptV1 + or type(run_claim) is not protocol.RunClaimV1 + ): + raise TypeError("RUN protocol observations are not canonical") + transcript_bytes = transcript.encode() + canonical_transcript = protocol.DecisionTranscriptV1.parse(transcript_bytes) + run_claim_bytes = run_claim.encode() + canonical_claim = protocol.RunClaimV1.parse(run_claim_bytes) + if ( + canonical_transcript.encode() != transcript_bytes + or canonical_claim.encode() != run_claim_bytes + ): + raise TypeError("RUN protocol bindings changed") + _require_canonical_digest_v1( + transcript.identity, + canonical_transcript.identity, + "RUN transcript identity", + ) + _require_canonical_digest_v1( + run_claim.identity, + canonical_claim.identity, + "RUN claim identity", + ) + return _identity( + _EVIDENCE_STABILITY_LABEL_V1, + ( + _identity( + b"labcolors.proof-region.arb-request-stability.v1\0", + _request_replay_coordinates_v1(fields_value.request), + ), + _identity( + b"labcolors.proof-region.arb-build-stability.v1\0", + _build_stability_coordinates_v1(fields_value.build), + ), + invocation_identity, + platform_identity, + process.binary_sha256, + _bytes_stability_coordinate_v1(process.stdout, "RUN stdout"), + _bytes_stability_coordinate_v1(process.stderr, "RUN stderr"), + _bytes_stability_coordinate_v1(transcript_bytes, "RUN transcript"), + _bytes_stability_coordinate_v1(run_claim_bytes, "RUN claim"), + bytes( + ( + fields_value.invocation.executable is fields_value.build.binary, + process.binary_sha256 == fields_value.build.binary_sha256, + transcript_bytes == process.stdout, + canonical_claim.binary_identity + == fields_value.build.binary_sha256, + canonical_claim.invocation_identity == invocation_identity, + canonical_claim.platform_identity == platform_identity, + canonical_claim.transcript_identity + == canonical_transcript.identity, + ) + ), + _exact_digest_v1(fields_value.source_identity, "source identity"), + _exact_digest_v1(fields_value.build_identity, "build identity"), + _exact_digest_v1(fields_value.run_identity, "run identity"), + _exact_digest_v1(fields_value.identity, "evidence identity"), + ), + ) + + +def _replay_evidence_fields_v1( + operation: pipeline._PipelineOperationSnapshotV1, + fields: _EvidenceFieldsV1, +) -> tuple[bytes, bytes, bytes, bytes]: + """Re-derive the three evidence edges from one owned source operation.""" + + if any( + type(value) is not bytes + for value in ( + fields.source_identity, + fields.build_identity, + fields.run_identity, + fields.identity, + ) + ): + raise TypeError("evidence identities must be exact bytes") + request = operation.request + source_identity = _source_identity_from_operation_v1(operation) + build_identity = _build_identity_from_operation_v2( + operation, + source_identity, + fields.build, + ) + run_identity = _run_identity_v1( + request, + fields.build, + build_identity, + fields.invocation, + fields.platform, + fields.process, + fields.transcript, + fields.run_claim, + ) + identity = _identity( + _EVIDENCE_ID_LABEL_V1, + (source_identity, build_identity, run_identity), + ) + if ( + fields.source_identity != source_identity + or fields.build_identity != build_identity + or fields.run_identity != run_identity + or fields.identity != identity + ): + raise TypeError("evidence identity did not replay") + return source_identity, build_identity, run_identity, identity + + def replay_evidence_is_well_bound_v1(value: object) -> bool: try: - if type(value) is not ContentResolvedEvaluatorReplayV1: + before = _capture_evidence_fields_v1(value) + before_stability = _evidence_stability_coordinates_v1(before) + operation = pipeline._snapshot_pipeline_operation_v1(before.request) + expected_request = _request_replay_coordinates_v1(operation.request) + first = _replay_evidence_fields_v1(operation, before) + + middle = _capture_evidence_fields_v1(value) + if ( + not _same_evidence_references_v1(before, middle) + or _evidence_stability_coordinates_v1(middle) != before_stability + or _request_replay_coordinates_v1(before.request) != expected_request + ): return False - source_identity = _source_identity_v1(value.request) - build_identity = _build_identity_v2( - value.request, - source_identity, - value.build, - ) - run_identity = _run_identity_v1( - value.request, - value.build, - build_identity, - value.invocation, - value.platform, - value.process, - value.transcript, - value.run_claim, - ) + + # The source operation has one retained materialization. A second + # edge replay and a second structural projection close the interval in + # which a mutable public object could otherwise change mid-check. + second = _replay_evidence_fields_v1(operation, middle) + after = _capture_evidence_fields_v1(value) return ( - value.source_identity == source_identity - and value.build_identity == build_identity - and value.run_identity == run_identity - and value._identity - == _identity( - _EVIDENCE_ID_LABEL_V1, - (source_identity, build_identity, run_identity), - ) + first == second + and _same_evidence_references_v1(middle, after) + and _evidence_stability_coordinates_v1(after) == before_stability + and _request_replay_coordinates_v1(before.request) == expected_request ) except Exception: return False @@ -572,27 +1061,6 @@ def __post_init__(self) -> None: | pipeline.TranscriptRejectedV1 ) - -def _limits_copy_v1(value: executor.ExecutionLimitsV1) -> executor.ExecutionLimitsV1: - return executor.ExecutionLimitsV1(*value) - - -def _resolve_request_v1( - request: pipeline.PipelineRequestV1, -) -> pipeline.PipelineRequestV1: - lock = provenance.ArbSourceLockV1.parse(request.source_lock.encode()) - if lock.identity != request.source_lock.identity: - raise TypeError("source lock did not replay") - return pipeline.PipelineRequestV1( - lock, - request.admitted_sources, - pipeline.admit_build_sources_v1(request.build_sources.files), - protocol.ProofJobV1.parse(request.job.encode()), - _limits_copy_v1(request.execution_limits), - request.host_trust, - ) - - def _enter_observer_cgroup_v1(parent: Path) -> None: """Move this dedicated one-shot controller into the declared observer group.""" @@ -666,7 +1134,8 @@ def execute(self, request: pipeline.PipelineRequestV1) -> SourceBoundResultV1: "exact SourceBoundArbControllerV1 and PipelineRequestV1 are required", ) try: - replay_request = _resolve_request_v1(request) + operation = pipeline._snapshot_pipeline_operation_v1(request) + replay_request = operation.request except Exception: return SourceBoundRejectedV1( SourceBoundFailureReasonV1.SOURCE_REPLAY_FAILED, @@ -682,8 +1151,10 @@ def execute(self, request: pipeline.PipelineRequestV1) -> SourceBoundResultV1: SourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, "native build backend authority changed", ) - built = pipeline.ControlledPipelineV1(build_backend=build_backend).build( - replay_request + built = pipeline.ControlledPipelineV1( + build_backend=build_backend + )._build_snapshot_v1( + operation ) if type(built) is not pipeline.DiagnosticBuildObservationV1: return built @@ -803,6 +1274,7 @@ def execute(self, request: pipeline.PipelineRequestV1) -> SourceBoundResultV1: observed, transcript, run_claim, + _operation=operation, _token=_EVIDENCE_TOKEN, ) claim = protocol.EvaluatorProvenanceClaimV1( diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py index 3df4660b..4ed23d96 100644 --- a/proof/region/v1/arb/tests/gate.py +++ b/proof/region/v1/arb/tests/gate.py @@ -19,7 +19,7 @@ "test_mpfi_input.py", ) EXPECTED_TEST_INVENTORY_SHA256 = ( - "c74942a9240b68327921160f86fd948532849234bb6da00a0075a137fef098cc" + "6a616daac6d2437b372d93e8e5fe03787557e7a75aea25475ca9d349568669aa" ) _EVALUATOR_REASON = "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary" EXPECTED_SKIPS = frozenset( @@ -112,7 +112,7 @@ def run_exact_suite_v1( or actual_inventory_sha256 != expected_inventory_sha256 ): print( - "Arb test inventory drift: " + "Proof fast gate inventory drift: " f"count={len(tests)} sha256={actual_inventory_sha256} " f"expected={expected_inventory_sha256}", file=sys.stderr, diff --git a/proof/region/v1/arb/tests/test_build_identity_v2.py b/proof/region/v1/arb/tests/test_build_identity_v2.py index 00bf006b..6a146ff5 100644 --- a/proof/region/v1/arb/tests/test_build_identity_v2.py +++ b/proof/region/v1/arb/tests/test_build_identity_v2.py @@ -469,14 +469,20 @@ def test_diagnostic_build_owns_one_capability_and_replayers_consume_its_identity comparator_calls = _called_names( pipeline._derive_arb_comparator_for_build_v1 ) - comparator_replay_calls = _called_names(receipt._comparator_replays_v1) + comparator_replay = receipt._comparator_replays_from_operation_v1 + comparator_replay_calls = _called_names(comparator_replay) receipt_build_calls = _called_names(receipt._build_identity_v2) + receipt_owned_build_calls = _called_names( + receipt._build_identity_from_operation_v2 + ) source_bound_calls = _called_names( receipt.source_bound_policy_identity_v2 ) self.assertIn("docker_capability_identity_v1", comparator_calls) - self.assertIn("docker_capability_identity_v1", comparator_replay_calls) - self.assertIn("docker_capability_identity_v1", receipt_build_calls) + self.assertIn("_derive_arb_comparator_for_build_v1", comparator_replay_calls) + self.assertIn("build.docker_capability", inspect.getsource(comparator_replay)) + self.assertIn("_build_identity_from_operation_v2", receipt_build_calls) + self.assertIn("docker_capability_identity_v1", receipt_owned_build_calls) self.assertIn("docker_capability_identity_v1", source_bound_calls) def test_path_uid_daemon_and_hostname_flow_to_downstream_build_identity_only(self) -> None: diff --git a/proof/region/v1/arb/tests/test_pipeline.py b/proof/region/v1/arb/tests/test_pipeline.py index f09c9af9..272ffa1c 100644 --- a/proof/region/v1/arb/tests/test_pipeline.py +++ b/proof/region/v1/arb/tests/test_pipeline.py @@ -408,6 +408,47 @@ def test_capabilities_cannot_be_directly_forged(self) -> None: _token=object(), ) + def test_admission_owns_a_fresh_build_file_snapshot(self) -> None: + source_files = tuple( + pipeline.BuildSourceFileV1(item.path, item.mode, item.contents) + for item in _build_sources().files + ) + admitted = pipeline.admit_build_sources_v1(source_files) + original = source_files[0].contents + object.__setattr__(source_files[0], "contents", b"forged") + try: + self.assertEqual(admitted.contents(source_files[0].path), original) + self.assertEqual( + admitted.identity, + pipeline.admit_build_sources_v1(admitted.files).identity, + ) + finally: + object.__setattr__(source_files[0], "contents", original) + + def test_manifest_replay_rejects_forged_retained_identities(self) -> None: + sources = _build_sources() + sentinel = object() + originals = { + name: sources.__dict__.get(name, sentinel) + for name in ( + "identity", + "build_input_identity", + "formula_support_identity", + ) + } + + for name, original in originals.items(): + with self.subTest(retained_coordinate=name): + object.__setattr__(sources, name, _digest(f"forged-{name}")) + try: + with self.assertRaises(TypeError): + pipeline.build_source_manifest_bytes_v1(sources) + finally: + if original is sentinel: + del sources.__dict__[name] + else: + sources.__dict__[name] = original + class FlintSourcePartitionTests(unittest.TestCase): def test_partition_is_nonempty_and_separately_binds_both_content_sets(self) -> None: @@ -645,6 +686,470 @@ def test_admission_uses_only_explicit_cross_module_verification_api(self) -> Non self.assertTrue(callable(executor.platform_identity_v1)) self.assertFalse(hasattr(pipeline, "invocation_identity_v1")) self.assertFalse(hasattr(pipeline, "platform_identity_v1")) + self.assertFalse(hasattr(pipeline, "comparator_build_preimage_v2")) + + def test_arb_input_keeps_one_source_snapshot_across_reentrant_mutation( + self, + ) -> None: + request = _request() + source = request.admitted_sources.sources[0] + original_tree_identity = source.tree_identity + real_encoder = pipeline.build_input.canonical_ustar_v1 + + def encode_then_mutate( + entries: tuple[tuple[str, int, bytes], ...], + limits: build_input.CanonicalInputLimitsV1, + ) -> bytes: + encoded = real_encoder(entries, limits) + object.__setattr__(source, "tree_identity", _digest("reentrant-tree")) + return encoded + + try: + with mock.patch.object( + pipeline.build_input, + "canonical_ustar_v1", + side_effect=encode_then_mutate, + ): + sealed = pipeline._seal_build_input_bundle_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + self.assertFalse( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + + def test_arb_input_rejects_reentrant_unadmitted_build_source(self) -> None: + request = _request() + build_file = next( + item + for item in request.build_sources.files + if item.path == pipeline.BUILD_RECIPE_PATH_V1 + ) + original_contents = build_file.contents + real_encoder = pipeline.build_input.canonical_ustar_v1 + + def encode_then_mutate( + entries: tuple[tuple[str, int, bytes], ...], + limits: build_input.CanonicalInputLimitsV1, + ) -> bytes: + encoded = real_encoder(entries, limits) + object.__setattr__( + build_file, + "contents", + b"#!/bin/sh\nprintf '%s\\n' forged-build-source\n", + ) + return encoded + + try: + with mock.patch.object( + pipeline.build_input, + "canonical_ustar_v1", + side_effect=encode_then_mutate, + ): + sealed = pipeline._seal_build_input_bundle_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + self.assertFalse( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + finally: + object.__setattr__(build_file, "contents", original_contents) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + + def test_pipeline_rederives_both_cached_build_coordinates(self) -> None: + for field_name in ("build_input_identity", "formula_support_identity"): + with self.subTest(cached_coordinate=field_name): + request = _request() + fresh = pipeline.admit_build_sources_v1(request.build_sources.files) + original = request.build_sources.__dict__.get(field_name) + forged = _digest(f"forged-{field_name}") + request.build_sources.__dict__[field_name] = forged + binary = _static_elf(b"cached-coordinate") + backend = _BuildBackend((binary, binary)) + try: + result = pipeline.ControlledPipelineV1( + build_backend=backend, + ).build(request) + finally: + if original is None: + request.build_sources.__dict__.pop(field_name, None) + else: + request.build_sources.__dict__[field_name] = original + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + self.assertEqual( + getattr(result, field_name), + getattr(fresh, field_name), + ) + self.assertNotEqual(getattr(result, field_name), forged) + + def test_constructor_rejects_a_nominal_forged_build_capability( + self, + ) -> None: + normal = _request() + forged_files = tuple( + replace( + item, + contents=b"#!/bin/sh\nprintf '%s\\n' forged-build-source\n", + ) + if item.path == pipeline.BUILD_RECIPE_PATH_V1 + else item + for item in normal.build_sources.files + ) + forged = object.__new__(pipeline.AdmittedBuildSourcesV1) + object.__setattr__(forged, "files", forged_files) + object.__setattr__(forged, "identity", _digest("forged-build-closure")) + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + _request(build_sources=forged) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.INVALID_RETAINED_INPUT, + ) + self.assertEqual(caught.exception.field, "build_sources") + + def test_private_build_recheck_keeps_the_entry_snapshot_across_attempts( + self, + ) -> None: + request = _request() + source = request.admitted_sources.sources[0] + original_tree_identity = source.tree_identity + binary = _static_elf(b"snapshot-attempt") + backend = _BuildBackend((binary, binary)) + real_run_build = backend.run_build + mutated = False + + def run_then_mutate( + value: build_transport.DockerBuildRequestV1, + ) -> build_transport.DockerBuildProcessObservationV1: + nonlocal mutated + if not mutated: + object.__setattr__(source, "tree_identity", _digest("late-tree")) + mutated = True + return real_run_build(value) + + backend.run_build = run_then_mutate # type: ignore[method-assign] + try: + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + self.assertFalse( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + result.input_bundle, + ) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + self.assertTrue(mutated) + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + result.input_bundle, + ) + ) + + def test_snapshot_captures_job_before_source_replay(self) -> None: + request = _request() + original_domain = request.job.domain + foreign_job = replace( + request.job, + domain=type(original_domain).from_ordinals((0,)), + ) + real_replay = provenance.replay_admitted_source_closure_v1 + + def replay_then_mutate( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + ) -> provenance.ReplayedSourceClosureV1: + snapshot = real_replay(source_lock, admitted_sources) + object.__setattr__(request.job, "domain", foreign_job.domain) + return snapshot + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_mutate, + ): + snapshot = pipeline._snapshot_pipeline_operation_v1(request) + finally: + object.__setattr__(request.job, "domain", original_domain) + + self.assertEqual( + snapshot.request.job.domain.point_count, + original_domain.point_count, + ) + + def test_snapshot_ignores_a_proof_job_encoder_shadow(self) -> None: + request = _request() + job = request.job + original_identity = job.identity + foreign_budget = replace( + job.policy.comparators[0], + global_pregrant=job.policy.comparators[0].global_pregrant + 1, + ) + foreign_policy = replace( + job.policy, + comparators=(foreign_budget, job.policy.comparators[1]), + ) + foreign_job = replace(job, policy=foreign_policy) + job.__dict__["encode"] = lambda: ProofJobV1.encode(foreign_job) + try: + snapshot = pipeline._snapshot_pipeline_operation_v1(request) + finally: + del job.__dict__["encode"] + + self.assertEqual(snapshot.request.job.identity, original_identity) + + def test_snapshot_uses_raw_nested_job_coordinates_not_caches_or_encoders( + self, + ) -> None: + request = _request() + job = request.job + original_identity = job.identity + sentinel = object() + shadows = ( + (job.definition, "encode"), + (job.definition, "definition_digest"), + (job.domain, "encode"), + (job.domain, "identity"), + (job.policy, "encode"), + (job.policy, "identity"), + ) + originals = [ + (value, name, value.__dict__.get(name, sentinel)) + for value, name in shadows + ] + + def explode() -> bytes: + raise AssertionError("snapshot dispatched caller-owned job state") + + for value, name in shadows: + value.__dict__[name] = explode if name == "encode" else _digest(name) + try: + snapshot = pipeline._snapshot_pipeline_operation_v1(request) + finally: + for value, name, original in originals: + if original is sentinel: + del value.__dict__[name] + else: + value.__dict__[name] = original + + self.assertEqual(snapshot.request.job.identity, original_identity) + + def test_private_operation_snapshot_cannot_be_constructed_by_a_caller(self) -> None: + snapshot = pipeline._snapshot_pipeline_operation_v1(_request()) + + with self.assertRaises(TypeError): + pipeline._PipelineOperationSnapshotV1( + snapshot.request, + snapshot.source_closure, + _token=object(), + ) + + def test_constructor_rejects_a_foreign_admitted_source_closure(self) -> None: + request = _request() + foreign_flint = replace( + request.source_lock.sources[2], + version="foreign-release", + ) + foreign_lock = provenance.ArbSourceLockV1( + request.source_lock.sources[:2] + (foreign_flint,) + ) + + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + pipeline.PipelineRequestV1( + foreign_lock, + request.admitted_sources, + request.build_sources, + request.job, + request.execution_limits, + request.host_trust, + ) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_constructor_rejects_a_noncanonical_retained_source_manifest( + self, + ) -> None: + """A nominal capability cannot retain a mutable manifest container.""" + + request = _request() + source = request.admitted_sources.sources[2] + original_files = source.files + object.__setattr__(source, "files", list(original_files)) + try: + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + pipeline.PipelineRequestV1( + request.source_lock, + request.admitted_sources, + request.build_sources, + request.job, + request.execution_limits, + request.host_trust, + ) + finally: + object.__setattr__(source, "files", original_files) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_constructor_totalizes_a_hostile_retained_source_path(self) -> None: + request = _request() + source = request.admitted_sources.sources[2] + archive_file = source.files[0] + original_path = archive_file.path + + class HashBomb: + def __hash__(self) -> int: + raise RuntimeError("unexpected hash dispatch") + + object.__setattr__(archive_file, "path", HashBomb()) + try: + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + pipeline.PipelineRequestV1( + request.source_lock, + request.admitted_sources, + request.build_sources, + request.job, + request.execution_limits, + request.host_trust, + ) + finally: + object.__setattr__(archive_file, "path", original_path) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_build_uses_one_source_operation_snapshot(self) -> None: + request = _request() + binary = _static_elf(b"one-source-operation") + backend = _BuildBackend((binary, binary)) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 3) + + def test_request_admission_rechecks_separately_from_its_operation( + self, + ) -> None: + """Request admission and a later operation must not share mutable evidence.""" + + source_lock, admitted_sources = _source_fixture() + build_sources = _build_sources() + job = _job() + limits = _limits() + binary = _static_elf(b"request-then-one-operation") + backend = _BuildBackend((binary, binary)) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + request = pipeline.PipelineRequestV1( + source_lock, + admitted_sources, + build_sources, + job, + limits, + pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST, + ) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 0) + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(replay.call_count, 6) + self.assertEqual(materialize.call_count, 3) + + def test_build_rejects_mutated_request_before_it_can_start_a_build(self) -> None: + for field_name, replacement in ( + ("host_trust", "foreign"), + ("execution_limits", "foreign"), + ): + with self.subTest(field=field_name): + request = _request() + original = getattr(request, field_name) + backend = _BuildBackend((_static_elf(b"first"), _static_elf(b"second"))) + object.__setattr__(request, field_name, replacement) + try: + result = pipeline.ControlledPipelineV1( + build_backend=backend, + ).build(request) + finally: + object.__setattr__(request, field_name, original) + self.assertEqual(len(backend.requests), 0) + self.assertEqual( + result, + build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ), + ) def test_host_trust_claims_only_backend_observable_facts(self) -> None: trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST @@ -658,6 +1163,20 @@ def test_host_trust_claims_only_backend_observable_facts(self) -> None: ) ) + def test_host_trust_wire_is_private_and_does_not_read_mutable_enum_storage( + self, + ) -> None: + trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + baseline = pipeline.pipeline_policy_identity_v2(trust, policy) + self.assertFalse(hasattr(pipeline, "host_trust_wire_v1")) + original_value = trust._value_ + object.__setattr__(trust, "_value_", "forged-host-boundary") + try: + self.assertEqual(pipeline.pipeline_policy_identity_v2(trust, policy), baseline) + finally: + object.__setattr__(trust, "_value_", original_value) + def test_pipeline_policy_identity_binds_the_stream_bootstrap(self) -> None: trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 diff --git a/proof/region/v1/arb/tests/test_receipt.py b/proof/region/v1/arb/tests/test_receipt.py index 368014b3..6733e8b8 100644 --- a/proof/region/v1/arb/tests/test_receipt.py +++ b/proof/region/v1/arb/tests/test_receipt.py @@ -30,6 +30,9 @@ import receipt # noqa: E402 from region_proof_protocol import ( # noqa: E402 BoundaryUnprovenWitnessV1, + ComparatorKindV1, + ComparatorManifestV2, + ContentResolvedComparatorManifestV2, DecisionTranscriptV1, DecisionV1, RunClaimV1, @@ -221,6 +224,243 @@ def _replace_invocation( class SourceBoundReceiptTests(unittest.TestCase): + def test_public_verifier_rejects_a_top_level_switch_during_replay(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "request", result.evidence.request) + switched_request = _request( + execution_limits=_replace_limits( + result.evidence.request.execution_limits, + wall_timeout_ns=result.evidence.request.execution_limits.wall_timeout_ns + - 1, + ) + ) + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_switch( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence, "request", switched_request) + switched = True + return replayed + + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_switch, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + self.assertTrue(switched) + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_nested_request_switch_during_replay( + self, + ) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "request", result.evidence.request) + switched_limits = _replace_limits( + evidence.request.execution_limits, + wall_timeout_ns=evidence.request.execution_limits.wall_timeout_ns - 1, + ) + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_switch( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence.request, "execution_limits", switched_limits) + switched = True + return replayed + + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_switch, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + self.assertTrue(switched) + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_source_archive_switch_during_replay( + self, + ) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "request", result.evidence.request) + source = evidence.request.admitted_sources.sources[0] + original_archive = source.archive_bytes + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_switch( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(source, "_archive_bytes", original_archive + b"x") + switched = True + return replayed + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_switch, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + finally: + object.__setattr__(source, "_archive_bytes", original_archive) + + self.assertTrue(switched) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_build_repair_during_replay(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + original_binary = result.evidence.build.binary + evidence = _tamper( + result.evidence, + "build", + _tamper(result.evidence.build, "_binary", b"corrupt"), + ) + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_repair( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence.build, "_binary", original_binary) + switched = True + return replayed + + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_repair, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + self.assertTrue(switched) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_process_repair_during_replay(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "process", result.evidence.process) + original_stdout = evidence.process.stdout + object.__setattr__(evidence.process, "stdout", b"corrupt") + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_repair( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence.process, "stdout", original_stdout) + switched = True + return replayed + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_repair, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + finally: + object.__setattr__(evidence.process, "stdout", original_stdout) + + self.assertTrue(switched) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_poisoned_cached_identity_before_replay( + self, + ) -> None: + """Cached identities are observable state, not an unguarded speed cache.""" + + targets = ( + ( + "request definition", + lambda evidence: evidence.request.job.definition, + "definition_digest", + ), + ( + "request domain", + lambda evidence: evidence.request.job.domain, + "identity", + ), + ( + "request policy", + lambda evidence: evidence.request.job.policy, + "identity", + ), + ("request job", lambda evidence: evidence.request.job, "identity"), + ( + "inner comparator manifest", + lambda evidence: evidence.build.comparator.manifest.manifest, + "identity", + ), + ( + "resolved comparator manifest", + lambda evidence: evidence.build.comparator.manifest, + "identity", + ), + ("transcript", lambda evidence: evidence.transcript, "identity"), + ("run claim", lambda evidence: evidence.run_claim, "identity"), + ) + for name, select, field_name in targets: + with self.subTest(cache=name): + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = result.evidence + target = select(evidence) + original_identity = getattr(target, field_name) + forged_identity = _digest(f"forged {name}") + real_replay = provenance.replay_admitted_source_closure_v1 + repaired = False + object.__setattr__(target, field_name, forged_identity) + + def replay_then_repair( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal repaired + replayed = real_replay(*args, **kwargs) + object.__setattr__(target, field_name, original_identity) + repaired = True + return replayed + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_repair, + ): + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1(evidence) + ) + finally: + object.__setattr__(target, field_name, original_identity) + + self.assertFalse(repaired) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + def test_source_bound_policy_identity_binds_immutable_coordinates(self) -> None: capability = _docker_capability() request = _request() @@ -292,6 +532,8 @@ def test_only_controller_execution_can_seal_a_receipt(self) -> None: self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) self.assertIs(type(result.comparator), pipeline.DiagnosticArbComparatorV1) + self.assertFalse(hasattr(result.evidence, "source_closure")) + self.assertFalse(hasattr(result.evidence, "operation")) self.assertEqual(result.run_claim.identity, result.claim.run_claim_identity) self.assertEqual(result.evidence.identity, result.claim.replay_evidence_identity) self.assertEqual( @@ -313,6 +555,106 @@ def test_only_controller_execution_can_seal_a_receipt(self) -> None: second.input_transfer.bundle_identity, ) + def test_source_bound_controller_uses_one_source_operation_snapshot(self) -> None: + request = _request() + run_backend = _NativeRunBackend() + controller, patches = _controller( + _static_elf(b"one-source-bound-operation"), + run_backend, + ) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + result = controller.execute(request) + + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + self.assertEqual(len(run_backend.requests), 1) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 3) + + def test_source_bound_snapshot_survives_source_replay_reentrancy(self) -> None: + request = _request() + original_domain = request.job.domain + foreign_domain = type(original_domain).from_ordinals((0,)) + run_backend = _NativeRunBackend() + controller, patches = _controller( + _static_elf(b"source-bound-reentrancy"), + run_backend, + ) + real_replay = provenance.replay_admitted_source_closure_v1 + replay_calls = 0 + + def replay_then_mutate( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal replay_calls + replay_calls += 1 + snapshot = real_replay(source_lock, admitted_sources) + object.__setattr__(request.job, "domain", foreign_domain) + return snapshot + + try: + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_mutate, + ), + ): + result = controller.execute(request) + finally: + object.__setattr__(request.job, "domain", original_domain) + + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + self.assertEqual(replay_calls, 1) + self.assertEqual(result.evidence.request.job.domain, original_domain) + + def test_evidence_verifier_owns_one_fresh_source_operation_snapshot(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(result.evidence)) + + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 3) + def test_no_public_object_or_diagnostic_can_mint(self) -> None: result, _backend = _execute() with self.assertRaises(TypeError): @@ -332,16 +674,18 @@ def test_no_public_object_or_diagnostic_can_mint(self) -> None: self.assertFalse(hasattr(pipeline, "DiagnosticPipelineObservationV1")) self.assertFalse(hasattr(receipt.SourceBoundEvaluatorReceiptV1, "parse")) - def test_receipt_uses_only_versioned_public_cross_module_verifiers(self) -> None: + def test_receipt_keeps_snapshot_only_on_the_private_operation_path(self) -> None: source = (ARB / "receipt.py").read_text(encoding="utf-8") self.assertNotIn("pipeline._sealed_build_input_bundle_is_well_bound_v1", source) self.assertNotIn("pipeline._build_process_bytes_v1", source) self.assertNotIn("executor._execution_identity_v1", source) self.assertNotIn("sealed_build_input_bundle_is_well_bound_v1", source) - self.assertIn("pipeline.arb_input_is_bound_v1", source) + self.assertIn("pipeline._seal_build_input_from_snapshot_v1", source) + self.assertIn("pipeline._owned_arb_input_is_bound_v1", source) + self.assertIn("pipeline._derive_arb_comparator_for_build_v1", source) self.assertIn("build_transport.build_process_bytes_v1", source) - self.assertTrue(hasattr(pipeline, "arb_input_is_bound_v1")) + self.assertNotIn("pipeline.replay_pipeline_request_v1", source) self.assertTrue(hasattr(build_transport, "build_process_bytes_v1")) self.assertTrue(hasattr(executor, "invocation_identity_v1")) self.assertTrue(hasattr(executor, "platform_identity_v1")) @@ -591,6 +935,43 @@ def test_source_process_transfer_and_comparator_mutations_fail(self) -> None: receipt.replay_evidence_is_well_bound_v1(_tamper(dag, "build", build)) ) + # Keep the BUILD preimage itself intact: a verifier that only checks + # self-consistency would otherwise accept this fully well-formed but + # source-unrelated comparator manifest. + preimage_values = tuple( + dag.build.comparator.preimages.build_identity + if index == 5 + else f"forged-comparator-{index}".encode("ascii") + for index in range(10) + ) + self.assertEqual(len(set(preimage_values)), len(preimage_values)) + preimages = pipeline.ArbComparatorPreimagesV1(*preimage_values) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(value).digest() for value in preimage_values), + ) + by_digest = { + hashlib.sha256(value).digest(): value for value in preimage_values + } + resolved = ContentResolvedComparatorManifestV2.admit( + manifest, + by_digest.get, + ) + comparator = pipeline.DiagnosticArbComparatorV1( + preimages, + resolved, + dag.build.structural_source_identity, + dag.build.build_input_identity, + dag.build.pipeline_policy_identity, + dag.build.binary_sha256, + dag.build.rebuild_sha256s, + _token=pipeline._COMPARATOR_TOKEN, + ) + build = _tamper(dag.build, "comparator", comparator) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1(_tamper(dag, "build", build)) + ) + def test_source_replay_rejects_a_self_consistent_forged_manifest(self) -> None: request = _request() lock = request.source_lock.sources[2] @@ -634,6 +1015,20 @@ def test_invocation_process_and_same_object_mutations_fail(self) -> None: equal_executable_copy = bytes(bytearray(dag.invocation.executable)) self.assertEqual(equal_executable_copy, dag.invocation.executable) self.assertIsNot(equal_executable_copy, dag.invocation.executable) + operation = pipeline._snapshot_pipeline_operation_v1(dag.request) + request = operation.request + baseline = receipt.ContentResolvedEvaluatorReplayV1( + request, + dag.build, + dag.invocation, + dag.platform, + dag.process, + dag.transcript, + dag.run_claim, + _operation=operation, + _token=receipt._EVIDENCE_TOKEN, + ) + self.assertEqual(baseline.identity, dag.identity) mutants = ( _replace_invocation(dag.invocation, executable=equal_executable_copy), _replace_invocation( @@ -655,7 +1050,7 @@ def test_invocation_process_and_same_object_mutations_fail(self) -> None: ) ) forged_claim = RunClaimV1.for_transcript( - dag.request.job, + request.job, dag.build.comparator.manifest, dag.transcript, dag.build.binary_sha256, @@ -664,13 +1059,14 @@ def test_invocation_process_and_same_object_mutations_fail(self) -> None: ) with self.assertRaises(TypeError): receipt.ContentResolvedEvaluatorReplayV1( - dag.request, + request, dag.build, invocation, dag.platform, dag.process, dag.transcript, forged_claim, + _operation=operation, _token=receipt._EVIDENCE_TOKEN, ) mutated_limits = _replace_limits( diff --git a/proof/region/v1/arb/tests/test_transport.py b/proof/region/v1/arb/tests/test_transport.py index 4ec1aae0..ed0f92ec 100644 --- a/proof/region/v1/arb/tests/test_transport.py +++ b/proof/region/v1/arb/tests/test_transport.py @@ -736,6 +736,38 @@ def test_cleanup_failure_preserves_input_trigger_and_progress(self) -> None: class SealedBuildTransportContractTests(unittest.TestCase): + def test_closed_user_mode_keeps_policy_identity_when_enum_payload_is_tampered( + self, + ) -> None: + """The wire coordinate follows the admitted member, never mutable Enum data.""" + + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + baseline_identity = build_transport.transport_policy_identity_v1(policy) + user_mode = build_transport.DockerUserModeV1.HOST_EFFECTIVE_IDS + original_value = user_mode._value_ + object.__setattr__( + user_mode, + "_value_", + "forged_host_effective_ids", + ) + try: + self.assertEqual(user_mode.value, "forged_host_effective_ids") + self.assertTrue(build_transport.docker_policy_is_valid_v1(policy)) + self.assertEqual( + build_transport.transport_policy_identity_v1(policy), + baseline_identity, + ) + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + policy, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + self.assertIs(backend._policy.user_mode, user_mode) + finally: + object.__setattr__(user_mode, "_value_", original_value) + def test_diagnostic_details_have_one_strict_admission_law(self) -> None: constructors = ( ( @@ -829,11 +861,22 @@ def test_successful_probe_keeps_machine_readable_stdout_despite_cli_warning(self ) def test_controller_owns_one_sealed_bundle_for_both_builds(self) -> None: - pipeline_source = inspect.getsource(pipeline.ControlledPipelineV1.build) + public_pipeline_source = inspect.getsource(pipeline.ControlledPipelineV1.build) + owned_pipeline_source = inspect.getsource( + pipeline.ControlledPipelineV1._build_snapshot_v1 + ) transport_source = inspect.getsource( build_transport.ControlledBuildTransportV1.build ) - self.assertEqual(pipeline_source.count("_seal_build_input_bundle_v1("), 1) + self.assertEqual( + public_pipeline_source.count("_snapshot_pipeline_operation_v1("), + 1, + ) + self.assertIn("return self._build_snapshot_v1(snapshot)", public_pipeline_source) + self.assertEqual( + owned_pipeline_source.count("_seal_build_input_from_snapshot_v1("), + 1, + ) self.assertIn("for attempt in (1, 2)", transport_source) def test_docker_request_carries_only_semantic_build_coordinates(self) -> None: diff --git a/proof/region/v1/build/transport.py b/proof/region/v1/build/transport.py index 53bc552a..7a72e9d1 100644 --- a/proof/region/v1/build/transport.py +++ b/proof/region/v1/build/transport.py @@ -167,6 +167,17 @@ class DockerUserModeV1(StrEnum): HOST_EFFECTIVE_IDS = "host_effective_ids" +_DOCKER_USER_MODE_HOST_EFFECTIVE_IDS_WIRE_V1 = b"host_effective_ids" + + +def _docker_user_mode_wire_v1(value: object) -> bytes: + """Render a closed semantic member without reading mutable Enum payloads.""" + + if value is DockerUserModeV1.HOST_EFFECTIVE_IDS: + return _DOCKER_USER_MODE_HOST_EFFECTIVE_IDS_WIRE_V1 + raise TypeError("invalid Docker user mode") + + class DockerBuildPolicyV1(tuple): """Deeply immutable coordinates for one bounded Docker build transport.""" @@ -232,8 +243,7 @@ def __new__( tmpfs_specs = tuple(owned_tmpfs) if len(set(tmpfs_specs)) != len(tmpfs_specs): raise TypeError("invalid tmpfs_specs") - if type(user_mode) is not DockerUserModeV1: - raise TypeError("invalid Docker user mode") + _docker_user_mode_wire_v1(user_mode) limits = ( (stdout_limit, BUILD_STDOUT_LIMIT_V1, "stdout_limit"), (stderr_limit, BUILD_STDERR_LIMIT_V1, "stderr_limit"), @@ -337,7 +347,7 @@ def transport_policy_identity_v1(policy: DockerBuildPolicyV1) -> bytes: policy.bootstrap_argv0.encode("utf-8"), len(policy.tmpfs_specs).to_bytes(4, "big"), *(spec.encode("utf-8") for spec in policy.tmpfs_specs), - policy.user_mode.value.encode("ascii"), + _docker_user_mode_wire_v1(policy.user_mode), policy.stdout_limit.to_bytes(8, "big"), policy.stderr_limit.to_bytes(8, "big"), policy.build_timeout_ns.to_bytes(8, "big"), diff --git a/proof/region/v1/mpfi/input.py b/proof/region/v1/mpfi/input.py index e1caa8b5..c983efb6 100644 --- a/proof/region/v1/mpfi/input.py +++ b/proof/region/v1/mpfi/input.py @@ -49,17 +49,25 @@ def _canonical_lock_v1( if type(source_lock) is not provenance.MpfiSourceLockV1: _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "source_lock") try: - return provenance.MpfiSourceLockV1.parse(source_lock.encode()) - except provenance.ProvenanceErrorV1: + canonical = provenance.snapshot_source_closure_lock_v1(source_lock) + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "source_lock") - except (AttributeError, TypeError, ValueError, OverflowError): + if type(canonical) is not provenance.MpfiSourceLockV1: _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "source_lock") + return canonical def _fresh_admitted_sources_v1( source_lock: provenance.MpfiSourceLockV1, admitted_sources: provenance.AdmittedMpfiSourcesV1, -) -> provenance.AdmittedMpfiSourcesV1: +) -> provenance.ReplayedSourceClosureV1: if type(admitted_sources) is not provenance.AdmittedMpfiSourcesV1: _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "admitted_sources") try: @@ -74,21 +82,29 @@ def _fresh_admitted_sources_v1( MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources", ) - # Re-admission сохраняет semantic slot order: сортировка выдала бы - # forged GMP/MPFR exchange за легитимный closure. - return provenance.admit_mpfi_sources(source_lock, admitted_sources.sources) + except Exception: + # Exact type не делает retained capability неуязвимой к post-admission + # подмене; ordinary hostile failure обязан остаться typed rejection. + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + # Replay owns nested archive evidence; its typed provenance taxonomy must + # survive instead of being flattened into an MPFI declaration error. + try: + return provenance.replay_admitted_source_closure_v1( + source_lock, + admitted_sources, + ) except provenance.ProvenanceErrorV1 as error: - if error.reason is provenance.ProvenanceReasonV1.FOREIGN_BINDING: + if error.artifact == "source-closure-replay-v1": _fail( MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources", ) raise - except (AttributeError, TypeError, ValueError, OverflowError): - _fail( - MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, - "admitted_sources", - ) + except TypeError: + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources") def _canonical_limits_v1( @@ -138,8 +154,7 @@ def _preflight_declared_resource_bounds_v1( def _source_entries_v1( - source_lock: provenance.MpfiSourceLockV1, - admitted_sources: provenance.AdmittedMpfiSourcesV1, + snapshot: provenance.ReplayedSourceClosureV1, ) -> tuple[tuple[str, int, bytes], ...]: entries = tuple( ( @@ -147,15 +162,12 @@ def _source_entries_v1( mode, contents, ) - for lock, admitted in zip( - source_lock.sources, - admitted_sources.sources, + for lock, materialized in zip( + snapshot.source_lock.sources, + snapshot.sources, strict=True, ) - for relative, mode, contents in provenance.materialize_admitted_source_files_v1( - lock, - admitted, - ) + for relative, mode, contents in materialized.files ) if not entries: _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources") @@ -198,12 +210,19 @@ def seal_mpfi_source_input_v1( canonical_lock = _canonical_lock_v1(source_lock) canonical_limits = _canonical_limits_v1(limits) - canonical_admitted = _fresh_admitted_sources_v1(canonical_lock, admitted_sources) + # Declared totals are authenticated by the canonical release lock. Check + # them before archive replay so an impossible caller budget cannot force + # archive-body allocation merely to learn it is impossible. _preflight_declared_resource_bounds_v1(canonical_lock, canonical_limits) - entries = _source_entries_v1(canonical_lock, canonical_admitted) + snapshot = _fresh_admitted_sources_v1(canonical_lock, admitted_sources) + entries = _source_entries_v1(snapshot) contents = build_input.canonical_ustar_v1(entries, canonical_limits) return build_input.seal_input_v1( - _binding_identity_v1(canonical_lock, canonical_admitted, contents), + _binding_identity_v1( + snapshot.source_lock, + snapshot.admitted_sources, + contents, + ), contents, ) diff --git a/proof/region/v1/provenance.py b/proof/region/v1/provenance.py index 42b8c9b0..ce03a3c1 100644 --- a/proof/region/v1/provenance.py +++ b/proof/region/v1/provenance.py @@ -582,6 +582,21 @@ def parse_from(cls, reader: _Reader) -> "SourceReleaseLockV1": integrity, ) + @classmethod + def parse(cls, data: bytes) -> "SourceReleaseLockV1": + """Rebuild one source declaration before it crosses a replay boundary.""" + + reader = _Reader(data, "source-release-lock-v1") + result = cls.parse_from(reader) + reader.finish() + if result.encode() != data: + _fail( + "source-release-lock-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "re-encode drift", + ) + return result + @property def identity(self) -> bytes: return _identity(b"labcolors.proof-region.source-release-lock.v1\0", self.encode()) @@ -709,6 +724,101 @@ def identity(self) -> bytes: return _identity(SOURCE_LOCK_ID_LABEL_V1, self.encode()) +def _rebuild_legal_file_v1(value: object) -> LegalFileV1: + if type(value) is not LegalFileV1: + raise TypeError("legal file must be LegalFileV1") + return LegalFileV1(value.path, value.length, value.sha256) + + +def _rebuild_project_pinned_release_only_file_v1( + value: object, +) -> ProjectPinnedReleaseOnlyFileV1: + if type(value) is not ProjectPinnedReleaseOnlyFileV1: + raise TypeError("release-only file must be ProjectPinnedReleaseOnlyFileV1") + return ProjectPinnedReleaseOnlyFileV1( + value.path, + value.mode, + value.length, + value.sha256, + ) + + +def _rebuild_integrity_policy_v1(value: object) -> SourceIntegrityPolicyV1: + """Copies only primitive policy coordinates; never dispatches caller methods.""" + + if type(value) is DetachedSignaturePolicyV1: + return DetachedSignaturePolicyV1( + value.signature_url, + value.signature_length, + value.signature_sha256, + value.public_key_packets_sha256, + value.signer_fingerprint, + ) + if type(value) is GitContentRelationPolicyV1: + omitted_paths = value.omitted_paths + release_only = value.project_pinned_release_only_files + if type(omitted_paths) is not tuple or type(release_only) is not tuple: + raise TypeError("git policy collections must be exact tuples") + return GitContentRelationPolicyV1( + value.repository_url, + value.tag, + value.commit, + value.tree, + value.common_file_count, + tuple(omitted_paths), + tuple( + _rebuild_project_pinned_release_only_file_v1(item) + for item in release_only + ), + ) + if type(value) is ProjectPinnedArchiveDigestPolicyV1: + return ProjectPinnedArchiveDigestPolicyV1() + raise TypeError("unknown source integrity policy") + + +def _rebuild_source_release_lock_v1(expected: object) -> SourceReleaseLockV1: + """Makes a fresh lock before a boundary can derive an authority identity.""" + + if type(expected) is not SourceReleaseLockV1: + raise TypeError("expected must be SourceReleaseLockV1") + legal_files = expected.legal_files + if type(legal_files) is not tuple: + raise TypeError("legal files must be an exact tuple") + return SourceReleaseLockV1( + expected.role, + expected.version, + expected.archive_url, + expected.archive_format, + expected.archive_length, + expected.archive_sha256, + expected.tar_stream_length, + expected.root_prefix, + expected.regular_file_count, + expected.regular_file_bytes, + tuple(_rebuild_legal_file_v1(item) for item in legal_files), + _rebuild_integrity_policy_v1(expected.integrity), + ) + + +def _rebuild_source_closure_lock_v1( + expected: object, +) -> ArbSourceLockV1 | MpfiSourceLockV1: + if type(expected) not in (ArbSourceLockV1, MpfiSourceLockV1): + raise TypeError("expected must be an exact three-source lock") + sources = expected.sources + if type(sources) is not tuple or len(sources) != SOURCE_CLOSURE_COUNT_V1: + raise TypeError("source closure must be an exact three-source tuple") + first, second, third = sources + rebuilt = ( + _rebuild_source_release_lock_v1(first), + _rebuild_source_release_lock_v1(second), + _rebuild_source_release_lock_v1(third), + ) + if type(expected) is ArbSourceLockV1: + return ArbSourceLockV1(rebuilt) + return MpfiSourceLockV1(rebuilt) + + @dataclass(frozen=True) class ArchiveFileV1: path: str @@ -720,6 +830,8 @@ class ArchiveFileV1: _SAFE_ARCHIVE_TOKEN = object() _ADMITTED_ARB_SOURCES_TOKEN = object() _ADMITTED_MPFI_SOURCES_TOKEN = object() +_REPLAYED_SOURCE_MATERIALIZATION_TOKEN = object() +_REPLAYED_SOURCE_CLOSURE_TOKEN = object() @dataclass(frozen=True, init=False) @@ -768,6 +880,49 @@ def archive_bytes(self) -> bytes: return self._archive_bytes +@dataclass(frozen=True, init=False) +class ReplayedSourceMaterializationV1: + """One private replay snapshot: lock, archive metadata and file bytes move together. + + Public callers may mutate a nominally frozen input after admission. This + value therefore owns a freshly parsed lock and re-admitted archive before + any downstream identity or USTAR layout is derived from it. + """ + + source_lock: SourceReleaseLockV1 + source: SafeSourceArchiveV1 + files: tuple[tuple[str, int, bytes], ...] + + def __init__( + self, + source_lock: SourceReleaseLockV1, + source: SafeSourceArchiveV1, + files: tuple[tuple[str, int, bytes], ...], + *, + _token: object, + ) -> None: + if _token is not _REPLAYED_SOURCE_MATERIALIZATION_TOKEN: + raise TypeError( + "ReplayedSourceMaterializationV1 is created only by source replay" + ) + if ( + type(source_lock) is not SourceReleaseLockV1 + or type(source) is not SafeSourceArchiveV1 + or type(files) is not tuple + or not files + or any( + type(path) is not str + or type(mode) is not int + or type(contents) is not bytes + for path, mode, contents in files + ) + ): + raise TypeError("invalid replayed source materialization") + object.__setattr__(self, "source_lock", source_lock) + object.__setattr__(self, "source", source) + object.__setattr__(self, "files", files) + + def archive_file_manifest_bytes_v1( files_value: tuple[ArchiveFileV1, ...], ) -> bytes: @@ -808,18 +963,29 @@ def archive_file_manifest_bytes_v1( return b"".join(_blob(chunk) for chunk in chunks) -def source_archive_replay_coordinates_v1( - expected: SourceReleaseLockV1, - admitted: SafeSourceArchiveV1, +def _source_archive_coordinates_from_replayed_v1( + source_lock: SourceReleaseLockV1, + replayed: SafeSourceArchiveV1, ) -> tuple[bytes, ...]: - """Recompute the retained source coordinates without reopening a path.""" + """Encode the sole coordinate tuple shared by replay and owned snapshots. + + This is deliberately a leaf: callers establish whether their snapshot is + fresh or retained. Keeping only the wire projection here prevents those + two ownership paths from quietly acquiring different source identities. + """ - replayed, _raw_tar = replay_admitted_source_archive_v1(expected, admitted) + if ( + type(source_lock) is not SourceReleaseLockV1 + or type(replayed) is not SafeSourceArchiveV1 + ): + raise TypeError("invalid replayed source snapshot") archive = replayed.archive_bytes + if type(archive) is not bytes: + raise TypeError("invalid replayed source archive") manifest = archive_file_manifest_bytes_v1(replayed.files) return ( - bytes((int(expected.role),)), - expected.encode(), + bytes((int(source_lock.role),)), + source_lock.encode(), replayed.source_lock_identity, replayed.archive_sha256, replayed.tree_identity, @@ -831,6 +997,32 @@ def source_archive_replay_coordinates_v1( ) +def source_archive_replay_coordinates_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[bytes, ...]: + """Replay coordinates without creating separate extracted file-byte buffers.""" + + source_lock, replayed, _raw_tar = _replay_admitted_source_archive_snapshot_v1( + expected, + admitted, + ) + return _source_archive_coordinates_from_replayed_v1(source_lock, replayed) + + +def _materialized_source_coordinates_v1( + value: ReplayedSourceMaterializationV1, +) -> tuple[bytes, ...]: + """Encode coordinates already owned by one operation without replaying it.""" + + if type(value) is not ReplayedSourceMaterializationV1: + raise TypeError("value must be ReplayedSourceMaterializationV1") + return _source_archive_coordinates_from_replayed_v1( + value.source_lock, + value.source, + ) + + _SafeSourceClosureV1: TypeAlias = tuple[ SafeSourceArchiveV1, SafeSourceArchiveV1, @@ -936,6 +1128,47 @@ def identity(self) -> bytes: ) +@dataclass(frozen=True, init=False) +class ReplayedSourceClosureV1: + """One operation-owned three-source snapshot without caller-held refs.""" + + source_lock: ArbSourceLockV1 | MpfiSourceLockV1 + admitted_sources: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1 + sources: tuple[ + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ] + + def __init__( + self, + source_lock: ArbSourceLockV1 | MpfiSourceLockV1, + admitted_sources: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1, + sources: tuple[ + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ], + *, + _token: object, + ) -> None: + if _token is not _REPLAYED_SOURCE_CLOSURE_TOKEN: + raise TypeError("ReplayedSourceClosureV1 is created only by closure replay") + if ( + type(source_lock) not in (ArbSourceLockV1, MpfiSourceLockV1) + or type(admitted_sources) + not in (AdmittedArbSourcesV1, AdmittedMpfiSourcesV1) + or type(sources) is not tuple + or len(sources) != SOURCE_CLOSURE_COUNT_V1 + or any(type(source) is not ReplayedSourceMaterializationV1 for source in sources) + or tuple(source.source for source in sources) != admitted_sources.sources + ): + raise TypeError("invalid replayed source closure") + object.__setattr__(self, "source_lock", source_lock) + object.__setattr__(self, "admitted_sources", admitted_sources) + object.__setattr__(self, "sources", sources) + + def _decompress_exact( archive: bytes, archive_format: ArchiveFormatV1, @@ -1214,46 +1447,142 @@ def _admit_source_archive_once( def admit_source_archive(expected: SourceReleaseLockV1, archive: bytes) -> SafeSourceArchiveV1: """Hash then scan one locked archive; this establishes no origin trust.""" - admitted, _raw_tar = _admit_source_archive_once(expected, archive) + source_lock = _canonical_source_lock_for_replay_v1(expected) + admitted, _raw_tar = _admit_source_archive_once(source_lock, archive) return admitted -def replay_admitted_source_archive_v1( +def _canonical_source_lock_for_replay_v1( expected: SourceReleaseLockV1, - admitted: SafeSourceArchiveV1, -) -> tuple[SafeSourceArchiveV1, bytes]: - """Re-admit owned bytes and require their retained coordinates to agree. - - The caller cannot supply a second tar stream, so replay coordinates and - materialization bytes remain causally bound without decompressing twice. - """ - +) -> SourceReleaseLockV1: if type(expected) is not SourceReleaseLockV1: raise TypeError("expected must be SourceReleaseLockV1") + try: + return _rebuild_source_release_lock_v1(expected) + except ( + ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained source lock", + ) + + +_RetainedSourceArchiveSnapshotV1: TypeAlias = tuple[ + bytes, + bytes, + bytes, + int, + int, + bytes, + bytes, +] + + +def _retained_source_archive_snapshot_v1( + admitted: SafeSourceArchiveV1, +) -> _RetainedSourceArchiveSnapshotV1: + """Copies only exact primitives before a replay can re-enter caller code.""" + if type(admitted) is not SafeSourceArchiveV1: raise TypeError("admitted must be SafeSourceArchiveV1") try: - replayed, raw_tar = _admit_source_archive_once( - expected, - admitted.archive_bytes, + source_lock_identity = admitted.source_lock_identity + archive_sha256 = admitted.archive_sha256 + tree_identity = admitted.tree_identity + regular_file_count = admitted.regular_file_count + regular_file_bytes = admitted.regular_file_bytes + files = admitted.files + archive = admitted.archive_bytes + _digest( + source_lock_identity, + "source-archive-replay-v1", + "source_lock_identity", + ) + _digest( + archive_sha256, + "source-archive-replay-v1", + "archive_sha256", + ) + _digest(tree_identity, "source-archive-replay-v1", "tree_identity") + _positive( + regular_file_count, + "source-archive-replay-v1", + "regular_file_count", ) - retained_coordinates_match = ( - admitted.source_lock_identity == replayed.source_lock_identity - and admitted.archive_sha256 == replayed.archive_sha256 - and admitted.tree_identity == replayed.tree_identity - and admitted.regular_file_count == replayed.regular_file_count - and admitted.regular_file_bytes == replayed.regular_file_bytes - and admitted.files == replayed.files + _positive( + regular_file_bytes, + "source-archive-replay-v1", + "regular_file_bytes", + ) + if type(archive) is not bytes: + raise TypeError("archive must be exact bytes") + manifest = archive_file_manifest_bytes_v1(files) + except ( + ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained source capability", ) + return ( + source_lock_identity, + archive_sha256, + tree_identity, + regular_file_count, + regular_file_bytes, + manifest, + archive, + ) + + +def _replay_source_archive_from_retained_v1( + source_lock: SourceReleaseLockV1, + retained: _RetainedSourceArchiveSnapshotV1, +) -> tuple[SafeSourceArchiveV1, bytes]: + """Re-admit one copied archive before extracting individual file-byte buffers.""" + + ( + retained_source_lock_identity, + retained_archive_sha256, + retained_tree_identity, + retained_file_count, + retained_file_bytes, + retained_manifest, + archive, + ) = retained + + try: + replayed, raw_tar = _admit_source_archive_once(source_lock, archive) + replayed_manifest = archive_file_manifest_bytes_v1(replayed.files) except ProvenanceErrorV1: raise - except Exception: + except (AttributeError, TypeError, ValueError, OverflowError): _fail( "source-archive-replay-v1", ProvenanceReasonV1.FOREIGN_BINDING, - "invalid retained source capability", + "archive replay failed", ) - if not retained_coordinates_match: + if ( + retained_source_lock_identity != replayed.source_lock_identity + or retained_archive_sha256 != replayed.archive_sha256 + or retained_tree_identity != replayed.tree_identity + or retained_file_count != replayed.regular_file_count + or retained_file_bytes != replayed.regular_file_bytes + or retained_manifest != replayed_manifest + ): _fail( "source-archive-replay-v1", ProvenanceReasonV1.FOREIGN_BINDING, @@ -1262,17 +1591,27 @@ def replay_admitted_source_archive_v1( return replayed, raw_tar -def materialize_admitted_source_files_v1( +def _replay_admitted_source_archive_snapshot_v1( expected: SourceReleaseLockV1, admitted: SafeSourceArchiveV1, -) -> tuple[tuple[str, int, bytes], ...]: - """Return exact relative regular files from one replayed source capability. +) -> tuple[SourceReleaseLockV1, SafeSourceArchiveV1, bytes]: + """Makes the source-owned replay needed by metadata and body consumers.""" + + source_lock = _canonical_source_lock_for_replay_v1(expected) + replayed, raw_tar = _replay_source_archive_from_retained_v1( + source_lock, + _retained_source_archive_snapshot_v1(admitted), + ) + return source_lock, replayed, raw_tar - This shared leaf owns archive replay, not an engine's USTAR namespace or - build recipe. Callers choose their own layout after this function returns. - """ - replayed, raw_tar = replay_admitted_source_archive_v1(expected, admitted) +def _materialize_replayed_source_files_v1( + source_lock: SourceReleaseLockV1, + replayed: SafeSourceArchiveV1, + raw_tar: bytes, +) -> tuple[tuple[str, int, bytes], ...]: + """Reads only one locally replayed archive and its canonical lock snapshot.""" + expected_by_path = {item.path: item for item in replayed.files} values: list[tuple[str, int, bytes]] = [] seen: set[str] = set() @@ -1281,13 +1620,13 @@ def materialize_admitted_source_files_v1( for member in archive: if member.isdir(): continue - if not member.isreg() or not member.name.startswith(expected.root_prefix): + if not member.isreg() or not member.name.startswith(source_lock.root_prefix): _fail( "source-archive-materialization-v1", ProvenanceReasonV1.FOREIGN_BINDING, "unexpected archive member", ) - relative = member.name[len(expected.root_prefix) :] + relative = member.name[len(source_lock.root_prefix) :] coordinate = expected_by_path.get(relative) if ( coordinate is None @@ -1351,69 +1690,363 @@ def materialize_admitted_source_files_v1( return tuple(sorted(values)) -def _validate_source_capability_closure_v1( +def replay_materialize_admitted_source_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> ReplayedSourceMaterializationV1: + """Builds one owned replay snapshot for all source-derived consumers. + + The snapshot is deliberately below engine and recipe layers. Its lock, + archive identity and materialized bytes originate from the same fresh + replay, so a caller-held capability cannot relabel already-read bytes. + """ + + source_lock, replayed, raw_tar = _replay_admitted_source_archive_snapshot_v1( + expected, + admitted, + ) + files = _materialize_replayed_source_files_v1(source_lock, replayed, raw_tar) + return ReplayedSourceMaterializationV1( + source_lock, + replayed, + files, + _token=_REPLAYED_SOURCE_MATERIALIZATION_TOKEN, + ) + + +def replay_admitted_source_archive_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[SafeSourceArchiveV1, bytes]: + """Return a bounded-decompressed replay without extracted file-byte buffers.""" + + _source_lock, replayed, raw_tar = _replay_admitted_source_archive_snapshot_v1( + expected, + admitted, + ) + return replayed, raw_tar + + +def materialize_admitted_source_files_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[tuple[str, int, bytes], ...]: + """Return exact relative files from one owned replay snapshot.""" + + return replay_materialize_admitted_source_v1(expected, admitted).files + + +def _source_closure_lock_snapshot_v1( + expected: ArbSourceLockV1 | MpfiSourceLockV1, +) -> ArbSourceLockV1 | MpfiSourceLockV1: + try: + return _rebuild_source_closure_lock_v1(expected) + except ( + ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-closure-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained source closure lock", + ) + + +def snapshot_source_closure_lock_v1( + expected: object, +) -> ArbSourceLockV1 | MpfiSourceLockV1: + """Return a detached structural lock snapshot before a public replay.""" + + return _source_closure_lock_snapshot_v1(expected) + + +def _source_capability_matches_lock_v1( + lock: SourceReleaseLockV1, + source: SafeSourceArchiveV1, +) -> _RetainedSourceArchiveSnapshotV1: + retained = _retained_source_archive_snapshot_v1(source) + ( + retained_lock_identity, + retained_archive_sha256, + _retained_tree_identity, + retained_file_count, + retained_file_bytes, + _retained_manifest, + _archive, + ) = retained + if ( + retained_lock_identity != lock.identity + or retained_archive_sha256 != lock.archive_sha256 + or retained_file_count != lock.regular_file_count + or retained_file_bytes != lock.regular_file_bytes + ): + _fail( + "source-closure-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "source capability does not match ordered lock", + ) + return retained + + +def snapshot_admitted_source_closure_v1( + expected: object, + admitted: object, +) -> AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1: + """Return a detached source-closure declaration without extracted file buffers. + + The archive is re-admitted so the retained manifest, tree and compressed + bytes agree. Re-admission bounded-decompresses and scans the tar, but + unlike an operation replay it does not retain separate file-byte buffers; + consumers that need those buffers must still call + ``replay_admitted_source_closure_v1``. + """ + + canonical_lock = _source_closure_lock_snapshot_v1(expected) + if ( + ( + type(canonical_lock) is ArbSourceLockV1 + and type(admitted) is not AdmittedArbSourcesV1 + ) + or ( + type(canonical_lock) is MpfiSourceLockV1 + and type(admitted) is not AdmittedMpfiSourcesV1 + ) + ): + raise TypeError("admitted sources do not match the source lock kind") + try: + retained_lock_identity = admitted.source_lock_identity + retained_sources = admitted.sources + _digest( + retained_lock_identity, + "source-closure-snapshot-v1", + "source_lock_identity", + ) + if retained_lock_identity != canonical_lock.identity: + _fail( + "source-closure-snapshot-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "admitted closure lock identity changed", + ) + sources = _validate_source_replay_arguments_v1( + canonical_lock.sources, + retained_sources, + ) + snapshots = _replay_source_archives_v1( + canonical_lock.sources, + sources, + ) + except ProvenanceErrorV1: + raise + except ( + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-closure-snapshot-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained admitted closure", + ) + return _fresh_admitted_source_closure_v1(canonical_lock, snapshots) + + +def _validate_source_replay_arguments_v1( expected_sources: _SourceClosureV1, sources: _SafeSourceClosureV1, - artifact: str, -) -> None: +) -> tuple[SafeSourceArchiveV1, SafeSourceArchiveV1, SafeSourceArchiveV1]: + if ( + type(expected_sources) is not tuple + or len(expected_sources) != SOURCE_CLOSURE_COUNT_V1 + or any(type(lock) is not SourceReleaseLockV1 for lock in expected_sources) + ): + raise TypeError("expected sources must be three SourceReleaseLockV1 values") if ( type(sources) is not tuple or len(sources) != SOURCE_CLOSURE_COUNT_V1 or any(type(source) is not SafeSourceArchiveV1 for source in sources) ): raise TypeError("sources must be three SafeSourceArchiveV1 values") - for lock, source in zip(expected_sources, sources, strict=True): - if ( - source.source_lock_identity != lock.identity - or source.archive_sha256 != lock.archive_sha256 - or source.regular_file_count != lock.regular_file_count - or source.regular_file_bytes != lock.regular_file_bytes - ): + first, second, third = sources + return first, second, third + + +def _replay_source_archives_v1( + expected_sources: _SourceClosureV1, + sources: _SafeSourceClosureV1, +) -> _SafeSourceClosureV1: + """Re-admit a closure for metadata without retaining file-byte buffers.""" + + admitted_sources = _validate_source_replay_arguments_v1(expected_sources, sources) + replayed: list[SafeSourceArchiveV1] = [] + for lock, source in zip(expected_sources, admitted_sources, strict=True): + retained = _source_capability_matches_lock_v1(lock, source) + fresh, _raw_tar = _replay_source_archive_from_retained_v1(lock, retained) + replayed.append(fresh) + first, second, third = replayed + return first, second, third + + +def _replay_source_materializations_v1( + expected_sources: _SourceClosureV1, + sources: _SafeSourceClosureV1, +) -> tuple[ + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, +]: + """Materialize only the operation path that actually needs source bytes.""" + + admitted_sources = _validate_source_replay_arguments_v1(expected_sources, sources) + materializations: list[ReplayedSourceMaterializationV1] = [] + for lock, source in zip(expected_sources, admitted_sources, strict=True): + retained = _source_capability_matches_lock_v1(lock, source) + replayed, raw_tar = _replay_source_archive_from_retained_v1(lock, retained) + materializations.append( + ReplayedSourceMaterializationV1( + lock, + replayed, + _materialize_replayed_source_files_v1(lock, replayed, raw_tar), + _token=_REPLAYED_SOURCE_MATERIALIZATION_TOKEN, + ) + ) + first, second, third = materializations + return first, second, third + + +def _fresh_admitted_source_closure_v1( + source_lock: ArbSourceLockV1 | MpfiSourceLockV1, + sources: _SafeSourceClosureV1, +) -> AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1: + if type(source_lock) is ArbSourceLockV1: + return AdmittedArbSourcesV1( + source_lock.identity, + sources, + _token=_ADMITTED_ARB_SOURCES_TOKEN, + ) + if type(source_lock) is MpfiSourceLockV1: + return AdmittedMpfiSourcesV1( + source_lock.identity, + sources, + _token=_ADMITTED_MPFI_SOURCES_TOKEN, + ) + raise TypeError("source lock is not a supported closure") + + +def _admitted_closure_snapshot_v1( + source_lock: ArbSourceLockV1 | MpfiSourceLockV1, + admitted: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1, +) -> ReplayedSourceClosureV1: + canonical_lock = _source_closure_lock_snapshot_v1(source_lock) + if ( + ( + type(canonical_lock) is ArbSourceLockV1 + and type(admitted) is not AdmittedArbSourcesV1 + ) + or ( + type(canonical_lock) is MpfiSourceLockV1 + and type(admitted) is not AdmittedMpfiSourcesV1 + ) + ): + raise TypeError("admitted sources do not match the source lock kind") + try: + retained_lock_identity = admitted.source_lock_identity + retained_sources = admitted.sources + _digest( + retained_lock_identity, + "source-closure-replay-v1", + "source_lock_identity", + ) + if retained_lock_identity != canonical_lock.identity: _fail( - artifact, + "source-closure-replay-v1", ProvenanceReasonV1.FOREIGN_BINDING, - "source capability does not match ordered lock", + "admitted closure lock identity changed", ) + _validate_source_replay_arguments_v1( + canonical_lock.sources, + retained_sources, + ) + except ProvenanceErrorV1: + raise + except ( + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-closure-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained admitted closure", + ) + snapshots = _replay_source_materializations_v1( + canonical_lock.sources, + retained_sources, + ) + fresh = _fresh_admitted_source_closure_v1( + canonical_lock, + tuple(snapshot.source for snapshot in snapshots), + ) + return ReplayedSourceClosureV1( + canonical_lock, + fresh, + snapshots, + _token=_REPLAYED_SOURCE_CLOSURE_TOKEN, + ) def admit_arb_sources( expected: ArbSourceLockV1, sources: _SafeSourceClosureV1, ) -> AdmittedArbSourcesV1: - """Collapse three individually admitted archives into one ordered capability.""" + """Replay and own three archives before minting one Arb closure capability.""" if type(expected) is not ArbSourceLockV1: raise TypeError("expected must be ArbSourceLockV1") - _validate_source_capability_closure_v1( - expected.sources, - sources, - "admitted-arb-sources-v1", - ) - return AdmittedArbSourcesV1( - expected.identity, + canonical_lock = _source_closure_lock_snapshot_v1(expected) + replayed_sources = _replay_source_archives_v1( + canonical_lock.sources, sources, - _token=_ADMITTED_ARB_SOURCES_TOKEN, ) + fresh = _fresh_admitted_source_closure_v1(canonical_lock, replayed_sources) + if type(fresh) is not AdmittedArbSourcesV1: + raise AssertionError("Arb closure kind changed during admission") + return fresh def admit_mpfi_sources( expected: MpfiSourceLockV1, sources: _SafeSourceClosureV1, ) -> AdmittedMpfiSourcesV1: - """Collapse the exact MPFI source closure into one ordered capability.""" + """Replay and own three archives before minting one MPFI closure capability.""" if type(expected) is not MpfiSourceLockV1: raise TypeError("expected must be MpfiSourceLockV1") - _validate_source_capability_closure_v1( - expected.sources, - sources, - "admitted-mpfi-sources-v1", - ) - return AdmittedMpfiSourcesV1( - expected.identity, + canonical_lock = _source_closure_lock_snapshot_v1(expected) + replayed_sources = _replay_source_archives_v1( + canonical_lock.sources, sources, - _token=_ADMITTED_MPFI_SOURCES_TOKEN, ) + fresh = _fresh_admitted_source_closure_v1(canonical_lock, replayed_sources) + if type(fresh) is not AdmittedMpfiSourcesV1: + raise AssertionError("MPFI closure kind changed during admission") + return fresh + + +def replay_admitted_source_closure_v1( + expected: ArbSourceLockV1 | MpfiSourceLockV1, + admitted: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1, +) -> ReplayedSourceClosureV1: + """Take one local source-closure snapshot for a build-like operation.""" + + return _admitted_closure_snapshot_v1(expected, admitted) def _legal_file(path: str, length: int, digest_hex: str) -> LegalFileV1: diff --git a/proof/region/v1/region_proof_protocol.py b/proof/region/v1/region_proof_protocol.py index 48db70fe..3778d6a3 100644 --- a/proof/region/v1/region_proof_protocol.py +++ b/proof/region/v1/region_proof_protocol.py @@ -713,6 +713,71 @@ def identity(self) -> bytes: return _identity(JOB_ID_LABEL_V1, self.encode()) +def _snapshot_contextual_region_definition_v1( + value: object, +) -> ContextualRegionDefinitionV1: + if type(value) is not ContextualRegionDefinitionV1: + raise TypeError("definition must be ContextualRegionDefinitionV1") + fields_value = value.fields + if type(fields_value) is not tuple: + raise TypeError("definition fields must be an exact tuple") + return ContextualRegionDefinitionV1(tuple(fields_value), value.knot_count) + + +def _snapshot_reduced_domain_manifest_v1( + value: object, +) -> ReducedDomainManifestV1: + if type(value) is not ReducedDomainManifestV1: + raise TypeError("domain must be ReducedDomainManifestV1") + ranges = value.ranges + if type(ranges) is not tuple: + raise TypeError("domain ranges must be an exact tuple") + return ReducedDomainManifestV1(tuple(ranges), value.point_count) + + +def _snapshot_comparator_budget_v1(value: object) -> ComparatorBudgetV1: + if type(value) is not ComparatorBudgetV1: + raise TypeError("comparator budget must be ComparatorBudgetV1") + ladder = value.precision_ladder + if type(ladder) is not tuple: + raise TypeError("precision ladder must be an exact tuple") + return ComparatorBudgetV1( + value.kind, + tuple(ladder), + value.per_point_work, + value.global_pregrant, + ) + + +def _snapshot_proof_policy_v1(value: object) -> ProofPolicyV1: + if type(value) is not ProofPolicyV1: + raise TypeError("policy must be ProofPolicyV1") + comparators = value.comparators + if type(comparators) is not tuple or len(comparators) != 2: + raise TypeError("policy comparators must be an exact pair") + arb, mpfi = comparators + return ProofPolicyV1( + value.equality_release, + ( + _snapshot_comparator_budget_v1(arb), + _snapshot_comparator_budget_v1(mpfi), + ), + ) + + +def snapshot_proof_job_v1(value: object) -> ProofJobV1: + """Return a detached job from raw coordinates, never caller-dispatched wire methods.""" + + if type(value) is not ProofJobV1: + raise TypeError("job must be ProofJobV1") + return ProofJobV1( + _snapshot_contextual_region_definition_v1(value.definition), + value.formula_spec, + _snapshot_reduced_domain_manifest_v1(value.domain), + _snapshot_proof_policy_v1(value.policy), + ) + + @dataclass(frozen=True) class ComparatorManifestV2: kind: ComparatorKindV1 diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index abe02d5c..c2011e1a 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -34,16 +34,16 @@ from test_receipt import _execute # noqa: E402 -# These literals are an independent outer oracle for the Arb gate: importing -# its expected hash here would let a coordinated gate edit hide inventory drift. -# A deliberate test-set change updates both values from fresh enumeration. +# Эти литералы — независимый внешний оракул proof fast gate. Импорт его hash +# позволил бы согласованной правке gate скрыть drift; осознанная смена набора +# тестов обновляет оба значения только по свежему перечислению. ARB_INVENTORY_SHA256_V1 = ( - "c74942a9240b68327921160f86fd948532849234bb6da00a0075a137fef098cc" + "6a616daac6d2437b372d93e8e5fe03787557e7a75aea25475ca9d349568669aa" ) ARB_ORDER_SHA256_V1 = ( - "bbf8711108939c4658e0b17bc037c5b592499fdf73c67121c492e5edea4635e9" + "160269a28292a0d60b2a04189fa20add09a2e5fad958592ad5ef3c33aad51f48" ) -ARB_TEST_COUNT_V1 = 198 +ARB_TEST_COUNT_V1 = 231 MOVED_INPUT_SURFACE_V1 = ( "CanonicalInputLimitsV1", diff --git a/proof/region/v1/tests/test_mpfi_input.py b/proof/region/v1/tests/test_mpfi_input.py index 8a7a4cdc..12ec3325 100644 --- a/proof/region/v1/tests/test_mpfi_input.py +++ b/proof/region/v1/tests/test_mpfi_input.py @@ -10,6 +10,7 @@ import sys import tarfile import unittest +from dataclasses import replace from pathlib import Path from unittest import mock @@ -383,11 +384,11 @@ def test_limits_reject_declared_closure_before_archive_materialization(self) -> with self.subTest(limit=field): with mock.patch.object( mpfi_input.provenance, - "materialize_admitted_source_files_v1", - ) as materialize: + "replay_admitted_source_closure_v1", + ) as replay_closure: with self.assertRaises(build_input.InputErrorV1) as caught: mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) - materialize.assert_not_called() + replay_closure.assert_not_called() self.assertEqual(caught.exception.reason, build_input.InputReasonV1.RESOURCE_LIMIT) self.assertEqual(caught.exception.field, field) @@ -447,6 +448,7 @@ def test_missing_capability_field_is_a_typed_rejection(self) -> None: caught.exception.reason, mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, ) + self.assertEqual(caught.exception.field, "admitted_sources") class ExplodesOnComparison: def __ne__(self, _other: object) -> bool: @@ -462,6 +464,116 @@ def __ne__(self, _other: object) -> bool: caught.exception.reason, mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_hostile_exact_source_capability_stays_a_typed_replay_failure( + self, + ) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + source = admitted.sources[0] + original = source.source_lock_identity + + class ExplodesOnComparison: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("comparison ran") + + object.__setattr__(source, "source_lock_identity", ExplodesOnComparison()) + try: + with self.assertRaises(provenance.ProvenanceErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(source, "source_lock_identity", original) + self.assertEqual( + caught.exception.reason, + provenance.ProvenanceReasonV1.FOREIGN_BINDING, + ) + + def test_source_input_keeps_one_snapshot_across_reentrant_source_mutation( + self, + ) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + source = admitted.sources[0] + original_tree_identity = source.tree_identity + real_encoder = build_input.canonical_ustar_v1 + + def encode_then_mutate( + entries: tuple[tuple[str, int, bytes], ...], + encoder_limits: build_input.CanonicalInputLimitsV1, + ) -> bytes: + encoded = real_encoder(entries, encoder_limits) + object.__setattr__(source, "tree_identity", _sha256(b"reentrant-tree")) + return encoded + + try: + with mock.patch.object( + mpfi_input.build_input, + "canonical_ustar_v1", + side_effect=encode_then_mutate, + ): + sealed = mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + limits, + ) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + + def test_hostile_replayed_source_stays_a_typed_provenance_failure(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + source = admitted.sources[0] + original = source.files + + class ExplodesOnComparison: + def __eq__(self, _other: object) -> bool: + raise RuntimeError("comparison ran") + + object.__setattr__(source, "files", ExplodesOnComparison()) + try: + with self.assertRaises(provenance.ProvenanceErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(source, "files", original) + self.assertEqual( + caught.exception.reason, + provenance.ProvenanceReasonV1.FOREIGN_BINDING, + ) def test_noncanonical_exact_type_lock_is_a_typed_rejection(self) -> None: lock, admitted, expected_entries = _admitted_closure() @@ -481,6 +593,81 @@ def test_noncanonical_exact_type_lock_is_a_typed_rejection(self) -> None: mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, ) + def test_hostile_exact_lock_cannot_escape_public_boundary(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + original_sources = lock.sources + + class ExplodesOnEncode: + def encode(self) -> bytes: + raise RuntimeError("encode ran") + + object.__setattr__(lock, "sources", (ExplodesOnEncode(),) * 3) + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(lock, "sources", original_sources) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "source_lock") + + def test_source_lock_encoder_shadow_cannot_select_foreign_closure(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + foreign_releases = list(lock.sources) + foreign_releases[0] = replace(foreign_releases[0], version="shadowed") + foreign_lock = provenance.MpfiSourceLockV1(tuple(foreign_releases)) + foreign_sources = tuple( + provenance.admit_source_archive(release, source.archive_bytes) + for release, source in zip( + foreign_lock.sources, + admitted.sources, + strict=True, + ) + ) + foreign_admitted = provenance.admit_mpfi_sources( + foreign_lock, + foreign_sources, + ) + lock.__dict__["encode"] = lambda: provenance.MpfiSourceLockV1.encode( + foreign_lock + ) + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + lock, + foreign_admitted, + limits, + ) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + del lock.__dict__["encode"] + def test_source_input_owner_has_no_engine_dependency(self) -> None: source_path = ROOT / "mpfi" / "input.py" self.assertTrue(source_path.is_file()) diff --git a/proof/region/v1/tests/test_mpfi_source_lock.py b/proof/region/v1/tests/test_mpfi_source_lock.py index 017944c6..a1ec7fc5 100644 --- a/proof/region/v1/tests/test_mpfi_source_lock.py +++ b/proof/region/v1/tests/test_mpfi_source_lock.py @@ -244,7 +244,8 @@ def test_reference_does_not_upgrade_the_mpfi_digest_to_publisher_evidence(self) reference = (ROOT / "PROTOCOL.md").read_text(encoding="utf-8") self.assertIn("ProjectPinnedArchiveDigestPolicyV1", reference) - self.assertIn("materialize_admitted_source_files_v1", reference) + self.assertIn("replay_materialize_admitted_source_v1", reference) + self.assertIn("bounded-decompresses", reference) self.assertIn("не приписывает этот digest издателю", reference) self.assertIn("не заявляет publisher authentication", reference) diff --git a/proof/region/v1/tests/test_source_lock.py b/proof/region/v1/tests/test_source_lock.py index 649ee7db..e7fffe78 100644 --- a/proof/region/v1/tests/test_source_lock.py +++ b/proof/region/v1/tests/test_source_lock.py @@ -72,6 +72,29 @@ def admitted_closure_identity( return canonical_identity(label, b"".join(chunks)) +def _replay_source( + lock: SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, +) -> provenance.ReplayedSourceMaterializationV1: + """Exercise the one source materialization contract from a fresh replay.""" + + return provenance.replay_materialize_admitted_source_v1(lock, admitted) + + +def _source_files( + lock: SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, +) -> tuple[tuple[str, int, bytes], ...]: + return _replay_source(lock, admitted).files + + +def _source_coordinates( + lock: SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, +) -> tuple[bytes, ...]: + return provenance.source_archive_replay_coordinates_v1(lock, admitted) + + def tar_gz( entries: tuple[tuple[str, bytes | None, bytes | None], ...], ) -> bytes: @@ -457,6 +480,79 @@ def test_three_locked_sources_become_one_ordered_capability(self) -> None: _token=object(), ) + def test_aggregate_admission_replays_each_source_before_it_owns_the_closure( + self, + ) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + arb_third = replace( + gmp, + role=SourceRoleV1.FLINT_ARB, + integrity=GitContentRelationPolicyV1( + "https://example.invalid/fixture.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + ("missing",), + ( + ProjectPinnedReleaseOnlyFileV1( + "value", + 0o644, + 4, + sha256(b"data"), + ), + ), + ), + ) + mpfi_third = replace( + gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + + class CounterfeitDigest(bytes): + def __eq__(self, _other: object) -> bool: + return True + + def __ne__(self, _other: object) -> bool: + return False + + cases = ( + ( + provenance.ArbSourceLockV1((gmp, mpfr, arb_third)), + admit_arb_sources, + ), + ( + MpfiSourceLockV1((gmp, mpfr, mpfi_third)), + admit_mpfi_sources, + ), + ) + for lock, admit in cases: + with self.subTest(lock_type=type(lock).__name__): + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + original = sources[0].archive_sha256 + object.__setattr__( + sources[0], + "archive_sha256", + CounterfeitDigest(b"\x92" * 32), + ) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + admit(lock, sources) + finally: + object.__setattr__(sources[0], "archive_sha256", original) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + + fresh = admit(lock, sources) + self.assertIsNot(fresh.sources[0], sources[0]) + def test_archive_is_hash_checked_then_scanned_without_extracting(self) -> None: lock = fixture_lock(GOOD_ARCHIVE) admitted = admit_source_archive(lock, GOOD_ARCHIVE) @@ -517,11 +613,8 @@ def test_derived_identities_ignore_injected_instance_caches(self) -> None: self.assertEqual(release.identity, release_identity) admitted_release = admit_source_archive(release, GOOD_ARCHIVE) self.assertEqual(admitted_release.source_lock_identity, release_identity) - replayed_release, _ = provenance.replay_admitted_source_archive_v1( - release, - admitted_release, - ) - self.assertEqual(replayed_release.source_lock_identity, release_identity) + replay = _replay_source(release, admitted_release) + self.assertEqual(replay.source.source_lock_identity, release_identity) self.assertEqual( provenance.source_archive_replay_coordinates_v1( release, @@ -530,10 +623,7 @@ def test_derived_identities_ignore_injected_instance_caches(self) -> None: release_identity, ) self.assertEqual( - provenance.materialize_admitted_source_files_v1( - release, - admitted_release, - ), + replay.files, (("LICENSE", 0o644, b"license"), ("value", 0o644, b"data")), ) @@ -608,12 +698,195 @@ def test_derived_identities_ignore_injected_instance_caches(self) -> None: mpfi_admitted.__dict__["identity"] = poison self.assertEqual(mpfi_admitted.identity, mpfi_admitted_identity) + def test_operation_owned_source_coordinates_have_no_public_projection(self) -> None: + self.assertFalse(hasattr(provenance, "materialized_source_coordinates_v1")) + + def test_replay_rejects_an_instance_encode_shadow(self) -> None: + """A frozen dataclass can still shadow a method through ``__dict__``.""" + + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + foreign_lock = replace(lock, version="2") + foreign_admitted = admit_source_archive(foreign_lock, GOOD_ARCHIVE) + lock.__dict__["encode"] = lambda: SourceReleaseLockV1.encode(foreign_lock) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.replay_materialize_admitted_source_v1( + lock, + foreign_admitted, + ) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + replay = provenance.replay_materialize_admitted_source_v1(lock, admitted) + self.assertEqual(replay.source_lock.version, "1") + finally: + del lock.__dict__["encode"] + + def test_replay_rejects_a_nested_encoder_shadow(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + foreign_legal_file = LegalFileV1("value", 4, sha256(b"data")) + foreign_lock = replace(lock, legal_files=(foreign_legal_file,)) + foreign_admitted = admit_source_archive(foreign_lock, GOOD_ARCHIVE) + legal_file = lock.legal_files[0] + legal_file.__dict__["encode"] = lambda: LegalFileV1.encode(foreign_legal_file) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.replay_materialize_admitted_source_v1( + lock, + foreign_admitted, + ) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + replay = provenance.replay_materialize_admitted_source_v1(lock, admitted) + self.assertEqual(replay.source_lock.legal_files[0].path, "LICENSE") + finally: + del legal_file.__dict__["encode"] + + def test_metadata_replays_do_not_materialize_file_bodies(self) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + mpfi = replace( + gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + + with mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + ) as materialize: + provenance.source_archive_replay_coordinates_v1(gmp, sources[0]) + admitted = admit_mpfi_sources(lock, sources) + + materialize.assert_not_called() + self.assertIs(type(admitted), AdmittedMpfiSourcesV1) + + def test_closure_snapshot_replays_metadata_without_materializing_bodies( + self, + ) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + flint = replace( + gmp, + role=SourceRoleV1.FLINT_ARB, + integrity=GitContentRelationPolicyV1( + "https://example.invalid/fixture.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + ("missing",), + ( + ProjectPinnedReleaseOnlyFileV1( + "value", + 0o644, + 4, + sha256(b"data"), + ), + ), + ), + ) + lock = provenance.ArbSourceLockV1((gmp, mpfr, flint)) + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + admitted = admit_arb_sources(lock, sources) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + snapshot = provenance.snapshot_admitted_source_closure_v1(lock, admitted) + + self.assertIs(type(snapshot), AdmittedArbSourcesV1) + self.assertEqual(snapshot.identity, admitted.identity) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 0) + self.assertTrue( + all( + snapshot_source is not retained_source + for snapshot_source, retained_source in zip( + snapshot.sources, + admitted.sources, + strict=True, + ) + ) + ) + + flint_source = admitted.sources[2] + original_files = flint_source.files + for replacement in ( + list(original_files), + (replace(original_files[0], path="LICENSE-FORGED"), *original_files[1:]), + ): + with self.subTest(retained_manifest=type(replacement).__name__): + object.__setattr__(flint_source, "files", replacement) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.snapshot_admitted_source_closure_v1(lock, admitted) + finally: + object.__setattr__(flint_source, "files", original_files) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + original_archive = flint_source.archive_bytes + corrupted_archive = bytes((original_archive[0] ^ 1,)) + original_archive[1:] + object.__setattr__(flint_source, "_archive_bytes", corrupted_archive) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.snapshot_admitted_source_closure_v1(lock, admitted) + finally: + object.__setattr__(flint_source, "_archive_bytes", original_archive) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.ARCHIVE_DIGEST_MISMATCH, + ) + + def test_public_closure_replay_totalizes_a_mutated_source_tuple(self) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + mpfi = replace( + gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + admitted = admit_mpfi_sources(lock, sources) + original = admitted.sources + object.__setattr__(admitted, "sources", object()) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.replay_admitted_source_closure_v1(lock, admitted) + finally: + object.__setattr__(admitted, "sources", original) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + def test_shared_materializer_replays_only_the_exact_admitted_archive(self) -> None: lock = fixture_lock(GOOD_ARCHIVE) admitted = admit_source_archive(lock, GOOD_ARCHIVE) self.assertEqual( - provenance.materialize_admitted_source_files_v1(lock, admitted), + _source_files(lock, admitted), ( ("LICENSE", 0o644, b"license"), ("value", 0o644, b"data"), @@ -634,7 +907,7 @@ def test_shared_materializer_replays_only_the_exact_admitted_archive(self) -> No object.__setattr__(admitted, field_name, replacement) try: with self.assertRaises(ProvenanceErrorV1) as caught: - provenance.materialize_admitted_source_files_v1(lock, admitted) + _source_files(lock, admitted) finally: object.__setattr__(admitted, field_name, original) self.assertEqual( @@ -646,7 +919,7 @@ def test_shared_materializer_replays_only_the_exact_admitted_archive(self) -> No object.__setattr__(admitted, "_archive_bytes", GOOD_ARCHIVE[:-1]) try: with self.assertRaises(ProvenanceErrorV1) as caught: - provenance.materialize_admitted_source_files_v1(lock, admitted) + _source_files(lock, admitted) finally: object.__setattr__(admitted, "_archive_bytes", original_archive_bytes) self.assertEqual( @@ -658,7 +931,7 @@ def test_shared_materializer_replays_only_the_exact_admitted_archive(self) -> No object.__setattr__(lock, "role", 999) try: with self.assertRaises(ProvenanceErrorV1) as caught: - provenance.materialize_admitted_source_files_v1(lock, admitted) + _source_files(lock, admitted) finally: object.__setattr__(lock, "role", original_role) self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) @@ -666,10 +939,7 @@ def test_shared_materializer_replays_only_the_exact_admitted_archive(self) -> No for hostile_lock, hostile_admitted in ((object(), admitted), (lock, object())): with self.subTest(hostile=type(hostile_lock).__name__): with self.assertRaises(TypeError): - provenance.materialize_admitted_source_files_v1( - hostile_lock, - hostile_admitted, - ) + _source_files(hostile_lock, hostile_admitted) def test_replay_boundary_totalizes_hostile_nominal_coordinates(self) -> None: lock = fixture_lock(GOOD_ARCHIVE) @@ -683,27 +953,7 @@ def __ne__(self, _other: object) -> bool: object.__setattr__(lock, "archive_length", ExplodingCoordinate()) try: for name, operation in ( - ( - "replay", - lambda: provenance.replay_admitted_source_archive_v1( - lock, - admitted, - ), - ), - ( - "materialize", - lambda: provenance.materialize_admitted_source_files_v1( - lock, - admitted, - ), - ), - ( - "coordinates", - lambda: provenance.source_archive_replay_coordinates_v1( - lock, - admitted, - ), - ), + ("atomic-source-snapshot", lambda: _replay_source(lock, admitted)), ): with self.subTest(operation=name): with self.assertRaises(ProvenanceErrorV1) as caught: @@ -716,15 +966,104 @@ def __ne__(self, _other: object) -> bool: object.__setattr__(lock, "archive_length", original_length) class InterruptedCoordinate: - def __ne__(self, _other: object) -> bool: + def to_bytes(self, _length: int, _order: str) -> bytes: raise KeyboardInterrupt("source lock interruption") object.__setattr__(lock, "archive_length", InterruptedCoordinate()) try: - with self.assertRaises(KeyboardInterrupt): - provenance.replay_admitted_source_archive_v1(lock, admitted) + with self.assertRaises(ProvenanceErrorV1) as caught: + _replay_source(lock, admitted) finally: object.__setattr__(lock, "archive_length", original_length) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + def test_replay_rejects_counterfeit_retained_coordinates_before_equality( + self, + ) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + + class CounterfeitDigest(bytes): + def __eq__(self, _other: object) -> bool: + return True + + def __ne__(self, _other: object) -> bool: + return False + + originals = { + field_name: getattr(admitted, field_name) + for field_name in ( + "source_lock_identity", + "archive_sha256", + "tree_identity", + ) + } + for field_name, original in originals.items(): + with self.subTest(retained_coordinate=field_name): + object.__setattr__( + admitted, + field_name, + CounterfeitDigest(b"\xa5" * 32), + ) + try: + for operation in (lambda: _replay_source(lock, admitted),): + with self.assertRaises(ProvenanceErrorV1) as caught: + operation() + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + finally: + object.__setattr__(admitted, field_name, original) + + original_file = admitted.files[0] + original_digest = original_file.sha256 + object.__setattr__( + original_file, + "sha256", + CounterfeitDigest(b"\x91" * 32), + ) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + _replay_source(lock, admitted) + finally: + object.__setattr__(original_file, "sha256", original_digest) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + def test_replay_coordinates_keep_one_lock_snapshot_across_reentrancy(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + original_root_prefix = lock.root_prefix + real_admit = provenance._admit_source_archive_once + mutated = False + + def admit_then_mutate( + expected: SourceReleaseLockV1, + archive: bytes, + ) -> tuple[provenance.SafeSourceArchiveV1, bytes]: + nonlocal mutated + replayed = real_admit(expected, archive) + object.__setattr__(lock, "root_prefix", "other/") + mutated = True + return replayed + + try: + with mock.patch.object( + provenance, + "_admit_source_archive_once", + side_effect=admit_then_mutate, + ): + coordinates = _source_coordinates(lock, admitted) + finally: + object.__setattr__(lock, "root_prefix", original_root_prefix) + self.assertTrue(mutated) + self.assertEqual( + canonical_identity( + b"labcolors.proof-region.source-release-lock.v1\0", + coordinates[1], + ), + coordinates[2], + ) def test_shared_materializer_is_invariant_under_regular_member_permutation(self) -> None: expected = ( @@ -798,7 +1137,7 @@ def test_shared_materializer_rechecks_the_replayed_tar_before_returning_files(se with self.subTest(mutation=name): with mock.patch.object( provenance, - "replay_admitted_source_archive_v1", + "_admit_source_archive_once", return_value=(replayed, raw_tar), ): with self.assertRaises(ProvenanceErrorV1) as caught: From bd7ddbaafa3698571f3843a4e9757b31e31f5d69 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 20:26:32 +0300 Subject: [PATCH 6/9] =?UTF-8?q?Proof:=20=D1=83=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20stale=20test=20scaffolding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/arb/tests/gate.py | 2 +- proof/region/v1/arb/tests/test_receipt.py | 11 ++++++----- proof/region/v1/mpfi/input.py | 20 ++++++++++---------- proof/region/v1/provenance.py | 2 +- proof/region/v1/tests/test_build.py | 4 ++-- proof/region/v1/tests/test_mpfi_input.py | 6 +----- 6 files changed, 21 insertions(+), 24 deletions(-) diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py index 4ed23d96..94e9c8d7 100644 --- a/proof/region/v1/arb/tests/gate.py +++ b/proof/region/v1/arb/tests/gate.py @@ -19,7 +19,7 @@ "test_mpfi_input.py", ) EXPECTED_TEST_INVENTORY_SHA256 = ( - "6a616daac6d2437b372d93e8e5fe03787557e7a75aea25475ca9d349568669aa" + "8adbe7c5ed352e7f100d943a98bbbeec1bace8a60080933364cfdc737ebbe644" ) _EVALUATOR_REASON = "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary" EXPECTED_SKIPS = frozenset( diff --git a/proof/region/v1/arb/tests/test_receipt.py b/proof/region/v1/arb/tests/test_receipt.py index 6733e8b8..3e6967fc 100644 --- a/proof/region/v1/arb/tests/test_receipt.py +++ b/proof/region/v1/arb/tests/test_receipt.py @@ -12,7 +12,7 @@ import tempfile import time import unittest -from dataclasses import replace +from dataclasses import fields, replace from pathlib import Path from unittest import mock @@ -424,12 +424,12 @@ def test_public_verifier_rejects_a_poisoned_cached_identity_before_replay( ("transcript", lambda evidence: evidence.transcript, "identity"), ("run claim", lambda evidence: evidence.run_claim, "identity"), ) - for name, select, field_name in targets: + for name, target_selector, field_name in targets: with self.subTest(cache=name): result, _backend = _execute() self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) evidence = result.evidence - target = select(evidence) + target = target_selector(evidence) original_identity = getattr(target, field_name) forged_identity = _digest(f"forged {name}") real_replay = provenance.replay_admitted_source_closure_v1 @@ -938,11 +938,12 @@ def test_source_process_transfer_and_comparator_mutations_fail(self) -> None: # Keep the BUILD preimage itself intact: a verifier that only checks # self-consistency would otherwise accept this fully well-formed but # source-unrelated comparator manifest. + preimage_fields = fields(pipeline.ArbComparatorPreimagesV1) preimage_values = tuple( dag.build.comparator.preimages.build_identity - if index == 5 + if field.name == "build_identity" else f"forged-comparator-{index}".encode("ascii") - for index in range(10) + for index, field in enumerate(preimage_fields) ) self.assertEqual(len(set(preimage_values)), len(preimage_values)) preimages = pipeline.ArbComparatorPreimagesV1(*preimage_values) diff --git a/proof/region/v1/mpfi/input.py b/proof/region/v1/mpfi/input.py index c983efb6..9a827db9 100644 --- a/proof/region/v1/mpfi/input.py +++ b/proof/region/v1/mpfi/input.py @@ -72,16 +72,6 @@ def _fresh_admitted_sources_v1( _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "admitted_sources") try: source_lock_identity = admitted_sources.source_lock_identity - if ( - type(source_lock_identity) is not bytes - or len(source_lock_identity) != 32 - or source_lock_identity == bytes(32) - or source_lock_identity != source_lock.identity - ): - _fail( - MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, - "admitted_sources", - ) except Exception: # Exact type не делает retained capability неуязвимой к post-admission # подмене; ordinary hostile failure обязан остаться typed rejection. @@ -89,6 +79,16 @@ def _fresh_admitted_sources_v1( MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources", ) + if ( + type(source_lock_identity) is not bytes + or len(source_lock_identity) != 32 + or source_lock_identity == bytes(32) + or source_lock_identity != source_lock.identity + ): + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) # Replay owns nested archive evidence; its typed provenance taxonomy must # survive instead of being flattened into an MPFI declaration error. try: diff --git a/proof/region/v1/provenance.py b/proof/region/v1/provenance.py index ce03a3c1..f5f2606f 100644 --- a/proof/region/v1/provenance.py +++ b/proof/region/v1/provenance.py @@ -583,7 +583,7 @@ def parse_from(cls, reader: _Reader) -> "SourceReleaseLockV1": ) @classmethod - def parse(cls, data: bytes) -> "SourceReleaseLockV1": + def parse(cls, data: bytes) -> SourceReleaseLockV1: """Rebuild one source declaration before it crosses a replay boundary.""" reader = _Reader(data, "source-release-lock-v1") diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index c2011e1a..99e3e4ea 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -38,10 +38,10 @@ # позволил бы согласованной правке gate скрыть drift; осознанная смена набора # тестов обновляет оба значения только по свежему перечислению. ARB_INVENTORY_SHA256_V1 = ( - "6a616daac6d2437b372d93e8e5fe03787557e7a75aea25475ca9d349568669aa" + "8adbe7c5ed352e7f100d943a98bbbeec1bace8a60080933364cfdc737ebbe644" ) ARB_ORDER_SHA256_V1 = ( - "160269a28292a0d60b2a04189fa20add09a2e5fad958592ad5ef3c33aad51f48" + "9e1a1569a2766f9e3c0eefe6ed901231b84bb8f3629207a926449b3e85f9675f" ) ARB_TEST_COUNT_V1 = 231 diff --git a/proof/region/v1/tests/test_mpfi_input.py b/proof/region/v1/tests/test_mpfi_input.py index 12ec3325..36c1c02e 100644 --- a/proof/region/v1/tests/test_mpfi_input.py +++ b/proof/region/v1/tests/test_mpfi_input.py @@ -293,17 +293,15 @@ def test_reordered_or_foreign_closure_is_rejected_before_ustar_encoding( encoder.assert_not_called() finally: object.__setattr__(admitted, "sources", original_sources) - admitted.__dict__.pop("identity", None) self.assertEqual( caught.exception.reason, mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, ) - def test_cached_lock_identity_cannot_hide_source_or_capability_drift(self) -> None: + def test_lock_identity_cannot_hide_source_or_capability_drift(self) -> None: lock, admitted, expected_entries = _admitted_closure() limits = _limits_for_entries(expected_entries) sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) - cached_lock_identity = lock.identity original_version = lock.sources[0].version object.__setattr__(lock.sources[0], "version", "2") try: @@ -314,8 +312,6 @@ def test_cached_lock_identity_cannot_hide_source_or_capability_drift(self) -> No ) finally: object.__setattr__(lock.sources[0], "version", original_version) - lock.__dict__.pop("identity", None) - lock.__dict__["identity"] = cached_lock_identity self.assertEqual( caught.exception.reason, mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, From 6cd4f1bd3577c6eedbce738a2ae148c4ded199bd Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 20:29:04 +0300 Subject: [PATCH 7/9] Test: bind policy mutations to constructor fields --- .../v1/arb/tests/test_build_identity_v2.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/proof/region/v1/arb/tests/test_build_identity_v2.py b/proof/region/v1/arb/tests/test_build_identity_v2.py index 6a146ff5..313dd092 100644 --- a/proof/region/v1/arb/tests/test_build_identity_v2.py +++ b/proof/region/v1/arb/tests/test_build_identity_v2.py @@ -31,19 +31,12 @@ ) -_POLICY_FIELDS = ( - "image_reference", - "platform", - "hostname", - "bootstrap", - "bootstrap_argv0", - "tmpfs_specs", - "user_mode", - "stdout_limit", - "stderr_limit", - "build_timeout_ns", - "probe_output_limit", - "probe_timeout_ns", +_POLICY_FIELDS = tuple( + name + for name in inspect.signature( + build_transport.DockerBuildPolicyV1.__new__ + ).parameters + if name != "cls" ) From bd47c142f08615e8ca102dc17b83ae1128e93f08 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:23:05 +0300 Subject: [PATCH 8/9] Proof: keep MPFI capability rejection explicit --- proof/region/v1/mpfi/input.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/proof/region/v1/mpfi/input.py b/proof/region/v1/mpfi/input.py index 9a827db9..c2623047 100644 --- a/proof/region/v1/mpfi/input.py +++ b/proof/region/v1/mpfi/input.py @@ -79,12 +79,13 @@ def _fresh_admitted_sources_v1( MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources", ) - if ( - type(source_lock_identity) is not bytes - or len(source_lock_identity) != 32 - or source_lock_identity == bytes(32) - or source_lock_identity != source_lock.identity - ): + bound = ( + type(source_lock_identity) is bytes + and len(source_lock_identity) == 32 + and source_lock_identity != bytes(32) + and source_lock_identity == source_lock.identity + ) + if not bound: _fail( MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources", From 931c4325b25d097a306036b160b1da7d5a9c4986 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:36:36 +0300 Subject: [PATCH 9/9] Test: expose Arb gate inventory helper --- proof/region/v1/arb/tests/gate.py | 6 ++++++ proof/region/v1/arb/tests/test_build_recipe.py | 6 +++--- proof/region/v1/tests/test_build.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py index 28b048c6..e6778f84 100644 --- a/proof/region/v1/arb/tests/gate.py +++ b/proof/region/v1/arb/tests/gate.py @@ -66,6 +66,12 @@ def _iter_tests_v1(suite: unittest.TestSuite) -> Iterator[unittest.TestCase]: raise TypeError("suite contains a non-test object") +def iter_tests_v1(suite: unittest.TestSuite) -> Iterator[unittest.TestCase]: + """Expose test enumeration without leaking the gate's private helper.""" + + return _iter_tests_v1(suite) + + def _inventory_preimage_v1(test_ids: tuple[str, ...]) -> bytes: return b"".join(test_id.encode("utf-8") + b"\n" for test_id in sorted(test_ids)) diff --git a/proof/region/v1/arb/tests/test_build_recipe.py b/proof/region/v1/arb/tests/test_build_recipe.py index 2cfe4838..f5321b57 100644 --- a/proof/region/v1/arb/tests/test_build_recipe.py +++ b/proof/region/v1/arb/tests/test_build_recipe.py @@ -20,7 +20,7 @@ class ArbBuildRecipeTests(unittest.TestCase): def test_fast_gate_includes_each_shared_contract_suite_exactly_once(self) -> None: - tests = tuple(arb_gate._iter_tests_v1(arb_gate.full_suite_v1())) + tests = tuple(arb_gate.iter_tests_v1(arb_gate.full_suite_v1())) identifiers = tuple(test.id() for test in tests) for pattern, module_prefix in ( ("test_executor.py", "test_executor."), @@ -34,7 +34,7 @@ def test_fast_gate_includes_each_shared_contract_suite_exactly_once(self) -> Non ) expected = tuple( test.id() - for test in arb_gate._iter_tests_v1( + for test in arb_gate.iter_tests_v1( unittest.defaultTestLoader.discover( str(arb_gate.SHARED_TEST_DIRECTORY), pattern=pattern, @@ -51,7 +51,7 @@ def test_mpfi_input_contract_cannot_green_by_skipping(self) -> None: str(arb_gate.SHARED_TEST_DIRECTORY), pattern="test_mpfi_input.py", ) - test_ids = tuple(test.id() for test in arb_gate._iter_tests_v1(suite)) + test_ids = tuple(test.id() for test in arb_gate.iter_tests_v1(suite)) result = unittest.TestResult() suite.run(result) diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index 78149613..f1b52264 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -263,7 +263,7 @@ def _controlled_build( class ExistingArbGateTests(unittest.TestCase): def test_existing_arb_suite_keeps_exact_count_order_and_inventory(self) -> None: - tests = tuple(arb_gate._iter_tests_v1(arb_gate.full_suite_v1())) + tests = tuple(arb_gate.iter_tests_v1(arb_gate.full_suite_v1())) identifiers = tuple(test.id() for test in tests) ordered_preimage = b"".join( identifier.encode("utf-8") + b"\n" for identifier in identifiers