Skip to main content
Version: v3

Migration Guide - OpenAttestation to TrustVC

OpenCerts v3 uses TrustVC, a comprehensive wrapper library that supports both W3C Verifiable Credentials (VC) and OpenAttestation Verifiable Documents. This guide covers the key changes and steps required to migrate to issuing W3C VCs.

Overview

OpenCerts v2 (OA)OpenCerts v3 (TrustVC)
StandardOpenAttestation v2W3C VC Data Model v2.0
Document FormatOA Wrapped DocumentW3C VC with Data Integrity Proofs
IdentityDNS-TXT record + Document Storedid:web hosted at /.well-known/did.json
SigningDocument wrapping (merkle tree)ecdsa-sd-2023 or bbs-2023 cryptosuites
IssuanceDocument Store (Smart Contract)Direct DID-based signing
RevocationDocument Store revoke method (on-chain)Bitstring Status List (off-chain, hosted)

Key Changes

1. Document Format

OpenCerts v2 used the OpenAttestation wrapped document format. OpenCerts v3 adopts the W3C Verifiable Credentials Data Model v2.0 via TrustVC.

v2 (OA format):

{
"issuers": [
{
"name": "Example University",
"documentStore": "0x1234...",
"identityProof": {
"type": "DNS-TXT",
"location": "example.edu"
}
}
],
"$template": {
"name": "EXAMPLE_CERT",
"type": "EMBEDDED_RENDERER",
"url": "https://renderer.example.edu"
}
}

v3 (W3C VC format):

{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://w3id.org/security/data-integrity/v2"
],
"type": ["VerifiableCredential"],
"issuer": "did:web:example.edu",
"validFrom": "2024-01-01T00:00:00Z",
"validUntil": "2029-12-31T23:59:59Z",
"credentialStatus": {
"id": "https://example.edu/credentials/statuslist/1#1",
"type": "BitstringStatusListEntry",
"statusPurpose": "revocation",
"statusListIndex": "1",
"statusListCredential": "https://example.edu/credentials/statuslist/1"
},
"credentialSubject": {
"name": "John Doe",
"course": "Bachelor of Science"
}
}

Note: W3C VC documents do not require wrapping. They are signed directly using signW3C.

2. Identity

  • v2: Used DNS-TXT records pointing to a Document Store smart contract address.
  • v3: Issuers are identified using did:web Decentralized Identifiers with Multikey verification methods. See Setting Up did:web Identity for details.

3. Signing

  • v2: Documents were wrapped using OA CLI, then issued by calling the issue method on a Document Store smart contract.
  • v3: Documents are signed directly using signW3C with ecdsa-sd-2023 (default) or bbs-2023 cryptosuites. No on-chain transaction is required for issuance.

4. Verification

  • v2: Used @opencerts/verify built on @govtechsg/oa-verify for fragment-based verification.
  • v3: Uses verifyDocument from @trustvc/trustvc, which auto-detects the document format and handles both OA and W3C VC verification.

5. Revocation

Migration Steps

Step 1: Update Dependencies

# Remove OA dependencies
npm uninstall @govtechsg/open-attestation @opencerts/verify @govtechsg/oa-verify

# Install TrustVC
npm install @trustvc/trustvc

Step 2: Update Document Schema

Convert your raw document templates from the OA format to the W3C VC format.

Key mapping:

  • issuersissuer (a string DID or object with id, type, name)
  • issuers[].documentStore + identityProof → issuer DID (did:web:...) — see Setting Up did:web Identity
  • $template → renderer configuration (check your renderer setup)
  • Certificate data fields → credentialSubject
  • To support revocation, add a credentialStatus field — see Setting Up Credential Revocation

Step 3: Sign with TrustVC

Use signW3C to sign credentials. The key pair used here comes from your did:web setup — see Setting Up did:web Identity for how to generate these keys.

import { signW3C, VerificationType } from '@trustvc/trustvc';

const signedCredential = await signW3C(rawDocument, {
'@context': 'https://w3id.org/security/multikey/v1',
id: 'did:web:example.edu#multikey-1',
type: VerificationType.Multikey,
controller: 'did:web:example.edu',
publicKeyMultibase: '<publicKeyMultibase>',
secretKeyMultibase: '<secretKeyMultibase>',
});

Step 4: Update Verification

Use verifyDocument to verify credentials:

import { verifyDocument } from '@trustvc/trustvc';

const resultFragments = await verifyDocument(signedDocument);

verifyDocument automatically handles both OA and W3C VC documents. For ecdsa-sd-2023 and bbs-2023 signed documents, derivation is handled internally.

Step 5: Update Renderer

Your decentralised renderer must be updated to handle the W3C VC document structure. See the Decentralised Renderer W3C VC Update guide for details.

Setting Up did:web Identity

OpenCerts v3 uses did:web to identify issuers. You will need to generate a DID document and host it on your domain.

Generate a DID Document

