Credentials

A Dart SDK for exchanging Relationship cards (R-Cards), Verifiable Relationship Credentials (VRCs), and optionally proving human liveness without sharing biometric data via Human Zero Knowledge Proof (Human ZKP). Built on DIDComm v2.1 and W3C Verifiable Credentials.

The Credentials SDK is built on top of meeting_place_core and provides the credential layer for Meeting Place applications.

It supports:

  • Relationship Cards and Verifiable Relationship Credentials (R-Cards and VRCs): Verifiable Credentials for exchanging contact details (name, email, phone, company, and custom fields) as jCard (RFC 7095), exportable to vCard 3.0 (RFC 6350), and for recording mutual relationships between DIDs through a two-step VDIP handshake. See R-Card and VRC Feature. No AWS account needed.
  • Human ZKP: Prove you are a real human to a contact without sharing biometric data. See Human Zero Knowledge Proof Feature. Requires an external liveness provider such as AWS Rekognition Face Liveness or Azure Face Liveness Detection for production; the Flutter Reference App includes a synthetic demo that works without one.

Install Dependency

Package: meeting_place_credentials

dart pub add meeting_place_credentials

Check the latest version on the pub.dev registry or view the source on GitHub.

R-Card and VRC Feature

Relationship Card (R-Card)

An R-Card is a signed W3C Verifiable Credential containing a jCard (RFC 7095) payload. Think of it as a digital business card with verifiable contact and relationship details: name, email, phone, company, and optional custom fields that can be cryptographically verified. R-Cards are exchanged automatically when a DIDComm channel is established and can be resent or shared manually at any time.

Verifiable Relationship Credential (VRC)

A VRC demonstrates that two DIDs have established a verified relationship. It uses a two-step VDIP handshake: the initiating side requests, and the responding side sends a relationship credential back. Both participants receive a signed copy of the final credential.

Human Zero Knowledge Proof Feature

The Human Zero Knowledge Proof (Human ZKP) feature lets one participant prove to another that they are a real human, without sharing personal or biometric data. The proof travels over the secure DIDComm channel and the recipient can validate it without seeing any personal data.

This SDK handles Stage 2. Stage 1 (the liveness check) is handled by an external provider such as AWS Rekognition. Stage 3 (proof generation) is performed by the consuming application using utilities provided by this SDK.

How Human ZKP works

Liveness check

The user performs real-time actions (such as blinking or turning their head) in front of their device camera. The liveness provider analyses the session and scores whether a live person is present.

Owner: External provider (for example AWS Rekognition) · Output: LivenessEvidence

Issue liveness credential

LivenessVcIssuanceService.issue(evidence) signs a W3C Verifiable Credential from the normalised evidence.

Owner: This SDK · Output: Signed LivenessCredential (W3C VC)

Generate proof

The consuming application uses the SDK’s LivenessCredentialSubject and ZKP attachment builders to generate a Groth16 proof from the liveness credential.

Owner: Consuming application (SDK utilities provided) · Output: Groth16 ZKP

Transmit proof

The ZKP is sent over the DIDComm channel with no personal data.

Owner: meeting_place_core transport · Output: ZKP message on channel

Validate proof

The contact’s device validates the proof cryptographically.

Owner: Verifier (contact’s device) · Output: Verified badge shown in UI

Why this is privacy-preserving: The Groth16 proof in Stage 3 is mathematically derived from the liveness credential, but it does not expose the credential contents. The validator learns only that the holder passed a liveness check.

Try it in action: The Flutter Reference App includes a working end-to-end Human ZKP demo. Enable it with ZKP_ENABLED=true and follow the end-to-end demo flow to see all five stages execute on a real device.

For implementation details and setup guides, refer to:

Classes and Methods

MeetingPlaceCredentialsSDK

The MeetingPlaceCredentialsSDK is the main facade for R-Card and VRC exchange flows. It wires protocol handlers, stream managers, and repositories on top of MeetingPlaceCoreSDK and exposes a single injectable service for credential operations.

MeetingPlaceCredentialsSDK (constructor)

Creates an instance of MeetingPlaceCredentialsSDK.

Parameters

coreSDK MeetingPlaceCoreSDK Required
An initialised MeetingPlaceCoreSDK instance providing DIDComm transport and VDIP primitives.
rCardRepository RCardRepository Required
Repository used to persist every incoming R-Card. Construct one with RCardRepositoryDrift from meeting_place_drift_repository.
vrcRepository VrcRepository Required
Repository used to persist every received VRC. Construct one with VrcRepositoryDrift from meeting_place_drift_repository.
logger MeetingPlaceCoreSDKLogger? Optional
Optional logger for diagnostic output. Defaults to DefaultMeetingPlaceCoreSDKLogger.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

final credentialsSDK = MeetingPlaceCredentialsSDK(
  coreSDK: coreSDK,
  rCardRepository: RCardRepositoryDrift(database: rCardDb),
  vrcRepository: VrcRepositoryDrift(database: vrcDb),
);

Streams

receivedRCards

A broadcast Stream<RCard> that emits a verified R-Card whenever one arrives over any channel: either via the DIDComm attachment path (OOB / channel inauguration) or the VDIP issued-credential path (chat-time update). Every emitted card is also automatically persisted via RCardRepository.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

credentialsSDK.receivedRCards.listen((RCard rCard) {
  print('R-Card received from ${rCard.subjectDid}');
});

receivedRCardsOnChannel

A broadcast Stream<ChannelRCardEvent> that emits an R-Card and its originating channel for every card received via the connection establishment path (OOB / channel inauguration). VDIP-path R-Cards are not emitted here; use receivedRCards for those.

ChannelRCardEvent.channel exposes permanentChannelDid and otherPartyPermanentChannelDid to correlate the card to its conversation.

Example

credentialsSDK.receivedRCardsOnChannel.listen((ChannelRCardEvent event) {
  print('R-Card from channel: ${event.channel.permanentChannelDid}');
  print('Contact DID: ${event.rCard.subjectDid}');
});

receivedVrcRequests

A broadcast Stream<VrcRequest> that emits an incoming VDIP issuance request for every VRC exchange initiated by a contact.

Example

credentialsSDK.receivedVrcRequests.listen((VrcRequest request) {
  print('VRC request from ${request.senderDid}');
});

receivedVrcs

A broadcast Stream<VrcIssuance> that emits a signature-verified VRC for each finished credential received over VDIP.

Example

credentialsSDK.receivedVrcs.listen((VrcIssuance issuance) {
  print('VRC received from ${issuance.senderDid}');
});

R-Card Operations

watchReceivedRCards

Returns a live Stream<List<RCard>> of all persisted R-Cards, ordered by receivedAt descending. Backed by RCardRepository.watchAll, it emits a new list whenever any record is added, updated, or removed from local storage.

Parameters

No parameters required.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

credentialsSDK.watchReceivedRCards().listen((List<RCard> cards) {
  print('Contact count: ${cards.length}');
});

listReceivedRCards

Returns a snapshot Future<List<RCard>> of all persisted R-Cards, ordered by receivedAt descending.

Parameters

No parameters required.

Example

final cards = await credentialsSDK.listReceivedRCards();

getReceivedRCardBySubjectDid

Returns the persisted R-Card whose sender DID matches subjectDid, or null if no such record exists.

Parameters

subjectDid String Required
The DID of the contact whose R-Card should be retrieved.

Example

final rCard = await credentialsSDK.getReceivedRCardBySubjectDid('did:example:alice');

updateReceivedRCardNotes

Updates the notes field on the R-Card identified by subjectDid. Pass null to clear the notes. Does nothing if no record with that DID exists.

Parameters

subjectDid String Required
The DID of the contact whose notes field should be updated.
notes String? Required
The new notes text, or null to clear existing notes.

Example

await credentialsSDK.updateReceivedRCardNotes('did:example:alice', 'Met at conference 2025');

// Clear the notes
await credentialsSDK.updateReceivedRCardNotes('did:example:alice', null);

deleteReceivedRCard

Removes the persisted R-Card identified by subjectDid.

Parameters

subjectDid String Required
The DID of the contact whose R-Card should be deleted.

Example

await credentialsSDK.deleteReceivedRCard('did:example:alice');

sendRCard

Builds, signs, and delivers an R-Card to the other party in channel via VDIP. Returns the sent RCard so callers can display or persist the issued card.

Parameters

channel Channel Required
The established DIDComm channel to the contact.
subjectDid String Required
The DID of the credential subject (the card recipient).
card RCardSubject Required
The contact fields to embed in the R-Card VC (name, email, phone, company, and so on).
issuerDidManager DidManager Required
The DidManager used to sign the credential with ecdsa-jcs-2019 Data Integrity proof.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

final sent = await credentialsSDK.sendRCard(
  channel: myChannel,
  subjectDid: 'did:example:bob',
  card: RCardSubject(
    firstName: 'Alice',
    lastName: 'Smith',
    email: 'alice@example.com',
    company: 'Acme Corp',
  ),
  issuerDidManager: myDidManager,
);

parseRCard

Parses and signature-verifies a raw R-Card VC blob. Returns null if the blob is not a valid, verified R-Card.

Parameters

vcBlob String Required
The raw serialised VC JSON string to parse.

Example

final rCard = await credentialsSDK.parseRCard(vcBlob: rawVcJson);
if (rCard != null) {
  print('Valid R-Card from ${rCard.subjectDid}');
}

consumePendingRCard

