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 31 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
10 changes: 7 additions & 3 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,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 +123,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,12 +666,15 @@ 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
"""
# 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)
96 changes: 96 additions & 0 deletions aries_cloudagent/resolver/default/peer2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""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,
MalformedPeerDIDError,
)

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


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
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.



def convert_to_did_peer_3(peer_did_2: Union[str, DID]) -> DIDDocument:
"""Generate did:peer:3 and corresponding DIDDocument."""
if not peer_did_2.startswith("did:peer:2"):
raise MalformedPeerDIDError("did:peer:2 expected")

doc = _resolve_peer_did_with_service_key_reference(peer_did_2)
_convert_to_did_peer_3_document(doc)
return doc
dbluhm marked this conversation as resolved.
Show resolved Hide resolved
193 changes: 193 additions & 0 deletions aries_cloudagent/resolver/default/peer3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""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, List
from multiformats import multibase, multicodec

from peerdid.dids import (
DID,
MalformedPeerDIDError,
DIDDocument,
DIDUrl,
)
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 StorageDuplicateError, StorageNotFoundError
from ...storage.record import StorageRecord

from ..base import BaseDIDResolver, DIDNotFound, ResolverType


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(
BaseConnectionManager.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(
BaseConnectionManager.RECORD_TYPE_DID_DOCUMENT, {"did": dp3_doc.id}
)
except StorageNotFoundError:
record = StorageRecord(
BaseConnectionManager.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)
else:
async with profile.session() as session:
storage: BaseStorage = session.inject(BaseStorage)
await storage.update_record(
record, dp3_doc.to_json(), {"did": dp3_doc.id}
)
await _reset_keys_from_did_doc(profile, dp3_doc)

return dp3_doc


async def _reset_keys_from_did_doc(profile, did_doc):
async with profile.session() as session:
storage: BaseStorage = session.inject(BaseStorage)
await storage.delete_all_records(
BaseConnectionManager.RECORD_TYPE_DID_KEY, {"did": did_doc.id}
)

for vm in did_doc.verification_method or []:
if vm.controller == did_doc.id:
if vm.public_key_base58:
await _add_key_for_did(profile, 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 _add_key_for_did(profile, did_doc.id, pk)


async def _add_key_for_did(profile, did: str, key: str):
"""Store a verkey for lookup against a DID.

Args:
did: The DID to associate with this key
key: The verkey to be added
"""
record = StorageRecord(
BaseConnectionManager.RECORD_TYPE_DID_KEY, key, {"did": did, "key": key}
)
async with profile.session() as session:
storage: BaseStorage = session.inject(BaseStorage)
try:
await storage.find_record(
BaseConnectionManager.RECORD_TYPE_DID_KEY, {"key": key}
)
except StorageNotFoundError:
await storage.add_record(record)
except StorageDuplicateError:
pass
# "Key already associated with DID: %s; this is likely caused by "
# "routing keys being erroneously stored in the past",
Copy link
Contributor

Choose a reason for hiding this comment

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

This method is a copy paste from the base connection manager. While I recommended not leaving doc storage and retrieval to the base connection manager, I would recommend leaving the key -> did -> connection management to the base connection manager.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it appropriate to instantiate an instance of the base manage to call these methods. I wasn't sure if that would look weirder. easy enough change to make if that is the preferred separation of concerns

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think instantiating an instance of the base connection manager is acceptable. I think it's better to have the explicit dependency than the implicit one we have by writing to the same records in the wallet.



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(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
_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
return dp2_document
Loading