Verification
A verdict, and the separate facts behind it, cross-issuer.
Verification is Glemo's product. One call returns a deterministic verdict with the checks that produced it.
Verdicts
| Verdict | Meaning |
|---|---|
valid | Signature, schema, status and validity window all pass |
revoked | The issuer flipped this credential's bit in their status list |
expired | Past validUntil |
tampered | Signature or schema check failed, the content was altered |
not_found | No credential with that id |
not_verifiable | We could not check it: the issuer's DID did not resolve (network, unknown issuer), or an uploaded image carries no credential we can read. Note the difference from not_found, which asserts the credential does not exist |
The facts behind the verdict
The verdict is a summary. Alongside it every verification returns a report: the
facts, one by one, in the shape the W3C VCDM 2.0 verification result
defines. You do not have to accept our summary to use our answer.
| Field | What it says |
|---|---|
proof | Whether the signature verified against the issuer's key |
credentialStatus | The issuer's status list, as a list whose value is an integer, not a boolean: a credential can be suspended rather than revoked, and a boolean cannot say so |
validFrom, validUntil | Two facts, not one. "Not valid yet" and "no longer valid" are different answers, and a single expired flag collapses them |
provenance | How the evidence reached us: artifact, zktls, wallet or registry. Two of the four work today, and every zkTLS recipe in the issuer registry is still sandbox: no real proof has been generated yet |
observations.issuerTrusted | Whether the issuer holds an accreditation |
warnings, errors | RFC 9457 problem details, split by whether they are recoverable |
Two rules make the report honest, and both are worth knowing before you build on it:
A fact we did not check comes back null with its reason, never false. If no
trust registry is configured, issuerTrusted is not "the issuer is untrusted": it is
"nobody asked". A verifier that renders those the same way is telling its user
something we never said.
Errors and warnings are not the same kind of thing. A signature that does not
verify is an error: nothing downstream can recover from it. A credential listed in
its issuer's status list is a warning: it is a fact about status, and what to do
about it is the calling application's decision, not ours.
Methods
byHash: look up a credential by id in the registry, check status and expiration. The fast path (sub-millisecond server compute).byVC: verify a portable VC-JWT: resolve the issuer's DID, check the ES256 signature, the schema, the status list and the validity window. Works for credentials issued by any issuer, not just those registered in Glemo.
Cross-issuer
For external issuers, Glemo resolves did:web documents over HTTPS (with
caching and timeouts) and fetches their status lists. If an issuer can't be
resolved, the verdict is an explicit not_verifiable, never a hang, never a
guess.
Webhooks
Every verification can notify your systems: see Webhooks.
Signed receipts
Every authenticated verification returns a signedEvidence field: a signed record of
what you checked, what came back, and when. It is a Security Event Token
(RFC 8417), so it carries no expiry: it
describes something that already happened, and a receipt you produce in three years to
show what you checked must not have gone stale.
Failed verdicts get a receipt too. Proving that you checked and it came back revoked is usually the reason you need one.
We keep no copy. The receipt is yours; if you lose it we can issue a new one, but
its iat will be today and the difference is visible in the token.
Verifying one, without asking us
The receipt verifies against the published key set. This depends on jose and nothing
of ours: no API key, no SDK, no call to a private endpoint.
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://api.glemo.io/.well-known/jwks.json"));
const { payload, protectedHeader } = await jwtVerify(signedEvidence, JWKS, {
issuer: "https://api.glemo.io",
});
console.log(protectedHeader.typ); // secevent+jwt
console.log(payload.toe, payload.txn); // when it happened, which verification
console.log(payload.events["https://glemo.io/ns/verification"]);toe is when the verification ran and iat is when the receipt was signed: two
instants, not one. txn correlates with the verification event in your own logs.
A receipt whose payload was altered fails with ERR_JWS_SIGNATURE_VERIFICATION_FAILED;
one signed with a key that is not in the set fails with ERR_JWKS_NO_MATCHING_KEY.
Rotating our key does not invalidate old receipts, because the superseded key stays
published.