Skip to content

Commit

Permalink
Add did:indy support
Browse files Browse the repository at this point in the history
Signed-off-by: jamshale <[email protected]>
  • Loading branch information
jamshale committed Oct 30, 2024
1 parent f499871 commit 8889e7f
Show file tree
Hide file tree
Showing 26 changed files with 878 additions and 138 deletions.
5 changes: 3 additions & 2 deletions acapy_agent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,7 @@ async def load_plugins(self, context: InjectionContext):

# Currently providing admin routes only
plugin_registry.register_plugin("acapy_agent.holder")

plugin_registry.register_plugin("acapy_agent.ledger")

plugin_registry.register_plugin("acapy_agent.messaging.jsonld")
plugin_registry.register_plugin("acapy_agent.resolver")
plugin_registry.register_plugin("acapy_agent.settings")
Expand All @@ -143,6 +141,9 @@ async def load_plugins(self, context: InjectionContext):
plugin_registry.register_plugin("acapy_agent.wallet")
plugin_registry.register_plugin("acapy_agent.wallet.keys")

# Did management plugins
plugin_registry.register_plugin("acapy_agent.did.indy")

anoncreds_plugins = [
"acapy_agent.anoncreds",
"acapy_agent.anoncreds.default.did_indy",
Expand Down
3 changes: 2 additions & 1 deletion acapy_agent/config/tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from acapy_agent.tests import mock

from ...core.error import StartupError
from ...core.in_memory import InMemoryProfile
from ...core.profile import ProfileManager, ProfileSession
from ...storage.base import BaseStorage
Expand Down Expand Up @@ -143,7 +144,7 @@ async def test_wallet_config_auto_provision(self):
):
mock_mgr_open.side_effect = test_module.ProfileNotFoundError()

with self.assertRaises(test_module.ProfileNotFoundError):
with self.assertRaises(StartupError):
await test_module.wallet_config(self.context, provision=False)

self.context.update_settings({"auto_provision": True})
Expand Down
207 changes: 124 additions & 83 deletions acapy_agent/config/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from typing import Tuple

from ..core.error import ProfileNotFoundError
from ..core.error import ProfileNotFoundError, StartupError
from ..core.profile import Profile, ProfileManager, ProfileSession
from ..storage.base import BaseStorage
from ..storage.error import StorageNotFoundError
Expand All @@ -29,104 +29,77 @@
}


async def wallet_config(
context: InjectionContext, provision: bool = False
) -> Tuple[Profile, DIDInfo]:
"""Initialize the root profile."""
def _create_config_with_settings(settings) -> dict:
profile_config = {}

mgr = context.inject(ProfileManager)

settings = context.settings
profile_cfg = {}
for k in CFG_MAP:
pk = f"wallet.{k}"
if pk in settings:
profile_cfg[k] = settings[pk]
profile_config[k] = settings[pk]

# may be set by `aca-py provision --recreate`
if settings.get("wallet.recreate"):
profile_cfg["auto_recreate"] = True
profile_config["auto_recreate"] = True

if provision:
profile = await mgr.provision(context, profile_cfg)
else:
try:
profile = await mgr.open(context, profile_cfg)
except ProfileNotFoundError:
if settings.get("auto_provision", False):
profile = await mgr.provision(context, profile_cfg)
provision = True
else:
raise
return profile_config

if provision:
if profile.created:
print("Created new profile")

async def _attempt_open_profile(
profile_manager: ProfileManager,
context: InjectionContext,
profile_config: dict,
settings: dict,
) -> Tuple[Profile, bool]:
provision = False
try:
profile = await profile_manager.open(context, profile_config)
except ProfileNotFoundError:
if settings.get("auto_provision", False):
profile = await profile_manager.provision(context, profile_config)
provision = True
else:
print("Opened existing profile")
print("Profile backend:", profile.backend)
print("Profile name:", profile.name)
raise StartupError(
"Profile not found. Use `aca-py start --auto-provision` to create."
)

wallet_seed = context.settings.get("wallet.seed")
wallet_local_did = context.settings.get("wallet.local_did")
txn = await profile.transaction()
wallet = txn.inject(BaseWallet)
return (profile, provision)

