Skip to content

Commit

Permalink
Use pydid library for DIDDoc (#92)
Browse files Browse the repository at this point in the history
* Use pydid library for DIDDoc

* Fix some tests

* Minor improvements

* Modify validation to support str dids.
  • Loading branch information
dkulic committed Mar 13, 2023
1 parent ec4e806 commit 89c6922
Show file tree
Hide file tree
Showing 29 changed files with 650 additions and 843 deletions.
20 changes: 10 additions & 10 deletions didcomm/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Any, Union, List
from pydid.did import DID, DIDUrl

JSON_OBJ = Dict[str, Any]
JSON_VALUE = Union[type(None), str, int, bool, float, Dict, List]
JSON = str
JWK = JSON
JWT = JSON
JWS = JSON
DID = str
DID_URL = str
DID_URL = DIDUrl
DID_OR_DID_URL = Union[DID, DID_URL]


class VerificationMethodType(Enum):
JSON_WEB_KEY_2020 = 1
X25519_KEY_AGREEMENT_KEY_2019 = 2
ED25519_VERIFICATION_KEY_2018 = 3
X25519_KEY_AGREEMENT_KEY_2020 = 4
ED25519_VERIFICATION_KEY_2020 = 5
# ECDSA_SECP_256K1_VERIFICATION_KEY_2019 = 6 - not supported now
OTHER = 1000
class VerificationMethodType:
JSON_WEB_KEY_2020 = "JsonWebKey2020"
X25519_KEY_AGREEMENT_KEY_2019 = "X25519KeyAgreementKey2019"
ED25519_VERIFICATION_KEY_2018 = "Ed25519VerificationKey2018"
X25519_KEY_AGREEMENT_KEY_2020 = "X25519KeyAgreementKey2020"
ED25519_VERIFICATION_KEY_2020 = "Ed25519VerificationKey2020"
# ECDSA_SECP_256K1_VERIFICATION_KEY_2019 = "EcdsaSecp256k1VerificationKey2019" - not supported now
OTHER = "Other"


class VerificationMaterialFormat(Enum):
Expand Down
4 changes: 2 additions & 2 deletions didcomm/core/keys/anoncrypt_keys_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async def _find_anoncrypt_pack_recipient_public_keys_by_kid(
if did_doc is None:
raise DIDDocNotResolvedError(to_did)

if to_kid not in did_doc.key_agreement_kids:
if not did_doc.key_agreement or to_kid not in did_doc.key_agreement:
raise DIDUrlNotFoundError(
f"DID URL `{to_kid}` is not found in keyAgreement verification relationships of DID `{to_did}`"
)
Expand All @@ -78,7 +78,7 @@ async def _find_anoncrypt_pack_recipient_public_keys_by_did(
if did_doc is None:
raise DIDDocNotResolvedError(to_did)

kids = did_doc.key_agreement_kids
kids = did_doc.key_agreement
if not kids:
raise DIDUrlNotFoundError(
f"No keyAgreement verification relationships are found for DID `{to_did}`"
Expand Down
12 changes: 6 additions & 6 deletions didcomm/core/keys/authcrypt_keys_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ async def find_authcrypt_pack_sender_and_recipient_keys(
sender_did_doc = await resolvers_config.did_resolver.resolve(frm_did)
if sender_did_doc is None:
raise DIDDocNotResolvedError(frm_did)
if not sender_did_doc.key_agreement_kids:
if not sender_did_doc.key_agreement:
raise DIDUrlNotFoundError(
f"No keyAgreement verification relationships are found for DID `{frm_did}`"
)
sender_kids = sender_did_doc.key_agreement_kids
sender_kids = sender_did_doc.key_agreement
else:
sender_kids = [frm_kid]

Expand All @@ -51,13 +51,13 @@ async def find_authcrypt_pack_sender_and_recipient_keys(
raise DIDDocNotResolvedError(to_did)

if to_kid is None:
if not recipient_did_doc.key_agreement_kids:
if not recipient_did_doc.key_agreement:
raise DIDUrlNotFoundError(
f"No keyAgreement verification relationships are found for DID `{to_did}`"
)
recipient_kids = recipient_did_doc.key_agreement_kids
recipient_kids = recipient_did_doc.key_agreement
else:
if to_kid not in recipient_did_doc.key_agreement_kids:
if to_kid not in recipient_did_doc.key_agreement:
raise DIDUrlNotFoundError(
f"DID URL `{to_kid}` is not found in keyAgreement verification relationships of DID `{to_did}`"
)
Expand Down Expand Up @@ -104,7 +104,7 @@ async def find_authcrypt_unpack_sender_and_recipient_keys(
sender_did_doc = await resolvers_config.did_resolver.resolve(frm_did)
if sender_did_doc is None:
raise DIDDocNotResolvedError(frm_did)
if not sender_did_doc.key_agreement_kids:
if not sender_did_doc.key_agreement:
raise DIDUrlNotFoundError(
f"No keyAgreement verification relationships are found for DID `{frm_did}`"
)
Expand Down
2 changes: 1 addition & 1 deletion didcomm/core/keys/forward_next_keys_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async def has_keys_for_forward_next(
next_did_doc = await resolvers_config.did_resolver.resolve(_next)
if next_did_doc is None:
return False
next_kids = next_did_doc.key_agreement_kids
next_kids = next_did_doc.key_agreement

secret_ids = await resolvers_config.secrets_resolver.get_keys(next_kids)
return len(secret_ids) > 0
8 changes: 4 additions & 4 deletions didcomm/core/keys/sign_keys_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async def find_verification_key(
if did_doc is None:
raise DIDDocNotResolvedError(did)

if frm_kid not in did_doc.authentication_kids:
if frm_kid not in did_doc.authentication:
raise DIDUrlNotFoundError(
f"DID URL `{frm_kid}` is not found in authentication verification relationships of DID `{did}`"
)
Expand Down Expand Up @@ -60,17 +60,17 @@ async def _find_signing_key_by_did(
if did_doc is None:
raise DIDDocNotResolvedError(frm_did)

if not did_doc.authentication_kids:
if not did_doc.authentication:
raise DIDUrlNotFoundError(
f"No authentication verification relationships are found for DID `{frm_did}`"
)

secret_ids = await resolvers_config.secrets_resolver.get_keys(
did_doc.authentication_kids
did_doc.authentication
)
if not secret_ids:
raise SecretNotFoundError(
f"No secrets are found in secrets resolver for DID URLs: {did_doc.authentication_kids}"
f"No secrets are found in secrets resolver for DID URLs: {did_doc.authentication}"
)

kid = secret_ids[0]
Expand Down
71 changes: 24 additions & 47 deletions didcomm/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,16 @@ def extract_key(
method: Union[VerificationMethod, Secret], align_kid=False
) -> AsymmetricKey:
if isinstance(method, VerificationMethod):
return _extract_key_from_verifciation_method(method, align_kid)
return _extract_key_from_verification_method(method, align_kid)
else:
return _extract_key_from_secret(method, align_kid)


def _extract_key_from_verifciation_method(
def _extract_key_from_verification_method(
verification_method: VerificationMethod, align_kid
) -> AsymmetricKey:
if verification_method.type == VerificationMethodType.JSON_WEB_KEY_2020:
if (
verification_method.verification_material.format
!= VerificationMaterialFormat.JWK
):
raise DIDCommValueError(
f"Verification material format {verification_method.verification_material.format} "
f"is not supported for verification method type {verification_method.type}"
)

jwk = json_str_to_dict(verification_method.verification_material.value)
jwk = verification_method.public_key_jwk

if align_kid:
jwk["kid"] = verification_method.id
Expand All @@ -79,16 +70,7 @@ def _extract_key_from_verifciation_method(
VerificationMethodType.X25519_KEY_AGREEMENT_KEY_2019,
VerificationMethodType.ED25519_VERIFICATION_KEY_2018,
]:
if (
verification_method.verification_material.format
!= VerificationMaterialFormat.BASE58
):
raise DIDCommValueError(
f"Verification material format {verification_method.verification_material.format} "
f"is not supported for verification method type {verification_method.type}"
)

raw_value = base58.b58decode(verification_method.verification_material.value)
raw_value = base58.b58decode(verification_method.public_key_base58)
base64url_value = urlsafe_b64encode(raw_value)

jwk = {
Expand All @@ -109,24 +91,15 @@ def _extract_key_from_verifciation_method(
VerificationMethodType.X25519_KEY_AGREEMENT_KEY_2020,
VerificationMethodType.ED25519_VERIFICATION_KEY_2020,
]:
if (
verification_method.verification_material.format
!= VerificationMaterialFormat.MULTIBASE
):
raise DIDCommValueError(
f"Verification material format {verification_method.verification_material.format} "
f"is not supported for verification method type {verification_method.type}"
)

# Currently only base58btc encoding is supported in scope of multibase support
if verification_method.verification_material.value.startswith("z"):
if verification_method.public_key_multibase.startswith("z"):
prefixed_raw_value = base58.b58decode(
verification_method.verification_material.value[1:]
verification_method.public_key_multibase[1:]
)
else:
raise DIDCommValueError(
f"Multibase keys containing internally Base58 values only are currently supported "
f"but got the value: {verification_method.verification_material.value}"
f"but got the value: {verification_method.public_key_multibase}"
)

codec, raw_value = _from_multicodec(prefixed_raw_value)
Expand Down Expand Up @@ -306,13 +279,15 @@ def _from_multicodec(value: bytes) -> (_Codec, bytes):

def extract_sign_alg(method: Union[VerificationMethod, Secret]) -> SignAlg:
if method.type == VerificationMethodType.JSON_WEB_KEY_2020:
if method.verification_material.format != VerificationMaterialFormat.JWK:
raise DIDCommValueError(
f"Verification material format {method.verification_material.format} "
f"is not supported for verification method type {method.type}"
)

jwk = json_str_to_dict(method.verification_material.value)
if isinstance(method, Secret):
if method.verification_material.format != VerificationMaterialFormat.JWK:
raise DIDCommValueError(
f"Verification material format {method.verification_material.format} "
f"is not supported for verification method type {method.type}"
)
jwk = json_str_to_dict(method.verification_material.value)
else:
jwk = method.public_key_jwk # instance of VerificationMethod

if jwk["kty"] == "EC" and jwk["crv"] == "P-256":
return SignAlg.ES256
Expand Down Expand Up @@ -386,12 +361,14 @@ def get_did_and_optionally_kid(did_or_kid: DID_OR_DID_URL) -> (DID, Optional[DID
def are_keys_compatible(
method1: Union[Secret, VerificationMethod], method2: VerificationMethod
) -> bool:
if method1.type == method2.type and (
method1.verification_material.format == method2.verification_material.format
):
if method1.verification_material.format == VerificationMaterialFormat.JWK:
private_jwk = json_str_to_dict(method1.verification_material.value)
public_jwk = json_str_to_dict(method2.verification_material.value)
if method1.type == method2.type:
if method1.type == VerificationMethodType.JSON_WEB_KEY_2020:
private_jwk = (
json_str_to_dict(method1.verification_material.value)
if isinstance(method1, Secret)
else method1.public_key_jwk
)
public_jwk = method2.public_key_jwk
return (
private_jwk["kty"] == public_jwk["kty"]
and private_jwk["crv"] == public_jwk["crv"]
Expand Down
106 changes: 17 additions & 89 deletions didcomm/did_doc/did_doc.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,28 @@
from __future__ import annotations

import attr
from dataclasses import dataclass
from typing import List, Optional

from didcomm.common.types import (
DID_URL,
DID,
VerificationMaterial,
VerificationMethodType,
)
from didcomm.common.utils import search_first_in_iterable
from typing import Optional

from pydid import DIDCommService
from pydid import VerificationMethod
from pydid.doc import DIDDocument

@dataclass
class DIDDoc:
"""
DID DOC abstraction (https://www.w3.org/TR/did-core/#dfn-did-documents)
Attributes:
did (str): a DID for the given DID Doc
key_agreement_kids(List[str]): Key IDs (DID URLs) of all verification methods from the 'keyAgreement' verification relationship in this DID DOC.
See https://www.w3.org/TR/did-core/#verification-methods.
authentication_kids(List[str]): Key IDs (DID URLs) of all verification methods from the 'authentication' verification relationship in this DID DOC.
See https://www.w3.org/TR/did-core/#authentication.
verification_methods(List[VerificationMethod): All local verification methods including embedded to key agreement and authentication sections.
See https://www.w3.org/TR/did-core/#verification-methods.
didcomm_services(List[DIDCommService]): All services of 'DIDCommMessaging' type in this DID DOC.
Empty list is returned if there are no services of 'DIDCommMessaging' type.
See https://www.w3.org/TR/did-core/#services and
https://identity.foundation/didcomm-messaging/spec/#did-document-service-endpoint.
"""
from didcomm.common.types import DID_URL
from didcomm.common.utils import search_first_in_iterable

did: DID
key_agreement_kids: List[DID_URL]
authentication_kids: List[DID_URL]
verification_methods: List[VerificationMethod]
didcomm_services: List[DIDCommService]

class DIDDoc(DIDDocument):
def get_verification_method(self, id: DID_URL) -> Optional[VerificationMethod]:
"""
Returns the verification method with the given identifier.
:param id: an identifier of a verification method
:return: the verification method or None of there is no one for the given identifier
"""
return search_first_in_iterable(self.verification_methods, lambda x: x.id == id)
return (
search_first_in_iterable(self.verification_method, lambda x: x.id == id)
if self.verification_method
else None
)

def get_didcomm_service(self, id: str) -> Optional[DIDCommService]:
"""
Expand All @@ -53,58 +31,8 @@ def get_didcomm_service(self, id: str) -> Optional[DIDCommService]:
:param id: an identifier of a service endpoint
:return: the service endpoint or None of there is no one for the given identifier
"""
return search_first_in_iterable(self.didcomm_services, lambda x: x.id == id)


@dataclass
class VerificationMethod:
"""
DID DOC Verification method.
It can be used in such verification relationships as Authentication, KeyAgreement, etc.
See https://www.w3.org/TR/did-core/#verification-methods.
Attributes:
id (DID_URL): verification method `id` field
type (VerificationMethodType): verification method `type` field as VerificationMethodType enum
controller (str): verification method `controller` field
verification_material (VerificationMaterial): A verification material representing a public key
"""

id: DID_URL
type: VerificationMethodType
controller: str
verification_material: VerificationMaterial


# might makes sense for future
# def converter__DIDDocServiceTypes(service_t: Union[str, Any]):
# return (
# DIDDocServiceTypes(service_t)
# if isinstance(service_t, str) else service_t
# )


@attr.s(auto_attribs=True)
class DIDCommService:
"""
DID DOC Service of 'DIDCommMessaging' type.
See https://www.w3.org/TR/did-core/#services,
https://identity.foundation/didcomm-messaging/spec/#did-document-service-endpoint
and https://www.w3.org/TR/did-spec-registries/#didcommmessaging
Attributes:
id (str): service's 'id' field
service_endpoint (str): `serviceEndpoint` field of DIDCommMessaging service.
It can be either a URI to be used for transport or a mediator's DID in case of alternative endpoints.
routing_keys (List[DID_URL]): `routingKeys` field of DIDCommMessaging service.
A possibly empty ordered array of strings referencing keys to be used when preparing the message for transmission.
accept (List[str]): `accept` field of DIDCommMessaging service.
A possibly empty ordered array of strings representing accepted didcomm specification versions.
"""

id: str = attr.ib(validator=attr.validators.instance_of(str))
service_endpoint: str = attr.ib(validator=attr.validators.instance_of(str))
# TODO validate each item (should be did-url)
routing_keys: List[DID_URL] = attr.ib(validator=attr.validators.instance_of(list))
# TODO validate each item (should be str)
accept: List[str] = attr.ib(validator=attr.validators.instance_of(list))
return (
search_first_in_iterable(self.service, lambda x: x.id == id)
if self.service
else None
)
2 changes: 1 addition & 1 deletion didcomm/did_doc/did_resolver_in_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class DIDResolverInMemory(DIDResolver):
def __init__(self, did_docs: List[DIDDoc]):
self._did_docs = {did_doc.did: did_doc for did_doc in did_docs}
self._did_docs = {did_doc.id: did_doc for did_doc in did_docs}

async def resolve(self, did: DID) -> Optional[DIDDoc]:
return self._did_docs.get(did)
Loading

0 comments on commit 89c6922

Please sign in to comment.