Skip to content

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 the JsonWebSignature2020 proof type and the JsonWebKey2020 verification-method type (https://w3c-ccg.github.io/lds-jws2020/)
  • W3C VC Data Model 2.0 — defines @context, type, issuer, validFrom, validUntil, credentialSubject, proof shape (https://www.w3.org/TR/vc-data-model-2.0/)
  • W3C Data Integrity — defines the proof envelope structure and the URDNA2015 canonicalization step that JsonWebSignature2020 requires (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:

  1. Take the credential JSON-LD document without the proof block.
  2. Canonicalize it via URDNA2015 to a sorted N-Quads string.
  3. SHA-256 hash the N-Quads bytes.
  4. Use that hash as the detached JWS payload.
  5. 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:

  1. Take the credential object without the proof block.
  2. JSON-stringify it with sorted keys (a custom canonicalStringify helper, not URDNA2015).
  3. Use that string's UTF-8 bytes as an attached JWS payload (not hashed first).
  4. Sign base64url(header) + "." + base64url(payload) with WebCrypto's crypto.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 with JsonWebSignature2020)
  • Algorithm support: ES256, EdDSA (per WebCrypto algForJws selection)
  • DID document hosting: requires https://<domain>/.well-known/did.json per did:web spec ✅ disclosed
  • DID URL fragment: #key-1 ✅ matches the verification-method id in 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 proof block AND wraps them in a Verifiable Presentation with a holder proof block (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 BitstringStatusList in 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

  1. DID URL conventions: verificationMethod is a DID URL with a key-id fragment, exactly as the W3C DID Core spec requires. Many tutorials get this wrong.
  2. proofPurpose values: each demo uses the correct purpose for its context (assertionMethod for issuance, authentication for VP holder).
  3. @context ordering: VC contexts MUST start with https://www.w3.org/ns/credentials/v2 per VC Data Model 2.0; all four demos do this.
  4. Time-window fields: validFrom/validUntil (V2 names) are used, not the deprecated issuanceDate/expirationDate (V1 names).
  5. type arrays: VCs use type: ['VerifiableCredential', '<SubjectType>'] exactly as the spec requires.

What's intentionally NOT a deviation but might look like one

  1. No @context resolution / 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.
  2. No cryptosuite field: the newer Data Integrity v2 spec adds a cryptosuite identifier (e.g. eddsa-rdfc-2022). JsonWebSignature2020 is from the older LDS-JWS2020 work and doesn't use cryptosuite. 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: pyld for JSON-LD/URDNA2015 + python-jose or joserfc for JWS
  • Rust: ssi crate (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