diff --git a/acapy_agent/wallet/askar.py b/acapy_agent/wallet/askar.py index 4be4ce29e4..94a3030f8d 100644 --- a/acapy_agent/wallet/askar.py +++ b/acapy_agent/wallet/askar.py @@ -20,6 +20,8 @@ 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 +96,13 @@ 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] + tags = {"kid": [kid]} if kid else None await self._session.handle.insert_key( verkey, keypair, @@ -131,6 +135,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 +151,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 +206,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/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, + }, } diff --git a/acapy_agent/wallet/keys/routes.py b/acapy_agent/wallet/keys/routes.py index 46647fc9b9..f891629372 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,8 @@ 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 +45,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 +63,29 @@ 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 +238,66 @@ 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), ] )