Skip to content

Commit

Permalink
feat: add did:peer:1 resolution support
Browse files Browse the repository at this point in the history
Resolves did:peer:1 received in did exchange

Signed-off-by: Daniel Bluhm <[email protected]>
  • Loading branch information
dbluhm committed Nov 17, 2023
1 parent 3e0fec6 commit 06a293b
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 32 deletions.
41 changes: 21 additions & 20 deletions aries_cloudagent/connections/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
For Connection, DIDExchange and OutOfBand Manager.
"""

import json
import logging
from typing import List, Optional, Sequence, Text, Tuple, Union

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down
15 changes: 9 additions & 6 deletions aries_cloudagent/connections/models/conn_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 4 additions & 5 deletions aries_cloudagent/protocols/didexchange/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -932,15 +931,15 @@ 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:
raise DIDXManagerError("DID doc attachment is not signed.")
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."""
Expand Down
6 changes: 6 additions & 0 deletions aries_cloudagent/resolver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion aries_cloudagent/resolver/default/legacy_peer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
132 changes: 132 additions & 0 deletions aries_cloudagent/resolver/default/peer1.py
Original file line number Diff line number Diff line change
@@ -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}")

0 comments on commit 06a293b

Please sign in to comment.