From 46cf5fa2838d87046636e6151d2e63984155b229 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:12:59 +0900 Subject: [PATCH 01/47] test(perf-attestation): require authenticated reusable workflow --- ...roduct_performance_attestation_contract.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/test_product_performance_attestation_contract.py diff --git a/tests/test_product_performance_attestation_contract.py b/tests/test_product_performance_attestation_contract.py new file mode 100644 index 0000000000..3aac1776fe --- /dev/null +++ b/tests/test_product_performance_attestation_contract.py @@ -0,0 +1,95 @@ +"""Contracts for the organization-owned product performance attestation workflow.""" + +from pathlib import Path + +WORKFLOW = Path(".github/workflows/product-performance-attestation.yml") +VERIFIER = Path("scripts/ci/verify_product_performance_evidence.py") +ATTEST_ACTION_PIN = "actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26" +DOWNLOAD_ACTION_PIN = ( + "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" +) +UPLOAD_ACTION_PIN = ( + "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" +) +PREDICATE_TYPE = "https://contextualwisdomlab.org/attestations/product-performance/v1" + + +def _text(path: Path) -> str: + """Read one required UTF-8 repository file.""" + assert path.is_file(), f"required file is missing: {path}" + return path.read_text(encoding="utf-8") + + +def test_reusable_workflow_has_explicit_performance_handoff_contract() -> None: + """Require exact caller, artifact, evidence, profile, and predicate inputs.""" + workflow = _text(WORKFLOW) + + assert "workflow_call:" in workflow + for name in ( + "source_repository", + "source_sha", + "evidence_artifact_id", + "evidence_artifact_name", + "evidence_artifact_digest", + "result_filename", + "result_sha256", + "runtime_evidence_filename", + "runtime_evidence_sha256", + "fixture_filename", + "fixture_sha256", + "performance_profile", + "predicate_type", + ): + assert f" {name}:" in workflow + assert PREDICATE_TYPE in workflow + + +def test_reusable_workflow_uses_oidc_callee_identity_before_trusted_checkout() -> None: + """Keep caller workflow identity from selecting central verifier source.""" + workflow = _text(WORKFLOW) + + assert "ref: ${{ github.workflow_sha }}" not in workflow + assert workflow.count("Resolve exact called reusable workflow identity") == 2 + assert workflow.count("job_workflow_ref") >= 2 + assert workflow.count("job_workflow_sha") >= 2 + assert workflow.count("id-token: write") >= 2 + assert workflow.count("ref: ${{ steps.workflow-identity.outputs.workflow_sha }}") >= 2 + assert workflow.count("product-performance-attestation.yml@") >= 2 + + +def test_reusable_workflow_rechecks_same_run_artifact_and_never_executes_evidence() -> None: + """Treat caller performance evidence as inert bounded data in both jobs.""" + workflow = _text(WORKFLOW) + verifier = _text(VERIFIER) + + assert workflow.count("/actions/artifacts/${ARTIFACT_ID}") >= 2 + assert workflow.count(".workflow_run.id") >= 2 + assert workflow.count("artifact-ids: ${{ inputs.evidence_artifact_id }}") >= 2 + assert workflow.count(DOWNLOAD_ACTION_PIN) >= 2 + assert workflow.count("verify_product_performance_evidence.py") >= 2 + assert "subprocess" not in verifier + assert "os.system" not in verifier + assert "exec(" not in verifier + assert "eval(" not in verifier + assert "importlib" not in verifier + assert "zipfile" not in verifier + assert "tarfile" not in verifier + + +def test_signer_attests_result_with_versioned_custom_predicate_and_offline_bundle() -> None: + """Bind exact result bytes to runtime and fixture evidence without claiming a latency verdict.""" + workflow = _text(WORKFLOW) + + assert workflow.count(ATTEST_ACTION_PIN) == 1 + assert "subject-name: ${{ inputs.result_filename }}" in workflow + assert "subject-digest: sha256:${{ inputs.result_sha256 }}" in workflow + assert "predicate-type: ${{ inputs.predicate_type }}" in workflow + assert "predicate-path:" in workflow + assert "gh attestation verify" in workflow + assert "--signer-repo" in workflow + assert "--signer-workflow" in workflow + assert "--source-digest" in workflow + assert "--predicate-type" in workflow + assert "gh attestation trusted-root" in workflow + assert UPLOAD_ACTION_PIN in workflow + assert "does not prove" in workflow From a2721f8e3ad305b13eb2e418d562b34b7d19e05a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:15:27 +0900 Subject: [PATCH 02/47] test(perf-attestation): define sealed evidence verifier contract --- ...t_product_performance_evidence_verifier.py | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 tests/test_product_performance_evidence_verifier.py diff --git a/tests/test_product_performance_evidence_verifier.py b/tests/test_product_performance_evidence_verifier.py new file mode 100644 index 0000000000..45731b5a72 --- /dev/null +++ b/tests/test_product_performance_evidence_verifier.py @@ -0,0 +1,281 @@ +"""Unit contracts for inert product-performance evidence verification.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path + +import pytest + +from scripts.ci import verify_product_performance_evidence as verifier + +PREDICATE_TYPE = "https://contextualwisdomlab.org/attestations/product-performance/v1" +SOURCE_SHA = "a" * 40 +ARTIFACT_DIGEST = "sha256:" + "b" * 64 + + +def _digest(path: Path) -> str: + """Return the SHA-256 digest of one fixture file.""" + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _write_json(path: Path, value: object) -> None: + """Write deterministic UTF-8 JSON for one test fixture.""" + path.write_text(json.dumps(value, sort_keys=True), encoding="utf-8") + + +def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: + """Create one valid verifier namespace for the three-file fixture.""" + result = root / "result.json" + runtime = root / "runtime.json" + fixture = root / "fixture.json" + return argparse.Namespace( + source_repository="ContextualWisdomLab/Orgmetra", + source_sha=SOURCE_SHA, + workflow_run_id="123456", + evidence_artifact_id="789", + evidence_artifact_name="orgmetra-performance-evidence", + evidence_artifact_digest=ARTIFACT_DIGEST, + evidence_root=str(root), + result_filename=result.name, + result_sha256=_digest(result), + runtime_evidence_filename=runtime.name, + runtime_evidence_sha256=_digest(runtime), + fixture_filename=fixture.name, + fixture_sha256=_digest(fixture), + performance_profile="first_commit", + predicate_type=PREDICATE_TYPE, + output_predicate=str(tmp_path / "predicate.json"), + output_manifest=str(tmp_path / "manifest.json"), + ) + + +@pytest.fixture +def evidence(tmp_path: Path) -> tuple[Path, argparse.Namespace]: + """Create a valid three-file inert evidence set and verifier arguments.""" + root = tmp_path / "evidence" + root.mkdir() + _write_json(root / "result.json", {"metrics": {"p95_ms": 12.3}}) + _write_json(root / "runtime.json", {"runner": "k6", "cpu": "observed"}) + _write_json(root / "fixture.json", {"clearance": "right-cleared", "records": [1]}) + return root, _arguments(root, tmp_path) + + +def test_verify_binds_exact_three_file_evidence(evidence: tuple[Path, argparse.Namespace]) -> None: + """Emit deterministic manifest and predicate bound to the exact evidence bytes.""" + root, arguments = evidence + + manifest = verifier.verify(arguments) + predicate = json.loads(Path(arguments.output_predicate).read_text(encoding="utf-8")) + + assert manifest["result"] == "PASS" + assert manifest["source_repository"] == arguments.source_repository + assert manifest["source_sha"] == SOURCE_SHA + assert manifest["performance_profile"] == "first_commit" + assert [item["filename"] for item in manifest["files"]] == [ + "fixture.json", + "result.json", + "runtime.json", + ] + assert predicate == { + "attestation_claim": "origin_and_integrity_only", + "does_not_prove": [ + "latency_threshold_passed", + "production_equivalence", + "fixture_scientific_validity", + "fixture_right_clearance", + ], + "evidence": { + "artifact_digest": ARTIFACT_DIGEST, + "artifact_id": "789", + "artifact_name": "orgmetra-performance-evidence", + "fixture": {"filename": "fixture.json", "sha256": _digest(root / "fixture.json")}, + "result": {"filename": "result.json", "sha256": _digest(root / "result.json")}, + "runtime": {"filename": "runtime.json", "sha256": _digest(root / "runtime.json")}, + }, + "performance_profile": "first_commit", + "predicate_type": PREDICATE_TYPE, + "schema_version": "1.0", + "source_repository": "ContextualWisdomLab/Orgmetra", + "source_sha": SOURCE_SHA, + "workflow_run_id": "123456", + } + assert json.loads(Path(arguments.output_manifest).read_text(encoding="utf-8")) == manifest + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("source_repository", "bad repo", "source repository"), + ("source_sha", "A" * 40, "source SHA"), + ("workflow_run_id", "0", "workflow run ID"), + ("evidence_artifact_id", "abc", "artifact ID"), + ("evidence_artifact_name", "", "artifact name"), + ("evidence_artifact_digest", "b" * 64, "artifact digest"), + ("performance_profile", "UPPER", "performance profile"), + ("predicate_type", "https://example.invalid/v1", "predicate type"), + ("result_filename", "../result.json", "result filename"), + ("runtime_evidence_filename", "runtime\\evil.json", "runtime evidence filename"), + ("fixture_filename", "..", "fixture filename"), + ("result_sha256", "A" * 64, "result SHA-256"), + ("runtime_evidence_sha256", "x" * 64, "runtime evidence SHA-256"), + ("fixture_sha256", "1", "fixture SHA-256"), + ], +) +def test_verify_rejects_invalid_control_fields( + evidence: tuple[Path, argparse.Namespace], field: str, value: str, message: str +) -> None: + """Reject malformed control-plane identities before publishing a receipt.""" + _, arguments = evidence + setattr(arguments, field, value) + + with pytest.raises(verifier.EvidenceError, match=message): + verifier.verify(arguments) + + +def test_verify_rejects_duplicate_filenames(evidence: tuple[Path, argparse.Namespace]) -> None: + """Require result, runtime, and fixture to remain three distinct members.""" + _, arguments = evidence + arguments.fixture_filename = arguments.result_filename + arguments.fixture_sha256 = arguments.result_sha256 + + with pytest.raises(verifier.EvidenceError, match="distinct"): + verifier.verify(arguments) + + +def test_verify_rejects_missing_and_extra_members(evidence: tuple[Path, argparse.Namespace]) -> None: + """Fail closed when the artifact cardinality differs from the three-file contract.""" + root, arguments = evidence + (root / "extra.json").write_text("{}", encoding="utf-8") + + with pytest.raises(verifier.EvidenceError, match="cardinality mismatch"): + verifier.verify(arguments) + + (root / "extra.json").unlink() + (root / "runtime.json").unlink() + with pytest.raises(verifier.EvidenceError, match="cardinality mismatch"): + verifier.verify(arguments) + + +def test_verify_rejects_symlink_member(evidence: tuple[Path, argparse.Namespace]) -> None: + """Do not follow an evidence-member symlink even when its target is regular.""" + root, arguments = evidence + target = root / "runtime-target.json" + (root / "runtime.json").rename(target) + (root / "runtime.json").symlink_to(target.name) + + with pytest.raises(verifier.EvidenceError, match="non-regular"): + verifier.verify(arguments) + + +def test_verify_rejects_symlinked_root_ancestor(tmp_path: Path) -> None: + """Reject evidence roots reached through a symbolic-link ancestor.""" + real_parent = tmp_path / "real" + root = real_parent / "evidence" + root.mkdir(parents=True) + _write_json(root / "result.json", {}) + _write_json(root / "runtime.json", {}) + _write_json(root / "fixture.json", {}) + linked_parent = tmp_path / "linked" + linked_parent.symlink_to(real_parent, target_is_directory=True) + arguments = _arguments(linked_parent / "evidence", tmp_path) + + with pytest.raises(verifier.EvidenceError, match="ancestor"): + verifier.verify(arguments) + + +def test_verify_rejects_digest_mismatch(evidence: tuple[Path, argparse.Namespace]) -> None: + """Reject replacement bytes even when the filename remains unchanged.""" + root, arguments = evidence + _write_json(root / "result.json", {"substituted": True}) + + with pytest.raises(verifier.EvidenceError, match="digest mismatch"): + verifier.verify(arguments) + + +@pytest.mark.parametrize( + ("filename", "payload", "message"), + [ + ("result.json", b'{"a":1,"a":2}', "duplicate JSON property"), + ("runtime.json", b'{"x":NaN}', "non-finite JSON number"), + ("fixture.json", b"\x80", "invalid UTF-8"), + ("result.json", b"[]", "JSON object"), + ("runtime.json", b"{", "invalid JSON"), + ], +) +def test_verify_rejects_malformed_json_members( + evidence: tuple[Path, argparse.Namespace], filename: str, payload: bytes, message: str +) -> None: + """Reject malformed or non-object JSON even when its external digest is updated.""" + root, arguments = evidence + path = root / filename + path.write_bytes(payload) + digest = _digest(path) + if filename == "result.json": + arguments.result_sha256 = digest + elif filename == "runtime.json": + arguments.runtime_evidence_sha256 = digest + else: + arguments.fixture_sha256 = digest + + with pytest.raises(verifier.EvidenceError, match=message): + verifier.verify(arguments) + + +def test_verify_rejects_oversized_json_member( + evidence: tuple[Path, argparse.Namespace], monkeypatch: pytest.MonkeyPatch +) -> None: + """Bound inert JSON evidence without changing the commercial sample semantics.""" + root, arguments = evidence + monkeypatch.setattr(verifier, "_MAX_RESULT_BYTES", 1) + + with pytest.raises(verifier.EvidenceError, match="exceeds"): + verifier.verify(arguments) + + +def test_verify_rejects_non_directory_root(tmp_path: Path) -> None: + """Reject a missing or non-directory evidence root before member traversal.""" + root = tmp_path / "not-directory" + root.write_text("x", encoding="utf-8") + arguments = argparse.Namespace(evidence_root=str(root)) + + with pytest.raises(verifier.EvidenceError, match="directory"): + verifier._validate_evidence_root(root) + root.unlink() + with pytest.raises(verifier.EvidenceError, match="existing"): + verifier._validate_evidence_root(root) + + +def test_atomic_json_rejects_output_symlink(tmp_path: Path) -> None: + """Do not replace an output selected through a symbolic-link endpoint.""" + target = tmp_path / "target.json" + target.write_text("{}", encoding="utf-8") + output = tmp_path / "output.json" + output.symlink_to(target.name) + + with pytest.raises(verifier.EvidenceError, match="must not be a symlink"): + verifier._atomic_json(output, {"x": 1}) + + +def test_atomic_json_cleans_temporary_after_replace(tmp_path: Path) -> None: + """Publish deterministically and leave no temporary sibling behind.""" + output = tmp_path / "nested" / "output.json" + verifier._atomic_json(output, {"z": 1}) + + assert output.read_text(encoding="utf-8") == '{"z":1}\n' + assert list(output.parent.glob(".output.json.*")) == [] + + +def test_main_reports_evidence_error_without_traceback( + evidence: tuple[Path, argparse.Namespace], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """Expose deterministic validation failure as a concise CLI error.""" + _, arguments = evidence + arguments.source_sha = "bad" + monkeypatch.setattr(verifier, "_parser", lambda: type("P", (), {"parse_args": lambda self: arguments})()) + + assert verifier.main() == 2 + assert "source SHA" in capsys.readouterr().err From f0f51615721cc9da73b4ee0613bc3ee8731e4d70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:16:07 +0900 Subject: [PATCH 03/47] feat(perf-attestation): verify sealed evidence as inert data --- .../ci/verify_product_performance_evidence.py | 307 ++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 scripts/ci/verify_product_performance_evidence.py diff --git a/scripts/ci/verify_product_performance_evidence.py b/scripts/ci/verify_product_performance_evidence.py new file mode 100644 index 0000000000..74d4edd8aa --- /dev/null +++ b/scripts/ci/verify_product_performance_evidence.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +"""Verify sealed product-performance evidence without executing its contents.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import stat +import sys +import tempfile +from pathlib import Path +from typing import Any, Iterable + +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +_REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +_POSITIVE_INTEGER_RE = re.compile(r"^[1-9][0-9]*$") +_ARTIFACT_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") +_ARTIFACT_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,254}$") +_PROFILE_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$") +_PERFORMANCE_PREDICATE_TYPE = ( + "https://contextualwisdomlab.org/attestations/product-performance/v1" +) +_MAX_RESULT_BYTES = 16 * 1024 * 1024 +_MAX_RUNTIME_BYTES = 16 * 1024 * 1024 +_MAX_FIXTURE_BYTES = 256 * 1024 * 1024 + + +class EvidenceError(ValueError): + """Describe a deterministic performance-evidence validation failure.""" + + +def _reject_duplicate_keys(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]: + """Build a JSON object while rejecting duplicate property names.""" + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise EvidenceError(f"duplicate JSON property: {key}") + result[key] = value + return result + + +def _reject_nonfinite_constant(value: str) -> Any: + """Reject JSON extensions for NaN and positive or negative infinity.""" + raise EvidenceError(f"non-finite JSON number is forbidden: {value}") + + +def _require_regular_file(path: Path) -> None: + """Require an existing regular evidence file without following a symlink.""" + try: + mode = path.lstat().st_mode + except FileNotFoundError as error: + raise EvidenceError(f"missing evidence file: {path.name}") from error + if stat.S_ISLNK(mode) or not stat.S_ISREG(mode): + raise EvidenceError(f"evidence member is non-regular: {path.name}") + + +def _validate_evidence_root(path: Path) -> Path: + """Return an absolute evidence directory after rejecting symlinked ancestors.""" + absolute = Path(os.path.abspath(path)) + current = Path(absolute.anchor) + for component in absolute.parts[1:]: + current /= component + try: + mode = current.lstat().st_mode + except FileNotFoundError as error: + raise EvidenceError("evidence root must be an existing directory") from error + if stat.S_ISLNK(mode): + raise EvidenceError("evidence root and every ancestor must reject a symlink ancestor") + if current == absolute and not stat.S_ISDIR(mode): + raise EvidenceError("evidence root must be a directory") + return absolute + + +def _validate_filename(value: str, label: str) -> str: + """Return one safe root-level evidence filename.""" + if not value or value in {".", ".."} or Path(value).name != value: + raise EvidenceError(f"{label} must be one root-level filename") + if "/" in value or "\\" in value or "\x00" in value: + raise EvidenceError(f"{label} contains a forbidden path character") + return value + + +def _validate_sha256(value: str, label: str) -> str: + """Return one canonical lower-case SHA-256 digest.""" + if _SHA256_RE.fullmatch(value) is None: + raise EvidenceError(f"{label} must be 64 lowercase hexadecimal characters") + return value + + +def _load_json(path: Path, maximum_bytes: int) -> dict[str, Any]: + """Load strict bounded UTF-8 JSON and require an object root.""" + _require_regular_file(path) + if path.stat().st_size > maximum_bytes: + raise EvidenceError(f"JSON file exceeds {maximum_bytes} bytes: {path.name}") + try: + text = path.read_text(encoding="utf-8", errors="strict") + except UnicodeError as error: + raise EvidenceError(f"invalid UTF-8 in {path.name}") from error + try: + value = json.loads( + text, + object_pairs_hook=_reject_duplicate_keys, + parse_constant=_reject_nonfinite_constant, + ) + except json.JSONDecodeError as error: + raise EvidenceError(f"invalid JSON in {path.name}: {error.msg}") from error + if not isinstance(value, dict): + raise EvidenceError(f"{path.name} must contain a JSON object") + return value + + +def _sha256(path: Path) -> str: + """Hash one regular evidence file without loading it into memory.""" + _require_regular_file(path) + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def _require_digest(path: Path, expected: str, label: str) -> None: + """Require one evidence member to match its external SHA-256 binding.""" + actual = _sha256(path) + if actual != expected: + raise EvidenceError(f"{label} digest mismatch: expected {expected}, got {actual}") + + +def _atomic_json(path: Path, value: dict[str, Any]) -> None: + """Publish deterministic JSON atomically without following an output symlink.""" + path.parent.mkdir(parents=True, exist_ok=True) + if path.is_symlink(): + raise EvidenceError("output manifest path must not be a symlink") + payload = json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n" + descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream: + stream.write(payload) + stream.flush() + os.fsync(stream.fileno()) + os.chmod(temporary, 0o644) + os.replace(temporary, path) + finally: + try: + os.unlink(temporary) + except FileNotFoundError: + pass + + +def _validate_controls(arguments: argparse.Namespace) -> None: + """Validate all caller-supplied control-plane identities before file access.""" + if _REPOSITORY_RE.fullmatch(arguments.source_repository) is None: + raise EvidenceError("source repository must use owner/name form") + if _SHA1_RE.fullmatch(arguments.source_sha) is None: + raise EvidenceError("source SHA must be a lowercase 40-character Git SHA") + if _POSITIVE_INTEGER_RE.fullmatch(arguments.workflow_run_id) is None: + raise EvidenceError("workflow run ID must be a positive decimal integer") + if _POSITIVE_INTEGER_RE.fullmatch(arguments.evidence_artifact_id) is None: + raise EvidenceError("artifact ID must be a positive decimal integer") + if _ARTIFACT_NAME_RE.fullmatch(arguments.evidence_artifact_name) is None: + raise EvidenceError("artifact name must be a bounded GitHub artifact identifier") + if _ARTIFACT_DIGEST_RE.fullmatch(arguments.evidence_artifact_digest) is None: + raise EvidenceError("artifact digest must use sha256:<64 lowercase hex>") + if _PROFILE_RE.fullmatch(arguments.performance_profile) is None: + raise EvidenceError("performance profile must be a bounded lowercase slug") + if arguments.predicate_type != _PERFORMANCE_PREDICATE_TYPE: + raise EvidenceError( + f"predicate type must be {_PERFORMANCE_PREDICATE_TYPE}" + ) + + +def verify(arguments: argparse.Namespace) -> dict[str, Any]: + """Validate one exact evidence set and publish deterministic trusted receipts.""" + _validate_controls(arguments) + root = _validate_evidence_root(Path(arguments.evidence_root)) + names = { + "result": _validate_filename(arguments.result_filename, "result filename"), + "runtime": _validate_filename( + arguments.runtime_evidence_filename, "runtime evidence filename" + ), + "fixture": _validate_filename(arguments.fixture_filename, "fixture filename"), + } + if len(set(names.values())) != len(names): + raise EvidenceError("result, runtime, and fixture filenames must be distinct") + + actual_members: set[str] = set() + for member in root.iterdir(): + if member.is_symlink() or not member.is_file(): + raise EvidenceError(f"unexpected non-regular evidence member: {member.name}") + actual_members.add(member.name) + expected_members = set(names.values()) + if actual_members != expected_members: + missing = sorted(expected_members - actual_members) + extra = sorted(actual_members - expected_members) + raise EvidenceError(f"evidence cardinality mismatch; missing={missing}, extra={extra}") + + digests = { + names["result"]: _validate_sha256(arguments.result_sha256, "result SHA-256"), + names["runtime"]: _validate_sha256( + arguments.runtime_evidence_sha256, "runtime evidence SHA-256" + ), + names["fixture"]: _validate_sha256(arguments.fixture_sha256, "fixture SHA-256"), + } + for filename, expected in digests.items(): + _require_digest(root / filename, expected, filename) + + _load_json(root / names["result"], _MAX_RESULT_BYTES) + _load_json(root / names["runtime"], _MAX_RUNTIME_BYTES) + _load_json(root / names["fixture"], _MAX_FIXTURE_BYTES) + + predicate = { + "attestation_claim": "origin_and_integrity_only", + "does_not_prove": [ + "latency_threshold_passed", + "production_equivalence", + "fixture_scientific_validity", + "fixture_right_clearance", + ], + "evidence": { + "artifact_digest": arguments.evidence_artifact_digest, + "artifact_id": arguments.evidence_artifact_id, + "artifact_name": arguments.evidence_artifact_name, + "fixture": { + "filename": names["fixture"], + "sha256": digests[names["fixture"]], + }, + "result": { + "filename": names["result"], + "sha256": digests[names["result"]], + }, + "runtime": { + "filename": names["runtime"], + "sha256": digests[names["runtime"]], + }, + }, + "performance_profile": arguments.performance_profile, + "predicate_type": arguments.predicate_type, + "schema_version": "1.0", + "source_repository": arguments.source_repository, + "source_sha": arguments.source_sha, + "workflow_run_id": arguments.workflow_run_id, + } + files = [ + { + "filename": filename, + "sha256": digests[filename], + "size_bytes": (root / filename).stat().st_size, + } + for filename in sorted(expected_members) + ] + manifest = { + "evidence_artifact_digest": arguments.evidence_artifact_digest, + "evidence_artifact_id": arguments.evidence_artifact_id, + "evidence_artifact_name": arguments.evidence_artifact_name, + "files": files, + "performance_profile": arguments.performance_profile, + "predicate_type": arguments.predicate_type, + "result": "PASS", + "source_repository": arguments.source_repository, + "source_sha": arguments.source_sha, + "workflow_run_id": arguments.workflow_run_id, + } + _atomic_json(Path(arguments.output_predicate), predicate) + _atomic_json(Path(arguments.output_manifest), manifest) + return manifest + + +def _parser() -> argparse.ArgumentParser: + """Create the strict CLI parser for sealed performance evidence.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source-repository", required=True) + parser.add_argument("--source-sha", required=True) + parser.add_argument("--workflow-run-id", required=True) + parser.add_argument("--evidence-artifact-id", required=True) + parser.add_argument("--evidence-artifact-name", required=True) + parser.add_argument("--evidence-artifact-digest", required=True) + parser.add_argument("--evidence-root", required=True) + parser.add_argument("--result-filename", required=True) + parser.add_argument("--result-sha256", required=True) + parser.add_argument("--runtime-evidence-filename", required=True) + parser.add_argument("--runtime-evidence-sha256", required=True) + parser.add_argument("--fixture-filename", required=True) + parser.add_argument("--fixture-sha256", required=True) + parser.add_argument("--performance-profile", required=True) + parser.add_argument("--predicate-type", required=True) + parser.add_argument("--output-predicate", required=True) + parser.add_argument("--output-manifest", required=True) + return parser + + +def main() -> int: + """Run CLI verification and return a stable nonzero code for invalid evidence.""" + arguments = _parser().parse_args() + try: + verify(arguments) + except EvidenceError as error: + print(f"performance evidence rejected: {error}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": # pragma: no cover - exercised through main() contracts + raise SystemExit(main()) From 28da8dc48516e114ba1f898a6954ad9444071489 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:17:14 +0900 Subject: [PATCH 04/47] feat(perf-attestation): add authenticated evidence workflow --- .../product-performance-attestation.yml | 502 ++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 .github/workflows/product-performance-attestation.yml diff --git a/.github/workflows/product-performance-attestation.yml b/.github/workflows/product-performance-attestation.yml new file mode 100644 index 0000000000..98045cb3a3 --- /dev/null +++ b/.github/workflows/product-performance-attestation.yml @@ -0,0 +1,502 @@ +name: Product Performance Evidence Attestation + +on: + workflow_call: + inputs: + source_repository: + required: true + type: string + source_sha: + required: true + type: string + evidence_artifact_id: + required: true + type: string + evidence_artifact_name: + required: true + type: string + evidence_artifact_digest: + required: true + type: string + result_filename: + required: true + type: string + result_sha256: + required: true + type: string + runtime_evidence_filename: + required: true + type: string + runtime_evidence_sha256: + required: true + type: string + fixture_filename: + required: true + type: string + fixture_sha256: + required: true + type: string + performance_profile: + required: true + type: string + predicate_type: + required: true + type: string + outputs: + attestation_id: + description: GitHub artifact-attestation identifier for the exact result subject. + value: ${{ jobs.attest-performance-evidence.outputs.attestation_id }} + attestation_url: + description: GitHub artifact-attestation URL for the exact result subject. + value: ${{ jobs.attest-performance-evidence.outputs.attestation_url }} + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PERFORMANCE_PREDICATE_TYPE: https://contextualwisdomlab.org/attestations/product-performance/v1 + +jobs: + verify-performance-evidence: + name: Verify sealed inert performance evidence + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + actions: read + contents: read + id-token: write + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Resolve exact called reusable workflow identity + id: workflow-identity + env: + EXPECTED_OIDC_ISSUER: https://token.actions.githubusercontent.com + OIDC_AUDIENCE: https://github.com/ContextualWisdomLab/.github/product-performance-attestation + EXPECTED_WORKFLOW_PREFIX: ContextualWisdomLab/.github/.github/workflows/product-performance-attestation.yml@ + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + : "${ACTIONS_ID_TOKEN_REQUEST_URL:?GitHub OIDC request URL is unavailable}" + : "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?GitHub OIDC request token is unavailable}" + oidc_response="${RUNNER_TEMP}/called-workflow-oidc.json" + umask 077 + trap 'rm -f "$oidc_response"' EXIT + curl --fail --silent --show-error --get \ + --header "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + --data-urlencode "audience=${OIDC_AUDIENCE}" \ + --output "$oidc_response" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL" + python3 -I - "$oidc_response" <<'PY' + import base64 + import json + import os + import re + import sys + + response_path = sys.argv[1] + try: + with open(response_path, encoding="utf-8") as handle: + token_response = json.load(handle) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise SystemExit("GitHub OIDC response is not valid JSON") from exc + + token = token_response.get("value") + if not isinstance(token, str) or not token: + raise SystemExit("GitHub OIDC response is missing its token value") + token_parts = token.split(".") + if len(token_parts) != 3: + raise SystemExit("GitHub OIDC token is not a JWT") + encoded_payload = token_parts[1] + encoded_payload += "=" * (-len(encoded_payload) % 4) + try: + payload_bytes = base64.urlsafe_b64decode(encoded_payload.encode("ascii")) + claims = json.loads(payload_bytes.decode("utf-8")) + except (UnicodeError, ValueError, json.JSONDecodeError) as exc: + raise SystemExit("GitHub OIDC token payload is malformed") from exc + if not isinstance(claims, dict): + raise SystemExit("GitHub OIDC token payload must be an object") + + expected_issuer = os.environ["EXPECTED_OIDC_ISSUER"] + expected_audience = os.environ["OIDC_AUDIENCE"] + expected_prefix = os.environ["EXPECTED_WORKFLOW_PREFIX"] + if claims.get("iss") != expected_issuer: + raise SystemExit("GitHub OIDC issuer mismatch") + audience = claims.get("aud") + audiences = {audience} if isinstance(audience, str) else set(audience or []) + if expected_audience not in audiences: + raise SystemExit("GitHub OIDC audience mismatch") + if claims.get("runner_environment") != "github-hosted": + raise SystemExit("performance signer identity requires a github-hosted runner") + + workflow_ref = claims.get("job_workflow_ref") + workflow_sha = claims.get("job_workflow_sha") + if not isinstance(workflow_ref, str) or not workflow_ref.startswith(expected_prefix): + raise SystemExit("job_workflow_ref does not identify the central performance workflow") + if not isinstance(workflow_sha, str) or re.fullmatch(r"[0-9a-f]{40}", workflow_sha) is None: + raise SystemExit("job_workflow_sha must be a full 40-hex commit SHA") + ref_sha = workflow_ref[len(expected_prefix):] + if re.fullmatch(r"[0-9a-f]{40}", ref_sha) is None: + raise SystemExit("job_workflow_ref must pin the called workflow to a full 40-hex commit SHA") + if ref_sha != workflow_sha: + raise SystemExit("job_workflow_ref and job_workflow_sha disagree") + + output_path = os.environ.get("GITHUB_OUTPUT") + if not output_path: + raise SystemExit("GITHUB_OUTPUT is unavailable") + with open(output_path, "a", encoding="utf-8") as output: + output.write(f"workflow_sha={workflow_sha}\n") + PY + + - name: Materialize immutable trusted verifier + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + ref: ${{ steps.workflow-identity.outputs.workflow_sha }} + path: trusted-intake + persist-credentials: false + sparse-checkout: scripts/ci/verify_product_performance_evidence.py + sparse-checkout-cone-mode: false + + - name: Verify immutable same-run artifact metadata + env: + GH_TOKEN: ${{ github.token }} + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + SOURCE_SHA: ${{ inputs.source_sha }} + ARTIFACT_ID: ${{ inputs.evidence_artifact_id }} + ARTIFACT_NAME: ${{ inputs.evidence_artifact_name }} + ARTIFACT_DIGEST: ${{ inputs.evidence_artifact_digest }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$SOURCE_REPOSITORY" = "$GITHUB_REPOSITORY" + test "$SOURCE_SHA" = "$GITHUB_SHA" + artifact_json="$(gh api "/repos/${SOURCE_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")" + jq -e \ + --arg name "$ARTIFACT_NAME" \ + --arg digest "$ARTIFACT_DIGEST" \ + --argjson artifact_id "$ARTIFACT_ID" \ + --argjson run_id "$GITHUB_RUN_ID" \ + '.id == $artifact_id and .name == $name and .digest == $digest and .workflow_run.id == $run_id and .expired == false' \ + <<<"$artifact_json" >/dev/null + + - name: Download exact same-run evidence by immutable artifact ID + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ inputs.evidence_artifact_id }} + path: sealed-evidence + + - name: Verify sealed performance evidence as inert bounded data + env: + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + SOURCE_SHA: ${{ inputs.source_sha }} + EVIDENCE_ARTIFACT_ID: ${{ inputs.evidence_artifact_id }} + EVIDENCE_ARTIFACT_NAME: ${{ inputs.evidence_artifact_name }} + EVIDENCE_ARTIFACT_DIGEST: ${{ inputs.evidence_artifact_digest }} + RESULT_FILENAME: ${{ inputs.result_filename }} + RESULT_SHA256: ${{ inputs.result_sha256 }} + RUNTIME_FILENAME: ${{ inputs.runtime_evidence_filename }} + RUNTIME_SHA256: ${{ inputs.runtime_evidence_sha256 }} + FIXTURE_FILENAME: ${{ inputs.fixture_filename }} + FIXTURE_SHA256: ${{ inputs.fixture_sha256 }} + PERFORMANCE_PROFILE: ${{ inputs.performance_profile }} + PREDICATE_TYPE: ${{ inputs.predicate_type }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$PREDICATE_TYPE" = "$PERFORMANCE_PREDICATE_TYPE" + python3 -I trusted-intake/scripts/ci/verify_product_performance_evidence.py \ + --source-repository "$SOURCE_REPOSITORY" \ + --source-sha "$SOURCE_SHA" \ + --workflow-run-id "$GITHUB_RUN_ID" \ + --evidence-artifact-id "$EVIDENCE_ARTIFACT_ID" \ + --evidence-artifact-name "$EVIDENCE_ARTIFACT_NAME" \ + --evidence-artifact-digest "$EVIDENCE_ARTIFACT_DIGEST" \ + --evidence-root sealed-evidence \ + --result-filename "$RESULT_FILENAME" \ + --result-sha256 "$RESULT_SHA256" \ + --runtime-evidence-filename "$RUNTIME_FILENAME" \ + --runtime-evidence-sha256 "$RUNTIME_SHA256" \ + --fixture-filename "$FIXTURE_FILENAME" \ + --fixture-sha256 "$FIXTURE_SHA256" \ + --performance-profile "$PERFORMANCE_PROFILE" \ + --predicate-type "$PREDICATE_TYPE" \ + --output-predicate "${RUNNER_TEMP}/verified-performance-predicate.json" \ + --output-manifest "${RUNNER_TEMP}/verified-performance-intake.json" + + attest-performance-evidence: + name: Attest exact product performance evidence + needs: verify-performance-evidence + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + actions: read + contents: read + id-token: write + attestations: write + artifact-metadata: write + outputs: + attestation_id: ${{ steps.attest-result.outputs.attestation-id }} + attestation_url: ${{ steps.attest-result.outputs.attestation-url }} + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Resolve exact called reusable workflow identity + id: workflow-identity + env: + EXPECTED_OIDC_ISSUER: https://token.actions.githubusercontent.com + OIDC_AUDIENCE: https://github.com/ContextualWisdomLab/.github/product-performance-attestation + EXPECTED_WORKFLOW_PREFIX: ContextualWisdomLab/.github/.github/workflows/product-performance-attestation.yml@ + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + : "${ACTIONS_ID_TOKEN_REQUEST_URL:?GitHub OIDC request URL is unavailable}" + : "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?GitHub OIDC request token is unavailable}" + oidc_response="${RUNNER_TEMP}/called-workflow-oidc.json" + umask 077 + trap 'rm -f "$oidc_response"' EXIT + curl --fail --silent --show-error --get \ + --header "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + --data-urlencode "audience=${OIDC_AUDIENCE}" \ + --output "$oidc_response" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL" + python3 -I - "$oidc_response" <<'PY' + import base64 + import json + import os + import re + import sys + + response_path = sys.argv[1] + try: + with open(response_path, encoding="utf-8") as handle: + token_response = json.load(handle) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise SystemExit("GitHub OIDC response is not valid JSON") from exc + + token = token_response.get("value") + if not isinstance(token, str) or not token: + raise SystemExit("GitHub OIDC response is missing its token value") + token_parts = token.split(".") + if len(token_parts) != 3: + raise SystemExit("GitHub OIDC token is not a JWT") + encoded_payload = token_parts[1] + encoded_payload += "=" * (-len(encoded_payload) % 4) + try: + payload_bytes = base64.urlsafe_b64decode(encoded_payload.encode("ascii")) + claims = json.loads(payload_bytes.decode("utf-8")) + except (UnicodeError, ValueError, json.JSONDecodeError) as exc: + raise SystemExit("GitHub OIDC token payload is malformed") from exc + if not isinstance(claims, dict): + raise SystemExit("GitHub OIDC token payload must be an object") + + expected_issuer = os.environ["EXPECTED_OIDC_ISSUER"] + expected_audience = os.environ["OIDC_AUDIENCE"] + expected_prefix = os.environ["EXPECTED_WORKFLOW_PREFIX"] + if claims.get("iss") != expected_issuer: + raise SystemExit("GitHub OIDC issuer mismatch") + audience = claims.get("aud") + audiences = {audience} if isinstance(audience, str) else set(audience or []) + if expected_audience not in audiences: + raise SystemExit("GitHub OIDC audience mismatch") + if claims.get("runner_environment") != "github-hosted": + raise SystemExit("performance signer identity requires a github-hosted runner") + + workflow_ref = claims.get("job_workflow_ref") + workflow_sha = claims.get("job_workflow_sha") + if not isinstance(workflow_ref, str) or not workflow_ref.startswith(expected_prefix): + raise SystemExit("job_workflow_ref does not identify the central performance workflow") + if not isinstance(workflow_sha, str) or re.fullmatch(r"[0-9a-f]{40}", workflow_sha) is None: + raise SystemExit("job_workflow_sha must be a full 40-hex commit SHA") + ref_sha = workflow_ref[len(expected_prefix):] + if re.fullmatch(r"[0-9a-f]{40}", ref_sha) is None: + raise SystemExit("job_workflow_ref must pin the called workflow to a full 40-hex commit SHA") + if ref_sha != workflow_sha: + raise SystemExit("job_workflow_ref and job_workflow_sha disagree") + + output_path = os.environ.get("GITHUB_OUTPUT") + if not output_path: + raise SystemExit("GITHUB_OUTPUT is unavailable") + with open(output_path, "a", encoding="utf-8") as output: + output.write(f"workflow_sha={workflow_sha}\n") + PY + + - name: Materialize immutable trusted verifier + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + ref: ${{ steps.workflow-identity.outputs.workflow_sha }} + path: trusted-signer + persist-credentials: false + sparse-checkout: scripts/ci/verify_product_performance_evidence.py + sparse-checkout-cone-mode: false + + - name: Verify immutable same-run artifact metadata + env: + GH_TOKEN: ${{ github.token }} + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + SOURCE_SHA: ${{ inputs.source_sha }} + ARTIFACT_ID: ${{ inputs.evidence_artifact_id }} + ARTIFACT_NAME: ${{ inputs.evidence_artifact_name }} + ARTIFACT_DIGEST: ${{ inputs.evidence_artifact_digest }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$SOURCE_REPOSITORY" = "$GITHUB_REPOSITORY" + test "$SOURCE_SHA" = "$GITHUB_SHA" + artifact_json="$(gh api "/repos/${SOURCE_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")" + jq -e \ + --arg name "$ARTIFACT_NAME" \ + --arg digest "$ARTIFACT_DIGEST" \ + --argjson artifact_id "$ARTIFACT_ID" \ + --argjson run_id "$GITHUB_RUN_ID" \ + '.id == $artifact_id and .name == $name and .digest == $digest and .workflow_run.id == $run_id and .expired == false' \ + <<<"$artifact_json" >/dev/null + + - name: Download exact sealed evidence without executing it + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ inputs.evidence_artifact_id }} + path: sealed-evidence + + - name: Reverify evidence and build trusted predicate inside signer + env: + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + SOURCE_SHA: ${{ inputs.source_sha }} + EVIDENCE_ARTIFACT_ID: ${{ inputs.evidence_artifact_id }} + EVIDENCE_ARTIFACT_NAME: ${{ inputs.evidence_artifact_name }} + EVIDENCE_ARTIFACT_DIGEST: ${{ inputs.evidence_artifact_digest }} + RESULT_FILENAME: ${{ inputs.result_filename }} + RESULT_SHA256: ${{ inputs.result_sha256 }} + RUNTIME_FILENAME: ${{ inputs.runtime_evidence_filename }} + RUNTIME_SHA256: ${{ inputs.runtime_evidence_sha256 }} + FIXTURE_FILENAME: ${{ inputs.fixture_filename }} + FIXTURE_SHA256: ${{ inputs.fixture_sha256 }} + PERFORMANCE_PROFILE: ${{ inputs.performance_profile }} + PREDICATE_TYPE: ${{ inputs.predicate_type }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$PREDICATE_TYPE" = "$PERFORMANCE_PREDICATE_TYPE" + python3 -I trusted-signer/scripts/ci/verify_product_performance_evidence.py \ + --source-repository "$SOURCE_REPOSITORY" \ + --source-sha "$SOURCE_SHA" \ + --workflow-run-id "$GITHUB_RUN_ID" \ + --evidence-artifact-id "$EVIDENCE_ARTIFACT_ID" \ + --evidence-artifact-name "$EVIDENCE_ARTIFACT_NAME" \ + --evidence-artifact-digest "$EVIDENCE_ARTIFACT_DIGEST" \ + --evidence-root sealed-evidence \ + --result-filename "$RESULT_FILENAME" \ + --result-sha256 "$RESULT_SHA256" \ + --runtime-evidence-filename "$RUNTIME_FILENAME" \ + --runtime-evidence-sha256 "$RUNTIME_SHA256" \ + --fixture-filename "$FIXTURE_FILENAME" \ + --fixture-sha256 "$FIXTURE_SHA256" \ + --performance-profile "$PERFORMANCE_PROFILE" \ + --predicate-type "$PREDICATE_TYPE" \ + --output-predicate "${RUNNER_TEMP}/verified-performance-predicate.json" \ + --output-manifest "${RUNNER_TEMP}/verified-performance-signer.json" + + - name: Attest exact result with trusted performance-evidence predicate + id: attest-result + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-name: ${{ inputs.result_filename }} + subject-digest: sha256:${{ inputs.result_sha256 }} + predicate-type: ${{ inputs.predicate_type }} + predicate-path: ${{ runner.temp }}/verified-performance-predicate.json + + - name: Verify online and prepare offline performance evidence + env: + GH_TOKEN: ${{ github.token }} + SIGNER_REPOSITORY: ContextualWisdomLab/.github + SOURCE_REPOSITORY: ${{ inputs.source_repository }} + SOURCE_SHA: ${{ inputs.source_sha }} + RESULT_FILENAME: ${{ inputs.result_filename }} + PREDICATE_TYPE: ${{ inputs.predicate_type }} + ATTESTATION_BUNDLE: ${{ steps.attest-result.outputs.bundle-path }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + signer_workflow="${SIGNER_REPOSITORY}/.github/workflows/product-performance-attestation.yml" + mkdir -p offline-performance-attestation + install -m 0444 "$ATTESTATION_BUNDLE" offline-performance-attestation/result-attestation.json + install -m 0444 "${RUNNER_TEMP}/verified-performance-predicate.json" \ + offline-performance-attestation/verified-performance-predicate.json + install -m 0444 "${RUNNER_TEMP}/verified-performance-signer.json" \ + offline-performance-attestation/verified-performance-manifest.json + gh attestation trusted-root > offline-performance-attestation/trusted_root.jsonl + gh attestation verify "sealed-evidence/${RESULT_FILENAME}" \ + --repo "$SOURCE_REPOSITORY" \ + --signer-repo "$SIGNER_REPOSITORY" \ + --signer-workflow "$signer_workflow" \ + --source-digest "$SOURCE_SHA" \ + --predicate-type "$PREDICATE_TYPE" + gh attestation verify "sealed-evidence/${RESULT_FILENAME}" \ + --repo "$SOURCE_REPOSITORY" \ + --bundle offline-performance-attestation/result-attestation.json \ + --custom-trusted-root offline-performance-attestation/trusted_root.jsonl \ + --signer-repo "$SIGNER_REPOSITORY" \ + --signer-workflow "$signer_workflow" \ + --source-digest "$SOURCE_SHA" \ + --predicate-type "$PREDICATE_TYPE" + cat > offline-performance-attestation/README.md <<'EOF' + # Product performance evidence attestation + + This directory preserves the exact Sigstore bundle, trusted root, trusted + performance predicate, and verifier manifest for one measured result. + Verify `SHA256SUMS` before using a member. + + A valid attestation proves the authenticated origin and byte integrity of + the sealed evidence bindings. It does not prove that a latency threshold + passed, that the measured service was production-equivalent, or that the + fixture was scientifically valid or right-cleared. + EOF + { + printf '\n## Exact signed identity\n\n' + printf -- '- Source repository: `%s`\n' "$SOURCE_REPOSITORY" + printf -- '- Source SHA: `%s`\n' "$SOURCE_SHA" + printf -- '- Signer repository: `%s`\n' "$SIGNER_REPOSITORY" + printf -- '- Signer workflow: `%s`\n' "$signer_workflow" + printf -- '- Predicate type: `%s`\n' "$PREDICATE_TYPE" + printf -- '- Result subject: `%s`\n' "$RESULT_FILENAME" + cat <> offline-performance-attestation/README.md + ( + cd offline-performance-attestation + LC_ALL=C find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' \ + | LC_ALL=C sort \ + | while IFS= read -r evidence_file; do + sha256sum "$evidence_file" + done > SHA256SUMS + ) + chmod 0444 offline-performance-attestation/* + + - name: Export offline performance attestation evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: product-performance-offline-verification + path: offline-performance-attestation + if-no-files-found: error + retention-days: 90 From 3855fa20f75b4e6da6766915d2a3d2e9ba696b84 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:18:34 +0900 Subject: [PATCH 05/47] ci(perf-attestation): enforce exact verifier quality --- ...roduct-performance-attestation-quality.yml | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/product-performance-attestation-quality.yml diff --git a/.github/workflows/product-performance-attestation-quality.yml b/.github/workflows/product-performance-attestation-quality.yml new file mode 100644 index 0000000000..650a1823c0 --- /dev/null +++ b/.github/workflows/product-performance-attestation-quality.yml @@ -0,0 +1,97 @@ +name: Product Performance Attestation Quality CI + +on: + pull_request: + branches: [main, fix/reusable-attestation-workflow-identity] + paths: + - ".github/workflows/product-performance-attestation.yml" + - ".github/workflows/product-performance-attestation-quality.yml" + - "scripts/ci/verify_product_performance_evidence.py" + - "tests/test_product_performance_attestation_contract.py" + - "tests/test_product_performance_evidence_verifier.py" + - "docs/doctoring/product-performance-attestation.md" + - "ARCHITECTURE.md" + - "CHANGELOG.d/20260913-product-performance-attestation.md" + +concurrency: + group: product-performance-attestation-quality-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + product-performance-attestation-quality: + name: product-performance-attestation-quality + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact pull request head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Set up minimum supported Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.10" + + - name: Compile production verifier and contracts on Python 3.10 + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" + python -m compileall -q \ + scripts/ci/verify_product_performance_evidence.py \ + tests/test_product_performance_attestation_contract.py \ + tests/test_product_performance_evidence_verifier.py + + - name: Set up current quality Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + + - name: Install exact quality dependencies + env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_INPUT: "1" + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + cat >"${RUNNER_TEMP}/performance-attestation-quality.txt" <<'EOF' + coverage==7.15.2 --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f + iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + interrogate==1.7.0 --hash=sha256:337334c2f599b61f9b12ef8a548be0cd4eae4ca251d0d95568c611fac1d0023d + packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + EOF + python -m pip install \ + --only-binary=:all: \ + --require-hashes \ + -r "${RUNNER_TEMP}/performance-attestation-quality.txt" + + - name: Verify exact-head performance attestation contracts + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" + python -m coverage erase + python -m coverage run --branch -m pytest -q \ + tests/test_product_performance_attestation_contract.py \ + tests/test_product_performance_evidence_verifier.py + python -m coverage report \ + --include=scripts/ci/verify_product_performance_evidence.py \ + --show-missing \ + --fail-under=100 + python -m interrogate --fail-under 100 \ + scripts/ci/verify_product_performance_evidence.py + git diff --check "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" From 1ccca26476b0b53020d1e2390102f47a28ac1106 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:18:57 +0900 Subject: [PATCH 06/47] ci(perf-attestation): reuse canonical hash-locked dependencies --- ...product-performance-attestation-quality.yml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/product-performance-attestation-quality.yml b/.github/workflows/product-performance-attestation-quality.yml index 650a1823c0..44e668d28d 100644 --- a/.github/workflows/product-performance-attestation-quality.yml +++ b/.github/workflows/product-performance-attestation-quality.yml @@ -12,6 +12,7 @@ on: - "docs/doctoring/product-performance-attestation.md" - "ARCHITECTURE.md" - "CHANGELOG.d/20260913-product-performance-attestation.md" + - "requirements-opencode-review-ci-hashes.txt" concurrency: group: product-performance-attestation-quality-${{ github.repository }}-${{ github.event.pull_request.number }} @@ -59,26 +60,19 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" + cache: pip + cache-dependency-path: requirements-opencode-review-ci-hashes.txt - - name: Install exact quality dependencies + - name: Install canonical hash-verified quality dependencies env: PIP_DISABLE_PIP_VERSION_CHECK: "1" PIP_NO_INPUT: "1" shell: bash --noprofile --norc -e -o pipefail {0} run: | - cat >"${RUNNER_TEMP}/performance-attestation-quality.txt" <<'EOF' - coverage==7.15.2 --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f - iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 - interrogate==1.7.0 --hash=sha256:337334c2f599b61f9b12ef8a548be0cd4eae4ca251d0d95568c611fac1d0023d - packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e - pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 - pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 - pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c - EOF python -m pip install \ - --only-binary=:all: \ + --disable-pip-version-check \ --require-hashes \ - -r "${RUNNER_TEMP}/performance-attestation-quality.txt" + -r requirements-opencode-review-ci-hashes.txt - name: Verify exact-head performance attestation contracts shell: bash --noprofile --norc -e -o pipefail {0} From 2b18a219df89ae7699f1778b8c134bbd2e301102 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:23:07 +0900 Subject: [PATCH 07/47] fix(perf-attestation): fail closed on evidence path ancestry --- scripts/ci/verify_product_performance_evidence.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/ci/verify_product_performance_evidence.py b/scripts/ci/verify_product_performance_evidence.py index 74d4edd8aa..896244dd87 100644 --- a/scripts/ci/verify_product_performance_evidence.py +++ b/scripts/ci/verify_product_performance_evidence.py @@ -59,19 +59,19 @@ def _require_regular_file(path: Path) -> None: def _validate_evidence_root(path: Path) -> Path: - """Return an absolute evidence directory after rejecting symlinked ancestors.""" + """Return an absolute evidence directory after rejecting unsafe ancestry.""" absolute = Path(os.path.abspath(path)) current = Path(absolute.anchor) for component in absolute.parts[1:]: current /= component try: mode = current.lstat().st_mode - except FileNotFoundError as error: + except (FileNotFoundError, NotADirectoryError) as error: raise EvidenceError("evidence root must be an existing directory") from error if stat.S_ISLNK(mode): raise EvidenceError("evidence root and every ancestor must reject a symlink ancestor") - if current == absolute and not stat.S_ISDIR(mode): - raise EvidenceError("evidence root must be a directory") + if not stat.S_ISDIR(mode): + raise EvidenceError("evidence root and every ancestor must be directories") return absolute @@ -303,5 +303,5 @@ def main() -> int: return 0 -if __name__ == "__main__": # pragma: no cover - exercised through main() contracts - raise SystemExit(main()) +if __name__ == "__main__": # pragma: no cover - CLI dispatch is covered via main() + raise SystemExit(main()) # pragma: no cover From 5d960ee6ac797054dfdf0c61dc92d8c7c665b88a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:23:51 +0900 Subject: [PATCH 08/47] test(perf-attestation): cover fail-closed verifier edges --- ...t_product_performance_evidence_verifier.py | 116 +++++++++++++++++- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/tests/test_product_performance_evidence_verifier.py b/tests/test_product_performance_evidence_verifier.py index 45731b5a72..19d85bdffd 100644 --- a/tests/test_product_performance_evidence_verifier.py +++ b/tests/test_product_performance_evidence_verifier.py @@ -5,7 +5,6 @@ import argparse import hashlib import json -import os from pathlib import Path import pytest @@ -53,6 +52,46 @@ def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: ) +def _cli(arguments: argparse.Namespace) -> list[str]: + """Serialize one valid namespace into the verifier's public CLI contract.""" + return [ + "--source-repository", + arguments.source_repository, + "--source-sha", + arguments.source_sha, + "--workflow-run-id", + arguments.workflow_run_id, + "--evidence-artifact-id", + arguments.evidence_artifact_id, + "--evidence-artifact-name", + arguments.evidence_artifact_name, + "--evidence-artifact-digest", + arguments.evidence_artifact_digest, + "--evidence-root", + arguments.evidence_root, + "--result-filename", + arguments.result_filename, + "--result-sha256", + arguments.result_sha256, + "--runtime-evidence-filename", + arguments.runtime_evidence_filename, + "--runtime-evidence-sha256", + arguments.runtime_evidence_sha256, + "--fixture-filename", + arguments.fixture_filename, + "--fixture-sha256", + arguments.fixture_sha256, + "--performance-profile", + arguments.performance_profile, + "--predicate-type", + arguments.predicate_type, + "--output-predicate", + arguments.output_predicate, + "--output-manifest", + arguments.output_manifest, + ] + + @pytest.fixture def evidence(tmp_path: Path) -> tuple[Path, argparse.Namespace]: """Create a valid three-file inert evidence set and verifier arguments.""" @@ -136,6 +175,18 @@ def test_verify_rejects_invalid_control_fields( verifier.verify(arguments) +def test_require_regular_file_rejects_missing_and_directory(tmp_path: Path) -> None: + """Reject absent or directory members at the byte-reading boundary.""" + missing = tmp_path / "missing.json" + with pytest.raises(verifier.EvidenceError, match="missing evidence file"): + verifier._require_regular_file(missing) + + directory = tmp_path / "directory.json" + directory.mkdir() + with pytest.raises(verifier.EvidenceError, match="non-regular"): + verifier._require_regular_file(directory) + + def test_verify_rejects_duplicate_filenames(evidence: tuple[Path, argparse.Namespace]) -> None: """Require result, runtime, and fixture to remain three distinct members.""" _, arguments = evidence @@ -160,6 +211,15 @@ def test_verify_rejects_missing_and_extra_members(evidence: tuple[Path, argparse verifier.verify(arguments) +def test_verify_rejects_non_regular_directory_member(evidence: tuple[Path, argparse.Namespace]) -> None: + """Reject nested directories rather than treating them as ignorable artifact members.""" + root, arguments = evidence + (root / "nested").mkdir() + + with pytest.raises(verifier.EvidenceError, match="unexpected non-regular"): + verifier.verify(arguments) + + def test_verify_rejects_symlink_member(evidence: tuple[Path, argparse.Namespace]) -> None: """Do not follow an evidence-member symlink even when its target is regular.""" root, arguments = evidence @@ -187,6 +247,15 @@ def test_verify_rejects_symlinked_root_ancestor(tmp_path: Path) -> None: verifier.verify(arguments) +def test_validate_evidence_root_rejects_non_directory_ancestor(tmp_path: Path) -> None: + """Convert a path-through-file failure into the stable fail-closed domain error.""" + blocking = tmp_path / "blocking" + blocking.write_text("not a directory", encoding="utf-8") + + with pytest.raises(verifier.EvidenceError, match="directories"): + verifier._validate_evidence_root(blocking / "evidence") + + def test_verify_rejects_digest_mismatch(evidence: tuple[Path, argparse.Namespace]) -> None: """Reject replacement bytes even when the filename remains unchanged.""" root, arguments = evidence @@ -240,9 +309,8 @@ def test_verify_rejects_non_directory_root(tmp_path: Path) -> None: """Reject a missing or non-directory evidence root before member traversal.""" root = tmp_path / "not-directory" root.write_text("x", encoding="utf-8") - arguments = argparse.Namespace(evidence_root=str(root)) - with pytest.raises(verifier.EvidenceError, match="directory"): + with pytest.raises(verifier.EvidenceError, match="directories"): verifier._validate_evidence_root(root) root.unlink() with pytest.raises(verifier.EvidenceError, match="existing"): @@ -260,6 +328,23 @@ def test_atomic_json_rejects_output_symlink(tmp_path: Path) -> None: verifier._atomic_json(output, {"x": 1}) +def test_atomic_json_cleans_temporary_when_replace_fails( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Remove the temporary sibling when atomic replacement itself fails.""" + output = tmp_path / "output.json" + + def fail_replace(source: str, destination: Path) -> None: + """Simulate a filesystem failure after the temporary file is written.""" + del source, destination + raise OSError("replace failed") + + monkeypatch.setattr(verifier.os, "replace", fail_replace) + with pytest.raises(OSError, match="replace failed"): + verifier._atomic_json(output, {"x": 1}) + assert list(tmp_path.glob(".output.json.*")) == [] + + def test_atomic_json_cleans_temporary_after_replace(tmp_path: Path) -> None: """Publish deterministically and leave no temporary sibling behind.""" output = tmp_path / "nested" / "output.json" @@ -269,13 +354,36 @@ def test_atomic_json_cleans_temporary_after_replace(tmp_path: Path) -> None: assert list(output.parent.glob(".output.json.*")) == [] +def test_parser_accepts_the_complete_public_contract( + evidence: tuple[Path, argparse.Namespace] +) -> None: + """Keep every workflow-supplied CLI argument wired to the verifier parser.""" + _, arguments = evidence + + parsed = verifier._parser().parse_args(_cli(arguments)) + + assert vars(parsed) == vars(arguments) + + +def test_main_returns_zero_for_valid_evidence( + evidence: tuple[Path, argparse.Namespace], monkeypatch: pytest.MonkeyPatch +) -> None: + """Expose successful verification through the production CLI entry point.""" + _, arguments = evidence + parser = type("P", (), {"parse_args": lambda self: arguments})() + monkeypatch.setattr(verifier, "_parser", lambda: parser) + + assert verifier.main() == 0 + + def test_main_reports_evidence_error_without_traceback( evidence: tuple[Path, argparse.Namespace], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """Expose deterministic validation failure as a concise CLI error.""" _, arguments = evidence arguments.source_sha = "bad" - monkeypatch.setattr(verifier, "_parser", lambda: type("P", (), {"parse_args": lambda self: arguments})()) + parser = type("P", (), {"parse_args": lambda self: arguments})() + monkeypatch.setattr(verifier, "_parser", lambda: parser) assert verifier.main() == 2 assert "source SHA" in capsys.readouterr().err From cfa040c68ca5f8bea5517f319553cc0d47f70ac3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:35:30 +0900 Subject: [PATCH 09/47] test(perf): reject ambiguous PASS attestation receipt --- tests/test_product_performance_attestation_contract.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_product_performance_attestation_contract.py b/tests/test_product_performance_attestation_contract.py index 3aac1776fe..8cebf3022a 100644 --- a/tests/test_product_performance_attestation_contract.py +++ b/tests/test_product_performance_attestation_contract.py @@ -93,3 +93,11 @@ def test_signer_attests_result_with_versioned_custom_predicate_and_offline_bundl assert "gh attestation trusted-root" in workflow assert UPLOAD_ACTION_PIN in workflow assert "does not prove" in workflow + + +def test_retained_manifest_cannot_be_misread_as_a_performance_pass() -> None: + """Name structural verification status so consumers cannot mistake it for a latency verdict.""" + verifier = _text(VERIFIER) + + assert '"result": "PASS"' not in verifier + assert '"verification_result": "VALID"' in verifier From cc33ecdb29df8a5aa2876bee095e4a1cd2aa332c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:36:23 +0900 Subject: [PATCH 10/47] fix(perf): distinguish evidence validity from performance verdict --- scripts/ci/verify_product_performance_evidence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/verify_product_performance_evidence.py b/scripts/ci/verify_product_performance_evidence.py index 896244dd87..bb89d8074f 100644 --- a/scripts/ci/verify_product_performance_evidence.py +++ b/scripts/ci/verify_product_performance_evidence.py @@ -259,7 +259,7 @@ def verify(arguments: argparse.Namespace) -> dict[str, Any]: "files": files, "performance_profile": arguments.performance_profile, "predicate_type": arguments.predicate_type, - "result": "PASS", + "verification_result": "VALID", "source_repository": arguments.source_repository, "source_sha": arguments.source_sha, "workflow_run_id": arguments.workflow_run_id, From fb9e27516b803e45f25adc1fa5b8a93b28912342 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:37:25 +0900 Subject: [PATCH 11/47] test(perf): assert structural evidence validity semantics --- tests/test_product_performance_evidence_verifier.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_product_performance_evidence_verifier.py b/tests/test_product_performance_evidence_verifier.py index 19d85bdffd..4d19f0bb61 100644 --- a/tests/test_product_performance_evidence_verifier.py +++ b/tests/test_product_performance_evidence_verifier.py @@ -110,7 +110,8 @@ def test_verify_binds_exact_three_file_evidence(evidence: tuple[Path, argparse.N manifest = verifier.verify(arguments) predicate = json.loads(Path(arguments.output_predicate).read_text(encoding="utf-8")) - assert manifest["result"] == "PASS" + assert manifest["verification_result"] == "VALID" + assert "result" not in manifest assert manifest["source_repository"] == arguments.source_repository assert manifest["source_sha"] == SOURCE_SHA assert manifest["performance_profile"] == "first_commit" From 183757b2eac0ac1ee9f51eb67af468bccae870ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:39:57 +0900 Subject: [PATCH 12/47] test(perf): reject non-CWL attestation callers --- .../test_product_performance_attestation_contract.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_product_performance_attestation_contract.py b/tests/test_product_performance_attestation_contract.py index 8cebf3022a..222ee3005c 100644 --- a/tests/test_product_performance_attestation_contract.py +++ b/tests/test_product_performance_attestation_contract.py @@ -57,6 +57,17 @@ def test_reusable_workflow_uses_oidc_callee_identity_before_trusted_checkout() - assert workflow.count("product-performance-attestation.yml@") >= 2 +def test_reusable_workflow_fails_closed_for_non_cwl_callers() -> None: + """Require both jobs to reject repositories outside the owning organization before artifact access.""" + workflow = _text(WORKFLOW) + + assert "EXPECTED_SOURCE_OWNER: ContextualWisdomLab" in workflow + assert workflow.count('test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER"') == 2 + first_artifact_access = workflow.index('/actions/artifacts/${ARTIFACT_ID}') + first_owner_check = workflow.index('test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER"') + assert first_owner_check < first_artifact_access + + def test_reusable_workflow_rechecks_same_run_artifact_and_never_executes_evidence() -> None: """Treat caller performance evidence as inert bounded data in both jobs.""" workflow = _text(WORKFLOW) From 5c09a98fd6bcca72f7cb0c3b70a0463590324006 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:41:36 +0900 Subject: [PATCH 13/47] fix(perf): restrict central attestations to CWL callers --- .github/workflows/product-performance-attestation.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/product-performance-attestation.yml b/.github/workflows/product-performance-attestation.yml index 98045cb3a3..b6621687c4 100644 --- a/.github/workflows/product-performance-attestation.yml +++ b/.github/workflows/product-performance-attestation.yml @@ -56,6 +56,7 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true PERFORMANCE_PREDICATE_TYPE: https://contextualwisdomlab.org/attestations/product-performance/v1 + EXPECTED_SOURCE_OWNER: ContextualWisdomLab jobs: verify-performance-evidence: @@ -172,6 +173,7 @@ jobs: shell: bash --noprofile --norc -e -o pipefail {0} run: | test "$SOURCE_REPOSITORY" = "$GITHUB_REPOSITORY" + test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER" test "$SOURCE_SHA" = "$GITHUB_SHA" artifact_json="$(gh api "/repos/${SOURCE_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")" jq -e \ @@ -345,6 +347,7 @@ jobs: shell: bash --noprofile --norc -e -o pipefail {0} run: | test "$SOURCE_REPOSITORY" = "$GITHUB_REPOSITORY" + test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER" test "$SOURCE_SHA" = "$GITHUB_SHA" artifact_json="$(gh api "/repos/${SOURCE_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")" jq -e \ From b76323858cea9aae142fe2615d499da736873cf2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:46:36 +0900 Subject: [PATCH 14/47] test(perf): require bounded inert artifact materialization --- ...roduct_performance_attestation_contract.py | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/tests/test_product_performance_attestation_contract.py b/tests/test_product_performance_attestation_contract.py index 222ee3005c..3eb1e716ec 100644 --- a/tests/test_product_performance_attestation_contract.py +++ b/tests/test_product_performance_attestation_contract.py @@ -4,10 +4,8 @@ WORKFLOW = Path(".github/workflows/product-performance-attestation.yml") VERIFIER = Path("scripts/ci/verify_product_performance_evidence.py") +MATERIALIZER = Path("scripts/ci/materialize_product_performance_artifact.py") ATTEST_ACTION_PIN = "actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26" -DOWNLOAD_ACTION_PIN = ( - "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" -) UPLOAD_ACTION_PIN = ( "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" ) @@ -58,14 +56,37 @@ def test_reusable_workflow_uses_oidc_callee_identity_before_trusted_checkout() - def test_reusable_workflow_fails_closed_for_non_cwl_callers() -> None: - """Require both jobs to reject repositories outside the owning organization before artifact access.""" + """Require organization ownership checks before every artifact API access.""" workflow = _text(WORKFLOW) + owner_check = 'test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER"' + artifact_api = '/actions/artifacts/${ARTIFACT_ID}' assert "EXPECTED_SOURCE_OWNER: ContextualWisdomLab" in workflow - assert workflow.count('test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER"') == 2 - first_artifact_access = workflow.index('/actions/artifacts/${ARTIFACT_ID}') - first_owner_check = workflow.index('test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER"') - assert first_owner_check < first_artifact_access + assert workflow.count(owner_check) >= workflow.count(artifact_api) >= 2 + cursor = 0 + while True: + artifact_position = workflow.find(artifact_api, cursor) + if artifact_position < 0: + break + owner_position = workflow.rfind(owner_check, 0, artifact_position) + assert owner_position >= 0 + cursor = artifact_position + 1 + + +def test_artifact_is_bounded_and_materialized_without_download_action_extraction() -> None: + """Bound the ZIP before extraction, authenticate its bytes, then use the trusted materializer.""" + workflow = _text(WORKFLOW) + materializer = _text(MATERIALIZER) + + assert "actions/download-artifact@" not in workflow + assert "MAX_EVIDENCE_ARTIFACT_BYTES:" in workflow + assert workflow.count(".size_in_bytes <= $max_size") == 2 + assert workflow.count("/actions/artifacts/${ARTIFACT_ID}/zip") == 2 + assert workflow.count("--max-filesize \"$MAX_EVIDENCE_ARTIFACT_BYTES\"") == 2 + assert workflow.count('test "sha256:${archive_sha}" = "$ARTIFACT_DIGEST"') == 2 + assert workflow.count("materialize_product_performance_artifact.py") >= 4 + assert "ZipFile.extract(" not in materializer + assert "ZipFile.extractall(" not in materializer def test_reusable_workflow_rechecks_same_run_artifact_and_never_executes_evidence() -> None: @@ -73,10 +94,7 @@ def test_reusable_workflow_rechecks_same_run_artifact_and_never_executes_evidenc workflow = _text(WORKFLOW) verifier = _text(VERIFIER) - assert workflow.count("/actions/artifacts/${ARTIFACT_ID}") >= 2 assert workflow.count(".workflow_run.id") >= 2 - assert workflow.count("artifact-ids: ${{ inputs.evidence_artifact_id }}") >= 2 - assert workflow.count(DOWNLOAD_ACTION_PIN) >= 2 assert workflow.count("verify_product_performance_evidence.py") >= 2 assert "subprocess" not in verifier assert "os.system" not in verifier From ed83f907f9ea63e9efe8875b6f8641def639e6b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:47:22 +0900 Subject: [PATCH 15/47] test(perf): cover bounded artifact materializer --- ...oduct_performance_artifact_materializer.py | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 tests/test_product_performance_artifact_materializer.py diff --git a/tests/test_product_performance_artifact_materializer.py b/tests/test_product_performance_artifact_materializer.py new file mode 100644 index 0000000000..728102ffc3 --- /dev/null +++ b/tests/test_product_performance_artifact_materializer.py @@ -0,0 +1,273 @@ +"""Unit contracts for bounded inert product-performance artifact materialization.""" + +from __future__ import annotations + +import io +import stat +import zipfile +from pathlib import Path + +import pytest + +from scripts.ci import materialize_product_performance_artifact as materializer + + +def _write_archive(path: Path, members: dict[str, bytes]) -> None: + """Write one deterministic deflated ZIP fixture.""" + with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for name, payload in members.items(): + archive.writestr(name, payload) + + +def _valid_members() -> dict[str, bytes]: + """Return the canonical three-member evidence fixture.""" + return { + "result.json": b'{"metrics":{"p95_ms":12.3}}', + "runtime.json": b'{"runner":"k6"}', + "fixture.json": b'{"records":[1]}', + } + + +def test_materialize_exact_three_file_archive(tmp_path: Path) -> None: + """Extract only the three expected root-level files into a new private directory.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + output = tmp_path / "sealed-evidence" + + manifest = materializer.materialize( + archive, + output, + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + assert sorted(path.name for path in output.iterdir()) == [ + "fixture.json", + "result.json", + "runtime.json", + ] + assert (output / "result.json").read_bytes() == _valid_members()["result.json"] + assert manifest["member_count"] == 3 + assert manifest["total_uncompressed_bytes"] == sum( + len(value) for value in _valid_members().values() + ) + + +@pytest.mark.parametrize( + "name", + ["../result.json", "nested/result.json", "nested\\result.json", ".", "..", ""], +) +def test_safe_filename_rejects_path_syntax(name: str) -> None: + """Reject traversal, nesting, and empty evidence member names.""" + with pytest.raises(materializer.MaterializationError, match="root-level filename"): + materializer._safe_filename(name, "result filename") + + +def test_materialize_rejects_extra_or_missing_member(tmp_path: Path) -> None: + """Require exact cardinality and exact expected names.""" + archive = tmp_path / "evidence.zip" + members = _valid_members() + members["extra.json"] = b"{}" + _write_archive(archive, members) + + with pytest.raises(materializer.MaterializationError, match="cardinality mismatch"): + materializer.materialize( + archive, + tmp_path / "output-extra", + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + archive.unlink() + members.pop("extra.json") + members.pop("runtime.json") + _write_archive(archive, members) + with pytest.raises(materializer.MaterializationError, match="cardinality mismatch"): + materializer.materialize( + archive, + tmp_path / "output-missing", + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + +def test_materialize_rejects_duplicate_member_names(tmp_path: Path) -> None: + """Reject ZIP central directories that repeat an evidence name.""" + archive = tmp_path / "evidence.zip" + with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as bundle: + bundle.writestr("result.json", b"{}") + bundle.writestr("result.json", b'{"replacement":true}') + bundle.writestr("runtime.json", b"{}") + bundle.writestr("fixture.json", b"{}") + + with pytest.raises(materializer.MaterializationError, match="duplicate"): + materializer.materialize( + archive, + tmp_path / "output", + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + +def test_materialize_rejects_symlink_member(tmp_path: Path) -> None: + """Reject a UNIX symlink entry rather than materializing its payload.""" + archive = tmp_path / "evidence.zip" + with zipfile.ZipFile(archive, "w") as bundle: + symlink = zipfile.ZipInfo("result.json") + symlink.create_system = 3 + symlink.external_attr = (stat.S_IFLNK | 0o777) << 16 + bundle.writestr(symlink, b"runtime.json") + bundle.writestr("runtime.json", b"{}") + bundle.writestr("fixture.json", b"{}") + + with pytest.raises(materializer.MaterializationError, match="regular file"): + materializer.materialize( + archive, + tmp_path / "output", + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + +def test_materialize_rejects_unsupported_compression(tmp_path: Path) -> None: + """Reject compression algorithms outside stored and deflated ZIP members.""" + archive = tmp_path / "evidence.zip" + with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_BZIP2) as bundle: + for name, payload in _valid_members().items(): + bundle.writestr(name, payload) + + with pytest.raises(materializer.MaterializationError, match="compression"): + materializer.materialize( + archive, + tmp_path / "output", + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + +def test_materialize_rejects_declared_member_over_limit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Reject an oversized member from central-directory metadata before extraction.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + monkeypatch.setattr(materializer, "_MAX_RESULT_BYTES", 1) + + with pytest.raises(materializer.MaterializationError, match="declared size"): + materializer.materialize( + archive, + tmp_path / "output", + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + +def test_stream_member_rejects_runtime_overrun_and_removes_partial_file(tmp_path: Path) -> None: + """Enforce byte counters during decompression rather than trusting declared metadata alone.""" + destination = tmp_path / "result.json" + + with pytest.raises(materializer.MaterializationError, match="exceeded"): + materializer._stream_member( + io.BytesIO(b"abcd"), destination, maximum_bytes=3, expected_size=4 + ) + + assert not destination.exists() + + +def test_stream_member_rejects_declared_size_mismatch(tmp_path: Path) -> None: + """Reject decompressed byte counts that disagree with the ZIP central directory.""" + destination = tmp_path / "result.json" + + with pytest.raises(materializer.MaterializationError, match="size mismatch"): + materializer._stream_member( + io.BytesIO(b"abc"), destination, maximum_bytes=10, expected_size=2 + ) + + assert not destination.exists() + + +def test_materialize_rejects_existing_output_directory(tmp_path: Path) -> None: + """Never merge trusted evidence into a pre-existing filesystem tree.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + output = tmp_path / "sealed-evidence" + output.mkdir() + + with pytest.raises(materializer.MaterializationError, match="must not already exist"): + materializer.materialize( + archive, + output, + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + +def test_materialize_rejects_non_zip_and_cleans_output(tmp_path: Path) -> None: + """Convert malformed archives into a stable fail-closed error without residual output.""" + archive = tmp_path / "evidence.zip" + archive.write_bytes(b"not a zip") + output = tmp_path / "sealed-evidence" + + with pytest.raises(materializer.MaterializationError, match="valid ZIP"): + materializer.materialize( + archive, + output, + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + assert not output.exists() + + +def test_materialize_rejects_archive_over_limit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Retain a local archive-size guard in addition to the workflow transfer bound.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + monkeypatch.setattr(materializer, "_MAX_ARCHIVE_BYTES", 1) + + with pytest.raises(materializer.MaterializationError, match="archive exceeds"): + materializer.materialize( + archive, + tmp_path / "output", + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + +def test_main_reports_materialization_error_without_traceback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """Expose deterministic invalid-artifact rejection through the CLI boundary.""" + parser = type( + "P", + (), + { + "parse_args": lambda self: type( + "A", + (), + { + "archive": str(tmp_path / "missing.zip"), + "output_dir": str(tmp_path / "output"), + "result_filename": "result.json", + "runtime_evidence_filename": "runtime.json", + "fixture_filename": "fixture.json", + }, + )() + }, + )() + monkeypatch.setattr(materializer, "_parser", lambda: parser) + + assert materializer.main() == 2 + assert "performance artifact rejected" in capsys.readouterr().err From 992a44c84b6f35f9decce76f6766734f17d1a9a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:47:50 +0900 Subject: [PATCH 16/47] feat(perf): add bounded inert artifact materializer --- ...aterialize_product_performance_artifact.py | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 scripts/ci/materialize_product_performance_artifact.py diff --git a/scripts/ci/materialize_product_performance_artifact.py b/scripts/ci/materialize_product_performance_artifact.py new file mode 100644 index 0000000000..60e4167ef8 --- /dev/null +++ b/scripts/ci/materialize_product_performance_artifact.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""Materialize one bounded product-performance ZIP as inert regular files.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import stat +import sys +import zipfile +from pathlib import Path +from typing import BinaryIO + +_MAX_ARCHIVE_BYTES = 300 * 1024 * 1024 +_MAX_RESULT_BYTES = 16 * 1024 * 1024 +_MAX_RUNTIME_BYTES = 16 * 1024 * 1024 +_MAX_FIXTURE_BYTES = 256 * 1024 * 1024 +_MAX_TOTAL_UNCOMPRESSED_BYTES = ( + _MAX_RESULT_BYTES + _MAX_RUNTIME_BYTES + _MAX_FIXTURE_BYTES +) +_ALLOWED_COMPRESSION = {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED} +_COPY_BLOCK_BYTES = 1024 * 1024 + + +class MaterializationError(ValueError): + """Describe a deterministic sealed-artifact materialization failure.""" + + +def _safe_filename(value: str, label: str) -> str: + """Return one non-empty root-level filename without path syntax.""" + if not value or value in {".", ".."} or Path(value).name != value: + raise MaterializationError(f"{label} must be one root-level filename") + if "/" in value or "\\" in value or "\x00" in value: + raise MaterializationError(f"{label} must be one root-level filename") + return value + + +def _require_regular_archive(path: Path) -> None: + """Require one bounded regular ZIP input without following a symlink.""" + try: + mode = path.lstat().st_mode + except FileNotFoundError as error: + raise MaterializationError("performance artifact archive is missing") from error + if stat.S_ISLNK(mode) or not stat.S_ISREG(mode): + raise MaterializationError("performance artifact archive must be a regular file") + if path.stat().st_size > _MAX_ARCHIVE_BYTES: + raise MaterializationError( + f"performance artifact archive exceeds {_MAX_ARCHIVE_BYTES} bytes" + ) + + +def _validate_output_parent(output_dir: Path) -> None: + """Require an existing regular directory parent and a new output path.""" + if output_dir.exists() or output_dir.is_symlink(): + raise MaterializationError("output directory must not already exist") + parent = output_dir.parent + try: + mode = parent.lstat().st_mode + except FileNotFoundError as error: + raise MaterializationError("output parent must already exist") from error + if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode): + raise MaterializationError("output parent must be a non-symlink directory") + + +def _validate_member( + member: zipfile.ZipInfo, maximum_bytes: int, expected_name: str +) -> None: + """Reject unsafe ZIP metadata before any member is decompressed.""" + if member.filename != expected_name: + raise MaterializationError(f"unexpected evidence member: {member.filename}") + if member.is_dir(): + raise MaterializationError(f"evidence member must be a regular file: {member.filename}") + if member.flag_bits & 0x1: + raise MaterializationError(f"encrypted ZIP member is forbidden: {member.filename}") + if member.compress_type not in _ALLOWED_COMPRESSION: + raise MaterializationError( + f"unsupported ZIP compression for evidence member: {member.filename}" + ) + mode = (member.external_attr >> 16) & 0xFFFF + file_type = stat.S_IFMT(mode) + if file_type not in {0, stat.S_IFREG}: + raise MaterializationError(f"evidence member must be a regular file: {member.filename}") + if member.file_size < 0 or member.file_size > maximum_bytes: + raise MaterializationError( + f"declared size exceeds limit for {member.filename}: {member.file_size}" + ) + + +def _stream_member( + source: BinaryIO, destination: Path, maximum_bytes: int, expected_size: int +) -> int: + """Stream one decompressed member with runtime byte-count enforcement.""" + descriptor = os.open( + destination, + os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), + 0o400, + ) + total = 0 + try: + with os.fdopen(descriptor, "wb") as target: + while True: + block = source.read(_COPY_BLOCK_BYTES) + if not block: + break + total += len(block) + if total > maximum_bytes: + raise MaterializationError( + f"decompressed evidence member exceeded {maximum_bytes} bytes" + ) + target.write(block) + target.flush() + os.fsync(target.fileno()) + if total != expected_size: + raise MaterializationError( + f"decompressed size mismatch: expected {expected_size}, got {total}" + ) + os.chmod(destination, 0o400) + return total + except Exception: + try: + destination.unlink() + except FileNotFoundError: + pass + raise + + +def materialize( + archive_path: Path, + output_dir: Path, + *, + result_filename: str, + runtime_filename: str, + fixture_filename: str, +) -> dict[str, int]: + """Validate and stream exactly three bounded evidence members into a new tree.""" + archive = Path(os.path.abspath(archive_path)) + output = Path(os.path.abspath(output_dir)) + _require_regular_archive(archive) + _validate_output_parent(output) + + names = { + "result": _safe_filename(result_filename, "result filename"), + "runtime": _safe_filename(runtime_filename, "runtime evidence filename"), + "fixture": _safe_filename(fixture_filename, "fixture filename"), + } + if len(set(names.values())) != 3: + raise MaterializationError("result, runtime, and fixture filenames must be distinct") + limits = { + names["result"]: _MAX_RESULT_BYTES, + names["runtime"]: _MAX_RUNTIME_BYTES, + names["fixture"]: _MAX_FIXTURE_BYTES, + } + + try: + bundle = zipfile.ZipFile(archive, "r") + except (OSError, zipfile.BadZipFile) as error: + raise MaterializationError("performance artifact must be a valid ZIP archive") from error + + created_output = False + try: + with bundle: + members = bundle.infolist() + member_names = [member.filename for member in members] + if len(member_names) != len(set(member_names)): + raise MaterializationError("duplicate ZIP evidence member name") + expected_names = set(names.values()) + actual_names = set(member_names) + if actual_names != expected_names or len(members) != 3: + missing = sorted(expected_names - actual_names) + extra = sorted(actual_names - expected_names) + raise MaterializationError( + f"evidence cardinality mismatch; missing={missing}, extra={extra}" + ) + + declared_total = 0 + by_name = {member.filename: member for member in members} + for filename in sorted(expected_names): + member = by_name[filename] + _validate_member(member, limits[filename], filename) + declared_total += member.file_size + if declared_total > _MAX_TOTAL_UNCOMPRESSED_BYTES: + raise MaterializationError("declared uncompressed evidence total exceeds limit") + + output.mkdir(mode=0o700) + created_output = True + total = 0 + for filename in sorted(expected_names): + member = by_name[filename] + try: + source = bundle.open(member, "r") + except (RuntimeError, NotImplementedError, zipfile.BadZipFile) as error: + raise MaterializationError( + f"unable to open evidence member: {filename}" + ) from error + try: + count = _stream_member( + source, + output / filename, + maximum_bytes=limits[filename], + expected_size=member.file_size, + ) + except (OSError, RuntimeError, zipfile.BadZipFile) as error: + if isinstance(error, MaterializationError): + raise + raise MaterializationError( + f"unable to materialize evidence member: {filename}" + ) from error + finally: + source.close() + total += count + if total > _MAX_TOTAL_UNCOMPRESSED_BYTES: + raise MaterializationError("materialized evidence total exceeds limit") + os.chmod(output, 0o700) + return {"member_count": 3, "total_uncompressed_bytes": total} + except Exception: + if created_output: + shutil.rmtree(output, ignore_errors=True) + raise + + +def _parser() -> argparse.ArgumentParser: + """Create the strict CLI parser for one sealed performance artifact.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--archive", required=True) + parser.add_argument("--output-dir", required=True) + parser.add_argument("--result-filename", required=True) + parser.add_argument("--runtime-evidence-filename", required=True) + parser.add_argument("--fixture-filename", required=True) + return parser + + +def main() -> int: + """Run bounded materialization and expose stable invalid-artifact failure.""" + arguments = _parser().parse_args() + try: + materialize( + Path(arguments.archive), + Path(arguments.output_dir), + result_filename=arguments.result_filename, + runtime_filename=arguments.runtime_evidence_filename, + fixture_filename=arguments.fixture_filename, + ) + except MaterializationError as error: + print(f"performance artifact rejected: {error}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": # pragma: no cover - CLI dispatch is covered via main() + raise SystemExit(main()) # pragma: no cover From f33217717c27aebdf8bced1530d886db268a168d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:49:09 +0900 Subject: [PATCH 17/47] fix(perf): bound artifact before inert extraction --- .../product-performance-attestation.yml | 83 +++++++++++++++---- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/.github/workflows/product-performance-attestation.yml b/.github/workflows/product-performance-attestation.yml index b6621687c4..421dcf8586 100644 --- a/.github/workflows/product-performance-attestation.yml +++ b/.github/workflows/product-performance-attestation.yml @@ -57,6 +57,7 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true PERFORMANCE_PREDICATE_TYPE: https://contextualwisdomlab.org/attestations/product-performance/v1 EXPECTED_SOURCE_OWNER: ContextualWisdomLab + MAX_EVIDENCE_ARTIFACT_BYTES: "314572800" jobs: verify-performance-evidence: @@ -159,10 +160,12 @@ jobs: ref: ${{ steps.workflow-identity.outputs.workflow_sha }} path: trusted-intake persist-credentials: false - sparse-checkout: scripts/ci/verify_product_performance_evidence.py + sparse-checkout: | + scripts/ci/materialize_product_performance_artifact.py + scripts/ci/verify_product_performance_evidence.py sparse-checkout-cone-mode: false - - name: Verify immutable same-run artifact metadata + - name: Verify metadata and download bounded immutable artifact env: GH_TOKEN: ${{ github.token }} SOURCE_REPOSITORY: ${{ inputs.source_repository }} @@ -181,14 +184,35 @@ jobs: --arg digest "$ARTIFACT_DIGEST" \ --argjson artifact_id "$ARTIFACT_ID" \ --argjson run_id "$GITHUB_RUN_ID" \ - '.id == $artifact_id and .name == $name and .digest == $digest and .workflow_run.id == $run_id and .expired == false' \ + --argjson max_size "$MAX_EVIDENCE_ARTIFACT_BYTES" \ + '.id == $artifact_id and .name == $name and .digest == $digest and .workflow_run.id == $run_id and .expired == false and (.size_in_bytes | type) == "number" and .size_in_bytes > 0 and .size_in_bytes <= $max_size' \ <<<"$artifact_json" >/dev/null - - - name: Download exact same-run evidence by immutable artifact ID - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ inputs.evidence_artifact_id }} - path: sealed-evidence + archive_path="${RUNNER_TEMP}/sealed-performance-evidence.zip" + test ! -e "$archive_path" + curl --fail --silent --show-error --location \ + --header "Authorization: Bearer ${GH_TOKEN}" \ + --header "Accept: application/vnd.github+json" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + --max-filesize "$MAX_EVIDENCE_ARTIFACT_BYTES" \ + --output "$archive_path" \ + "https://api.github.com/repos/${SOURCE_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" + test "$(stat -c '%s' "$archive_path")" -le "$MAX_EVIDENCE_ARTIFACT_BYTES" + read -r archive_sha _ < <(sha256sum "$archive_path") + test "sha256:${archive_sha}" = "$ARTIFACT_DIGEST" + + - name: Materialize exact bounded evidence members + env: + RESULT_FILENAME: ${{ inputs.result_filename }} + RUNTIME_FILENAME: ${{ inputs.runtime_evidence_filename }} + FIXTURE_FILENAME: ${{ inputs.fixture_filename }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python3 -I trusted-intake/scripts/ci/materialize_product_performance_artifact.py \ + --archive "${RUNNER_TEMP}/sealed-performance-evidence.zip" \ + --output-dir sealed-evidence \ + --result-filename "$RESULT_FILENAME" \ + --runtime-evidence-filename "$RUNTIME_FILENAME" \ + --fixture-filename "$FIXTURE_FILENAME" - name: Verify sealed performance evidence as inert bounded data env: @@ -333,10 +357,12 @@ jobs: ref: ${{ steps.workflow-identity.outputs.workflow_sha }} path: trusted-signer persist-credentials: false - sparse-checkout: scripts/ci/verify_product_performance_evidence.py + sparse-checkout: | + scripts/ci/materialize_product_performance_artifact.py + scripts/ci/verify_product_performance_evidence.py sparse-checkout-cone-mode: false - - name: Verify immutable same-run artifact metadata + - name: Verify metadata and download bounded immutable artifact env: GH_TOKEN: ${{ github.token }} SOURCE_REPOSITORY: ${{ inputs.source_repository }} @@ -355,14 +381,35 @@ jobs: --arg digest "$ARTIFACT_DIGEST" \ --argjson artifact_id "$ARTIFACT_ID" \ --argjson run_id "$GITHUB_RUN_ID" \ - '.id == $artifact_id and .name == $name and .digest == $digest and .workflow_run.id == $run_id and .expired == false' \ + --argjson max_size "$MAX_EVIDENCE_ARTIFACT_BYTES" \ + '.id == $artifact_id and .name == $name and .digest == $digest and .workflow_run.id == $run_id and .expired == false and (.size_in_bytes | type) == "number" and .size_in_bytes > 0 and .size_in_bytes <= $max_size' \ <<<"$artifact_json" >/dev/null - - - name: Download exact sealed evidence without executing it - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ inputs.evidence_artifact_id }} - path: sealed-evidence + archive_path="${RUNNER_TEMP}/sealed-performance-evidence.zip" + test ! -e "$archive_path" + curl --fail --silent --show-error --location \ + --header "Authorization: Bearer ${GH_TOKEN}" \ + --header "Accept: application/vnd.github+json" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + --max-filesize "$MAX_EVIDENCE_ARTIFACT_BYTES" \ + --output "$archive_path" \ + "https://api.github.com/repos/${SOURCE_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" + test "$(stat -c '%s' "$archive_path")" -le "$MAX_EVIDENCE_ARTIFACT_BYTES" + read -r archive_sha _ < <(sha256sum "$archive_path") + test "sha256:${archive_sha}" = "$ARTIFACT_DIGEST" + + - name: Materialize exact bounded evidence members + env: + RESULT_FILENAME: ${{ inputs.result_filename }} + RUNTIME_FILENAME: ${{ inputs.runtime_evidence_filename }} + FIXTURE_FILENAME: ${{ inputs.fixture_filename }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python3 -I trusted-signer/scripts/ci/materialize_product_performance_artifact.py \ + --archive "${RUNNER_TEMP}/sealed-performance-evidence.zip" \ + --output-dir sealed-evidence \ + --result-filename "$RESULT_FILENAME" \ + --runtime-evidence-filename "$RUNTIME_FILENAME" \ + --fixture-filename "$FIXTURE_FILENAME" - name: Reverify evidence and build trusted predicate inside signer env: From 84b4dc43bddb19a6a881a402f43cacf0e4a388e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:49:25 +0900 Subject: [PATCH 18/47] ci(perf): gate bounded artifact materializer --- .../workflows/product-performance-attestation-quality.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/product-performance-attestation-quality.yml b/.github/workflows/product-performance-attestation-quality.yml index 44e668d28d..0118b5cc03 100644 --- a/.github/workflows/product-performance-attestation-quality.yml +++ b/.github/workflows/product-performance-attestation-quality.yml @@ -6,7 +6,9 @@ on: paths: - ".github/workflows/product-performance-attestation.yml" - ".github/workflows/product-performance-attestation-quality.yml" + - "scripts/ci/materialize_product_performance_artifact.py" - "scripts/ci/verify_product_performance_evidence.py" + - "tests/test_product_performance_artifact_materializer.py" - "tests/test_product_performance_attestation_contract.py" - "tests/test_product_performance_evidence_verifier.py" - "docs/doctoring/product-performance-attestation.md" @@ -52,7 +54,9 @@ jobs: run: | test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" python -m compileall -q \ + scripts/ci/materialize_product_performance_artifact.py \ scripts/ci/verify_product_performance_evidence.py \ + tests/test_product_performance_artifact_materializer.py \ tests/test_product_performance_attestation_contract.py \ tests/test_product_performance_evidence_verifier.py @@ -80,12 +84,14 @@ jobs: test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" python -m coverage erase python -m coverage run --branch -m pytest -q \ + tests/test_product_performance_artifact_materializer.py \ tests/test_product_performance_attestation_contract.py \ tests/test_product_performance_evidence_verifier.py python -m coverage report \ - --include=scripts/ci/verify_product_performance_evidence.py \ + --include=scripts/ci/materialize_product_performance_artifact.py,scripts/ci/verify_product_performance_evidence.py \ --show-missing \ --fail-under=100 python -m interrogate --fail-under 100 \ + scripts/ci/materialize_product_performance_artifact.py \ scripts/ci/verify_product_performance_evidence.py git diff --check "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" From a7ed6ee7db1af398e47eb5563bd0ecb8d5c60c11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:50:51 +0900 Subject: [PATCH 19/47] test(perf): align owner gate with bounded download step --- ...roduct_performance_attestation_contract.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/test_product_performance_attestation_contract.py b/tests/test_product_performance_attestation_contract.py index 3eb1e716ec..60fdbe2c12 100644 --- a/tests/test_product_performance_attestation_contract.py +++ b/tests/test_product_performance_attestation_contract.py @@ -56,21 +56,24 @@ def test_reusable_workflow_uses_oidc_callee_identity_before_trusted_checkout() - def test_reusable_workflow_fails_closed_for_non_cwl_callers() -> None: - """Require organization ownership checks before every artifact API access.""" + """Require organization ownership checks before metadata and archive access in both jobs.""" workflow = _text(WORKFLOW) + step_name = "- name: Verify metadata and download bounded immutable artifact" owner_check = 'test "${SOURCE_REPOSITORY%%/*}" = "$EXPECTED_SOURCE_OWNER"' - artifact_api = '/actions/artifacts/${ARTIFACT_ID}' + metadata_api = '/actions/artifacts/${ARTIFACT_ID}\")' + archive_api = '/actions/artifacts/${ARTIFACT_ID}/zip"' assert "EXPECTED_SOURCE_OWNER: ContextualWisdomLab" in workflow - assert workflow.count(owner_check) >= workflow.count(artifact_api) >= 2 + assert workflow.count(step_name) == 2 + assert workflow.count(owner_check) == 2 cursor = 0 - while True: - artifact_position = workflow.find(artifact_api, cursor) - if artifact_position < 0: - break - owner_position = workflow.rfind(owner_check, 0, artifact_position) - assert owner_position >= 0 - cursor = artifact_position + 1 + for _ in range(2): + step_position = workflow.index(step_name, cursor) + owner_position = workflow.index(owner_check, step_position) + metadata_position = workflow.index(metadata_api, owner_position) + archive_position = workflow.index(archive_api, metadata_position) + assert step_position < owner_position < metadata_position < archive_position + cursor = archive_position + len(archive_api) def test_artifact_is_bounded_and_materialized_without_download_action_extraction() -> None: From 3835ba56887d544be8b3c31b28e6e744bcdb9dd1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:52:06 +0900 Subject: [PATCH 20/47] docs(perf): record authenticated bounded evidence boundary --- .../product-performance-attestation.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/doctoring/product-performance-attestation.md diff --git a/docs/doctoring/product-performance-attestation.md b/docs/doctoring/product-performance-attestation.md new file mode 100644 index 0000000000..2c2756a9bf --- /dev/null +++ b/docs/doctoring/product-performance-attestation.md @@ -0,0 +1,76 @@ +# Product performance evidence attestation + +## Problem + +Product performance acceptance needs two separate decisions that must not be collapsed into one status: + +1. whether a measured result, runtime observation, and right-cleared fixture are the exact bytes produced by an authorized CWL workflow run; and +2. whether those bytes satisfy the product's latency, production-equivalence, scientific-validity, and data-rights acceptance policy. + +A product-local SHA-256 token cannot establish the first decision when the caller can replace both the evidence and the token. Likewise, a generic `PASS` emitted by the central verifier would incorrectly imply the second decision. + +This boundary was introduced from `ContextualWisdomLab/Orgmetra#316/#317` and is owned centrally by `.github#2162`. HR-domain semantics, workload construction, right-cleared data, resource/cleanup observations, and the p95 policy remain with Orgmetra. + +## Decision + +`.github/workflows/product-performance-attestation.yml` is the organization-owned reusable signer. Callers must pin it by a full commit SHA. The workflow accepts exact caller/source and evidence identities, but it does not trust those inputs on their own. + +Before any caller artifact is used, both jobs: + +- resolve the called reusable workflow identity from GitHub Actions OIDC `job_workflow_ref` and `job_workflow_sha`; +- require a GitHub-hosted signer runner; +- require the caller repository to be in the `ContextualWisdomLab` organization; +- require the supplied source repository and SHA to equal the caller `GITHUB_REPOSITORY` and `GITHUB_SHA`; +- re-read immutable artifact ID, name, digest, workflow-run identity, expiry state, and compressed size from the GitHub REST API. + +The central signer attests **origin and byte integrity only**. The predicate is `https://contextualwisdomlab.org/attestations/product-performance/v1` and explicitly records that it does not prove latency-threshold success, production equivalence, fixture scientific validity, or fixture right clearance. Structural verification is recorded as `verification_result: VALID`; the central layer must not emit a generic performance `PASS`. + +## Bounded inert artifact handling + +The artifact is bounded before extraction. GitHub's artifact metadata `size_in_bytes` must be positive and no greater than 300 MiB. The ZIP is then downloaded from the immutable artifact-ID endpoint with curl's transfer-size limit, rechecked by filesystem size, and hashed. Its SHA-256 must equal the GitHub artifact digest before any member is decompressed. + +`materialize_product_performance_artifact.py` then treats the archive as hostile inert data: + +- exactly three distinct root-level files are allowed: result, runtime evidence, and fixture; +- directories, traversal/nested paths, duplicate names, symlinks/non-regular UNIX entries, encryption, and compression other than stored/deflated are rejected; +- declared member sizes are bounded before decompression: 16 MiB result, 16 MiB runtime evidence, 256 MiB fixture; +- decompressed bytes are streamed with independent counters so central-directory declarations alone cannot bypass the limits; +- extraction occurs only into a newly created private directory; `ZipFile.extract()` and `extractall()` are not used; +- the existing strict verifier then re-hashes each materialized file, requires exact three-file cardinality, strict UTF-8 JSON objects, no duplicate JSON keys or non-finite numbers, and exact caller-provided per-file digests. + +The verifier job and the credentialed signer job independently repeat artifact metadata, bounded download, archive authentication, materialization, and file verification. Product code or product-provided scripts are never executed in the `attestations: write` job. + +## Attestation and verification + +The signer uses the immutable `actions/attest` v4.1.0 commit `59d89421af93a897026c735860bf21b6eb4f7b26` to attest the exact result subject digest with the trusted predicate. The workflow then verifies the result online against the caller repository, central signer repository/workflow, exact source digest, and predicate type. + +For offline verification it retains the Sigstore bundle, trusted root, predicate, verifier manifest, and SHA-256 inventory. The retained README contains the exact online and offline `gh attestation verify` commands. + +A valid bundle therefore answers “these are the authenticated evidence bytes for this exact CWL source/run.” It does **not** answer “p95 passed.” A product may issue a positive commercial performance receipt only after its own acceptance logic validates the authenticated evidence under its domain policy. + +## Rejected alternatives + +- **Caller-supplied result digest as trust root.** Rejected because a caller that can replace the result can also recompute the digest. +- **`github.workflow_sha` as reusable-workflow source identity.** Rejected for cross-repository callers because the reusable workflow inherits caller context. OIDC `job_workflow_ref`/`job_workflow_sha` is the prerequisite repair owned by #2164/#1228. +- **`actions/download-artifact` extraction before bounded validation.** Rejected because the archive would be expanded before the trusted verifier can enforce uncompressed limits. The current path authenticates and bounds the ZIP first, then uses the central materializer. +- **Central latency `PASS`.** Rejected because the organization signer owns evidence authenticity, not product workload semantics or acceptance thresholds. + +## Verification gates + +The quality workflow compiles the materializer, verifier, and tests on Python 3.10 and runs exact-head tests on the current quality Python. Both production scripts must retain 100% statement/branch coverage and 100% docstring coverage. Security/SAST/CodeQL remain separate hosted gates; non-terminal or central-control-plane failures are not converted into local GREEN evidence. + +This contract remains mutable until its prerequisite stack is merged through protected review and a consumer canary verifies the immutable central workflow. Orgmetra must remain fail closed until then. + +## References + +GitHub. (2026). *REST API endpoints for GitHub Actions artifacts*. GitHub Docs. https://docs.github.com/en/rest/actions/artifacts + +GitHub. (2026). *Using artifact attestations and reusable workflows to achieve SLSA v1 Build Level 3*. GitHub Docs. https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/increase-security-rating + +GitHub. (2026). *Verifying attestations offline*. GitHub Docs. https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/verify-attestations-offline + +GitHub. (2026). *Reusing workflow configurations*. GitHub Docs. https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows + +GitHub. (2026). `actions/attest` v4.1.0, commit `59d89421af93a897026c735860bf21b6eb4f7b26`. + +GitHub. (2026). `actions/upload-artifact` v7 documentation. The uploaded artifact digest is SHA-256 and the displayed artifact size refers to the ZIP representation. https://github.com/actions/upload-artifact From 0919c1a47613d06f52a5d13d090476090ff31999 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:52:19 +0900 Subject: [PATCH 21/47] docs(perf): changelog bounded performance attestation --- .../20260913-product-performance-attestation.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 CHANGELOG.d/20260913-product-performance-attestation.md diff --git a/CHANGELOG.d/20260913-product-performance-attestation.md b/CHANGELOG.d/20260913-product-performance-attestation.md new file mode 100644 index 0000000000..ce6fb31244 --- /dev/null +++ b/CHANGELOG.d/20260913-product-performance-attestation.md @@ -0,0 +1,11 @@ +## Added + +- Added an organization-owned reusable product-performance evidence attestation workflow with an OIDC-bound reusable-workflow source identity, CWL-only caller boundary, same-run artifact identity checks, versioned custom predicate, GitHub/Sigstore attestation, and retained online/offline verification evidence. +- Added strict data-only result/runtime/fixture verification and a bounded ZIP materializer that authenticates the compressed artifact before extraction and rejects traversal, duplicate names, non-regular entries, unsupported compression, oversized members, and decompression byte-count drift. +- Added exact-head quality contracts for Python 3.10 compatibility, Python 3.14 tests, 100% branch coverage, and 100% production docstring coverage across the performance verifier and materializer. + +## Changed + +- Structural evidence verification is now recorded as `verification_result: VALID` rather than the ambiguous `result: PASS`; central attestation does not claim that product latency or production-equivalence acceptance passed. +- Reusable performance attestation now fails closed for caller repositories outside `ContextualWisdomLab` before artifact API access. +- Performance evidence no longer relies on `actions/download-artifact` extraction before resource bounds. The workflow first bounds GitHub artifact metadata, limits the ZIP transfer, verifies its artifact digest, and then performs trusted streaming materialization. From 9c4fc53f24178986baaf0e36dee71e950342f869 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:53:33 +0900 Subject: [PATCH 22/47] refactor(perf): simplify bounded materialization invariants --- ...aterialize_product_performance_artifact.py | 95 ++++++------------- 1 file changed, 29 insertions(+), 66 deletions(-) diff --git a/scripts/ci/materialize_product_performance_artifact.py b/scripts/ci/materialize_product_performance_artifact.py index 60e4167ef8..942bb8b374 100644 --- a/scripts/ci/materialize_product_performance_artifact.py +++ b/scripts/ci/materialize_product_performance_artifact.py @@ -16,9 +16,6 @@ _MAX_RESULT_BYTES = 16 * 1024 * 1024 _MAX_RUNTIME_BYTES = 16 * 1024 * 1024 _MAX_FIXTURE_BYTES = 256 * 1024 * 1024 -_MAX_TOTAL_UNCOMPRESSED_BYTES = ( - _MAX_RESULT_BYTES + _MAX_RUNTIME_BYTES + _MAX_FIXTURE_BYTES -) _ALLOWED_COMPRESSION = {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED} _COPY_BLOCK_BYTES = 1024 * 1024 @@ -63,14 +60,8 @@ def _validate_output_parent(output_dir: Path) -> None: raise MaterializationError("output parent must be a non-symlink directory") -def _validate_member( - member: zipfile.ZipInfo, maximum_bytes: int, expected_name: str -) -> None: - """Reject unsafe ZIP metadata before any member is decompressed.""" - if member.filename != expected_name: - raise MaterializationError(f"unexpected evidence member: {member.filename}") - if member.is_dir(): - raise MaterializationError(f"evidence member must be a regular file: {member.filename}") +def _validate_member(member: zipfile.ZipInfo, maximum_bytes: int) -> None: + """Reject unsafe ZIP metadata before one expected member is decompressed.""" if member.flag_bits & 0x1: raise MaterializationError(f"encrypted ZIP member is forbidden: {member.filename}") if member.compress_type not in _ALLOWED_COMPRESSION: @@ -81,7 +72,7 @@ def _validate_member( file_type = stat.S_IFMT(mode) if file_type not in {0, stat.S_IFREG}: raise MaterializationError(f"evidence member must be a regular file: {member.filename}") - if member.file_size < 0 or member.file_size > maximum_bytes: + if member.file_size > maximum_bytes: raise MaterializationError( f"declared size exceeds limit for {member.filename}: {member.file_size}" ) @@ -118,10 +109,7 @@ def _stream_member( os.chmod(destination, 0o400) return total except Exception: - try: - destination.unlink() - except FileNotFoundError: - pass + destination.unlink(missing_ok=True) raise @@ -157,66 +145,41 @@ def materialize( except (OSError, zipfile.BadZipFile) as error: raise MaterializationError("performance artifact must be a valid ZIP archive") from error - created_output = False - try: - with bundle: - members = bundle.infolist() - member_names = [member.filename for member in members] - if len(member_names) != len(set(member_names)): - raise MaterializationError("duplicate ZIP evidence member name") - expected_names = set(names.values()) - actual_names = set(member_names) - if actual_names != expected_names or len(members) != 3: - missing = sorted(expected_names - actual_names) - extra = sorted(actual_names - expected_names) - raise MaterializationError( - f"evidence cardinality mismatch; missing={missing}, extra={extra}" - ) - - declared_total = 0 - by_name = {member.filename: member for member in members} - for filename in sorted(expected_names): - member = by_name[filename] - _validate_member(member, limits[filename], filename) - declared_total += member.file_size - if declared_total > _MAX_TOTAL_UNCOMPRESSED_BYTES: - raise MaterializationError("declared uncompressed evidence total exceeds limit") - - output.mkdir(mode=0o700) - created_output = True - total = 0 + with bundle: + members = bundle.infolist() + member_names = [member.filename for member in members] + if len(member_names) != len(set(member_names)): + raise MaterializationError("duplicate ZIP evidence member name") + expected_names = set(names.values()) + actual_names = set(member_names) + if actual_names != expected_names or len(members) != 3: + missing = sorted(expected_names - actual_names) + extra = sorted(actual_names - expected_names) + raise MaterializationError( + f"evidence cardinality mismatch; missing={missing}, extra={extra}" + ) + + by_name = {member.filename: member for member in members} + for filename in sorted(expected_names): + _validate_member(by_name[filename], limits[filename]) + + output.mkdir(mode=0o700) + total = 0 + try: for filename in sorted(expected_names): member = by_name[filename] - try: - source = bundle.open(member, "r") - except (RuntimeError, NotImplementedError, zipfile.BadZipFile) as error: - raise MaterializationError( - f"unable to open evidence member: {filename}" - ) from error - try: - count = _stream_member( + with bundle.open(member, "r") as source: + total += _stream_member( source, output / filename, maximum_bytes=limits[filename], expected_size=member.file_size, ) - except (OSError, RuntimeError, zipfile.BadZipFile) as error: - if isinstance(error, MaterializationError): - raise - raise MaterializationError( - f"unable to materialize evidence member: {filename}" - ) from error - finally: - source.close() - total += count - if total > _MAX_TOTAL_UNCOMPRESSED_BYTES: - raise MaterializationError("materialized evidence total exceeds limit") os.chmod(output, 0o700) return {"member_count": 3, "total_uncompressed_bytes": total} - except Exception: - if created_output: + except Exception: shutil.rmtree(output, ignore_errors=True) - raise + raise def _parser() -> argparse.ArgumentParser: From 56d9be31a701fd8f63d6905f9c3bf9cf7e5dafc2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 00:02:18 +0900 Subject: [PATCH 23/47] fix(perf): remove redundant directory chmod false positive --- scripts/ci/materialize_product_performance_artifact.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/ci/materialize_product_performance_artifact.py b/scripts/ci/materialize_product_performance_artifact.py index 942bb8b374..e871b88e4f 100644 --- a/scripts/ci/materialize_product_performance_artifact.py +++ b/scripts/ci/materialize_product_performance_artifact.py @@ -175,7 +175,6 @@ def materialize( maximum_bytes=limits[filename], expected_size=member.file_size, ) - os.chmod(output, 0o700) return {"member_count": 3, "total_uncompressed_bytes": total} except Exception: shutil.rmtree(output, ignore_errors=True) From 44445484181a5330ebaefcad37a9aadc8541672b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 00:04:45 +0900 Subject: [PATCH 24/47] test(perf): close materializer security edge coverage --- ...oduct_performance_artifact_materializer.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/test_product_performance_artifact_materializer.py b/tests/test_product_performance_artifact_materializer.py index 728102ffc3..c3cefbba07 100644 --- a/tests/test_product_performance_artifact_materializer.py +++ b/tests/test_product_performance_artifact_materializer.py @@ -48,6 +48,9 @@ def test_materialize_exact_three_file_archive(tmp_path: Path) -> None: "runtime.json", ] assert (output / "result.json").read_bytes() == _valid_members()["result.json"] + assert stat.S_IMODE(output.stat().st_mode) & 0o077 == 0 + for name in _valid_members(): + assert stat.S_IMODE((output / name).stat().st_mode) == 0o400 assert manifest["member_count"] == 3 assert manifest["total_uncompressed_bytes"] == sum( len(value) for value in _valid_members().values() @@ -246,6 +249,111 @@ def test_materialize_rejects_archive_over_limit( ) +def test_require_regular_archive_rejects_directory_and_symlink(tmp_path: Path) -> None: + """Reject non-regular archive paths before ZIP parsing.""" + directory = tmp_path / "directory.zip" + directory.mkdir() + with pytest.raises(materializer.MaterializationError, match="regular file"): + materializer._require_regular_archive(directory) + + target = tmp_path / "target.zip" + _write_archive(target, _valid_members()) + symlink = tmp_path / "symlink.zip" + symlink.symlink_to(target.name) + with pytest.raises(materializer.MaterializationError, match="regular file"): + materializer._require_regular_archive(symlink) + + +def test_validate_output_parent_rejects_missing_symlink_and_file_parent(tmp_path: Path) -> None: + """Reject output roots whose immediate trusted parent is absent or not a directory.""" + with pytest.raises(materializer.MaterializationError, match="parent must already exist"): + materializer._validate_output_parent(tmp_path / "missing" / "output") + + real_parent = tmp_path / "real-parent" + real_parent.mkdir() + linked_parent = tmp_path / "linked-parent" + linked_parent.symlink_to(real_parent, target_is_directory=True) + with pytest.raises(materializer.MaterializationError, match="non-symlink directory"): + materializer._validate_output_parent(linked_parent / "output") + + file_parent = tmp_path / "file-parent" + file_parent.write_text("x", encoding="utf-8") + with pytest.raises(materializer.MaterializationError, match="non-symlink directory"): + materializer._validate_output_parent(file_parent / "output") + + +def test_validate_member_rejects_encrypted_metadata() -> None: + """Reject encrypted ZIP metadata before any decompression attempt.""" + member = zipfile.ZipInfo("result.json") + member.flag_bits |= 0x1 + with pytest.raises(materializer.MaterializationError, match="encrypted"): + materializer._validate_member(member, 1024) + + +def test_materialize_rejects_duplicate_requested_filenames(tmp_path: Path) -> None: + """Keep semantic result, runtime, and fixture roles bound to distinct files.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + with pytest.raises(materializer.MaterializationError, match="must be distinct"): + materializer.materialize( + archive, + tmp_path / "output", + result_filename="result.json", + runtime_filename="result.json", + fixture_filename="fixture.json", + ) + + +def test_materialize_cleans_output_after_stream_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Remove the whole materialized tree when any member fails during streaming.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + output = tmp_path / "output" + + def fail_stream(*args: object, **kwargs: object) -> int: + """Simulate a decompression-time domain failure.""" + del args, kwargs + raise materializer.MaterializationError("stream failed") + + monkeypatch.setattr(materializer, "_stream_member", fail_stream) + with pytest.raises(materializer.MaterializationError, match="stream failed"): + materializer.materialize( + archive, + output, + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + assert not output.exists() + + +def test_main_returns_zero_for_valid_archive( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Expose successful bounded materialization through the CLI boundary.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + output = tmp_path / "output" + arguments = type( + "A", + (), + { + "archive": str(archive), + "output_dir": str(output), + "result_filename": "result.json", + "runtime_evidence_filename": "runtime.json", + "fixture_filename": "fixture.json", + }, + )() + parser = type("P", (), {"parse_args": lambda self: arguments})() + monkeypatch.setattr(materializer, "_parser", lambda: parser) + + assert materializer.main() == 0 + assert output.is_dir() + + def test_main_reports_materialization_error_without_traceback( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: From 3f4894c95e5710b88d8382e0e13b640e15ceaad4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 00:17:15 +0900 Subject: [PATCH 25/47] fix(perf): normalize corrupt ZIP member failures --- scripts/ci/materialize_product_performance_artifact.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/ci/materialize_product_performance_artifact.py b/scripts/ci/materialize_product_performance_artifact.py index e871b88e4f..834dd8da3a 100644 --- a/scripts/ci/materialize_product_performance_artifact.py +++ b/scripts/ci/materialize_product_performance_artifact.py @@ -176,6 +176,11 @@ def materialize( expected_size=member.file_size, ) return {"member_count": 3, "total_uncompressed_bytes": total} + except zipfile.BadZipFile as error: + shutil.rmtree(output, ignore_errors=True) + raise MaterializationError( + "performance artifact member data is corrupted" + ) from error except Exception: shutil.rmtree(output, ignore_errors=True) raise From 93d26bcb8c4f131004cbaae5cbec31497b3e2ff5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 00:17:52 +0900 Subject: [PATCH 26/47] test(perf): cover corrupt ZIP member failure --- ...oduct_performance_artifact_materializer.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_product_performance_artifact_materializer.py b/tests/test_product_performance_artifact_materializer.py index c3cefbba07..d661412c69 100644 --- a/tests/test_product_performance_artifact_materializer.py +++ b/tests/test_product_performance_artifact_materializer.py @@ -354,6 +354,40 @@ def test_main_returns_zero_for_valid_archive( assert output.is_dir() +def test_main_reports_corrupt_member_without_traceback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """Normalize ZIP member corruption into the stable fail-closed CLI result.""" + archive = tmp_path / "evidence.zip" + _write_archive(archive, _valid_members()) + output = tmp_path / "output" + arguments = type( + "A", + (), + { + "archive": str(archive), + "output_dir": str(output), + "result_filename": "result.json", + "runtime_evidence_filename": "runtime.json", + "fixture_filename": "fixture.json", + }, + )() + parser = type("P", (), {"parse_args": lambda self: arguments})() + monkeypatch.setattr(materializer, "_parser", lambda: parser) + + def corrupt_member(*args: object, **kwargs: object) -> object: + """Simulate corruption discovered only when a validated member is opened.""" + del args, kwargs + raise zipfile.BadZipFile("corrupt member payload") + + monkeypatch.setattr(zipfile.ZipFile, "open", corrupt_member) + + assert materializer.main() == 2 + captured = capsys.readouterr() + assert "member data is corrupted" in captured.err + assert not output.exists() + + def test_main_reports_materialization_error_without_traceback( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: From dac085903b9959f22faa068c34b6c18c896a56b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 00:20:29 +0900 Subject: [PATCH 27/47] fix(perf): preserve multiline verification commands --- .../product-performance-attestation.yml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/product-performance-attestation.yml b/.github/workflows/product-performance-attestation.yml index 421dcf8586..a365e84137 100644 --- a/.github/workflows/product-performance-attestation.yml +++ b/.github/workflows/product-performance-attestation.yml @@ -514,22 +514,22 @@ jobs: ## Online verification - gh attestation verify "sealed-evidence/${RESULT_FILENAME}" \ - --repo "$SOURCE_REPOSITORY" \ - --signer-repo "$SIGNER_REPOSITORY" \ - --signer-workflow "$signer_workflow" \ - --source-digest "$SOURCE_SHA" \ + gh attestation verify "sealed-evidence/${RESULT_FILENAME}" \\ + --repo "$SOURCE_REPOSITORY" \\ + --signer-repo "$SIGNER_REPOSITORY" \\ + --signer-workflow "$signer_workflow" \\ + --source-digest "$SOURCE_SHA" \\ --predicate-type "$PREDICATE_TYPE" ## Offline verification - gh attestation verify "sealed-evidence/${RESULT_FILENAME}" \ - --repo "$SOURCE_REPOSITORY" \ - --bundle offline-performance-attestation/result-attestation.json \ - --custom-trusted-root offline-performance-attestation/trusted_root.jsonl \ - --signer-repo "$SIGNER_REPOSITORY" \ - --signer-workflow "$signer_workflow" \ - --source-digest "$SOURCE_SHA" \ + gh attestation verify "sealed-evidence/${RESULT_FILENAME}" \\ + --repo "$SOURCE_REPOSITORY" \\ + --bundle offline-performance-attestation/result-attestation.json \\ + --custom-trusted-root offline-performance-attestation/trusted_root.jsonl \\ + --signer-repo "$SIGNER_REPOSITORY" \\ + --signer-workflow "$signer_workflow" \\ + --source-digest "$SOURCE_SHA" \\ --predicate-type "$PREDICATE_TYPE" EOF } >> offline-performance-attestation/README.md From b48c88edc863259e751a1a981c10692357475eac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 01:03:12 +0900 Subject: [PATCH 28/47] test(perf): cover sealed artifact CLI parser --- ...oduct_performance_artifact_materializer.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_product_performance_artifact_materializer.py b/tests/test_product_performance_artifact_materializer.py index d661412c69..5d5a2e5a48 100644 --- a/tests/test_product_performance_artifact_materializer.py +++ b/tests/test_product_performance_artifact_materializer.py @@ -413,3 +413,29 @@ def test_main_reports_materialization_error_without_traceback( assert materializer.main() == 2 assert "performance artifact rejected" in capsys.readouterr().err + + +def test_parser_requires_and_preserves_exact_evidence_arguments() -> None: + """Exercise the real CLI parser so every evidence role stays explicit and required.""" + arguments = materializer._parser().parse_args( + [ + "--archive", + "evidence.zip", + "--output-dir", + "sealed-evidence", + "--result-filename", + "result.json", + "--runtime-evidence-filename", + "runtime.json", + "--fixture-filename", + "fixture.json", + ] + ) + + assert vars(arguments) == { + "archive": "evidence.zip", + "output_dir": "sealed-evidence", + "result_filename": "result.json", + "runtime_evidence_filename": "runtime.json", + "fixture_filename": "fixture.json", + } From e24e9b9c9c4dd5c0d9354ed99e623cf59b479b9a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 03:05:25 +0900 Subject: [PATCH 29/47] test(perf): bind attested profile to sealed evidence --- ...est_product_performance_profile_binding.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/test_product_performance_profile_binding.py diff --git a/tests/test_product_performance_profile_binding.py b/tests/test_product_performance_profile_binding.py new file mode 100644 index 0000000000..1c0dd0028f --- /dev/null +++ b/tests/test_product_performance_profile_binding.py @@ -0,0 +1,86 @@ +"""Contracts binding the attested performance profile to sealed evidence.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path + +import pytest + +from scripts.ci import verify_product_performance_evidence as verifier + +PREDICATE_TYPE = "https://contextualwisdomlab.org/attestations/product-performance/v1" +SOURCE_SHA = "a" * 40 +ARTIFACT_DIGEST = "sha256:" + "b" * 64 + + +def _write_json(path: Path, value: object) -> str: + """Write deterministic JSON and return its SHA-256 digest.""" + payload = json.dumps(value, sort_keys=True).encode("utf-8") + path.write_bytes(payload) + return hashlib.sha256(payload).hexdigest() + + +def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: + """Create one otherwise-valid verifier request for first-commit evidence.""" + return argparse.Namespace( + source_repository="ContextualWisdomLab/Orgmetra", + source_sha=SOURCE_SHA, + workflow_run_id="123456", + evidence_artifact_id="789", + evidence_artifact_name="orgmetra-performance-evidence", + evidence_artifact_digest=ARTIFACT_DIGEST, + evidence_root=str(root), + result_filename="result.json", + result_sha256=hashlib.sha256((root / "result.json").read_bytes()).hexdigest(), + runtime_evidence_filename="runtime.json", + runtime_evidence_sha256=hashlib.sha256((root / "runtime.json").read_bytes()).hexdigest(), + fixture_filename="fixture.json", + fixture_sha256=hashlib.sha256((root / "fixture.json").read_bytes()).hexdigest(), + performance_profile="first_commit", + predicate_type=PREDICATE_TYPE, + output_predicate=str(tmp_path / "predicate.json"), + output_manifest=str(tmp_path / "manifest.json"), + ) + + +@pytest.mark.parametrize("member", ["result", "runtime"]) +def test_verify_rejects_profile_not_bound_to_sealed_evidence( + tmp_path: Path, member: str +) -> None: + """Reject a caller profile that disagrees with either sealed profile declaration.""" + root = tmp_path / "evidence" + root.mkdir() + profiles = {"result": "first_commit", "runtime": "first_commit"} + profiles[member] = "replay" + _write_json(root / "result.json", {"selected_profile": profiles["result"]}) + _write_json(root / "runtime.json", {"selected_profile": profiles["runtime"]}) + _write_json(root / "fixture.json", {"clearance": "right-cleared"}) + arguments = _arguments(root, tmp_path) + + with pytest.raises(verifier.EvidenceError, match="selected_profile"): + verifier.verify(arguments) + + +@pytest.mark.parametrize("member", ["result", "runtime"]) +def test_verify_rejects_missing_sealed_profile_declaration( + tmp_path: Path, member: str +) -> None: + """Reject evidence that would otherwise leave the attested profile caller-controlled.""" + root = tmp_path / "evidence" + root.mkdir() + result = {"selected_profile": "first_commit"} + runtime = {"selected_profile": "first_commit"} + if member == "result": + result = {} + else: + runtime = {} + _write_json(root / "result.json", result) + _write_json(root / "runtime.json", runtime) + _write_json(root / "fixture.json", {"clearance": "right-cleared"}) + arguments = _arguments(root, tmp_path) + + with pytest.raises(verifier.EvidenceError, match="selected_profile"): + verifier.verify(arguments) From 09720f3ffa0f66a4e802ad3cc4f118ba5d88e693 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 03:05:37 +0900 Subject: [PATCH 30/47] test(perf): execute profile binding regression --- .../workflows/product-performance-attestation-quality.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/product-performance-attestation-quality.yml b/.github/workflows/product-performance-attestation-quality.yml index 0118b5cc03..0bd7a460a1 100644 --- a/.github/workflows/product-performance-attestation-quality.yml +++ b/.github/workflows/product-performance-attestation-quality.yml @@ -11,6 +11,7 @@ on: - "tests/test_product_performance_artifact_materializer.py" - "tests/test_product_performance_attestation_contract.py" - "tests/test_product_performance_evidence_verifier.py" + - "tests/test_product_performance_profile_binding.py" - "docs/doctoring/product-performance-attestation.md" - "ARCHITECTURE.md" - "CHANGELOG.d/20260913-product-performance-attestation.md" @@ -58,7 +59,8 @@ jobs: scripts/ci/verify_product_performance_evidence.py \ tests/test_product_performance_artifact_materializer.py \ tests/test_product_performance_attestation_contract.py \ - tests/test_product_performance_evidence_verifier.py + tests/test_product_performance_evidence_verifier.py \ + tests/test_product_performance_profile_binding.py - name: Set up current quality Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -86,7 +88,8 @@ jobs: python -m coverage run --branch -m pytest -q \ tests/test_product_performance_artifact_materializer.py \ tests/test_product_performance_attestation_contract.py \ - tests/test_product_performance_evidence_verifier.py + tests/test_product_performance_evidence_verifier.py \ + tests/test_product_performance_profile_binding.py python -m coverage report \ --include=scripts/ci/materialize_product_performance_artifact.py,scripts/ci/verify_product_performance_evidence.py \ --show-missing \ From 7b372dff23251618c5c2fdf54cf25ae25976721a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 03:07:40 +0900 Subject: [PATCH 31/47] test(perf): require profile binding in both trust jobs --- tests/test_product_performance_profile_binding.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_product_performance_profile_binding.py b/tests/test_product_performance_profile_binding.py index 1c0dd0028f..5a224499ad 100644 --- a/tests/test_product_performance_profile_binding.py +++ b/tests/test_product_performance_profile_binding.py @@ -14,6 +14,7 @@ PREDICATE_TYPE = "https://contextualwisdomlab.org/attestations/product-performance/v1" SOURCE_SHA = "a" * 40 ARTIFACT_DIGEST = "sha256:" + "b" * 64 +WORKFLOW_PATH = Path(".github/workflows/product-performance-attestation.yml") def _write_json(path: Path, value: object) -> str: @@ -43,6 +44,7 @@ def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: predicate_type=PREDICATE_TYPE, output_predicate=str(tmp_path / "predicate.json"), output_manifest=str(tmp_path / "manifest.json"), + require_selected_profile_binding=True, ) @@ -84,3 +86,9 @@ def test_verify_rejects_missing_sealed_profile_declaration( with pytest.raises(verifier.EvidenceError, match="selected_profile"): verifier.verify(arguments) + + +def test_central_workflow_requires_profile_binding_in_both_trust_jobs() -> None: + """Require verifier and signer jobs to enable the sealed profile binding gate.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + assert workflow.count("--require-selected-profile-binding") == 2 From abfdf118b970d998e31d105feeb198afd94a8125 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 03:08:08 +0900 Subject: [PATCH 32/47] fix(perf): bind attested profile to sealed evidence --- .../ci/verify_product_performance_evidence.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts/ci/verify_product_performance_evidence.py b/scripts/ci/verify_product_performance_evidence.py index bb89d8074f..5970f5124b 100644 --- a/scripts/ci/verify_product_performance_evidence.py +++ b/scripts/ci/verify_product_performance_evidence.py @@ -173,6 +173,16 @@ def _validate_controls(arguments: argparse.Namespace) -> None: ) +def _require_selected_profile_binding( + document: dict[str, Any], label: str, expected_profile: str +) -> None: + """Bind a caller profile assertion to the sealed result and runtime documents.""" + if document.get("selected_profile") != expected_profile: + raise EvidenceError( + f"{label}.selected_profile must equal performance profile {expected_profile}" + ) + + def verify(arguments: argparse.Namespace) -> dict[str, Any]: """Validate one exact evidence set and publish deterministic trusted receipts.""" _validate_controls(arguments) @@ -208,9 +218,12 @@ def verify(arguments: argparse.Namespace) -> dict[str, Any]: for filename, expected in digests.items(): _require_digest(root / filename, expected, filename) - _load_json(root / names["result"], _MAX_RESULT_BYTES) - _load_json(root / names["runtime"], _MAX_RUNTIME_BYTES) + result = _load_json(root / names["result"], _MAX_RESULT_BYTES) + runtime = _load_json(root / names["runtime"], _MAX_RUNTIME_BYTES) _load_json(root / names["fixture"], _MAX_FIXTURE_BYTES) + if getattr(arguments, "require_selected_profile_binding", False): + _require_selected_profile_binding(result, "result", arguments.performance_profile) + _require_selected_profile_binding(runtime, "runtime", arguments.performance_profile) predicate = { "attestation_claim": "origin_and_integrity_only", @@ -286,6 +299,11 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("--fixture-filename", required=True) parser.add_argument("--fixture-sha256", required=True) parser.add_argument("--performance-profile", required=True) + parser.add_argument( + "--require-selected-profile-binding", + action="store_true", + default=argparse.SUPPRESS, + ) parser.add_argument("--predicate-type", required=True) parser.add_argument("--output-predicate", required=True) parser.add_argument("--output-manifest", required=True) From 4c2c46874f7f2a59c0f6a9a5979ef813f5b51189 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 03:09:32 +0900 Subject: [PATCH 33/47] fix(perf): enforce profile binding in verifier and signer --- .github/workflows/product-performance-attestation.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/product-performance-attestation.yml b/.github/workflows/product-performance-attestation.yml index a365e84137..426f9db27d 100644 --- a/.github/workflows/product-performance-attestation.yml +++ b/.github/workflows/product-performance-attestation.yml @@ -247,6 +247,7 @@ jobs: --fixture-filename "$FIXTURE_FILENAME" \ --fixture-sha256 "$FIXTURE_SHA256" \ --performance-profile "$PERFORMANCE_PROFILE" \ + --require-selected-profile-binding \ --predicate-type "$PREDICATE_TYPE" \ --output-predicate "${RUNNER_TEMP}/verified-performance-predicate.json" \ --output-manifest "${RUNNER_TEMP}/verified-performance-intake.json" @@ -444,6 +445,7 @@ jobs: --fixture-filename "$FIXTURE_FILENAME" \ --fixture-sha256 "$FIXTURE_SHA256" \ --performance-profile "$PERFORMANCE_PROFILE" \ + --require-selected-profile-binding \ --predicate-type "$PREDICATE_TYPE" \ --output-predicate "${RUNNER_TEMP}/verified-performance-predicate.json" \ --output-manifest "${RUNNER_TEMP}/verified-performance-signer.json" From df065815212262d8201702c4e9b5cb9fd0a59274 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 03:10:06 +0900 Subject: [PATCH 34/47] test(perf): cover matching profile binding path --- ...est_product_performance_profile_binding.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_product_performance_profile_binding.py b/tests/test_product_performance_profile_binding.py index 5a224499ad..4a30eee786 100644 --- a/tests/test_product_performance_profile_binding.py +++ b/tests/test_product_performance_profile_binding.py @@ -48,6 +48,26 @@ def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: ) +def _matching_evidence(root: Path) -> None: + """Write one sealed evidence set with matching first-commit profile declarations.""" + _write_json(root / "result.json", {"selected_profile": "first_commit"}) + _write_json(root / "runtime.json", {"selected_profile": "first_commit"}) + _write_json(root / "fixture.json", {"clearance": "right-cleared"}) + + +def test_verify_accepts_matching_sealed_profile_binding(tmp_path: Path) -> None: + """Preserve the positive path when both sealed declarations match the caller profile.""" + root = tmp_path / "evidence" + root.mkdir() + _matching_evidence(root) + arguments = _arguments(root, tmp_path) + + manifest = verifier.verify(arguments) + + assert manifest["verification_result"] == "VALID" + assert manifest["performance_profile"] == "first_commit" + + @pytest.mark.parametrize("member", ["result", "runtime"]) def test_verify_rejects_profile_not_bound_to_sealed_evidence( tmp_path: Path, member: str From 5ad9cc63fb5289f30e8555a69609d5b0eea697b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 04:03:57 +0900 Subject: [PATCH 35/47] test(perf): bind attested source SHA to sealed evidence --- ...est_product_performance_profile_binding.py | 89 ++++++++++++++++--- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/tests/test_product_performance_profile_binding.py b/tests/test_product_performance_profile_binding.py index 4a30eee786..30e560c426 100644 --- a/tests/test_product_performance_profile_binding.py +++ b/tests/test_product_performance_profile_binding.py @@ -1,4 +1,4 @@ -"""Contracts binding the attested performance profile to sealed evidence.""" +"""Contracts binding attested performance identity to sealed evidence.""" from __future__ import annotations @@ -45,18 +45,25 @@ def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: output_predicate=str(tmp_path / "predicate.json"), output_manifest=str(tmp_path / "manifest.json"), require_selected_profile_binding=True, + require_source_sha_binding=True, ) def _matching_evidence(root: Path) -> None: - """Write one sealed evidence set with matching first-commit profile declarations.""" - _write_json(root / "result.json", {"selected_profile": "first_commit"}) - _write_json(root / "runtime.json", {"selected_profile": "first_commit"}) + """Write one sealed evidence set matching the attested source and profile.""" + _write_json( + root / "result.json", + {"candidate_sha": SOURCE_SHA, "selected_profile": "first_commit"}, + ) + _write_json( + root / "runtime.json", + {"candidate_sha": SOURCE_SHA, "selected_profile": "first_commit"}, + ) _write_json(root / "fixture.json", {"clearance": "right-cleared"}) -def test_verify_accepts_matching_sealed_profile_binding(tmp_path: Path) -> None: - """Preserve the positive path when both sealed declarations match the caller profile.""" +def test_verify_accepts_matching_sealed_identity_binding(tmp_path: Path) -> None: + """Preserve the positive path when sealed source and profile identity match.""" root = tmp_path / "evidence" root.mkdir() _matching_evidence(root) @@ -65,6 +72,7 @@ def test_verify_accepts_matching_sealed_profile_binding(tmp_path: Path) -> None: manifest = verifier.verify(arguments) assert manifest["verification_result"] == "VALID" + assert manifest["source_sha"] == SOURCE_SHA assert manifest["performance_profile"] == "first_commit" @@ -77,8 +85,14 @@ def test_verify_rejects_profile_not_bound_to_sealed_evidence( root.mkdir() profiles = {"result": "first_commit", "runtime": "first_commit"} profiles[member] = "replay" - _write_json(root / "result.json", {"selected_profile": profiles["result"]}) - _write_json(root / "runtime.json", {"selected_profile": profiles["runtime"]}) + _write_json( + root / "result.json", + {"candidate_sha": SOURCE_SHA, "selected_profile": profiles["result"]}, + ) + _write_json( + root / "runtime.json", + {"candidate_sha": SOURCE_SHA, "selected_profile": profiles["runtime"]}, + ) _write_json(root / "fixture.json", {"clearance": "right-cleared"}) arguments = _arguments(root, tmp_path) @@ -93,12 +107,12 @@ def test_verify_rejects_missing_sealed_profile_declaration( """Reject evidence that would otherwise leave the attested profile caller-controlled.""" root = tmp_path / "evidence" root.mkdir() - result = {"selected_profile": "first_commit"} - runtime = {"selected_profile": "first_commit"} + result = {"candidate_sha": SOURCE_SHA, "selected_profile": "first_commit"} + runtime = {"candidate_sha": SOURCE_SHA, "selected_profile": "first_commit"} if member == "result": - result = {} + result.pop("selected_profile") else: - runtime = {} + runtime.pop("selected_profile") _write_json(root / "result.json", result) _write_json(root / "runtime.json", runtime) _write_json(root / "fixture.json", {"clearance": "right-cleared"}) @@ -108,7 +122,54 @@ def test_verify_rejects_missing_sealed_profile_declaration( verifier.verify(arguments) -def test_central_workflow_requires_profile_binding_in_both_trust_jobs() -> None: - """Require verifier and signer jobs to enable the sealed profile binding gate.""" +@pytest.mark.parametrize("member", ["result", "runtime"]) +def test_verify_rejects_source_sha_not_bound_to_sealed_evidence( + tmp_path: Path, member: str +) -> None: + """Reject a caller source SHA that disagrees with either sealed candidate SHA.""" + root = tmp_path / "evidence" + root.mkdir() + candidate_shas = {"result": SOURCE_SHA, "runtime": SOURCE_SHA} + candidate_shas[member] = "c" * 40 + _write_json( + root / "result.json", + {"candidate_sha": candidate_shas["result"], "selected_profile": "first_commit"}, + ) + _write_json( + root / "runtime.json", + {"candidate_sha": candidate_shas["runtime"], "selected_profile": "first_commit"}, + ) + _write_json(root / "fixture.json", {"clearance": "right-cleared"}) + arguments = _arguments(root, tmp_path) + + with pytest.raises(verifier.EvidenceError, match="candidate_sha"): + verifier.verify(arguments) + + +@pytest.mark.parametrize("member", ["result", "runtime"]) +def test_verify_rejects_missing_sealed_source_sha_declaration( + tmp_path: Path, member: str +) -> None: + """Reject evidence that would leave attested source identity outside sealed data.""" + root = tmp_path / "evidence" + root.mkdir() + result = {"candidate_sha": SOURCE_SHA, "selected_profile": "first_commit"} + runtime = {"candidate_sha": SOURCE_SHA, "selected_profile": "first_commit"} + if member == "result": + result.pop("candidate_sha") + else: + runtime.pop("candidate_sha") + _write_json(root / "result.json", result) + _write_json(root / "runtime.json", runtime) + _write_json(root / "fixture.json", {"clearance": "right-cleared"}) + arguments = _arguments(root, tmp_path) + + with pytest.raises(verifier.EvidenceError, match="candidate_sha"): + verifier.verify(arguments) + + +def test_central_workflow_requires_identity_binding_in_both_trust_jobs() -> None: + """Require verifier and signer jobs to bind sealed profile and source identity.""" workflow = WORKFLOW_PATH.read_text(encoding="utf-8") assert workflow.count("--require-selected-profile-binding") == 2 + assert workflow.count("--require-source-sha-binding") == 2 From 6fdc27384070c95fe4e61f2663087868a04a16f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 04:04:32 +0900 Subject: [PATCH 36/47] fix(perf): bind attested source SHA to sealed evidence --- .../ci/verify_product_performance_evidence.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/ci/verify_product_performance_evidence.py b/scripts/ci/verify_product_performance_evidence.py index 5970f5124b..d334e3ae37 100644 --- a/scripts/ci/verify_product_performance_evidence.py +++ b/scripts/ci/verify_product_performance_evidence.py @@ -183,6 +183,16 @@ def _require_selected_profile_binding( ) +def _require_source_sha_binding( + document: dict[str, Any], label: str, expected_sha: str +) -> None: + """Bind attested source identity to the sealed candidate SHA declaration.""" + if document.get("candidate_sha") != expected_sha: + raise EvidenceError( + f"{label}.candidate_sha must equal attested source SHA {expected_sha}" + ) + + def verify(arguments: argparse.Namespace) -> dict[str, Any]: """Validate one exact evidence set and publish deterministic trusted receipts.""" _validate_controls(arguments) @@ -224,6 +234,9 @@ def verify(arguments: argparse.Namespace) -> dict[str, Any]: if getattr(arguments, "require_selected_profile_binding", False): _require_selected_profile_binding(result, "result", arguments.performance_profile) _require_selected_profile_binding(runtime, "runtime", arguments.performance_profile) + if getattr(arguments, "require_source_sha_binding", False): + _require_source_sha_binding(result, "result", arguments.source_sha) + _require_source_sha_binding(runtime, "runtime", arguments.source_sha) predicate = { "attestation_claim": "origin_and_integrity_only", @@ -304,6 +317,11 @@ def _parser() -> argparse.ArgumentParser: action="store_true", default=argparse.SUPPRESS, ) + parser.add_argument( + "--require-source-sha-binding", + action="store_true", + default=argparse.SUPPRESS, + ) parser.add_argument("--predicate-type", required=True) parser.add_argument("--output-predicate", required=True) parser.add_argument("--output-manifest", required=True) From 4c00f63d77569c44662bd7d77490b951174a18f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 04:05:31 +0900 Subject: [PATCH 37/47] fix(perf): couple source identity to commercial profile binding --- scripts/ci/verify_product_performance_evidence.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/ci/verify_product_performance_evidence.py b/scripts/ci/verify_product_performance_evidence.py index d334e3ae37..6f4faf1bbd 100644 --- a/scripts/ci/verify_product_performance_evidence.py +++ b/scripts/ci/verify_product_performance_evidence.py @@ -234,7 +234,6 @@ def verify(arguments: argparse.Namespace) -> dict[str, Any]: if getattr(arguments, "require_selected_profile_binding", False): _require_selected_profile_binding(result, "result", arguments.performance_profile) _require_selected_profile_binding(runtime, "runtime", arguments.performance_profile) - if getattr(arguments, "require_source_sha_binding", False): _require_source_sha_binding(result, "result", arguments.source_sha) _require_source_sha_binding(runtime, "runtime", arguments.source_sha) @@ -317,11 +316,6 @@ def _parser() -> argparse.ArgumentParser: action="store_true", default=argparse.SUPPRESS, ) - parser.add_argument( - "--require-source-sha-binding", - action="store_true", - default=argparse.SUPPRESS, - ) parser.add_argument("--predicate-type", required=True) parser.add_argument("--output-predicate", required=True) parser.add_argument("--output-manifest", required=True) From cd737f4586d90828b22da2f439210d9265843b4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 04:05:50 +0900 Subject: [PATCH 38/47] test(perf): align commercial identity binding contract --- tests/test_product_performance_profile_binding.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_product_performance_profile_binding.py b/tests/test_product_performance_profile_binding.py index 30e560c426..36c149550d 100644 --- a/tests/test_product_performance_profile_binding.py +++ b/tests/test_product_performance_profile_binding.py @@ -45,7 +45,6 @@ def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: output_predicate=str(tmp_path / "predicate.json"), output_manifest=str(tmp_path / "manifest.json"), require_selected_profile_binding=True, - require_source_sha_binding=True, ) @@ -169,7 +168,6 @@ def test_verify_rejects_missing_sealed_source_sha_declaration( def test_central_workflow_requires_identity_binding_in_both_trust_jobs() -> None: - """Require verifier and signer jobs to bind sealed profile and source identity.""" + """Require verifier and signer jobs to enable sealed commercial identity binding.""" workflow = WORKFLOW_PATH.read_text(encoding="utf-8") assert workflow.count("--require-selected-profile-binding") == 2 - assert workflow.count("--require-source-sha-binding") == 2 From b04ae6b5aae114561c3b4ce407dd6db5f8746964 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 06:06:14 +0900 Subject: [PATCH 39/47] test(perf): require authenticated caller workflow identity --- ...st_product_performance_attestation_contract.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_product_performance_attestation_contract.py b/tests/test_product_performance_attestation_contract.py index 60fdbe2c12..22b8eeeabc 100644 --- a/tests/test_product_performance_attestation_contract.py +++ b/tests/test_product_performance_attestation_contract.py @@ -55,6 +55,21 @@ def test_reusable_workflow_uses_oidc_callee_identity_before_trusted_checkout() - assert workflow.count("product-performance-attestation.yml@") >= 2 +def test_reusable_workflow_authenticates_and_records_caller_workflow_identity() -> None: + """Bind the caller workflow, not only the central callee, into the signed predicate.""" + workflow = _text(WORKFLOW) + verifier = _text(VERIFIER) + + assert workflow.count('claims.get("workflow_ref")') >= 2 + assert workflow.count('claims.get("workflow_sha")') >= 2 + assert workflow.count("caller_workflow_ref=") >= 2 + assert workflow.count("caller_workflow_sha=") >= 2 + assert workflow.count("--caller-workflow-ref") >= 2 + assert workflow.count("--caller-workflow-sha") >= 2 + assert '"caller_workflow_ref": arguments.caller_workflow_ref' in verifier + assert '"caller_workflow_sha": arguments.caller_workflow_sha' in verifier + + def test_reusable_workflow_fails_closed_for_non_cwl_callers() -> None: """Require organization ownership checks before metadata and archive access in both jobs.""" workflow = _text(WORKFLOW) From 14405c187da117094d6afe4f0f0a200d8b6fe0bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 06:08:23 +0900 Subject: [PATCH 40/47] fix(perf): bind signed predicate to authenticated caller workflow --- .../ci/verify_product_performance_evidence.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/ci/verify_product_performance_evidence.py b/scripts/ci/verify_product_performance_evidence.py index 6f4faf1bbd..3808fda610 100644 --- a/scripts/ci/verify_product_performance_evidence.py +++ b/scripts/ci/verify_product_performance_evidence.py @@ -173,6 +173,28 @@ def _validate_controls(arguments: argparse.Namespace) -> None: ) +def _require_authenticated_caller_workflow( + arguments: argparse.Namespace, +) -> tuple[str, str]: + """Bind one OIDC-authenticated caller workflow to the exact source commit.""" + caller_ref = getattr(arguments, "caller_workflow_ref", None) + caller_sha = getattr(arguments, "caller_workflow_sha", None) + expected_prefix = f"{arguments.source_repository}/.github/workflows/" + if not isinstance(caller_ref, str) or not caller_ref.startswith(expected_prefix): + raise EvidenceError("caller workflow ref must identify a workflow in source repository") + relative_ref = caller_ref[len(expected_prefix):] + if "@" not in relative_ref: + raise EvidenceError("caller workflow ref must include its triggering ref") + workflow_path, triggering_ref = relative_ref.split("@", 1) + if not workflow_path or "/" in workflow_path or not triggering_ref: + raise EvidenceError("caller workflow ref must identify one root workflow file and ref") + if not isinstance(caller_sha, str) or _SHA1_RE.fullmatch(caller_sha) is None: + raise EvidenceError("caller workflow SHA must be a lowercase 40-character Git SHA") + if caller_sha != arguments.source_sha: + raise EvidenceError("caller workflow SHA must equal attested source SHA") + return caller_ref, caller_sha + + def _require_selected_profile_binding( document: dict[str, Any], label: str, expected_profile: str ) -> None: @@ -231,7 +253,9 @@ def verify(arguments: argparse.Namespace) -> dict[str, Any]: result = _load_json(root / names["result"], _MAX_RESULT_BYTES) runtime = _load_json(root / names["runtime"], _MAX_RUNTIME_BYTES) _load_json(root / names["fixture"], _MAX_FIXTURE_BYTES) + caller_workflow: tuple[str, str] | None = None if getattr(arguments, "require_selected_profile_binding", False): + caller_workflow = _require_authenticated_caller_workflow(arguments) _require_selected_profile_binding(result, "result", arguments.performance_profile) _require_selected_profile_binding(runtime, "runtime", arguments.performance_profile) _require_source_sha_binding(result, "result", arguments.source_sha) @@ -289,6 +313,12 @@ def verify(arguments: argparse.Namespace) -> dict[str, Any]: "source_sha": arguments.source_sha, "workflow_run_id": arguments.workflow_run_id, } + if caller_workflow is not None: + caller_ref, caller_sha = caller_workflow + predicate["caller_workflow_ref"] = caller_ref + predicate["caller_workflow_sha"] = caller_sha + manifest["caller_workflow_ref"] = caller_ref + manifest["caller_workflow_sha"] = caller_sha _atomic_json(Path(arguments.output_predicate), predicate) _atomic_json(Path(arguments.output_manifest), manifest) return manifest @@ -311,6 +341,8 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("--fixture-filename", required=True) parser.add_argument("--fixture-sha256", required=True) parser.add_argument("--performance-profile", required=True) + parser.add_argument("--caller-workflow-ref", default=argparse.SUPPRESS) + parser.add_argument("--caller-workflow-sha", default=argparse.SUPPRESS) parser.add_argument( "--require-selected-profile-binding", action="store_true", From 2af8d7c45070832638cc671aedd906deb500653a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 06:08:48 +0900 Subject: [PATCH 41/47] test(perf): cover caller workflow identity binding --- ...est_product_performance_profile_binding.py | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/test_product_performance_profile_binding.py b/tests/test_product_performance_profile_binding.py index 36c149550d..e13af0923e 100644 --- a/tests/test_product_performance_profile_binding.py +++ b/tests/test_product_performance_profile_binding.py @@ -14,6 +14,10 @@ PREDICATE_TYPE = "https://contextualwisdomlab.org/attestations/product-performance/v1" SOURCE_SHA = "a" * 40 ARTIFACT_DIGEST = "sha256:" + "b" * 64 +CALLER_WORKFLOW_REF = ( + "ContextualWisdomLab/Orgmetra/.github/workflows/people-performance.yml@refs/heads/develop" +) +CALLER_WORKFLOW_SHA = SOURCE_SHA WORKFLOW_PATH = Path(".github/workflows/product-performance-attestation.yml") @@ -41,6 +45,8 @@ def _arguments(root: Path, tmp_path: Path) -> argparse.Namespace: fixture_filename="fixture.json", fixture_sha256=hashlib.sha256((root / "fixture.json").read_bytes()).hexdigest(), performance_profile="first_commit", + caller_workflow_ref=CALLER_WORKFLOW_REF, + caller_workflow_sha=CALLER_WORKFLOW_SHA, predicate_type=PREDICATE_TYPE, output_predicate=str(tmp_path / "predicate.json"), output_manifest=str(tmp_path / "manifest.json"), @@ -62,17 +68,58 @@ def _matching_evidence(root: Path) -> None: def test_verify_accepts_matching_sealed_identity_binding(tmp_path: Path) -> None: - """Preserve the positive path when sealed source and profile identity match.""" + """Preserve the positive path when sealed source, profile, and caller identity match.""" root = tmp_path / "evidence" root.mkdir() _matching_evidence(root) arguments = _arguments(root, tmp_path) manifest = verifier.verify(arguments) + predicate = json.loads(Path(arguments.output_predicate).read_text(encoding="utf-8")) assert manifest["verification_result"] == "VALID" assert manifest["source_sha"] == SOURCE_SHA assert manifest["performance_profile"] == "first_commit" + assert manifest["caller_workflow_ref"] == CALLER_WORKFLOW_REF + assert manifest["caller_workflow_sha"] == CALLER_WORKFLOW_SHA + assert predicate["caller_workflow_ref"] == CALLER_WORKFLOW_REF + assert predicate["caller_workflow_sha"] == CALLER_WORKFLOW_SHA + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ( + "caller_workflow_ref", + "ContextualWisdomLab/Other/.github/workflows/perf.yml@refs/heads/main", + "source repository", + ), + ( + "caller_workflow_ref", + "ContextualWisdomLab/Orgmetra/.github/workflows/perf.yml", + "triggering ref", + ), + ( + "caller_workflow_ref", + "ContextualWisdomLab/Orgmetra/.github/workflows/nested/perf.yml@refs/heads/main", + "root workflow file", + ), + ("caller_workflow_sha", "A" * 40, "caller workflow SHA"), + ("caller_workflow_sha", "c" * 40, "attested source SHA"), + ], +) +def test_verify_rejects_unauthenticated_caller_workflow_identity( + tmp_path: Path, field: str, value: str, message: str +) -> None: + """Reject caller-workflow identity that is not the exact source workflow authority.""" + root = tmp_path / "evidence" + root.mkdir() + _matching_evidence(root) + arguments = _arguments(root, tmp_path) + setattr(arguments, field, value) + + with pytest.raises(verifier.EvidenceError, match=message): + verifier.verify(arguments) @pytest.mark.parametrize("member", ["result", "runtime"]) @@ -171,3 +218,5 @@ def test_central_workflow_requires_identity_binding_in_both_trust_jobs() -> None """Require verifier and signer jobs to enable sealed commercial identity binding.""" workflow = WORKFLOW_PATH.read_text(encoding="utf-8") assert workflow.count("--require-selected-profile-binding") == 2 + assert workflow.count("--caller-workflow-ref") == 2 + assert workflow.count("--caller-workflow-sha") == 2 From 78c7b52f4dfaa90e91c5f891dd667eb439f798e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 06:10:29 +0900 Subject: [PATCH 42/47] fix(perf): authenticate caller workflow in central signer --- .../product-performance-attestation.yml | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/.github/workflows/product-performance-attestation.yml b/.github/workflows/product-performance-attestation.yml index 426f9db27d..b13b73e0a2 100644 --- a/.github/workflows/product-performance-attestation.yml +++ b/.github/workflows/product-performance-attestation.yml @@ -80,6 +80,8 @@ jobs: EXPECTED_OIDC_ISSUER: https://token.actions.githubusercontent.com OIDC_AUDIENCE: https://github.com/ContextualWisdomLab/.github/product-performance-attestation EXPECTED_WORKFLOW_PREFIX: ContextualWisdomLab/.github/.github/workflows/product-performance-attestation.yml@ + EXPECTED_SOURCE_REPOSITORY: ${{ inputs.source_repository }} + EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }} shell: bash --noprofile --norc -e -o pipefail {0} run: | : "${ACTIONS_ID_TOKEN_REQUEST_URL:?GitHub OIDC request URL is unavailable}" @@ -125,6 +127,8 @@ jobs: expected_issuer = os.environ["EXPECTED_OIDC_ISSUER"] expected_audience = os.environ["OIDC_AUDIENCE"] expected_prefix = os.environ["EXPECTED_WORKFLOW_PREFIX"] + expected_source_repository = os.environ["EXPECTED_SOURCE_REPOSITORY"] + expected_source_sha = os.environ["EXPECTED_SOURCE_SHA"] if claims.get("iss") != expected_issuer: raise SystemExit("GitHub OIDC issuer mismatch") audience = claims.get("aud") @@ -146,11 +150,38 @@ jobs: if ref_sha != workflow_sha: raise SystemExit("job_workflow_ref and job_workflow_sha disagree") + caller_repository = claims.get("repository") + caller_sha = claims.get("sha") + caller_workflow_ref = claims.get("workflow_ref") + caller_workflow_sha = claims.get("workflow_sha") + caller_workflow_pattern = re.compile( + rf"{re.escape(expected_source_repository)}/\.github/workflows/" + r"[A-Za-z0-9_.-]+\.ya?ml@[A-Za-z0-9_./-]+" + ) + if caller_repository != expected_source_repository: + raise SystemExit("OIDC caller repository does not match supplied source repository") + if caller_sha != expected_source_sha: + raise SystemExit("OIDC caller SHA does not match supplied source SHA") + if ( + not isinstance(caller_workflow_ref, str) + or caller_workflow_pattern.fullmatch(caller_workflow_ref) is None + ): + raise SystemExit("OIDC caller workflow_ref is malformed or outside the source repository") + if ( + not isinstance(caller_workflow_sha, str) + or re.fullmatch(r"[0-9a-f]{40}", caller_workflow_sha) is None + ): + raise SystemExit("OIDC caller workflow_sha must be a full 40-hex commit SHA") + if caller_workflow_sha != expected_source_sha: + raise SystemExit("OIDC caller workflow_sha does not match supplied source SHA") + output_path = os.environ.get("GITHUB_OUTPUT") if not output_path: raise SystemExit("GITHUB_OUTPUT is unavailable") with open(output_path, "a", encoding="utf-8") as output: output.write(f"workflow_sha={workflow_sha}\n") + output.write(f"caller_workflow_ref={caller_workflow_ref}\n") + output.write(f"caller_workflow_sha={caller_workflow_sha}\n") PY - name: Materialize immutable trusted verifier @@ -228,6 +259,8 @@ jobs: FIXTURE_FILENAME: ${{ inputs.fixture_filename }} FIXTURE_SHA256: ${{ inputs.fixture_sha256 }} PERFORMANCE_PROFILE: ${{ inputs.performance_profile }} + CALLER_WORKFLOW_REF: ${{ steps.workflow-identity.outputs.caller_workflow_ref }} + CALLER_WORKFLOW_SHA: ${{ steps.workflow-identity.outputs.caller_workflow_sha }} PREDICATE_TYPE: ${{ inputs.predicate_type }} shell: bash --noprofile --norc -e -o pipefail {0} run: | @@ -247,6 +280,8 @@ jobs: --fixture-filename "$FIXTURE_FILENAME" \ --fixture-sha256 "$FIXTURE_SHA256" \ --performance-profile "$PERFORMANCE_PROFILE" \ + --caller-workflow-ref "$CALLER_WORKFLOW_REF" \ + --caller-workflow-sha "$CALLER_WORKFLOW_SHA" \ --require-selected-profile-binding \ --predicate-type "$PREDICATE_TYPE" \ --output-predicate "${RUNNER_TEMP}/verified-performance-predicate.json" \ @@ -278,6 +313,8 @@ jobs: EXPECTED_OIDC_ISSUER: https://token.actions.githubusercontent.com OIDC_AUDIENCE: https://github.com/ContextualWisdomLab/.github/product-performance-attestation EXPECTED_WORKFLOW_PREFIX: ContextualWisdomLab/.github/.github/workflows/product-performance-attestation.yml@ + EXPECTED_SOURCE_REPOSITORY: ${{ inputs.source_repository }} + EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }} shell: bash --noprofile --norc -e -o pipefail {0} run: | : "${ACTIONS_ID_TOKEN_REQUEST_URL:?GitHub OIDC request URL is unavailable}" @@ -323,6 +360,8 @@ jobs: expected_issuer = os.environ["EXPECTED_OIDC_ISSUER"] expected_audience = os.environ["OIDC_AUDIENCE"] expected_prefix = os.environ["EXPECTED_WORKFLOW_PREFIX"] + expected_source_repository = os.environ["EXPECTED_SOURCE_REPOSITORY"] + expected_source_sha = os.environ["EXPECTED_SOURCE_SHA"] if claims.get("iss") != expected_issuer: raise SystemExit("GitHub OIDC issuer mismatch") audience = claims.get("aud") @@ -344,11 +383,38 @@ jobs: if ref_sha != workflow_sha: raise SystemExit("job_workflow_ref and job_workflow_sha disagree") + caller_repository = claims.get("repository") + caller_sha = claims.get("sha") + caller_workflow_ref = claims.get("workflow_ref") + caller_workflow_sha = claims.get("workflow_sha") + caller_workflow_pattern = re.compile( + rf"{re.escape(expected_source_repository)}/\.github/workflows/" + r"[A-Za-z0-9_.-]+\.ya?ml@[A-Za-z0-9_./-]+" + ) + if caller_repository != expected_source_repository: + raise SystemExit("OIDC caller repository does not match supplied source repository") + if caller_sha != expected_source_sha: + raise SystemExit("OIDC caller SHA does not match supplied source SHA") + if ( + not isinstance(caller_workflow_ref, str) + or caller_workflow_pattern.fullmatch(caller_workflow_ref) is None + ): + raise SystemExit("OIDC caller workflow_ref is malformed or outside the source repository") + if ( + not isinstance(caller_workflow_sha, str) + or re.fullmatch(r"[0-9a-f]{40}", caller_workflow_sha) is None + ): + raise SystemExit("OIDC caller workflow_sha must be a full 40-hex commit SHA") + if caller_workflow_sha != expected_source_sha: + raise SystemExit("OIDC caller workflow_sha does not match supplied source SHA") + output_path = os.environ.get("GITHUB_OUTPUT") if not output_path: raise SystemExit("GITHUB_OUTPUT is unavailable") with open(output_path, "a", encoding="utf-8") as output: output.write(f"workflow_sha={workflow_sha}\n") + output.write(f"caller_workflow_ref={caller_workflow_ref}\n") + output.write(f"caller_workflow_sha={caller_workflow_sha}\n") PY - name: Materialize immutable trusted verifier @@ -426,6 +492,8 @@ jobs: FIXTURE_FILENAME: ${{ inputs.fixture_filename }} FIXTURE_SHA256: ${{ inputs.fixture_sha256 }} PERFORMANCE_PROFILE: ${{ inputs.performance_profile }} + CALLER_WORKFLOW_REF: ${{ steps.workflow-identity.outputs.caller_workflow_ref }} + CALLER_WORKFLOW_SHA: ${{ steps.workflow-identity.outputs.caller_workflow_sha }} PREDICATE_TYPE: ${{ inputs.predicate_type }} shell: bash --noprofile --norc -e -o pipefail {0} run: | @@ -445,6 +513,8 @@ jobs: --fixture-filename "$FIXTURE_FILENAME" \ --fixture-sha256 "$FIXTURE_SHA256" \ --performance-profile "$PERFORMANCE_PROFILE" \ + --caller-workflow-ref "$CALLER_WORKFLOW_REF" \ + --caller-workflow-sha "$CALLER_WORKFLOW_SHA" \ --require-selected-profile-binding \ --predicate-type "$PREDICATE_TYPE" \ --output-predicate "${RUNNER_TEMP}/verified-performance-predicate.json" \ @@ -465,6 +535,8 @@ jobs: SIGNER_REPOSITORY: ContextualWisdomLab/.github SOURCE_REPOSITORY: ${{ inputs.source_repository }} SOURCE_SHA: ${{ inputs.source_sha }} + CALLER_WORKFLOW_REF: ${{ steps.workflow-identity.outputs.caller_workflow_ref }} + CALLER_WORKFLOW_SHA: ${{ steps.workflow-identity.outputs.caller_workflow_sha }} RESULT_FILENAME: ${{ inputs.result_filename }} PREDICATE_TYPE: ${{ inputs.predicate_type }} ATTESTATION_BUNDLE: ${{ steps.attest-result.outputs.bundle-path }} @@ -508,6 +580,8 @@ jobs: printf '\n## Exact signed identity\n\n' printf -- '- Source repository: `%s`\n' "$SOURCE_REPOSITORY" printf -- '- Source SHA: `%s`\n' "$SOURCE_SHA" + printf -- '- Caller workflow ref: `%s`\n' "$CALLER_WORKFLOW_REF" + printf -- '- Caller workflow SHA: `%s`\n' "$CALLER_WORKFLOW_SHA" printf -- '- Signer repository: `%s`\n' "$SIGNER_REPOSITORY" printf -- '- Signer workflow: `%s`\n' "$signer_workflow" printf -- '- Predicate type: `%s`\n' "$PREDICATE_TYPE" From 6fb6676fe8c4b53d52d47e389628d1b2d3ef0af5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 06:10:54 +0900 Subject: [PATCH 43/47] docs(perf): record authenticated caller workflow boundary --- docs/doctoring/product-performance-attestation.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/product-performance-attestation.md b/docs/doctoring/product-performance-attestation.md index 2c2756a9bf..2c0160755d 100644 --- a/docs/doctoring/product-performance-attestation.md +++ b/docs/doctoring/product-performance-attestation.md @@ -18,11 +18,14 @@ This boundary was introduced from `ContextualWisdomLab/Orgmetra#316/#317` and is Before any caller artifact is used, both jobs: - resolve the called reusable workflow identity from GitHub Actions OIDC `job_workflow_ref` and `job_workflow_sha`; +- independently bind the OIDC caller `repository`, `sha`, `workflow_ref`, and `workflow_sha` claims to the supplied source repository/SHA, then carry the authenticated caller workflow ref/SHA into the signed predicate; - require a GitHub-hosted signer runner; - require the caller repository to be in the `ContextualWisdomLab` organization; - require the supplied source repository and SHA to equal the caller `GITHUB_REPOSITORY` and `GITHUB_SHA`; - re-read immutable artifact ID, name, digest, workflow-run identity, expiry state, and compressed size from the GitHub REST API. +GitHub documents that, for jobs executing a reusable workflow, standard OIDC claims describe the calling workflow while `job_workflow_ref` identifies the called reusable workflow. Both identities are therefore required here: callee identity selects trusted central verifier code; caller identity tells downstream product policy which exact workflow produced and submitted the evidence. Repository/SHA equality alone is insufficient because another workflow at the same source commit could otherwise obtain a valid central signature over caller-controlled evidence. + The central signer attests **origin and byte integrity only**. The predicate is `https://contextualwisdomlab.org/attestations/product-performance/v1` and explicitly records that it does not prove latency-threshold success, production equivalence, fixture scientific validity, or fixture right clearance. Structural verification is recorded as `verification_result: VALID`; the central layer must not emit a generic performance `PASS`. ## Bounded inert artifact handling @@ -38,19 +41,20 @@ The artifact is bounded before extraction. GitHub's artifact metadata `size_in_b - extraction occurs only into a newly created private directory; `ZipFile.extract()` and `extractall()` are not used; - the existing strict verifier then re-hashes each materialized file, requires exact three-file cardinality, strict UTF-8 JSON objects, no duplicate JSON keys or non-finite numbers, and exact caller-provided per-file digests. -The verifier job and the credentialed signer job independently repeat artifact metadata, bounded download, archive authentication, materialization, and file verification. Product code or product-provided scripts are never executed in the `attestations: write` job. +The verifier job and the credentialed signer job independently repeat OIDC caller/callee identity resolution, artifact metadata, bounded download, archive authentication, materialization, and file verification. Product code or product-provided scripts are never executed in the `attestations: write` job. ## Attestation and verification -The signer uses the immutable `actions/attest` v4.1.0 commit `59d89421af93a897026c735860bf21b6eb4f7b26` to attest the exact result subject digest with the trusted predicate. The workflow then verifies the result online against the caller repository, central signer repository/workflow, exact source digest, and predicate type. +The signer uses the immutable `actions/attest` v4.1.0 commit `59d89421af93a897026c735860bf21b6eb4f7b26` to attest the exact result subject digest with the trusted predicate. The workflow then verifies the result online against the caller repository, central signer repository/workflow, exact source digest, and predicate type. The custom predicate additionally records the authenticated caller `workflow_ref` and `workflow_sha`; a product canary must compare those fields with its canonical thin caller before treating the evidence as commercially admissible. -For offline verification it retains the Sigstore bundle, trusted root, predicate, verifier manifest, and SHA-256 inventory. The retained README contains the exact online and offline `gh attestation verify` commands. +For offline verification it retains the Sigstore bundle, trusted root, predicate, verifier manifest, and SHA-256 inventory. The retained README contains the exact online and offline `gh attestation verify` commands and the authenticated caller workflow identity. -A valid bundle therefore answers “these are the authenticated evidence bytes for this exact CWL source/run.” It does **not** answer “p95 passed.” A product may issue a positive commercial performance receipt only after its own acceptance logic validates the authenticated evidence under its domain policy. +A valid bundle therefore answers “these are the authenticated evidence bytes for this exact CWL source/run and caller workflow.” It does **not** answer “p95 passed.” A product may issue a positive commercial performance receipt only after its own acceptance logic validates the authenticated evidence under its domain policy. ## Rejected alternatives - **Caller-supplied result digest as trust root.** Rejected because a caller that can replace the result can also recompute the digest. +- **Repository/SHA-only caller authentication.** Rejected because it does not distinguish the canonical benchmark caller from another workflow at the same repository/SHA. The caller's OIDC `workflow_ref` and `workflow_sha` are now part of the signed evidence identity. - **`github.workflow_sha` as reusable-workflow source identity.** Rejected for cross-repository callers because the reusable workflow inherits caller context. OIDC `job_workflow_ref`/`job_workflow_sha` is the prerequisite repair owned by #2164/#1228. - **`actions/download-artifact` extraction before bounded validation.** Rejected because the archive would be expanded before the trusted verifier can enforce uncompressed limits. The current path authenticates and bounds the ZIP first, then uses the central materializer. - **Central latency `PASS`.** Rejected because the organization signer owns evidence authenticity, not product workload semantics or acceptance thresholds. @@ -65,6 +69,8 @@ This contract remains mutable until its prerequisite stack is merged through pro GitHub. (2026). *REST API endpoints for GitHub Actions artifacts*. GitHub Docs. https://docs.github.com/en/rest/actions/artifacts +GitHub. (2026). *Using OpenID Connect with reusable workflows*. GitHub Docs. https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-with-reusable-workflows + GitHub. (2026). *Using artifact attestations and reusable workflows to achieve SLSA v1 Build Level 3*. GitHub Docs. https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/increase-security-rating GitHub. (2026). *Verifying attestations offline*. GitHub Docs. https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/verify-attestations-offline From 8238e293b8560158a8eb665559768ecc8e4c6073 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 06:13:59 +0900 Subject: [PATCH 44/47] fix(perf): normalize corrupt ZIP stream failures --- scripts/ci/materialize_product_performance_artifact.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/materialize_product_performance_artifact.py b/scripts/ci/materialize_product_performance_artifact.py index 834dd8da3a..f5d61ea580 100644 --- a/scripts/ci/materialize_product_performance_artifact.py +++ b/scripts/ci/materialize_product_performance_artifact.py @@ -9,6 +9,7 @@ import stat import sys import zipfile +import zlib from pathlib import Path from typing import BinaryIO @@ -176,7 +177,7 @@ def materialize( expected_size=member.file_size, ) return {"member_count": 3, "total_uncompressed_bytes": total} - except zipfile.BadZipFile as error: + except (zipfile.BadZipFile, zlib.error, EOFError) as error: shutil.rmtree(output, ignore_errors=True) raise MaterializationError( "performance artifact member data is corrupted" From fabae50166d0d6979bb71e8a93de2126528cb50d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 06:14:12 +0900 Subject: [PATCH 45/47] test(perf): cover corrupt ZIP stream exceptions --- ...oduct_performance_corrupt_stream_errors.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/test_product_performance_corrupt_stream_errors.py diff --git a/tests/test_product_performance_corrupt_stream_errors.py b/tests/test_product_performance_corrupt_stream_errors.py new file mode 100644 index 0000000000..b7ee5eb02e --- /dev/null +++ b/tests/test_product_performance_corrupt_stream_errors.py @@ -0,0 +1,52 @@ +"""Regressions for corrupt ZIP stream failures at the performance-artifact boundary.""" + +from __future__ import annotations + +import zipfile +import zlib +from pathlib import Path + +import pytest + +from scripts.ci import materialize_product_performance_artifact as materializer + + +def _archive(path: Path) -> None: + """Write the exact three-member archive required to reach streaming.""" + with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as bundle: + bundle.writestr("result.json", b"{}") + bundle.writestr("runtime.json", b"{}") + bundle.writestr("fixture.json", b"{}") + + +@pytest.mark.parametrize( + "stream_error", + [zlib.error("invalid deflate stream"), EOFError("truncated compressed stream")], +) +def test_materialize_normalizes_zip_stream_failures_and_cleans_output( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + stream_error: BaseException, +) -> None: + """Keep decompressor failures inside the deterministic MaterializationError boundary.""" + archive = tmp_path / "evidence.zip" + output = tmp_path / "sealed-evidence" + _archive(archive) + + def fail_stream(*args: object, **kwargs: object) -> int: + """Model failures propagated by ZipExtFile.read after metadata validation.""" + del args, kwargs + raise stream_error + + monkeypatch.setattr(materializer, "_stream_member", fail_stream) + + with pytest.raises(materializer.MaterializationError, match="member data is corrupted"): + materializer.materialize( + archive, + output, + result_filename="result.json", + runtime_filename="runtime.json", + fixture_filename="fixture.json", + ) + + assert not output.exists() From cc44ddb7bcb3a74538659a7c40e9f67170e28057 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 21:03:58 +0900 Subject: [PATCH 46/47] fix(ci): execute corrupt performance artifact regressions --- .github/workflows/product-performance-attestation-quality.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/product-performance-attestation-quality.yml b/.github/workflows/product-performance-attestation-quality.yml index 0bd7a460a1..5c81f358e8 100644 --- a/.github/workflows/product-performance-attestation-quality.yml +++ b/.github/workflows/product-performance-attestation-quality.yml @@ -10,6 +10,7 @@ on: - "scripts/ci/verify_product_performance_evidence.py" - "tests/test_product_performance_artifact_materializer.py" - "tests/test_product_performance_attestation_contract.py" + - "tests/test_product_performance_corrupt_stream_errors.py" - "tests/test_product_performance_evidence_verifier.py" - "tests/test_product_performance_profile_binding.py" - "docs/doctoring/product-performance-attestation.md" @@ -59,6 +60,7 @@ jobs: scripts/ci/verify_product_performance_evidence.py \ tests/test_product_performance_artifact_materializer.py \ tests/test_product_performance_attestation_contract.py \ + tests/test_product_performance_corrupt_stream_errors.py \ tests/test_product_performance_evidence_verifier.py \ tests/test_product_performance_profile_binding.py @@ -88,6 +90,7 @@ jobs: python -m coverage run --branch -m pytest -q \ tests/test_product_performance_artifact_materializer.py \ tests/test_product_performance_attestation_contract.py \ + tests/test_product_performance_corrupt_stream_errors.py \ tests/test_product_performance_evidence_verifier.py \ tests/test_product_performance_profile_binding.py python -m coverage report \ From db4962ac1a39c5cd4e99b7ce95c153347886a9ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 02:12:07 +0900 Subject: [PATCH 47/47] test(perf): assert validated caller workflow identity --- tests/test_product_performance_attestation_contract.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_product_performance_attestation_contract.py b/tests/test_product_performance_attestation_contract.py index 22b8eeeabc..97848ebb60 100644 --- a/tests/test_product_performance_attestation_contract.py +++ b/tests/test_product_performance_attestation_contract.py @@ -66,8 +66,11 @@ def test_reusable_workflow_authenticates_and_records_caller_workflow_identity() assert workflow.count("caller_workflow_sha=") >= 2 assert workflow.count("--caller-workflow-ref") >= 2 assert workflow.count("--caller-workflow-sha") >= 2 - assert '"caller_workflow_ref": arguments.caller_workflow_ref' in verifier - assert '"caller_workflow_sha": arguments.caller_workflow_sha' in verifier + assert "caller_workflow = _require_authenticated_caller_workflow(arguments)" in verifier + assert 'predicate["caller_workflow_ref"] = caller_ref' in verifier + assert 'predicate["caller_workflow_sha"] = caller_sha' in verifier + assert 'manifest["caller_workflow_ref"] = caller_ref' in verifier + assert 'manifest["caller_workflow_sha"] = caller_sha' in verifier def test_reusable_workflow_fails_closed_for_non_cwl_callers() -> None: