diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 75aec4af7f..25f3b59418 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -224,6 +224,20 @@ def __init__(self): ) +class DIDWeb(Regexp): + """Validate value against did:web specification.""" + + EXAMPLE = "did:web:example.com" + PATTERN = re.compile(r"^(did:web:)?([a-zA-Z0-9%._-]*:)*[a-zA-Z0-9%._-]+$") + + def __init__(self): + """Initializer.""" + + super().__init__( + DIDWeb.PATTERN, error="Value {input} is not in W3C did:web format" + ) + + class DIDPosture(OneOf): """Validate value against defined DID postures.""" diff --git a/aries_cloudagent/resolver/__init__.py b/aries_cloudagent/resolver/__init__.py index f13dcb7682..51a297e43b 100644 --- a/aries_cloudagent/resolver/__init__.py +++ b/aries_cloudagent/resolver/__init__.py @@ -31,3 +31,9 @@ async def setup(context: InjectionContext): registry.register(indy_resolver) else: LOGGER.warning("Ledger is not configured, not loading IndyDIDResolver") + + web_resolver = ClassProvider( + "aries_cloudagent.resolver.default.web.WebDIDResolver" + ).provide(context.settings, context.injector) + await web_resolver.setup(context) + registry.register(web_resolver) diff --git a/aries_cloudagent/resolver/default/tests/test_web.py b/aries_cloudagent/resolver/default/tests/test_web.py new file mode 100644 index 0000000000..3ff7098d75 --- /dev/null +++ b/aries_cloudagent/resolver/default/tests/test_web.py @@ -0,0 +1,27 @@ +"""Test did:web Resolver.""" + +import pytest +from ..web import WebDIDResolver + + +@pytest.fixture +def resolver(): + yield WebDIDResolver() + + +def test_transformation_domain_only(resolver): + did = "did:web:example.com" + url = resolver._WebDIDResolver__transform_to_url(did) + assert url == "https://example.com/.well-known/did.json" + + +def test_transformation_domain_with_path(resolver): + did = "did:web:example.com:department:example" + url = resolver._WebDIDResolver__transform_to_url(did) + assert url == "https://example.com/department/example/did.json" + + +def test_transformation_domain_with_port(resolver): + did = "did:web:localhost%3A443" + url = resolver._WebDIDResolver__transform_to_url(did) + assert url == "https://localhost:443/.well-known/did.json" diff --git a/aries_cloudagent/resolver/default/web.py b/aries_cloudagent/resolver/default/web.py new file mode 100644 index 0000000000..3475ca2a8d --- /dev/null +++ b/aries_cloudagent/resolver/default/web.py @@ -0,0 +1,83 @@ +"""Web DID Resolver.""" + +import urllib.parse + +from typing import Sequence, Pattern + +import aiohttp + +from pydid import DID, DIDDocument + +from ...config.injection_context import InjectionContext +from ...core.profile import Profile +from ...messaging.valid import DIDWeb + +from ..base import ( + BaseDIDResolver, + DIDNotFound, + ResolverError, + ResolverType, +) + + +class WebDIDResolver(BaseDIDResolver): + """Web DID Resolver.""" + + def __init__(self): + """Initialize Web DID Resolver.""" + super().__init__(ResolverType.NATIVE) + + async def setup(self, context: InjectionContext): + """Perform required setup for Web DID resolution.""" + + @property + def supported_did_regex(self) -> Pattern: + """Return supported_did_regex of Web DID Resolver.""" + return DIDWeb.PATTERN + + def supported_methods(self) -> Sequence[str]: + """Return list of supported methods.""" + return ["web"] + + def __transform_to_url(self, did): + """ + Transform did to url. + + according to + https://w3c-ccg.github.io/did-method-web/#read-resolve + """ + + as_did = DID(did) + method_specific_id = as_did.method_specific_id + if ":" in method_specific_id: + # contains path + url = method_specific_id.replace(":", "/") + else: + # bare domain needs /.well-known path + url = method_specific_id + "/.well-known" + + # Support encoded ports (See: https://github.com/w3c-ccg/did-method-web/issues/7) + url = urllib.parse.unquote(url) + + return "https://" + url + "/did.json" + + async def _resolve(self, profile: Profile, did: str) -> dict: + """Resolve did:web DIDs.""" + + url = self.__transform_to_url(did) + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + try: + # Validate DIDDoc with pyDID + did_doc = DIDDocument.from_json(await response.text()) + return did_doc.serialize() + except Exception as err: + raise ResolverError( + "Response was incorrectly formatted" + ) from err + if response.status == 404: + raise DIDNotFound(f"No document found for {did}") + raise ResolverError( + "Could not find doc for {}: {}".format(did, await response.text()) + ) diff --git a/requirements.txt b/requirements.txt index b4fa5cee58..83a965f3c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,4 @@ pyld~=2.0.3 pyyaml~=5.4.0 ConfigArgParse~=1.2.3 pyjwt~=1.7.1 -pydid~=0.2.3 \ No newline at end of file +pydid~=0.2.6 \ No newline at end of file