Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions scripts/openapi_release_provenance.py
Original file line number Diff line number Diff line change
@@ -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"
125 changes: 125 additions & 0 deletions scripts/test_openapi_release_provenance.py
Original file line number Diff line number Diff line change
@@ -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()
Loading