forked from openwallet-foundation/acapy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request openwallet-foundation#2472 from Jsyro/feature/peer…
…-did-resolution peer did 2/3 resolution
- Loading branch information
Showing
10 changed files
with
465 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.