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 specific route to create did key #3168

Closed
Closed
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
1 change: 1 addition & 0 deletions aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ async def load_plugins(self, context: InjectionContext):
plugin_registry.register_package("aries_cloudagent.protocols")

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

plugin_registry.register_plugin("aries_cloudagent.ledger")
Expand Down
11 changes: 11 additions & 0 deletions aries_cloudagent/did/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .did_key import DIDKey


class DidOperationError(Exception):
"""Generic DID operation Error."""


__all__ = [
"DIDKey",
"DidOperationError",
]
61 changes: 60 additions & 1 deletion aries_cloudagent/did/did_key.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""DID Key class and resolver methods."""

from ..vc.ld_proofs.constants import DID_V1_CONTEXT_URL
from ..core.profile import Profile
from ..wallet.did_method import KEY
from ..wallet.base import BaseWallet
from ..wallet.crypto import ed25519_pk_to_curve25519
from ..wallet.key_type import (
BLS12381G1,
Expand All @@ -20,11 +23,67 @@ class DIDKey:
_key_type: KeyType
_public_key: bytes

def __init__(self, public_key: bytes, key_type: KeyType) -> None:
def __init__(self, public_key: bytes = None, key_type: KeyType = None) -> None:
"""Initialize new DIDKey instance."""
self._public_key = public_key
self._key_type = key_type

@classmethod
async def create(
cls, key_type: KeyType, profile: Profile, kid: str = None, seed: str = None
):
"""Create a new key DID.

Args:
key_type: The key type to use for the DID
seed: An optional seed for generating the keypair
kid: An optional verification method to associate with the DID
profile: The profile to use for storing the DID keypair

Returns:
A string representing the created DID

Raises:
DidOperationError: If the an error occures during did registration

"""
async with profile.session() as session:
wallet = session.inject(BaseWallet)
did_info = await wallet.create_local_did(
method=KEY, key_type=key_type, seed=seed
)
kid = kid if kid else f"{did_info.did}#" + did_info.did.split(":")[-1]
await wallet.assign_kid_to_key(verkey=did_info.verkey, kid=kid)
await wallet.get_key_by_kid(kid=kid)
return {
"did": did_info.did,
"verificationMethod": kid,
"multikey": did_info.did.split(":")[-1],
}

@classmethod
async def bind(cls, profile: Profile, did: str, kid: str):
"""Create a new key DID.

Args:
did: The DID with which to bind the kid
kid: The new verification method to bind
profile: The profile to use for storing the DID keypair

Returns:
A string representing the created DID

Raises:
DidOperationError: If the an error occures during did registration

"""
async with profile.session() as session:
wallet = session.inject(BaseWallet)
did_info = await wallet.get_local_did(did=did)
await wallet.assign_kid_to_key(verkey=did_info.verkey, kid=kid)
await wallet.get_key_by_kid(kid=kid)
return {"did": did, "verificationMethod": kid, "multikey": did.split(":")[-1]}

@classmethod
def from_public_key(cls, public_key: bytes, key_type: KeyType) -> "DIDKey":
"""Initialize new DIDKey instance from public key and key type."""
Expand Down
98 changes: 98 additions & 0 deletions aries_cloudagent/did/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""DID Management Routes."""

from aiohttp import web
from aiohttp_apispec import docs, request_schema, response_schema
from marshmallow.exceptions import ValidationError

from ..wallet.key_type import ED25519
from ..admin.decorators.auth import tenant_authentication
from .web_requests import (
DIDKeyRegistrationRequest,
DIDKeyRegistrationResponse,
DIDKeyBindingRequest,
DIDKeyBindingResponse,
)
from . import DIDKey, DidOperationError

KEY_MAPPINGS = {"ed25519": ED25519}


@docs(tags=["did"], summary="Create DID Key")
@request_schema(DIDKeyRegistrationRequest())
@response_schema(DIDKeyRegistrationResponse(), 201, description="Create new DID key")
@tenant_authentication
async def create_did_key(request):
"""Request handler for registering a Key DID.

Args:
request: aiohttp request object

"""
try:
return web.json_response(
await DIDKey().create(
profile=request["context"].profile,
key_type=KEY_MAPPINGS[
request["data"]["type"] if "type" in request["data"] else "ed25519"
],
kid=request["data"]["kid"] if "kid" in request["data"] else None,
seed=request["data"]["seed"] if "seed" in request["data"] else None,
),
status=201,
)
except (KeyError, ValidationError, DidOperationError) as err:
return web.json_response({"message": str(err)}, status=400)


@docs(tags=["did"], summary="Bind DID Key")
@request_schema(DIDKeyBindingRequest())
@response_schema(
DIDKeyBindingResponse(), 201, description="Bind existing DID key to new KID"
)
@tenant_authentication
async def bind_did_key(request):
"""Request handler for binding a Key DID.

Args:
request: aiohttp request object

"""
try:
return web.json_response(
await DIDKey().bind(
profile=request["context"].profile,
did=request["data"]["did"],
kid=request["data"]["kid"],
),
status=200,
)
except (KeyError, ValidationError, DidOperationError) as err:
return web.json_response({"message": str(err)}, status=400)
Comment on lines +47 to +70
Copy link
Contributor

Choose a reason for hiding this comment

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

This key "binding" concept feels like a hack. I understand the intent is to enable ACA-Py to sign things with a DID that it doesn't know how to use natively. I think there are better paths to accomplish this but even if we did have a mechanism for creating a key and associating it with a DID, why would we take the intermediate step of representing it as a did:key first?



