DIDComm Library

An implementation of the DIDComm v2.1 protocol for decentralised communication.

A Dart package for implementing secure and private communication on your app using DIDComm v2 Messaging protocol. DIDComm v2 Messaging is a decentralised communication protocol that uses a Decentralised Identifier (DID) to establish a secure communication channel and send a private and verifiable message.

The DIDComm for Dart package provides the tools and libraries to enable your app to send DIDComm messages. It supports various encryption algorithms and DID methods, such as did:peer, did:key, and did:web for signing and encrypting to ensure the secure and private transport of messages to the intended recipient, establishing verifiable and trusted communication.

Core Concepts

The DIDComm for Dart package utilises existing open standards and cryptographic techniques to provide secure, private, and verifiable communication.

  • Decentralised Identifier (DID) - A globally unique identifier that enables secure interactions. The DID is the cornerstone of Self-Sovereign Identity (SSI), a concept that aims to put individuals or entities in control of their digital identities.

  • DID Document - A DID is a URI (Uniform Resource Identifier) that resolves into a DID Document that contains information such as cryptographic public keys, authentication methods, and service endpoints. It allows others to verify signatures, authenticate interactions, and validate messages cryptographically.

  • DIDComm Message - is a JSON Web Message (JWM), a lightweight, secure, and standardised format for structured communication using JSON. It represents headers, message types, routing metadata, and payloads designed to enable secure and interoperable communication across different systems.

  • Mediator - A service that handles and routes messages sent between participants (e.g., users, organisations, another mediator, or even AI agents).

  • DID Manager - Establishes the relationship between DID methods and key pairs from the Wallet, supporting different algorithms for signing and verifying messages.

Key Features

  • Implements the DIDComm Message v2.0 protocol.

  • Support for DIDComm Messaging Envelope types. Refer to the DIDComm Message Envelopes section to learn more.

  • Support for digital wallets under Affinidi Dart SSI to manage cryptographic keys.

  • Connect and authenticate with different mediator services that follow the DIDComm Message v2.0 protocol.

Supported Curves and Algorithms

The DIDComm package supports the following curves and algorithms for signing and encrypting content.

Curves and Signing Algorithms

CurveSigning AlgorithmUsed in Content EncryptionNotes
Ed25519EdDSA✅ Yes*Ed25519 is for signing only; X25519 is used for encryption/key exchange through Ed25519/X25519 key derivation
P-384ES384✅ YesES384 support for signing is not listed in the DIDComm spec
P-521ES521✅ YesES521 support for signing is not listed in the DIDComm spec
secp256k1ES256K✅ Yessecp256k1 support for encryption is not listed in the DIDComm spec
P-256ES256✅ YesDeprecated in DIDComm v2 in favor of P-384

Content Encryption Algorithms

Encryption AlgorithmNotes
A256CBC-HS512Used for Authcrypt/Anoncrypt
A256GCMUsed for Anoncrypt
XC20PNot supported yet

Note: XC20P is part of the DIDComm spec, but not yet supported by the Affinidi SSI.

Supported DID Methods

The DIDComm package supports the following DID methods to represent the identity of each entity.

DID MethodNote
did:keyFully supported by Affinidi Dart SSI’s DID Manager
did:peerFully supported by Affinidi Dart SSI’s DID Manager
did:webOnly DID resolution is supported

DIDComm Message Envelopes

DIDComm v2 messages can be sent in the following formats: plaintext, signed, and encrypted. Each format, called “envelope”, provides different security and privacy guarantees and can be combined in various ways.

didcomm messages

  • Plaintext message: A message that is neither signed nor encrypted. It is readable by anyone and provides no integrity or authenticity guarantees. Used for non-sensitive data, debugging, or as the inner content of other envelopes.

  • Signed message: A message that is digitally signed but not encrypted. Anyone can read it, but the recipient can prove who signed it (non-repudiation)—used when the message’s origin must be provable to the recipient or third parties.

  • Encrypted message: An encrypted message for one or more recipients. Only the intended recipients can read the content of the message. Encryption can be:

    • Authenticated encryption (authcrypt, ECDH-1PU): Proves the sender’s identity to the recipient (but not to intermediaries). Used when both confidentiality and sender authenticity are required.

      It uses the ECDH-1PU for authenticated encryption (authcrypt), where the sender’s key is involved in the encryption process, allowing the recipient to verify the sender’s identity.

    • Anonymous encryption (anoncrypt, ECDH-ES): Hides the sender’s identity from the recipient and intermediaries. It is used when the sender’s anonymity is required.

      It uses ECDH-ES for anonymous encryption (anoncrypt), where only the recipient’s key is used, and the sender remains anonymous.

Get Started

Requirements

  • Dart SDK version ^3.6.0

Installation

Run:

dart pub add didcomm

or manually, add the package into your pubspec.yaml file:

dependencies: didcomm: ^<version_number>

and then run the command below to install the package:

dart pub get

Visit the pub.dev install page of the Dart package for more information.

Sample Usage

Below is a step-by-step example of secure communication between Alice and Bob using the DIDComm Dart package. The example demonstrates how to construct, sign, encrypt, and unpack messages according to the DIDComm Messaging spec.

1. Set up DID Manager and DIDs

final aliceKeyStore = InMemoryKeyStore(); final aliceWallet = PersistentWallet(aliceKeyStore); final aliceDidManager = DidKeyManager( wallet: aliceWallet, store: InMemoryDidStore(), ); final bobKeyStore = InMemoryKeyStore(); final bobWallet = PersistentWallet(bobKeyStore); final bobDidManager = DidKeyManager( wallet: bobWallet, store: InMemoryDidStore(), ); final aliceKeyId = 'alice-key-1'; await aliceWallet.generateKey( keyId: aliceKeyId, keyType: KeyType.p256, ); await aliceDidManager.addVerificationMethod(aliceKeyId); final aliceDidDocument = await aliceDidManager.getDidDocument(); final bobKeyId = 'bob-key-1'; await bobWallet.generateKey( keyId: bobKeyId, keyType: KeyType.p256, ); await bobDidManager.addVerificationMethod(bobKeyId); final bobDidDocument = await bobDidManager.getDidDocument();

2. Compose a Plain Text Message

A plain text message is a simple JSON message with headers and a body.

final plainTextMessage = PlainTextMessage( id: '041b47d4-9c8f-4a24-ae85-b60ec91b025c', // Unique message ID from: aliceDidDocument.id, // Sender's DID to: [bobDidDocument.id], // Recipient's DID(s) type: Uri.parse('https://didcomm.org/example/1.0/message'), // Message type URI body: {'content': 'Hello, Bob!'}, // Message payload ); plainTextMessage['custom-header'] = 'custom-value'; // Add custom headers if needed

3. Sign the Message

Signing a message is optional in DIDComm. It is required when you need to provide non-repudiation—proof that the sender cannot deny authorship of the message. Signing a message is essential for scenarios where:

  • The recipient must prove to a third party that the sender authored the message (e.g., legal, regulatory, or audit requirements).
  • The message may be forwarded or relayed, and recipients must verify its origin independently of the transport channel.
  • You want to ensure message integrity and origin even if the message is not encrypted.
final aliceSigner = await aliceDidManager.getSigner( aliceDidDocument.assertionMethod.first.id, ); final signedMessage = await SignedMessage.pack( plainTextMessage, signer: aliceSigner, // The signer instance );

4. Encrypt the Message

Although optional, encrypting DIDComm messages is highly recommended to protect their confidentiality. DIDComm supports two main types of encryption:

  • Authenticated Encryption (authcrypt, ECDH-1PU):
    • Proves the sender’s identity to the recipient (but not to intermediaries).
    • Used when both confidentiality and sender authenticity are required.
    • Only the intended recipient can read the message and verify that the sender’s key encrypts the message.
    • Protects the sender’s identity from intermediaries and eavesdroppers.

Choose authcrypt when you want the recipient to know who sent the message (authenticated, private communication).

  • Anonymous Encryption (anoncrypt, ECDH-ES):
    • Hides the sender’s identity from both the recipient and intermediaries.
    • Used when sender anonymity is required.
    • Only the intended recipient can read the message but cannot determine who sent it.

Choose anoncrypt when you want to keep the sender’s identity hidden (anonymous tips, whistleblowing, or privacy-preserving scenarios).

Example: Authenticated Encryption (authcrypt)
final aliceMatchedKeyIds = aliceDidDocument.matchKeysInKeyAgreement( otherDidDocuments: [bobDidDocument], ); final encryptedMessage = await EncryptedMessage.packWithAuthentication( message, // The signed or plain text message to encrypt keyPair: await aliceDidManager.getKeyPairByDidKeyId(aliceMatchedKeyIds.first), didKeyId: aliceMatchedKeyIds.first, recipientDidDocuments: [bobDidDocument], );
  • keyPair: Alice’s key pair used for authenticated encryption.
  • didKeyId: The key ID from Alice’s DID Document for key agreement.
  • recipientDidDocuments: The recipient’s DID Document(s).
  • encryptionAlgorithm: The encryption algorithm to use (e.g., a256cbc).
Example: Anonymous Encryption (anoncrypt)

If you want to encrypt a message without revealing the sender’s identity, use packAnonymously:

final anonymousEncryptedMessage = await EncryptedMessage.packAnonymously( message, // The signed or plain text message to encrypt keyType: KeyType.p256, // Key type for recipient's key agreement (required) recipientDidDocuments: [bobDidDocument], // List of recipient DID Documents encryptionAlgorithm: EncryptionAlgorithm.a256cbc, // Encryption algorithm );
  • message: The message to encrypt (can be plain or signed).
  • recipientDidDocuments: The recipient’s DID Document(s).
  • encryptionAlgorithm: The encryption algorithm to use.
  • keyType: The key type for the recipient’s key agreement key (e.g., KeyType.p256, KeyType.ed25519).

In this case, Bob can decrypt and read the message but cannot determine who sent it. This approach is helpful for scenarios where sender anonymity is required.

More details about the key type selection for authcrypt and anoncrypt.

Key Type Selection for Authcrypt and Anoncrypt

When encrypting messages, you must select a key type for a key agreement that all parties support.

  • Authcrypt (authenticated encryption, ECDH-1PU):

    • For key agreement, both the sender and all recipients must use compatible key types (e.g., both must have P-256 or X25519 keys in their DID Documents).
    • You typically use the sender’s DidManager to find a compatible key pair and key agreement method with each recipient.
    • Use the matchKeysInKeyAgreement extension method to find compatible key IDs from the sender’s wallet for all recipients.
  • Anoncrypt (anonymous encryption, ECDH-ES):

    • Only uses the recipients’ key agreement keys; does not involve the sender’s key pair.
    • You must select a key type that all recipients for key agreement support.
    • Use the getCommonKeyTypesInKeyAgreements extension method on the list of recipient DID Documents to determine the set of key types common to all recipients.

Examples:

Authcrypt:

final compatibleKeyIds = aliceDidDocument.matchKeysInKeyAgreement( otherDidDocuments: [ bobDidDocument // and other recipients ], ); if (compatibleKeyIds.isEmpty) { throw Exception('No compatible key agreement method found between Alice and Bob'); } final aliceDidKeyId = compatibleKeyIds.first; // Use this key ID for Alice

Anoncrypt:

final commonKeyTypes = [ bobDidDocument, // and other recipients ].getCommonKeyTypesInKeyAgreements(); if (commonKeyTypes.isEmpty) { throw Exception('No common key type found for anoncrypt between recipients'); } final keyType = commonKeyTypes.first; // Use this key type for anoncrypt

This ensures that the correct and compatible keys are used for ECDH-1PU (authcrypt) and ECDH-ES (anoncrypt) operations, and that all recipients can decrypt the message using a supported key agreement method.

Security Safeguards

The DIDComm package performs different verification processes to ensure the integrity and authenticity of the messages received, including a custom verification and processing after the message is unpacked.

Message Layer Addressing Consistency

To ensure trust and prevent message tampering or misrouting, DIDComm v2 enforces message layer addressing consistency between the plaintext message and its cryptographic envelopes. According to the DIDComm v2 spec:

  • The from attribute in the plaintext message must match the skid (sender key ID) in the encryption layer.
  • The to attribute in the plaintext message must contain the kid (recipient key ID) in the encryption layer.
  • The from attribute in the plaintext message must match the signer’s kid in a signed message.

Envelope’s layout:

encrypted message: header: skid: did:example:alice#key-1 recipients: - kid: did:example:bob#key-1 payload: signed message: signatures: - kid: did:example:alice#key-1 payload: plain text message: from: did:example:alice#key-1 to: did:example:bob#key-1 body: ...

If any of these checks fail, the message is considered invalid and an error is raised.

When you call unpackToPlainTextMessage in this Dart library, addressing consistency checks are performed automatically. If any inconsistency is detected, unpackToPlainTextMessage throws an error, preventing further processing of potentially malicious or misrouted messages. This strict enforcement helps maintain the integrity and authenticity of DIDComm messages, as required by the specification.

For development or debugging purposes, you can disable addressing consistency checks by passing validateAddressingConsistency: false to unpackToPlainTextMessage. This allows you to inspect or process messages that would otherwise be rejected due to addressing mismatches.

Important: Disabling these checks should only be done in trusted, non-production environments, as it weakens security guarantees and may expose your application to spoofed or misrouted messages.

Message Wrapping Verification

The expectedMessageWrappingTypes argument of unpackToPlainTextMessage lets you specify which message wrapping types (e.g., authenticated encryption, signed, plaintext) are allowed when unpacking a message. This is an important security feature that helps prevent downgrade attacks and ensures the message you receive matches your security expectations.

  • If you provide a list of wrapping types (such as [MessageWrappingType.authcryptSignPlaintext, MessageWrappingType.authcryptPlaintext, MessageWrappingType.anoncryptPlaintext]), only messages with those wrappings will be accepted. Any message with a different wrapping will be rejected.
  • If you do not specify expectedMessageWrappingTypes (i.e., leave it null or omit it), the default is [MessageWrappingType.authcryptPlaintext] as recommended by the DIDComm v2 specification. This means only authenticated encrypted messages will be accepted by default.

Tip: Always set expectedMessageWrappingTypes explicitly to match your protocol or application’s requirements. This helps ensure you are not tricked into processing a message with weaker security than intended.

Example how to use:

final plainTextMessage = await DidcommMessage.unpackToPlainTextMessage( message: json, recipientDidManager: bobDidManager, expectedMessageWrappingTypes: [ MessageWrappingType.anoncryptSignPlaintext, MessageWrappingType.authcryptSignPlaintext, MessageWrappingType.authcryptPlaintext, MessageWrappingType.anoncryptAuthcryptPlaintext, ], );

Signers Verification

When unpacking a message, every signature present is automatically and individually verified by the library to be cryptographically correct and to match the claimed key. If you want to make sure that a certain signers signed the message, you can use the expectedSigners argument.

The expectedSigners argument of unpackToPlainTextMessage lets you specify a set of signers’ key IDs that you require to have signed the message. This is especially important for signed messages that may contain multiple signatures (for example, in multi-party workflows or protocols requiring multiple approvals).

Example with multiple signatures:

{ "payload":"<decoded plain text message>", "signatures":[ { "protected":"...", "signature":"<signature-1 base64>", "header":{ "kid":"did:example:alice#key-1" } }, { "protected":"...", "signature":"<signature-2 base64>", "header":{ "kid":"did:example:bob#key-1" } } ] }

When you provide a list of key IDs in expectedSigners, the function will check that all of the provided signers have actually signed the message. Additional signatures beyond those listed in expectedSigners are allowed, but if any of the expected signers are missing from the message’s signatures, the message will be rejected. This ensures that all required parties have signed the message, and prevents partial or incomplete multi-signature messages from being processed.

If you do not specify expectedSigners, the function will not enforce any signer requirements beyond the addressing consistency checks (i.e., it will only check that the from field matches at least one signature’s key ID, as required by the DIDComm spec).

Tip: Always use expectedSigners when you need to ensure that a message was signed by at least a specific set of parties, especially in scenarios where multiple signatures are possible or required. This is critical for protocols that require multi-party approval or consensus.

Example how to use:

final plainTextMessage = await DidcommMessage.unpackToPlainTextMessage( message: json, recipientDidManager: bobDidManager, expectedMessageWrappingTypes: [ MessageWrappingType.anoncryptSignPlaintext, MessageWrappingType.authcryptSignPlaintext, MessageWrappingType.signPlaintext, ], expectedSigners: [ 'did:example:alice#key-1', 'did:example:bob#key-1', ], );