diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index 79ab5f06..f4a168e3 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -236,13 +236,15 @@ observation. Право на Arb receipt получает не executor, а от ## Общая граница BUILD -`proof/region/v1/build/input.py` принимает уже нормализованные lane entries, -кодирует один канонический USTAR и владеет точными input bytes. Он не -импортирует и не перепроверяет source capability: это ответственность -потребляющего lane. `SealedInputV1` структурно неизменяем, связывает -целостность байтов с opaque caller digest и не утверждает recipe либо engine -semantics. Resource bounds передаёт lane: общий encoder не вводит собственный -fixture-specific cap. +`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. +`SealedInputV1` структурно неизменяем, связывает целостность байтов с opaque +caller digest и не утверждает recipe либо engine semantics. Resource bounds +передаёт lane: общий encoder не вводит собственный fixture-specific cap. `proof/region/v1/build/transport.py` владеет immutable Docker policy, одноразовым probe→build lease, bounded stdin/stdout observation, cleanup и diff --git a/proof/region/v1/arb/pipeline.py b/proof/region/v1/arb/pipeline.py index e66af0d6..b65bd01d 100644 --- a/proof/region/v1/arb/pipeline.py +++ b/proof/region/v1/arb/pipeline.py @@ -11,8 +11,6 @@ from __future__ import annotations import hashlib -import io -import tarfile from dataclasses import dataclass, fields from enum import StrEnum from functools import cached_property @@ -418,13 +416,20 @@ def _seal_build_input_bundle_v1( if not build_transport.docker_policy_is_valid_v1(exact_policy): raise TypeError("exact_policy must be canonical DockerBuildPolicyV1") source_entries = tuple( - entry + ( + f"inputs/{lock.root_prefix[:-1]}/{relative}", + mode, + contents, + ) for lock, admitted in zip( request.source_lock.sources, request.admitted_sources.sources, strict=True, ) - for entry in _normalized_source_entries_v1(lock, admitted) + for relative, mode, contents in provenance.materialize_admitted_source_files_v1( + lock, + admitted, + ) ) workspace_entries = tuple( ( @@ -509,89 +514,6 @@ def __str__(self) -> str: return f"{self.reason.value}: {self.field}" -def _normalized_source_entries_v1( - lock: provenance.SourceReleaseLockV1, - admitted: provenance.SafeSourceArchiveV1, -) -> tuple[tuple[str, int, bytes], ...]: - """Replay Arb-owned source authority into generic canonical-tree entries.""" - - def reject(field_name: str) -> NoReturn: - raise PipelineInputErrorV1( - PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, - field_name, - ) - - if type(lock) is not provenance.SourceReleaseLockV1: - reject("lock") - if type(admitted) is not provenance.SafeSourceArchiveV1: - reject("admitted") - try: - replayed, raw_tar = provenance.replay_admitted_source_archive_v1( - lock, - admitted, - ) - except Exception: - reject("admitted") - if ( - replayed.source_lock_identity != admitted.source_lock_identity - or replayed.archive_sha256 != admitted.archive_sha256 - or replayed.tree_identity != admitted.tree_identity - or replayed.regular_file_count != admitted.regular_file_count - or replayed.regular_file_bytes != admitted.regular_file_bytes - or replayed.files != admitted.files - ): - reject("admitted") - expected = {item.path: item for item in replayed.files} - values: list[tuple[str, int, bytes]] = [] - seen: set[str] = set() - try: - with tarfile.open(fileobj=io.BytesIO(raw_tar), mode="r:") as archive: - for member in archive: - if member.isdir(): - continue - if not member.isreg() or not member.name.startswith(lock.root_prefix): - reject("member") - relative = member.name[len(lock.root_prefix) :] - coordinate = expected.get(relative) - if coordinate is None or relative in seen: - reject("file set") - stream = archive.extractfile(member) - if stream is None: - reject("regular file") - chunks: list[bytes] = [] - length = 0 - hasher = hashlib.sha256() - while True: - chunk = stream.read(provenance.READ_CHUNK_BYTES) - if not chunk: - break - length += len(chunk) - if length > coordinate.length: - reject("file length") - chunks.append(chunk) - hasher.update(chunk) - if ( - length != coordinate.length - or hasher.digest() != coordinate.sha256 - ): - reject("file contents") - values.append( - ( - f"inputs/{lock.root_prefix[:-1]}/{relative}", - coordinate.mode, - b"".join(chunks), - ) - ) - seen.add(relative) - except PipelineInputErrorV1: - raise - except (OSError, tarfile.TarError, ValueError): - reject("archive") - if seen != set(expected): - reject("incomplete archive") - return tuple(sorted(values)) - - @dataclass(frozen=True) class FlintSourceContentPartitionV1: """Structural FLINT archive partition, not an origin assertion. @@ -1580,7 +1502,6 @@ def build(self, request: PipelineRequestV1) -> BuildResultV1: OSError, TypeError, ValueError, - tarfile.TarError, BuildSourceAdmissionErrorV1, provenance.ProvenanceErrorV1, build_input.InputErrorV1, diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py index a4eabb9b..b24a2845 100644 --- a/proof/region/v1/arb/tests/gate.py +++ b/proof/region/v1/arb/tests/gate.py @@ -15,9 +15,9 @@ REPO = Path(__file__).resolve().parents[5] sys.path.insert(0, str(REPO)) EXPECTED_TEST_INVENTORY_SHA256 = ( - "75462b6e595a3642705ce5135ca6a38b5c634be61b0ef33ea81f73abe17b564b" + "721fcceb07c3d73e30032814a181e9c3d86f2185cfb3382cc79b34e05618fa48" ) -EXPECTED_TEST_COUNT = 182 +EXPECTED_TEST_COUNT = 184 _EVALUATOR_REASON = "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary" EXPECTED_SKIPS = frozenset( { diff --git a/proof/region/v1/arb/tests/test_pipeline.py b/proof/region/v1/arb/tests/test_pipeline.py index 831420e2..f09c9af9 100644 --- a/proof/region/v1/arb/tests/test_pipeline.py +++ b/proof/region/v1/arb/tests/test_pipeline.py @@ -831,6 +831,49 @@ def test_input_transport_or_invalid_binary_is_typed_failure(self) -> None: self.assertEqual(result.attempt, 1) self.assertEqual(result.reason, reason) + def test_forged_source_coordinate_is_rejected_before_build_without_escape(self) -> None: + request = _request() + source = request.admitted_sources.sources[0] + original_tree_identity = source.tree_identity + object.__setattr__(source, "tree_identity", _digest("foreign-tree")) + backend = _BuildBackend((_static_elf(), _static_elf())) + try: + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + + self.assertIs(type(result), build_transport.BuildRejectedV1) + self.assertEqual(result.attempt, 1) + self.assertIs( + result.reason, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(backend.requests, []) + + def test_hostile_nominal_source_coordinate_is_typed_before_build(self) -> None: + request = _request() + source = request.source_lock.sources[0] + original_length = source.archive_length + + class ExplodingCoordinate: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("hostile coordinate comparison") + + object.__setattr__(source, "archive_length", ExplodingCoordinate()) + backend = _BuildBackend((_static_elf(), _static_elf())) + try: + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + finally: + object.__setattr__(source, "archive_length", original_length) + + self.assertIs(type(result), build_transport.BuildRejectedV1) + self.assertEqual(result.attempt, 1) + self.assertIs( + result.reason, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(backend.requests, []) + def test_job_that_exceeds_exact_run_limits_is_rejected_before_build(self) -> None: with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: _request( diff --git a/proof/region/v1/arb/tests/test_transport.py b/proof/region/v1/arb/tests/test_transport.py index 3cd7d368..a1de1280 100644 --- a/proof/region/v1/arb/tests/test_transport.py +++ b/proof/region/v1/arb/tests/test_transport.py @@ -30,6 +30,7 @@ from build import input as build_input # noqa: E402 from build import transport as build_transport # noqa: E402 import pipeline # noqa: E402 +import provenance # noqa: E402 from test_pipeline import ( # noqa: E402 _docker_capability, _probe_native_backend, @@ -278,15 +279,27 @@ def test_bundle_is_reproducible_normalized_ustar_with_no_host_authority(self) -> self.assertEqual(first.sha256, second.sha256) self.assertEqual(first.binding_identity, second.binding_identity) self.assertTrue(pipeline.arb_input_is_bound_v1(request, policy, first)) + self.assertEqual( + first.sha256.hex(), + "5d6e789a721aeed1a8ff023f0af5389711f85f6fe95294d8290b20301235f4df", + ) + self.assertEqual(first.length, 174_080) source_entries = tuple( - entry + ( + f"inputs/{lock.root_prefix[:-1]}/{relative}", + mode, + contents, + ) for lock, admitted in zip( request.source_lock.sources, request.admitted_sources.sources, strict=True, ) - for entry in pipeline._normalized_source_entries_v1(lock, admitted) + for relative, mode, contents in provenance.materialize_admitted_source_files_v1( + lock, + admitted, + ) ) workspace_entries = tuple( ( @@ -491,8 +504,8 @@ def test_replayed_source_coordinates_must_match_the_admitted_capability(self) -> original = admitted.tree_identity object.__setattr__(admitted, "tree_identity", _digest("mutated-tree")) try: - with self.assertRaises(pipeline.PipelineInputErrorV1): - pipeline._normalized_source_entries_v1( + with self.assertRaises(provenance.ProvenanceErrorV1): + provenance.materialize_admitted_source_files_v1( request.source_lock.sources[0], admitted, ) diff --git a/proof/region/v1/provenance.py b/proof/region/v1/provenance.py index aa0f0429..42b8c9b0 100644 --- a/proof/region/v1/provenance.py +++ b/proof/region/v1/provenance.py @@ -15,7 +15,6 @@ import zlib from dataclasses import dataclass, field from enum import IntEnum, StrEnum -from functools import cached_property from pathlib import PurePosixPath from typing import NoReturn, TypeAlias from urllib.parse import urlsplit @@ -37,6 +36,9 @@ ALLOWED_REGULAR_MODES_V1 = frozenset((0o644, 0o700, 0o755)) ALLOWED_DIRECTORY_MODE_V1 = 0o755 +# Derived identities are capability coordinates, never cache state: a frozen +# dataclass still has a writable __dict__ through hostile object mutation. + class ProvenanceReasonV1(StrEnum): BAD_MAGIC = "bad_magic" @@ -580,7 +582,7 @@ def parse_from(cls, reader: _Reader) -> "SourceReleaseLockV1": integrity, ) - @cached_property + @property def identity(self) -> bytes: return _identity(b"labcolors.proof-region.source-release-lock.v1\0", self.encode()) @@ -655,7 +657,7 @@ def parse(cls, data: bytes) -> ArbSourceLockV1: _fail("arb-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") return result - @cached_property + @property def identity(self) -> bytes: return _identity(SOURCE_LOCK_ID_LABEL_V1, self.encode()) @@ -702,7 +704,7 @@ def parse(cls, data: bytes) -> MpfiSourceLockV1: _fail("mpfi-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") return result - @cached_property + @property def identity(self) -> bytes: return _identity(SOURCE_LOCK_ID_LABEL_V1, self.encode()) @@ -812,24 +814,7 @@ def source_archive_replay_coordinates_v1( ) -> tuple[bytes, ...]: """Recompute the retained source coordinates without reopening a path.""" - if type(expected) is not SourceReleaseLockV1: - raise TypeError("expected must be SourceReleaseLockV1") - if type(admitted) is not SafeSourceArchiveV1: - raise TypeError("admitted must be SafeSourceArchiveV1") replayed, _raw_tar = replay_admitted_source_archive_v1(expected, admitted) - if ( - admitted.source_lock_identity != replayed.source_lock_identity - or admitted.archive_sha256 != replayed.archive_sha256 - or admitted.tree_identity != replayed.tree_identity - or admitted.regular_file_count != replayed.regular_file_count - or admitted.regular_file_bytes != replayed.regular_file_bytes - or admitted.files != replayed.files - ): - _fail( - "source-archive-replay-v1", - ProvenanceReasonV1.FOREIGN_BINDING, - "retained source coordinates changed", - ) archive = replayed.archive_bytes manifest = archive_file_manifest_bytes_v1(replayed.files) return ( @@ -909,7 +894,7 @@ def __init__( object.__setattr__(self, "source_lock_identity", source_lock_identity) object.__setattr__(self, "sources", sources) - @cached_property + @property def identity(self) -> bytes: return _admitted_source_closure_identity_v1( ADMITTED_ARB_SOURCES_ID_LABEL_V1, @@ -942,7 +927,7 @@ def __init__( object.__setattr__(self, "source_lock_identity", source_lock_identity) object.__setattr__(self, "sources", sources) - @cached_property + @property def identity(self) -> bytes: return _admitted_source_closure_identity_v1( ADMITTED_MPFI_SOURCES_ID_LABEL_V1, @@ -1237,7 +1222,7 @@ def replay_admitted_source_archive_v1( expected: SourceReleaseLockV1, admitted: SafeSourceArchiveV1, ) -> tuple[SafeSourceArchiveV1, bytes]: - """Re-admit owned bytes and return the raw tar from that exact pass. + """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. @@ -1247,7 +1232,123 @@ def replay_admitted_source_archive_v1( raise TypeError("expected must be SourceReleaseLockV1") if type(admitted) is not SafeSourceArchiveV1: raise TypeError("admitted must be SafeSourceArchiveV1") - return _admit_source_archive_once(expected, admitted.archive_bytes) + try: + replayed, raw_tar = _admit_source_archive_once( + expected, + admitted.archive_bytes, + ) + 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 + ) + except ProvenanceErrorV1: + raise + except Exception: + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained source capability", + ) + if not retained_coordinates_match: + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "retained source coordinates changed", + ) + return replayed, raw_tar + + +def materialize_admitted_source_files_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[tuple[str, int, bytes], ...]: + """Return exact relative regular files from one replayed source capability. + + 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) + expected_by_path = {item.path: item for item in replayed.files} + values: list[tuple[str, int, bytes]] = [] + seen: set[str] = set() + try: + with tarfile.open(fileobj=io.BytesIO(raw_tar), mode="r:") as archive: + for member in archive: + if member.isdir(): + continue + if not member.isreg() or not member.name.startswith(expected.root_prefix): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "unexpected archive member", + ) + relative = member.name[len(expected.root_prefix) :] + coordinate = expected_by_path.get(relative) + if ( + coordinate is None + or relative in seen + or member.mode != coordinate.mode + or member.size != coordinate.length + ): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "archive file set changed", + ) + stream = archive.extractfile(member) + if stream is None: + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + relative, + ) + chunks: list[bytes] = [] + length = 0 + hasher = hashlib.sha256() + while True: + chunk = stream.read(READ_CHUNK_BYTES) + if not chunk: + break + length += len(chunk) + if length > coordinate.length: + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + relative, + ) + chunks.append(chunk) + hasher.update(chunk) + if ( + length != coordinate.length + or hasher.digest() != coordinate.sha256 + ): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + relative, + ) + values.append((relative, coordinate.mode, b"".join(chunks))) + seen.add(relative) + except ProvenanceErrorV1: + raise + except (OSError, tarfile.TarError, ValueError): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.NONCANONICAL_TAR, + "archive replay failed", + ) + if seen != set(expected_by_path): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "archive file set is incomplete", + ) + return tuple(sorted(values)) def _validate_source_capability_closure_v1( diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py index e506d660..845e2605 100644 --- a/proof/region/v1/tests/test_build.py +++ b/proof/region/v1/tests/test_build.py @@ -38,7 +38,7 @@ # of maintaining a second literal that could drift from the executed gate. ARB_INVENTORY_SHA256_V1 = arb_gate.EXPECTED_TEST_INVENTORY_SHA256 ARB_ORDER_SHA256_V1 = ( - "6700241b8685179ecaed8eab062e65581ae183fa544b02b039b464d26ce53d7c" + "ad40ffaf023f70b347c1ad691e0ebfa9e0dbfb53cdda45aacd86f1dbb2c5c999" ) ARB_TEST_COUNT_V1 = arb_gate.EXPECTED_TEST_COUNT @@ -414,6 +414,7 @@ def test_observation_contract_uses_current_non_claiming_language(self) -> None: def test_arb_consumers_move_atomically_without_compatibility_reexports(self) -> None: build_input = importlib.import_module("build.input") transport = importlib.import_module("build.transport") + provenance = importlib.import_module("provenance") arb_pipeline = pipeline arb_receipt = importlib.import_module("receipt") request = _request() @@ -425,6 +426,10 @@ def test_arb_consumers_move_atomically_without_compatibility_reexports(self) -> self.assertIs(arb_pipeline.build_input, build_input) self.assertIs(arb_pipeline.build_transport, transport) self.assertIs(arb_receipt.build_transport, transport) + self.assertTrue( + hasattr(provenance, "materialize_admitted_source_files_v1") + ) + self.assertFalse(hasattr(arb_pipeline, "_normalized_source_entries_v1")) self.assertFalse(hasattr(arb_receipt, "build_input")) self.assertIs(type(bundle), build_input.SealedInputV1) for name in MOVED_INPUT_SURFACE_V1 + MOVED_TRANSPORT_SURFACE_V1: @@ -472,8 +477,7 @@ def test_shared_input_and_policy_are_deeply_immutable_coordinates(self) -> None: self.assertIs(type(sealed), build_input.SealedInputV1) self.assertIs(type(policy), transport.DockerBuildPolicyV1) - def test_forged_source_authorities_fail_in_the_arb_taxonomy(self) -> None: - build_input = importlib.import_module("build.input") + def test_forged_source_authorities_fail_in_shared_provenance_taxonomy(self) -> None: provenance = importlib.import_module("provenance") request = _request() lock = request.source_lock.sources[0] @@ -484,14 +488,14 @@ def test_forged_source_authorities_fail_in_the_arb_taxonomy(self) -> None: (lock, object.__new__(provenance.SafeSourceArchiveV1)), ): with self.subTest(authority=type(hostile_lock).__name__): - with self.assertRaises(pipeline.PipelineInputErrorV1) as raised: - pipeline._normalized_source_entries_v1( + with self.assertRaises(provenance.ProvenanceErrorV1) as raised: + provenance.materialize_admitted_source_files_v1( hostile_lock, hostile_admitted, ) self.assertEqual( raised.exception.reason, - pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + provenance.ProvenanceReasonV1.FOREIGN_BINDING, ) diff --git a/proof/region/v1/tests/test_mpfi_source_lock.py b/proof/region/v1/tests/test_mpfi_source_lock.py index e601a904..017944c6 100644 --- a/proof/region/v1/tests/test_mpfi_source_lock.py +++ b/proof/region/v1/tests/test_mpfi_source_lock.py @@ -223,10 +223,28 @@ def test_three_locked_sources_become_one_mpfi_capability(self) -> None: with self.assertRaises(TypeError): AdmittedMpfiSourcesV1(lock.identity, sources, _token=object()) + def test_mpfi_archive_uses_the_shared_engine_neutral_materializer(self) -> None: + archive = fixture_archive() + lock = fixture_release( + SourceRoleV1.MPFI, + archive, + ProjectPinnedArchiveDigestPolicyV1(), + ) + admitted = admit_source_archive(lock, archive) + + self.assertEqual( + provenance.materialize_admitted_source_files_v1(lock, admitted), + ( + ("LICENSE", 0o644, b"license"), + ("value", 0o644, b"data"), + ), + ) + def test_reference_does_not_upgrade_the_mpfi_digest_to_publisher_evidence(self) -> None: reference = (ROOT / "PROTOCOL.md").read_text(encoding="utf-8") self.assertIn("ProjectPinnedArchiveDigestPolicyV1", reference) + self.assertIn("materialize_admitted_source_files_v1", 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 5db241b9..649ee7db 100644 --- a/proof/region/v1/tests/test_source_lock.py +++ b/proof/region/v1/tests/test_source_lock.py @@ -12,6 +12,7 @@ import unittest from dataclasses import replace from pathlib import Path +from unittest import mock ROOT = Path(__file__).resolve().parents[1] @@ -20,10 +21,13 @@ import provenance # noqa: E402 from provenance import ( # noqa: E402 AdmittedArbSourcesV1, + AdmittedMpfiSourcesV1, ArchiveFormatV1, DetachedSignaturePolicyV1, GitContentRelationPolicyV1, LegalFileV1, + MpfiSourceLockV1, + ProjectPinnedArchiveDigestPolicyV1, ProjectPinnedReleaseOnlyFileV1, ProvenanceErrorV1, ProvenanceReasonV1, @@ -31,6 +35,7 @@ SourceRoleV1, admit_source_archive, admit_arb_sources, + admit_mpfi_sources, arb_source_lock_v1, ) @@ -39,6 +44,34 @@ def sha256(value: bytes) -> bytes: return hashlib.sha256(value).digest() +def canonical_identity(label: bytes, encoded: bytes) -> bytes: + """Independent literal oracle for identity values in hostile cache tests.""" + + return hashlib.sha256( + label + len(encoded).to_bytes(8, "big") + encoded + ).digest() + + +def admitted_closure_identity( + label: bytes, + source_lock_identity: bytes, + sources: tuple[provenance.SafeSourceArchiveV1, ...], +) -> bytes: + """Keep the cache-poisoning oracle independent of production preimages.""" + + chunks = [source_lock_identity] + for ordinal, source in enumerate(sources): + chunks.extend( + ( + bytes((ordinal,)), + source.source_lock_identity, + source.archive_sha256, + source.tree_identity, + ) + ) + return canonical_identity(label, b"".join(chunks)) + + def tar_gz( entries: tuple[tuple[str, bytes | None, bytes | None], ...], ) -> bytes: @@ -70,6 +103,25 @@ def tar_gz( return gzip.compress(raw.getvalue(), compresslevel=9, mtime=0) +def raw_ustar( + entries: tuple[tuple[str, bytes, int], ...], +) -> bytes: + """Build a replay fixture without reusing admission's compressed input.""" + + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + root = tarfile.TarInfo("fixture-1/") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + archive.addfile(root) + for name, body, mode in entries: + member = tarfile.TarInfo(f"fixture-1/{name}") + member.mode = mode + member.size = len(body) + archive.addfile(member, io.BytesIO(body)) + return raw.getvalue() + + def fixture_lock( archive: bytes, *, @@ -453,6 +505,306 @@ def test_archive_is_hash_checked_then_scanned_without_extracting(self) -> None: ProvenanceReasonV1.LEGAL_FILES_MISMATCH, ) + def test_derived_identities_ignore_injected_instance_caches(self) -> None: + poison = bytes.fromhex("a5" * 32) + release = fixture_lock(GOOD_ARCHIVE) + release_identity = canonical_identity( + b"labcolors.proof-region.source-release-lock.v1\0", + release.encode(), + ) + release.__dict__["identity"] = poison + + 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) + self.assertEqual( + provenance.source_archive_replay_coordinates_v1( + release, + admitted_release, + )[2], + release_identity, + ) + self.assertEqual( + provenance.materialize_admitted_source_files_v1( + release, + admitted_release, + ), + (("LICENSE", 0o644, b"license"), ("value", 0o644, b"data")), + ) + + arb_gmp = fixture_lock(GOOD_ARCHIVE) + arb_mpfr = replace(arb_gmp, role=SourceRoleV1.MPFR) + arb_flint = replace( + arb_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"), + ), + ), + ), + ) + arb_lock = provenance.ArbSourceLockV1((arb_gmp, arb_mpfr, arb_flint)) + arb_lock_identity = canonical_identity( + b"labcolors.proof-region.source-lock.v1\0", + arb_lock.encode(), + ) + arb_lock.__dict__["identity"] = poison + arb_sources = tuple( + admit_source_archive(source, GOOD_ARCHIVE) + for source in arb_lock.sources + ) + arb_admitted = admit_arb_sources(arb_lock, arb_sources) + self.assertIs(type(arb_admitted), AdmittedArbSourcesV1) + self.assertEqual(arb_admitted.source_lock_identity, arb_lock_identity) + arb_admitted_identity = admitted_closure_identity( + b"labcolors.proof-region.admitted-arb-sources.v1\0", + arb_lock_identity, + arb_sources, + ) + arb_admitted.__dict__["identity"] = poison + self.assertEqual(arb_admitted.identity, arb_admitted_identity) + + mpfi_gmp = fixture_lock(GOOD_ARCHIVE) + mpfi_mpfr = replace(mpfi_gmp, role=SourceRoleV1.MPFR) + mpfi_release = replace( + mpfi_gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + mpfi_lock = MpfiSourceLockV1((mpfi_gmp, mpfi_mpfr, mpfi_release)) + mpfi_lock_identity = canonical_identity( + b"labcolors.proof-region.source-lock.v1\0", + mpfi_lock.encode(), + ) + mpfi_lock.__dict__["identity"] = poison + mpfi_sources = tuple( + admit_source_archive(source, GOOD_ARCHIVE) + for source in mpfi_lock.sources + ) + mpfi_admitted = admit_mpfi_sources(mpfi_lock, mpfi_sources) + self.assertIs(type(mpfi_admitted), AdmittedMpfiSourcesV1) + self.assertEqual(mpfi_admitted.source_lock_identity, mpfi_lock_identity) + mpfi_admitted_identity = admitted_closure_identity( + b"labcolors.proof-region.admitted-mpfi-sources.v1\0", + mpfi_lock_identity, + mpfi_sources, + ) + mpfi_admitted.__dict__["identity"] = poison + self.assertEqual(mpfi_admitted.identity, mpfi_admitted_identity) + + 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), + ( + ("LICENSE", 0o644, b"license"), + ("value", 0o644, b"data"), + ), + ) + + mutations = ( + ("source_lock_identity", sha256(b"foreign-lock")), + ("archive_sha256", sha256(b"foreign-archive")), + ("tree_identity", sha256(b"foreign-tree")), + ("regular_file_count", admitted.regular_file_count + 1), + ("regular_file_bytes", admitted.regular_file_bytes + 1), + ("files", tuple(reversed(admitted.files))), + ) + for field_name, replacement in mutations: + with self.subTest(retained_coordinate=field_name): + original = getattr(admitted, field_name) + object.__setattr__(admitted, field_name, replacement) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.materialize_admitted_source_files_v1(lock, admitted) + finally: + object.__setattr__(admitted, field_name, original) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + + original_archive_bytes = admitted.archive_bytes + object.__setattr__(admitted, "_archive_bytes", GOOD_ARCHIVE[:-1]) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.materialize_admitted_source_files_v1(lock, admitted) + finally: + object.__setattr__(admitted, "_archive_bytes", original_archive_bytes) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.ARCHIVE_LENGTH_MISMATCH, + ) + + original_role = lock.role + object.__setattr__(lock, "role", 999) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.materialize_admitted_source_files_v1(lock, admitted) + finally: + object.__setattr__(lock, "role", original_role) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + 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, + ) + + def test_replay_boundary_totalizes_hostile_nominal_coordinates(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + + class ExplodingCoordinate: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("hostile coordinate comparison") + + original_length = lock.archive_length + 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, + ), + ), + ): + with self.subTest(operation=name): + with self.assertRaises(ProvenanceErrorV1) as caught: + operation() + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + finally: + object.__setattr__(lock, "archive_length", original_length) + + class InterruptedCoordinate: + def __ne__(self, _other: object) -> bool: + raise KeyboardInterrupt("source lock interruption") + + object.__setattr__(lock, "archive_length", InterruptedCoordinate()) + try: + with self.assertRaises(KeyboardInterrupt): + provenance.replay_admitted_source_archive_v1(lock, admitted) + finally: + object.__setattr__(lock, "archive_length", original_length) + + def test_shared_materializer_is_invariant_under_regular_member_permutation(self) -> None: + expected = ( + ("LICENSE", 0o644, b"license"), + ("value", 0o644, b"data"), + ) + for entries in ( + ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/value", b"data", None), + ), + ( + ("fixture-1/", None, None), + ("fixture-1/value", b"data", None), + ("fixture-1/LICENSE", b"license", None), + ), + ): + with self.subTest(member_order=entries): + archive = tar_gz(entries) + lock = fixture_lock(archive) + admitted = admit_source_archive(lock, archive) + self.assertEqual( + provenance.materialize_admitted_source_files_v1(lock, admitted), + expected, + ) + + def test_shared_materializer_rechecks_the_replayed_tar_before_returning_files(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + replayed, _ = provenance.replay_admitted_source_archive_v1(lock, admitted) + cases = ( + ( + "body", + raw_ustar( + ( + ("LICENSE", b"license", 0o644), + ("value", b"evil", 0o644), + ) + ), + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + ), + ( + "mode", + raw_ustar( + ( + ("LICENSE", b"license", 0o644), + ("value", b"data", 0o755), + ) + ), + ProvenanceReasonV1.FOREIGN_BINDING, + ), + ( + "duplicate", + raw_ustar( + ( + ("LICENSE", b"license", 0o644), + ("value", b"data", 0o644), + ("value", b"data", 0o644), + ) + ), + ProvenanceReasonV1.FOREIGN_BINDING, + ), + ( + "missing", + raw_ustar((("LICENSE", b"license", 0o644),)), + ProvenanceReasonV1.FOREIGN_BINDING, + ), + ) + for name, raw_tar, reason in cases: + with self.subTest(mutation=name): + with mock.patch.object( + provenance, + "replay_admitted_source_archive_v1", + return_value=(replayed, raw_tar), + ): + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.materialize_admitted_source_files_v1(lock, admitted) + self.assertEqual(caught.exception.reason, reason) + def test_unsafe_member_kinds_and_paths_are_rejected(self) -> None: fixtures = ( (ProvenanceReasonV1.UNSAFE_PATH, (