Returns and removes the last RCard from senderDid that arrived while no stream listener was attached. Returns null if no pending card exists.

Parameters

senderDid String Required
The DID of the sender whose pending R-Card should be consumed.

Example

final pending = credentialsSDK.consumePendingRCard('did:example:alice');

VRC Operations

requestVrcExchange

Initiates a VRC exchange by sending a VDIP issuance request to the contact identified by channelDid.

Parameters

channelDid String Required
The permanent channel DID of the contact to whom the request is sent.
identityDid String Required
The DID of the local identity to include in the request metadata.
identityName String Required
The display name of the local identity to include in the request metadata.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

await credentialsSDK.requestVrcExchange(
  channelDid: myChannelDid,
  identityDid: myDid,
  identityName: 'Alice',
);

handleReceivedVrcRequest

Handles the credential-protocol outcome of receiving a VRC request. Returns a VrcRequestProcessingResult indicating whether a reciprocal credential was sent or the exchange was deferred.

Parameters

permanentChannelDid String Required
The permanent channel DID on which the request arrived.
request VrcRequest Required
The received VrcRequest object from the receivedVrcRequests stream.
hasVrcExchangeInitiated bool Required
Whether the local side has already initiated a VRC exchange on this channel.
isConnectionInitiator bool Required
Whether the local side initiated the original DIDComm connection.
issuerDid String? Optional
The local issuer DID to embed in the reciprocal VRC. Required when the handler decides to send a credential.
issuerName String? Optional
The display name to embed in the reciprocal VRC.

Example

credentialsSDK.receivedVrcRequests.listen((VrcRequest request) async {
  final result = await credentialsSDK.handleReceivedVrcRequest(
    permanentChannelDid: myChannelDid,
    request: request,
    hasVrcExchangeInitiated: false,
    isConnectionInitiator: true,
    issuerDid: myDid,
    issuerName: 'Alice',
  );
  print('VRC request handled: $result');
});

handleReceivedVrc

Handles the credential-protocol outcome of receiving a finished VRC. Returns VrcProcessingResultIgnored when the exchange is already completed, so no pre-guard is needed.

Parameters

permanentChannelDid String Required
The permanent channel DID on which the VRC arrived.
vcBlob String Required
The raw serialised VC JSON from the VrcIssuance.vcBlob field.
exchangeState VrcExchangeState Required
The current VRC exchange state for this channel.
issuerDid String? Optional
The local issuer DID, required when the handler decides to send a reciprocal credential.
issuerName String? Optional
The display name to embed in the reciprocal VRC.

Example

credentialsSDK.receivedVrcs.listen((VrcIssuance issuance) async {
  final result = await credentialsSDK.handleReceivedVrc(
    permanentChannelDid: myChannelDid,
    vcBlob: issuance.vcBlob,
    exchangeState: currentExchangeState,
    issuerDid: myDid,
    issuerName: 'Alice',
  );
  print('VRC processed: $result');
});

watchVrcs

Returns a live Stream<List<Vrc>> of all persisted VRCs. Emits a new list whenever any record is added or removed from local storage.

Parameters

No parameters required.

Example

credentialsSDK.watchVrcs().listen((List<Vrc> vrcs) {
  print('VRC count: ${vrcs.length}');
});

listVrcs

Returns a snapshot Future<List<Vrc>> of all persisted VRCs.

Parameters

No parameters required.

Example

final vrcs = await credentialsSDK.listVrcs();

getVrcById

Returns the persisted VRC identified by id, or null if no such record exists.

Parameters

id String Required
The unique credential identifier (from the VC id field).

Example

final vrc = await credentialsSDK.getVrcById('<VRC-ID>');

listVrcsByHolderDid

Returns all persisted VRCs where the holder DID matches holderDid.

Parameters

holderDid String Required
The DID of the credential holder to filter by.

Example

final vrcs = await credentialsSDK.listVrcsByHolderDid('did:example:alice');

countVrcsByHolderDid

Returns the number of persisted VRCs where the holder DID matches holderDid.

Parameters

holderDid String Required
The DID of the credential holder to count by.

Example

final count = await credentialsSDK.countVrcsByHolderDid('did:example:alice');
print('Verified relationships: $count');

deleteVrc

Removes the persisted VRC identified by id.

Parameters

id String Required
The unique credential identifier of the VRC to delete.

Example

await credentialsSDK.deleteVrc('<VRC-ID>');

storeVrc

Parses, verifies, and persists a VRC from a raw VC blob. Throws MeetingPlaceCredentialsSDKException with error code vrcInvalidCredential if the blob cannot be parsed.

Parameters