import { issuer } from '@trustvc/trustvc';

const { issueDID, CryptoSuite } = issuer;

const issuedDidWeb = await issueDID({
domain: 'example.com',
type: CryptoSuite.EcdsaSd2023,
});

// The DID document to host publicly
console.log(JSON.stringify(issuedDidWeb.wellKnownDid, null, 2));

// Store these securely — they are needed for signing
console.log('Key pairs:', issuedDidWeb.didKeyPairs);

Important: Securely store the generated key pairs (didKeyPairs). They are required for signing credentials.

Host the DID Document

Place the generated DID document at /.well-known/did.json on your web server:

  • Serve the file over HTTPS with a valid SSL certificate.
  • Set the content type to application/json.
  • Configure appropriate CORS headers (Access-Control-Allow-Origin: *) — without this, web-based verifiers cannot resolve your DID.

For example, a DID of did:web:example.com resolves to https://example.com/.well-known/did.json.

You can also host path-based DIDs for different departments or environments:

DIDDocument Location
did:web:example.comhttps://example.com/.well-known/did.json
did:web:example.com:department-ahttps://example.com/department-a/did.json

Setting Up Credential Revocation

OpenCerts v3 uses the Bitstring Status List method for credential revocation. This allows you to revoke or suspend issued credentials without modifying the original credential.

How It Works

A Bitstring Status List is a sequence of bits where each bit corresponds to a credential:

  • 0 — credential is valid (active)
  • 1 — credential is revoked or suspended

The status list is hosted as a signed Verifiable Credential at a publicly accessible URL. Verifiers fetch this list and check the bit at the credential's index.

Create a Status List

import {
createCredentialStatusPayload,
signW3C,
StatusList,
VCBitstringCredentialSubject,
} from '@trustvc/trustvc';

// URL where the status list will be hosted
const hostingUrl = 'https://example.com/credentials/status/1';

// Initialize a new status list
const statusList = new StatusList({ length: 131072 });

// Encode the status list
const encodedList = await statusList.encode();

// Create the credential status payload
const credentialSubject: VCBitstringCredentialSubject = {
id: `${hostingUrl}#list`,
type: 'BitstringStatusList',
statusPurpose: 'revocation',
encodedList,
};

const keyPair = {
'@context': 'https://w3id.org/security/multikey/v1',
id: 'did:web:example.com#keys-1',
type: 'Multikey',
controller: 'did:web:example.com',
secretKeyMultibase: '<secretKeyMultibase>',
publicKeyMultibase: '<publicKeyMultibase>',
};

const credentialStatusVC = await createCredentialStatusPayload(
{ id: hostingUrl, credentialSubject },
keyPair,
'BitstringStatusListCredential',
'ecdsa-sd-2023'
);

const { signed, error } = await signW3C(credentialStatusVC, keyPair);
if (error) throw new Error(error);

Reference the Status List in Your Credential

Include a credentialStatus field in your raw credential before signing:

"credentialStatus": {
"id": "https://example.com/credentials/status/1#94567",
"type": "BitstringStatusListEntry",
"statusPurpose": "revocation",
"statusListIndex": "94567",
"statusListCredential": "https://example.com/credentials/status/1"
}

Revoke a Credential

To revoke a credential, fetch the existing status list, flip the bit at the credential's index, and re-sign:

import {
createCredentialStatusPayload,
fetchCredentialStatusVC,
signW3C,
StatusList,
VCBitstringCredentialSubject,
} from '@trustvc/trustvc';

// Fetch the existing status list
const credentialStatusVC = await fetchCredentialStatusVC(
'https://example.com/credentials/status/1'
);

// Decode, update, and re-encode
const statusList = await StatusList.decode({
encodedList: credentialStatusVC.credentialSubject.encodedList,
});
statusList.setStatus(94567, true); // true = revoked
const encodedList = await statusList.encode();

// Re-sign and re-host the updated status list
const credentialSubject: VCBitstringCredentialSubject = {
id: 'https://example.com/credentials/status/1#list',
type: 'BitstringStatusList',
statusPurpose: 'revocation',
encodedList,
};

const updatedVC = await createCredentialStatusPayload(
{ id: 'https://example.com/credentials/status/1', credentialSubject },
keyPair,
'BitstringStatusListCredential',
'ecdsa-sd-2023'
);

const { signed, error } = await signW3C(updatedVC, keyPair);
if (error) throw new Error(error);
// Host the updated signed VC at the same URL

Host the Status List

Host the signed status list VC at the URL specified in statusListCredential (e.g., https://example.com/credentials/status/1). Ensure:

  • The endpoint is publicly accessible over HTTPS.
  • CORS is enabled (Access-Control-Allow-Origin: *) — without this, web-based verifiers cannot check credential status.
  • The content type is application/json.

Backward Compatibility

verifyDocument supports both OA documents and W3C VC documents. Existing v1 and v2 certificates will continue to verify correctly without any changes.

Additional Resources