From 06a293b30e12063ebcff1028cbe85e9db5bf57bb Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 16 Nov 2023 23:21:45 -0500 Subject: [PATCH] feat: add did:peer:1 resolution support Resolves did:peer:1 received in did exchange Signed-off-by: Daniel Bluhm --- aries_cloudagent/connections/base_manager.py | 41 +++--- .../connections/models/conn_record.py | 15 +- .../protocols/didexchange/v1_0/manager.py | 9 +- aries_cloudagent/resolver/__init__.py | 6 + .../resolver/default/legacy_peer.py | 2 +- aries_cloudagent/resolver/default/peer1.py | 132 ++++++++++++++++++ 6 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 aries_cloudagent/resolver/default/peer1.py diff --git a/aries_cloudagent/connections/base_manager.py b/aries_cloudagent/connections/base_manager.py index f40a670ae8..f07064b631 100644 --- a/aries_cloudagent/connections/base_manager.py +++ b/aries_cloudagent/connections/base_manager.py @@ -3,6 +3,7 @@ For Connection, DIDExchange and OutOfBand Manager. """ +import json import logging from typing import List, Optional, Sequence, Text, Tuple, Union @@ -132,35 +133,35 @@ async def create_did_document( return did_doc - async def store_did_document(self, did_doc: DIDDoc): + async def store_did_document(self, value: Union[DIDDoc, dict]): """Store a DID document. Args: - did_doc: The `DIDDoc` instance to persist + value: The `DIDDoc` instance to persist """ - assert did_doc.did + if isinstance(value, DIDDoc): + did = value.did + doc = value.to_json() + else: + did = value["id"] + doc = json.dumps(value) + + self._logger.debug("Storing DID document for %s: %s", did, doc) try: - stored_doc, record = await self.fetch_did_document(did_doc.did) + stored_doc, record = await self.fetch_did_document(did) except StorageNotFoundError: - record = StorageRecord( - self.RECORD_TYPE_DID_DOC, - did_doc.to_json(), - {"did": did_doc.did}, - ) + record = StorageRecord(self.RECORD_TYPE_DID_DOC, doc, {"did": did}) async with self._profile.session() as session: storage: BaseStorage = session.inject(BaseStorage) await storage.add_record(record) else: async with self._profile.session() as session: storage: BaseStorage = session.inject(BaseStorage) - await storage.update_record( - record, did_doc.to_json(), {"did": did_doc.did} - ) - await self.remove_keys_for_did(did_doc.did) - for key in did_doc.pubkey.values(): - if key.controller == did_doc.did: - await self.add_key_for_did(did_doc.did, key.value) + await storage.update_record(record, doc, {"did": did}) + + await self.remove_keys_for_did(did) + await self.record_did(did) async def add_key_for_did(self, did: str, key: str): """Store a verkey for lookup against a DID. @@ -219,12 +220,12 @@ async def resolve_didcomm_services( doc: ResolvedDocument = pydid.deserialize_document(doc_dict, strict=True) except ResolverError as error: raise BaseConnectionManagerError( - "Failed to resolve public DID in invitation" + "Failed to resolve DID services" ) from error if not doc.service: raise BaseConnectionManagerError( - "Cannot connect via public DID that has no associated services" + "Cannot connect via DID that has no associated services" ) didcomm_services = sorted( @@ -617,7 +618,7 @@ def diddoc_connection_targets( ) return targets - async def fetch_did_document(self, did: str) -> Tuple[DIDDoc, StorageRecord]: + async def fetch_did_document(self, did: str) -> Tuple[dict, StorageRecord]: """Retrieve a DID Document for a given DID. Args: @@ -627,7 +628,7 @@ async def fetch_did_document(self, did: str) -> Tuple[DIDDoc, StorageRecord]: async with self._profile.session() as session: storage = session.inject(BaseStorage) record = await storage.find_record(self.RECORD_TYPE_DID_DOC, {"did": did}) - return DIDDoc.from_json(record.value), record + return json.loads(record.value), record async def find_connection( self, diff --git a/aries_cloudagent/connections/models/conn_record.py b/aries_cloudagent/connections/models/conn_record.py index 24fc2fbf34..dfcebf5476 100644 --- a/aries_cloudagent/connections/models/conn_record.py +++ b/aries_cloudagent/connections/models/conn_record.py @@ -9,8 +9,8 @@ from ...core.profile import ProfileSession from ...messaging.models.base_record import BaseRecord, BaseRecordSchema from ...messaging.valid import ( - INDY_DID_EXAMPLE, - INDY_DID_VALIDATE, + GENERIC_DID_EXAMPLE, + GENERIC_DID_VALIDATE, INDY_RAW_PUBLIC_KEY_EXAMPLE, INDY_RAW_PUBLIC_KEY_VALIDATE, UUID4_EXAMPLE, @@ -653,15 +653,18 @@ class Meta: ) my_did = fields.Str( required=False, - validate=INDY_DID_VALIDATE, - metadata={"description": "Our DID for connection", "example": INDY_DID_EXAMPLE}, + validate=GENERIC_DID_VALIDATE, + metadata={ + "description": "Our DID for connection", + "example": GENERIC_DID_EXAMPLE, + }, ) their_did = fields.Str( required=False, - validate=INDY_DID_VALIDATE, + validate=GENERIC_DID_VALIDATE, metadata={ "description": "Their DID for connection", - "example": INDY_DID_EXAMPLE, + "example": GENERIC_DID_EXAMPLE, }, ) their_label = fields.Str( diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index 1f4bc3a590..36404f41d3 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -10,7 +10,6 @@ from ....connections.base_manager import BaseConnectionManager from ....connections.models.conn_record import ConnRecord -from ....connections.models.diddoc import DIDDoc from ....core.error import BaseError from ....core.oob_processor import OobMessageProcessor from ....core.profile import Profile @@ -485,11 +484,11 @@ async def receive_request( wallet = session.inject(BaseWallet) conn_did_doc = await self.verify_diddoc(wallet, request.did_doc_attach) await self.store_did_document(conn_did_doc) - if request.did != conn_did_doc.did: + if request.did != conn_did_doc["id"]: raise DIDXManagerError( ( f"Connection DID {request.did} does not match " - f"DID Doc id {conn_did_doc.did}" + f"DID Doc id {conn_did_doc['id']}" ), error_code=ProblemReportReason.REQUEST_NOT_ACCEPTED.value, ) @@ -932,7 +931,7 @@ async def verify_diddoc( wallet: BaseWallet, attached: AttachDecorator, invi_key: str = None, - ) -> DIDDoc: + ) -> dict: """Verify DIDDoc attachment and return signed data.""" signed_diddoc_bytes = attached.data.signed if not signed_diddoc_bytes: @@ -940,7 +939,7 @@ async def verify_diddoc( if not await attached.data.verify(wallet, invi_key): raise DIDXManagerError("DID doc attachment signature failed verification") - return DIDDoc.deserialize(json.loads(signed_diddoc_bytes.decode())) + return json.loads(signed_diddoc_bytes.decode()) async def get_resolved_did_document(self, qualified_did: str) -> ResolvedDocument: """Return resolved DID document.""" diff --git a/aries_cloudagent/resolver/__init__.py b/aries_cloudagent/resolver/__init__.py index a72b017dbe..3b83e466f9 100644 --- a/aries_cloudagent/resolver/__init__.py +++ b/aries_cloudagent/resolver/__init__.py @@ -51,6 +51,12 @@ async def setup(context: InjectionContext): await universal_resolver.setup(context) registry.register_resolver(universal_resolver) + peer_did_1_resolver = ClassProvider( + "aries_cloudagent.resolver.default.peer1.PeerDID1Resolver" + ).provide(context.settings, context.injector) + await peer_did_1_resolver.setup(context) + registry.register_resolver(peer_did_1_resolver) + peer_did_2_resolver = ClassProvider( "aries_cloudagent.resolver.default.peer2.PeerDID2Resolver" ).provide(context.settings, context.injector) diff --git a/aries_cloudagent/resolver/default/legacy_peer.py b/aries_cloudagent/resolver/default/legacy_peer.py index 3b012adede..623afff3b2 100644 --- a/aries_cloudagent/resolver/default/legacy_peer.py +++ b/aries_cloudagent/resolver/default/legacy_peer.py @@ -282,7 +282,7 @@ async def _fetch_did_document(self, profile: Profile, did: str) -> RetrieveResul try: doc, _ = await conn_mgr.fetch_did_document(did) LOGGER.debug("Fetched doc %s", doc) - to_cache = RetrieveResult(True, doc=doc.serialize()) + to_cache = RetrieveResult(True, doc=doc) except StorageNotFoundError: LOGGER.debug("Failed to fetch doc for did %s", did) to_cache = RetrieveResult(False) diff --git a/aries_cloudagent/resolver/default/peer1.py b/aries_cloudagent/resolver/default/peer1.py new file mode 100644 index 0000000000..8f1c928759 --- /dev/null +++ b/aries_cloudagent/resolver/default/peer1.py @@ -0,0 +1,132 @@ +"""did:peer:1 resolver implementation.""" + +import logging +import re +from typing import Callable, Optional, Pattern, Sequence, Text, Union + +from aries_cloudagent.messaging.valid import B58 + +from ...config.injection_context import InjectionContext +from ...connections.base_manager import BaseConnectionManager +from ...core.profile import Profile +from ...resolver.base import BaseDIDResolver, DIDNotFound, ResolverType +from ...storage.error import StorageNotFoundError + + +LOGGER = logging.getLogger(__name__) + + +# TODO Copy pasted from did-peer-4, reuse when available +def _operate_on_embedded( + visitor: Callable[[dict], dict] +) -> Callable[[Union[dict, str]], Union[dict, str]]: + """Return an adapter function that turns a vm visitor into a vm | ref visitor. + + The adapter function calls a visitor on embedded vms but just returns on refs. + """ + + def _adapter(vm: Union[dict, str]) -> Union[dict, str]: + if isinstance(vm, dict): + return visitor(vm) + return vm + + return _adapter + + +def _visit_verification_methods(document: dict, visitor: Callable[[dict], dict]): + """Visit all verification methods in a document. + + This includes the main verificationMethod list as well as verification + methods embedded in relationships. + """ + verification_methods = document.get("verificationMethod") + if verification_methods: + document["verificationMethod"] = [visitor(vm) for vm in verification_methods] + + for relationship in ( + "authentication", + "assertionMethod", + "keyAgreement", + "capabilityInvocation", + "capabilityDelegation", + ): + vms_and_refs = document.get(relationship) + if vms_and_refs: + document[relationship] = [ + _operate_on_embedded(visitor)(vm) for vm in vms_and_refs + ] + + return document + + +def contextualize(did: str, document: dict): + """Contextualize a peer DID document.""" + + def _contextualize_verification_method(vm: dict): + """Contextualize a verification method.""" + if vm["controller"] == "#id": + vm["controller"] = did + if vm["id"].startswith("#"): + vm["id"] = f"{did}{vm['id']}" + return vm + + document = _visit_verification_methods(document, _contextualize_verification_method) + + for service in document.get("service", []): + if service["id"].startswith("#"): + service["id"] = f"{did}{service['id']}" + + return document + + +class PeerDID1Resolver(BaseDIDResolver): + """Resolve legacy peer DIDs.""" + + PEER1_PATTERN = re.compile(rf"^did:peer:1zQm[{B58}]{{44}}$") + + def __init__(self): + """Initialize the resolver instance.""" + super().__init__(ResolverType.NATIVE) + + async def setup(self, context: InjectionContext): + """Perform required setup for the resolver.""" + + @property + def supported_did_regex(self) -> Pattern: + """Return supported_did_regex of DID Peer 1 Resolver.""" + return self.PEER1_PATTERN + + async def _fetch_did_document(self, profile: Profile, did: str) -> Optional[dict]: + """Fetch DID from wallet if available. + + This is the method to be used with fetch_did_document to enable caching. + """ + conn_mgr = BaseConnectionManager(profile) + try: + doc, _ = await conn_mgr.fetch_did_document(did) + LOGGER.debug("Fetched doc %s", doc) + return doc + except StorageNotFoundError: + LOGGER.debug("Failed to fetch doc for did %s", did) + + return None + + async def _resolve( + self, + profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: + """Resolve Legacy Peer DID to a DID document by fetching from the wallet. + + By the time this resolver is selected, it should be impossible for it + to raise a DIDNotFound. + """ + result = await self._fetch_did_document(profile, did) + if result: + # Apply corrections? + result = contextualize(did, result) + LOGGER.debug("Resolved %s to %s", did, result) + return result + else: + raise DIDNotFound(f"DID not found: {did}")