vcBlob String Required
The raw serialised VC JSON string to parse and store.
referenceId String Required
An app-defined identifier used to correlate this VRC with its exchange context (for example, a channel DID or proposal ID).
verifiedAt DateTime? Optional
Optional timestamp recording when the credential was cryptographically verified.
receivedAt DateTime? Optional
Optional timestamp recording when the credential was received. Defaults to the current UTC time if omitted.
credentialFormat String? Optional
Optional serialisation format identifier from the VDIP issuance message.

Example

final vrc = await credentialsSDK.storeVrc(
  vcBlob: issuance.vcBlob,
  referenceId: myChannelDid,
  receivedAt: DateTime.now().toUtc(),
);
print('Stored VRC: ${vrc.id}');

consumePendingVrcRequest

Returns and removes the last VrcRequest from senderDid that arrived while no stream listener was attached. Returns null if no pending request exists.

Parameters

senderDid String Required
The DID of the sender whose pending VRC request should be consumed.

Example

final pending = credentialsSDK.consumePendingVrcRequest('did:example:alice');

consumePendingVrc

Returns and removes the last VrcIssuance from senderDid that arrived while no stream listener was attached. Returns null if no pending issuance exists.

Parameters

senderDid String Required
The DID of the sender whose pending VRC issuance should be consumed.

Example

final pending = credentialsSDK.consumePendingVrc('did:example:alice');

Lifecycle

closeCredentialStreams

Cancels all internal stream subscriptions, closes the broadcast stream controller, and releases VDIP resources. Call this when the SDK instance is no longer needed to prevent memory leaks.

Parameters

No parameters required.

Example

await credentialsSDK.closeCredentialStreams();

RCardSubject

Parsed contact fields from an R-Card Verifiable Credential. All fields are optional because a contact card may be partially filled.

RCardSubject (constructor)

Creates an RCardSubject with the given optional contact fields.

Parameters

id String? Optional
Optional DID of the credential subject.
firstName String? Optional
Given name.
lastName String? Optional
Family name.
email String? Optional
Email address.
phone String? Optional
Phone number.
company String? Optional
Organisation name.
position String? Optional
Job title.
website String? Optional
Personal or professional website URL.
social String? Optional
Social profile URL or handle.
profilePic String? Optional
Profile picture URL or base64-encoded image data.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

final subject = RCardSubject(
  firstName: 'Alice',
  lastName: 'Smith',
  email: 'alice@example.com',
  phone: '+1-555-0100',
  company: 'Acme Corp',
  position: 'Engineer',
);

toVCard

An extension method on RCardSubject that serialises the contact fields to a vCard 3.0 string following RFC 6350.

Parameters

notes String? Optional
Optional text appended as the vCard NOTE field.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

final subject = RCardSubject(firstName: 'Alice', email: 'alice@example.com');
final vCard = subject.toVCard(notes: 'Met at conference 2025');
print(vCard);
// BEGIN:VCARD
// VERSION:3.0
// ...
// END:VCARD

LivenessEvidenceSource

An abstract interface that collects LivenessEvidence from a configured liveness provider. Implement this in your application or in a provider-specific package.

getEvidence

Collects raw liveness evidence from the configured provider for the given holder.

Parameters

holderDid String Required
The DID of the user whose liveness is being checked.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

class AwsRekognitionEvidenceSource implements LivenessEvidenceSource {
  @override
  Future<LivenessEvidence> getEvidence({required String holderDid}) async {
    // Call AWS Rekognition and map the response to LivenessEvidence
    return LivenessEvidence(
      providerId: 'aws_rekognition',
      providerTransactionId: '<SESSION-ID>',
      livenessScore: 0.98,
      livenessThreshold: 0.90,
      checkedAt: DateTime.now().toUtc(),
    );
  }
}

LivenessVcIssuanceService

Issues signed W3C liveness credentials from normalised provider evidence. This is Stage 2 of the Human ZKP pipeline.

LivenessVcIssuanceService (constructor)

Creates a stateless liveness credential issuance service. Takes no arguments.

Parameters

No parameters required.


issue

Issues a signed VcDataModelV2 liveness credential for the given holder.

Parameters

issuerDid String Required
The DID of the credential issuer.
holderDid String Required
The DID of the credential holder (the user who passed the liveness check).
issuerDidManager DidManager Required
The DidManager used to sign the credential.
evidence LivenessEvidence Required
The normalised liveness evidence from the provider.
validFor Duration Optional
How long the credential remains valid. Defaults to 5 days.

Example

import 'package:meeting_place_credentials/meeting_place_credentials.dart';

const issuanceService = LivenessVcIssuanceService();

final evidence = await evidenceSource.getEvidence(holderDid: myDid);

final livenessVc = await issuanceService.issue(
  issuerDid: myIssuerDid,
  holderDid: myDid,
  issuerDidManager: myDidManager,
  evidence: evidence,
  validFor: const Duration(days: 1),
);