From d442922e731f38c1c82aa876763749349462a520 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Mon, 4 Nov 2024 12:39:53 +0530 Subject: [PATCH] feat: Init did:cheqd integration Signed-off-by: DaevMithran --- .../anoncreds/default/did_cheqd/__init__.py | 0 .../anoncreds/default/did_cheqd/registry.py | 120 ++++++++++++++++++ .../anoncreds/default/did_cheqd/routes.py | 1 + acapy_agent/config/default_context.py | 9 ++ acapy_agent/did/cheqd/__init__.py | 0 acapy_agent/did/cheqd/cheqd_manager.py | 117 +++++++++++++++++ acapy_agent/did/cheqd/registrar.py | 45 +++++++ acapy_agent/did/cheqd/routes.py | 91 +++++++++++++ acapy_agent/resolver/default/cheqd.py | 43 +++++++ acapy_agent/wallet/did_method.py | 7 + 10 files changed, 433 insertions(+) create mode 100644 acapy_agent/anoncreds/default/did_cheqd/__init__.py create mode 100644 acapy_agent/anoncreds/default/did_cheqd/registry.py create mode 100644 acapy_agent/anoncreds/default/did_cheqd/routes.py create mode 100644 acapy_agent/did/cheqd/__init__.py create mode 100644 acapy_agent/did/cheqd/cheqd_manager.py create mode 100644 acapy_agent/did/cheqd/registrar.py create mode 100644 acapy_agent/did/cheqd/routes.py create mode 100644 acapy_agent/resolver/default/cheqd.py diff --git a/acapy_agent/anoncreds/default/did_cheqd/__init__.py b/acapy_agent/anoncreds/default/did_cheqd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/acapy_agent/anoncreds/default/did_cheqd/registry.py b/acapy_agent/anoncreds/default/did_cheqd/registry.py new file mode 100644 index 0000000000..8d4454ccfb --- /dev/null +++ b/acapy_agent/anoncreds/default/did_cheqd/registry.py @@ -0,0 +1,120 @@ +"""DID Indy Registry.""" + +import logging +import re +from typing import Optional, Pattern, Sequence + +from ....config.injection_context import InjectionContext +from ....core.profile import Profile +from ...base import BaseAnonCredsRegistrar, BaseAnonCredsResolver +from ...models.anoncreds_cred_def import CredDef, CredDefResult, GetCredDefResult +from ...models.anoncreds_revocation import ( + GetRevListResult, + GetRevRegDefResult, + RevList, + RevListResult, + RevRegDef, + RevRegDefResult, +) +from ...models.anoncreds_schema import AnonCredsSchema, GetSchemaResult, SchemaResult + +LOGGER = logging.getLogger(__name__) + + +class DIDCheqdRegistry(BaseAnonCredsResolver, BaseAnonCredsRegistrar): + """DIDCheqdRegistry.""" + + def __init__(self): + """Initialize an instance. + + Args: + None + + """ + self._supported_identifiers_regex = re.compile(r"^did:cheqd:.*$") + + @property + def supported_identifiers_regex(self) -> Pattern: + """Supported Identifiers regex.""" + return self._supported_identifiers_regex + # TODO: fix regex (too general) + + async def setup(self, context: InjectionContext): + """Setup.""" + print("Successfully registered DIDCheqdRegistry") + + async def get_schema(self, profile: Profile, schema_id: str) -> GetSchemaResult: + """Get a schema from the registry.""" + raise NotImplementedError() + + async def register_schema( + self, + profile: Profile, + schema: AnonCredsSchema, + options: Optional[dict] = None, + ) -> SchemaResult: + """Register a schema on the registry.""" + raise NotImplementedError() + + async def get_credential_definition( + self, profile: Profile, credential_definition_id: str + ) -> GetCredDefResult: + """Get a credential definition from the registry.""" + raise NotImplementedError() + + async def register_credential_definition( + self, + profile: Profile, + schema: GetSchemaResult, + credential_definition: CredDef, + options: Optional[dict] = None, + ) -> CredDefResult: + """Register a credential definition on the registry.""" + raise NotImplementedError() + + async def get_revocation_registry_definition( + self, profile: Profile, revocation_registry_id: str + ) -> GetRevRegDefResult: + """Get a revocation registry definition from the registry.""" + raise NotImplementedError() + + async def register_revocation_registry_definition( + self, + profile: Profile, + revocation_registry_definition: RevRegDef, + options: Optional[dict] = None, + ) -> RevRegDefResult: + """Register a revocation registry definition on the registry.""" + raise NotImplementedError() + + async def get_revocation_list( + self, + profile: Profile, + revocation_registry_id: str, + timestamp_from: Optional[int] = 0, + timestamp_to: Optional[int] = None, + ) -> GetRevListResult: + """Get a revocation list from the registry.""" + raise NotImplementedError() + + async def register_revocation_list( + self, + profile: Profile, + rev_reg_def: RevRegDef, + rev_list: RevList, + options: Optional[dict] = None, + ) -> RevListResult: + """Register a revocation list on the registry.""" + raise NotImplementedError() + + async def update_revocation_list( + self, + profile: Profile, + rev_reg_def: RevRegDef, + prev_list: RevList, + curr_list: RevList, + revoked: Sequence[int], + options: Optional[dict] = None, + ) -> RevListResult: + """Update a revocation list on the registry.""" + raise NotImplementedError() diff --git a/acapy_agent/anoncreds/default/did_cheqd/routes.py b/acapy_agent/anoncreds/default/did_cheqd/routes.py new file mode 100644 index 0000000000..8e06e82a53 --- /dev/null +++ b/acapy_agent/anoncreds/default/did_cheqd/routes.py @@ -0,0 +1 @@ +"""Routes for DID Cheqd Registry.""" diff --git a/acapy_agent/config/default_context.py b/acapy_agent/config/default_context.py index 136c79791d..aeeb602c4d 100644 --- a/acapy_agent/config/default_context.py +++ b/acapy_agent/config/default_context.py @@ -143,10 +143,13 @@ async def load_plugins(self, context: InjectionContext): plugin_registry.register_plugin("acapy_agent.wallet") plugin_registry.register_plugin("acapy_agent.wallet.keys") + did_plugins = ["acapy_agent.did.cheqd"] + anoncreds_plugins = [ "acapy_agent.anoncreds", "acapy_agent.anoncreds.default.did_indy", "acapy_agent.anoncreds.default.did_web", + "acapy_agent.anoncreds.default.did_cheqd", "acapy_agent.anoncreds.default.legacy_indy", "acapy_agent.revocation_anoncreds", ] @@ -157,6 +160,10 @@ async def load_plugins(self, context: InjectionContext): "acapy_agent.revocation", ] + def register_did_plugins(): + for plugin in did_plugins: + plugin_registry.register_plugin(plugin) + def register_askar_plugins(): for plugin in askar_plugins: plugin_registry.register_plugin(plugin) @@ -165,6 +172,8 @@ def register_anoncreds_plugins(): for plugin in anoncreds_plugins: plugin_registry.register_plugin(plugin) + register_did_plugins() + if wallet_type == "askar-anoncreds": register_anoncreds_plugins() else: diff --git a/acapy_agent/did/cheqd/__init__.py b/acapy_agent/did/cheqd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/acapy_agent/did/cheqd/cheqd_manager.py b/acapy_agent/did/cheqd/cheqd_manager.py new file mode 100644 index 0000000000..bf1d4716d8 --- /dev/null +++ b/acapy_agent/did/cheqd/cheqd_manager.py @@ -0,0 +1,117 @@ +"""DID manager for Cheqd.""" + +from aries_askar import AskarError, Key + +from .registrar import DidCheqdRegistrar +from ...core.profile import Profile +from ...wallet.askar import CATEGORY_DID +from ...wallet.crypto import validate_seed +from ...wallet.did_method import CHEQD, 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, b64_to_bytes, bytes_to_b64 + + +class DidCheqdManager: + """DID manager for Cheqd.""" + + registrar: DidCheqdRegistrar + + def __init__(self, profile: Profile) -> None: + """Initialize the DID manager.""" + self.profile = profile + self.registrar = DidCheqdRegistrar() + + 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 Cheqd.""" + 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(CHEQD, 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) + + public_key_hex = verkey_bytes.hex() + network = options.get("network") or "testnet" + # generate payload + did_document = await self.registrar.generate_did_doc(network, public_key_hex) + did: str = did_document.get("id") or "" + # request create did + create_request_res = await self.registrar.create( + {"didDocument": did_document, "network": network} + ) + + if create_request_res.get("state") == "action": + job_id: str = create_request_res.get("jobId") + sign_req: dict = create_request_res.get("signingRequest")[0] + kid: str = sign_req.get("kid") + payload_to_sign: str = sign_req.get("serializedPayload") + # publish did + publish_did_res = await self.registrar.create( + { + "jobId": job_id, + "network": network, + "secret": { + "signing_response": [ + { + "kid": kid, + "signature": bytes_to_b64( + key_pair.key.sign_message( + b64_to_bytes(payload_to_sign) + ) + ), + } + ], + }, + } + ) + if publish_did_res.get("state") != "finished": + raise WalletError("Error registering DID") + else: + raise WalletError("Error registering DID") + + 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": CHEQD.method_name, + "verkey": verkey, + "verkey_type": ED25519.key_type, + "metadata": {}, + }, + tags={ + "method": CHEQD.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, + } diff --git a/acapy_agent/did/cheqd/registrar.py b/acapy_agent/did/cheqd/registrar.py new file mode 100644 index 0000000000..ebfaf666bd --- /dev/null +++ b/acapy_agent/did/cheqd/registrar.py @@ -0,0 +1,45 @@ +"""DID Registrar for Cheqd.""" + +from aiohttp import ClientSession + + +class DidCheqdRegistrar: + """DID Registrar for Cheqd.""" + + DID_REGISTRAR_BASE_URL = "https://did-registrar.cheqd.net/1.0/" + + async def generate_did_doc(self, network: str, public_key_hex: str) -> dict | None: + """Generates a did_document with the provided params.""" + async with ClientSession() as session: + try: + async with session.get( + self.DID_REGISTRAR_BASE_URL + "did-document", + params={ + "verificationMethod": "Ed25519VerificationKey2020", + "methodSpecificIdAlgo": "uuid", + "network": network, + "publicKeyHex": public_key_hex, + }, + ) as response: + if response.status == 200: + return await response.json() + finally: + return None + + async def create(self, options: dict) -> dict | None: + """Request Create and Publish a DID Document.""" + async with ClientSession() as session: + try: + async with session.post( + self.DID_REGISTRAR_BASE_URL + "create", json=options + ) as response: + if response.status == 200 or response.status == 201: + return await response.json() + finally: + return None + + # async def update(self, options: dict) -> dict: + # + # async def deactivate(self, options: dict) -> dict: + # + # async def create_resource(self, options:dict) -> dict diff --git a/acapy_agent/did/cheqd/routes.py b/acapy_agent/did/cheqd/routes.py new file mode 100644 index 0000000000..eea9185eb6 --- /dev/null +++ b/acapy_agent/did/cheqd/routes.py @@ -0,0 +1,91 @@ +"""DID Cheqd routes.""" + +from http import HTTPStatus + +from aiohttp import web +from aiohttp_apispec import docs, request_schema, response_schema +from marshmallow import fields + +from ...admin.decorators.auth import tenant_authentication +from ...admin.request_context import AdminRequestContext +from ...did.cheqd.cheqd_manager import DidCheqdManager +from ...messaging.models.openapi import OpenAPISchema +from ...wallet.error import WalletError + + +class CreateRequestSchema(OpenAPISchema): + """Parameters and validators for create DID endpoint.""" + + options = fields.Dict( + required=False, + metadata={ + "description": "Additional configuration options", + "example": { + "network": "testnet", + "method_specific_id_algo": "uuid", + "key_type": "ed25519", + }, + }, + ) + features = fields.Dict( + required=False, + metadata={ + "description": "Additional features to enable for the did.", + "example": "{}", + }, + ) + + +class CreateResponseSchema(OpenAPISchema): + """Response schema for create DID endpoint.""" + + did = fields.Str( + metadata={ + "description": "DID created", + "example": "did:cheqd:mainnet:DFZgMggBEXcZFVQ2ZBTwdr", + } + ) + verkey = fields.Str( + metadata={ + "description": "Verification key", + "example": "BnSWTUQmdYCewSGFrRUhT6LmKdcCcSzRGqWXMPnEP168", + } + ) + + +@docs(tags=["did"], summary="Create a did:cheqd") +@request_schema(CreateRequestSchema()) +@response_schema(CreateResponseSchema, HTTPStatus.OK) +@tenant_authentication +async def create_cheqd_did(request: web.BaseRequest): + """Create a Cheqd DID.""" + context: AdminRequestContext = request["context"] + body = await request.json() + try: + return web.json_response( + (await DidCheqdManager(context.profile).register(body.get("options"))), + ) + except WalletError as e: + raise web.HTTPBadRequest(reason=str(e)) + + +async def register(app: web.Application): + """Register routes.""" + app.add_routes([web.post("/did/cheqd/create", create_cheqd_did)]) + + +def post_process_routes(app: web.Application): + """Amend swagger API.""" + # Add top-level tags description + if "tags" not in app._state["swagger_dict"]: + app._state["swagger_dict"]["tags"] = [] + app._state["swagger_dict"]["tags"].append( + { + "name": "did", + "description": "Endpoints for managing dids", + "externalDocs": { + "description": "Specification", + "url": "https://www.w3.org/TR/did-core/", + }, + } + ) diff --git a/acapy_agent/resolver/default/cheqd.py b/acapy_agent/resolver/default/cheqd.py new file mode 100644 index 0000000000..7a70046ba3 --- /dev/null +++ b/acapy_agent/resolver/default/cheqd.py @@ -0,0 +1,43 @@ +"""Key DID Resolver. + +Resolution is performed using the IndyLedger class. +""" + +from typing import Optional, Pattern, Sequence, Text + +from ...config.injection_context import InjectionContext +from ...core.profile import Profile +from ...did.did_key import DIDKey +from ...messaging.valid import DIDKey as DIDKeyType +from ..base import BaseDIDResolver, DIDNotFound, ResolverType + + +class KeyDIDResolver(BaseDIDResolver): + """Key DID Resolver.""" + + def __init__(self): + """Initialize Key Resolver.""" + super().__init__(ResolverType.NATIVE) + + async def setup(self, context: InjectionContext): + """Perform required setup for Key DID resolution.""" + + @property + def supported_did_regex(self) -> Pattern: + """Return supported_did_regex of Key DID Resolver.""" + return DIDKeyType.PATTERN + + async def _resolve( + self, + profile: Profile, + did: str, + service_accept: Optional[Sequence[Text]] = None, + ) -> dict: + """Resolve a Key DID.""" + try: + did_key = DIDKey.from_did(did) + + except Exception as e: + raise DIDNotFound(f"Unable to resolve did: {did}") from e + + return did_key.did_doc diff --git a/acapy_agent/wallet/did_method.py b/acapy_agent/wallet/did_method.py index bf6ff57304..67b4511ee8 100644 --- a/acapy_agent/wallet/did_method.py +++ b/acapy_agent/wallet/did_method.py @@ -89,6 +89,12 @@ def holder_defined_did(self) -> HolderDefinedDid: rotation=False, holder_defined_did=HolderDefinedDid.NO, ) +CHEQD = DIDMethod( + name="cheqd", + key_types=[ED25519], + rotation=True, + holder_defined_did=HolderDefinedDid.ALLOWED, +) class DIDMethods: @@ -102,6 +108,7 @@ def __init__(self) -> None: WEB.method_name: WEB, PEER2.method_name: PEER2, PEER4.method_name: PEER4, + CHEQD.method_name: CHEQD, } def registered(self, method: str) -> bool: