Skip to content
Merged
67 changes: 60 additions & 7 deletions acapy_agent/wallet/askar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .did_parameters_validation import DIDParametersValidation
from .error import WalletDuplicateError, WalletError, WalletNotFoundError
from .key_type import BLS12381G2, ED25519, P256, X25519, KeyType, KeyTypes
from .keys.manager import verkey_to_multikey
from .util import b58_to_bytes, bytes_to_b58

CATEGORY_DID = "did"
Expand Down Expand Up @@ -94,11 +95,14 @@ 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())
tags = {
"multikey": verkey_to_multikey(verkey, key_type.key_type),
"kid": [kid] if kid else [],
}

await self._session.handle.insert_key(
verkey,
keypair,
Expand Down Expand Up @@ -138,7 +142,57 @@ 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})
tags = key_entry.tags or {"kid": []}
key_ids = tags.get("kid", [])
key_ids = key_ids if isinstance(key_ids, list) else [key_ids]
key_ids.append(kid)
tags["kid"] = key_ids

await self._session.handle.update_key(name=verkey, tags=tags)
return KeyInfo(verkey=verkey, metadata=metadata, key_type=key_type, kid=kid)

async def unassign_kid_from_key(self, verkey: str, kid: str) -> KeyInfo:
"""Remove a kid association.

Args:
kid: The key identifier
verkey: The verification key of the keypair

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}")

key_entry = key_entries[0]
key = cast(Key, key_entry.key)
fetched_verkey = bytes_to_b58(key.get_public_bytes())

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}")

if fetched_verkey != verkey:
raise WalletError(f"Multikey mismatch: {fetched_verkey} != {verkey}")

key_tags = key_entry.tags or {"kid": []}
key_kids = key_tags.get("kid", [])
key_kids = key_kids if isinstance(key_kids, list) else [key_kids]

try:
key_kids.remove(kid)
except ValueError:
pass

key_tags["kid"] = key_kids

await self._session.handle.update_key(name=verkey, tags=key_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 @@ -156,6 +210,8 @@ async def get_key_by_kid(self, kid: str) -> KeyInfo:
)
if len(key_entries) > 1:
raise WalletDuplicateError(f"More than one key found by kid {kid}")
elif len(key_entries) < 1:
raise WalletNotFoundError(f"No key found for kid {kid}")

entry = key_entries[0]
key = cast(Key, entry.key)
Expand Down Expand Up @@ -190,10 +246,7 @@ async def get_signing_key(self, verkey: str) -> KeyInfo:
raise WalletNotFoundError("Unknown key: {}".format(verkey))
metadata = json.loads(key_entry.metadata or "{}")

try:
kid = key_entry.tags.get("kid")
except Exception:
kid = None
kid = key_entry.tags.get("kid", []) if key_entry.tags else []

key = cast(Key, key_entry.key)
key_types = self.session.inject(KeyTypes)
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
69 changes: 45 additions & 24 deletions acapy_agent/wallet/keys/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ...core.profile import ProfileSession
from ...resolver.did_resolver import DIDResolver
from ...utils.multiformats import multibase
from ...wallet.error import WalletNotFoundError
from ...wallet.error import WalletError, WalletDuplicateError, WalletNotFoundError
from ..base import BaseWallet
from ..key_type import BLS12381G2, ED25519, P256, KeyType
from ..util import b58_to_bytes, bytes_to_b58
Expand All @@ -20,6 +20,12 @@
"prefix_hex": "ed01",
"prefix_length": 2,
},
"x25519": {
"key_type": ED25519,
"multikey_prefix": "z6LS",
"prefix_hex": "ec01",
"prefix_length": 2,
},
"p256": {
"key_type": P256,
"multikey_prefix": "zDn",
Expand Down Expand Up @@ -107,11 +113,11 @@ async def resolve_and_bind_kid(self, kid: str):
This function is idempotent.
"""
if await self.kid_exists(kid):
LOGGER.debug(f"kid {kid} already bound in storage, will not resolve.")
LOGGER.info(f"kid {kid} already bound in storage, will not resolve.")
return await self.from_kid(kid)
else:
multikey = await self.resolve_multikey_from_verification_method_id(kid)
LOGGER.debug(
LOGGER.info(
f"kid {kid} binding not found in storage, \
binding to resolved multikey {multikey}."
)
Expand Down Expand Up @@ -165,14 +171,18 @@ async def multikey_exists(self, multikey: str):
async def from_kid(self, kid: str):
"""Fetch a single key."""

key_info = await self.wallet.get_key_by_kid(kid=kid)

return {
"kid": key_info.kid,
"multikey": verkey_to_multikey(
key_info.verkey, alg=key_info.key_type.key_type
),
}
try:
key_info = await self.wallet.get_key_by_kid(kid=kid)

return {
"kid": key_info.kid,
"multikey": verkey_to_multikey(
key_info.verkey, alg=key_info.key_type.key_type
),
}
except (WalletDuplicateError, WalletError, WalletNotFoundError) as err:
LOGGER.info(err)
Comment thread
PatStLouis marked this conversation as resolved.
Outdated
return None

async def from_multikey(self, multikey: str):
"""Fetch a single key."""
Expand Down Expand Up @@ -205,19 +215,30 @@ async def create(self, seed: str = None, kid: str = None, alg: str = DEFAULT_ALG
"multikey": verkey_to_multikey(key_info.verkey, alg=alg),
}

async def update(self, multikey: str, kid: str):
"""Assign a new kid to a key pair."""
async def update(self, multikey: str, kid: str, unbind=False):
"""Bind or unbind a kid with a key pair."""
(
await self.unbind_key_id(multikey, kid)
if unbind
else await self.bind_key_id(multikey, kid)
)

if kid and await self.kid_exists(kid=kid):
raise MultikeyManagerError(f"kid '{kid}' already exists in wallet.")
return {"kid": kid, "multikey": multikey}

key_info = await self.wallet.assign_kid_to_key(
verkey=multikey_to_verkey(multikey), kid=kid
)
async def bind_key_id(self, multikey: str, kid: str):
"""Bind a new key id to a key pair."""
try:
return await self.wallet.assign_kid_to_key(multikey_to_verkey(multikey), kid)
except (WalletError, WalletDuplicateError, WalletNotFoundError) as err:
LOGGER.info(err)
Comment thread
PatStLouis marked this conversation as resolved.
Outdated
raise MultikeyManagerError(err)

return {
"kid": key_info.kid,
"multikey": verkey_to_multikey(
key_info.verkey, alg=key_info.key_type.key_type
),
}
async def unbind_key_id(self, multikey: str, kid: str):
"""Unbind a key id from a key pair."""
try:
return await self.wallet.unassign_kid_from_key(
multikey_to_verkey(multikey), kid
)
except (WalletError, WalletDuplicateError, WalletNotFoundError) as err:
LOGGER.info(err)
Comment thread
PatStLouis marked this conversation as resolved.
Outdated
raise MultikeyManagerError(err)
40 changes: 29 additions & 11 deletions acapy_agent/wallet/keys/tests/test_key_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,48 @@ async def test_key_creation(self):
(self.bls12381g2_alg, self.bls12381g2_multikey),
]
):
manager = MultikeyManager(session=session)
kid = f"did:web:example.com#key-0{i}"