public_did_info = await wallet.get_public_did()
public_did = None

if public_did_info:
public_did = public_did_info.did
if wallet_seed and seed_to_did(wallet_seed) != public_did:
if context.settings.get("wallet.replace_public_did"):
replace_did_info = await wallet.create_local_did(
method=SOV, key_type=ED25519, seed=wallet_seed
)
public_did = replace_did_info.did
await wallet.set_public_did(public_did)
print(f"Created new public DID: {public_did}")
print(f"Verkey: {replace_did_info.verkey}")
else:
# If we already have a registered public did and it doesn't match
# the one derived from `wallet_seed` then we error out.
raise ConfigError(
"New seed provided which doesn't match the registered"
+ f" public did {public_did}"
)
# wait until ledger config to set public DID endpoint - wallet goes first
elif wallet_seed:
if wallet_local_did:
endpoint = context.settings.get("default_endpoint")
metadata = {"endpoint": endpoint} if endpoint else None

local_did_info = await wallet.create_local_did(
method=SOV,
key_type=ED25519,
seed=wallet_seed,
metadata=metadata,
)
local_did = local_did_info.did
if provision:
print(f"Created new local DID: {local_did}")
print(f"Verkey: {local_did_info.verkey}")
else:
public_did_info = await wallet.create_public_did(
method=SOV, key_type=ED25519, seed=wallet_seed
def _log_provision_info(profile: Profile) -> None:
print(f'{"Created new profile" if profile.created else "Opened existing profile"}')
print(f"Profile name: {profile.name} Profile backend: {profile.backend}")


async def _initialize_with_public_did(
public_did_info: DIDInfo,
wallet: BaseWallet,
settings: dict,
wallet_seed: str,
) -> str:
public_did = public_did_info.did
# Check did:sov seed matches public DID
if wallet_seed and (seed_to_did(wallet_seed) != public_did):
if not settings.get("wallet.replace_public_did"):
raise ConfigError(
"New seed provided which doesn't match the registered"
+ f" public did {public_did}"
)
public_did = public_did_info.did
if provision:
print(f"Created new public DID: {public_did}")
print(f"Verkey: {public_did_info.verkey}")
# wait until ledger config to set public DID endpoint - wallet goes first

if provision and not wallet_local_did and not public_did:
print("No public DID")
print("Replacing public DID due to --replace-public-did flag")
replace_did_info = await wallet.create_local_did(
method=SOV, key_type=ED25519, seed=wallet_seed
)
public_did = replace_did_info.did
await wallet.set_public_did(public_did)
print(
f"Created new public DID: {public_did}, with verkey: {replace_did_info.verkey}" # noqa: E501
)

# Debug settings
test_seed = context.settings.get("debug.seed")
if context.settings.get("debug.enabled"):
if not test_seed:
test_seed = "testseed000000000000000000000001"

async def _initialize_with_debug_settings(settings: dict, wallet: BaseWallet):
test_seed = settings.get("debug.seed")
if settings.get("debug.enabled") and not test_seed:
test_seed = "testseed000000000000000000000001"
if test_seed:
await wallet.create_local_did(
method=SOV,
Expand All @@ -135,6 +108,74 @@ async def wallet_config(
metadata={"endpoint": "1.2.3.4:8021"},
)


async def _initialize_with_seed(
settings: dict, wallet: BaseWallet, provision: bool, create_local_did: bool, seed: str
):
if create_local_did:
endpoint = settings.get("default_endpoint")
metadata = {"endpoint": endpoint} if endpoint else None

local_did_info = await wallet.create_local_did(
method=SOV,
key_type=ED25519,
seed=seed,
metadata=metadata,
)
local_did = local_did_info.did
if provision:
print(f"Created new local DID: {local_did}")
print(f"Verkey: {local_did_info.verkey}")
else:
public_did_info = await wallet.create_public_did(
method=SOV, key_type=ED25519, seed=seed
)
public_did = public_did_info.did
if provision:
print(f"Created new public DID: {public_did}")
print(f"Verkey: {public_did_info.verkey}")


async def wallet_config(
context: InjectionContext, provision: bool = False
) -> Tuple[Profile, DIDInfo]:
"""Initialize the root profile."""

profile_manager = context.inject(ProfileManager)

settings = context.settings
profile_config = _create_config_with_settings(settings)
wallet_seed = settings.get("wallet.seed")
create_local_did = settings.get("wallet.local_did")

if provision:
profile = await profile_manager.provision(context, profile_config)
else:
profile, provision = await _attempt_open_profile(
profile_manager, context, profile_config, settings
)

_log_provision_info(profile)

txn = await profile.transaction()
wallet = txn.inject(BaseWallet)
public_did_info = await wallet.get_public_did()
public_did = None

if public_did_info:
public_did = await _initialize_with_public_did(
public_did_info, wallet, settings, wallet_seed
)
elif wallet_seed:
await _initialize_with_seed(
settings, wallet, provision, create_local_did, wallet_seed
)

if provision and not create_local_did and not public_did:
print("No public DID")

await _initialize_with_debug_settings(settings, wallet)

await txn.commit()

return (profile, public_did_info)
Expand Down
90 changes: 90 additions & 0 deletions acapy_agent/did/indy/indy_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""DID manager for Indy."""

from aries_askar import AskarError, Key

from ...core.profile import Profile
from ...utils.general import strip_did_prefix
from ...wallet.askar import CATEGORY_DID
from ...wallet.crypto import validate_seed
from ...wallet.did_method import INDY, DIDMethods
from ...wallet.did_parameters_validation import DIDParametersValidation
from ...wallet.error import WalletError
from ...wallet.key_type import ED25519, KeyType, KeyTypes
from ...wallet.util import bytes_to_b58


class DidIndyManager:
"""DID manager for Indy."""

def __init__(self, profile: Profile) -> None:
"""Initialize the DID manager."""
self.profile = profile

async def _get_holder_defined_did(self, options: dict) -> str | None:
async with self.profile.session() as session:
did_methods = session.inject(DIDMethods)
indy_method = did_methods.from_method(INDY.method_name)

if indy_method.holder_defined_did() and options.get("did"):
return strip_did_prefix(options.get("did"))

return None

async def _get_key_type(self, key_type: str) -> KeyType:
async with self.profile.session() as session:
key_types = session.inject(KeyTypes)
return key_types.from_key_type(key_type) or ED25519

def _create_key_pair(self, options: dict, key_type: KeyType) -> Key:
seed = options.get("seed")
if seed and not self.profile.settings.get("wallet.allow_insecure_seed"):
raise WalletError("Insecure seed is not allowed")

if seed:
seed = validate_seed(seed)
return Key.from_secret_bytes(key_type, seed)
return Key.generate(key_type)

async def register(self, options: dict) -> dict:
"""Register a DID Indy."""
options = options or {}

key_type = await self._get_key_type(options.get("key_type") or ED25519)
did_validation = DIDParametersValidation(self.profile.inject(DIDMethods))
did_validation.validate_key_type(INDY, key_type)

key_pair = self._create_key_pair(options, key_type.key_type)
verkey_bytes = key_pair.get_public_bytes()
verkey = bytes_to_b58(verkey_bytes)

nym = did_validation.validate_or_derive_did(
INDY, ED25519, verkey_bytes, (await self._get_holder_defined_did(options))
)
did = f"did:indy:{nym}"

async with self.profile.session() as session:
try:
await session.handle.insert_key(verkey, key_pair)
await session.handle.insert(
CATEGORY_DID,
did,
value_json={
"did": did,
"method": INDY.method_name,
"verkey": verkey,
"verkey_type": ED25519.key_type,
"metadata": {},
},
tags={
"method": INDY.method_name,
"verkey": verkey,
"verkey_type": ED25519.key_type,
},
)
except AskarError as err:
raise WalletError(f"Error registering DID: {err}") from err

return {
"did": did,
"verkey": verkey,
}
Loading

0 comments on commit 8889e7f

Please sign in to comment.