From 64aa8497340fee2bca3a6b3e0e92243afde0c194 Mon Sep 17 00:00:00 2001 From: zoahdev Date: Sat, 22 Aug 2026 22:18:59 +0800 Subject: [PATCH] fix: harden releases and make demo self-verifying --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 45 ++++++++++++++++++++++++++++----- README.md | 4 +++ README.zh-CN.md | 2 ++ docs/RELEASE-REPRODUCIBILITY.md | 9 ++++--- src/kinegrant/cli.py | 36 ++++++++++++++++++++++---- tests/test_protocol.py | 26 +++++++++++++++++++ 7 files changed, 107 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05454e1..5fe18d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: Unit, schema, and security regression tests run: python -m unittest discover -s tests -v - name: Demo smoke test - run: kinegrant-demo > /tmp/kinegrant-demo.json && python -m json.tool /tmp/kinegrant-demo.json >/dev/null + run: kinegrant-demo > /tmp/kinegrant-demo.json && python -c 'import json; assert json.load(open("/tmp/kinegrant-demo.json"))["overall_result"] == "PASS"' - name: Machine Permission Test run: kinegrant-mpt --source-commit "$GITHUB_SHA" --output /tmp/kinegrant-mpt.json >/dev/null && python challenge/verify_evidence.py /tmp/kinegrant-mpt.json - name: External reproduction packet diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9c9eb9..7162473 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,13 +13,42 @@ jobs: hashes: ${{ steps.hash.outputs.hashes }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 with: python-version: "3.12" + - name: Verify tag matches package version + shell: bash + run: | + tag_version="${GITHUB_REF_NAME#v}" + package_version="$(python - <<'PY' + import tomllib + from pathlib import Path + + print(tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]) + PY + )" + source_version="$(python - <<'PY' + import ast + from pathlib import Path + + tree = ast.parse(Path("src/kinegrant/__init__.py").read_text()) + for node in tree.body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == "__version__" + for target in node.targets + ): + print(ast.literal_eval(node.value)) + break + else: + raise SystemExit("__version__ not found") + PY + )" + test "$tag_version" = "$package_version" + test "$tag_version" = "$source_version" - name: Get source date epoch id: epoch - run: echo "epoch=" >> "$GITHUB_OUTPUT" + run: echo "epoch=$(git log -1 --format=%ct)" >> "$GITHUB_OUTPUT" - name: Build distributions env: SOURCE_DATE_EPOCH: ${{ steps.epoch.outputs.epoch }} @@ -31,9 +60,11 @@ jobs: shell: bash run: | cd dist + version="${GITHUB_REF_NAME#v}" + sha256sum *.whl *.tar.gz | sort -k2 > "SHA256SUMS-v${version}.txt" echo "hashes=$(sha256sum * | base64 -w0)" >> "$GITHUB_OUTPUT" - name: Upload artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dist path: dist/ @@ -46,7 +77,7 @@ jobs: actions: read id-token: write contents: write - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 with: base64-subjects: "${{ needs.build.outputs.hashes }}" upload-assets: true @@ -59,12 +90,12 @@ jobs: contents: write steps: - name: Download artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: dist/ - name: Create release and upload assets - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3 with: files: dist/* generate_release_notes: true diff --git a/README.md b/README.md index d28f45a..1f2dac4 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,10 @@ kinegrant-demo docker run --rm ghcr.io/zoahdev/kinegrant-protocol ``` +The command ends with `"overall_result": "PASS"` only when all four boundary +checks pass: the exact action is allowed, recording is denied, replay is denied, +and the signed receipt chain verifies against the caller's executor trust anchor. + > **KGP-001 实验性开放草案 0.1 · 稳定线格式 1.0** > > **参考实现 v2.65.5 · Apache-2.0** diff --git a/README.zh-CN.md b/README.zh-CN.md index 9b0dd3d..4100732 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -25,6 +25,8 @@ kinegrant-demo docker run --rm ghcr.io/zoahdev/kinegrant-protocol ``` +命令只有在四项边界检查全部通过时才会输出 `"overall_result": "PASS"`:精确动作被允许、录像被拒绝、重放被拒绝,并且签名收据链通过调用方提供的执行方信任锚验证。 + > **KGP-001 实验性开放草案 0.1 · 稳定线格式 1.0** > > **参考实现 v2.65.5 · Apache-2.0** diff --git a/docs/RELEASE-REPRODUCIBILITY.md b/docs/RELEASE-REPRODUCIBILITY.md index ece3547..b77b1d9 100644 --- a/docs/RELEASE-REPRODUCIBILITY.md +++ b/docs/RELEASE-REPRODUCIBILITY.md @@ -13,9 +13,10 @@ mtime (after fixing file times), content, and order across builds. The only remaining byte difference is the top-level directory mtime, which setuptools recreates at build time. -- **Authoritative hashes**: produced by the CI release workflow - (`SHA256SUMS-v2.65.5.txt` and the SLSA provenance attestation). Use these - for release verification. +- **Authoritative hashes**: the release workflow derives the build timestamp + from the tagged commit, rejects tags that disagree with both package version + declarations, and publishes `SHA256SUMS-v.txt` alongside the SLSA + provenance attestation. Use these for release verification. ## Not byte-reproducible (known limit) @@ -38,4 +39,4 @@ export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) python -m build --sdist --wheel sha256sum dist/* # compare against SHA256SUMS-v2.65.5.txt -``` \ No newline at end of file +``` diff --git a/src/kinegrant/cli.py b/src/kinegrant/cli.py index 514f085..f23977a 100644 --- a/src/kinegrant/cli.py +++ b/src/kinegrant/cli.py @@ -53,18 +53,44 @@ def run_demo() -> dict[str, object]: gate = ActionGate(trusted_issuers={authority.kid}) claims = gate.authorize(capability, request) + replay_denied = False + try: + gate.authorize(capability, request) + except PermissionError: + replay_denied = True + + recording_request = ActionRequest( + request_id="demo-request-002", + agent=request.agent, + target=request.target, + action="record", + purpose="training", + context=request.context, + ) + recording_decision = engine.evaluate(recording_request) + executor = Ed25519KeyPair.generate() log = ReceiptLog(executor) receipt = log.append(claims, result="succeeded", evidence_hash="sha256:" + "00" * 32) + receipt_chain_valid = verify_receipt_chain( + log.entries, + trusted_executors={executor.kid}, + expected_capability_ids={claims["capability_id"]}, + ) + checks = { + "exact_action_allowed": decision.allowed, + "recording_denied": not recording_decision.allowed, + "replay_denied": replay_denied, + "receipt_chain_valid": receipt_chain_valid, + } return { + "overall_result": "PASS" if all(checks.values()) else "FAIL", + "checks": checks, "decision": decision.to_dict(), + "recording_decision": recording_decision.to_dict(), "capability": capability, "receipt": receipt, - "receipt_chain_valid": verify_receipt_chain( - log.entries, - trusted_executors={executor.kid}, - expected_capability_ids={claims["capability_id"]}, - ), + "receipt_chain_valid": receipt_chain_valid, } diff --git a/tests/test_protocol.py b/tests/test_protocol.py index e941582..0f48c6d 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,8 +1,11 @@ from __future__ import annotations import copy +import json import unittest +from contextlib import redirect_stdout from datetime import timedelta +from io import StringIO from kinegrant.adapters import ( matter_command_request, @@ -18,6 +21,29 @@ from kinegrant.models import ActionRequest, PolicyRule, utc_now from kinegrant.policy import PolicyEngine from kinegrant.receipt import ReceiptLog, verify_receipt_chain +from kinegrant.cli import main as demo_main, run_demo + + +class DemoTests(unittest.TestCase): + def test_demo_is_a_self_verifying_permission_boundary(self) -> None: + report = run_demo() + self.assertEqual(report["overall_result"], "PASS") + self.assertEqual( + report["checks"], + { + "exact_action_allowed": True, + "recording_denied": True, + "replay_denied": True, + "receipt_chain_valid": True, + }, + ) + + def test_demo_cli_emits_machine_readable_json(self) -> None: + output = StringIO() + with redirect_stdout(output): + demo_main() + report = json.loads(output.getvalue()) + self.assertEqual(report["overall_result"], "PASS") class PolicyTests(unittest.TestCase):