diff --git a/didcomm_messaging/__init__.py b/didcomm_messaging/__init__.py index 2f5f1af..4be60e5 100644 --- a/didcomm_messaging/__init__.py +++ b/didcomm_messaging/__init__.py @@ -1,170 +1,20 @@ """DIDComm Messaging.""" -from dataclasses import dataclass -import json -from typing import Generic, Optional, List - -from pydid.service import DIDCommV2Service - -from didcomm_messaging.crypto import CryptoService, SecretsManager, P, S +from didcomm_messaging.crypto import CryptoService, P, S, SecretsManager +from didcomm_messaging.messaging import DIDCommMessaging, DIDCommMessagingService 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_services: List[DIDCommV2Service] - - def get_endpoint(self, protocol: str) -> str: - """Get the first matching endpoint to send the message to.""" - return self.get_service(protocol).service_endpoint.uri - - def get_service(self, protocol: str) -> DIDCommV2Service: - """Get the first matching service to send the message to.""" - return self.filter_services_by_protocol(protocol)[0] - - def filter_services_by_protocol(self, protocol: str) -> List[DIDCommV2Service]: - """Get all services that start with a specific uri protocol.""" - return [ - service - for service in self.target_services - if service.service_endpoint.uri.startswith(protocol) - ] - - -@dataclass -class UnpackResult: - """Result of unpacking a message.""" - - message: dict - encrytped: bool - authenticated: bool - recipient_kid: str - sender_kid: Optional[str] = None - - -class DIDCommMessagingService(Generic[P, S]): - """Main entrypoint for DIDComm Messaging.""" - - 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, - crypto: CryptoService[P, S], - resolver: DIDResolver, - secrets: SecretsManager[S], - packaging: PackagingService[P, S], - routing: RoutingService, - 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 packaging.pack( - crypto, - resolver, - secrets, - json.dumps(message).encode(), - [to], - frm, - **options, - ) - - forward, services = await routing.prepare_forward( - crypto, packaging, resolver, secrets, to, encoded_message - ) - return PackResult(forward, services) - - async def unpack( - self, - crypto: CryptoService[P, S], - resolver: DIDResolver, - secrets: SecretsManager[S], - packaging: PackagingService[P, S], - encoded_message: bytes, - **options, - ) -> UnpackResult: - """Unpack a message.""" - unpacked, metadata = await packaging.unpack( - crypto, resolver, secrets, 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, - ) - - -class DIDCommMessaging(Generic[P, S]): - """Main entrypoint for DIDComm Messaging.""" - - def __init__( - self, - crypto: CryptoService[P, S], - secrets: SecretsManager[S], - resolver: DIDResolver, - packaging: PackagingService[P, S], - routing: RoutingService, - ): - """Initialize the DIDComm Messaging service.""" - self.crypto = crypto - self.secrets = secrets - self.resolver = resolver - self.packaging = packaging - self.routing = routing - self.dmp = DIDCommMessagingService() - - async def pack( - self, - message: dict, - to: str, - frm: Optional[str] = None, - **options, - ) -> PackResult: - """Pack a message.""" - return await self.dmp.pack( - self.crypto, - self.resolver, - self.secrets, - self.packaging, - self.routing, - message, - to, - frm, - **options, - ) - - async def unpack( - self, - encoded_message: bytes, - **options, - ) -> UnpackResult: - """Unpack a message.""" - return await self.dmp.unpack( - self.crypto, - self.resolver, - self.secrets, - self.packaging, - encoded_message, - **options, - ) +__all__ = [ + "CryptoService", + "DIDCommMessaging", + "DIDCommMessagingService", + "DIDResolver", + "P", + "PackagingService", + "RoutingService", + "S", + "SecretsManager", +] diff --git a/didcomm_messaging/legacy/__init__.py b/didcomm_messaging/legacy/__init__.py new file mode 100644 index 0000000..3e75325 --- /dev/null +++ b/didcomm_messaging/legacy/__init__.py @@ -0,0 +1,10 @@ +"""Legacy DIDComm v1 Interfaces. + +These components are intended to provide a similar structure to the DIDComm v2 +interfaces provided by this library. While the community is transitioning from +DIDComm v1 to v2, having a consistent interface to interact with will help +implementers to support both versions until the transition is complete. + +It is expected that a future version of this library will eventually remove +these interfaces. +""" diff --git a/didcomm_messaging/legacy/askar.py b/didcomm_messaging/legacy/askar.py index 2cabca3..e6378d4 100644 --- a/didcomm_messaging/legacy/askar.py +++ b/didcomm_messaging/legacy/askar.py @@ -4,6 +4,7 @@ from typing import Optional, Sequence, Tuple, cast from base58 import b58decode +from pydid import VerificationMethod from didcomm_messaging.crypto.jwe import JweBuilder, JweEnvelope, JweRecipient from didcomm_messaging.legacy.base import ( @@ -30,6 +31,11 @@ def kid_to_public_key(self, kid: str) -> AskarKey: """ return AskarKey(Key.from_public_bytes(KeyAlg.ED25519, b58decode(kid)), kid) + @classmethod + def verification_method_to_public_key(cls, vm: VerificationMethod) -> AskarKey: + """Convert a verification method to a public key.""" + return AskarKey.from_verification_method(vm) + async def pack_message( self, to_verkeys: Sequence[AskarKey], diff --git a/didcomm_messaging/legacy/base.py b/didcomm_messaging/legacy/base.py index 6edbee1..5bbd9bb 100644 --- a/didcomm_messaging/legacy/base.py +++ b/didcomm_messaging/legacy/base.py @@ -2,6 +2,8 @@ from abc import ABC, abstractmethod from typing import Generic, NamedTuple, Optional, Sequence + +from pydid import VerificationMethod from didcomm_messaging.crypto.base import P, S from didcomm_messaging.crypto.jwe import JweEnvelope from didcomm_messaging.multiformats.multibase import Base64UrlEncoder @@ -36,6 +38,11 @@ def kid_to_public_key(self, kid: str) -> P: In DIDComm v1, kids are the base58 encoded keys. """ + @classmethod + @abstractmethod + def verification_method_to_public_key(cls, vm: VerificationMethod) -> P: + """Convert a verification method to a public key.""" + @abstractmethod async def pack_message( self, to_verkeys: Sequence[P], from_key: Optional[S], message: bytes diff --git a/didcomm_messaging/legacy/messaging.py b/didcomm_messaging/legacy/messaging.py new file mode 100644 index 0000000..870d227 --- /dev/null +++ b/didcomm_messaging/legacy/messaging.py @@ -0,0 +1,245 @@ +"""Legacy messaging service.""" + +from dataclasses import dataclass +import json +from typing import Generic, Optional, Sequence, Union + +from pydantic import AnyUrl +from pydid import VerificationMethod +from pydid.service import DIDCommV1Service + +from didcomm_messaging.crypto import SecretsManager, P, S +from didcomm_messaging.legacy.base import LegacyCryptoService +from didcomm_messaging.legacy.packaging import LegacyPackagingService +from didcomm_messaging.resolver import DIDResolver + + +class LegacyDIDCommMessagingError(Exception): + """Raised on error in legacy didcomm messaging.""" + + +@dataclass +class LegacyPackResult: + """Result of packing a message.""" + + message: bytes + target_service: str + + +@dataclass +class LegacyUnpackResult: + """Result of unpacking a message.""" + + unpacked: bytes + encrytped: bool + authenticated: bool + recipient_kid: str + sender_kid: Optional[str] = None + + @property + def message(self) -> dict: + """Return unpacked value as a dict. + + This value is used to preserve backwards compatibility. + """ + return json.loads(self.unpacked) + + +@dataclass +class Target: + """Recipient info for sending a message.""" + + recipient_keys: Sequence[str] + routing_keys: Sequence[str] + endpoint: str + + +class LegacyDIDCommMessagingService(Generic[P, S]): + """Main entrypoint for DIDComm Messaging.""" + + async def did_to_target( + self, crypto: LegacyCryptoService[P, S], resolver: DIDResolver, did: str + ) -> Target: + """Resolve recipient information from a DID.""" + doc = await resolver.resolve_and_parse(did) + services = [ + service + for service in doc.service or [] + if isinstance(service, DIDCommV1Service) + ] + if not services: + raise LegacyDIDCommMessagingError(f"Unable to send message to DID {did}") + target = services[0] + + recipient_keys = [ + crypto.verification_method_to_public_key( + doc.dereference_as(VerificationMethod, recip) + ).kid + for recip in target.recipient_keys + ] + routing_keys = [ + crypto.verification_method_to_public_key( + doc.dereference_as(VerificationMethod, routing_key) + ).kid + for routing_key in target.routing_keys + ] + endpoint = target.service_endpoint + if isinstance(endpoint, AnyUrl): + endpoint = str(endpoint) + if not endpoint.startswith("http") or not endpoint.startswith("ws"): + raise LegacyDIDCommMessagingError( + f"Unable to send message to endpoint {endpoint}" + ) + + return Target(recipient_keys, routing_keys, endpoint) + + def forward_wrap(self, to: str, msg: str) -> bytes: + """Wrap a message in a forward.""" + forward = { + "@type": "https://didcomm.org/routing/1.0/forward", + "to": to, + "msg": msg, + } + return json.dumps(forward, separators=(",", ":")).encode() + + async def pack( + self, + crypto: LegacyCryptoService[P, S], + resolver: DIDResolver, + secrets: SecretsManager[S], + packaging: LegacyPackagingService[P, S], + message: Union[dict, str, bytes], + to: str, + frm: Optional[str] = None, + **options, + ): + """Pack a message. + + Args: + crypto: crytpo service to use to pack the message + resolver: resolver to use to resolve DIDs + secrets: secrets manager to use to look up private key material + packaging: packaging service + routing: routing service + message: to send + to: recipient of the message, expressed as a DID + frm: the sender of the message, expressed as a DID + options: arbitrary values to pass to the packaging service + + Returns: + PackResult with packed message and target services + + """ + if isinstance(message, str): + message = message.encode() + elif isinstance(message, dict): + message = json.dumps(message, separators=(",", ":")).encode() + elif isinstance(message, bytes): + pass + else: + raise TypeError("message must be bytes, str, or dict") + + target = await self.did_to_target(crypto, resolver, to) + + encoded_message = await packaging.pack( + crypto, + secrets, + message, + target.recipient_keys, + frm, + **options, + ) + + if target.routing_keys: + forward_to = target.recipient_keys[0] + for routing_key in target.routing_keys: + encoded_message = await packaging.pack( + crypto, + secrets, + self.forward_wrap(forward_to, encoded_message.to_json()), + [routing_key], + ) + forward_to = routing_key + + return LegacyPackResult(encoded_message.to_json().encode(), target.endpoint) + + async def unpack( + self, + crypto: LegacyCryptoService[P, S], + secrets: SecretsManager[S], + packaging: LegacyPackagingService[P, S], + encoded_message: bytes, + **options, + ) -> LegacyUnpackResult: + """Unpack a message.""" + unpacked, recip, sender = await packaging.unpack(crypto, secrets, encoded_message) + return LegacyUnpackResult( + unpacked, + encrytped=bool(recip), + authenticated=bool(sender), + recipient_kid=recip, + sender_kid=sender, + ) + + +class LegacyDIDCommMessaging(Generic[P, S]): + """Main entrypoint for DIDComm Messaging.""" + + def __init__( + self, + crypto: LegacyCryptoService[P, S], + secrets: SecretsManager[S], + resolver: DIDResolver, + packaging: LegacyPackagingService[P, S], + ): + """Initialize the DIDComm Messaging service.""" + self.crypto = crypto + self.secrets = secrets + self.resolver = resolver + self.packaging = packaging + self.dmp = LegacyDIDCommMessagingService() + + async def pack( + self, + message: Union[dict, str, bytes], + to: str, + frm: Optional[str] = None, + **options, + ) -> LegacyPackResult: + """Pack a message. + + Args: + message: to send + to: recipient of the message, expressed as a KID which is a Base58 + encoded Ed25519 public key + frm: the sender of the message, expressed as a KID which is a Base58 + encoded Ed25519 public key + options: arbitrary values to pass to the packaging service + + Returns: + LegacyPackResult with packed message and target services + """ + return await self.dmp.pack( + self.crypto, + self.resolver, + self.secrets, + self.packaging, + message, + to, + frm, + **options, + ) + + async def unpack( + self, + encoded_message: bytes, + **options, + ) -> LegacyUnpackResult: + """Unpack a message.""" + return await self.dmp.unpack( + self.crypto, + self.secrets, + self.packaging, + encoded_message, + **options, + ) diff --git a/didcomm_messaging/legacy/nacl.py b/didcomm_messaging/legacy/nacl.py index 6b3db32..e63dd6b 100644 --- a/didcomm_messaging/legacy/nacl.py +++ b/didcomm_messaging/legacy/nacl.py @@ -80,6 +80,11 @@ def kid_to_public_key(self, kid: str): """ return EdPublicKey(base58.b58decode(kid)) + @classmethod + def verification_method_to_public_key(cls, vm: VerificationMethod) -> EdPublicKey: + """Convert a verification method to a public key.""" + return EdPublicKey.from_verification_method(vm) + async def pack_message( self, to_verkeys: Sequence[EdPublicKey], diff --git a/didcomm_messaging/messaging.py b/didcomm_messaging/messaging.py new file mode 100644 index 0000000..3a98106 --- /dev/null +++ b/didcomm_messaging/messaging.py @@ -0,0 +1,213 @@ +"""DIDComm Messaging Service.""" + +from dataclasses import dataclass +import json +from typing import Generic, Optional, List, Union + +from pydid.service import DIDCommV2Service + +from didcomm_messaging.crypto import CryptoService, SecretsManager, P, S +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_services: List[DIDCommV2Service] + + def get_endpoint(self, protocol: str) -> str: + """Get the first matching endpoint to send the message to.""" + return self.get_service(protocol).service_endpoint.uri + + def get_service(self, protocol: str) -> DIDCommV2Service: + """Get the first matching service to send the message to.""" + return self.filter_services_by_protocol(protocol)[0] + + def filter_services_by_protocol(self, protocol: str) -> List[DIDCommV2Service]: + """Get all services that start with a specific uri protocol.""" + return [ + service + for service in self.target_services + if service.service_endpoint.uri.startswith(protocol) + ] + + +@dataclass +class UnpackResult: + """Result of unpacking a message.""" + + unpacked: bytes + encrytped: bool + authenticated: bool + recipient_kid: str + sender_kid: Optional[str] = None + + @property + def message(self) -> dict: + """Return unpacked value as a dict. + + This value is used to preserve backwards compatibility. + """ + return json.loads(self.unpacked) + + +class DIDCommMessagingService(Generic[P, S]): + """Main entrypoint for DIDComm Messaging.""" + + 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, + crypto: CryptoService[P, S], + resolver: DIDResolver, + secrets: SecretsManager[S], + packaging: PackagingService[P, S], + routing: RoutingService, + message: Union[dict, str, bytes], + to: str, + frm: Optional[str] = None, + **options, + ): + """Pack a message. + + Args: + crypto: crytpo service to use to pack the message + resolver: resolver to use to resolve DIDs + secrets: secrets manager to use to look up private key material + packaging: packaging service + routing: routing service + message: to send + to: recipient of the message, expressed as a DID or a DID URL to a + verification method + frm: the sender of the message, expressed as a DID or a DID URL to a + verification method + options: arbitrary values to pass to the packaging service + + Returns: + PackResult with packed message and target services + """ + if isinstance(message, str): + message = message.encode() + elif isinstance(message, dict): + message = json.dumps(message, separators=(",", ":")).encode() + elif isinstance(message, bytes): + pass + else: + raise TypeError("message must be bytes, str, or dict") + + encoded_message = await packaging.pack( + crypto, + resolver, + secrets, + message, + [to], + frm, + **options, + ) + + forward, services = await routing.prepare_forward( + crypto, packaging, resolver, secrets, to, encoded_message + ) + return PackResult(forward, services) + + async def unpack( + self, + crypto: CryptoService[P, S], + resolver: DIDResolver, + secrets: SecretsManager[S], + packaging: PackagingService[P, S], + encoded_message: bytes, + **options, + ) -> UnpackResult: + """Unpack a message.""" + unpacked, metadata = await packaging.unpack( + crypto, resolver, secrets, encoded_message, **options + ) + return UnpackResult( + unpacked, + encrytped=bool(metadata.method), + authenticated=bool(metadata.sender_kid), + recipient_kid=metadata.recip_key.kid, + sender_kid=metadata.sender_kid, + ) + + +class DIDCommMessaging(Generic[P, S]): + """Main entrypoint for DIDComm Messaging.""" + + def __init__( + self, + crypto: CryptoService[P, S], + secrets: SecretsManager[S], + resolver: DIDResolver, + packaging: PackagingService[P, S], + routing: RoutingService, + ): + """Initialize the DIDComm Messaging service.""" + self.crypto = crypto + self.secrets = secrets + self.resolver = resolver + self.packaging = packaging + self.routing = routing + self.dmp = DIDCommMessagingService() + + async def pack( + self, + message: Union[dict, str, bytes], + to: str, + frm: Optional[str] = None, + **options, + ) -> PackResult: + """Pack a message. + + Args: + message: to send + to: recipient of the message, expressed as a DID or a DID URL to a + verification method + frm: the sender of the message, expressed as a DID or a DID URL to a + verification method + options: arbitrary values to pass to the packaging service + + Returns: + PackResult with packed message and target services + """ + return await self.dmp.pack( + self.crypto, + self.resolver, + self.secrets, + self.packaging, + self.routing, + message, + to, + frm, + **options, + ) + + async def unpack( + self, + encoded_message: bytes, + **options, + ) -> UnpackResult: + """Unpack a message.""" + return await self.dmp.unpack( + self.crypto, + self.resolver, + self.secrets, + self.packaging, + encoded_message, + **options, + )