From cd9e590f7f9fd00b19764b4c18b8a048cb234041 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:56:43 +0300 Subject: [PATCH 1/3] Add exact MPFI source admission --- proof/region/v1/PROTOCOL.md | 22 +- proof/region/v1/provenance.py | 368 ++++++++++++++---- .../region/v1/tests/test_mpfi_source_lock.py | 234 +++++++++++ 3 files changed, 552 insertions(+), 72 deletions(-) create mode 100644 proof/region/v1/tests/test_mpfi_source_lock.py diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index 5b68d409..4cb1cc85 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -12,8 +12,9 @@ protocol fixtures и не является evaluator runner. `arb/evaluator` в Arb-enclosures и выпускает связанные transcript bytes; `SourceBoundArbControllerV1` заново собирает evaluator, запускает его и создаёт только provenance receipt. Ни один из этих путей не выполняет независимый -semantic replay и не создаёт mathematical proof type. MPFI evaluator/provenance -path и semantic verifier в текущем release отсутствуют. +semantic replay и не создаёт mathematical proof type. MPFI source lock и +archive admission уже представлены, но MPFI evaluator/source-bound receipt и +semantic verifier в текущем release отсутствуют. Structural protocol/admission сам не является математическим proof. `DualComparisonCandidateV1` кодирует только structural agreement и не создаёт @@ -27,8 +28,9 @@ evidence. В тесте протокола такое значение явно Протокол не входит в Cargo workspace, Core, WASM, FFI, bindings или packages. Текущий `SourceBoundEvaluatorReceiptV1` подтверждает причинную цепь только Arb. -MPFI provenance, cross-path dependency overlap и diversity не представлены -admitted типом; structural coordinates не восполняют это отсутствие. +MPFI source closure ещё не является provenance исполнения: MPFI source-bound +receipt, cross-path dependency overlap и diversity не представлены admitted +типом; structural coordinates не восполняют это отсутствие. ## Wire и identity @@ -153,8 +155,8 @@ Arb controller связывает его с наблюдёнными BUILD/RUN ## Source lock и integrity observations `SourceReleaseLockV1` фиксирует bytes и структурный состав архива. Поле -`.integrity` содержит один `SourceIntegrityPolicyV1`; это требование проверки, -а не заявление о publisher origin. Поле +`.integrity` содержит один `SourceIntegrityPolicyV1`; это точная граница +доступного evidence, а не безусловное заявление о publisher origin. Поле `legal_files` — только точный project-pinned набор находящихся в архиве legal files; оно не заявляет полноту legal-набора или compliance распространяемого бинарника. Несовпадение этого набора имеет отдельную причину @@ -182,6 +184,14 @@ Git executable/version, repository URL и tag являются диагност координатами поиска и не входят в authority этой relation. Relation доказывает совпадение content graph, но не publisher или канал получения архива. +Для MPFI 1.5.4 не подтверждены detached signature, опубликованный upstream +checksum или равенство release-архива Git tag/tree. Поэтому +`ProjectPinnedArchiveDigestPolicyV1` намеренно не содержит внешнего payload: +Lab Colors фиксирует exact HTTPS URL, длину и SHA-256 полученных archive bytes, +но не приписывает этот digest издателю и не заявляет publisher authentication. +`MpfiSourceLockV1` использует те же единичные GMP/MPFR source declarations, что +и Arb, однако имеет отдельную aggregate identity и отдельный typed admission. + ## Diagnostic execution boundary `proof/region/v1/executor.py` — общий для enclosure engines leaf без импорта diff --git a/proof/region/v1/provenance.py b/proof/region/v1/provenance.py index b72e973b..0721def4 100644 --- a/proof/region/v1/provenance.py +++ b/proof/region/v1/provenance.py @@ -25,8 +25,9 @@ SOURCE_LOCK_ID_LABEL_V1 = b"labcolors.proof-region.source-lock.v1\0" SOURCE_TREE_ID_LABEL_V1 = b"labcolors.proof-region.safe-source-tree.v1\0" ADMITTED_ARB_SOURCES_ID_LABEL_V1 = b"labcolors.proof-region.admitted-arb-sources.v1\0" +ADMITTED_MPFI_SOURCES_ID_LABEL_V1 = b"labcolors.proof-region.admitted-mpfi-sources.v1\0" SOURCE_LOCK_RELEASE_V1 = 1 -ARBITRARY_PRECISION_SOURCE_COUNT_V1 = 3 +SOURCE_CLOSURE_COUNT_V1 = 3 SHA256_BYTES = 32 SHA1_BYTES = 20 OPENPGP_V4_FINGERPRINT_BYTES = 20 @@ -210,11 +211,13 @@ class SourceRoleV1(IntEnum): GMP = 1 MPFR = 2 FLINT_ARB = 3 + MPFI = 4 class IntegrityKindV1(IntEnum): DETACHED_SIGNATURE = 1 GIT_CONTENT_RELATION = 2 + PROJECT_PINNED_ARCHIVE_DIGEST = 3 @dataclass(frozen=True) @@ -411,8 +414,32 @@ def parse_from(cls, reader: _Reader) -> "GitContentRelationPolicyV1": return cls(repository, tag, commit, tree, common_file_count, omitted, release_only) +@dataclass(frozen=True) +class ProjectPinnedArchiveDigestPolicyV1: + """State that the project pins archive bytes without upstream authentication. + + The digest and exact archive coordinates live in ``SourceReleaseLockV1``. + This marker prevents an HTTPS download plus a project-chosen digest from + being misreported as a publisher signature or a verified Git relation. + """ + + kind: IntegrityKindV1 = field( + init=False, + default=IntegrityKindV1.PROJECT_PINNED_ARCHIVE_DIGEST, + ) + + def encode_payload(self) -> bytes: + return b"" + + @classmethod + def parse_from(cls, _reader: _Reader) -> "ProjectPinnedArchiveDigestPolicyV1": + return cls() + + SourceIntegrityPolicyV1: TypeAlias = ( - DetachedSignaturePolicyV1 | GitContentRelationPolicyV1 + DetachedSignaturePolicyV1 + | GitContentRelationPolicyV1 + | ProjectPinnedArchiveDigestPolicyV1 ) @@ -424,7 +451,11 @@ def _parse_integrity_policy(reader: _Reader) -> SourceIntegrityPolicyV1: _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_ENUM, "integrity kind") if kind is IntegrityKindV1.DETACHED_SIGNATURE: return DetachedSignaturePolicyV1.parse_from(reader) - return GitContentRelationPolicyV1.parse_from(reader) + if kind is IntegrityKindV1.GIT_CONTENT_RELATION: + return GitContentRelationPolicyV1.parse_from(reader) + if kind is IntegrityKindV1.PROJECT_PINNED_ARCHIVE_DIGEST: + return ProjectPinnedArchiveDigestPolicyV1.parse_from(reader) + _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_ENUM, "integrity kind") @dataclass(frozen=True) @@ -469,6 +500,7 @@ def __post_init__(self) -> None: if type(self.integrity) not in ( DetachedSignaturePolicyV1, GitContentRelationPolicyV1, + ProjectPinnedArchiveDigestPolicyV1, ): _fail( "source-release-lock-v1", @@ -553,12 +585,44 @@ def identity(self) -> bytes: return _identity(b"labcolors.proof-region.source-release-lock.v1\0", self.encode()) +_SourceClosureV1: TypeAlias = tuple[ + SourceReleaseLockV1, + SourceReleaseLockV1, + SourceReleaseLockV1, +] + + +def _encode_source_closure_v1(sources: _SourceClosureV1) -> bytes: + return ( + SOURCE_LOCK_MAGIC_V1 + + bytes((SOURCE_LOCK_RELEASE_V1, len(sources))) + + b"".join(source.encode() for source in sources) + ) + + +def _parse_source_closure_v1(data: bytes, artifact: str) -> _SourceClosureV1: + reader = _Reader(data, artifact) + if reader.exact(len(SOURCE_LOCK_MAGIC_V1)) != SOURCE_LOCK_MAGIC_V1: + _fail(reader.artifact, ProvenanceReasonV1.BAD_MAGIC, "source lock magic") + if reader.u8() != SOURCE_LOCK_RELEASE_V1: + _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_RELEASE, "source lock release") + if reader.u8() != SOURCE_CLOSURE_COUNT_V1: + _fail(reader.artifact, ProvenanceReasonV1.INVALID_FIELD, "source count") + result = ( + SourceReleaseLockV1.parse_from(reader), + SourceReleaseLockV1.parse_from(reader), + SourceReleaseLockV1.parse_from(reader), + ) + reader.finish() + return result + + @dataclass(frozen=True) class ArbSourceLockV1: - sources: tuple[SourceReleaseLockV1, SourceReleaseLockV1, SourceReleaseLockV1] + sources: _SourceClosureV1 def __post_init__(self) -> None: - if type(self.sources) is not tuple or len(self.sources) != ARBITRARY_PRECISION_SOURCE_COUNT_V1: + if type(self.sources) is not tuple or len(self.sources) != SOURCE_CLOSURE_COUNT_V1: _fail("arb-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source count") if any(type(value) is not SourceReleaseLockV1 for value in self.sources): _fail("arb-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source type") @@ -582,25 +646,60 @@ def __post_init__(self) -> None: ) def encode(self) -> bytes: - return ( - SOURCE_LOCK_MAGIC_V1 - + bytes((SOURCE_LOCK_RELEASE_V1, len(self.sources))) - + b"".join(source.encode() for source in self.sources) - ) + return _encode_source_closure_v1(self.sources) @classmethod def parse(cls, data: bytes) -> "ArbSourceLockV1": - reader = _Reader(data, "arb-source-lock-v1") - if reader.exact(len(SOURCE_LOCK_MAGIC_V1)) != SOURCE_LOCK_MAGIC_V1: - _fail(reader.artifact, ProvenanceReasonV1.BAD_MAGIC, "source lock magic") - if reader.u8() != SOURCE_LOCK_RELEASE_V1: - _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_RELEASE, "source lock release") - if reader.u8() != ARBITRARY_PRECISION_SOURCE_COUNT_V1: - _fail(reader.artifact, ProvenanceReasonV1.INVALID_FIELD, "source count") - result = cls(tuple(SourceReleaseLockV1.parse_from(reader) for _ in range(3))) - reader.finish() + result = cls(_parse_source_closure_v1(data, "arb-source-lock-v1")) if result.encode() != data: - _fail(reader.artifact, ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") + _fail("arb-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") + return result + + @cached_property + def identity(self) -> bytes: + return _identity(SOURCE_LOCK_ID_LABEL_V1, self.encode()) + + +@dataclass(frozen=True) +class MpfiSourceLockV1: + sources: _SourceClosureV1 + + def __post_init__(self) -> None: + if type(self.sources) is not tuple or len(self.sources) != SOURCE_CLOSURE_COUNT_V1: + _fail("mpfi-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source count") + if any(type(value) is not SourceReleaseLockV1 for value in self.sources): + _fail("mpfi-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source type") + if tuple(value.role for value in self.sources) != ( + SourceRoleV1.GMP, + SourceRoleV1.MPFR, + SourceRoleV1.MPFI, + ): + _fail( + "mpfi-source-lock-v1", + ProvenanceReasonV1.NONCANONICAL_ORDER, + "GMP, MPFR, MPFI", + ) + if any( + not isinstance(value.integrity, DetachedSignaturePolicyV1) + for value in self.sources[:2] + ) or not isinstance( + self.sources[2].integrity, + ProjectPinnedArchiveDigestPolicyV1, + ): + _fail( + "mpfi-source-lock-v1", + ProvenanceReasonV1.INTEGRITY_KIND_MISMATCH, + "integrity policy", + ) + + def encode(self) -> bytes: + return _encode_source_closure_v1(self.sources) + + @classmethod + def parse(cls, data: bytes) -> "MpfiSourceLockV1": + result = cls(_parse_source_closure_v1(data, "mpfi-source-lock-v1")) + if result.encode() != data: + _fail("mpfi-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") return result @cached_property @@ -618,6 +717,7 @@ class ArchiveFileV1: _SAFE_ARCHIVE_TOKEN = object() _ADMITTED_ARB_SOURCES_TOKEN = object() +_ADMITTED_MPFI_SOURCES_TOKEN = object() @dataclass(frozen=True, init=False) @@ -746,54 +846,109 @@ def source_archive_replay_coordinates_v1( ) +_SafeSourceClosureV1: TypeAlias = tuple[ + SafeSourceArchiveV1, + SafeSourceArchiveV1, + SafeSourceArchiveV1, +] + + +def _validate_admitted_source_closure_v1( + source_lock_identity: bytes, + sources: _SafeSourceClosureV1, + artifact: str, +) -> None: + _digest(source_lock_identity, artifact, "source_lock_identity") + 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(f"invalid admitted source tuple for {artifact}") + + +def _admitted_source_closure_identity_v1( + label: bytes, + source_lock_identity: bytes, + sources: _SafeSourceClosureV1, +) -> bytes: + 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 _identity(label, b"".join(chunks)) + + @dataclass(frozen=True, init=False) class AdmittedArbSourcesV1: """One ordered capability for the complete locked Arb dependency closure.""" source_lock_identity: bytes - sources: tuple[SafeSourceArchiveV1, SafeSourceArchiveV1, SafeSourceArchiveV1] + sources: _SafeSourceClosureV1 def __init__( self, source_lock_identity: bytes, - sources: tuple[ - SafeSourceArchiveV1, - SafeSourceArchiveV1, - SafeSourceArchiveV1, - ], + sources: _SafeSourceClosureV1, *, _token: object, ) -> None: if _token is not _ADMITTED_ARB_SOURCES_TOKEN: raise TypeError("AdmittedArbSourcesV1 is created only by source admission") - _digest( + _validate_admitted_source_closure_v1( source_lock_identity, + sources, "admitted-arb-sources-v1", - "source_lock_identity", ) - if ( - type(sources) is not tuple - or len(sources) != ARBITRARY_PRECISION_SOURCE_COUNT_V1 - or any(type(source) is not SafeSourceArchiveV1 for source in sources) - ): - raise TypeError("invalid admitted Arb source tuple") object.__setattr__(self, "source_lock_identity", source_lock_identity) object.__setattr__(self, "sources", sources) @cached_property def identity(self) -> bytes: - chunks = [self.source_lock_identity] - for ordinal, source in enumerate(self.sources): - chunks.extend( - ( - bytes((ordinal,)), - source.source_lock_identity, - source.archive_sha256, - source.tree_identity, - ) - ) - encoded = b"".join(chunks) - return _identity(ADMITTED_ARB_SOURCES_ID_LABEL_V1, encoded) + return _admitted_source_closure_identity_v1( + ADMITTED_ARB_SOURCES_ID_LABEL_V1, + self.source_lock_identity, + self.sources, + ) + + +@dataclass(frozen=True, init=False) +class AdmittedMpfiSourcesV1: + """One ordered capability for the complete locked MPFI dependency closure.""" + + source_lock_identity: bytes + sources: _SafeSourceClosureV1 + + def __init__( + self, + source_lock_identity: bytes, + sources: _SafeSourceClosureV1, + *, + _token: object, + ) -> None: + if _token is not _ADMITTED_MPFI_SOURCES_TOKEN: + raise TypeError("AdmittedMpfiSourcesV1 is created only by source admission") + _validate_admitted_source_closure_v1( + source_lock_identity, + sources, + "admitted-mpfi-sources-v1", + ) + object.__setattr__(self, "source_lock_identity", source_lock_identity) + object.__setattr__(self, "sources", sources) + + @cached_property + def identity(self) -> bytes: + return _admitted_source_closure_identity_v1( + ADMITTED_MPFI_SOURCES_ID_LABEL_V1, + self.source_lock_identity, + self.sources, + ) def _decompress_exact( @@ -1095,25 +1250,18 @@ def replay_admitted_source_archive_v1( return _admit_source_archive_once(expected, admitted.archive_bytes) -def admit_arb_sources( - expected: ArbSourceLockV1, - sources: tuple[ - SafeSourceArchiveV1, - SafeSourceArchiveV1, - SafeSourceArchiveV1, - ], -) -> AdmittedArbSourcesV1: - """Collapse three individually admitted archives into one ordered capability.""" - - if type(expected) is not ArbSourceLockV1: - raise TypeError("expected must be ArbSourceLockV1") +def _validate_source_capability_closure_v1( + expected_sources: _SourceClosureV1, + sources: _SafeSourceClosureV1, + artifact: str, +) -> None: if ( type(sources) is not tuple - or len(sources) != ARBITRARY_PRECISION_SOURCE_COUNT_V1 + 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): + for lock, source in zip(expected_sources, sources, strict=True): if ( source.source_lock_identity != lock.identity or source.archive_sha256 != lock.archive_sha256 @@ -1121,10 +1269,25 @@ def admit_arb_sources( or source.regular_file_bytes != lock.regular_file_bytes ): _fail( - "admitted-arb-sources-v1", + artifact, ProvenanceReasonV1.FOREIGN_BINDING, "source capability does not match ordered lock", ) + + +def admit_arb_sources( + expected: ArbSourceLockV1, + sources: _SafeSourceClosureV1, +) -> AdmittedArbSourcesV1: + """Collapse three individually admitted archives into one ordered 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, sources, @@ -1132,14 +1295,32 @@ def admit_arb_sources( ) +def admit_mpfi_sources( + expected: MpfiSourceLockV1, + sources: _SafeSourceClosureV1, +) -> AdmittedMpfiSourcesV1: + """Collapse the exact MPFI source closure into one ordered 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, + sources, + _token=_ADMITTED_MPFI_SOURCES_TOKEN, + ) + + def _legal_file(path: str, length: int, digest_hex: str) -> LegalFileV1: return LegalFileV1(path, length, bytes.fromhex(digest_hex)) -def arb_source_lock_v1() -> ArbSourceLockV1: - """Return the exact published source declarations for the first Arb lane.""" - - gmp = SourceReleaseLockV1( +def _gmp_source_release_v1() -> SourceReleaseLockV1: + return SourceReleaseLockV1( SourceRoleV1.GMP, "6.3.0", "https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz", @@ -1165,7 +1346,10 @@ def arb_source_lock_v1() -> ArbSourceLockV1: bytes.fromhex("343c2ff0fbee5ec2edbef399f3599ff828c67298"), ), ) - mpfr = SourceReleaseLockV1( + + +def _mpfr_source_release_v1() -> SourceReleaseLockV1: + return SourceReleaseLockV1( SourceRoleV1.MPFR, "4.2.2", "https://www.mpfr.org/mpfr-4.2.2/mpfr-4.2.2.tar.xz", @@ -1189,6 +1373,13 @@ def arb_source_lock_v1() -> ArbSourceLockV1: bytes.fromhex("a534be3f83e241d918280aeb5831d11a0d4db02a"), ), ) + + +def arb_source_lock_v1() -> ArbSourceLockV1: + """Return the exact published source declarations for the first Arb lane.""" + + gmp = _gmp_source_release_v1() + mpfr = _mpfr_source_release_v1() omitted = ( ".gitattributes", ".github/ISSUE_TEMPLATE/bug_report.md", @@ -1264,3 +1455,48 @@ def arb_source_lock_v1() -> ArbSourceLockV1: ), ) return ArbSourceLockV1((gmp, mpfr, flint)) + + +def mpfi_source_lock_v1() -> MpfiSourceLockV1: + """Return the exact source declarations for the first MPFI lane. + + MPFI 1.5.4 has no verified detached signature or archive-to-Git content + relation. Its archive is therefore named honestly as a project-pinned + byte digest while GMP and MPFR retain their independently signed locks. + """ + + mpfi = SourceReleaseLockV1( + SourceRoleV1.MPFI, + "1.5.4", + "https://perso.ens-lyon.fr/nathalie.revol/softwares/mpfi-1.5.4.tar.xz", + ArchiveFormatV1.TAR_XZ, + 370_932, + bytes.fromhex( + "819e98bc7dad7cf7e67c9ddb592f44545c300de143fe30bc29ca1b422b55306a" + ), + 3_502_080, + "mpfi-1.5.4/", + 495, + 3_117_639, + ( + _legal_file( + "COPYING", + 35_147, + "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903", + ), + _legal_file( + "COPYING.LESSER", + 7_651, + "da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768", + ), + _legal_file( + "README", + 1_336, + "dab7a52115f111ff3771dc4311a837919d45ffaa654e64c110af78bd2a003e20", + ), + ), + ProjectPinnedArchiveDigestPolicyV1(), + ) + return MpfiSourceLockV1( + (_gmp_source_release_v1(), _mpfr_source_release_v1(), mpfi) + ) diff --git a/proof/region/v1/tests/test_mpfi_source_lock.py b/proof/region/v1/tests/test_mpfi_source_lock.py new file mode 100644 index 00000000..0b9c1d5f --- /dev/null +++ b/proof/region/v1/tests/test_mpfi_source_lock.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""Hostile MPFI source-lock and ordered-capability tests for proof V1.""" + +from __future__ import annotations + +import hashlib +import io +import lzma +import sys +import tarfile +import unittest +from dataclasses import replace +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import provenance # noqa: E402 +from provenance import ( # noqa: E402 + AdmittedMpfiSourcesV1, + ArchiveFormatV1, + DetachedSignaturePolicyV1, + LegalFileV1, + MpfiSourceLockV1, + ProjectPinnedArchiveDigestPolicyV1, + ProvenanceErrorV1, + ProvenanceReasonV1, + SourceReleaseLockV1, + SourceRoleV1, + admit_mpfi_sources, + admit_source_archive, + arb_source_lock_v1, + mpfi_source_lock_v1, +) + + +def sha256(value: bytes) -> bytes: + return hashlib.sha256(value).digest() + + +def fixture_archive() -> bytes: + 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 in (("LICENSE", b"license"), ("value", b"data")): + member = tarfile.TarInfo(f"fixture-1/{name}") + member.mode = 0o644 + member.size = len(body) + archive.addfile(member, io.BytesIO(body)) + return lzma.compress(raw.getvalue(), format=lzma.FORMAT_XZ) + + +def fixture_release( + role: SourceRoleV1, + archive: bytes, + integrity: DetachedSignaturePolicyV1 | ProjectPinnedArchiveDigestPolicyV1, +) -> SourceReleaseLockV1: + raw_tar = lzma.decompress(archive) + return SourceReleaseLockV1( + role, + "1", + "https://example.invalid/fixture-1.tar.xz", + ArchiveFormatV1.TAR_XZ, + len(archive), + sha256(archive), + len(raw_tar), + "fixture-1/", + 2, + 11, + (LegalFileV1("LICENSE", 7, sha256(b"license")),), + integrity, + ) + + +def detached_policy() -> DetachedSignaturePolicyV1: + return DetachedSignaturePolicyV1( + "https://example.invalid/fixture-1.tar.xz.sig", + 3, + sha256(b"sig"), + sha256(b"packets"), + bytes.fromhex("00112233445566778899aabbccddeeff00112233"), + ) + + +class MpfiSourceLockTests(unittest.TestCase): + def test_lane_specific_capabilities_have_no_generic_public_aggregate(self) -> None: + self.assertFalse(hasattr(provenance, "SourceClosureV1")) + self.assertFalse(hasattr(provenance, "SafeSourceClosureV1")) + + def test_exact_primary_coordinates_are_canonical_and_round_trip(self) -> None: + lock = mpfi_source_lock_v1() + self.assertEqual( + tuple(source.role for source in lock.sources), + (SourceRoleV1.GMP, SourceRoleV1.MPFR, SourceRoleV1.MPFI), + ) + + mpfi = lock.sources[2] + self.assertEqual(mpfi.version, "1.5.4") + self.assertEqual( + mpfi.archive_url, + "https://perso.ens-lyon.fr/nathalie.revol/softwares/mpfi-1.5.4.tar.xz", + ) + self.assertIs(mpfi.archive_format, ArchiveFormatV1.TAR_XZ) + self.assertEqual(mpfi.archive_length, 370_932) + self.assertEqual( + mpfi.archive_sha256.hex(), + "819e98bc7dad7cf7e67c9ddb592f44545c300de143fe30bc29ca1b422b55306a", + ) + self.assertEqual( + mpfi.identity.hex(), + "66289ae877f4526f992e4ffe5143433f6e17d73acfaeb936482ae4b797b876fb", + ) + self.assertEqual(mpfi.tar_stream_length, 3_502_080) + self.assertEqual(mpfi.root_prefix, "mpfi-1.5.4/") + self.assertEqual(mpfi.regular_file_count, 495) + self.assertEqual(mpfi.regular_file_bytes, 3_117_639) + self.assertEqual( + tuple((item.path, item.length, item.sha256.hex()) for item in mpfi.legal_files), + ( + ( + "COPYING", + 35_147, + "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903", + ), + ( + "COPYING.LESSER", + 7_651, + "da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768", + ), + ( + "README", + 1_336, + "dab7a52115f111ff3771dc4311a837919d45ffaa654e64c110af78bd2a003e20", + ), + ), + ) + self.assertIs(type(mpfi.integrity), ProjectPinnedArchiveDigestPolicyV1) + encoded = lock.encode() + self.assertEqual(len(encoded), 1_339) + self.assertEqual( + lock.identity.hex(), + "03636d1f8c6c4950ba74943cf148d65b596d23a078391eedf6c7606c8613f830", + ) + self.assertEqual(MpfiSourceLockV1.parse(encoded), lock) + self.assertEqual(MpfiSourceLockV1.parse(encoded).encode(), encoded) + + def test_shared_sources_have_one_declaration_but_aggregate_lanes_differ(self) -> None: + arb = arb_source_lock_v1() + mpfi = mpfi_source_lock_v1() + + self.assertEqual(arb.sources[:2], mpfi.sources[:2]) + self.assertNotEqual(arb.sources[2], mpfi.sources[2]) + self.assertNotEqual(arb.identity, mpfi.identity) + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.ArbSourceLockV1.parse(mpfi.encode()) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.NONCANONICAL_ORDER) + with self.assertRaises(ProvenanceErrorV1) as caught: + MpfiSourceLockV1.parse(arb.encode()) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.NONCANONICAL_ORDER) + + def test_digest_only_policy_is_explicit_and_identity_bound(self) -> None: + archive = fixture_archive() + policy = ProjectPinnedArchiveDigestPolicyV1() + gmp = fixture_release(SourceRoleV1.GMP, archive, detached_policy()) + mpfr = fixture_release(SourceRoleV1.MPFR, archive, detached_policy()) + mpfi = fixture_release(SourceRoleV1.MPFI, archive, policy) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + + encoded = lock.encode() + self.assertEqual(MpfiSourceLockV1.parse(encoded), lock) + for kind, reason in ( + (1, ProvenanceReasonV1.TRUNCATED), + (2, ProvenanceReasonV1.TRUNCATED), + (255, ProvenanceReasonV1.UNKNOWN_ENUM), + ): + with self.subTest(kind=kind): + with self.assertRaises(ProvenanceErrorV1) as caught: + MpfiSourceLockV1.parse(encoded[:-1] + bytes((kind,))) + self.assertEqual(caught.exception.reason, reason) + self.assertNotEqual( + lock.identity, + MpfiSourceLockV1((gmp, mpfr, replace(mpfi, version="2"))).identity, + ) + with self.assertRaises(ProvenanceErrorV1) as caught: + MpfiSourceLockV1((gmp, mpfr, replace(mpfi, integrity=detached_policy()))) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.INTEGRITY_KIND_MISMATCH, + ) + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.ArbSourceLockV1((gmp, mpfr, mpfi)) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.NONCANONICAL_ORDER, + ) + + def test_three_locked_sources_become_one_mpfi_capability(self) -> None: + archive = fixture_archive() + gmp = fixture_release(SourceRoleV1.GMP, archive, detached_policy()) + mpfr = fixture_release(SourceRoleV1.MPFR, archive, detached_policy()) + mpfi = fixture_release( + SourceRoleV1.MPFI, + archive, + ProjectPinnedArchiveDigestPolicyV1(), + ) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + sources = tuple( + admit_source_archive(expected, archive) for expected in lock.sources + ) + + admitted = admit_mpfi_sources(lock, sources) + + self.assertIs(type(admitted), AdmittedMpfiSourcesV1) + self.assertEqual(admitted.source_lock_identity, lock.identity) + self.assertEqual(admitted.sources, sources) + with self.assertRaises((ProvenanceErrorV1, TypeError)): + admit_mpfi_sources(lock, (sources[1], sources[0], sources[2])) + with self.assertRaises(TypeError): + AdmittedMpfiSourcesV1(lock.identity, sources, _token=object()) + + 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("не приписывает этот digest издателю", reference) + self.assertIn("не заявляет publisher authentication", reference) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 8a63b2321aabf11d97b0ca9977152cb6802840bf Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:02:08 +0300 Subject: [PATCH 2/3] =?UTF-8?q?Proof:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20MPFI=20source=20admission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/provenance.py | 6 +++--- proof/region/v1/tests/test_mpfi_source_lock.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/proof/region/v1/provenance.py b/proof/region/v1/provenance.py index 0721def4..aa0f0429 100644 --- a/proof/region/v1/provenance.py +++ b/proof/region/v1/provenance.py @@ -432,7 +432,7 @@ def encode_payload(self) -> bytes: return b"" @classmethod - def parse_from(cls, _reader: _Reader) -> "ProjectPinnedArchiveDigestPolicyV1": + def parse_from(cls, _reader: _Reader) -> ProjectPinnedArchiveDigestPolicyV1: return cls() @@ -649,7 +649,7 @@ def encode(self) -> bytes: return _encode_source_closure_v1(self.sources) @classmethod - def parse(cls, data: bytes) -> "ArbSourceLockV1": + def parse(cls, data: bytes) -> ArbSourceLockV1: result = cls(_parse_source_closure_v1(data, "arb-source-lock-v1")) if result.encode() != data: _fail("arb-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") @@ -696,7 +696,7 @@ def encode(self) -> bytes: return _encode_source_closure_v1(self.sources) @classmethod - def parse(cls, data: bytes) -> "MpfiSourceLockV1": + def parse(cls, data: bytes) -> MpfiSourceLockV1: result = cls(_parse_source_closure_v1(data, "mpfi-source-lock-v1")) if result.encode() != data: _fail("mpfi-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") diff --git a/proof/region/v1/tests/test_mpfi_source_lock.py b/proof/region/v1/tests/test_mpfi_source_lock.py index 0b9c1d5f..e601a904 100644 --- a/proof/region/v1/tests/test_mpfi_source_lock.py +++ b/proof/region/v1/tests/test_mpfi_source_lock.py @@ -217,8 +217,9 @@ def test_three_locked_sources_become_one_mpfi_capability(self) -> None: self.assertIs(type(admitted), AdmittedMpfiSourcesV1) self.assertEqual(admitted.source_lock_identity, lock.identity) self.assertEqual(admitted.sources, sources) - with self.assertRaises((ProvenanceErrorV1, TypeError)): + with self.assertRaises(ProvenanceErrorV1) as caught: admit_mpfi_sources(lock, (sources[1], sources[0], sources[2])) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) with self.assertRaises(TypeError): AdmittedMpfiSourcesV1(lock.identity, sources, _token=object()) From 443d0a8409f5a01e889f28abc5a5027a1ffecc41 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:52:04 +0300 Subject: [PATCH 3/3] =?UTF-8?q?Docs:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BB=D0=B8=D0=BD=D1=83=20source-lo?= =?UTF-8?q?ck=20wire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proof/region/v1/PROTOCOL.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index 4cb1cc85..609982b4 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -34,11 +34,13 @@ receipt, cross-path dependency overlap и diversity не представлен ## Wire и identity -Все целые беззнаковые и записаны big-endian как `u8`, `u32be` или `u64be`. -`digest` — ровно 32 ненулевых bytes SHA-256. `blob` равен -`u64be(length) || bytes`. Enum занимает один `u8` и принимает только -перечисленные значения. Padding, alignment, reserved fields и trailing bytes -отсутствуют. +Для wire-artifact-ов из `region_proof_protocol.py` все целые беззнаковые и +записаны big-endian как `u8`, `u32be` или `u64be`; `digest` — ровно 32 +ненулевых bytes SHA-256, а `blob` равен `u64be(length) || bytes`. +`SourceReleaseLockV1` и связанные provenance-artifact-ы имеют отдельный codec +в `provenance.py`: его `blob` равен `u32be(length) || bytes`. Enum занимает +один `u8` и принимает только перечисленные значения. Padding, alignment, +reserved fields и trailing bytes отсутствуют. До allocation и цикла по records parser проверяет арифметику длины без переполнения, остаток input, точный или минимальный wire-размер всех