Skip to content

ReadBridge/content-license-verify

Repository files navigation

clverify — offline verifier for content-licensing credentials

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.

Why this exists

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.

Install

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 PATH

TypeScript / Node (≥ 18):

cd ts && npm run build           # zero-dependency build; then:
node dist/cli.js verify …        # same commands & output as the Python CLI

See ts/README.md. Both implementations pass the same fixtures identically.

The 10-minute "does it actually work?" walkthrough

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.jwt

If (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.

The SPUR seam — license_ref → a verifiable entitlement

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.

Commands

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.

Status — what v0.1 does, and what's still open (honest)

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_fingerprint binding (sha256 ↔ ISCC …).
  • A defensible fully-offline trust-freshness modeldid:web resolution + 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.

Scope — deliberately minimal

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.

Run the tests (locked — the way CI does)

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.)

Licence

Apache-2.0. Contributions welcome; no CLA assigning exclusive rights. The test vectors are a conformance starter kit — point your own verifier at them.

About

Offline, vendor-neutral verifier for content-licensing Verifiable Credentials — did:web, VC-JOSE-COSE, Bitstring Status List revocation. Python + TypeScript, cross-runtime parity-tested.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages