Skip to content

Commit

Permalink
Merge pull request #2472 from Jsyro/feature/peer-did-resolution
Browse files Browse the repository at this point in the history
peer did 2/3 resolution
  • Loading branch information
swcurran authored Sep 7, 2023
2 parents f189bd0 + 8a1178e commit 9aa7334
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 6 deletions.
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"remoteUser": "vscode",

"remoteEnv": {
"RUST_LOG":"aries-askar::log::target=error"
//"PATH": "${containerEnv:PATH}:${workspaceRoot}/.venv/bin"
},

Expand Down
5 changes: 3 additions & 2 deletions aries_cloudagent/connections/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
Ed25519VerificationKey2020,
JsonWebKey2020,
)

from ..cache.base import BaseCache
from ..config.base import InjectionError
from ..config.logging import get_logger_inst
Expand Down Expand Up @@ -61,7 +60,7 @@ class BaseConnectionManagerError(BaseError):
class BaseConnectionManager:
"""Class to provide utilities regarding connection_targets."""

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

def __init__(self, profile: Profile):
Expand Down Expand Up @@ -123,6 +122,7 @@ async def create_did_document(
f"Router connection not completed: {router_id}"
)
routing_doc, _ = await self.fetch_did_document(router.their_did)
assert isinstance(routing_doc, DIDDoc)
if not routing_doc.service:
raise BaseConnectionManagerError(
f"No services defined by routing DIDDoc: {router_id}"
Expand Down Expand Up @@ -671,6 +671,7 @@ async def fetch_did_document(self, did: str) -> Tuple[DIDDoc, StorageRecord]:
Args:
did: The DID to search for
"""
# legacy documents for unqualified dids
async with self._profile.session() as session:
storage = session.inject(BaseStorage)
record = await storage.find_record(self.RECORD_TYPE_DID_DOC, {"did": did})
Expand Down
12 changes: 12 additions & 0 deletions aries_cloudagent/resolver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ async def setup(context: InjectionContext):
).provide(context.settings, context.injector)
await universal_resolver.setup(context)
registry.register_resolver(universal_resolver)

peer_did_2_resolver = ClassProvider(
"aries_cloudagent.resolver.default.peer2.PeerDID2Resolver"
).provide(context.settings, context.injector)
await peer_did_2_resolver.setup(context)
registry.register_resolver(peer_did_2_resolver)

peer_did_3_resolver = ClassProvider(
"aries_cloudagent.resolver.default.peer3.PeerDID3Resolver"
).provide(context.settings, context.injector)
await peer_did_3_resolver.setup(context)
registry.register_resolver(peer_did_3_resolver)
85 changes: 85 additions & 0 deletions aries_cloudagent/resolver/default/peer2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Peer DID Resolver.
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 peerdid.dids import (
is_peer_did,
PEER_DID_PATTERN,
resolve_peer_did,
DID,
DIDDocument,
)

from ...config.injection_context import InjectionContext
from ...core.profile import Profile
from ..base import BaseDIDResolver, DIDNotFound, ResolverType
from .peer3 import PeerDID3Resolver


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

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

async def setup(self, context: InjectionContext):
"""Perform required setup for Key DID resolution."""

@property
def supported_did_regex(self) -> Pattern:
"""Return supported_did_regex of Key DID Resolver."""
return PEER_DID_PATTERN

async def _resolve(
self,
profile: Profile,
did: str,
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:
raise DIDNotFound(f"did is not a peer did: {did}")

return did_doc.dict()

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
134 changes: 134 additions & 0 deletions aries_cloudagent/resolver/default/peer3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Peer DID Resolver.
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.
"""

import re
from copy import deepcopy
from hashlib import sha256
from typing import Optional, Pattern, Sequence, Text
from multiformats import multibase, multicodec

from peerdid.dids import (
DID,
MalformedPeerDIDError,
DIDDocument,
)
from peerdid.keys import to_multibase, MultibaseFormat
from ...wallet.util import bytes_to_b58

from ...connections.base_manager import BaseConnectionManager
from ...config.injection_context import InjectionContext
from ...core.profile import Profile
from ...storage.base import BaseStorage
from ...storage.error import StorageNotFoundError
from ...storage.record import StorageRecord

from ..base import BaseDIDResolver, DIDNotFound, ResolverType

RECORD_TYPE_DID_DOCUMENT = "did_document" # pydid DIDDocument


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

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

async def setup(self, context: InjectionContext):
"""Perform required setup for Key DID resolution."""

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

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}
)
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)
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
93 changes: 93 additions & 0 deletions aries_cloudagent/resolver/default/tests/test_peer2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Test PeerDIDResolver."""

from asynctest import mock as async_mock
from peerdid.dids import resolve_peer_did, DIDDocument, DID
import pytest

from .. import legacy_peer as test_module
from ....cache.base import BaseCache
from ....cache.in_memory import InMemoryCache
from ....core.in_memory import InMemoryProfile
from ....core.profile import Profile
from ...did_resolver import DIDResolver
from ..peer2 import PeerDID2Resolver, _resolve_peer_did_with_service_key_reference


TEST_DID0 = "did:peer:2.Ez6LSpkcni2KTTxf4nAp6cPxjRbu26Tj4b957BgHcknVeNFEj.Vz6MksXhfmxm2i3RnoHH2mKQcx7EY4tToJR9JziUs6bp8a6FM.SeyJ0IjoiZGlkLWNvbW11bmljYXRpb24iLCJzIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjkwNzAiLCJyZWNpcGllbnRfa2V5cyI6W119"
TEST_DID0_DOC = _resolve_peer_did_with_service_key_reference(TEST_DID0).dict()
TEST_DID0_RAW_DOC = resolve_peer_did(TEST_DID0).dict()


@pytest.fixture
def common_resolver():
"""Resolver fixture."""
yield DIDResolver([PeerDID2Resolver()])


@pytest.fixture
def resolver():
"""Resolver fixture."""
yield PeerDID2Resolver()


@pytest.fixture
def profile():
"""Profile fixture."""
profile = InMemoryProfile.test_profile()
profile.context.injector.bind_instance(BaseCache, InMemoryCache())
yield profile


class TestPeerDID2Resolver:
@pytest.mark.asyncio
async def test_resolution_types(self, resolver: PeerDID2Resolver, profile: Profile):
"""Test supports."""
assert DID.is_valid(TEST_DID0)
assert isinstance(resolve_peer_did(TEST_DID0), DIDDocument)
assert isinstance(
_resolve_peer_did_with_service_key_reference(TEST_DID0), DIDDocument
)

@pytest.mark.asyncio
async def test_supports(self, resolver: PeerDID2Resolver, profile: Profile):
"""Test supports."""
with async_mock.patch.object(test_module, "BaseConnectionManager") as mock_mgr:
mock_mgr.return_value = async_mock.MagicMock(
fetch_did_document=async_mock.CoroutineMock(
return_value=(TEST_DID0_DOC, None)
)
)
assert await resolver.supports(profile, TEST_DID0)

@pytest.mark.asyncio
async def test_supports_no_cache(
self, resolver: PeerDID2Resolver, profile: Profile
):
"""Test supports."""
profile.context.injector.clear_binding(BaseCache)
with async_mock.patch.object(test_module, "BaseConnectionManager") as mock_mgr:
mock_mgr.return_value = async_mock.MagicMock(
fetch_did_document=async_mock.CoroutineMock(
return_value=(TEST_DID0_DOC, None)
)
)
assert await resolver.supports(profile, TEST_DID0)

@pytest.mark.asyncio
async def test_supports_service_referenced(
self, resolver: PeerDID2Resolver, common_resolver: DIDResolver, profile: Profile
):
"""Test supports."""
profile.context.injector.clear_binding(BaseCache)
with async_mock.patch.object(test_module, "BaseConnectionManager") as mock_mgr:
mock_mgr.return_value = async_mock.MagicMock(
fetch_did_document=async_mock.CoroutineMock(
return_value=(TEST_DID0_DOC, None)
)
)
recipient_key = await common_resolver.dereference(
profile,
TEST_DID0_DOC["service"][0]["recipient_keys"][0],
document=DIDDocument.deserialize(TEST_DID0_DOC),
)
assert recipient_key
Loading

0 comments on commit 9aa7334

Please sign in to comment.