Skip to content

fix(receipts): reject empty-quote receipts on the verify side too - #655

Merged
plind-junior merged 3 commits into
vouchdev:testfrom
galuis116:fix/receipts-empty-quote-verify
Jul 30, 2026
Merged

fix(receipts): reject empty-quote receipts on the verify side too#655
plind-junior merged 3 commits into
vouchdev:testfrom
galuis116:fix/receipts-empty-quote-verify

Conversation

@galuis116

Copy link
Copy Markdown
Contributor

What changed

verify_receipt() and verify_evidence() in src/vouch/receipts.py both
now reject an empty quote (not quote) instead of only quote 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-equals
the empty quote and returned VERIFIED — regardless of source content or
length, even against an empty source. 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 (this can't happen
through the normal propose path, which already routes through
locate_span's guard via receipt_for_quote).

With review.auto_approve_on_receipt (the starter-config default), a
claim citing only that forged receipt cleared evaluate_claim_receipts
and 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 is
made 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 own
source-read, additionally misreporting a missing-source-plus-empty-quote
case as FORGED rather than the more correct NO_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: an Evidence with an
empty quote now returns NO_RECEIPT instead of (incorrectly) VERIFIED
or FORGED depending on path — strictly a correctness fix in the stricter
direction. No legitimate receipt (non-empty quote matching its span) is
affected.

VEP

Not applicable — no object model, kb.* method, on-disk layout, bundle
format, 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 (a CHANGELOG.md
conflict), not for anything wrong with the change — the maintainer's
closing comment explicitly invited a rebase or fresh PR. Re-verified
independently against current test HEAD: the fix never landed, so the
bug is still live. This PR reintroduces the same fix, freshly rebased,
with additional coverage for the end-to-end evaluate_claim_receipts
gate.

Tests

  • Local make check-equivalent: ruff clean (src + tests); mypy
    clean on receipts.py; all 28 tests/test_receipts.py cases pass
    (25 pre-existing + 3 new); tests/test_proposals.py and
    tests/test_extract.py (both depend on the receipt gate) pass with
    no regressions
  • New / changed behaviour has a test —
    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-end
    evaluate_claim_receipts)
  • CHANGELOG.md updated under ## [Unreleased]

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
@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance tests tests and fixtures size: XS less than 50 changed non-doc lines labels Jul 30, 2026
@plind-junior
plind-junior enabled auto-merge July 30, 2026 19:02
@github-actions
github-actions Bot disabled auto-merge July 30, 2026 19:02
@plind-junior
plind-junior enabled auto-merge July 30, 2026 19:05
@plind-junior
plind-junior merged commit 0d63d9e into vouchdev:test Jul 30, 2026
16 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

diff coverage: n/a — this PR changes no python under src/vouch/, so there is nothing for the gate to measure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci: passing ci is green docs documentation, specs, examples, and repo guidance size: XS less than 50 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants