Skip to content

fix(verify): forward the anti-fake QR MAC (?m=) to the KeyHalve rail — distinct fail-closed MAC errors - #19

Merged
ValidPay-io merged 2 commits into
mainfrom
fix/qr-mac-forwarding
Jul 19, 2026
Merged

fix(verify): forward the anti-fake QR MAC (?m=) to the KeyHalve rail — distinct fail-closed MAC errors#19
ValidPay-io merged 2 commits into
mainfrom
fix/qr-mac-forwarding

Conversation

@ValidPay-io

@ValidPay-io ValidPay-io commented Jul 19, 2026

Copy link
Copy Markdown
Owner

The bug (Mike's live repro, 2026-07-17)

Documents sealed since QR_MAC_ENFORCE carry &m= in their verify URL, and the KeyHalve rail gates GET /v1/piece/{id} behind it (bare → 403 {"error":"mac_required"}; with ?m= → 200; wrong m → 403 mac_invalid). The SDK's verify path never forwarded m to the rail piece fetch → 403 → surfaced as a generic rail_error, which downstream (the MCP connector) reported as "the KeyHalve rail is unreachable / down" — while the rail was healthy.

Live repro: verify.keyhalve.com/verify/vp_t4oct5g4qq0b?t=validpay&m=X6n5UyGi#key=… — rail piece 403 bare, 200 with m (re-confirmed live before this fix).

The fix

  • verifyIntent(id, key, { qrMac }) — new explicit option; forwarded as ?m= on the rail piece request (fetchRailPiece gained a qrMac parameter). Shape-validated up front against QR_MAC_RE = /^[A-Za-z0-9_-]{8,16}$/.
  • Error mapping — MAC verdicts are never network errors:
    • rail 403 mac_invalidValidPayError("qr_mac_invalid")"Anti-fake code rejected — … treat this document as fraudulent." Fail closed.
    • rail 403 mac_requiredValidPayError("qr_mac_required")"This document requires its anti-fake code (m) from its QR/URL …" (actionable; occurs for legacy callers that don't pass m).
    • other 403s stay rail_error; real transport failure stays rail_unreachable.
  • Pinned-key Ed25519 rail-signature verification (v1 custody + M2 content binding) is untouched.
  • Exports VerifyIntentOptions and QR_MAC_RE so URL-parsing callers (MCP, verify surfaces) share the contract.
  • Legacy path byte-identical: no qrMac → bare piece GET, exactly as before (pre-enforcement documents still verify).

Tests

vitest: 109 passing (13 new) — ?m= forwarding (rail + full end-cell client flow, mock rail), mac_invalid/mac_required/other-403/unparseable-403 mapping, network failure still rail_unreachable, malformed qrMac rejected before any network call, no-qrMac legacy path unchanged.

Related

  • Companion MCP PR: ValidPay-io/validpay-mcp#2 — makes validpay_verify pass the pasted URL's m through and word the three failure modes distinctly.
  • Same bug class on the certificate surface: keyhalve/keyhalve-website#49.
  • Same gap exists in the Python and React Native SDKs — NOT fixed here; touch-points listed in the task report.

HELD FOR MIKE'S GO — do not merge/release without explicit approval. Release (npm publish) stays Mike-gated per the SDK publish gate.

🤖 Generated with Claude Code


Extension (2026-07-19): seal-side half of the MAC class — 4th surface

Mike's live repro #2: a real lender document sealed through the connector produced a QR that scans RED. Cause: the API's POST /v1/intent response includes a one-time qr_mac (rail-minted at deposit, never served by any public endpoint afterwards), but CreateIntentResult was {retrievalId, key} only — the MAC was dropped on the floor, so the built verify URL/QR had no ?m= and the rail (correctly) refuses the share.

  • CreateIntentResult.qrMac — threaded from the raw response in every creation path: createIntent, createEndCellIntent, createFileIntent, createSelectiveIntent, and per-item in createIntentBatch (defensive; the batch path rejects End-Cell today so it mints none).
  • buildVerifyUrl / embedQr converged URL shape — new VerifyUrlOptions.tenant + .qrMac: <base>/verify/<id>[?t=<tenant>][&m=<qrMac>]#key=… (t before m; params omitted when absent; the bare legacy shape is byte-identical when neither is given; qrMac shape-validated, fail closed).
  • Tests: 119 passing (10 more) — creation carries qrMac (present/absent/batch per-item), URL shapes (none / t-only / m-only / both / custom base / malformed).

For the record: documents already sealed through the old connector (e.g. vp_ybzx03cmbgxj) have their correct m server-side (intents.rail_qr_mac → the dashboard detail page's verification_url carries it) — their QR just needs rebuilding from that URL + the held key (the MCP's validpay_qr now preserves ?m= on rebuild).

Mike and others added 2 commits July 19, 2026 12:35
… distinct fail-closed MAC errors

Documents sealed since QR_MAC_ENFORCE carry &m= in their verify URL and the
rail gates GET /v1/piece/{id} behind it. verifyIntent never forwarded it, so
MAC-gated documents 403'd and surfaced as a generic rail_error ("rail
unreachable") even though the rail was healthy.

- verifyIntent(id, key, { qrMac }) forwards ?m= on the rail piece request
- rail 403 mac_invalid  -> qr_mac_invalid  (treat as fraudulent, fail closed)
- rail 403 mac_required -> qr_mac_required (supply the document's m)
- real transport failures remain rail_unreachable / rail_error
- pinned-key Ed25519 verification of the rail response unchanged
- exports VerifyIntentOptions + QR_MAC_RE for URL-parsing callers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed ?t=/&m= verify URL

POST /v1/intent returns a ONE-TIME qr_mac for End-Cell seals minted under
QR-MAC enforcement (ValidPay-API intent.ts). CreateIntentResult dropped it,
so freshly sealed documents were given verify URLs/QRs without ?m= — and the
rail (correctly) refuses their share: the QR scans RED.

- CreateIntentResult.qrMac threaded from every creation path: createIntent,
  createEndCellIntent, createFileIntent, createSelectiveIntent, and
  per-item in createIntentBatch (defensive; batch mints none today)
- buildVerifyUrl/embedQr: new VerifyUrlOptions.tenant + .qrMac emit the
  converged shape <base>/verify/<id>[?t=<tenant>][&m=<qrMac>]#key=...
  (t before m, omitted when absent; bare legacy shape byte-identical
  when neither is given; qrMac shape-validated, fail closed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ValidPay-io
ValidPay-io merged commit f4a18c6 into main Jul 19, 2026
1 check passed
@ValidPay-io
ValidPay-io deleted the fix/qr-mac-forwarding branch July 19, 2026 18:56
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