A small, offline, vendor-neutral command-line tool that verifies content-licensing
Verifiable Credentials — signature, validity window, and revocation — with no account,
no login, and no phone-home. It verifies any issuer's did:web credential, and it depends only
on cryptography + the Python standard library — not on any vendor SDK.
There are two independent implementations — Python and TypeScript — that return byte-identical
verdicts (the Node build depends only on its own built-in crypto); see tests/test_parity.py.
Independence: this tool depends on no hosted service and no single vendor's product. It verifies credentials from any issuer, offline, and anyone (including a competitor) can run or reimplement it from the published standards. That is the point.
It is built entirely on open standards:
- W3C Verifiable Credentials Data Model 2.0
- VC-JOSE-COSE (the credential is the JWS payload;
typ: vc+jwt) did:web(issuer key discovery)- W3C Bitstring Status List v1.0 (revocation)
A credential here proves that a party made a signed, revocable declaration about an asset — never that it "owns" anything. This tool checks the declaration; it does not adjudicate ownership.
The AI-content-rights stack has standards for communicating rights (CoMP), expressing them (RSL), and reporting usage (SPUR) — but the layer that lets anyone independently verify that a specific grant was made and is still valid is usually missing or locked inside a proprietary service. This is an open reference for that verify layer. Anyone — including a competitor — can implement the same thing from the published standards; that's the point.
Python (≥ 3.10) — from a clone of this repo (a PyPI package is coming shortly; until then, install from source):
pip install . # puts the `clverify` command on your PATHTypeScript / Node (≥ 18):
cd ts && npm run build # zero-dependency build; then:
node dist/cli.js verify … # same commands & output as the Python CLISee ts/README.md. Both implementations pass the same fixtures identically.
Everything below runs fully offline against the bundled test vectors in
examples/vectors/ — no network, no account.
cd examples/vectors
# 1. A VALID grant → verdict VALID, exit 0
clverify verify credential-valid.jwt \
--did-document did-sandbox.licensefoundry.com.json \
--status-list statuslist-A.jwt --offline
# 2. A REVOKED grant → revocation ✗, verdict INVALID, exit 1 (this is the point)
clverify verify credential-revoked.jwt \
--did-document did-sandbox.licensefoundry.com.json \
--status-list statuslist-A.jwt --offline
# 3. An EXPIRED grant → validity ✗ 4. A TAMPERED grant → signature ✗
clverify verify credential-expired.jwt --did-document did-sandbox.licensefoundry.com.json --status-list statuslist-A.jwt --offline
clverify verify credential-tampered.jwt --did-document did-sandbox.licensefoundry.com.json --status-list statuslist-A.jwt --offline
# 5. A credential from a DIFFERENT issuer verifies too → vendor-neutral, not LF-specific
clverify verify credential-other-issuer.jwt \
--did-document did-publisher.example.json --status-list statuslist-B.jwt --offline
# Human-readable rights (decode only, no verify)
clverify inspect credential-valid.jwtIf (1) and (5) pass, (2)–(4) fail with the right reason, and none of it touched the network, the verify layer works — from open standards, against any issuer, offline.
This is the composition SPUR's Content Telemetry spec leaves open (§8.9): a telemetry event carries a
license_ref (§5.2.3), and this resolves it to a signed, unrevoked grant and verifies it offline.
The grant is checked as of the event's own timestamp (its occurrence time) — not wall-clock, so a
grant revoked or expired before the reported use fails. (--now overrides; if an event carries no
timestamp the tool falls back to wall-clock and says so.)
cd examples/vectors # (continued from the walkthrough above)
# a reported use COVERED by a valid grant, checked at the event's occurrence time:
clverify license-ref spur-event.json --bundle .
# checked as of occurrence time 2026-08-01T00:00:00Z
# ⇒ the reported use of urn:asset:… was covered by a signed, unrevoked grant
# permitting `train`, verified OFFLINE.
# a reported use whose grant was REVOKED → the seam fails:
clverify license-ref spur-event-revoked.json --bundle .For a worked example against a real, live-issued sandbox grant (over LicenseFoundry's own founding
statement), including revoked-before-use and use-after-term cases, see
examples/spur-seam/ and its seam_test.py.
It also checks that the event's content_fingerprint.sha256 matches the credential's asset — i.e.
whose content (SPUR) + was it licensed (this). Where SPUR uses a different fingerprint scheme
(e.g. ISCC) than the credential's sha256, the tool says so plainly rather than faking a match — that
cross-scheme binding is a real, open interop question.
| Command | What it does |
|---|---|
verify CRED [--did-document F] [--status-list F] [--offline] [--accept-issuer DID] [--now ISO] [--json] |
Full check: EdDSA signature (via did:web), validity window, Bitstring Status List revocation. Exit 0 valid / 1 invalid. |
inspect CRED [--json] |
Decode and render the rights as a checklist. Does not verify. |
license-ref EVENT --bundle DIR [--now ISO] |
Resolve a SPUR event's license_ref and verify it offline, as of the event's own timestamp (occurrence time). --now overrides. |
--offline guarantees no network: you must supply --did-document and (if the credential has a
status entry) --status-list. The tool will refuse to pass a credential whose revocation it
cannot check, rather than silently skip it.
v0.1 is a working reference for the basic verify path: offline signature (EdDSA / VC-JOSE-COSE), validity window, did:web key resolution, and Bitstring Status List revocation, against any issuer. A solid, runnable foundation — not a claim that the hard problems are solved.
Open problems (the roadmap — contributions welcome):
- Privacy-preserving offline revocation at scale — the size / freshness / herd-privacy trade-off for status lists.
- Lossless per-right ↔ package (CoMP) transformation — bidirectional and provably lossless across non-isomorphic schemas.
- Tamper-evident content-addressed anchoring — a stable content identity + an economically viable anchoring path; and cross-scheme
content_fingerprintbinding (sha256 ↔ ISCC …). - A defensible fully-offline trust-freshness model —
did:webresolution + a bounded cache window + rotation, safe with no online trust path. - Forward-compatibility across still-moving standards (VC-JOSE-COSE, SPUR
license_ref, AIPREF) without lock-in.
v0.1 deliberately does the simple thing (a plain status-list lookup, caller-supplied keys) so the reference stays honest about where the frontier actually is.
Deliberately not here (they belong to a separate layer): the evidence-tier mapping and the
RFC 8785 assertion-binding — those are part of the forthcoming entitlement-profile module that sits
on top of this base verifier, not in the verifier itself. This repo is the seam-independent core
(credential, did:web issuer, status list, verifier); the profile adapts to whatever shape the
standard settles on.
In: offline verification (signature / validity / revocation), inspection, the SPUR license_ref
demo, public test vectors. Out: issuance, accounts, payment, production key management, any hosted
service, Verifiable Presentations. The fixtures are signed with sandbox keys — clearly marked, and
production verifiers must reject the sandbox issuer.
Reproducible via uv against the pinned uv.lock:
uv sync # install the exact locked toolchain
uv run --locked python generate_fixtures.py # fresh sandbox keys; same structure
uv run --locked pytest tests/test_acceptance.py
uv run --locked python tests/test_parity.py # Python == TypeScript, byte-identical verdicts--locked pins every dependency to uv.lock, so the run is reproducible — this is the exact
sequence CI runs. (No uv? PYTHONPATH=src python3 tests/test_acceptance.py still works stdlib-only.)
Apache-2.0. Contributions welcome; no CLA assigning exclusive rights. The test vectors are a conformance starter kit — point your own verifier at them.