Skip to content

Commit

Permalink
refactor: use did-peer-2 instead of peerdid
Browse files Browse the repository at this point in the history
Speaking frankly, I felt that the peerdid library was a bit overly
complicated. On top of that, peerdid is not up to date with the
corrections proposed in this PR to the did:peer method specification:

decentralized-identity/peer-did-method-spec#62

did-peer-2 is a minimal implementation of did:peer:2 and did:peer:3.
It is up to date with the above PR. The corrections to the spec and the
library lend themselves to a simpler implementation of the did:peer:2
and did:peer:3 resolvers. This refactor supports work to add did:peer:2
support to OOB + DID Exchange.

New in this PR is a cleanup mechanism for the did:peer:3 to did:peer:2
mapping records. When a connection is deleted, we check whether there
are any potential mappings to remove.

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
  • Loading branch information
dbluhm committed Oct 24, 2023
1 parent b8c00ec commit c5a9458
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 650 deletions.
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

0 comments on commit c5a9458

Please sign in to comment.