From 03261148771e89df35c9050559685b328f98a197 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 16:49:51 +0900 Subject: [PATCH 1/2] test(api): reproduce missing OpenAPI release provenance --- scripts/test_openapi_release_provenance.py | 125 +++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 scripts/test_openapi_release_provenance.py diff --git a/scripts/test_openapi_release_provenance.py b/scripts/test_openapi_release_provenance.py new file mode 100644 index 000000000..dd905d0b1 --- /dev/null +++ b/scripts/test_openapi_release_provenance.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Specify deterministic release provenance for the repository-owned OpenAPI contract.""" + +from __future__ import annotations + +import hashlib +import json +import tempfile +import unittest +from pathlib import Path + +from openapi_release_provenance import ( + CONTRACT_RELATIVE_PATH, + MAX_CONTRACT_BYTES, + build_openapi_release_provenance, + render_openapi_release_provenance, +) + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + + +class OpenApiReleaseProvenanceTest(unittest.TestCase): + """Bind the exact public API bytes to one exact protected release identity.""" + + def test_builds_deterministic_digest_bound_record(self) -> None: + """Record exact contract bytes and source revision without environment metadata.""" + + contract_bytes = b"openapi: 3.0.3\ninfo:\n version: 0.1.0\n" + revision = "0123456789abcdef0123456789abcdef01234567" + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + contract_path = root / CONTRACT_RELATIVE_PATH + contract_path.parent.mkdir(parents=True) + contract_path.write_bytes(contract_bytes) + + provenance = build_openapi_release_provenance(root, revision) + + self.assertEqual( + { + "format": "clearfolio.openapi.release-provenance/v1", + "contract": CONTRACT_RELATIVE_PATH.as_posix(), + "sha256": hashlib.sha256(contract_bytes).hexdigest(), + "sizeBytes": len(contract_bytes), + "sourceRevision": revision, + }, + provenance, + ) + + def test_rendering_is_canonical_and_reproducible(self) -> None: + """Produce one stable JSON representation suitable for signing or packaging.""" + + record = { + "sourceRevision": "0123456789abcdef0123456789abcdef01234567", + "sizeBytes": 12, + "sha256": "a" * 64, + "contract": CONTRACT_RELATIVE_PATH.as_posix(), + "format": "clearfolio.openapi.release-provenance/v1", + } + + rendered = render_openapi_release_provenance(record) + + self.assertEqual(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n", rendered) + self.assertNotIn(" ", rendered) + + def test_rejects_noncanonical_source_revision(self) -> None: + """Fail closed instead of binding provenance to ambiguous source identity.""" + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + contract_path = root / CONTRACT_RELATIVE_PATH + contract_path.parent.mkdir(parents=True) + contract_path.write_text("openapi: 3.0.3\n", encoding="utf-8") + + for revision in ( + "", + "abc", + "G" * 40, + "A" * 40, + "0" * 39, + "0" * 41, + ): + with self.subTest(revision=revision): + with self.assertRaisesRegex(ValueError, "source revision must be lowercase 40-hex"): + build_openapi_release_provenance(root, revision) + + def test_rejects_missing_symlinked_and_oversized_contracts(self) -> None: + """Hash only the bounded repository-owned regular file selected by the contract.""" + + revision = "f" * 40 + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + contract_path = root / CONTRACT_RELATIVE_PATH + + with self.assertRaisesRegex(ValueError, "OpenAPI contract must be a regular file"): + build_openapi_release_provenance(root, revision) + + contract_path.parent.mkdir(parents=True) + target = root / "external.yaml" + target.write_text("openapi: 3.0.3\n", encoding="utf-8") + contract_path.symlink_to(target) + with self.assertRaisesRegex(ValueError, "OpenAPI contract must be a regular file"): + build_openapi_release_provenance(root, revision) + + contract_path.unlink() + contract_path.write_bytes(b"x" * (MAX_CONTRACT_BYTES + 1)) + with self.assertRaisesRegex(ValueError, "OpenAPI contract exceeds release provenance limit"): + build_openapi_release_provenance(root, revision) + + def test_current_repository_contract_is_hashable_without_normalization(self) -> None: + """Preserve the byte-exact checked-in OpenAPI artifact as the provenance authority.""" + + source_revision = "0" * 40 + contract_bytes = (REPOSITORY_ROOT / CONTRACT_RELATIVE_PATH).read_bytes() + + provenance = build_openapi_release_provenance(REPOSITORY_ROOT, source_revision) + + self.assertEqual(hashlib.sha256(contract_bytes).hexdigest(), provenance["sha256"]) + self.assertEqual(len(contract_bytes), provenance["sizeBytes"]) + self.assertEqual(source_revision, provenance["sourceRevision"]) + + +if __name__ == "__main__": + unittest.main() From ad97e7918c24fdd2f88100d33e79832a6d81399f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 16:55:12 +0900 Subject: [PATCH 2/2] feat(api): bind OpenAPI bytes to release provenance --- scripts/openapi_release_provenance.py | 99 +++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts/openapi_release_provenance.py diff --git a/scripts/openapi_release_provenance.py b/scripts/openapi_release_provenance.py new file mode 100644 index 000000000..f83b87495 --- /dev/null +++ b/scripts/openapi_release_provenance.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Build byte-exact release provenance for Clearfolio's public OpenAPI contract.""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import stat +from pathlib import Path +from typing import Mapping + + +CONTRACT_RELATIVE_PATH = Path("docs/deployment/clearfolio-buyer-connector.openapi.yaml") +"""Repository-relative location of the buyer-facing OpenAPI contract.""" + +MAX_CONTRACT_BYTES = 1024 * 1024 +"""Maximum OpenAPI contract size accepted by release provenance generation.""" + +_SOURCE_REVISION_PATTERN = re.compile(r"[0-9a-f]{40}") +_READ_CHUNK_BYTES = 64 * 1024 + + +def _read_bounded_regular_file(path: Path) -> bytes: + """Read one bounded regular file while rejecting symlink authority.""" + + try: + link_stat = path.lstat() + except OSError as exc: + raise ValueError("OpenAPI contract must be a regular file") from exc + if not stat.S_ISREG(link_stat.st_mode): + raise ValueError("OpenAPI contract must be a regular file") + if link_stat.st_size > MAX_CONTRACT_BYTES: + raise ValueError("OpenAPI contract exceeds release provenance limit") + + flags = os.O_RDONLY + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + descriptor = os.open(path, flags) + except OSError as exc: + raise ValueError("OpenAPI contract must be a regular file") from exc + + try: + opened_stat = os.fstat(descriptor) + if not stat.S_ISREG(opened_stat.st_mode): + raise ValueError("OpenAPI contract must be a regular file") + if opened_stat.st_size > MAX_CONTRACT_BYTES: + raise ValueError("OpenAPI contract exceeds release provenance limit") + + chunks: list[bytes] = [] + remaining = MAX_CONTRACT_BYTES + 1 + while remaining > 0: + chunk = os.read(descriptor, min(_READ_CHUNK_BYTES, remaining)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + contract_bytes = b"".join(chunks) + if len(contract_bytes) > MAX_CONTRACT_BYTES: + raise ValueError("OpenAPI contract exceeds release provenance limit") + return contract_bytes + finally: + os.close(descriptor) + + +def build_openapi_release_provenance(repository_root: Path, source_revision: str) -> dict[str, object]: + """Bind exact OpenAPI bytes to one canonical Git source revision. + + Args: + repository_root: Root directory containing the repository checkout. + source_revision: Lowercase 40-hex Git commit identifier for the release source. + + Returns: + Canonical release-provenance fields suitable for deterministic rendering. + + Raises: + ValueError: If source identity or contract-file authority is invalid. + """ + + if not isinstance(source_revision, str) or _SOURCE_REVISION_PATTERN.fullmatch(source_revision) is None: + raise ValueError("source revision must be lowercase 40-hex") + + root = Path(repository_root) + contract_bytes = _read_bounded_regular_file(root / CONTRACT_RELATIVE_PATH) + return { + "format": "clearfolio.openapi.release-provenance/v1", + "contract": CONTRACT_RELATIVE_PATH.as_posix(), + "sha256": hashlib.sha256(contract_bytes).hexdigest(), + "sizeBytes": len(contract_bytes), + "sourceRevision": source_revision, + } + + +def render_openapi_release_provenance(record: Mapping[str, object]) -> str: + """Render a provenance record as stable compact JSON with one trailing newline.""" + + return json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n"