@orbis-id/verifier
v0.1.0
Published
Verify an ORBIS-issued SD-JWT VC presentation offline, from published artifacts alone. Zero runtime dependencies; Node and browser. Using it is OPTIONAL — the conformance vectors are the contract.
Downloads
86
Maintainers
Readme
@orbis-id/verifier
Verify an ORBIS-issued credential offline, with no ORBIS account, no ORBIS permission and no network access.
Using this package is OPTIONAL. The contract is the conformance vector set, not this code. If you write your own verifier — in Rust, Go, Java, anything — and it passes the vectors, it is as correct as this one. That is the point: an SDK alone would make ORBIS a soft chokepoint, because ORBIS's bugs would become the specification. An SDK plus vectors makes ORBIS a spec.
- Zero runtime dependencies. Nothing to audit but this package.
- Node ≥ 20 and every modern browser. WebCrypto and
DecompressionStreamonly; nonode:import anywhere in the runtime surface. - No I/O. You fetch the artifacts; this verifies them. That is what makes verification survive ORBIS being unreachable.
Verify in ten lines
import { verifyPresentation } from '@orbis-id/verifier';
const didDocument = await (await fetch('https://orbis.id/.well-known/did.json')).json();
const token = await (await fetch('https://orbis.id/status/1')).text();
const outcome = await verifyPresentation({
presentation, // the vp_token you received
didDocument, // the issuer's published did:web document
statusList: { uri: 'https://orbis.id/status/1', token },
expected: { audience: 'https://your-app.example', nonce: yourNonce },
});
if (outcome.verdict === 'accept') use(outcome.claims);Three verdicts, not two
accept · reject · indeterminate.
indeterminate means the verifier could not answer. There are two ways to get
it from the revocation check, and they are treated identically:
- the credential names a status list and you did not supply one; or
- the credential carries no
statusclaim at all, so it declares no revocation mechanism for this package to read.
It is never an accept, and turning it into a reject is your policy decision to make explicitly, not one this package will make quietly for you.
A credential with no revocation mechanism is never accepted. Not being told whether something was revoked is not the same as being told it was not, and this package will not spell the first as the second. The only exception is a
vctin the publishedSTATUS_EXEMPT_VCTSlist, which is exported so you can read it — it is currently empty.
The outcome also carries a check ledger: all ten links of the chain, each
with its own status (passed, failed, not_applicable, not_performed).
The verdict is derived from that ledger, so a check that did not run can never
be reported as one that passed.
outcome.checks
// [ { id: 'presentation_wellformed', status: 'passed', detail: '…' },
// { id: 'revocation', status: 'not_performed', detail: '…' }, … ]What it checks
| Check | What it establishes |
|---|---|
| presentation_wellformed | It parses as an ES256 SD-JWT VC with a key-binding JWT |
| did_document_binding | The document's own id is the DID the credential names, and its keys are well-formed public P-256 points |
| issuer_signature | The issuer JWT verifies under a key that document publishes |
| disclosure_integrity | Every presented disclosure's digest is in the signed _sd |
| holder_binding | The KB-JWT is signed by the credential's own cnf.jwk |
| key_binding_challenge | That KB-JWT names your audience and nonce, and its sd_hash covers exactly these disclosures |
| credential_validity_window | exp / nbf at your evaluation instant |
| credential_type | vct is the type you asked for |
| required_claims | The claims you required were actually disclosed |
| revocation | The signed status list says the bit is clear |
Deliberate limitation, stated rather than hidden: this implementation checks
top-level named disclosures only. A credential using nested or array-element
_sd is refused with unsupported_disclosure_structure — never partially
verified and reported as accepted. ORBIS issues the flat form
(src/slice/issuer.ts), so this is a real limit rather than a theoretical one.
Prove your own verifier
npx orbis-conformance --vectors https://orbis.id/conformance/vectors \
--cmd './target/release/my-verifier'Your command is spawned once and speaks newline-delimited JSON:
→ stdin {"id":"valid","input":{ …the vector's `input` object verbatim… }}
← stdout {"id":"valid","verdict":"accept","failure":null,"claims":{…}}verdict is the only required field beyond id. Exit code is 0 when every
vector passes, 1 otherwise. --self runs this package's own verifier instead;
--json emits a machine-readable report; --require-level2 demands matching
failure codes too.
Two conformance levels. Level 1 is the verdict alone — the binding
contract, which says nothing about your error vocabulary. Level 2 additionally
requires the failure code and the disclosed claims to match; recommended,
because a shared reason code is what lets two systems debug an interop failure,
but never required.
A worked example lives in
examples/independent-verifier.mjs: a
second, complete implementation over Node built-ins that imports nothing from
this package and scores 16/16. It exists to prove the vectors carry the meaning,
and it is the subject of the repository's mutation suite — twelve real code
mutations, every one of which the vectors must catch at level 1.
The vectors
The set is minted, not committed. The bytes are signed SD-JWT VCs and a
statuslist+jwt — high-entropy eyJ… strings that the ORBIS program gate's
secret scanner matches on sight (measured: four jwt findings on an emitted
set), and the constitution reserves the allowlist for verified false positives.
So the repository holds a deterministic recipe and the bytes come from
GET /conformance/vectors or npm run vectors:emit.
Consequences you should design for:
- The bytes differ between mints; never pin them.
schemaversions the shape,setVersionversions the contract, anddigestnames the exact bytes a given report was run against. - Every artifact is inert. Verify with the network switched off — that is the property being demonstrated.
- No PII,
.examplehosts only (RFC 6761), and no JWK private scalar anywhere.
Publishing
This package has not been published, and publishing it is an owner
decision. npm publish fails closed until both of the following are true:
ORBIS_PUBLISH_APPROVED=1is set, and- the package carries a real licence — the repository has no
LICENSEfile, so the licence of a public rail artifact has never been decided, and a verifier a government is asked to depend on cannot ship asUNLICENSED.
The guard is scripts/publish-guard.mjs and it is tested.
