@trustvc/w3c-vc
v2.4.2
Published
This repository provides utilities for signing, verifying, and deriving Verifiable Credentials (VCs) using modern cryptographic signature schemes including ECDSA-SD-2023 and BBS-2023, with selective disclosure capabilities. These functions ensure the auth
Readme
TrustVC W3C VC
This repository provides utilities for signing, verifying, and deriving Verifiable Credentials (VCs) using modern cryptographic signature schemes including ECDSA-SD-2023 and BBS-2023, with selective disclosure capabilities. These functions ensure the authenticity and integrity of VCs within a W3C-compliant ecosystem.
Table of Contents
- Installation
- Features
- Supported Cryptosuites
- Usage
- Migration from Legacy BbsBlsSignature2020
- Best Practices
- License
Installation
To install the package, use:
npm install @trustvc/w3c-vcFeatures
- Modern Cryptosuites: ECDSA-SD-2023 (default) and BBS-2023 with selective disclosure
- W3C Compliance: Support for both W3C VC Data Model v1.1 and v2.0
- DID Method Agnostic: Sign and verify with
did:webissuers (hosted DID document) ordid:keyissuers (self-certifying, no hosting). See@trustvc/w3c-issuerfor issuance. - Selective Disclosure: Derive credentials with selective field revelation
- Verifiable Presentations: Create (with strict input validation + mandatory expiry), sign (optional holder binding via
ecdsa-rdfc-2019, with or withoutchallenge/domain,did:keyordid:webholder) and verify presentations that may wrap credentials of mixed cryptosuites and versions - Legacy Support: Deprecated BbsBlsSignature2020 signature support (verification only)
- Schema Validation: Checks if payload matches W3C VC schema
- Version Detection: Helper functions to detect credential versions
Supported Cryptosuites
| Cryptosuite | Status | Signing | Verification | Derivation | Notes |
|-------------|--------|---------|--------------|------------|-------|
| ecdsa-sd-2023 | ✅ Active (Default) | ✅ | ✅ | ✅ | Modern, fast, selective disclosure |
| bbs-2023 | ✅ Active | ✅ | ✅ | ✅ | Modern BBS with selective disclosure |
| BbsBlsSignature2020 | ⚠️ Deprecated | ❌ | ✅ | ❌ | Legacy support only |
Usage
A note on issuer DIDs: the examples below use
did:webissuers, but the samesignCredential/verifyCredential/deriveCredentialcalls work unchanged fordid:keyissuers — just replace the keyPair'sid/controllerand the credential'sissuerwith thedid:key:zXxx#zXxx/did:key:zXxxform. The default document loader from@trustvc/w3c-contextresolvesdid:keyURIs in-memory (no network call), so verification works out of the box. See@trustvc/w3c-issuer's did:key guide for the full flow.
1. Signing a Credential
The signCredential function allows you to sign a Verifiable Credential using modern cryptographic signature schemes. Default cryptosuite is ecdsa-sd-2023.
ECDSA-SD-2023 Signing (Default)
import { signCredential } from '@trustvc/w3c-vc';
/**
* Parameters:
* - credential (RawVerifiableCredential): The credential to be signed.
* - keyPair (PrivateKeyPair): The key pair options for signing.
* - cryptoSuite (CryptoSuiteName, optional): The cryptosuite to be used. Defaults to "ecdsa-sd-2023".
* - options (object, optional): Optional parameters including documentLoader and mandatoryPointers.
*
* Returns:
* - A Promise that resolves to:
* - signedCredential.signed (SignedVerifiableCredential): The signed credential.
* - signedCredential.error (string): The error message in case of failure.
*/
const ecdsaKeyPair = {
"id": "did:web:trustvc.github.io:did:1#multikey-1",
"type": "Multikey",
"controller": "did:web:trustvc.github.io:did:1",
"secretKeyMultibase": "<secretKeyMultibase>",
"publicKeyMultibase": "<publicKeyMultibase>"
};
const credential = {
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://w3id.org/security/data-integrity/v2",
"https://w3id.org/citizenship/v2"
],
"credentialSubject": {
"id": "did:example:b34ca6cd37bbf23",
"type": ["Person"],
"name": "TrustVC"
},
"validFrom": "2024-04-01T12:19:52Z",
"validUntil": "2029-12-03T12:19:52Z",
"issuer": "did:web:trustvc.github.io:did:1",
"type": ["VerifiableCredential"]
};
// Sign with default ECDSA-SD-2023 cryptosuite
const signedCredential = await signCredential(credential, ecdsaKeyPair);
// Or explicitly specify cryptosuite with mandatory pointers
const signedCredentialWithOptions = await signCredential(credential, ecdsaKeyPair, 'ecdsa-sd-2023', {
mandatoryPointers: ['/credentialSubject/name']
});
if (signedCredential.signed) {
console.log('Signed Credential:', signedCredential.signed);
} else {
console.error('Error:', signedCredential.error);
}BBS-2023 Signing
import { signCredential } from '@trustvc/w3c-vc';
const bbsKeyPair = {
"id": "did:web:trustvc.github.io:did:1#multikey-2",
"type": "Multikey",
"controller": "did:web:trustvc.github.io:did:1",
"secretKeyMultibase": "<secretKeyMultibase>",
"publicKeyMultibase": "<publicKeyMultibase>"
};
const credential = {
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://w3id.org/security/data-integrity/v2",
"https://w3id.org/citizenship/v2"
],
"credentialSubject": {
"id": "did:example:b34ca6cd37bbf23",
"type": ["Person"],
"name": "TrustVC"
},
"validFrom": "2024-04-01T12:19:52Z",
"validUntil": "2029-12-03T12:19:52Z",
"issuer": "did:web:trustvc.github.io:did:1",
"type": ["VerifiableCredential"]
};
// Sign with BBS-2023 cryptosuite
const signedCredential = await signCredential(credential, bbsKeyPair, 'bbs-2023', {
mandatoryPointers: ['/credentialSubject/name']
});
if (signedCredential.signed) {
console.log('BBS-2023 Signed Credential:', signedCredential.signed);
} else {
console.error('Error:', signedCredential.error);
}Legacy BbsBlsSignature2020 Signing (Deprecated)
import { signCredential } from '@trustvc/w3c-vc';
// ⚠️ DEPRECATED: BbsBlsSignature2020 signing is no longer supported
const legacyResult = await signCredential(credential, keyPair, 'BbsBlsSignature2020');
// This will return an error:
// { error: "BbsBlsSignature2020 is no longer supported. Please use the latest cryptosuite versions instead." }2. Verifying a Credential
The verifyCredential function allows you to verify signed Verifiable Credentials.
Modern Cryptosuite Verification (ECDSA-SD-2023 / BBS-2023)
Important: For modern cryptosuites (ECDSA-SD-2023, BBS-2023), base credentials must be derived before verification. You cannot directly verify the original signed credential.
import { verifyCredential, deriveCredential } from '@trustvc/w3c-vc';
/**
* Parameters:
* - credential (SignedVerifiableCredential): The credential to be verified.
* - options (object, optional): Optional parameters including documentLoader.
*
* Returns:
* - A Promise that resolves to:
* - verificationResult.verified (boolean): Whether the verification was successful.
* - verificationResult.error (string): The error message if the verification failed.
*/
const signedCredential = {
'@context': [
'https://www.w3.org/ns/credentials/v2',
'https://w3id.org/security/data-integrity/v2',
"https://w3id.org/citizenship/v2"
],
credentialSubject: {
id: 'did:example:b34ca6cd37bbf23',
type: ['Person'],
name: 'TrustVC'
},
validFrom: '2024-04-01T12:19:52Z',
validUntil: '2029-12-03T12:19:52Z',
issuer: 'did:web:trustvc.github.io:did:1',
type: ['VerifiableCredential'],
proof: {
type: 'DataIntegrityProof',
cryptosuite: 'ecdsa-sd-2023',
created: '2024-10-02T09:04:07Z',
proofPurpose: 'assertionMethod',
proofValue: 'z...',
verificationMethod: 'did:web:trustvc.github.io:did:1#multikey-1'
}
};
// ❌ This will fail - base credentials cannot be verified directly
const baseVerification = await verifyCredential(signedCredential);
// Returns: { verified: false, error: "ecdsa-sd-2023 base credentials must be derived before verification. Use deriveCredential() first." }
// ✅ Correct approach: Derive first, then verify
const derivedResult = await deriveCredential(signedCredential, ['/credentialSubject/name']);
if (derivedResult.derived) {
const verificationResult = await verifyCredential(derivedResult.derived);
if (verificationResult.verified) {
console.log('Credential verified successfully.');
} else {
console.error('Verification failed:', verificationResult.error);
}
}Legacy BbsBlsSignature2020 Verification
import { verifyCredential } from '@trustvc/w3c-vc';
// Legacy BbsBlsSignature2020 credentials can be verified directly (both base and derived credentials)
const bbsCredential = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://w3id.org/security/bbs/v1',
'https://w3id.org/citizenship/v1'
],
credentialSubject: {
id: 'did:example:b34ca6cd37bbf23',
type: ['Person'],
name: 'TrustVC'
},
issuanceDate: '2024-04-01T12:19:52Z',
expirationDate: '2029-12-03T12:19:52Z',
issuer: 'did:web:trustvc.github.io:did:1',
type: [ 'VerifiableCredential' ],
proof: {
type: 'BbsBlsSignature2020',
created: '2024-10-02T09:04:07Z',
proofPurpose: 'assertionMethod',
proofValue: 'A...',
verificationMethod: 'did:web:trustvc.github.io:did:1#keys-1'
}
};
const verificationResult = await verifyCredential(bbsCredential);
if (verificationResult.verified) {
console.log('Legacy BbsBlsSignature2020 credential verified successfully.');
} else {
console.error('Verification failed:', verificationResult.error);
}3. Deriving a Credential (Selective Disclosure)
The deriveCredential function allows you to create selective disclosure from signed credentials.
ECDSA-SD-2023 Selective Disclosure
import { deriveCredential } from '@trustvc/w3c-vc';
/**
* Parameters:
* - credential (SignedVerifiableCredential): The verifiable credential to be selectively disclosed.
* - revealedAttributes (string[]): Array of JSON pointers specifying which fields to reveal.
* - options (object, optional): Optional parameters including documentLoader.
*
* Returns:
* - A Promise that resolves to:
* - derived (SignedVerifiableCredential): The selectively disclosed credential.
* - error (string): The error message in case of failure.
*/
const signedCredential = {
'@context': [
'https://www.w3.org/ns/credentials/v2',
'https://w3id.org/security/data-integrity/v2',
"https://w3id.org/citizenship/v2"
],
credentialSubject: {
id: 'did:example:b34ca6cd37bbf23',
type: ['Person'],
name: 'TrustVC'
},
validFrom: '2024-04-01T12:19:52Z',
validUntil: '2029-12-03T12:19:52Z',
issuer: 'did:web:trustvc.github.io:did:1',
type: ['VerifiableCredential'],
proof: {
type: 'DataIntegrityProof',
cryptosuite: 'ecdsa-sd-2023',
// ... proof details
}
};
// Reveal only specific fields (mandatory pointers are automatically included)
const selectivePointers = ['/credentialSubject/name'];
const derivedResult = await deriveCredential(signedCredential, selectivePointers);
if (derivedResult.derived) {
console.log('Derived ECDSA-SD-2023 Credential:', derivedResult.derived);
} else {
console.error('Error:', derivedResult.error);
}
// ❌ Multiple derivation rounds are not supported
const secondDerivation = await deriveCredential(derivedResult.derived, selectivePointers);
// Returns: { error: "ecdsa-sd-2023 derived credentials cannot be further derived. Multiple rounds of derivation are not supported by this cryptosuite." }BBS-2023 Selective Disclosure
import { deriveCredential } from '@trustvc/w3c-vc';
const bbsSignedCredential = {
'@context': [
'https://www.w3.org/ns/credentials/v2',
'https://w3id.org/security/data-integrity/v2',
"https://w3id.org/citizenship/v2"
],
credentialSubject: {
id: 'did:example:b34ca6cd37bbf23',
type: ['Person'],
name: 'TrustVC'
},
validFrom: '2024-04-01T12:19:52Z',
validUntil: '2029-12-03T12:19:52Z',
issuer: 'did:web:trustvc.github.io:did:1',
type: ['VerifiableCredential'],
proof: {
type: 'DataIntegrityProof',
cryptosuite: 'bbs-2023',
// ... proof details
}
};
// Reveal only specific fields (mandatory pointers are automatically included)
const selectivePointers = ['/credentialSubject/name'];
const derivedResult = await deriveCredential(bbsSignedCredential, selectivePointers);
if (derivedResult.derived) {
console.log('Derived BBS-2023 Credential:', derivedResult.derived);
} else {
console.error('Error:', derivedResult.error);
}Legacy BbsBlsSignature2020 Derivation (Deprecated)
import { deriveCredential } from '@trustvc/w3c-vc';
// ⚠️ DEPRECATED: BbsBlsSignature2020 derivation is no longer supported
const legacyDerivation = await deriveCredential(credential, revealedAttributes);
// This will return an error:
// { error: "BbsBlsSignature2020 is no longer supported for derivation. Please use the latest cryptosuite versions instead." }4. Schema Validation
Validate if a document meets W3C VC schema requirements:
import {
isRawDocument,
isSignedDocument,
isRawDocumentV1_1,
isRawDocumentV2_0,
isSignedDocumentV1_1,
isSignedDocumentV2_0,
isDerived
} from '@trustvc/w3c-vc';
/**
* Available validation functions:
* - isRawDocument(document): Checks if document is a valid raw credential
* - isSignedDocument(document): Checks if document is a valid signed credential
* - isRawDocumentV1_1(document): Checks if document is a valid v1.1 raw credential
* - isRawDocumentV2_0(document): Checks if document is a valid v2.0 raw credential
* - isSignedDocumentV1_1(document): Checks if document is a valid v1.1 signed credential
* - isSignedDocumentV2_0(document): Checks if document is a valid v2.0 signed credential
* - isDerived(document): Checks if document is a derived (selective disclosure) credential
*/
const document = {
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://w3id.org/security/data-integrity/v2",
"https://w3id.org/citizenship/v2"
],
"type": ["VerifiableCredential"],
"issuer": "did:web:example.com",
"validFrom": "2024-01-01T00:00:00Z",
"credentialSubject": {
"id": "did:example:123",
"type": ["Person"],
"name": "John Doe"
}
};
// Basic validation
console.log('Is raw document:', isRawDocument(document)); // true
console.log('Is signed document:', isSignedDocument(document)); // false (no proof)
// Version-specific validation
console.log('Is v1.1 raw document:', isRawDocumentV1_1(document)); // false
console.log('Is v2.0 raw document:', isRawDocumentV2_0(document)); // true
// Check if credential is derived (selective disclosure)
const signedDocument = { ...document, proof: { /* proof object */ } };
console.log('Is derived credential:', await isDerived(signedDocument));5. Verifiable Presentations
A Verifiable Presentation (VP) is an envelope that a holder wraps around one or more Verifiable Credentials to present them to a verifier. It has two independent layers:
- The embedded credentials (
verifiableCredential) — each keeps its own proof. A single VP may freely mix cryptosuites (ecdsa-sd-2023,bbs-2023,BbsBlsSignature2020) and VC data-model versions (v1.1 / v2.0). Each is verified independently. - An optional holder-binding proof over the whole envelope, with a
challenge(and optionaldomain) supplied by the verifier to prevent replay. This proves the holder controls the key and made this presentation for this verifier.
Which cryptosuite signs the presentation?
The holder proof must sign the entire envelope, so it uses a plain
(non-selective-disclosure) suite: ecdsa-rdfc-2019. This reuses the same ECDSA
(P-256) Multikey used for ecdsa-sd-2023 credentials — the holder's signing key is
independent of the suites used by the credentials inside. The holder DID may be a
did:key (self-certifying, nothing to host) or a did:web — both resolve out of
the box.
The selective-disclosure suites (
ecdsa-sd-2023,bbs-2023) cannot sign a presentation:verifiableCredentialis a JSON-LD@graphcontainer that JSON-pointer selective disclosure cannot cover, so the proof would not bind the credentials being presented. The deprecatedBbsBlsSignature2020is likewise not used.signPresentationreturns a descriptive error if you request any of them. This only affects the outer proof — the credentials inside the VP may still use any suite.A BBS-only holder (no ECDSA key) cannot produce a modern holder proof (there is no non-deprecated plain BBS suite) — either generate an ECDSA key or present an unsigned VP (see below).
| Layer | Cryptosuite |
|-------|-------------|
| VP holder proof (outer) | ecdsa-rdfc-2019 (ECDSA P-256 Multikey, did:key or did:web) |
| Embedded credentials | any (ecdsa-sd-2023, bbs-2023, BbsBlsSignature2020), any version |
Transferable records cannot be presented via a VP
Credentials with a TransferableRecords credentialStatus are controlled on-chain
via their token registry — possession is proven by token ownership, not by a
presentation. createPresentation (and signPresentation) therefore throw / error if
any embedded credential carries a TransferableRecords status. Present those through their
token ownership instead.
Strict validation at creation + mandatory expiry
createPresentation is async and validates every input credential before producing
a VP. If any credential is not signed, expired, future-dated, revoked, or a
transferable record, it throws with a clear reason (and index) and no VP is
produced. The VP is also stamped with a mandatory, configurable expiry
(validFrom = now, validUntil = now + lifetime; default 5 minutes).
The presentation envelope defaults to VC Data Model v2.0 regardless of the embedded
credentials' versions (each credential keeps its own @context); pass version: 'v1' only
if a verifier specifically requires a v1.1 presentation.
const presentation = await createPresentation(signedCredentials, {
holder: 'did:web:holder.example',
expiresInSeconds: 300, // VP lifetime (default 300s); or pass an explicit validUntil
// version: 'v2', // envelope version (default 'v2'); wraps v1.1 or v2.0 credentials
// fullDisclosure: true, // auto-derive any base SD credential (reveals ALL fields)
});
// throws e.g. "credential at index 0 is not valid: Invalid signature."
// "credential at index 1 has expired (2024-01-01T…)."
// "credential at index 0 has been revoked (credentialStatus)."Three signing routes
| Route | Call | Proof | Notes |
|-------|------|-------|-------|
| 1. Unsigned | createPresentation(...) → verifyPresentation(vp) | none | verifies credential authenticity only |
| 2. Signed, no challenge | signPresentation(vp, key) | assertionMethod | proves holder signed it; not anti-replay |
| 3. Signed, with challenge | signPresentation(vp, key, { challenge, domain }) | authentication | challenge/domain enforced on verify (anti-replay) |
import { createPresentation, signPresentation, verifyPresentation } from '@trustvc/w3c-vc';
// `signedCredentials` are already signed (and, for SD suites, derived) — mixed suites/versions ok.
const vp = await createPresentation(signedCredentials, { holder: 'did:web:holder.example' });
// Route 3 — signed with a verifier-issued challenge (strongest; needs a server to issue/track it)
const challenge = 'a-random-verifier-supplied-nonce';
const { signed, error } = await signPresentation(vp, holderEcdsaKey, {
challenge,
domain: 'verifier.example.com', // optional audience binding
});
if (error) throw new Error(error);
const result = await verifyPresentation(signed, {
challenge, // REQUIRED for an authentication proof; never read from the proof
domain: 'verifier.example.com',
requireProof: true, // reject an unsigned VP
checkHolderBinding: true, // signer DID == holder == every credentialSubject.id
maxLifetimeSeconds: 300, // reject a VP whose lifetime exceeds this (defeats a huge validUntil)
});
console.log(result.verified); // true only if the holder proof + all credentials verify + not expired
console.log(result.presentationResult); // outcome of the holder proof (undefined if unsigned)
console.log(result.credentialResults); // per-credential outcomes (with credentialIndex)
challenge/domainare optional (per the W3C Data Integrity spec). Provide achallenge(Route 3) for single-use anti-replay — this needs a server to issue a fresh nonce and track/consume it. Omit it (Route 2) for a signed-but-replayable proof.verifyPresentationrequires you to pass thechallengeit issued — the value in the proof is never trusted (that would allow replay).Stateless alternative for frontends. If you have no backend to track challenges, rely on the VP expiry instead: a short-lived signed VP +
maxLifetimeSecondson verify gives time-boxed replay mitigation with no server state.
Unsigned presentations
If you only need to bundle credentials (no cryptographic holder binding), omit the
signing step. verifyPresentation still verifies every embedded credential (and the VP expiry):
const presentation = await createPresentation(signedCredentials);
const result = await verifyPresentation(presentation);
// result.verified reflects whether all embedded credentials verify and the VP is not expired;
// result.presentationResult is undefined (no holder proof present).Note: an unsigned VP does not prove who is presenting it — anyone holding a copy of the credentials could send it. Sign it (Route 2/3) when you need to prove the holder controls the subject's key.
Presentation helper functions
import { isRawPresentation, isSignedPresentation } from '@trustvc/w3c-vc';
isRawPresentation(presentation); // true for a structurally valid unsigned VP
isSignedPresentation(signed); // true for a structurally valid VP carrying a proofMigration from Legacy BbsBlsSignature2020
If you're migrating from legacy BbsBlsSignature2020 signatures:
- Update Key Pairs: Use Multikey format instead of Bls12381G2Key2020
- Change Cryptosuite: Use 'ecdsa-sd-2023' or 'bbs-2023' instead of 'BbsBlsSignature2020'
- Update Contexts: Use W3C VC Data Model v2.0 contexts
- Modify Derivation: Use JSON pointer arrays instead of ContextDocument format
- Update Verification Flow: Derive credentials before verification for modern cryptosuites
// Legacy BbsBlsSignature2020 (deprecated)
const legacyKeyPair = {
type: "Bls12381G2Key2020",
privateKeyBase58: "...",
publicKeyBase58: "..."
};
// Modern approach
const modernKeyPair = {
type: "Multikey",
secretKeyMultibase: "...",
publicKeyMultibase: "..."
};Best Practices
- Use Modern Cryptosuites: Prefer 'ecdsa-sd-2023' (default) or 'bbs-2023' over legacy BbsBlsSignature2020
- Implement Proper Error Handling: Always check for errors in function results
- Derive Before Verify: For modern cryptosuites, always derive credentials before verification
- Use Mandatory Pointers: Specify fields that should always be revealed in selective disclosure
- Version Detection: Use helper functions to detect credential versions when needed
- Single Derivation: Remember that modern cryptosuites support only single-round derivation
License
This project is licensed under the Apache 2.0 License.
