affinidi_tdk_vault_iota 1.0.0-dev copy "affinidi_tdk_vault_iota: ^1.0.0-dev" to clipboard
affinidi_tdk_vault_iota: ^1.0.0-dev copied to clipboard

The Affinidi TDK - Vault Iota package provides support for OID4VP share flow, parsing Iota Framework request URIs and submitting Verifiable Presentations.

Affinidi TDK - Vault Iota #

The Affinidi TDK - Vault Iota package provides libraries to implement the OID4VP (OpenID for Verifiable Presentations) sharing flow using the Affinidi Iota Framework. It handles the end-to-end presentation exchange, from verifier request ingestion to Verifiable Presentation submission, matching requested credentials against those stored in the user's Vault.

Key Features #

  • Parse and validate Iota OID4VP request URIs.
  • Classify the verifier's request and extract the requested credentials and purpose metadata.
  • Match the requested credentials against credentials stored in the user's Vault.
  • Build and submit a signed Verifiable Presentation in response to the verifier's request.
  • Manage consent records and support automatic consent for trusted verifiers.
  • Storage-agnostic, with no dependency on a specific credential or consent record store.

Note: For the WebSocket data-sharing flow, we recommend authenticating the user before starting the flow, so responses are tied to a known holder.

Requirements #

  • Dart SDK version ^3.8.0

Installation #

Run:

dart pub add affinidi_tdk_vault_iota

or manually add the package to your pubspec.yaml file:

dependencies:
  affinidi_tdk_vault_iota: ^<version_number>

and then run the command below to install the package:

dart pub get

Usage #

After successfully installing the package, import it into your code.

import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';

Step 1 — Parse the OID4VP request URI #

final service = ShareFlowService(cryptography: myCryptographyService);

final shareRequest = await service.validateOid4vpRequest(
  Uri.parse('openid4vp://authorize?request=<jwt>'),
  walletDid: 'did:key:z6Mk...', // optional — validates the `aud` claim
);

// shareRequest.request exposes the normalised parameters
// (clientId, nonce, acceptResponseUri, state, ...).

validateOid4vpRequest returns an Oid4vpShareRequest. Treat it as an opaque handle — the package routes between PEX and DCQL internally, so you pass the same shareRequest to the credential-matching and submission services regardless of which query protocol the verifier used.

Step 2 — Find matching credentials #

Pass the shareRequest and the credentials stored in the user's Vault to CredentialMatcherService.match(). The service routes to PEX or DCQL internally and returns a MatchedCredentialsResult.

final matcher = CredentialMatcherService();

final result = await matcher.match(shareRequest, allVaultCredentials);

if (!result.hasEnoughVCsAvailableToShare) {
  // The Vault does not hold enough credentials to satisfy the request.
  return;
}

// A sensible default selection to present to the user, or submit directly.
final selectedVcs = result.recommendedMaximumVCs;

allVaultCredentials is the List<VerifiableCredential> you load from your own storage. Use result.groups to enforce per-group minimum and maximum selection counts when building a selection UI.

Step 3 — Submit (or reject) the Verifiable Presentation (VP) #

Use IotaShareResponseService to build a signed VP from the selected credentials and POST it to the verifier. It handles both PEX and DCQL responses internally and uses the request nonce and clientId to bind the presentation for replay protection.

IotaShareResponseService needs a DidSigner that controls the holder's signing key. In a real application this comes from your wallet integration; the snippet below builds one from an ssi wallet:

final wallet = PersistentWallet(InMemoryKeyStore());
final keyPair = await wallet.generateKey(keyType: KeyType.ed25519);

final didManager = DidKeyManager(wallet: wallet, store: InMemoryDidStore());
await didManager.addVerificationMethod(keyPair.id);

final signer = await didManager.getSigner(
  didManager.assertionMethod.first,
  signatureScheme: SignatureScheme.ed25519,
);

Then submit (or reject) the presentation:

final responseService = IotaShareResponseService(
  signer: signer,
  trustedVerifiersList: ['verifier.example.com'],
);

// On user approval — build and submit the VP.
final redirectUri = await responseService.submitShareResponse(
  shareRequest: shareRequest,
  selectedCredentials: selectedVcs.cast<ParsedVerifiableCredential<dynamic>>(),
  acceptResponseUri: shareRequest.request.acceptResponseUri,
);

// On user rejection — notify the verifier.
await responseService.rejectShareResponse(
  shareRequest: shareRequest,
  rejectResponseUri: shareRequest.request.rejectResponseUri,
);

Both methods return the redirect Uri that the verifier optionally includes in its response. When non-null, send the user to this URL to complete the flow on the verifier's side (for example, back to the verifier's web app); when null, the verifier expects no follow-up navigation. Both methods throw a TdkException with code submission_failed if the call fails.

After a successful VP submission, call IotaConsentRecordService.saveConsentRecord() to store a history entry for the share event. The service is storage-agnostic — you supply the backend by implementing ConsentStorage.

Using the built-in Flutter Secure Storage backend

If your app already depends on affinidi_tdk_vault_flutter_utils, use the provided FlutterSecureConsentStorage:

import 'package:affinidi_tdk_vault_flutter_utils/vault_flutter_utils.dart';
import 'package:affinidi_tdk_vault_iota/affinidi_tdk_vault_iota.dart';

final consentService = IotaConsentRecordService(
  store: FlutterSecureConsentStorage(),
  cryptography: myCryptographyService,
  shareResponseService: responseService,
);

// Call this after a successful submitShareResponse:
await consentService.saveConsentRecord(
  shareRequest: shareRequest,
  verifierMetadata: verifierMetadata,
  profileId: profileId,
  profileName: profileName,
  vaultId: holderVaultId,
  sharedVcs: selectedVcs,
  claimedVcTypesCsv: 'EmailV1VC,PhoneNumberV1VC',
  isAutoShareEnabled: false,
);

