Skip to main content

Creating a Verifiable Presentation

Overview

A presentation is created by signW3CPresentation in the library, or by trustvc vp-sign on the command line:

npm install @trustvc/trustvc # library
npm install -g @trustvc/trustvc-cli # CLI

Either way it bundles one or more signed Verifiable Credentials into an envelope and signs it with the holder's key, applying the holder-binding rule first, so a presentation it produces is always one the holder is entitled to make.

Three things have to hold — they are the checks that fail in practice:

  1. Every credential's credentialSubject.id is the holder's DID.
  2. The holder's key pair is ECDSA (P-256); a BBS key cannot sign a presentation.
  3. Every credential is currently valid — not expired, not revoked, and its issuer resolvable.

Signing

import { readFileSync } from 'node:fs';
import { signW3CPresentation } from '@trustvc/trustvc';

// The holder's DID key pair — `controller` is the holder DID, and the file holds
// the private key, so keep it out of source control.
const holderKeyPair = JSON.parse(readFileSync('./didKeyPairs.json', 'utf8'));
const signedCredential = JSON.parse(readFileSync('./credentials/bill_of_lading.json', 'utf8'));

const { signed, error } = await signW3CPresentation(
signedCredential, // one credential, or an array of them
holderKeyPair,
{
holder: holderKeyPair.controller, // must equal every credentialSubject.id
expiresInSeconds: 600, // or: validUntil: <ISO 8601, must be in the future>
},
);

if (error) throw new Error(error);
console.log(signed);

The result is the presentation, ready to send:

{
"@context": ["https://www.w3.org/ns/credentials/v2", "..."],
"type": ["VerifiablePresentation"],
"holder": "did:key:zDnaeSSj4pMHnBjMEQHKmT2hVFNGxAujN3JXWnyDaEwrNKvxc",
"validFrom": "<the moment of signing>",
"validUntil": "<600 seconds later>",
"verifiableCredential": [{ "...": "the credentials, unchanged" }],
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "ecdsa-rdfc-2019",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:key:zDnaeSSj4pMHnBjMEQHKmT2hVFNGxAujN3JXWnyDaEwrNKvxc#zDnaeSSj…",
"proofValue": "z4oey5q2M3XKaxup3tmz…"
}
}

Presenting several credentials

Pass an array. Each one is checked and bound to the holder independently:

const { signed, error } = await signW3CPresentation(
[billOfLading, certificateOfOrigin],
holderKeyPair,
{ holder: holderKeyPair.controller, expiresInSeconds: 600 },
);

Errors name the offending credential by index, so you can tell which of several failed:

credential at index 1 is about "did:key:zDnaerUv…", which does not match the holder "did:key:zDnaeSSj…"

Setting the validity window

A presentation must have an expiry — signW3CPresentation refuses without one, because an open-ended presentation would be replayable forever. Choose either form:

OptionMeaning
expiresInSecondsRelative to now. The CLI default is 600 (10 minutes).
validUntilAn absolute ISO 8601 timestamp. Must be in the future.

Keep it short. A presentation is made for one exchange, so its window should cover that exchange and no more. The credentials inside are unaffected — they keep their own, much longer validity.

validFrom is stamped, not chosen

The opening edge of the window is set for you — it is the moment of signing. Every presentation gets one, you cannot omit it, and you cannot remove it afterwards: it sits inside the signed payload, so deleting it invalidates the holder's proof.

You are only ever asked how the window closes. So a presentation's validFrom records when it was signed rather than scheduling when it becomes usable, and a "not yet valid" presentation is not a state an honest document reaches.

note

A credential's validFrom is different — it is chosen by its issuer and can legitimately sit in the future. Such a credential cannot be presented until it becomes valid.

Common signing failures

Signing is refused rather than producing a presentation that could not be verified, and nothing is written when it fails. Every refusal names the credential responsible by its index in the list you passed, so with several credentials you can tell which one is at fault — and the CLI names the file it came from as well.

The refusals fall into two groups.

The credential cannot be bound to this holder

What went wrongFix
The credential is about somebody else — its credentialSubject.id is not the holder.Present a credential whose subject is the holder, or sign with the key pair of the credential's actual subject.
The credential has no credentialSubject.id at all, so there is nothing to bind.Ask the issuer to reissue it with a subject id. It cannot be added afterwards.
The declared holder is not the signing key's DID.Set holder to the DID of the key you are signing with — through the CLI this is automatic.

The credential is not currently presentable

What went wrongFix
It has expired.Ask the issuer to reissue. Re-presenting cannot help — only the issuer can extend a credential's life.
It is not yet valid — its validFrom is in the future.Wait until it becomes valid, or ask the issuer.
It has been revoked or suspended on its status list.Ask the issuer. A revoked credential can never be presented.
It is a transferable record — its credentialStatus is TransferableRecords.Present it through its token ownership instead. Ownership lives on-chain, not in a presentation.
It is unsigned — a raw credential with no proof.Sign the credential first.
It was edited after signing, so its own signature no longer verifies.Re-sign from the original source data. Never edit a signed credential.
Its issuer's DID cannot be resolved, so its signature cannot be checked.Publish or restore the issuer's DID document.

Two failures are about the presentation rather than a credential:

What went wrongFix
The holder key is not ECDSA (P-256) — a BBS key, or unreadable key material.Use an ECDSA holder key. The credentials inside may still be BBS; only the holder's key is constrained.
No expiry was given, or validUntil is not after validFrom.Set expiresInSeconds or a future validUntil.

Sample credentials for testing

Credentials have to be issued to your holder DID before they can be presented, so if you do not have a suitable set yet — or want fixed inputs to test an integration against — the CLI ships a generator:

git clone https://github.com/TrustVC/trustvc-cli
cd trustvc-cli && npm install
node tests/fixtures/vp/generate.cjs

It writes a holder key and credentials already bound to it, plus one presentation per outcome, laid out so a file's expected result is readable from its path: credentials/presentable/ and credentials/rejected/, presentations/valid/ and presentations/invalid/. That folder's README.md records the exact message each file produces.

Next steps