From 68e7f88ad26011aafb5ceb02f79dcee7499ffddb Mon Sep 17 00:00:00 2001
From: DaevMithran <daevmithran1999@gmail.com>
Date: Mon, 4 Nov 2024 12:39:53 +0530
Subject: [PATCH] feat: Init did:cheqd integration

Signed-off-by: DaevMithran <daevmithran1999@gmail.com>
---
 .../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        | 127 ++++++++++++++++++
 acapy_agent/did/cheqd/registrar.py            |  48 +++++++
 acapy_agent/did/cheqd/routes.py               |  91 +++++++++++++
 acapy_agent/resolver/default/cheqd.py         |  43 ++++++
 acapy_agent/wallet/did_method.py              |   7 +
 10 files changed, 446 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..aa26380640
--- /dev/null
+++ b/acapy_agent/did/cheqd/cheqd_manager.py
@@ -0,0 +1,127 @@
+"""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 = "testnet"
+
+        try:
+            # generate payload
+            generate_res = await self.registrar.generate_did_doc(network, public_key_hex)
+            if generate_res is None:
+                raise WalletError("Error constructing DID Document")
+
+            did_document = generate_res.get("didDoc")
+            did: str = did_document.get("id")
+            # request create did
+            create_request_res = await self.registrar.create(
+                {"didDocument": did_document, "network": network}
+            )
+
+            job_id: str = create_request_res.get("jobId")
+            did_state = create_request_res.get("didState")
+            if did_state.get("state") == "action":
+                sign_req: dict = did_state.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": {
+                            "signingResponse": [
+                                {
+                                    "kid": kid,
+                                    "signature": bytes_to_b64(
+                                        key_pair.sign_message(
+                                            b64_to_bytes(payload_to_sign)
+                                        )
+                                    ),
+                                }
+                            ],
+                        },
+                    }
+                )
+                publish_did_state = publish_did_res.get("didState")
+                if publish_did_state.get("state") != "finished":
+                    raise WalletError("Error registering DID")
+            else:
+                raise WalletError("Error registering DID")
+        except Exception:
+            raise
+
+        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..18af7cf6f5
--- /dev/null
+++ b/acapy_agent/did/cheqd/registrar.py
@@ -0,0 +1,48 @@
+"""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:
+                        # print(f"Response Text: {await response.text()}")
+                        return await response.json()
+                    else:
+                        raise Exception(response)
+            except Exception:
+                raise
+
+    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()
+            except Exception:
+                raise
+
+    # 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: