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

peer did 2/3 resolution #2472

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2f5cb46
supress askar logging by default
Jsyro Sep 5, 2023
0eeb254
import peer did, simple test and resolver classes
Jsyro Sep 5, 2023
36b0681
did service check
Jsyro Sep 5, 2023
57395db
separate peer did 2 and 3 into separate files
Jsyro Sep 6, 2023
c4dd42f
Merge branch 'main' into feature/peer-did-resolution
Jsyro Sep 6, 2023
b18457f
remove and add peerdid to rebuild lock file
Jsyro Sep 6, 2023
a824f04
Merge branch 'main' into feature/peer-did-resolution
Jsyro Sep 6, 2023
ca866f7
black formatter
Jsyro Sep 6, 2023
2f1ca5e
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro Sep 6, 2023
fdf14ba
formatting
Jsyro Sep 6, 2023
990ca08
pytest enforces linting??
Jsyro Sep 6, 2023
dbccf5a
linting
Jsyro Sep 6, 2023
580ef8b
found black command
Jsyro Sep 6, 2023
c737152
lines to long
Jsyro Sep 6, 2023
7f9a826
resolver pulls directly from storage
Jsyro Sep 6, 2023
0494462
more dp3 laoding to dp3 resolver
Jsyro Sep 6, 2023
1404fcc
pytest linting.. but there is also black linting?
Jsyro Sep 6, 2023
76b9a41
black formatting
Jsyro Sep 6, 2023
3c2b499
Merge branch 'main' into feature/peer-did-resolution
Jsyro Sep 6, 2023
5f202a4
spelling police
Jsyro Sep 6, 2023
ff18579
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro Sep 6, 2023
557df3e
after resolving did:peer:2. alwasys save it's did:peer:3
Jsyro Sep 6, 2023
70809c4
Merge branch 'main' into feature/peer-did-resolution
Jsyro Sep 6, 2023
1a0dc62
black formatting
Jsyro Sep 6, 2023
f6d5d07
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro Sep 6, 2023
b888c4a
black formatting
Jsyro Sep 6, 2023
bb9244b
issue with circular import
Jsyro Sep 6, 2023
3bcd14f
black formatting
Jsyro Sep 6, 2023
c46a668
docstring
Jsyro Sep 6, 2023
a2be744
remove unused import
Jsyro Sep 6, 2023
a260f19
period
Jsyro Sep 6, 2023
d779b8a
convert to string to do a string replace.....
Jsyro Sep 7, 2023
07a54c0
pr feedback
Jsyro Sep 7, 2023
8248ec0
readability
Jsyro Sep 7, 2023
61e0dd5
remove unused, move a constant
Jsyro Sep 7, 2023
e51d803
return value needs to captured
Jsyro Sep 7, 2023
b50c2bd
formatting
Jsyro Sep 7, 2023
7ee1370
revert return type
Jsyro Sep 7, 2023
46857b8
remove unused
Jsyro Sep 7, 2023
2335732
Merge branch 'main' into feature/peer-did-resolution
Jsyro Sep 7, 2023
e2cb29e
formatting
Jsyro Sep 7, 2023
48836d7
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro Sep 7, 2023
4518dc5
linting
Jsyro Sep 7, 2023
3b6b382
Merge branch 'main' into feature/peer-did-resolution
Jsyro Sep 7, 2023
bf23cbf
simplify methods because did and did_doc are linked
Jsyro Sep 7, 2023
3ab76ef
asda
Jsyro Sep 7, 2023
8a1178e
puncutation
Jsyro Sep 7, 2023
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
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
29 changes: 22 additions & 7 deletions aries_cloudagent/connections/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
BaseDIDDocument as ResolvedDocument,
DIDCommService,
VerificationMethod,
DID,
)
import pydid
from pydid.verification_method import (
Ed25519VerificationKey2018,
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 +61,8 @@ 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_DOCUMENT = "did_document" # pydid DIDDocument
dbluhm marked this conversation as resolved.
Show resolved Hide resolved
RECORD_TYPE_DID_KEY = "did_key"

def __init__(self, profile: Profile):
Expand Down Expand Up @@ -123,6 +124,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 @@ -665,16 +667,29 @@ 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[Union[DIDDoc, ResolvedDocument], StorageRecord]:
dbluhm marked this conversation as resolved.
Show resolved Hide resolved
"""Retrieve a DID Document for a given DID.

Args:
did: The DID to search for
"""
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
if DID.is_valid(did):
async with self._profile.session() as session:
storage = session.inject(BaseStorage)
record = await storage.find_record(
self.RECORD_TYPE_DID_DOCUMENT, {"did": did}
)
return ResolvedDocument.from_json(record.value), record

else: # 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}
)
return DIDDoc.from_json(record.value), record
dbluhm marked this conversation as resolved.
Show resolved Hide resolved

