@zanii/credentials
v0.1.0
Published
Verifiable institutional credentials — diplomas, licences, certifications — offline-verifiable in milliseconds with no call to the registrar. Four checks: signature, expiry, revocation (with an explicit freshness window), and a domain-bound issuer root-of
Readme
@zanii/credentials
Verifiable institutional credentials — diplomas, professional licences, halal/organic certifications, pilot ratings. Verify one offline, in milliseconds, with no phone call to the registrar.
A credential is the sentence Zanii already signs for agents: "Issuer I attests that holder H holds claim C, until date E, revocably."
The package is not the credential — it is the root of trust
A signature only proves a key signed this. It does not prove the key is the University of Dubai. So verification runs four checks, and the fourth is the one that matters:
- the issuer's signature is valid,
- the credential has not expired,
- it is not revoked — against a signed list with an explicit freshness window,
- the issuer DID resolves to a named institution — bound to a domain the institution
controls, at
https://<domain>/.well-known/zanii-issuer.json.
Domain-binding deliberately reuses the web's existing trust anchor (the same one TLS and
.well-known OIDC rely on) rather than inventing a new one.
Zanii is not an accreditation authority and must never become one. We verify that whoever controls
uod.ac.aepublished this DID. We do not decide who is a real university. Becoming the accreditor would make us a political chokepoint, a liability sink, and exactly the dependency institutions cannot afford.
npm install @zanii/credentials @zanii/coreIssue (the institution)
import { buildCredential, buildRevocationList } from '@zanii/credentials';
const cred = buildCredential({
issuer: uni.did, issuerDomain: 'uod.ac.ae', holder: graduate.did,
scope: 'degree.bachelor.cs', claim: { title: 'BSc Computer Science', grade: 'First' },
issuedAt, exp: '2099-01-01T00:00:00Z',
}, uni.privateKey);
// publish periodically; the window is the freshness bound verifiers rely on
const list = buildRevocationList({ issuer: uni.did, revoked: [revokedHash], issuedAt, validUntil }, uni.privateKey);The institution serves, at https://uod.ac.ae/.well-known/zanii-issuer.json:
{ "v": 1, "name": "University of Dubai", "domain": "uod.ac.ae", "dids": ["did:key:z..."] }(Key rotation: list the new DID alongside the old during the overlap window.)
Verify (the employer, bank, border officer — anyone, free, offline)
import { verifyCredential, resolveIssuer, verifyPresentation, buildPresentation } from '@zanii/credentials';
const r = await verifyCredential(cred, { resolver: resolveIssuer, revocationList: list });
r.ok; // all four checks passed
r.issuer.name; // "University of Dubai" — the institution, not just a key
r.revocationFreshness; // 'fresh' | 'stale' | null ← never assumed
// A credential is NOT a bearer token — make the holder prove possession:
const challenge = crypto.randomUUID();
const p = buildPresentation({ credential: cred, challenge, ts }, holder.privateKey);
verifyPresentation(p, cred, challenge); // a thief with the FILE but not the KEY failsNothing is silently assumed. Omit the resolver and verifyCredential tells you
institutional identity was not verified. Omit the revocation list and it tells you
revocation was not checked. ok is never true on an unchecked assumption.
Honest limits (read these)
- We verify institutional identity, never the truth of the claim. A corrupt university can sign a fake degree — but it does so with its own key, on an anchored record. The fraud becomes attributable and permanent instead of deniable. That is the product; it is not "we make lying impossible."
- The domain is the trust root. Whoever controls
uod.ac.aecontrols the credential. A compromised domain compromises everything issued under it. Harden with DNSSEC, rotate keys with an overlap window, and consider an independent co-signer (@zanii/witness). - Offline revocation is a snapshot. A credential verified offline is verified as of the
list you hold. The list carries
valid_until, and a stale list is reported as stale — the freshness bound is stated, never assumed. High-assurance verifiers should reach the log. - W3C Verifiable Credentials export is not v1 — our credentials are conceptually VC-shaped and an export adapter is an obvious later bridge. Deliberately out of scope here.
Changelog
- 0.1.0 — initial release:
buildCredential,verifyCredential(four checks),resolveIssuer(domain-bound root of trust),buildRevocationList(explicit freshness window),buildPresentation/verifyPresentation(possession, not bearer).
License
Apache-2.0.
