-
Notifications
You must be signed in to change notification settings - Fork 517
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
swcurran
merged 47 commits into
openwallet-foundation:main
from
Jsyro:feature/peer-did-resolution
Sep 7, 2023
Merged
peer did 2/3 resolution #2472
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
2f5cb46
supress askar logging by default
Jsyro 0eeb254
import peer did, simple test and resolver classes
Jsyro 36b0681
did service check
Jsyro 57395db
separate peer did 2 and 3 into separate files
Jsyro c4dd42f
Merge branch 'main' into feature/peer-did-resolution
Jsyro b18457f
remove and add peerdid to rebuild lock file
Jsyro a824f04
Merge branch 'main' into feature/peer-did-resolution
Jsyro ca866f7
black formatter
Jsyro 2f1ca5e
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro fdf14ba
formatting
Jsyro 990ca08
pytest enforces linting??
Jsyro dbccf5a
linting
Jsyro 580ef8b
found black command
Jsyro c737152
lines to long
Jsyro 7f9a826
resolver pulls directly from storage
Jsyro 0494462
more dp3 laoding to dp3 resolver
Jsyro 1404fcc
pytest linting.. but there is also black linting?
Jsyro 76b9a41
black formatting
Jsyro 3c2b499
Merge branch 'main' into feature/peer-did-resolution
Jsyro 5f202a4
spelling police
Jsyro ff18579
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro 557df3e
after resolving did:peer:2. alwasys save it's did:peer:3
Jsyro 70809c4
Merge branch 'main' into feature/peer-did-resolution
Jsyro 1a0dc62
black formatting
Jsyro f6d5d07
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro b888c4a
black formatting
Jsyro bb9244b
issue with circular import
Jsyro 3bcd14f
black formatting
Jsyro c46a668
docstring
Jsyro a2be744
remove unused import
Jsyro a260f19
period
Jsyro d779b8a
convert to string to do a string replace.....
Jsyro 07a54c0
pr feedback
Jsyro 8248ec0
readability
Jsyro 61e0dd5
remove unused, move a constant
Jsyro e51d803
return value needs to captured
Jsyro b50c2bd
formatting
Jsyro 7ee1370
revert return type
Jsyro 46857b8
remove unused
Jsyro 2335732
Merge branch 'main' into feature/peer-did-resolution
Jsyro e2cb29e
formatting
Jsyro 48836d7
Merge remote-tracking branch 'Jsyro/feature/peer-did-resolution' into…
Jsyro 4518dc5
linting
Jsyro 3b6b382
Merge branch 'main' into feature/peer-did-resolution
Jsyro bf23cbf
simplify methods because did and did_doc are linked
Jsyro 3ab76ef
asda
Jsyro 8a1178e
puncutation
Jsyro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.