fix(receipts): reject empty-quote receipts on the verify side too - #655
Merged
plind-junior merged 3 commits intoJul 30, 2026
Merged
Conversation
verify_receipt and verify_evidence both guarded on quote is None, not an empty string. quote="" paired with a zero-length span (byte_start == byte_end) decodes source_bytes[start:end] to "", which trivially string-equals the empty quote and returned VERIFIED regardless of source content or length. Evidence.quote carries no min-length constraint at the model layer, and bundle/sync intake write incoming Evidence straight to disk after schema validation only, so a hand-crafted bundle or a malicious federation peer can plant a forged empty-quote Evidence and cite it from an inbound claim. With review.auto_approve_on_receipt (the starter-config default), a claim citing only that receipt cleared evaluate_claim_receipts and landed as a durable, approved claim with zero real evidentiary backing and no human review. locate_span already refuses to mint a receipt for an empty quote (`if not needle: return None`) — the verify side never got the matching guard. Both now use `not quote`, treating "" the same as None. this exact fix was previously submitted and CodeRabbit-approved as vouchdev#513, but that PR was closed unmerged for going stale against a fast-moving test branch, not for anything wrong with the change; the maintainer's closing comment explicitly invited a fresh PR. Fixes vouchdev#654
plind-junior
enabled auto-merge
July 30, 2026 19:02
plind-junior
enabled auto-merge
July 30, 2026 19:05
Contributor
|
diff coverage: n/a — this PR changes no python under |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
verify_receipt()andverify_evidence()insrc/vouch/receipts.pybothnow reject an empty
quote(not quote) instead of onlyquote is None,matching the guard
locate_span()already applies on the mint side(
if not needle: return None).Why
quote=""paired with a zero-length span (byte_start == byte_end)decodes
source_bytes[start:end]to"", which trivially string-equalsthe empty quote and returned
VERIFIED— regardless of source content orlength, even against an empty source.
Evidence.quotecarries nomin-length constraint at the model layer, and
bundle/syncintake writeincoming
Evidencestraight to disk after schema validation only, so ahand-crafted bundle or a malicious federation peer can plant a forged
empty-quote Evidence and cite it from an inbound claim (this can't happen
through the normal propose path, which already routes through
locate_span's guard viareceipt_for_quote).With
review.auto_approve_on_receipt(the starter-config default), aclaim citing only that forged receipt cleared
evaluate_claim_receiptsand landed as a durable, approved claim with zero real evidentiary
backing and no human review — confirmed end-to-end:
proposals. auto_approve_receipts()durably approved a claim reading "the moon ismade of cheese and nobody can stop me" backed by nothing but an
empty-quote, zero-length-span receipt.
verify_evidence()carried the identical gap ahead of its ownsource-read, additionally misreporting a missing-source-plus-empty-quote
case as
FORGEDrather than the more correctNO_RECEIPT.Fixes #654
What might break
Nothing for users with an existing
.vouch/directory — no on-disk shape,kb.*method, or object model change. Behaviorally: anEvidencewith anempty quote now returns
NO_RECEIPTinstead of (incorrectly)VERIFIEDor
FORGEDdepending on path — strictly a correctness fix in the stricterdirection. No legitimate receipt (non-empty quote matching its span) is
affected.
VEP
Not applicable — no object model,
kb.*method, on-disk layout, bundleformat, or audit-log shape change. A guard-condition fix inside
receipts.py's pure verification functions.Prior art
This exact bug and fix were previously submitted as #513
(CodeRabbit-approved, all substantive checks green) but closed unmerged
on 2026-07-29 purely for going stale against
test(aCHANGELOG.mdconflict), not for anything wrong with the change — the maintainer's
closing comment explicitly invited a rebase or fresh PR. Re-verified
independently against current
testHEAD: the fix never landed, so thebug is still live. This PR reintroduces the same fix, freshly rebased,
with additional coverage for the end-to-end
evaluate_claim_receiptsgate.
Tests
make check-equivalent: ruff clean (src+tests); mypyclean on
receipts.py; all 28tests/test_receipts.pycases pass(25 pre-existing + 3 new);
tests/test_proposals.pyandtests/test_extract.py(both depend on the receipt gate) pass withno regressions
test_no_receipt_when_quote_is_empty_string(unit),test_verify_evidence_no_receipt_for_empty_quote(store-backed),test_claim_gate_rejects_forged_empty_quote_receipt(end-to-endevaluate_claim_receipts)CHANGELOG.mdupdated under## [Unreleased]