diff --git a/acapy_agent/messaging/tests/test_valid.py b/acapy_agent/messaging/tests/test_valid.py index 8983d27964..0f3b3d3363 100644 --- a/acapy_agent/messaging/tests/test_valid.py +++ b/acapy_agent/messaging/tests/test_valid.py @@ -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, @@ -114,6 +115,24 @@ 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", + ] + for valid_tdw_did in valid_tdw_dids: + DID_TDW_VALIDATE(valid_tdw_did) + + non_valid_tdw_dids = [ + "did:web:QmUchSB5f5DJQks9CeyLJjhAy4iKJcYzRyiuYq3sjV13px", + # Did urls are not allowed + "did:tdw:QmP9VWaTCHcyztDpRj9XSHvZbmYe3m9HZ61KoDtZgWaXVU:example.com%3A5000#z6MkkzY9skorPaoEbCJFKUo7thD8Yb8MBs28aJRopf1TUo9V", + "did:tdw:QmZiKXwQVfyZVuvCsuHpQh4arSUpEmeVVRvSfv3uiEycSr:example.com%3A5000/whois", + ] + 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 diff --git a/acapy_agent/messaging/valid.py b/acapy_agent/messaging/valid.py index 894c1a819b..fc1749d600 100644 --- a/acapy_agent/messaging/valid.py +++ b/acapy_agent/messaging/valid.py @@ -319,6 +319,20 @@ def __init__(self): ) +class DIDTdw(Regexp): + """Validate value against did:tdw specification.""" + + EXAMPLE = "did:tdw:QmP9VWaTCHcyztDpRj9XSHvZbmYe3m9HZ61KoDtZgWaXVU:example.com%3A5000" + PATTERN = re.compile(r"^(did:tdw:)([a-zA-Z0-9%._-]*:)*[a-zA-Z0-9%._-]+$") + + 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.""" @@ -934,6 +948,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 diff --git a/acapy_agent/resolver/__init__.py b/acapy_agent/resolver/__init__.py index 005cce6df9..3998b3da1c 100644 --- a/acapy_agent/resolver/__init__.py +++ b/acapy_agent/resolver/__init__.py @@ -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" diff --git a/acapy_agent/resolver/default/tdw.py b/acapy_agent/resolver/default/tdw.py new file mode 100644 index 0000000000..4aabd198e8 --- /dev/null +++ b/acapy_agent/resolver/default/tdw.py @@ -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 diff --git a/acapy_agent/resolver/default/tests/test_tdw.py b/acapy_agent/resolver/default/tests/test_tdw.py new file mode 100644 index 0000000000..8829b9f031 --- /dev/null +++ b/acapy_agent/resolver/default/tests/test_tdw.py @@ -0,0 +1,36 @@ +import pytest + +from ....core.profile import Profile +from ....messaging.valid import DIDTdw +from ....utils.testing import create_test_profile +from ..tdw import TdwDIDResolver + +TEST_DID = "did:tdw:Qma6mc1qZw3NqxwX6SB5GPQYzP4pGN2nXD15Jwi4bcDBKu:domain.example" + + +@pytest.fixture +def resolver(): + """Resolver fixture.""" + yield TdwDIDResolver() + + +@pytest.fixture +async def profile(): + """Profile fixture.""" + yield await create_test_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) diff --git a/acapy_agent/wallet/did_method.py b/acapy_agent/wallet/did_method.py index bf6ff57304..2acf670837 100644 --- a/acapy_agent/wallet/did_method.py +++ b/acapy_agent/wallet/did_method.py @@ -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.""" @@ -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: diff --git a/poetry.lock b/poetry.lock index 60bfdb4510..465055db55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,15 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] [[package]] name = "aiohappyeyeballs" @@ -213,7 +224,7 @@ yaml = ["PyYAML (>=3.10)"] name = "aries-askar" version = "0.3.2" description = "" -optional = true +optional = false python-versions = ">=3.6.3" files = [ {file = "aries_askar-0.3.2-py3-none-macosx_10_9_universal2.whl", hash = "sha256:02ddbe1773ce72c57edafff5777a1337d4a678da7484596712949170fb3ca1dc"}, @@ -283,11 +294,29 @@ files = [ [package.extras] tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"] +[[package]] +name = "bases" +version = "0.3.0" +description = "Python library for general Base-N encodings." +optional = false +python-versions = ">=3.7" +files = [ + {file = "bases-0.3.0-py3-none-any.whl", hash = "sha256:a2fef3366f3e522ff473d2e95c21523fe8e44251038d5c6150c01481585ebf5b"}, + {file = "bases-0.3.0.tar.gz", hash = "sha256:70f04a4a45d63245787f9e89095ca11042685b6b64b542ad916575ba3ccd1570"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0" +typing-validation = ">=1.1.0" + +[package.extras] +dev = ["base58", "mypy", "pylint", "pytest", "pytest-cov"] + [[package]] name = "cached-property" version = "1.5.2" description = "A decorator for caching properties in classes." -optional = true +optional = false python-versions = "*" files = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, @@ -855,6 +884,25 @@ files = [ [package.dependencies] base58 = ">=2.1.1" +[[package]] +name = "did-tdw" +version = "0.2.1" +description = "This repository includes Python libraries for working with `did:tdw` (Trust DID Web) DID documents and the underlying log format." +optional = false +python-versions = "<4.0,>=3.10" +files = [ + {file = "did_tdw-0.2.1-py3-none-any.whl", hash = "sha256:80c727d0bef37e2211d3caddb97ba3c4aa508c67d4ef502da5f326d9bf4c3ffb"}, + {file = "did_tdw-0.2.1.tar.gz", hash = "sha256:a61ed9f49369ea4c365e5e380431feae8cb3988375de37f73be2abe15d0bfde6"}, +] + +[package.dependencies] +aiofiles = ">=24.1.0,<25.0.0" +aiohttp = ">=3.10.5,<4.0.0" +aries-askar = ">=0.3.2,<0.4.0" +base58 = ">=2.1.0,<2.2.0" +jsoncanon = ">=0.2.3,<0.3.0" +multiformats = ">=0.3.1,<0.4.0" + [[package]] name = "didcomm-messaging" version = "0.1.1" @@ -1258,6 +1306,17 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jsoncanon" +version = "0.2.3" +description = "Typed Python implementation of JSON Canonicalization Scheme as described in RFC 8785. Currently lacks full floating point support" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "jsoncanon-0.2.3-py3-none-any.whl", hash = "sha256:adb35dac2d0c5dd56f1cb374f1ea6f1fff2ebbb4e844b06d9c96b9ccadf12bf0"}, + {file = "jsoncanon-0.2.3.tar.gz", hash = "sha256:483c1ef14e6c8151ba69c0bf646551f249698dd523e9c6da1339a688c5f96d6d"}, +] + [[package]] name = "jsonpath-ng" version = "1.7.0" @@ -1644,6 +1703,44 @@ files = [ {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] +[[package]] +name = "multiformats" +version = "0.3.1.post4" +description = "Python implementation of multiformats protocols." +optional = false +python-versions = ">=3.7" +files = [ + {file = "multiformats-0.3.1.post4-py3-none-any.whl", hash = "sha256:5b1d61bd8275c9e817bdbee38dbd501b26629011962ee3c86c46e7ccd0b14129"}, + {file = "multiformats-0.3.1.post4.tar.gz", hash = "sha256:d00074fdbc7d603c2084b4c38fa17bbc28173cf2750f51f46fbbc5c4d5605fbb"}, +] + +[package.dependencies] +bases = ">=0.3.0" +multiformats-config = ">=0.3.0" +typing-extensions = ">=4.6.0" +typing-validation = ">=1.1.0" + +[package.extras] +dev = ["blake3", "mmh3", "mypy", "pycryptodomex", "pylint", "pyskein", "pytest", "pytest-cov", "rich"] +full = ["blake3", "mmh3", "pycryptodomex", "pyskein", "rich"] + +[[package]] +name = "multiformats-config" +version = "0.3.1" +description = "Pre-loading configuration module for the 'multiformats' package." +optional = false +python-versions = ">=3.7" +files = [ + {file = "multiformats-config-0.3.1.tar.gz", hash = "sha256:7eaa80ef5d9c5ee9b86612d21f93a087c4a655cbcb68960457e61adbc62b47a7"}, + {file = "multiformats_config-0.3.1-py3-none-any.whl", hash = "sha256:dec4c9d42ed0d9305889b67440f72e8e8d74b82b80abd7219667764b5b0a8e1d"}, +] + +[package.dependencies] +multiformats = "*" + +[package.extras] +dev = ["mypy", "pylint", "pytest", "pytest-cov"] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -2746,6 +2843,20 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-validation" +version = "1.2.11.post4" +description = "A simple library for runtime type-checking." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_validation-1.2.11.post4-py3-none-any.whl", hash = "sha256:73dd504ddebf5210e80d5f65ba9b30efbd0fa42f266728fda7c4f0ba335c699c"}, + {file = "typing_validation-1.2.11.post4.tar.gz", hash = "sha256:7aed04ecfbda07e63b7266f90e5d096f96344f7facfe04bb081b21e4a9781670"}, +] + +[package.extras] +dev = ["mypy", "pylint", "pytest", "pytest-cov", "rich"] + [[package]] name = "unflatten" version = "0.2.0" @@ -3028,4 +3139,4 @@ didcommv2 = ["didcomm-messaging"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "d5ab2d9c2721f87d92ca77cc8bf7a044cfc2c41b58b111af84a454ac2cc96910" +content-hash = "543d20028efad5a9d798071ba86e9d1ab39b202fe1afaa27f8f4c7f656f1d7cc" diff --git a/pyproject.toml b/pyproject.toml index 0064c4701d..7de97f37b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,9 +45,13 @@ requests = "~2.32.3" rlp = "4.0.1" unflatten = "~0.2" sd-jwt = "^0.10.3" +uuid_utils = "^0.9.0" + +# did libraries did-peer-2 = "^0.1.2" did-peer-4 = "^0.1.4" -uuid_utils = "^0.9.0" +did-tdw = "^0.2.1" + # askar aries-askar = { version = "~0.3.2", optional = true }