Skip to Content
ConceptsChain of CustodyHolder Claim Integrity

Holder Claim Integrity

Companion to Holder Authentication (the passwordless session contract this builds on) and Custodian Onboarding (where the identity that gets claimed is first captured, during intake). This closes a gap neither one addresses on its own: proving the person claiming a credential is the actual subject of the document, not just someone who controls an email address.

Why this exists

  • A custodian-attested credential’s credentialSubject.id is a placeholder DID derived from a single string — holder_email — set either by OCR extraction or by a reviewer typing it directly.
  • Claiming it requires proving control of that email inbox (magic link or passkey sign-in) and nothing else by default: session’s verified email matches the credential’s placeholder DID, full stop.
  • A worse path existed: an exact single-hit fuzzy name match could bind a credential straight to an existing account’s real DID — no placeholder, no claim link, no click.

None of this was a bug in the sense of broken code — every piece did exactly what it said. The gap was architectural: the system verifies who signed a credential perfectly, and verified who was claiming it barely at all. A sloppy OCR read, a typo’d email, or one dishonest custodian reviewer was sufficient to hand a real person’s real, validly-signed credential to the wrong wallet.

How the phases below chain together:

Threat model

  • An attacker who does not control the target inbox cannot reach any of these checks. The claim endpoint already requires a session whose verified email hashes to the credential’s placeholder DID, or the call fails before anything else runs. A stranger who merely knows a credential id (public by design) cannot get past this. The knowledge challenge below defends against a party who already controls, or has compromised, the correct inbox — a narrower, identity-fraud-shaped threat.
  • The actual fraud surface is upstream, at data entry. The identity string a claim gets checked against is only as trustworthy as whoever wrote holder_email — a reviewer’s manual override has zero independent verification otherwise. This is the highest-value target to harden.
  • The fuzzy name auto-bind was the one true zero-click path — no session, no claim, no challenge, nothing. Highest-severity item, with no legitimate reason to be an auto-bind rather than a suggestion.

Design guardrails

GuardrailMeaning
No LLM ever adjudicates a security pass/failAn LLM may phrase a challenge prompt or normalize a name variant, purely advisory — never the code path deciding claimable vs. blocked.
A hold is a floor, not a ceilingElapsed time alone never clears a raised mismatch/fraud flag — only a deterministic auto-pass or explicit admin clearance does.
No new cross-holder or cross-custodian data exposureEvery automated check compares a claim attempt only against the same holder account’s own history.
No new answer-oracleEvery new failure mode returns the same generic caller-facing message and status shape, so an attacker can’t binary-search which check they’re failing.
Everything security-relevant is chained into the tamper-evident audit trailNever a bare log line, and a challenge answer is stored hashed, never in plaintext, including in audit rows.
No new authentication methodEverything is additive to the existing passwordless contract — no password, no new session model.
No permanent dead end for a legitimate holderEvery hard stop has a human path out — never a silent, unappealable block.
Identity resolution stays session/tenant-derivedNothing accepts a client-supplied holder id or email as ground truth without independent verification.

Kill the silent name-only auto-bind

A name-only fuzzy hit is downgraded to a suggestion, surfaced to the custodian reviewer as a pre-filled candidate they can accept or reject — exactly like the existing catalog-entry and issuer suggestions in the same review screen. It never becomes the matched holder without a human confirming it. Every credential issuance that isn’t an exact, pre-existing verified-email match goes out placeholder-first, unconditionally — there is no code path left that delivers directly into an existing account’s wallet without a claim step.

Document-derived knowledge challenge

At review time, one field with independently-guessable-low probability is selected (preferring the last 4 characters of a credential number; falling back to a weaker combination; falling through entirely to the hold-based path if neither is usable). Only SHA-256(normalize(answer)) is ever stored — never the plaintext — alongside an attempt counter and lock timestamp. The claim-link email and wallet claim screen never disclose which field is being asked about beyond a generic label. An LLM may optionally rephrase the prompt for display only, sanitized before rendering and never re-fed into any decision.

Load-bearing identity-check

A computed name-match signal that previously never affected anything now escalates into the hold-and-verify path below on a mismatch or an uncertain signal for a manually-overridden identity — it never auto-denies a claim outright, since name-matching has real false-negative rates (maiden names, transliteration, nicknames).

Rate limiting & lockout

Progressive backoff per (credential_id, session): a short delay after the first miss, growing, then a temporary lock after a small fixed number of misses. A lock requires step-up (a fresh magic-link or passkey ceremony), never a permanent ban — defeating a compromised, already-open browser session without permanently punishing a legitimate holder for typos. Every attempt is chained into the audit trail.

Automated risk resolution during the claim hold

A hold applies whenever the reviewer used a manual holder-email override, OCR confidence was below threshold, or the identity-check signal came back false/unavailable.

Elapsed time is a necessary condition for AUTO_CLEARED (via Consistent), never a sufficient one for ESCALATED — that state has no time-based exit at all, only an admin decision does. The existing-account lookup here resolves through PersonResolutionService (see Cross-Wallet Identity Consolidation), which distinguishes verified-control email from unverified intake evidence and holds a plausible-but-unproven cross-email match rather than merging it — a claim can be blocked on an identity-resolution hold, a credential hold, or both at once.

Audit chain wiring

Every step above — challenge attempts, hold transitions, onboarding-invite send/complete, admin fraud-queue decisions, dispute filed/resolved — writes into the same tamper-evident audit chain verification and issuance events already extend, never a second ledger. A hold’s escalation reason records which comparison failed, never the actual differing strings, so the audit log itself never becomes a new PII leak.

Dispute path

A new credential status, DISPUTED, distinct from REVOKED: hidden from public verification display while under review, but its signature and audit trail remain fully inspectable for the investigation — a false accusation must not destroy a legitimate holder’s record.

Two ways to raise one:

  1. A signed-in holder disputing a credential already sitting in their own wallet.
  2. A no-session report path for someone who was never able to claim their own document because someone else’s account already holds it — necessarily lower-trust, so it routes to manual admin review and requires the reporter to independently re-submit proof out of band; it can only ever create a pending review row, never change the disputed credential’s state directly.

An admin resolves a dispute with a mandatory reason: uphold the existing claim, or reverse it (revoked with category FRAUD_DISPUTE, and the original intake item re-opens for re-identification from scratch rather than using the supersession mechanism, which is for tier convergence, not fraud reversal).