Add EvidenceReceipt v2 verifier support#14
Conversation
Adds EvidenceReceipt v2 verification alongside the existing ActionReceipt v1 path. v1 callers see no behavior change. New v2 surface is required by Pipelock v2.4 release-spec criteria 2 and 5: every contract-aware proxy decision and contract-lifecycle event ships in the new envelope, and external auditors must be able to verify them. New surface: - Version routing in `verify()`: dispatches on the top-level `record_type` discriminator. `"action_receipt_v1"` (or absent) routes to v1. `"evidence_receipt_v2"` routes to v2. Unknown types are rejected with a clear error. - `verify_evidence()` for direct v2 verification with full payload detail. - 13 EvidenceReceipt v2 payload kinds: `proxy_decision`, `contract_ratified`, `contract_promote_intent`, `contract_promote_committed`, `contract_rollback_authorized`, `contract_rollback_committed`, `contract_demoted`, `contract_expired`, `contract_drift`, `shadow_delta`, `opportunity_missing`, `key_rotation`, `contract_redaction_request`. Each has its own schema validator with strict unknown-field rejection. - Key-purpose authority matrix: receipts must carry the correct `key_purpose` for their payload kind. Mismatches reject. - RFC 8785 JCS canonicalization (`_jcs.py`) for signable preimage computation. Mirrors the Go reference: parse strict, reject duplicate keys, reject floats, NFC-normalize strings, sort keys. - Well-known directory fetch (`_directory.py`): `fetch_directory()` and `parse_directory()` per RFC 9421. The README example switches from hardcoded SHA hex to directory-based key discovery. Backward compatibility: confirmed across all 62 original tests. Public v1 API surface unchanged. v1 conformance golden files verify byte-identically. Tests: 172 pass (62 v1 unchanged + 110 new). Build: clean wheel + sdist. Lint: ruff check + ruff format both pass. Follow-ups (tracked): - Cross-implementation Go-generated v2 conformance golden vectors not yet in `tests/conformance/`. v2 signature round-trip is currently proven via Python-generated keys only. To prove byte-for-byte parity with the Go reference, generate v2 fixtures from `internal/contract/receipt` and copy them in. - `verify_chain()` still validates v1 chain linkage only. Mixed v1/v2 chains require a chain-verifier upgrade in a follow-up.
Adds three Go-emitted v2 receipt fixtures and a conformance test suite
that loads each via the Python verifier. Proves byte-for-byte JCS
preimage parity between the Go reference (internal/contract/receipt)
and the Python implementation for the proxy_decision, contract_promote_
committed, and shadow_delta payload kinds — the load-bearing audit
kinds for the v2.4 release spec's external verification claim.
Fixtures (under tests/conformance/):
valid-evidence-proxy-decision.json
valid-evidence-promote-committed.json
valid-evidence-shadow-delta.json
Each is signed with the RFC 8032 section 7.1 test-1 private seed, so
the corresponding public key (also in v2-test-keys.json) is the same
across both implementations. A divergence in either side's
canonicalisation logic surfaces here as a signature mismatch.
tests/test_v2_conformance.py covers:
- parametrised happy-path verification for each fixture
- tampered-payload rejection (single-byte verdict flip on
proxy_decision must invalidate the signature)
- wrong-key rejection (all-zero public key must fail)
172 v2 tests in test_evidence.py + 18 in test_directory.py + 5 here +
the v0.1.x suite = 178 total. All pass.
Follow-up: the remaining 10 v2 payload kinds (contract_ratified,
contract_promote_intent, contract_rollback_authorized/committed,
contract_demoted, contract_expired, contract_drift, opportunity_missing,
key_rotation, contract_redaction_request) are still synthesised
internally by test_evidence.py and not yet covered by Go-emitted
goldens. Adding them follows the same pattern: emit from
internal/contract/receipt/golden_vectors_test.go with UPDATE_GOLDEN=1,
copy into tests/conformance/, append to V2_FIXTURES.
Extract InvalidReceiptError, _is_valid_rfc3339, and _RFC3339_RE into a new leaf module _common.py. Both _verify (v1) and _evidence (v2) now import from _common, which has no intra-package imports of its own. This removes the static cycle CodeQL flagged at _evidence.py:19 and _verify.py:184. The class object identity is preserved (re-exported via `import X as X` from _verify), so existing `except InvalidReceiptError` callers and the public pipelock_verify.InvalidReceiptError API continue to work.
The v2-in-chain fail-closed branch reported the list index for broken_at_seq instead of the receipt's declared chain_seq, which diverges from the v1 branch (which reads action_record.chain_seq with index fallback). Auditors now see the same sequence number the emitter wrote. Defensive: only accept int values (rejecting bool, which Python treats as int). Missing or non-int chain_seq falls back to the list index so broken_at_seq is never None on the fail-closed path. Adds three regression tests covering declared, missing, and non-int chain_seq cases.
CI on the branch has been failing both lint (ruff format) and typecheck (mypy strict no-any-return) since b64fa7d. Fixing both together so the branch goes green. mypy: _PAYLOAD_VALIDATORS was typed `dict[str, Any]`, which lost the validators' `(payload: dict[str, Any]) -> str | None` signature and made `_validate_payload` return Any from a `str | None` slot. Replace with `dict[str, Callable[[dict[str, Any]], str | None]]` so the return type checks through the dispatch. ruff format: blank-line and string-wrap normalisations across _verify.py and the new test_regressions.py block. Auto-applied.
…receipt-v2 # Conflicts: # README.md # pipelock_verify/__init__.py # pyproject.toml
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR establishes explicit scope boundaries around receipt verification modes, introduces deterministic dependency management via hash-locked requirements, updates CI/Release workflows to use those lockfiles, and adds Atheris-based fuzzing infrastructure to detect unexpected verifier behavior during arbitrary input parsing. ChangesScope, Dependencies, and Workflows
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@fuzz/receipt_fuzzer.py`:
- Around line 29-32: The broad "except Exception" in the try/except block should
not swallow unexpected crashes; replace it with explicit expected exception
types (e.g. JSONDecodeError, ValueError, SignatureVerificationError or whatever
verifier-specific exception classes are raised) and let any other exceptions
propagate (or re-raise them) so the fuzzer can surface real bugs. Locate the
try/except around receipt parsing/verification in fuzz/receipt_fuzzer.py (the
block using "except Exception") and change it to catch only the known, benign
errors returned by your parser/verifier, returning for those cases and
re-raising or not catching any other exception types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a41b2e0a-1e46-44e5-9efb-06af36d7dd4d
📒 Files selected for processing (13)
.github/workflows/ci.yml.github/workflows/fuzz.yml.github/workflows/release.ymlREADME.mdfuzz/receipt_fuzzer.pypipelock_verify/__init__.pypyproject.tomlrequirements/ci.txtrequirements/fuzz.inrequirements/fuzz.txtrequirements/pip.inrequirements/pip.txtrequirements/release.txt
Summary
Adds Python verification support for Pipelock EvidenceReceipt v2 while preserving ActionReceipt v1 chain verification. The release also hardens CI and release workflows with hashed dependency installs, pinned release tooling, and a fuzz smoke workflow gated behind the Pipelock security scan.
Changes
License-Expression: Apache-2.0andRequires-Python: >=3.9.2.python -m build --no-isolation.Validation
python -m pip install --require-hashes -r requirements/ci.txtpytest --cov=pipelock_verify --cov-report=term-missing— 180 passed, 90% coverageruff check pipelock_verify tests fuzzruff format --check pipelock_verify tests fuzzmypy pipelock_verifypython fuzz/receipt_fuzzer.py -runs=256python -m pip install --require-hashes -r requirements/pip.txtinstalledpip==26.0.1python -m pip install --require-hashes -r requirements/release.txtpython -m build --no-isolationtwine checkon the built sdist and wheelNotes
This does not change repository branch protection, reviewer requirements, or external project settings.
Summary by CodeRabbit
Documentation
New Features
Chores