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

did:tdw resolver #3237

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions acapy_agent/messaging/tests/test_valid.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CREDENTIAL_TYPE_VALIDATE,
DID_KEY_VALIDATE,
DID_POSTURE_VALIDATE,
DID_TDW_VALIDATE,
ENDPOINT_TYPE_VALIDATE,
ENDPOINT_VALIDATE,
INDY_CRED_DEF_ID_VALIDATE,
Expand Down Expand Up @@ -114,6 +115,23 @@ def test_indy_did(self):
INDY_DID_VALIDATE("Q4zqM7aXqm7gDQkUVLng9h")
INDY_DID_VALIDATE("did:sov:Q4zqM7aXqm7gDQkUVLng9h")

def test_tdw_did(self):
valid_tdw_dids = [
"did:tdw:QmUchSB5f5DJQks9CeyLJjhAy4iKJcYzRyiuYq3sjV13px:example.com",
"did:tdw:QmZiKXwQVfyZVuvCsuHpQh4arSUpEmeVVRvSfv3uiEycSr:example.com%3A5000",
"did:tdw:QmP9VWaTCHcyztDpRj9XSHvZbmYe3m9HZ61KoDtZgWaXVU:example.com%3A5000#z6MkkzY9skorPaoEbCJFKUo7thD8Yb8MBs28aJRopf1TUo9V",
"did:tdw:QmZiKXwQVfyZVuvCsuHpQh4arSUpEmeVVRvSfv3uiEycSr:example.com%3A5000#whois",
]
for valid_tdw_did in valid_tdw_dids:
DID_TDW_VALIDATE(valid_tdw_did)

non_valid_tdw_dids = [
"did:web:QmUchSB5f5DJQks9CeyLJjhAy4iKJcYzRyiuYq3sjV13px",
]
for non_valid_tdw_did in non_valid_tdw_dids:
with self.assertRaises(ValidationError):
DID_TDW_VALIDATE(non_valid_tdw_did)

def test_indy_raw_public_key(self):
non_indy_raw_public_keys = [
"Q4zqM7aXqm7gDQkUVLng9JQ4zqM7aXqm7gDQkUVLng9I", # 'I' not a base58 char
Expand Down
19 changes: 19 additions & 0 deletions acapy_agent/messaging/valid.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,22 @@ def __init__(self):
)


class DIDTdw(Regexp):
"""Validate value against did:tdw specification."""

EXAMPLE = (
"did:tdw:QmP9VWaTCHcyztDpRj9XSHvZbmYe3m9HZ61KoDtZgWaXVU:example.com%3A5000#whois"
)
PATTERN = re.compile(r"^(did:tdw:)([a-zA-Z0-9%._-]*:)*[a-zA-Z0-9%._-]+(#\w+)?$")

def __init__(self):
"""Initialize the instance."""

super().__init__(
DIDTdw.PATTERN, error="Value {input} is not in W3C did:tdw format"
)


class DIDPosture(OneOf):
"""Validate value against defined DID postures."""

Expand Down Expand Up @@ -934,6 +950,9 @@ def __init__(
DID_WEB_VALIDATE = DIDWeb()
DID_WEB_EXAMPLE = DIDWeb.EXAMPLE

DID_TDW_VALIDATE = DIDTdw()
DID_TDW_EXAMPLE = DIDTdw.EXAMPLE

ROUTING_KEY_VALIDATE = RoutingKey()
ROUTING_KEY_EXAMPLE = RoutingKey.EXAMPLE

Expand Down
6 changes: 6 additions & 0 deletions acapy_agent/resolver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ async def setup(context: InjectionContext):
await web_resolver.setup(context)
registry.register_resolver(web_resolver)

tdw_resolver = ClassProvider(
"acapy_agent.resolver.default.tdw.TdwDIDResolver"
).provide(context.settings, context.injector)
await tdw_resolver.setup(context)
registry.register_resolver(tdw_resolver)

if context.settings.get("resolver.universal"):
universal_resolver = ClassProvider(
"acapy_agent.resolver.default.universal.UniversalResolver"
Expand Down
6 changes: 4 additions & 2 deletions acapy_agent/resolver/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from enum import Enum
from typing import NamedTuple, Optional, Pattern, Sequence, Text, Union

from pydid import DID
from pydid import DID, DIDUrl, InvalidDIDError

from ..cache.base import BaseCache
from ..config.injection_context import InjectionContext
Expand Down Expand Up @@ -145,7 +145,9 @@ async def resolve(
if isinstance(did, DID):
did = str(did)
else:
DID.validate(did)
if not DID.is_valid(did) and not DIDUrl.is_valid(did):
raise InvalidDIDError(f"Invalid DID or DID URL: {did}")

if not await self.supports(profile, did):
raise DIDMethodNotSupported(
f"{self.__class__.__name__} does not support DID method for: {did}"
Expand Down
40 changes: 40 additions & 0 deletions acapy_agent/resolver/default/tdw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""TDW DID Resolver.

Resolution is performed by the did_tdw library.
"""

from re import Pattern
from typing import Optional, Sequence, Text

from did_tdw.resolver import ResolutionResult, resolve_did

from ...config.injection_context import InjectionContext
from ...core.profile import Profile
from ...messaging.valid import DIDTdw
from ..base import BaseDIDResolver, ResolverType


class TdwDIDResolver(BaseDIDResolver):
"""TDW DID Resolver."""

def __init__(self):
"""Initialize the TDW DID Resolver."""
super().__init__(ResolverType.NATIVE)

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

@property
def supported_did_regex(self) -> Pattern:
"""Return supported DID regex of TDW DID Resolver."""
return DIDTdw.PATTERN

async def _resolve(
self, profile: Profile, did: str, service_accept: Optional[Sequence[Text]] = None
) -> dict:
"""Resolve DID using TDW."""
response: ResolutionResult = await resolve_did(did)
if response.resolution_metadata and response.resolution_metadata.get("error"):
return response.resolution_metadata

return response.document
37 changes: 37 additions & 0 deletions acapy_agent/resolver/default/tests/test_tdw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest

from ....core.in_memory import InMemoryProfile
from ....core.profile import Profile
from ....messaging.valid import DIDTdw
from ..tdw import TdwDIDResolver

TEST_DID = "did:tdw:Qma6mc1qZw3NqxwX6SB5GPQYzP4pGN2nXD15Jwi4bcDBKu:domain.example"


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


@pytest.fixture
def profile():
"""Profile fixture."""
profile = InMemoryProfile.test_profile()
yield profile


@pytest.mark.asyncio
async def test_supported_did_regex(profile, resolver: TdwDIDResolver):
"""Test the supported_did_regex."""
assert resolver.supported_did_regex == DIDTdw.PATTERN
assert await resolver.supports(
profile,
TEST_DID,
)


@pytest.mark.asyncio
async def test_resolve(resolver: TdwDIDResolver, profile: Profile):
"""Test resolve method."""
assert await resolver.resolve(profile, TEST_DID)
6 changes: 4 additions & 2 deletions acapy_agent/resolver/did_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import List, Optional, Sequence, Text, Tuple, Union

import pydid
from pydid import DID, DIDError, DIDUrl, Resource, VerificationMethod
from pydid import DID, DIDError, DIDUrl, InvalidDIDError, Resource, VerificationMethod
from pydid.doc.doc import BaseDIDDocument, IDNotFoundError

from ..core.profile import Profile
Expand Down Expand Up @@ -56,7 +56,9 @@ async def _resolve(
if isinstance(did, DID):
did = str(did)
else:
DID.validate(did)
if not DID.is_valid(did) and not DIDUrl.is_valid(did):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This allows for the DIDUrl to be resolved. I admittedly don't have a lot of knowledge here.

raise InvalidDIDError(f"Invalid DID or DID URL: {did}")

for resolver in await self._match_did_to_resolver(profile, did):
try:
LOGGER.debug("Resolving DID %s with %s", did, resolver)
Expand Down
8 changes: 6 additions & 2 deletions acapy_agent/resolver/routes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Resolve did document admin routes."""

import re

from aiohttp import web
from aiohttp_apispec import docs, match_info_schema, response_schema
from marshmallow import fields, validate
from pydid.common import DID_PATTERN

from ..admin.decorators.auth import tenant_authentication
from ..admin.request_context import AdminRequestContext
Expand All @@ -23,7 +24,10 @@ class W3cDID(validate.Regexp):
"""Validate value against w3c DID."""

EXAMPLE = "did:ted:WgWxqztrNooG92RXvxSTWv"
PATTERN = DID_PATTERN
# Did or DidUrl regex
PATTERN = re.compile(
"^did:([a-z0-9]+):((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)(#\w+)?$"
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changes the pattern to allow did url pattern. Couldn't figure out how to do it with the pydid library.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused why we're doing this. A DID is distinct from a DID URL?

Copy link
Contributor

@swcurran swcurran Nov 13, 2024

Choose a reason for hiding this comment

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

I suspect it is because a DID resolves to a DIDDoc, and a DID URL, resolves to whatever the resource is pointed to by the URL (AnonCreds object, OCA bundle, status list VC, etc). The DID still needs to be resolved (perhaps from a cache) before getting the resource, but the resolution result object will be different.


def __init__(self):
"""Initialize the instance."""
Expand Down
8 changes: 8 additions & 0 deletions acapy_agent/wallet/did_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ def holder_defined_did(self) -> HolderDefinedDid:
holder_defined_did=HolderDefinedDid.NO,
)

TDW = DIDMethod(
name="tdw",
key_types=[ED25519, X25519],
rotation=False,
holder_defined_did=HolderDefinedDid.NO,
)


class DIDMethods:
"""DID Method class specifying DID methods with supported key types."""
Expand All @@ -102,6 +109,7 @@ def __init__(self) -> None:
WEB.method_name: WEB,
PEER2.method_name: PEER2,
PEER4.method_name: PEER4,
TDW.method_name: TDW,
}

def registered(self, method: str) -> bool:
Expand Down
Loading
Loading