key_info = await MultikeyManager(session=session).create(
seed=self.seed, alg=alg
)
key_info = await manager.create(seed=self.seed, alg=alg)
assert key_info["multikey"] == expected_multikey
assert key_info["kid"] is None

key_info = await MultikeyManager(session=session).from_multikey(
multikey=expected_multikey
)
key_info = await manager.from_multikey(multikey=expected_multikey)
assert key_info["multikey"] == expected_multikey
assert key_info["kid"] is None
assert key_info["kid"] == []

key_info = await MultikeyManager(session=session).update(
multikey=expected_multikey, kid=kid
)
key_info = await manager.update(multikey=expected_multikey, kid=kid)
assert key_info["multikey"] == expected_multikey
assert key_info["kid"] == kid

key_info = await MultikeyManager(session=session).from_kid(kid=kid)
key_info = await manager.from_kid(kid=kid)
assert key_info["multikey"] == expected_multikey
assert key_info["kid"] == kid

async def test_key_id_binding(self):
async with self.profile.session() as session:
test_multikey = self.ed25519_multikey
key_id_01 = "did:web:example.com#key-01"
key_id_02 = "did:web:example.com#key-02"
key_id_03 = "did:web:example.com#key-03"

manager = MultikeyManager(session=session)

await manager.create(self.seed, key_id_01, self.ed25519_alg)
await manager.bind_key_id(test_multikey, key_id_02)
await manager.bind_key_id(test_multikey, key_id_03)

assert (await manager.from_kid(key_id_01))["multikey"] == test_multikey
assert (await manager.from_kid(key_id_02))["multikey"] == test_multikey
assert (await manager.from_kid(key_id_03))["multikey"] == test_multikey

await manager.unbind_key_id(test_multikey, key_id_01)

assert (await manager.from_kid(key_id_01)) is None
assert (await manager.from_kid(key_id_02))["multikey"] == test_multikey
assert (await manager.from_kid(key_id_03))["multikey"] == test_multikey

async def test_key_transformations(self):
for alg, multikey, verkey in [
(self.ed25519_alg, self.ed25519_multikey, self.ed25519_verkey),
Expand Down