Skip to content

Commit bc4f43e

Browse files
Align AnonCreds implementation with draft spec + update DID events processing (#28)
Signed-off-by: Alexander Shenshin <[email protected]> Signed-off-by: Alexander Shenshin <[email protected]>
1 parent 7b8421e commit bc4f43e

File tree

15 files changed

+111
-46
lines changed

15 files changed

+111
-46
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Release](https://img.shields.io/github/v/release/hiero-ledger/hiero-did-sdk-python)](https://img.shields.io/github/v/release/hiero-ledger/hiero-did-sdk-python)
44
[![Build status](https://img.shields.io/github/actions/workflow/status/hiero-ledger/hiero-did-sdk-python/main.yml?branch=main)](https://github.com/hiero-ledger/hiero-did-sdk-python/actions/workflows/main.yml?query=branch%3Amain)
55
[![codecov](https://codecov.io/gh/hiero-ledger/hiero-did-sdk-python/branch/main/graph/badge.svg)](https://codecov.io/gh/hiero-ledger/hiero-did-sdk-python)
6-
[![Commit activity](https://img.shields.io/github/commit-activity/m/hiero-ledger/hiero-did-sdk-python)](https://img.shields.io/github/commit-activity/m/hiero-ledger/hiero-did-sdk-python)
6+
[![Commit activity](https://img.shields.io/github/commit-activity/m/hiero-ledger/hiero-did-sdk-python)](https://github.com/hiero-ledger/hiero-did-sdk-python/commits/main)
77
[![License](https://img.shields.io/github/license/hiero-ledger/hiero-did-sdk-python)](https://github.com/hiero-ledger/hiero-did-sdk-python/blob/main/LICENSE)
88

99
This repository contains the Python SDK that enables developers to manage Decentralized Identifiers (DIDs) and AnonCreds Verifiable Credentials on the Hedera network using the Hedera Consensus Service.

hiero_did_sdk_python/anoncreds/hedera_anoncreds_registry.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ async def register_rev_reg_def(
336336
entries_topic_id = await self._hcs_topic_service.create_topic(entries_topic_options, [issuer_key])
337337

338338
rev_reg_def_with_metadata = RevRegDefWithHcsMetadata(
339-
rev_reg_def=rev_reg_def, hcs_metadata={"entries_topic_id": entries_topic_id}
339+
rev_reg_def=rev_reg_def, hcs_metadata={"entriesTopicId": entries_topic_id}
340340
)
341341

342342
hcs_file_payload = rev_reg_def_with_metadata.to_json().encode()
@@ -391,7 +391,7 @@ async def get_rev_list(self, rev_reg_id: str, timestamp: int) -> GetRevListResul
391391
)
392392

393393
rev_reg_def = rev_reg_def_result.revocation_registry_definition
394-
entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entries_topic_id")
394+
entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entriesTopicId")
395395

396396
if not entries_topic_id:
397397
return GetRevListResult(
@@ -588,7 +588,7 @@ async def _submit_rev_list_entry(
588588
revocation_list_metadata={},
589589
)
590590

591-
entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entries_topic_id")
591+
entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entriesTopicId")
592592

593593
if not entries_topic_id:
594594
return RegisterRevListResult(

hiero_did_sdk_python/anoncreds/models/revocation/revocation_registry_definition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def get_json_payload(self):
8585

8686

8787
class RevRegDefHcsMetadata(TypedDict):
88-
entries_topic_id: str
88+
entriesTopicId: str
8989

9090

9191
@dataclass(frozen=True)

hiero_did_sdk_python/anoncreds/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
ANONCREDS_IDENTIFIER_SEPARATOR = "/"
88

99
ANONCREDS_OBJECT_FAMILY = "anoncreds"
10-
ANONCREDS_VERSION = "v0"
10+
ANONCREDS_VERSION = "v1"
1111

1212

1313
class AnonCredsObjectType(StrEnum):

hiero_did_sdk_python/did/did_document.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import logging
22
from typing import cast
33

4+
from hiero_sdk_python import PublicKey
5+
6+
from ..utils.encoding import b58_to_bytes, b64_to_bytes
47
from ..utils.ipfs import download_ipfs_document_by_cid
58
from ..utils.serializable import Serializable
69
from .did_document_operation import DidDocumentOperation
@@ -15,7 +18,7 @@
1518
from .hcs.events.verification_relationship.hcs_did_update_verification_relationship_event import (
1619
HcsDidUpdateVerificationRelationshipEvent,
1720
)
18-
from .hcs.hcs_did_message import HcsDidMessage
21+
from .hcs.hcs_did_message import HcsDidMessage, HcsDidMessageEnvelope
1922

2023
LOGGER = logging.getLogger(__name__)
2124

@@ -56,21 +59,36 @@ def __init__(self, id_: str):
5659
DidDocumentJsonProperties.CAPABILITY_DELEGATION.value: [],
5760
}
5861

59-
async def process_messages(self, messages: list[HcsDidMessage]):
62+
self._public_key: PublicKey | None = None
63+
64+
async def process_messages(self, envelopes: list[HcsDidMessageEnvelope]):
6065
"""
6166
Process HCS DID messages - apply DID document state changes according to events.
6267
6368
Args:
64-
messages: HCS DID messages to process
69+
envelopes: HCS DID message envelopes (message + signature) to process
6570
6671
"""
67-
for message in messages:
68-
if not self.controller and message.operation == DidDocumentOperation.CREATE:
72+
for envelope in envelopes:
73+
message = cast(HcsDidMessage, envelope.message)
74+
75+
if not self.controller:
6976
event_target = message.event.event_target
7077
if event_target != HcsDidEventTarget.DID_OWNER and event_target != HcsDidEventTarget.DID_DOCUMENT:
71-
LOGGER.warning("DID document is not registered, skipping DID update event...")
78+
LOGGER.warning("DID document is not registered, skipping DID event...")
7279
continue
7380

81+
# TODO: Find a good way to support CID-based DID Document creation without workarounds and redundancy
82+
# It's possible that we want to drop support for this case instead
83+
is_signature_valid = (
84+
message.event.event_target == HcsDidEventTarget.DID_DOCUMENT
85+
or self._is_message_signature_valid(message, cast(str, envelope.signature))
86+
)
87+
88+
if not is_signature_valid:
89+
LOGGER.warning("HCS DID message signature is invalid, skipping event...")
90+
continue
91+
7492
match message.operation:
7593
case DidDocumentOperation.CREATE:
7694
await self._process_create_message(message)
@@ -154,6 +172,14 @@ async def _process_create_message(self, message: HcsDidMessage): # noqa: C901
154172
for verificationMethod in document.get(DidDocumentJsonProperties.VERIFICATION_METHOD, [])
155173
}
156174

175+
root_verification_method = next(
176+
filter(
177+
lambda verification_method: "#did-root-key" in verification_method["id"],
178+
self.verification_methods.values(),
179+
)
180+
)
181+
self._public_key = PublicKey.from_bytes(b58_to_bytes(root_verification_method["publicKeyBase58"]))
182+
157183
self.verification_relationships[DidDocumentJsonProperties.ASSERTION_METHOD] = document.get(
158184
DidDocumentJsonProperties.ASSERTION_METHOD, []
159185
)
@@ -174,7 +200,10 @@ async def _process_create_message(self, message: HcsDidMessage): # noqa: C901
174200
LOGGER.warning(f"DID owner is already registered: {self.controller}, skipping event...")
175201
return
176202

177-
self.controller = cast(HcsDidUpdateDidOwnerEvent, event).get_owner_def()
203+
did_owner_event = cast(HcsDidUpdateDidOwnerEvent, event)
204+
205+
self.controller = did_owner_event.get_owner_def()
206+
self._public_key = did_owner_event.public_key
178207
self._on_activated(message.timestamp)
179208
case HcsDidEventTarget.SERVICE:
180209
update_service_event = cast(HcsDidUpdateServiceEvent, event)
@@ -228,7 +257,10 @@ def _process_update_message(self, message: HcsDidMessage):
228257

229258
match event.event_target:
230259
case HcsDidEventTarget.DID_OWNER:
231-
self.controller = cast(HcsDidUpdateDidOwnerEvent, event).get_owner_def()
260+
did_owner_event = cast(HcsDidUpdateDidOwnerEvent, event)
261+
262+
self.controller = did_owner_event.get_owner_def()
263+
self._public_key = did_owner_event.public_key
232264
self._on_updated(message.timestamp)
233265
case HcsDidEventTarget.SERVICE:
234266
update_service_event = cast(HcsDidUpdateServiceEvent, event)
@@ -354,6 +386,32 @@ def _process_delete_message(self, message: HcsDidMessage):
354386
case _:
355387
LOGGER.warning(f"Delete {event.event_target} operation is not supported, skipping event...")
356388

389+
def _is_message_signature_valid(self, message: HcsDidMessage, signature: str) -> bool:
390+
is_create_or_update_event = (
391+
message.operation == DidDocumentOperation.CREATE or message.operation == DidDocumentOperation.UPDATE
392+
)
393+
is_did_owner_change_event = (
394+
is_create_or_update_event and message.event.event_target == HcsDidEventTarget.DID_OWNER
395+
)
396+
397+
public_key = (
398+
cast(HcsDidUpdateDidOwnerEvent, message.event).public_key if is_did_owner_change_event else self._public_key
399+
)
400+
401+
if not public_key:
402+
raise Exception("Cannot verify HCS DID Message signature - controller public key is not defined")
403+
404+
message_bytes = message.to_json().encode()
405+
signature_bytes = b64_to_bytes(signature)
406+
407+
try:
408+
public_key.verify(signature_bytes, message_bytes)
409+
except Exception as error:
410+
LOGGER.warning(f"HCS DID Message signature verification failed with error: {error!s}")
411+
return False
412+
413+
return True
414+
357415
def _on_activated(self, timestamp: float):
358416
self.created = timestamp
359417
self.updated = timestamp

hiero_did_sdk_python/did/hcs/hcs_did_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def from_json_payload(cls, payload: dict):
9191
match payload:
9292
case {"timestamp": timestamp, "operation": operation, "did": did, "event": event_base64}:
9393
parsed_event = _parse_hcs_did_event(event_base64, operation)
94-
return cls(operation=operation, did=did, event=parsed_event, timestamp=timestamp)
94+
return cls(operation=DidDocumentOperation(operation), did=did, event=parsed_event, timestamp=timestamp)
9595
case _:
9696
raise Exception(f"{cls.__name__} JSON parsing failed: Invalid JSON structure")
9797

hiero_did_sdk_python/did/hedera_did.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ def __init__(self, client: Client, identifier: str | None = None, private_key_de
6262
else:
6363
self.topic_id = None
6464

65-
self._messages: list[HcsDidMessage] = []
6665
self.document: DidDocument | None = None
6766

6867
async def register(self):
@@ -349,9 +348,8 @@ async def _handle_resolution_result(self, result: list[HcsDidMessageEnvelope]):
349348
if not self.identifier:
350349
raise Exception("Cannot handle DID resolution result: DID identifier is not defined")
351350

352-
self._messages = [cast(HcsDidMessage, envelope.message) for envelope in result]
353351
self.document = DidDocument(self.identifier)
354-
await self.document.process_messages(self._messages)
352+
await self.document.process_messages(result)
355353

356354
def _assert_can_submit_transaction(self):
357355
if not self.identifier:

hiero_did_sdk_python/did/hedera_did_resolver.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ..utils.cache import Cache, MemoryCache, TimestampedRecord
1111
from .did_document import DidDocument
1212
from .did_error import DidErrorCode, DidException
13-
from .hcs.hcs_did_message import HcsDidMessage, HcsDidMessageEnvelope
13+
from .hcs.hcs_did_message import HcsDidMessageEnvelope
1414
from .hedera_did import HederaDid
1515
from .types import DIDDocument, DIDDocumentMetadata, DIDResolutionResult
1616

@@ -102,11 +102,7 @@ async def resolve(self, did: str) -> DIDResolutionResult:
102102
timestamp_from=Timestamp(int(last_updated_timestamp), 0),
103103
).execute(self._client)
104104

105-
messages = [
106-
cast(HcsDidMessage, envelope.message) for envelope in cast(list[HcsDidMessageEnvelope], result)
107-
]
108-
109-
await did_document.process_messages(messages)
105+
await did_document.process_messages(cast(list[HcsDidMessageEnvelope], result))
110106

111107
self._cache.set(
112108
topic_id,

hiero_did_sdk_python/hcs/hcs_message_envelope.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def sign(self, signing_key: PrivateKey):
2222
raise Exception("Message is already signed")
2323

2424
message_bytes = self.message.to_json().encode()
25-
signature_bytes = bytes(signing_key.sign(message_bytes))
25+
signature_bytes = signing_key.sign(message_bytes)
2626

2727
self.signature = bytes_to_b64(signature_bytes)
2828

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "hiero-did-sdk-python"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "The repository contains the Python SDK for managing DID Documents and Anoncreds Verifiable Credentials registry using Hedera Consensus Service."
55
authors = ["Alexander Shenshin <[email protected]>", "Paulo Caldas <[email protected]>"]
66
repository = "https://github.com/hiero-ledger/hiero-did-sdk-python"

0 commit comments

Comments
 (0)