Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multiple kid per key feature, kid removal and get by kid routes #3472

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions acapy_agent/wallet/askar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -131,14 +135,58 @@ 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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
key_type = key_types.from_key_type(key.algorithm.value)
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should catch the correct type of exception for removing entry that doesn't exist. Can't remember of the top of my head.

In general try to only catch the base Exception class when nessecary.

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:
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions acapy_agent/wallet/did_info.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand Down
8 changes: 7 additions & 1 deletion acapy_agent/wallet/keys/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +22,12 @@
"prefix_hex": "8024",
"prefix_length": 2,
},
"bls12381g2": {
"key_type": BLS12381G1G2,
"multikey_prefix": "zUC7",
"prefix_hex": "eb01",
"prefix_length": 2,
},
}


Expand Down
81 changes: 77 additions & 4 deletions acapy_agent/wallet/keys/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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,
},
)

Expand All @@ -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."""

Expand Down Expand Up @@ -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),
]
)
Loading