Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DIDComm V2 Initial Implementation #2959

Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
223e9f9
feat: (WIP) didcommv2 handling
mepeltier Apr 16, 2024
f6ed06d
feat: wallet methods for assigning kid to keys
dbluhm Apr 16, 2024
88ba288
refactor: inject DMP dependencies
dbluhm Apr 23, 2024
c41d437
fix: x25519 key creation and transformation
mepeltier Apr 23, 2024
7a9030d
feat: (WIP) didcomm v2 message handling
mepeltier May 10, 2024
a8530ff
feat: add flag for didcomm v2 support
mepeltier May 14, 2024
d39eb4d
chore: remove unnecessary logs
mepeltier May 14, 2024
2693a93
feat: Add first pass of DIDComm V2 response
TheTechmage May 13, 2024
e15267a
refactor: revert profile/session injection
dbluhm May 14, 2024
4808708
feat: resolve circular dependency
mepeltier May 17, 2024
1089a22
feat: add additional dcv2 flag checks
mepeltier May 17, 2024
1ced95a
feat: revert bbs removal
mepeltier May 17, 2024
1c47e3b
feat: make dmp an optional dep
mepeltier May 20, 2024
87ec574
chore: adjust flag pattern
mepeltier May 20, 2024
291d4e1
feat: Return Problem Report for DIDComm V2
TheTechmage May 21, 2024
30107e8
chore: balck
TheTechmage May 21, 2024
d6c3f19
chore: cleanup lock file after rebase
TheTechmage May 21, 2024
bb2a20a
feat: split out v2 pack format
mepeltier May 21, 2024
1131c91
feat: gate didcommv2 imports on didcommv2 flag
mepeltier May 21, 2024
2a3067e
chore: cleanup lingering logging statements
mepeltier May 21, 2024
4fa84ad
feat: Return DIDComm V2 problem report
TheTechmage May 21, 2024
258d8fc
Merge branch 'feat/didcommv2-proof-of-concept' of github.com:Indicio-…
TheTechmage May 21, 2024
4e7fc62
fix: conditional imports
mepeltier May 22, 2024
3b0c966
fix: get unit tests passing
mepeltier Jun 3, 2024
6a8e140
Merge branch 'main' into feat/didcommv2-proof-of-concept
mepeltier Jun 4, 2024
0244092
chore: poetry lock
mepeltier Jun 4, 2024
8ae16d3
chore: minor cleanup
mepeltier Jun 5, 2024
8bf1d85
fix: fixes for tests
mepeltier Jun 6, 2024
d20918e
Merge branch 'main' into feat/didcommv2-proof-of-concept
mepeltier Jun 6, 2024
bc5fde9
chore: poetry lock
mepeltier Jun 6, 2024
b7073bb
fix: move split to last possible moment
TheTechmage May 21, 2024
cd44785
feat: Add didcommv2 extra to Dockerfile for builds
TheTechmage Jun 11, 2024
0020ff8
Merge remote-tracking branch 'origin/main' into feat/didcommv2-proof-…
TheTechmage Jun 11, 2024
3d151c6
feat: adjust test build for didcommv2 tests
mepeltier Jun 28, 2024
5fa9e11
feat: add tests for didcommv2 adapters
mepeltier Jun 28, 2024
2cb2ca2
feat: tests for v2 pack format
mepeltier Jun 28, 2024
85a3a52
chore: poetry lock
mepeltier Jun 28, 2024
a515948
Merge branch 'main' into feat/didcommv2-proof-of-concept
mepeltier Jun 28, 2024
f917714
chore: poetry lock
mepeltier Jun 28, 2024
eed6981
feat: additional in_memory wallet test
mepeltier Jun 29, 2024
dc1ee0a
Merge branch 'main' into feat/didcommv2-proof-of-concept
mepeltier Jul 1, 2024
8bb32d0
chore: poetry lock
mepeltier Jul 1, 2024
fdc4408
Merge branch 'main' into feat/didcommv2-proof-of-concept
mepeltier Jul 2, 2024
118d319
chore: poetry lock
mepeltier Jul 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions aries_cloudagent/askar/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import asyncio
import logging
import time

# import traceback

from typing import Any, Mapping
from weakref import ref

from aries_askar import AskarError, Session, Store
from didcomm_messaging import (
CryptoService,
DIDCommMessaging,
PackagingService,
RoutingService,
SecretsManager,
)
from didcomm_messaging.resolver import DIDResolver as DMPResolver

