How a Relying Party Verifies a Credential a Holder Presents
The SDK tells you a signature is valid. It does not tell you whether the issuer deserves your trust, and it will not fetch the issuer's key for you: you have to supply it. That boundary is the most important thing on this page.
The three steps, and only one of them is ours
- Decide which issuers you accept. This is your policy decision. Nothing in our software makes it, suggests it, or records it.
- Get that issuer's public key. For a
did:solidusissuer, resolve the identifier against the chain and read the key out of the document: one unauthenticated request, shown on what a resolution actually returns. - Verify the presented credential against that key. This step is the SDK's.
Step 1 has no technical answer. A valid signature from an issuer you have never heard of is a valid signature from an issuer you have never heard of.
What verification actually enforces, run, not described
Executed on 2026-07-31 from a clean directory against the published package:
1 bearer credential, plain verify → valid=true
2 credential expiring 2020-01-01 → valid=false "Verify Error: JWT is expired"
3 Wrong issuer public key → valid=false "Verify Error: Invalid JWT Signature"
4 bearer + verifier demands key binding → valid=false "KB-JWT required but not present in compact form"
Case 3 is the control that makes the rest mean anything: a verifier that returns valid=true for
the wrong key is not verifying. This one does not.
Case 2 corrected us. Reading the function, we saw no expiry check and were about to write that freshness is not enforced. It is, the underlying library rejects an expired credential before our code sees it. We tested instead of publishing the reading, and the reading was wrong.
Case 1 is the one to worry about
A plain verify accepts a bearer credential. Our own module documentation says it outright: "bearer presentations remain accepted."
So the default posture of a verifier written the obvious way is: whoever hands me these bytes, I believe. If the credential was issued without a holder key, which is the issuance default: nothing in a plain verify distinguishes the person it was issued to from anybody who copied it.
Two opt-ins have to be taken, on opposite sides, by different parties, for that to be fixed. The issuer has to bind the credential to a holder key. The verifier has to ask for proof. Miss either and you have a bearer token with extra steps.
How a verifier refuses bearer
Supply an expected audience, an expected nonce, or both. Then the verifier:
- requires a Key-Binding JWT to be present,
- pulls the holder's key out of the credential's
cnfclaim, and fails if there isn't one, - checks the key-binding signature against that key,
- checks the audience and the nonce separately, so you learn which one was wrong,
- and checks that the key-binding proof covers the exact credential body presented.
Case 4 above is that path working: a bearer credential offered to a verifier that demands key binding is rejected, and the error says exactly why.
Ask for a fresh nonce every time. That is what makes a captured presentation useless later, and it is the verifier's job, not ours.
Two honest sharp edges
- The body-binding check is skipped when the field is absent. The proof-covers-this-credential check runs only if the holder's proof carries that field. A verifier that wants specification-strict behaviour should require it rather than assume it was checked. We are not calling this an exploit, the audience and nonce checks still apply, but "checked when present" is not the same as "required", and the difference belongs in the open.
- Revocation is not checked unless you wire it up. The verifier only consults a status list if you hand it a fetcher. Otherwise a revoked credential verifies exactly like a live one. And the bound from credential status still applies on top: no published list identifier is discoverable from outside.
What comes back
A boolean, the claims the holder chose to disclose, and the issuer and type strings. Housekeeping fields are stripped, the caller sees claims, not the machinery that made them selectively disclosable.
What you cannot check from this page
What the code does is checkable; that anyone relies on it is not, because nobody does yet.
Our content plan calls this page /accept-a-credential. That route is not live: it returns
404, and so does a deliberately fake path.

