Skip to content

Add EvidenceReceipt v2 verifier support#14

Merged
luckyPipewrench merged 9 commits into
mainfrom
feat/v0.2.0-evidence-receipt-v2
May 10, 2026
Merged

Add EvidenceReceipt v2 verifier support#14
luckyPipewrench merged 9 commits into
mainfrom
feat/v0.2.0-evidence-receipt-v2

Conversation

@luckyPipewrench
Copy link
Copy Markdown
Owner

@luckyPipewrench luckyPipewrench commented May 10, 2026

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

  • Add EvidenceReceipt v2 verification API and exports, including payload-kind validation, key-purpose authority checks, JCS canonicalization, and Ed25519 verification.
  • Document v2 usage, trust-anchor behavior, well-known directory helpers, and the current v0.2.0 boundary that v2 receipts verify individually while v1 remains the supported chain mode.
  • Require PEP 639 package metadata with License-Expression: Apache-2.0 and Requires-Python: >=3.9.2.
  • Lock CI, fuzz, pip bootstrap, and release dependencies with hashes; release builds use python -m build --no-isolation.
  • Add an Atheris receipt parser smoke workflow that depends on the Pipelock security-scan job.

Validation

  • python -m pip install --require-hashes -r requirements/ci.txt
  • pytest --cov=pipelock_verify --cov-report=term-missing — 180 passed, 90% coverage
  • ruff check pipelock_verify tests fuzz
  • ruff format --check pipelock_verify tests fuzz
  • mypy pipelock_verify
  • python fuzz/receipt_fuzzer.py -runs=256
  • python -m pip install --require-hashes -r requirements/pip.txt installed pip==26.0.1
  • python -m pip install --require-hashes -r requirements/release.txt
  • python -m build --no-isolation
  • twine check on the built sdist and wheel

Notes

This does not change repository branch protection, reviewer requirements, or external project settings.

Summary by CodeRabbit

  • Documentation

    • Clarified support for ActionReceipt v1 chains and individual EvidenceReceipt v2 envelopes; added that v2 receipts are rejected in chain mode.
    • Expanded package documentation with examples for chain verification.
  • New Features

    • Added fuzz testing to improve robustness.
  • Chores

    • Updated minimum Python version to 3.9.2.
    • Introduced reproducible dependency installations with hash verification.
    • Simplified license metadata format.

Review Change Stack

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Warning

Rate limit exceeded

@luckyPipewrench has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 47 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53536982-231a-4a17-b4fc-9632e9395b38

📥 Commits

Reviewing files that changed from the base of the PR and between e939004 and 683710c.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • fuzz/receipt_fuzzer.py
📝 Walkthrough

Walkthrough

This 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.

Changes

Scope, Dependencies, and Workflows

Layer / File(s) Summary
Project Metadata
pyproject.toml
description, requires-python, and license fields are updated to reflect ActionReceipt v1 chain and EvidenceReceipt v2 envelope support; Python floor raised to >=3.9.2.
Module Documentation
pipelock_verify/__init__.py
Package docstring and usage examples document ActionReceipt v1 chain verification and individual v2 envelope support; clarify that v0.2.0 rejects v2 in chain mode.
User Documentation
README.md
Sections on "What gets verified" and "Input formats" specify ActionReceipt v1 chain mode, v2 chain rejection, and individual v2 envelope verification only.
Requirement Input Files
requirements/fuzz.in, requirements/pip.in
Source files pin atheris==3.0.0 (fuzz) and pip==26.0.1 (pip) for compilation into lockfiles.
Compiled Requirement Lockfiles
requirements/ci.txt, requirements/fuzz.txt, requirements/pip.txt, requirements/release.txt
Hash-locked requirements for Python 3.9.2 (CI), 3.11 (Fuzz), 3.12 (Release/Pip) enable deterministic --require-hashes installation.
CI Workflow Updates
.github/workflows/ci.yml
Test, lint, and typecheck jobs now install dependencies from requirements/ci.txt using --require-hashes; lint extends coverage to fuzz directory.
Release Workflow Updates
.github/workflows/release.yml
Release workflow replaces inline pins with hash-locked installs from requirements/pip.txt and requirements/release.txt; build uses --no-isolation.
Fuzz Workflow and Target
.github/workflows/fuzz.yml, fuzz/receipt_fuzzer.py
New Fuzz workflow runs on push/PR/schedule with Pipelock security scan and Atheris fuzzing job; fuzz target truncates byte inputs, decodes as UTF-8, invokes pipelock_verify.verify, and reports uncaught exceptions.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 A fuzzer hops with Atheris keen,
Hash-locked deps keep workflows clean,
v1 chains and v2 envelopes bright,
Scope is now crystal clear—what a sight!
Coverage expands, from lint to the fuzz,
This PR's got all the buzzz. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add EvidenceReceipt v2 verifier support' directly reflects the main change: introducing Python verification support for EvidenceReceipt v2, which is the primary objective of the PR despite also updating documentation, workflows, and packaging metadata.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v0.2.0-evidence-receipt-v2

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 572e157 and e939004.

📒 Files selected for processing (13)
  • .github/workflows/ci.yml
  • .github/workflows/fuzz.yml
  • .github/workflows/release.yml
  • README.md
  • fuzz/receipt_fuzzer.py
  • pipelock_verify/__init__.py
  • pyproject.toml
  • requirements/ci.txt
  • requirements/fuzz.in
  • requirements/fuzz.txt
  • requirements/pip.in
  • requirements/pip.txt
  • requirements/release.txt

Comment thread fuzz/receipt_fuzzer.py Outdated
@luckyPipewrench luckyPipewrench merged commit 8d6aff7 into main May 10, 2026
15 checks passed
@luckyPipewrench luckyPipewrench deleted the feat/v0.2.0-evidence-receipt-v2 branch May 10, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant