Verifiable Data Issuance Protocol

Affinidi TDK - VDIP library facilitates secure, interoperable, verifiable credential issuance between Issuers and Holders using the DIDComm v2.1 protocol.

VDIP seamlessly integrates with Affinidi Messaging and other DIDComm-based mediators to implement DIDComm protocols for requesting and issuing verifiable credentials.

The VDIP provides the following main capabilities:

  • VdipIssuer - Represents the credential-issuing authority (Issuer) that creates and signs verifiable credentials, such as diplomas, certificates, licenses, or identity documents, attesting to the claim about the subject (Holder).

  • VdipHolder - Represents the user or entity requesting and claiming the credential offer issued by the issuer.

It simplifies the credential issuance and supports different credential formats such as W3C Data Model V1 and V2, JWT VC, and SD-JWT VC. It also implements holder-bound assertions to bind the credential to the holder cryptographically, ensuring only the intended recipient of the credential offer can claim it.

Key Features

  • Implements DIDComm v2.1 protocol for secure, end-to-end encrypted communication.
  • Support for feature discovery between issuers and holders.
  • Support multiple credential formats, such as W3C VC Data Model v1 & v2, JWT VC, and SD-JWT VC.
  • Holder-bound assertions to prove control of specific DIDs via signed JWT tokens.
  • Extensible credential metadata to pass auxiliary information during issuance.
  • Automatic security verification, such as message wrapping validation and addressing consistency checks.
  • Problem reporting mechanism for error handling.

DIDComm Protocols

VDIP utilises DIDComm protocols to define higher-level protocols, which facilitate the exchange of credential data from feature discovery, credential request, and processing the credential issuance.

Message TypePurposeDirection
discover-features/2.0/queriesQuery Supported FeaturesHolder → Issuer
discover-features/2.0/discloseDisclose Supported FeaturesIssuer → Holder
vdip/1.0/request-issuanceRequest Credential IssuanceHolder → Issuer
vdip/1.0/issued-credentialDeliver Issued CredentialIssuer → Holder
vdip/1.0/switch-contextRedirect to Browser ContextIssuer → Holder
report-problem/2.0/problem-reportReport Errors or WarningsAny → Any

For more details on the DIDComm protocols, see the VDIP protocol document.

Supported Credential Formats

FormatDescriptionUse Case
w3cV1W3C Verifiable Credentials Data Model 1.0 with JSON-LD and Data Integrity proofs.Standard JSON-LD credentials with linked data semantics.
w3cV2W3C Verifiable Credentials Data Model 2.0 with enhanced proof options.Next-generation W3C credentials with improved proof mechanisms.
jwtVcJWT-encoded Verifiable Credential (JWS).Compact JWT format for credentials, widely supported.
sdJwtVcSelective Disclosure JWT-based Verifiable Credential.Privacy-preserving credentials allowing selective claim disclosure.

Installation

Run the following command to add the package to your Dart/Flutter project:

dart pub add affinidi_tdk_vdip

Prerequisite

You must have a running and accessible DIDComm mediator instance before proceeding. The mediator provides the messaging layer for secure communication between Issuers and Holders.

If you don’t have a mediator yet, see deployment options.


Initialise Issuer and Holder

Set up DID Managers and Mediator

Both the issuer and holder need DID managers and a connection to a DIDComm mediator.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart'; import 'package:ssi/ssi.dart';
Example
// Resolve the mediator's DID document final mediatorDid = 'did:web:...'; // Your mediator's DID final mediatorDidDocument = await UniversalDIDResolver.defaultResolver.resolveDid( mediatorDid, ); // Set up issuer's DID manager final issuerKeyStore = InMemoryKeyStore(); final issuerWallet = PersistentWallet(issuerKeyStore); final issuerDidManager = DidKeyManager( wallet: issuerWallet, store: InMemoryDidStore(), ); final issuerKeyId = 'issuer-key-1'; await issuerWallet.generateKey( keyId: issuerKeyId, keyType: KeyType.p256, ); await issuerDidManager.addVerificationMethod(issuerKeyId); // Set up holder's DID manager final holderKeyStore = InMemoryKeyStore(); final holderWallet = PersistentWallet(holderKeyStore); final holderDidManager = DidKeyManager( wallet: holderWallet, store: InMemoryDidStore(), ); final holderKeyId = 'holder-key-1'; await holderWallet.generateKey( keyId: holderKeyId, keyType: KeyType.p256, ); await holderDidManager.addVerificationMethod(holderKeyId);

Initialise VDIP Issuer

Create a VDIP issuer for issuing credentials to holders.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
final vdipIssuer = await VdipIssuer.init( mediatorDidDocument: mediatorDidDocument, didManager: issuerDidManager, featureDisclosures: FeatureDiscoveryHelper.vdipIssuerDisclosures, );

Initialise VDIP Holder

Create a VDIP holder for requesting credentials from issuers.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
final vdipHolder = await VdipHolder.init( mediatorDidDocument: mediatorDidDocument, didManager: holderDidManager, featureDisclosures: FeatureDiscoveryHelper.vdipHolderDisclosures, );

Issuer Methods

Manage the issuer’s interactions with holders for credential issuance.

Disclose Features

Responds to feature queries from holders with supported credential formats and capabilities.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
// Issuer listens for feature queries and responds vdipIssuer.listenForIncomingMessages( onFeatureQuery: (message) async { await vdipIssuer.disclose(queryMessage: message); }, onRequestToIssueCredential: (/* ... */) { // Handle credential request }, );

Issue Credential

Issues a verifiable credential to a holder after validating the request and holder-bound assertions.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart'; import 'package:ssi/ssi.dart'; import 'package:uuid/uuid.dart';
Example
vdipIssuer.listenForIncomingMessages( onFeatureQuery: (message) async { await vdipIssuer.disclose(queryMessage: message); }, onRequestToIssueCredential: ({ required message, holderDidFromAssertion, isAssertionValid, }) async { // Parse the request final body = VdipRequestIssuanceMessageBody.fromJson(message.body!); // Validate assertion if present if (holderDidFromAssertion != null) { if (isAssertionValid != true) { // Send problem report for invalid assertion return; } } // Extract metadata final email = body.credentialMeta?.data?['email'] as String?; if (email == null) { throw ArgumentError('Email is required'); } // Determine credential subject DID final subjectDid = holderDidFromAssertion ?? message.from!; // Get issuer signer final issuerSigner = await issuerDidManager.getSigner( issuerDidManager.assertionMethod.first, ); // Construct unsigned credential (W3C v1 example) final unsignedVc = VcDataModelV1( context: [ dmV1ContextUrl, 'https://example.com/contexts/EmailCredentialV1.jsonld', ], credentialSchema: [ CredentialSchema( id: Uri.parse('https://example.com/schemas/EmailCredentialV1.json'), type: 'JsonSchemaValidator2018', ), ], id: Uri.parse('urn:uuid:${const Uuid().v4()}'), issuer: Issuer.uri(issuerSigner.did), type: {'VerifiableCredential', 'EmailVerificationCredential'}, issuanceDate: DateTime.now().toUtc(), credentialSubject: [ CredentialSubject.fromJson({ 'id': subjectDid, 'email': email, 'verified': true, }), ], ); // Sign the credential final suite = LdVcDm1Suite(); final issuedVc = await suite.issue( unsignedData: unsignedVc, proofGenerator: DataIntegrityEcdsaJcsGenerator(signer: issuerSigner), ); // Send the issued credential await vdipIssuer.sendIssuedCredentials( holderDid: message.from!, verifiableCredential: issuedVc, comment: 'Email verification credential issued successfully', ); }, onProblemReport: (message) { print('Problem reported: ${message.body}'); }, );

Send Switch Context

Sends a switch context message to redirect the holder to a specified URL as part of the issuer’s issuance workflow, such as a verification URL for third-party verification before credential issuance.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart'; import 'package:uuid/uuid.dart';
Example
vdipIssuer.listenForIncomingMessages( onRequestToIssueCredential: ({ required message, holderDidFromAssertion, isAssertionValid, }) async { final body = VdipRequestIssuanceMessageBody.fromJson(message.body!); final threadId = message.thid ?? message.id; // Your verification logic // For example, based on business flow - determine if browser-based verification is needed final requiresVerification = true; if (requiresVerification) { // Generate unique nonce for this verification session final contextNonce = const Uuid().v4(); // Send switch context message with issuer's base URL await vdipIssuer.sendSwitchContext( holderDid: message.from!, baseIssuerUrl: Uri.parse('https://verification.example.com'), nonce: contextNonce, threadId: threadId, ); // Store nonce and wait for verification callback // After verification completes, issue the credential } else { // Issue credential directly without context switching } }, );

Listen for Incoming Messages (Issuer)

Listens for incoming messages from holders, including feature queries, credential requests, and problem reports.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
vdipIssuer.listenForIncomingMessages( onFeatureQuery: (message) async { // Respond to feature discovery await vdipIssuer.disclose(queryMessage: message); }, onRequestToIssueCredential: ({ required message, holderDidFromAssertion, isAssertionValid, }) async { // Process credential request // Validate assertion if present // Issue credential }, onProblemReport: (message) { print('Problem reported: ${message.body}'); }, );

Holder Methods

Manage the holder’s interactions with issuers for requesting credentials.

Query Issuer Features

Queries the issuer to discover supported credential formats and features before requesting credentials.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
// Holder queries issuer's features await vdipHolder.queryIssuerFeatures( issuerDid: (await issuerDidManager.getDidDocument()).id, featureQueries: FeatureDiscoveryHelper.getFeatureQueriesByDisclosures( FeatureDiscoveryHelper.vdipIssuerDisclosures, ), );

Request Credential

Requests a verifiable credential from an issuer with specified format and metadata.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
// Request credential from issuer final requestMessage = await vdipHolder.requestCredential( issuerDid: issuerDid, options: RequestCredentialsOptions( proposalId: 'proposal-123', credentialFormat: CredentialFormat.w3cV1, credentialMeta: CredentialMeta( data: { 'email': 'holder@example.com', 'name': 'Alice Holder', }, ), comment: 'Requesting email verification credential', ), );

Request Credential (Holder-bound Assertion)

Requests a credential for a specific DID with holder-bound assertion proving control of that DID.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
// Get the signer for the holder DID final holderSigner = await holderDidManager.getSigner( holderDidManager.assertionMethod.first, ); // Request credential with holder-bound assertion final requestWithAssertion = await vdipHolder.requestCredentialForHolder( holderSigner.did, issuerDid: issuerDid, assertionSigner: holderSigner, options: RequestCredentialsOptions( proposalId: 'proposal-456', credentialFormat: CredentialFormat.jwtVc, jsonWebSignatureAlgorithm: JsonWebSignatureAlgorithm.es256, challenge: 'random-challenge-12345', credentialMeta: CredentialMeta( data: {'membershipLevel': 'premium'}, ), tokenExpiration: Duration(minutes: 5), ), );

Build Browser Context URL

Builds a browser context URL from a switch context message received from the issuer, allowing the holder to navigate to the specified URL in a browser for additional verification steps.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
vdipHolder.listenForIncomingMessages( onSwitchContextMessage: (message) async { // Build the browser verification URL from the switch context message final verificationUrl = await vdipHolder.buildBrowserContextUrl( switchContextMessage: message, ); print('Open this URL in your browser: $verificationUrl'); // In a real app, open this URL in the system browser or embedded web view // Example: launchUrl(Uri.parse(verificationUrl)); // After user completes verification in browser, // the issuer will send the credential via DIDComm }, onCredentialsIssuanceResponse: (message) { // Handle received credential }, );

Listen for Incoming Messages (Holder)

Listens for incoming messages from issuers, including feature disclosures, issued credentials, and problem reports.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
vdipHolder.listenForIncomingMessages( onDiscloseMessage: (message) async { print('Issuer supports: ${message.body}'); final body = DiscloseBody.fromJson(message.body!); // Check if issuer supports required features // Proceed with credential request if features are supported }, onCredentialsIssuanceResponse: (message) { // Parse the issued credential final body = VdipIssuedCredentialBody.fromJson(message.body!); print('Received credential in format: ${body.credentialFormat}'); print('Credential: ${body.credential}'); if (body.comment != null) { print('Issuer comment: ${body.comment}'); } // Store or process the credential // For W3C v1 credentials: if (body.credentialFormat == CredentialFormat.w3cV1) { final credential = VcDataModelV1.fromJson( jsonDecode(body.credential) as Map<String, dynamic>, ); // Verify the credential // Store in wallet // Present to verifier } }, onProblemReport: (message) { print('Problem reported: ${message.body}'); }, );

Complete Workflow Example

The following example provides a quick code overview of how a holder discovers the issuer’s supported features, requests credentials, and the issuer issues them.

Import
import 'package:affinidi_tdk_vdip/affinidi_tdk_vdip.dart';
Example
// Holder queries issuer's features await vdipHolder.queryIssuerFeatures( issuerDid: (await issuerDidManager.getDidDocument()).id, featureQueries: FeatureDiscoveryHelper.getFeatureQueriesByDisclosures( FeatureDiscoveryHelper.vdipIssuerDisclosures, ), ); // Holder listens for disclose messages vdipHolder.listenForIncomingMessages( onDiscloseMessage: (message) async { print('Issuer supports: ${message.body}'); final body = DiscloseBody.fromJson(message.body!); // Check if issuer supports required features // Proceed with credential request if features are supported }, onCredentialsIssuanceResponse: (message) { // Handle received credential final body = VdipIssuedCredentialBody.fromJson(message.body!); print('Received credential: ${body.credential}'); }, ); // Issuer listens for feature queries and responds vdipIssuer.listenForIncomingMessages( onFeatureQuery: (message) async { await vdipIssuer.disclose(queryMessage: message); }, onRequestToIssueCredential: ({ required message, holderDidFromAssertion, isAssertionValid, }) async { // Process and issue credential final body = VdipRequestIssuanceMessageBody.fromJson(message.body!); // Validate and issue credential // (See Issue Credential section for complete example) }, ); // Holder requests credential await vdipHolder.requestCredential( issuerDid: issuerDid, options: RequestCredentialsOptions( proposalId: 'proposal-123', credentialFormat: CredentialFormat.w3cV1, credentialMeta: CredentialMeta( data: { 'email': 'holder@example.com', 'name': 'Alice Holder', }, ), comment: 'Requesting email verification credential', ), );

For a complete runnable example demonstrating the full workflow, see the example on GitHub that shows:

  • Feature discovery between holder and issuer.
  • Credential request workflows with and without holder-bound assertions.
  • Credential issuance in multiple formats (W3C v1, JWT VC, SD-JWT VC).
  • Security verification and error handling.
  • Problem report messaging.