From 96e6dbf6713305ab3ab3ec3774f75ee6fd0eb289 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 1 Dec 2023 17:06:26 -0500 Subject: [PATCH 1/2] feat: add did:jwk resolver This implements a did:jwk resolver. See https://github.com/hyperledger/aries-acapy-plugins/pull/47 for context. Signed-off-by: Daniel Bluhm --- aries_cloudagent/resolver/__init__.py | 6 ++ aries_cloudagent/resolver/default/jwk.py | 71 +++++++++++++++++++ .../resolver/default/tests/test_jwk.py | 54 ++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 aries_cloudagent/resolver/default/jwk.py create mode 100644 aries_cloudagent/resolver/default/tests/test_jwk.py diff --git a/aries_cloudagent/resolver/__init__.py b/aries_cloudagent/resolver/__init__.py index a72b017dbe..82efd99987 100644 --- a/aries_cloudagent/resolver/__init__.py +++ b/aries_cloudagent/resolver/__init__.py @@ -29,6 +29,12 @@ async def setup(context: InjectionContext): await key_resolver.setup(context) registry.register_resolver(key_resolver) + jwk_resolver = ClassProvider( + "aries_cloudagent.resolver.default.jwk.JwkDIDResolver" + ).provide(context.settings, context.injector) + await jwk_resolver.setup(context) + registry.register_resolver(jwk_resolver) + if not context.settings.get("ledger.disabled"): indy_resolver = ClassProvider( "aries_cloudagent.resolver.default.indy.IndyDIDResolver" diff --git a/aries_cloudagent/resolver/default/jwk.py b/aries_cloudagent/resolver/default/jwk.py new file mode 100644 index 0000000000..ba1bc71393 --- /dev/null +++ b/aries_cloudagent/resolver/default/jwk.py @@ -0,0 +1,71 @@ +"""did:jwk: resolver implementation.""" + +import re +from typing import Optional, Pattern, Sequence, Text +from aries_cloudagent.config.injection_context import InjectionContext +from aries_cloudagent.core.profile import Profile +from aries_cloudagent.resolver.base import BaseDIDResolver, ResolverType, ResolverError +from aries_cloudagent.wallet.jwt import b64_to_dict + + +class JwkDIDResolver(BaseDIDResolver): + """did:jwk: resolver implementation.""" + + PATTERN = re.compile(r"^did:jwk:(?P.*)$") + + def __init__(self): + """Initialize the resolver.""" + super().__init__(ResolverType.NATIVE) + + async def setup(self, context: InjectionContext): + """Perform required setup for the resolver.""" + pass + + @property + def supported_did_regex(self) -> Pattern: + """Return supported DID regex.""" + return self.PATTERN + + async def _resolve( + self, + profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: + """Resolve a DID.""" + if match := self.PATTERN.match(did): + encoded = match.group("did") + else: + raise ResolverError(f"Invalid DID: {did}") + + jwk = b64_to_dict(encoded) + doc = { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + ], + "id": f"did:jwk:{encoded}", + "verificationMethod": [ + { + "id": f"did:jwk:{encoded}#0", + "type": "JsonWebKey2020", + "controller": f"did:jwk:{encoded}", + "publicKeyJwk": jwk, + } + ], + } + + use = jwk.get("use") + if use == "sig": + doc.update( + { + "assertionMethod": [f"did:jwk:{encoded}#0"], + "authentication": [f"did:jwk:{encoded}#0"], + "capabilityInvocation": [f"did:jwk:{encoded}#0"], + "capabilityDelegation": [f"did:jwk:{encoded}#0"], + } + ) + elif use == "enc": + doc.update({"keyAgreement": [f"did:jwk:{encoded}#0"]}) + + return doc diff --git a/aries_cloudagent/resolver/default/tests/test_jwk.py b/aries_cloudagent/resolver/default/tests/test_jwk.py new file mode 100644 index 0000000000..2a467a6359 --- /dev/null +++ b/aries_cloudagent/resolver/default/tests/test_jwk.py @@ -0,0 +1,54 @@ +"""Test JwkDIDResolver.""" + +import pytest + +from ....core.in_memory import InMemoryProfile +from ....core.profile import Profile + +from ...base import DIDNotFound +from ..jwk import JwkDIDResolver + +# pylint: disable=W0621 +TEST_DIDS = [ + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", + "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", +] + +TEST_DID_INVALID = "did:key:z1MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + + +@pytest.fixture +def resolver(): + """Resolver fixture.""" + yield JwkDIDResolver() + + +@pytest.fixture +def profile(): + """Profile fixture.""" + profile = InMemoryProfile.test_profile() + yield profile + + +@pytest.mark.asyncio +@pytest.mark.parametrize("did", TEST_DIDS) +async def test_supported_did_regex(profile, resolver: JwkDIDResolver, did: str): + """Test the supported_did_regex.""" + assert await resolver.supports( + profile, + did, + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("did", TEST_DIDS) +async def test_resolve(resolver: JwkDIDResolver, profile: Profile, did: str): + """Test resolve method.""" + assert await resolver.resolve(profile, did) + + +@pytest.mark.asyncio +async def test_resolve_x_did_not_found(resolver: JwkDIDResolver, profile: Profile): + """Test resolve method when no did is found.""" + with pytest.raises(DIDNotFound): + await resolver.resolve(profile, TEST_DID_INVALID) From b25c23957882056fb660c0a421e40ed813ce5f53 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 1 Dec 2023 17:11:50 -0500 Subject: [PATCH 2/2] fix: bad test in jwk resolver Signed-off-by: Daniel Bluhm --- aries_cloudagent/resolver/default/tests/test_jwk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/resolver/default/tests/test_jwk.py b/aries_cloudagent/resolver/default/tests/test_jwk.py index 2a467a6359..9298c41d51 100644 --- a/aries_cloudagent/resolver/default/tests/test_jwk.py +++ b/aries_cloudagent/resolver/default/tests/test_jwk.py @@ -5,7 +5,7 @@ from ....core.in_memory import InMemoryProfile from ....core.profile import Profile -from ...base import DIDNotFound +from ...base import DIDMethodNotSupported from ..jwk import JwkDIDResolver # pylint: disable=W0621 @@ -50,5 +50,5 @@ async def test_resolve(resolver: JwkDIDResolver, profile: Profile, did: str): @pytest.mark.asyncio async def test_resolve_x_did_not_found(resolver: JwkDIDResolver, profile: Profile): """Test resolve method when no did is found.""" - with pytest.raises(DIDNotFound): + with pytest.raises(DIDMethodNotSupported): await resolver.resolve(profile, TEST_DID_INVALID)