From 6d80bf368b365a46804ebfb3446926df0e62a013 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Mon, 27 Jan 2025 20:01:16 +0000 Subject: [PATCH 1/5] add multiple kid per key feature, kid removal and get by kid routes Signed-off-by: PatStLouis --- acapy_agent/wallet/askar.py | 53 +++++++++++++++++++-- acapy_agent/wallet/did_info.py | 4 +- acapy_agent/wallet/keys/routes.py | 77 +++++++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 9 deletions(-) diff --git a/acapy_agent/wallet/askar.py b/acapy_agent/wallet/askar.py index 4be4ce29e4..fc9a5c68db 100644 --- a/acapy_agent/wallet/askar.py +++ b/acapy_agent/wallet/askar.py @@ -20,6 +20,7 @@ from .did_method import SOV, DIDMethod, DIDMethods from .did_parameters_validation import DIDParametersValidation from .error import WalletDuplicateError, WalletError, WalletNotFoundError +from .keys.manager import verkey_to_multikey from .key_type import BLS12381G2, ED25519, P256, X25519, KeyType, KeyTypes from .util import b58_to_bytes, bytes_to_b58 @@ -94,11 +95,12 @@ async def create_key( if metadata is None: metadata = {} - tags = {"kid": kid} if kid else None - try: keypair = _create_keypair(key_type, seed) verkey = bytes_to_b58(keypair.get_public_bytes()) + multikey = verkey_to_multikey(verkey, alg=key_type.key_type) + default_kid = f'did:key:{multikey}#{multikey}' + tags = {"kid": [default_kid, kid]} if kid else [default_kid] await self._session.handle.insert_key( verkey, keypair, @@ -131,6 +133,15 @@ async def assign_kid_to_key(self, verkey: str, kid: str) -> KeyInfo: if not key_entry: raise WalletNotFoundError(f"No key entry found for verkey {verkey}") + try: + existing_kid = key_entry.tags.get("kid") + except Exception: + existing_kid = [] + + existing_kid = existing_kid if isinstance(existing_kid, list) else [existing_kid] + existing_kid.append(kid) + tags = {"kid": existing_kid} + key = cast(Key, key_entry.key) metadata = cast(dict, key_entry.metadata) key_types = self.session.inject(KeyTypes) @@ -138,7 +149,42 @@ async def assign_kid_to_key(self, verkey: str, kid: str) -> KeyInfo: if not key_type: raise WalletError(f"Unknown key type {key.algorithm.value}") - await self._session.handle.update_key(name=verkey, tags={"kid": kid}) + await self._session.handle.update_key(name=verkey, tags=tags) + return KeyInfo(verkey=verkey, metadata=metadata, key_type=key_type, kid=kid) + + async def remove_kid_from_key(self, kid: str) -> KeyInfo: + """Remove a kid association. + + Args: + kid: the key identifier + + Returns: + The key identified by kid + + """ + key_entries = await self._session.handle.fetch_all_keys( + tag_filter={"kid": kid}, limit=2 + ) + if len(key_entries) > 1: + raise WalletDuplicateError(f"More than one key found by kid {kid}") + + entry = key_entries[0] + existing_kid = entry.tags.get("kid") + key = cast(Key, entry.key) + verkey = bytes_to_b58(key.get_public_bytes()) + metadata = cast(dict, entry.metadata) + key_types = self.session.inject(KeyTypes) + key_type = key_types.from_key_type(key.algorithm.value) + if not key_type: + raise WalletError(f"Unknown key type {key.algorithm.value}") + try: + existing_kid.remove(kid) + except Exception: + pass + + tags = {'kid': existing_kid} + + await self._session.handle.update_key(name=verkey, tags=tags) return KeyInfo(verkey=verkey, metadata=metadata, key_type=key_type, kid=kid) async def get_key_by_kid(self, kid: str) -> KeyInfo: @@ -158,6 +204,7 @@ async def get_key_by_kid(self, kid: str) -> KeyInfo: raise WalletDuplicateError(f"More than one key found by kid {kid}") entry = key_entries[0] + kid = entry.tags.get("kid") key = cast(Key, entry.key) verkey = bytes_to_b58(key.get_public_bytes()) metadata = cast(dict, entry.metadata) diff --git a/acapy_agent/wallet/did_info.py b/acapy_agent/wallet/did_info.py index e3a539616b..ebfbecebca 100644 --- a/acapy_agent/wallet/did_info.py +++ b/acapy_agent/wallet/did_info.py @@ -1,6 +1,6 @@ """KeyInfo, DIDInfo.""" -from typing import NamedTuple +from typing import NamedTuple, Union, List from .did_method import DIDMethod from .key_type import KeyType @@ -14,7 +14,7 @@ class KeyInfo(NamedTuple): verkey: str metadata: dict key_type: KeyType - kid: str = None + kid: Union[List[str], str] = None DIDInfo = NamedTuple( diff --git a/acapy_agent/wallet/keys/routes.py b/acapy_agent/wallet/keys/routes.py index 46647fc9b9..e200ffac9d 100644 --- a/acapy_agent/wallet/keys/routes.py +++ b/acapy_agent/wallet/keys/routes.py @@ -3,7 +3,7 @@ import logging from aiohttp import web -from aiohttp_apispec import docs, request_schema, response_schema +from aiohttp_apispec import docs, request_schema, response_schema, querystring_schema from marshmallow import fields from ...admin.decorators.auth import tenant_authentication @@ -14,6 +14,7 @@ LOGGER = logging.getLogger(__name__) +GENERIC_KID_EXAMPLE = "did:web:example.com#key-01" class CreateKeyRequestSchema(OpenAPISchema): """Request schema for creating a new key.""" @@ -43,7 +44,7 @@ class CreateKeyRequestSchema(OpenAPISchema): "description": ( "Optional kid to bind to the keypair, such as a verificationMethod." ), - "example": "did:web:example.com#key-01", + "example": GENERIC_KID_EXAMPLE, }, ) @@ -61,11 +62,27 @@ class CreateKeyResponseSchema(OpenAPISchema): kid = fields.Str( metadata={ "description": "The associated kid", - "example": "did:web:example.com#key-01", + "example": GENERIC_KID_EXAMPLE, }, ) +class FetchKeyQueryStringSchema(OpenAPISchema): + """Parameters for key request query string.""" + + kid = fields.Str( + required=True, + metadata={"description": "KID of interest", "example": GENERIC_KID_EXAMPLE}, + ) + +class DeleteKidQueryStringSchema(OpenAPISchema): + """Parameters for kid delete request query string.""" + + kid = fields.Str( + required=True, + metadata={"description": "KID of interest", "example": GENERIC_KID_EXAMPLE}, + ) + class UpdateKeyRequestSchema(OpenAPISchema): """Request schema for updating an existing key pair.""" @@ -218,13 +235,65 @@ async def update_key(request: web.BaseRequest): return web.json_response({"message": str(err)}, status=400) +@docs(tags=["wallet"], summary="Fetch key info.") +@querystring_schema(FetchKeyQueryStringSchema()) +@response_schema(FetchKeyResponseSchema, 200, description="") +@tenant_authentication +async def fetch_key_by_kid(request: web.BaseRequest): + """Request handler for fetching a key. + + Args: + request: aiohttp request object + + """ + context: AdminRequestContext = request["context"] + filter_kid = request.query.get("kid") + + try: + async with context.session() as session: + key_info = await MultikeyManager(session).get_key_by_kid(kid=filter_kid) + return web.json_response( + key_info, + status=200, + ) + + except (MultikeyManagerError, WalletDuplicateError, WalletNotFoundError) as err: + return web.json_response({"message": str(err)}, status=400) + +@docs(tags=["wallet"], summary="Fetch key info.") +@querystring_schema(DeleteKidQueryStringSchema()) +@tenant_authentication +async def remove_kid(request: web.BaseRequest): + """Request handler for fetching a key. + + Args: + request: aiohttp request object + + """ + context: AdminRequestContext = request["context"] + filter_kid = request.query.get("kid") + + try: + async with context.session() as session: + key_info = await MultikeyManager(session).get_key_by_kid(kid=filter_kid) + return web.json_response( + key_info, + status=200, + ) + + except (MultikeyManagerError, WalletDuplicateError, WalletNotFoundError) as err: + return web.json_response({"message": str(err)}, status=400) + + async def register(app: web.Application): """Register routes.""" app.add_routes( [ - web.get("/wallet/keys/{multikey}", fetch_key, allow_head=False), web.post("/wallet/keys", create_key), web.put("/wallet/keys", update_key), + web.get("/wallet/keys/{multikey}", fetch_key, allow_head=False), + web.get("/wallet/keys", fetch_key_by_kid, allow_head=False), + web.delete("/wallet/keys", remove_kid, allow_head=False), ] ) From 912f3b7477a8ab6a1b482e63b0236f87cf749320 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Mon, 27 Jan 2025 20:02:15 +0000 Subject: [PATCH 2/5] linting Signed-off-by: PatStLouis --- acapy_agent/wallet/askar.py | 12 ++++++------ acapy_agent/wallet/keys/routes.py | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/acapy_agent/wallet/askar.py b/acapy_agent/wallet/askar.py index fc9a5c68db..902039079e 100644 --- a/acapy_agent/wallet/askar.py +++ b/acapy_agent/wallet/askar.py @@ -99,7 +99,7 @@ async def create_key( keypair = _create_keypair(key_type, seed) verkey = bytes_to_b58(keypair.get_public_bytes()) multikey = verkey_to_multikey(verkey, alg=key_type.key_type) - default_kid = f'did:key:{multikey}#{multikey}' + default_kid = f"did:key:{multikey}#{multikey}" tags = {"kid": [default_kid, kid]} if kid else [default_kid] await self._session.handle.insert_key( verkey, @@ -137,11 +137,11 @@ async def assign_kid_to_key(self, verkey: str, kid: str) -> KeyInfo: existing_kid = key_entry.tags.get("kid") except Exception: existing_kid = [] - + existing_kid = existing_kid if isinstance(existing_kid, list) else [existing_kid] existing_kid.append(kid) tags = {"kid": existing_kid} - + key = cast(Key, key_entry.key) metadata = cast(dict, key_entry.metadata) key_types = self.session.inject(KeyTypes) @@ -181,9 +181,9 @@ async def remove_kid_from_key(self, kid: str) -> KeyInfo: existing_kid.remove(kid) except Exception: pass - - tags = {'kid': existing_kid} - + + tags = {"kid": existing_kid} + await self._session.handle.update_key(name=verkey, tags=tags) return KeyInfo(verkey=verkey, metadata=metadata, key_type=key_type, kid=kid) diff --git a/acapy_agent/wallet/keys/routes.py b/acapy_agent/wallet/keys/routes.py index e200ffac9d..f891629372 100644 --- a/acapy_agent/wallet/keys/routes.py +++ b/acapy_agent/wallet/keys/routes.py @@ -16,6 +16,7 @@ GENERIC_KID_EXAMPLE = "did:web:example.com#key-01" + class CreateKeyRequestSchema(OpenAPISchema): """Request schema for creating a new key.""" @@ -75,6 +76,7 @@ class FetchKeyQueryStringSchema(OpenAPISchema): metadata={"description": "KID of interest", "example": GENERIC_KID_EXAMPLE}, ) + class DeleteKidQueryStringSchema(OpenAPISchema): """Parameters for kid delete request query string.""" @@ -83,6 +85,7 @@ class DeleteKidQueryStringSchema(OpenAPISchema): metadata={"description": "KID of interest", "example": GENERIC_KID_EXAMPLE}, ) + class UpdateKeyRequestSchema(OpenAPISchema): """Request schema for updating an existing key pair.""" @@ -260,6 +263,7 @@ async def fetch_key_by_kid(request: web.BaseRequest): except (MultikeyManagerError, WalletDuplicateError, WalletNotFoundError) as err: return web.json_response({"message": str(err)}, status=400) + @docs(tags=["wallet"], summary="Fetch key info.") @querystring_schema(DeleteKidQueryStringSchema()) @tenant_authentication From d8a3be8a6564b71b74e13b00f3ddab5dba9849a3 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Mon, 27 Jan 2025 20:28:43 +0000 Subject: [PATCH 3/5] remove default kid, conflict with did key Signed-off-by: PatStLouis --- acapy_agent/wallet/askar.py | 7 ++++--- acapy_agent/wallet/keys/manager.py | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/acapy_agent/wallet/askar.py b/acapy_agent/wallet/askar.py index 902039079e..fa957b55de 100644 --- a/acapy_agent/wallet/askar.py +++ b/acapy_agent/wallet/askar.py @@ -98,9 +98,10 @@ async def create_key( try: keypair = _create_keypair(key_type, seed) verkey = bytes_to_b58(keypair.get_public_bytes()) - multikey = verkey_to_multikey(verkey, alg=key_type.key_type) - default_kid = f"did:key:{multikey}#{multikey}" - tags = {"kid": [default_kid, kid]} if kid else [default_kid] + # multikey = verkey_to_multikey(verkey, alg=key_type.key_type) + # default_kid = f"did:key:{multikey}#{multikey}" + # tags = {"kid": [default_kid, kid]} if kid else [default_kid] + tags = {"kid": [kid]} if kid else None await self._session.handle.insert_key( verkey, keypair, diff --git a/acapy_agent/wallet/keys/manager.py b/acapy_agent/wallet/keys/manager.py index bab88b3dcb..92d667b6eb 100644 --- a/acapy_agent/wallet/keys/manager.py +++ b/acapy_agent/wallet/keys/manager.py @@ -2,7 +2,7 @@ from ...core.profile import ProfileSession from ..base import BaseWallet -from ..key_type import ED25519, P256, KeyType +from ..key_type import ED25519, P256, BLS12381G1G2, KeyType from ..util import b58_to_bytes, bytes_to_b58 from ...utils.multiformats import multibase from ...wallet.error import WalletNotFoundError @@ -22,6 +22,12 @@ "prefix_hex": "8024", "prefix_length": 2, }, + "bls12381g2": { + "key_type": BLS12381G1G2, + "multikey_prefix": "zUC7", + "prefix_hex": "eb01", + "prefix_length": 2, + }, } From 01c80862cd87fcb88694f1a66feb61a76debb133 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Mon, 27 Jan 2025 20:32:55 +0000 Subject: [PATCH 4/5] comment unused import Signed-off-by: PatStLouis --- acapy_agent/wallet/askar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acapy_agent/wallet/askar.py b/acapy_agent/wallet/askar.py index fa957b55de..07cfc45018 100644 --- a/acapy_agent/wallet/askar.py +++ b/acapy_agent/wallet/askar.py @@ -20,7 +20,7 @@ from .did_method import SOV, DIDMethod, DIDMethods from .did_parameters_validation import DIDParametersValidation from .error import WalletDuplicateError, WalletError, WalletNotFoundError -from .keys.manager import verkey_to_multikey +# from .keys.manager import verkey_to_multikey from .key_type import BLS12381G2, ED25519, P256, X25519, KeyType, KeyTypes from .util import b58_to_bytes, bytes_to_b58 From a62e42905838811e7423821628efee30d75b8e02 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Mon, 27 Jan 2025 20:35:54 +0000 Subject: [PATCH 5/5] linting Signed-off-by: PatStLouis --- acapy_agent/wallet/askar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/acapy_agent/wallet/askar.py b/acapy_agent/wallet/askar.py index 07cfc45018..94a3030f8d 100644 --- a/acapy_agent/wallet/askar.py +++ b/acapy_agent/wallet/askar.py @@ -20,6 +20,7 @@ from .did_method import SOV, DIDMethod, DIDMethods from .did_parameters_validation import DIDParametersValidation from .error import WalletDuplicateError, WalletError, WalletNotFoundError + # from .keys.manager import verkey_to_multikey from .key_type import BLS12381G2, ED25519, P256, X25519, KeyType, KeyTypes from .util import b58_to_bytes, bytes_to_b58