Request fingerprint (computed by the SDK)

You no longer compute a requestHash. The SDK derives an internal request fingerprint from the shareRequest (the verifier client_id plus the sorted credential-group ids — PEX input-descriptor ids or DCQL credential-query ids) and the vaultId, and uses the same derivation for both saveConsentRecord and tryAutomaticConsent. Because the SDK owns this computation, an auto-consent lookup can never silently miss due to a mismatched, caller-computed hash.

Bringing your own storage backend

Implement ConsentStorage with any persistence technology you prefer (Drift, Hive, SQLite, a remote API, etc.):

class MyConsentStore implements ConsentStorage {
  @override
  Future<void> saveOrUpdate(IotaConsentRecord record) async {
    // upsert by record.hash in your database
  }

  @override
  Future<IotaConsentRecord?> findByRequestHash(String requestHash) async {
    // return the most recent matching record, or null
  }

  @override
  Future<List<IotaConsentRecord>> findAllByRequestHash(String requestHash) async {
    // return all matching records, or an empty list
  }
}

final consentService = IotaConsentRecordService(
  store: MyConsentStore(),
  cryptography: myCryptographyService,
  shareResponseService: responseService,
);

Security considerations #

Replay attack protection #

ShareFlowService enforces nonce uniqueness based on OID4VP spec, calling validateOid4vpRequest with the same JWT nonce a second time (while the JWT is still within its exp window) throws a TdkException with code replay_detected.

By default, nonces are tracked in an in-memory cache scoped to the ShareFlowService instance. This means replay protection does not survive a process restart. For persistent cross-session protection, implement the NonceReplayStore interface to back it with durable storage, then inject it via the replayCache parameter:

class MyPersistentNonceStore implements NonceReplayStore {
  @override
  Future<bool> record(String nonce, int expEpochSeconds) async {
    // Check and store in your database; return false if already seen.
    return myDb.recordNonce(nonce, expEpochSeconds);
  }
}

final service = ShareFlowService(
  cryptography: myCryptographyService,
  replayCache: MyPersistentNonceStore(),
);

Trusted verifiers list #

IotaShareResponseService requires a non-empty trustedVerifiersList of plain host names. Before posting the Verifiable Presentation, it checks that the response_uri from the OID4VP request belongs to one of those hosts — preventing a compromised or malicious request from redirecting your VP to an attacker-controlled server.

final responseService = IotaShareResponseService(
  signer: mySigner,
  trustedVerifiersList: ['verifier.example.com', 'other-verifier.example.com'],
);

Rules for entries in trustedVerifiersList:

  • Must be plain host names — no scheme (https://), no path, no port, no query string.
  • The check is case-insensitive.
  • Passing an empty list throws empty_trusted_verifiers_list immediately at construction time.
  • A response URI whose host is not in the list throws untrusted_response_uri before any network call is made.

Error handling #

All errors are thrown as TdkException with one of the following codes:

Code Description
parse_failure The request query parameter is absent, the JWT could not be decoded, or a required payload field is missing.
invalid_or_expired_jwt The JWT signature is invalid or the token has expired.
invalid_client_id_scheme The client_id_scheme in the request is not did.
invalid_audience The JWT aud claim does not match the walletDid passed to validateOid4vpRequest.
missing_client_id The client_id field is missing from the request.
invalid_client_id An empty clientId was passed to the verifier metadata service.
invalid_response_mode response_mode is not direct_post.
invalid_response_type response_type is not vp_token.
invalid_response_uri The response URI is malformed, not HTTPS, or contains an IP address or invalid hostname.
untrusted_response_uri The response URI host is not in the trustedVerifiersList passed to IotaShareResponseService.
empty_trusted_verifiers_list IotaShareResponseService was constructed with an empty trustedVerifiersList.
invalid_presentation_definition The Presentation Definition is structurally invalid.
invalid_dcql_query The DCQL query is structurally invalid.
unsupported_multiple_idv_types An IDV input descriptor requests more than two VC types.
replay_detected The OID4VP request nonce has already been consumed — indicates a JWT replay attempt.
empty_credentials submitShareResponse was called with an empty credentials list.
incomplete_credential_selection The selected credentials do not cover every required DCQL credential query.
submission_failed Submitting the VP to the verifier callback failed (network error, invalid state, or non-2xx response).
failed_to_fetch_verifier_metadata The verifier's client metadata could not be fetched or parsed.
failed_to_persist_consent_record Persisting a consent record to the ConsentStorage backend failed.
failed_to_read_consent_record Reading a consent record from the ConsentStorage backend failed.

Support & feedback #

If you face any issues or have suggestions, please don't hesitate to contact us using this link.

Reporting technical issues #

If you have a technical issue with the package's codebase, you can also create an issue directly in GitHub.

  1. Ensure the bug was not already reported by searching on GitHub under Issues.

  2. If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behaviour that is not occurring.

Contributing #

Want to contribute?

Head over to our CONTRIBUTING guidelines.

0
likes
150
points
72
downloads

Documentation

API reference

Publisher

verified publisheraffinidi.com

Weekly Downloads

The Affinidi TDK - Vault Iota package provides support for OID4VP share flow, parsing Iota Framework request URIs and submitting Verifiable Presentations.

Homepage
Repository (GitHub)
View/report issues
Contributing

License

Apache-2.0 (license)

Dependencies

affinidi_tdk_common, affinidi_tdk_cryptography, dcql, dio, json_schema, meta, ssi, uuid

More

Packages that depend on affinidi_tdk_vault_iota