async def find_connection(
self,
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)
79 changes: 79 additions & 0 deletions aries_cloudagent/resolver/default/peer2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""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


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)
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 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
Comment on lines +67 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workaround is painful but given that peer did creators won't necessarily generate the same ids for their verification methods that our library does (since it's not defined in the spec) means we can't avoid this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea.... it's jank.. but because it's from a peer_did we know exactly what transformation needs to happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sicpa-dlab/peer-did-python#63

I also opened an issue in the peer-did-python library pointing out my need for this workaround.

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

import re
from hashlib import sha256
from typing import Optional, Pattern, Sequence, Text, Union, Tuple, List

from peerdid.dids import (
DID,
MalformedPeerDIDError,
DIDDocument,
DIDUrl,
)
from peerdid.keys import to_multibase, MultibaseFormat

from ...connections.base_manager import BaseConnectionManager
from ...config.injection_context import InjectionContext
from ...core.profile import Profile
from ..base import BaseDIDResolver, DIDNotFound, ResolverType
from .peer2 import _resolve_peer_did_with_service_key_reference


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
did_doc, rec = await BaseConnectionManager(profile).fetch_did_document(
did=did
)
assert isinstance(did_doc, DIDDocument)
else:
raise DIDNotFound(f"did is not a peer did: {did}")

return did_doc.dict()


def gen_did_peer_3(peer_did_2: Union[str, DID]) -> Tuple[DID, DIDDocument]:
if not peer_did_2.startswith("did:peer:2"):
raise MalformedPeerDIDError("did:peer:2 expected")

content = to_multibase(
sha256(peer_did_2.lstrip("did:peer:2").encode()).digest(),
MultibaseFormat.BASE58,
)
dp3 = DID("did:peer:3" + content)

doc = _resolve_peer_did_with_service_key_reference(peer_did_2)
convert_to_did_peer_3_document(dp3, doc)
return dp3, doc


def _replace_all_values(input, org, new):
for k, v in input.items():
if isinstance(v, type(dict)):
_replace_all_values(v, org, new)
if isinstance(v, List):
for i, item in enumerate(v):
if isinstance(item, type(dict)):
_replace_all_values(item, org, new)
elif (
isinstance(item, str)
or isinstance(item, DID)
or isinstance(item, DIDUrl)
):
v.pop(i)
v.append(item.replace(org, new, 1))
elif hasattr(item, "__dict__"):
_replace_all_values(item.__dict__, org, new)
else:
pass

elif isinstance(v, str) or isinstance(v, DID) or isinstance(v, DIDUrl):
input[k] = v.replace(org, new, 1)
else:
pass
dbluhm marked this conversation as resolved.
Show resolved Hide resolved


def convert_to_did_peer_3_document(dp3, dp2_document: DIDDocument) -> None:
dp2 = dp2_document.id
_replace_all_values(dp2_document.__dict__, dp2, dp3)

# update document indexes
new_indexes = {}
for ind, val in dp2_document._index.items():
new_indexes[ind.replace(dp2, dp3)] = val

dp2_document._index = new_indexes
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