from ..cache.base import BaseCache
from ..config.injection_context import InjectionContext
Expand All @@ -21,13 +26,13 @@
from ..indy.verifier import IndyVerifier
from ..ledger.base import BaseLedger
from ..ledger.indy_vdr import IndyVdrLedger, IndyVdrLedgerPool
from ..resolver.did_resolver import DIDResolver
from ..storage.base import BaseStorage, BaseStorageSearch
from ..storage.vc_holder.base import VCHolder
from ..utils.multi_ledger import get_write_ledger_config_for_profile
from ..wallet.base import BaseWallet
from ..wallet.crypto import validate_seed

from .store import AskarStoreConfig, AskarOpenStore
from .store import AskarOpenStore, AskarStoreConfig

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -94,6 +99,14 @@ def bind_providers(self):
"""Initialize the profile-level instance providers."""
injector = self._context.injector

injector.bind_provider(
DMPResolver,
ClassProvider(
"aries_cloudagent.didcomm_v2.adapters.ResolverAdapter",
ref(self),
ClassProvider.Inject(DIDResolver),
),
)
injector.bind_provider(
BaseStorageSearch,
ClassProvider(
Expand Down Expand Up @@ -250,6 +263,24 @@ async def _setup(self):
BaseStorage,
ClassProvider("aries_cloudagent.storage.askar.AskarStorage", ref(self)),
)
injector.bind_provider(
SecretsManager,
ClassProvider(
"aries_cloudagent.didcomm_v2.adapters.SecretsAdapter", ref(self)
),
)
if self.profile.settings.get("experiment.didcomm_v2"):
injector.bind_provider(
DIDCommMessaging,
ClassProvider(
DIDCommMessaging,
ClassProvider.Inject(CryptoService),
ClassProvider.Inject(SecretsManager),
ClassProvider.Inject(DMPResolver),
ClassProvider.Inject(PackagingService),
ClassProvider.Inject(RoutingService),
),
)

async def _teardown(self, commit: bool = None):
"""Dispose of the session or transaction connection."""
Expand Down
9 changes: 9 additions & 0 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,13 @@ def add_arguments(self, parser: ArgumentParser):
),
)

parser.add_argument(
"--experimental-didcomm-v2",
action="store_true",
env_var="ACAPY_EXP_DIDCOMM_V2",
help="Enable experimental DIDComm V2 support.",
)

def get_settings(self, args: Namespace) -> dict:
"""Get protocol settings."""
settings = {}
Expand Down Expand Up @@ -1235,6 +1242,8 @@ def get_settings(self, args: Namespace) -> dict:
if args.exch_use_unencrypted_tags:
settings["exch_use_unencrypted_tags"] = True
environ["EXCH_UNENCRYPTED_TAGS"] = "True"
if args.experimental_didcomm_v2:
settings["experiment.didcomm_v2"] = True

return settings

Expand Down
20 changes: 18 additions & 2 deletions aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
"""Classes for configuring the default injection context."""

from didcomm_messaging import (
CryptoService,
PackagingService,
RoutingService,
)
from didcomm_messaging.crypto.backend.askar import AskarCryptoService


from ..anoncreds.registry import AnonCredsRegistry
from ..cache.base import BaseCache
from ..cache.in_memory import InMemoryCache
from ..core.event_bus import EventBus
from ..core.goal_code_registry import GoalCodeRegistry
from ..core.plugin_registry import PluginRegistry
from ..core.profile import ProfileManager, ProfileManagerProvider
from ..core.profile import (
ProfileManager,
ProfileManagerProvider,
)
from ..core.protocol_registry import ProtocolRegistry
from ..protocols.actionmenu.v1_0.base_service import BaseMenuService
from ..protocols.actionmenu.v1_0.driver_service import DriverMenuService
Expand Down Expand Up @@ -53,14 +64,19 @@ async def build_context(self) -> InjectionContext:
context.injector.bind_instance(EventBus, EventBus())

# Global did resolver
context.injector.bind_instance(DIDResolver, DIDResolver([]))
context.injector.bind_instance(DIDResolver, DIDResolver())
context.injector.bind_instance(AnonCredsRegistry, AnonCredsRegistry())
context.injector.bind_instance(DIDMethods, DIDMethods())
context.injector.bind_instance(KeyTypes, KeyTypes())
context.injector.bind_instance(
BaseVerificationKeyStrategy, DefaultVerificationKeyStrategy()
)

