From 7823ed2de609e823dc81087ed08339d3ce8b4bd7 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 2 Nov 2023 17:41:18 -0400 Subject: [PATCH] feat: add stubbed didcomm messaging and routing services Signed-off-by: Daniel Bluhm --- didcomm_messaging/__init__.py | 85 +++++++++++++++++++++++++++++++++- didcomm_messaging/packaging.py | 18 +++++-- didcomm_messaging/routing.py | 54 +++++++++++++++++++++ 3 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 didcomm_messaging/routing.py diff --git a/didcomm_messaging/__init__.py b/didcomm_messaging/__init__.py index abb5314..f1c07f0 100644 --- a/didcomm_messaging/__init__.py +++ b/didcomm_messaging/__init__.py @@ -1 +1,84 @@ -"""DIDComm Messaging implementation using Aries Askar.""" +"""DIDComm Messaging.""" +from dataclasses import dataclass +import json +from typing import Optional + +from pydid.service import DIDCommV2Service + +from didcomm_messaging.crypto import CryptoService, SecretsManager +from didcomm_messaging.packaging import PackagingService +from didcomm_messaging.resolver import DIDResolver +from didcomm_messaging.routing import RoutingService + + +@dataclass +class PackResult: + """Result of packing a message.""" + + message: bytes + target: str + + +@dataclass +class UnpackResult: + """Result of unpacking a message.""" + + message: dict + encrytped: bool + authenticated: bool + recipient_kid: str + sender_kid: Optional[str] = None + + +class DIDCommMessaging: + """Main entrypoint for DIDComm Messaging.""" + + def __init__( + self, + crypto: CryptoService, + secrets: SecretsManager, + resolver: DIDResolver, + packaging: PackagingService, + routing: RoutingService, + ): + """Initialize the DIDComm Messaging service.""" + self.crypto = crypto + self.secrets = secrets + self.resolver = resolver + self.packaging = packaging + self.routing = routing + + def service_to_target(self, service: DIDCommV2Service) -> str: + """Convert a service to a target uri. + + This is a very simple implementation that just returns the first one. + """ + if isinstance(service.service_endpoint, list): + service_endpoint = service.service_endpoint[0] + else: + service_endpoint = service.service_endpoint + + return service_endpoint.uri + + async def pack(self, message: dict, to: str, frm: Optional[str] = None, **options): + """Pack a message.""" + # TODO crypto layer permits packing to multiple recipients; should we as well? + + encoded_message = await self.packaging.pack( + json.dumps(message).encode(), [to], frm, **options + ) + + forward, service = await self.routing.prepare_forward(to, encoded_message) + return PackResult(forward, self.service_to_target(service)) + + async def unpack(self, encoded_message: bytes, **options) -> UnpackResult: + """Unpack a message.""" + unpacked, metadata = await self.packaging.unpack(encoded_message, **options) + message = json.loads(unpacked.decode()) + return UnpackResult( + message, + encrytped=bool(metadata.method), + authenticated=bool(metadata.sender_kid), + recipient_kid=metadata.recip_key.kid, + sender_kid=metadata.sender_kid, + ) diff --git a/didcomm_messaging/packaging.py b/didcomm_messaging/packaging.py index 9b82791..cef2192 100644 --- a/didcomm_messaging/packaging.py +++ b/didcomm_messaging/packaging.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from typing import Generic, Literal, Optional, Sequence, Union +from typing import Generic, Literal, Optional, Sequence, Tuple, Union from pydid import DIDUrl, VerificationMethod from didcomm_messaging.crypto import P, S, CryptoService, SecretsManager @@ -84,12 +84,17 @@ async def extract_packed_message_metadata( # noqa: C901 return PackedMessageMetadata(wrapper, method, recip_key, sender_kid) - async def unpack(self, enc_message: Union[str, bytes]) -> bytes: + async def unpack( + self, enc_message: Union[str, bytes] + ) -> Tuple[bytes, PackedMessageMetadata]: """Unpack a DIDComm message.""" metadata = await self.extract_packed_message_metadata(enc_message) if metadata.method == "ECDH-ES": - return await self.crypto.ecdh_es_decrypt(enc_message, metadata.recip_key) + return ( + await self.crypto.ecdh_es_decrypt(enc_message, metadata.recip_key), + metadata, + ) if not metadata.sender_kid: raise PackagingServiceError("Missing sender key ID") @@ -99,8 +104,11 @@ async def unpack(self, enc_message: Union[str, bytes]) -> bytes: ) sender_key = self.crypto.verification_method_to_public_key(sender_vm) - return await self.crypto.ecdh_1pu_decrypt( - enc_message, metadata.recip_key, sender_key + return ( + await self.crypto.ecdh_1pu_decrypt( + enc_message, metadata.recip_key, sender_key + ), + metadata, ) async def recip_for_kid_or_default_for_did(self, kid_or_did: str) -> P: diff --git a/didcomm_messaging/routing.py b/didcomm_messaging/routing.py new file mode 100644 index 0000000..85801a5 --- /dev/null +++ b/didcomm_messaging/routing.py @@ -0,0 +1,54 @@ +"""RoutingService interface.""" + +from typing import Tuple +from pydid.service import DIDCommV2Service +from didcomm_messaging.packaging import PackagingService +from didcomm_messaging.resolver import DIDResolver + + +class RoutingServiceError(Exception): + """Raised when an error occurs in the RoutingService.""" + + +class RoutingService: + """RoutingService.""" + + def __init__(self, packaging: PackagingService, resolver: DIDResolver): + """Initialize the RoutingService.""" + self.packaging = packaging + self.resolver = resolver + + async def _resolve_service(self, to: str) -> DIDCommV2Service: + """Resolve the service endpoint for a given DID.""" + doc = await self.resolver.resolve_and_parse(to) + if not doc.service: + raise RoutingServiceError(f"No service endpoint found for {to}") + + first_didcomm_service = next( + ( + service + for service in doc.service + if isinstance(service, DIDCommV2Service) + ), + None, + ) + if not first_didcomm_service: + raise RoutingServiceError(f"No DIDCommV2 service endpoint found for {to}") + + return first_didcomm_service + + async def prepare_forward( + self, to: str, encoded_message: bytes + ) -> Tuple[bytes, DIDCommV2Service]: + """Prepare a forward message, if necessary. + + Args: + to (str): The recipient of the message. This will be a DID. + encoded_message (bytes): The encoded message. + + Returns: + The encoded message, and the service endpoint to forward to. + """ + service = await self._resolve_service(to) + # TODO Do the stuff + return encoded_message, service