async def register(app: web.Application):
"""Register routes."""

app.add_routes(
[
web.post("/did/key/create", create_did_key),
web.post("/did/key/bind", bind_did_key),
]
)


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/",
},
}
)
33 changes: 33 additions & 0 deletions aries_cloudagent/did/tests/test_did_key_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from unittest import IsolatedAsyncioTestCase
from ..did_key import DIDKey
from ...core.in_memory import InMemoryProfile
from ...wallet.did_method import DIDMethods
from ...wallet.key_type import ED25519


class TestDIDKeyOperations(IsolatedAsyncioTestCase):
test_seed = "00000000000000000000000000000000"
did = "did:key:z6MkgKA7yrw5kYSiDuQFcye4bMaJpcfHFry3Bx45pdWh3s8i"
kid = "did:key:z6MkgKA7yrw5kYSiDuQFcye4bMaJpcfHFry3Bx45pdWh3s8i#z6MkgKA7yrw5kYSiDuQFcye4bMaJpcfHFry3Bx45pdWh3s8i"
new_kid = "did:web:example.com#key-01"
multikey = "z6MkgKA7yrw5kYSiDuQFcye4bMaJpcfHFry3Bx45pdWh3s8i"
profile = InMemoryProfile.test_profile({}, {DIDMethods: DIDMethods()})

async def test_create_ed25519_did_key(self):
results = await DIDKey().create(
key_type=ED25519, profile=self.profile, seed=self.test_seed
)
assert results["did"] == self.did
assert results["multikey"] == self.multikey
assert results["verificationMethod"] == self.kid

async def test_bind_did_key(self):
results = await DIDKey().create(
key_type=ED25519, profile=self.profile, seed=self.test_seed
)
results = await DIDKey().bind(
profile=self.profile, did=results["did"], kid=self.new_kid
)
assert results["did"] == self.did
assert results["multikey"] == self.multikey
assert results["verificationMethod"] == self.new_kid
72 changes: 72 additions & 0 deletions aries_cloudagent/did/web_requests.py
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm really not a fan of having to pull up yet another file to find the expected inputs to the handlers. I'd strongly recommend keeping this in the same file as the handlers.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""DID routes web requests schemas."""

from marshmallow import fields, Schema


class DIDKeyRegistrationRequest(Schema):
"""Request schema for creating a dids."""

type = fields.Str(
default="ed25519",
required=False,
metadata={
"description": "Key Type",
"example": "ed25519",
},
)

seed = fields.Str(
default=None,
required=False,
metadata={
"description": "Seed",
"example": "00000000000000000000000000000000",
},
)

kid = fields.Str(
default=None,
required=False,
metadata={
"description": "Verification Method",
"example": "did:web:example.com#key-01",
},
)


class DIDKeyRegistrationResponse(Schema):
"""Response schema for creating a did."""

did = fields.Str()
multikey = fields.Str()
verificationMethod = fields.Str()


class DIDKeyBindingRequest(Schema):
"""Request schema for binding a kid to a did."""

did = fields.Str(
default=None,
required=True,
metadata={
"description": "DID",
"example": "did:key:z6MkgKA7yrw5kYSiDuQFcye4bMaJpcfHFry3Bx45pdWh3s8i",
},
)

kid = fields.Str(
default=None,
required=True,
metadata={
"description": "Verification Method",
"example": "did:web:example.com#key-02",
},
)


class DIDKeyBindingResponse(Schema):
"""Response schema for binding a kid to a did."""

did = fields.Str()
multikey = fields.Str()
verificationMethod = fields.Str()
1 change: 1 addition & 0 deletions aries_cloudagent/wallet/askar.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ async def create_local_did(
seed: Optional seed to use for DID
did: The DID to use
metadata: Metadata to store with DID
kid: Optional key identifier
Copy link
Contributor

Choose a reason for hiding this comment

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

kid in docstring but missing from method parameters


Returns:
A `DIDInfo` instance representing the created DID
Expand Down
8 changes: 8 additions & 0 deletions aries_cloudagent/wallet/in_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ async def create_local_did(
key_type: The key type to use for the DID
seed: Optional seed to use for DID
did: The DID to use
kid: Optional kid to assign to the DID
Copy link
Contributor

Choose a reason for hiding this comment

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

Same

metadata: Metadata to store with DID

Returns:
Expand Down Expand Up @@ -297,6 +298,13 @@ async def create_local_did(
"key_type": key_type,
"method": method,
}
self.profile.keys[verkey_enc] = {
"seed": seed,
"secret": secret,
"verkey": verkey_enc,
"metadata": metadata.copy() if metadata else {},
"key_type": key_type,
}
Comment on lines +301 to +307
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you give some background to this addition?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The askar wallet and in memory wallet both have the function create local did. In the askar wallet, it also inserts a key through that function:
https://github.com/hyperledger/aries-cloudagent-python/blob/17e9f1611c624c9e92d9abe233b233be8ce3addf/aries_cloudagent/wallet/askar.py#L264

If I had a wallet type of askar, I could assign a kid to this keypair, however with the in memory wallet it wouldn't be able to find the key_pair from the verkey when calling the assign_kid function because it only created the did. Both the askar wallet and in memory wallet are classes derived from the BaseWallet, but when creating a did through the AskarWallet, it also creates a keypair as with the in memory wallet it only creates the did.

return DIDInfo(
did=did,
verkey=verkey_enc,
Expand Down
Loading