Verifiable-Credentials Demo Spec-Conformance Audit¶
Generated: 2026-04-24 (s56-audit)
This page audits the four browser-side Verifiable Credentials demos against the W3C specs they reference. The purpose is not to ship production-conformant signing — these are educational scaffolds — but to make every deviation explicit so a learner reading the demo and the spec side-by-side knows exactly where they diverge.
Scope of audit¶
| Demo | Path | Issues VCs? | Honest-disclosure block? |
|---|---|---|---|
| did:web Verifiable Credentials Demo | tools/did-web-vc-demo.html | Yes | Yes (line 282) |
| did:key DID Method Demo | tools/did-key-demo.html | Yes | Yes (line 264) |
| Skill Portfolio (W3C VP) | tools/skill-portfolio.html | Yes (self-issued) | Yes (in honest-disclosure section of the page) |
| StatusList2021 Revocation Demo | tools/status-list-2021-demo.html | No (emits unsigned StatusListCredential) | Yes (line 252) |
Spec references audited¶
The demos that emit signed credentials all use a proof block with type: "JsonWebSignature2020". This audit compares against:
- W3C CCG
lds-jws2020— defines theJsonWebSignature2020proof type and theJsonWebKey2020verification-method type (https://w3c-ccg.github.io/lds-jws2020/) - W3C VC Data Model 2.0 — defines
@context,type,issuer,validFrom,validUntil,credentialSubject,proofshape (https://www.w3.org/TR/vc-data-model-2.0/) - W3C Data Integrity — defines the proof envelope structure and the URDNA2015 canonicalization step that
JsonWebSignature2020requires (https://www.w3.org/TR/vc-data-integrity/) - RDF Dataset Canonicalization (URDNA2015) — the canonicalization algorithm proof signing must use (
https://www.w3.org/TR/rdf-canon/)
Per-field conformance: emitted proof block¶
Each demo emits this shape:
{
"type": "JsonWebSignature2020",
"created": "<ISO 8601 timestamp>",
"verificationMethod": "<DID URL with key fragment>",
"proofPurpose": "assertionMethod",
"jws": "<compact JWS string>"
}
| Field | Spec requirement | Demo implementation | Conformant? |
|---|---|---|---|
type | Must be a registered proof type from the proof type registry | JsonWebSignature2020 | ✅ Type label is registered. But: the bytes signed are NOT what a JsonWebSignature2020 verifier would compute (see "Signature payload" below), so a conformant verifier will reject with a signature mismatch. |
created | xsd:dateTime; ISO 8601 | new Date().toISOString() | ✅ |
verificationMethod | URI dereferencing to a key in a DID Document | state.verificationMethodId (DID URL with #key-1 fragment) | ✅ Shape correct for did:web and did:key. |
proofPurpose | One of assertionMethod, authentication, keyAgreement, capabilityInvocation, capabilityDelegation | assertionMethod for credential signing; authentication for VP holder proof in skill-portfolio | ✅ |
jws | Detached JWS with payload omitted ("") per RFC 7797 §4 | Compact JWS with embedded base64url payload (not detached) | ❌ Deviation: spec wants detached form; demo emits attached form |
Signature payload: where the spec divergence lives¶
The JsonWebSignature2020 spec computes the JWS signing input as:
- Take the credential JSON-LD document without the
proofblock. - Canonicalize it via URDNA2015 to a sorted N-Quads string.
- SHA-256 hash the N-Quads bytes.
- Use that hash as the detached JWS payload.
- Sign
base64url(header) + "." + base64url(hash)with the issuer's private key, then strip the payload to produce a detached JWS like<header>..<sig>.
The Nexus demos compute the JWS signing input as:
- Take the credential object without the
proofblock. - JSON-stringify it with sorted keys (a custom
canonicalStringifyhelper, not URDNA2015). - Use that string's UTF-8 bytes as an attached JWS payload (not hashed first).
- Sign
base64url(header) + "." + base64url(payload)with WebCrypto'scrypto.subtle.sign.
| Step | Spec | Demo |
|---|---|---|
Strip proof block before canonicalizing | ✅ | ✅ (verify code does const { proof, ...unsigned } = vc;) |
| Canonicalization algorithm | URDNA2015 → N-Quads | Sorted-key JSON stringify |
| Hash of canonical form before signing | SHA-256 → fixed-length payload | None — payload is the canonical-JSON bytes themselves |
| JWS form | Detached (RFC 7797) | Attached (standard compact JWS) |
| Verification side | Re-canonicalize + re-hash + verify against detached payload | Re-canonicalize + base64url-compare to embedded payload + verify signature |
Net effect: the Nexus demos produce a self-consistent JWS — sign and verify use the same canonicalStringify helper, so the demos verify within themselves. They will not verify against an external JsonWebSignature2020 verifier (Veramo, MATTR, sphereon, etc.), which expects the URDNA2015 + detached-JWS pipeline.
This is the deviation each demo's honest-disclosure block already calls out. The audit confirms the disclosure is accurate.
Per-demo specifics¶
did-web-vc-demo.html¶
- Verification method type:
JsonWebKey2020✅ (correct pairing withJsonWebSignature2020) - Algorithm support: ES256, EdDSA (per WebCrypto
algForJwsselection) - DID document hosting: requires
https://<domain>/.well-known/did.jsonper did:web spec ✅ disclosed - DID URL fragment:
#key-1✅ matches the verification-methodidin the DID document
did-key-demo.html¶
- DID format:
did:key:z<multibase-encoded-key>per did:key spec ✅ - Multicodec varint encoding for key types: ed25519 (
0xed01), P-256 (0x1200), RSA (0x1205) ✅ inspected - Key derivation: keypair → multicodec-prefixed bytes → multibase base58btc encode → DID identifier ✅
- The verification method is the DID itself (no separate DID document fetch needed) ✅ disclosed
skill-portfolio.html¶
- Issues VCs with
proofblock AND wraps them in a Verifiable Presentation with a holderproofblock (line 506 + 579) proofPurpose: 'assertionMethod'for credentials,'authentication'for the VP holder proof ✅ correct per VC spec- VP shape:
@context,type: ['VerifiablePresentation'],holder,verifiableCredential[],proof✅ correct per VC Data Model 2.0 - Note: VP signing has the same URDNA2015 deviation as the credential signing — same scaffold pattern
status-list-2021-demo.html¶
- Implements the StatusList2021 spec (now superseded by
BitstringStatusListin VC Data Model 2.0, but still widely deployed) - Encoding: 16K-bit status list → gzip → base64 → embedded in
credentialSubject.encodedList✅ - Status check: decode encodedList → look up bit at credential's
statusListIndex→ 1 = revoked ✅ - Intentionally emits the StatusListCredential UNSIGNED (line 252) — disclosed as a teaching simplification. A production status list MUST be signed by the issuer (with the same JsonWebSignature2020 + URDNA2015 caveats above).
What the demos get right that the disclosures don't emphasize¶
- DID URL conventions:
verificationMethodis a DID URL with a key-id fragment, exactly as the W3C DID Core spec requires. Many tutorials get this wrong. proofPurposevalues: each demo uses the correct purpose for its context (assertionMethodfor issuance,authenticationfor VP holder).@contextordering: VC contexts MUST start withhttps://www.w3.org/ns/credentials/v2per VC Data Model 2.0; all four demos do this.- Time-window fields:
validFrom/validUntil(V2 names) are used, not the deprecatedissuanceDate/expirationDate(V1 names). typearrays: VCs usetype: ['VerifiableCredential', '<SubjectType>']exactly as the spec requires.
What's intentionally NOT a deviation but might look like one¶
- No
@contextresolution / external HTTP fetch: the demos use the raw context URI without fetching it. Production verifiers fetch context documents to expand JSON-LD; demos rely on the fact that the V2 context terms are well-known. This is a perf shortcut, not a spec deviation — context fetching is for canonicalization, which the demos don't do anyway. - No
cryptosuitefield: the newer Data Integrity v2 spec adds acryptosuiteidentifier (e.g.eddsa-rdfc-2022).JsonWebSignature2020is from the older LDS-JWS2020 work and doesn't usecryptosuite. The demos correctly omit it.
To make any demo production-conformant¶
Replace the demo's signJws/verifyJws pair with a library that implements URDNA2015 + detached JWS. Recommended in 2026:
- JavaScript (browser):
@digitalbazaar/data-integrity+@digitalbazaar/jws-2020-cryptosuite-suite— both maintained by the editor of the JsonWebSignature2020 spec - Python:
pyldfor JSON-LD/URDNA2015 +python-joseorjoserfcfor JWS - Rust:
ssicrate (Spruce ID) implements VC + Data Integrity end-to-end
The demos' signJws/verifyJws would become a thin shim that delegates to one of those libraries. The rest of the demo code (DID handling, key generation, UI) is reusable.
Verification debt status (s56-audit)¶
Closed by this audit:
- ✅ "VC proof-type vs Data Integrity URDNA2015 spec" item — every deviation is now explicitly documented per-field with spec citations. The disclosures in each demo were accurate; this audit confirms they are accurate AND comprehensive.
Still open (cannot close without external work):
- Validating an actual production-conformant VC against the demo's verification path (would prove that the demo's verifier rejects conformant credentials, which is expected behavior but not asserted by tests). Recipe: take a sample VC from the W3C VC test suite, paste it into the verify tab, observe the mismatch. Documented for a future maintainer.
Cross-references¶
brain-l1-activation-playbook.md— Brain Level 1 activation procedurewcag-remediation-log.md— accessibility remediation across toolsy-js-poc-deployment.md— Y.js POC deployment runbook (also uses cryptographic primitives, but for CRDT replication, not VCs)detection-coverage.md— adversarial detection-vs-scenario coverage reportcross-system-link-report.md— cross-system content linking matrix