# DIDComm Messaging
context.injector.bind_instance(CryptoService, AskarCryptoService())
context.injector.bind_instance(PackagingService, PackagingService())
context.injector.bind_instance(RoutingService, RoutingService())

await self.bind_providers(context)
await self.load_plugins(context)

Expand Down
38 changes: 33 additions & 5 deletions aries_cloudagent/connections/base_manager.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling out a limitation: experimental v2 support will only work using did:peer:2 right now.

Should we implement it for did:peer:4 as well at this stage?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given how early we are in the process, I don't think its necessary, but at the same time, it might be worth getting it in before our changes are more solidified. It'll also probably make it easier to switch off of did:peer:2, which is a good thing imo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do believe that it's not limited to did:peer:2. Other did:peer methods should be supported as well. Once I have something better to key off of for outbound, things like did:web should work (in theory). Though you are right in that all did methods, aside from did:peer:2, are currently untested

Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
from ..wallet.did_info import INVITATION_REUSE_KEY, DIDInfo, KeyInfo
from ..wallet.did_method import PEER2, PEER4, SOV, DIDMethod
from ..wallet.error import WalletNotFoundError
from ..wallet.key_type import ED25519
from ..wallet.key_type import ED25519, X25519
from ..wallet.util import b64_to_bytes, bytes_to_b58
from .models.conn_record import ConnRecord
from .models.connection_target import ConnectionTarget
Expand Down Expand Up @@ -85,9 +85,18 @@ def __init__(self, profile: Profile):
@staticmethod
def _key_info_to_multikey(key_info: KeyInfo) -> str:
"""Convert a KeyInfo to a multikey."""
return multibase.encode(
multicodec.wrap("ed25519-pub", b58decode(key_info.verkey)), "base58btc"
)
if key_info.key_type == ED25519:
return multibase.encode(
multicodec.wrap("ed25519-pub", b58decode(key_info.verkey)), "base58btc"
)
elif key_info.key_type == X25519:
return multibase.encode(
multicodec.wrap("x25519-pub", b58decode(key_info.verkey)), "base58btc"
)
else:
raise BaseConnectionManagerError(
"Unsupported key type. Could not convert to multikey."
)

def long_did_peer_to_short(self, long_did: str) -> str:
"""Convert did:peer:4 long format to short format and return."""
Expand Down Expand Up @@ -222,13 +231,30 @@ async def create_did_peer_2(
"serviceEndpoint": endpoint,
}
)
if self._profile.settings.get("experiment.didcomm_v2"):
services.append(
{
"id": f"#service-{index}",
"type": "DIDCommMessaging",
"serviceEndpoint": {
"uri": endpoint,
"accept": ["didcomm/v2"],
"routingKeys": routing_keys,
},
}
)

async with self._profile.session() as session:
wallet = session.inject(BaseWallet)
key = await wallet.create_key(ED25519)
xk = await wallet.create_key(X25519)

did = generate(
[KeySpec.verification(self._key_info_to_multikey(key))], services
[
KeySpec.verification(self._key_info_to_multikey(key)),
KeySpec.key_agreement(self._key_info_to_multikey(xk)),
],
services,
)

did_metadata = metadata if metadata else {}
Expand All @@ -240,6 +266,8 @@ async def create_did_peer_2(
key_type=ED25519,
)
await wallet.store_did(did_info)
await wallet.assign_kid_to_key(key.verkey, f"{did}#key-1")
await wallet.assign_kid_to_key(xk.verkey, f"{did}#key-2")

return did_info

