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.idis 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
| Guardrail | Meaning |
|---|---|
| No LLM ever adjudicates a security pass/fail | An 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 ceiling | Elapsed 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 exposure | Every automated check compares a claim attempt only against the same holder account’s own history. |
| No new answer-oracle | Every 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 trail | Never a bare log line, and a challenge answer is stored hashed, never in plaintext, including in audit rows. |
| No new authentication method | Everything is additive to the existing passwordless contract — no password, no new session model. |
| No permanent dead end for a legitimate holder | Every hard stop has a human path out — never a silent, unappealable block. |
| Identity resolution stays session/tenant-derived | Nothing 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:
- A signed-in holder disputing a credential already sitting in their own wallet.
- 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).