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

refactor: use did-peer-2 instead of peerdid #2561

Merged
merged 3 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions aries_cloudagent/connections/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class BaseConnectionManagerError(BaseError):
class BaseConnectionManager:
"""Class to provide utilities regarding connection_targets."""

RECORD_TYPE_DID_DOC = "did_doc" # legacy
RECORD_TYPE_DID_DOC = "did_doc"
RECORD_TYPE_DID_KEY = "did_key"

def __init__(self, profile: Profile):
Expand Down Expand Up @@ -291,8 +291,8 @@ async def resolve_invitation(
[self._extract_key_material_in_base58_format(key) for key in routing_keys],
)

async def record_keys_for_public_did(self, did: str):
"""Record the keys for a public DID.
async def record_did(self, did: str):
"""Record DID for later use.

This is required to correlate sender verkeys back to a connection.
"""
Expand Down
8 changes: 4 additions & 4 deletions aries_cloudagent/connections/tests/test_base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,7 @@ async def test_resolve_connection_targets_x_unsupported_key(self):
await self.manager.resolve_connection_targets(did)
assert "not supported" in str(cm.exception)

async def test_record_keys_for_public_did_empty(self):
async def test_record_did_empty(self):
did = "did:sov:" + self.test_did
service_builder = ServiceBuilder(DID(did))
service_builder.add_didcomm(
Expand All @@ -1154,9 +1154,9 @@ async def test_record_keys_for_public_did_empty(self):
self.manager.resolve_didcomm_services = async_mock.CoroutineMock(
return_value=(DIDDocument(id=DID(did)), service_builder.services)
)
await self.manager.record_keys_for_public_did(did)
await self.manager.record_did(did)

async def test_record_keys_for_public_did(self):
async def test_record_did(self):
did = "did:sov:" + self.test_did
doc_builder = DIDDocumentBuilder(did)
vm = doc_builder.verification_method.add(
Expand All @@ -1170,7 +1170,7 @@ async def test_record_keys_for_public_did(self):
self.manager.resolve_didcomm_services = async_mock.CoroutineMock(
return_value=(doc, doc.service)
)
await self.manager.record_keys_for_public_did(did)
await self.manager.record_did(did)

async def test_diddoc_connection_targets_diddoc_underspecified(self):
with self.assertRaises(BaseConnectionManagerError):
Expand Down
4 changes: 2 additions & 2 deletions aries_cloudagent/protocols/didexchange/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ async def receive_request(
self._logger.debug(
"No DID Doc attachment in request; doc will be resolved from DID"
)
await self.record_keys_for_public_did(request.did)
await self.record_did(request.did)

if conn_rec: # request is against explicit invitation
auto_accept = (
Expand Down Expand Up @@ -773,7 +773,7 @@ async def accept_response(
self._logger.debug(
"No DID Doc attachment in response; doc will be resolved from DID"
)
await self.record_keys_for_public_did(response.did)
await self.record_did(response.did)

conn_rec.their_did = their_did
conn_rec.state = ConnRecord.State.RESPONSE.rfc160
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,8 +726,8 @@ async def test_receive_request_public_did_no_did_doc_attachment(self):
), async_mock.patch.object(
self.manager, "create_did_document", async_mock.CoroutineMock()
) as mock_create_did_doc, async_mock.patch.object(
self.manager, "record_keys_for_public_did", async_mock.CoroutineMock()
) as mock_record_keys_for_public_did, async_mock.patch.object(
self.manager, "record_did", async_mock.CoroutineMock()
), async_mock.patch.object(
MediationManager, "prepare_request", autospec=True
) as mock_mediation_mgr_prep_req:
mock_create_did_doc.return_value = async_mock.MagicMock(
Expand Down Expand Up @@ -1957,8 +1957,8 @@ async def test_accept_response_find_by_thread_id_no_did_doc_attached(self):
) as mock_conn_retrieve_by_id, async_mock.patch.object(
DIDDoc, "deserialize", async_mock.MagicMock()
) as mock_did_doc_deser, async_mock.patch.object(
self.manager, "record_keys_for_public_did", async_mock.CoroutineMock()
) as mock_record_keys_for_public_did:
self.manager, "record_did", async_mock.CoroutineMock()
):
mock_did_doc_deser.return_value = async_mock.MagicMock(
did=TestConfig.test_target_did
)
Expand Down Expand Up @@ -1998,8 +1998,8 @@ async def test_accept_response_find_by_thread_id_no_did_doc_attached_no_did(self
) as mock_conn_retrieve_by_id, async_mock.patch.object(
DIDDoc, "deserialize", async_mock.MagicMock()
) as mock_did_doc_deser, async_mock.patch.object(
self.manager, "record_keys_for_public_did", async_mock.CoroutineMock()
) as mock_record_keys_for_public_did:
self.manager, "record_did", async_mock.CoroutineMock()
):
mock_did_doc_deser.return_value = async_mock.MagicMock(
did=TestConfig.test_target_did
)
Expand Down
55 changes: 7 additions & 48 deletions aries_cloudagent/resolver/default/peer2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@
Resolution is performed using the peer-did-python library https://github.com/sicpa-dlab/peer-did-python.
"""

from typing import Optional, Pattern, Sequence, Text, Union
from typing import Optional, Pattern, Sequence, Text

from peerdid.dids import (
is_peer_did,
PEER_DID_PATTERN,
resolve_peer_did,
DID,
DIDDocument,
)
from did_peer_2 import resolve, PATTERN

from ...config.injection_context import InjectionContext
from ...core.profile import Profile
Expand All @@ -32,7 +26,7 @@ async def setup(self, context: InjectionContext):
@property
def supported_did_regex(self) -> Pattern:
"""Return supported_did_regex of Key DID Resolver."""
return PEER_DID_PATTERN
return PATTERN

async def _resolve(
self,
Expand All @@ -41,45 +35,10 @@ async def _resolve(
service_accept: Optional[Sequence[Text]] = None,
) -> dict:
"""Resolve a Key DID."""
try:
peer_did = is_peer_did(did)
except Exception as e:
raise DIDNotFound(f"peer_did is not formatted correctly: {did}") from e
if peer_did:
did_doc = self.resolve_peer_did_with_service_key_reference(did)
await PeerDID3Resolver().create_and_store_document(profile, did_doc)
else:
if not PATTERN.match(did):
raise DIDNotFound(f"did is not a peer did: {did}")

return did_doc.dict()
doc = resolve(did)
await PeerDID3Resolver().create_and_store(profile, did)

def resolve_peer_did_with_service_key_reference(
self, peer_did_2: Union[str, DID]
) -> DIDDocument:
"""Generate a DIDDocument from the did:peer:2 based on peer-did-python library.

And additional modification to ensure recipient key
references verificationmethod in same document.
"""
return _resolve_peer_did_with_service_key_reference(peer_did_2)


def _resolve_peer_did_with_service_key_reference(
peer_did_2: Union[str, DID]
) -> DIDDocument:
try:
doc = resolve_peer_did(peer_did_2)
## WORKAROUND LIBRARY NOT REREFERENCING RECEIPIENT_KEY
services = doc.service
signing_keys = [
vm
for vm in doc.verification_method or []
if vm.type == "Ed25519VerificationKey2020"
]
if services and signing_keys:
services[0].__dict__["recipient_keys"] = [signing_keys[0].id]
else:
raise Exception("no recipient_key signing_key pair")
except Exception as e:
raise ValueError("pydantic validation error:" + str(e))
return doc
return doc
153 changes: 64 additions & 89 deletions aries_cloudagent/resolver/default/peer3.py
Original file line number Diff line number Diff line change
@@ -1,129 +1,104 @@
"""Peer DID Resolver.

Resolution is performed by converting did:peer:2 to did:peer:3 according to
Resolution is performed by converting did:peer:2 to did:peer:3 according to
https://identity.foundation/peer-did-method-spec/#generation-method:~:text=Method%203%3A%20DID%20Shortening%20with%20SHA%2D256%20Hash
DID Document is just a did:peer:2 document (resolved by peer-did-python) where
the did:peer:2 has been replaced with the did:peer:3.
"""

from copy import deepcopy
from hashlib import sha256
import logging
import re
from typing import Optional, Pattern, Sequence, Text

from peerdid.dids import DID, DIDDocument, MalformedPeerDIDError
from peerdid.keys import MultibaseFormat, to_multibase
from did_peer_2 import PATTERN as PEER2_PATTERN, PEER3_PATTERN, peer2to3, resolve_peer3

from ...config.injection_context import InjectionContext
from ...connections.base_manager import BaseConnectionManager
from ...core.event_bus import Event, EventBus
from ...core.profile import Profile
from ...storage.base import BaseStorage
from ...storage.error import StorageNotFoundError
from ...storage.record import StorageRecord
from ...utils.multiformats import multibase, multicodec
from ...wallet.util import bytes_to_b58
from ..base import BaseDIDResolver, DIDNotFound, ResolverType

RECORD_TYPE_DID_DOCUMENT = "did_document" # pydid DIDDocument

LOGGER = logging.getLogger(__name__)


class PeerDID3Resolver(BaseDIDResolver):
"""Peer DID Resolver."""

RECORD_TYPE_3_TO_2 = "peer3_to_peer2"

def __init__(self):
"""Initialize Key Resolver."""
super().__init__(ResolverType.NATIVE)

async def setup(self, context: InjectionContext):
"""Perform required setup for Key DID resolution."""
event_bus = context.inject(EventBus)
event_bus.subscribe(
re.compile("acapy::record::connections::deleted"),
self.remove_record_for_deleted_conn,
)

@property
def supported_did_regex(self) -> Pattern:
"""Return supported_did_regex of Key DID Resolver."""
return re.compile(r"^did:peer:3(.*)")
return PEER3_PATTERN

async def _resolve(
self,
profile: Profile,
did: str,
service_accept: Optional[Sequence[Text]] = None,
) -> dict:
"""Resolve a Key DID."""
if did.startswith("did:peer:3"):
# retrieve did_doc from storage using did:peer:3
async with profile.session() as session:
storage = session.inject(BaseStorage)
record = await storage.find_record(
RECORD_TYPE_DID_DOCUMENT, {"did": did}
)
did_doc = DIDDocument.from_json(record.value)
else:
raise DIDNotFound(f"did is not a did:peer:3 {did}")

return did_doc.dict()

async def create_and_store_document(
self, profile: Profile, peer_did_2_doc: DIDDocument
):
"""Injest did:peer:2 document create did:peer:3 and store document."""
if not peer_did_2_doc.id.startswith("did:peer:2"):
raise MalformedPeerDIDError("did:peer:2 expected")

dp3_doc = deepcopy(peer_did_2_doc)
_convert_to_did_peer_3_document(dp3_doc)
try:
async with profile.session() as session:
storage = session.inject(BaseStorage)
record = await storage.find_record(
RECORD_TYPE_DID_DOCUMENT, {"did": dp3_doc.id}
"""Resolve a did:peer:3 DID."""
async with profile.session() as session:
storage = session.inject(BaseStorage)
try:
record = await storage.get_record(self.RECORD_TYPE_3_TO_2, did)
except StorageNotFoundError:
raise DIDNotFound(
f"did:peer:3 does not correspond to a known did:peer:2 {did}"
)
except StorageNotFoundError:
record = StorageRecord(
RECORD_TYPE_DID_DOCUMENT,
dp3_doc.to_json(),
{"did": dp3_doc.id},
)
async with profile.session() as session:
storage: BaseStorage = session.inject(BaseStorage)

doc = resolve_peer3(record.value)
return doc

async def create_and_store(self, profile: Profile, peer2: str):
"""Injest did:peer:2 create did:peer:3 and store document."""
if not PEER2_PATTERN.match(peer2):
raise ValueError("did:peer:2 expected")

peer3 = peer2to3(peer2)
async with profile.session() as session:
storage = session.inject(BaseStorage)
try:
record = await storage.get_record(self.RECORD_TYPE_3_TO_2, peer3)
except StorageNotFoundError:
record = StorageRecord(self.RECORD_TYPE_3_TO_2, peer2, {}, peer3)
await storage.add_record(record)
await set_keys_from_did_doc(profile, dp3_doc)
else:
# If doc already exists for did:peer:3 then it cannot have been modified
pass
return dp3_doc


async def set_keys_from_did_doc(profile, did_doc):
"""Add verificationMethod keys for lookup by conductor."""
conn_mgr = BaseConnectionManager(profile)

for vm in did_doc.verification_method or []:
if vm.controller == did_doc.id:
if vm.public_key_base58:
await conn_mgr.add_key_for_did(did_doc.id, vm.public_key_base58)
if vm.public_key_multibase:
pk = multibase.decode(vm.public_key_multibase)
if len(pk) == 32: # No multicodec prefix
pk = bytes_to_b58(pk)
else:
codec, key = multicodec.unwrap(pk)
if codec == multicodec.multicodec("ed25519-pub"):
pk = bytes_to_b58(key)
else:
continue
await conn_mgr.add_key_for_did(did_doc.id, pk)


def _convert_to_did_peer_3_document(dp2_document: DIDDocument) -> DIDDocument:
content = to_multibase(
sha256(dp2_document.id.lstrip("did:peer:2").encode()).digest(),
MultibaseFormat.BASE58,
)
dp3 = DID("did:peer:3" + content)
dp2 = dp2_document.id

dp2_doc_str = dp2_document.to_json()
dp3_doc_str = dp2_doc_str.replace(dp2, dp3)

dp3_doc = DIDDocument.from_json(dp3_doc_str)
return dp3_doc
else:
pass

doc = resolve_peer3(peer2)
return doc

async def remove_record_for_deleted_conn(self, profile: Profile, event: Event):
"""Remove record for deleted connection, if found."""
their_did = event.payload["their_did"]
my_did = event.payload["my_did"]
dids = [
*(did for did in (their_did, my_did) if PEER3_PATTERN.match(did)),
*(peer2to3(did) for did in (their_did, my_did) if PEER2_PATTERN.match(did)),
]
if dids:
LOGGER.debug(
"Removing peer 2 to 3 mapping for deleted connection: %s", dids
)
async with profile.session() as session:
storage = session.inject(BaseStorage)
for did in dids:
try:
record = StorageRecord(self.RECORD_TYPE_3_TO_2, None, None, did)
await storage.delete_record(record)
except StorageNotFoundError:
LOGGER.debug("No peer 2 to 3 mapping found for: %s", did)
Loading