Expand Down
50 changes: 47 additions & 3 deletions aries_cloudagent/core/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..connections.models.conn_record import ConnRecord
from ..core.profile import Profile
from ..messaging.agent_message import AgentMessage
from ..messaging.base_message import BaseMessage
from ..messaging.base_message import BaseMessage, DIDCommVersion
from ..messaging.error import MessageParseError
from ..messaging.models.base import BaseModelError
from ..messaging.request_context import RequestContext
Expand Down Expand Up @@ -108,12 +108,56 @@ def queue_message(
A pending task instance resolving to the handler task

"""

if (
self.profile.settings.get("experiment.didcomm_v2")
and inbound_message.receipt.didcomm_version == DIDCommVersion.v2
):
handle = self.handle_v2_message(profile, inbound_message, send_outbound)
else:
handle = self.handle_v1_message(profile, inbound_message, send_outbound)

return self.put_task(
self.handle_message(profile, inbound_message, send_outbound),
handle,
complete,
)

async def handle_message(
async def handle_v2_message(
self,
profile: Profile,
inbound_message: InboundMessage,
send_outbound: Coroutine,
):
"""Handle a DIDComm V2 message."""

# send a DCV2 Problem Report here for testing, and to punt procotol handling down
# the road a bit
context = RequestContext(profile)
# context.message = message
context.message_receipt = inbound_message.receipt
responder = DispatcherResponder(
context,
inbound_message,
send_outbound,
reply_session_id=inbound_message.session_id,
reply_to_verkey=inbound_message.receipt.sender_verkey,
)

context.injector.bind_instance(BaseResponder, responder)
# responder.connection_id = connection and connection.connection_id
error_result = ProblemReport(
description={
"en": str("No Handlers Found"),
"code": "message-parse-failure",
}
)
TheTechmage marked this conversation as resolved.
Show resolved Hide resolved
if inbound_message.receipt.thread_id:
error_result.assign_thread_id(inbound_message.receipt.thread_id)
logging.getLogger(__name__).debug("CONSTRUCTED V2 DISPATCHER RESPONSE")
TheTechmage marked this conversation as resolved.
Show resolved Hide resolved
await responder.send_reply(error_result)
logging.getLogger(__name__).debug("LEFT V2 DISPATCHER HANDLER")

async def handle_v1_message(
self,
profile: Profile,
inbound_message: InboundMessage,
Expand Down
4 changes: 4 additions & 0 deletions aries_cloudagent/didcomm_v2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .adapters import ResolverAdapter, SecretsAdapter


__all__ = ["ResolverAdapter", "SecretsAdapter"]
75 changes: 75 additions & 0 deletions aries_cloudagent/didcomm_v2/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Adapters for DMP library."""

import logging
from typing import Optional, cast
from aries_askar import Key
from didcomm_messaging import SecretsManager
from didcomm_messaging.crypto.backend.askar import AskarSecretKey
from didcomm_messaging.resolver import DIDResolver as DMPResolver

from ..core.error import BaseError
from ..core.profile import Profile, ProfileSession
from ..resolver.did_resolver import DIDResolver
from ..askar.profile import AskarProfileSession


LOGGER = logging.getLogger(__name__)


class DMPAdapterError(BaseError):
"""Raised on general errors from DMP Adapters."""


class ResolverAdapter(DMPResolver):
"""Adapter for ACA-Py resolver to DMP Resolver."""

def __init__(self, profile: Profile, resolver: DIDResolver):
"""Init the adapter."""
self.profile = profile
self.resolver = resolver

async def resolve(self, did: str) -> dict:
"""Resolve a DID."""
return await self.resolver.resolve(self.profile, did)

async def is_resolvable(self, did: str) -> bool:
"""Check to see if a DID is resolvable."""
for resolver in self.resolver.resolvers:
if await resolver.supports(self.profile, did):
return True

return False


class SecretsAdapterError(DMPAdapterError):
"""Errors from DMP Secrets Adapter."""


class SecretsAdapter(SecretsManager[AskarSecretKey]):
"""Adapter for providing a secrets manager compatible with DMP."""

def __init__(self, session: ProfileSession):
"""Init the adapter."""
self.session = session

async def get_secret_by_kid(self, kid: str) -> Optional[AskarSecretKey]:
"""Get a secret key by its ID."""
if not isinstance(self.session, AskarProfileSession):
raise SecretsAdapterError(
"ACA-Py's implementation of DMP only supports an Askar backend"
)

LOGGER.debug("GETTING SECRET BY KID: %s", kid)
key_entries = await self.session.handle.fetch_all_keys(
tag_filter={"kid": kid}, limit=2
)
if len(key_entries) > 1:
raise SecretsAdapterError(f"More than one key found with kid {kid}")

entry = key_entries[0]
if entry:
key = cast(Key, entry.key)
return AskarSecretKey(key=key, kid=kid)

LOGGER.debug("RETURNING NONE")
return None
14 changes: 14 additions & 0 deletions aries_cloudagent/messaging/v2_agent_message.py
TheTechmage marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""DIDComm V2 Agent message base class and schema."""

from .base_message import BaseMessage
from .models.base import BaseModel


class V2AgentMessage(BaseModel, BaseMessage):
"""DIDComm V2 message base class."""

class Meta:
"""DIDComm V2 message metadats."""

schema_class = None
message_type = None
Loading
Loading