Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![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)
[![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)
[![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)
[![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)
[![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)
[![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)

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.
Expand Down
6 changes: 3 additions & 3 deletions hiero_did_sdk_python/anoncreds/hedera_anoncreds_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ async def register_rev_reg_def(
entries_topic_id = await self._hcs_topic_service.create_topic(entries_topic_options, [issuer_key])

rev_reg_def_with_metadata = RevRegDefWithHcsMetadata(
rev_reg_def=rev_reg_def, hcs_metadata={"entries_topic_id": entries_topic_id}
rev_reg_def=rev_reg_def, hcs_metadata={"entriesTopicId": entries_topic_id}
)

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

rev_reg_def = rev_reg_def_result.revocation_registry_definition
entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entries_topic_id")
entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entriesTopicId")

if not entries_topic_id:
return GetRevListResult(
Expand Down Expand Up @@ -588,7 +588,7 @@ async def _submit_rev_list_entry(
revocation_list_metadata={},
)

entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entries_topic_id")
entries_topic_id = rev_reg_def_result.revocation_registry_definition_metadata.get("entriesTopicId")

if not entries_topic_id:
return RegisterRevListResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def get_json_payload(self):


class RevRegDefHcsMetadata(TypedDict):
entries_topic_id: str
entriesTopicId: str


@dataclass(frozen=True)
Expand Down
2 changes: 1 addition & 1 deletion hiero_did_sdk_python/anoncreds/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
ANONCREDS_IDENTIFIER_SEPARATOR = "/"

ANONCREDS_OBJECT_FAMILY = "anoncreds"
ANONCREDS_VERSION = "v0"
ANONCREDS_VERSION = "v1"


class AnonCredsObjectType(StrEnum):
Expand Down
72 changes: 65 additions & 7 deletions hiero_did_sdk_python/did/did_document.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import logging
from typing import cast

from hiero_sdk_python import PublicKey

from ..utils.encoding import b58_to_bytes, b64_to_bytes
from ..utils.ipfs import download_ipfs_document_by_cid
from ..utils.serializable import Serializable
from .did_document_operation import DidDocumentOperation
Expand All @@ -15,7 +18,7 @@
from .hcs.events.verification_relationship.hcs_did_update_verification_relationship_event import (
HcsDidUpdateVerificationRelationshipEvent,
)
from .hcs.hcs_did_message import HcsDidMessage
from .hcs.hcs_did_message import HcsDidMessage, HcsDidMessageEnvelope

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -56,21 +59,36 @@ def __init__(self, id_: str):
DidDocumentJsonProperties.CAPABILITY_DELEGATION.value: [],
}

async def process_messages(self, messages: list[HcsDidMessage]):
self._public_key: PublicKey | None = None

async def process_messages(self, envelopes: list[HcsDidMessageEnvelope]):
"""
Process HCS DID messages - apply DID document state changes according to events.

Args:
messages: HCS DID messages to process
envelopes: HCS DID message envelopes (message + signature) to process

"""
for message in messages:
if not self.controller and message.operation == DidDocumentOperation.CREATE:
for envelope in envelopes:
message = cast(HcsDidMessage, envelope.message)

if not self.controller:
event_target = message.event.event_target
if event_target != HcsDidEventTarget.DID_OWNER and event_target != HcsDidEventTarget.DID_DOCUMENT:
LOGGER.warning("DID document is not registered, skipping DID update event...")
LOGGER.warning("DID document is not registered, skipping DID event...")
continue

# TODO: Find a good way to support CID-based DID Document creation without workarounds and redundancy
# It's possible that we want to drop support for this case instead
is_signature_valid = (
message.event.event_target == HcsDidEventTarget.DID_DOCUMENT
or self._is_message_signature_valid(message, cast(str, envelope.signature))
)

if not is_signature_valid:
LOGGER.warning("HCS DID message signature is invalid, skipping event...")
continue

match message.operation:
case DidDocumentOperation.CREATE:
await self._process_create_message(message)
Expand Down Expand Up @@ -154,6 +172,14 @@ async def _process_create_message(self, message: HcsDidMessage): # noqa: C901
for verificationMethod in document.get(DidDocumentJsonProperties.VERIFICATION_METHOD, [])
}

root_verification_method = next(
filter(
lambda verification_method: "#did-root-key" in verification_method["id"],
self.verification_methods.values(),
)
)
self._public_key = PublicKey.from_bytes(b58_to_bytes(root_verification_method["publicKeyBase58"]))

self.verification_relationships[DidDocumentJsonProperties.ASSERTION_METHOD] = document.get(
DidDocumentJsonProperties.ASSERTION_METHOD, []
)
Expand All @@ -174,7 +200,10 @@ async def _process_create_message(self, message: HcsDidMessage): # noqa: C901
LOGGER.warning(f"DID owner is already registered: {self.controller}, skipping event...")
return

self.controller = cast(HcsDidUpdateDidOwnerEvent, event).get_owner_def()
did_owner_event = cast(HcsDidUpdateDidOwnerEvent, event)

self.controller = did_owner_event.get_owner_def()
self._public_key = did_owner_event.public_key
self._on_activated(message.timestamp)
case HcsDidEventTarget.SERVICE:
update_service_event = cast(HcsDidUpdateServiceEvent, event)
Expand Down Expand Up @@ -228,7 +257,10 @@ def _process_update_message(self, message: HcsDidMessage):

match event.event_target:
case HcsDidEventTarget.DID_OWNER:
did_owner_event = cast(HcsDidUpdateDidOwnerEvent, event)

self.controller = cast(HcsDidUpdateDidOwnerEvent, event).get_owner_def()
self._public_key = did_owner_event.public_key
self._on_updated(message.timestamp)
case HcsDidEventTarget.SERVICE:
update_service_event = cast(HcsDidUpdateServiceEvent, event)
Expand Down Expand Up @@ -354,6 +386,32 @@ def _process_delete_message(self, message: HcsDidMessage):
case _:
LOGGER.warning(f"Delete {event.event_target} operation is not supported, skipping event...")

def _is_message_signature_valid(self, message: HcsDidMessage, signature: str) -> bool:
is_create_or_update_event = (
message.operation == DidDocumentOperation.CREATE or message.operation == DidDocumentOperation.UPDATE
)
is_did_owner_change_event = (
is_create_or_update_event and message.event.event_target == HcsDidEventTarget.DID_OWNER
)

public_key = (
cast(HcsDidUpdateDidOwnerEvent, message.event).public_key if is_did_owner_change_event else self._public_key
)

if not public_key:
raise Exception("Cannot verify HCS DID Message signature - controller public key is not defined")

message_bytes = message.to_json().encode()
signature_bytes = b64_to_bytes(signature)

try:
public_key.verify(signature_bytes, message_bytes)
except Exception as error:
LOGGER.warning(f"HCS DID Message signature verification failed with error: {error!s}")
return False

return True

def _on_activated(self, timestamp: float):
self.created = timestamp
self.updated = timestamp
Expand Down
2 changes: 1 addition & 1 deletion hiero_did_sdk_python/did/hcs/hcs_did_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def from_json_payload(cls, payload: dict):
match payload:
case {"timestamp": timestamp, "operation": operation, "did": did, "event": event_base64}:
parsed_event = _parse_hcs_did_event(event_base64, operation)
return cls(operation=operation, did=did, event=parsed_event, timestamp=timestamp)
return cls(operation=DidDocumentOperation(operation), did=did, event=parsed_event, timestamp=timestamp)
case _:
raise Exception(f"{cls.__name__} JSON parsing failed: Invalid JSON structure")

Expand Down
4 changes: 1 addition & 3 deletions hiero_did_sdk_python/did/hedera_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ def __init__(self, client: Client, identifier: str | None = None, private_key_de
else:
self.topic_id = None

self._messages: list[HcsDidMessage] = []
self.document: DidDocument | None = None

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

self._messages = [cast(HcsDidMessage, envelope.message) for envelope in result]
self.document = DidDocument(self.identifier)
await self.document.process_messages(self._messages)
await self.document.process_messages(result)

def _assert_can_submit_transaction(self):
if not self.identifier:
Expand Down
8 changes: 2 additions & 6 deletions hiero_did_sdk_python/did/hedera_did_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ..utils.cache import Cache, MemoryCache, TimestampedRecord
from .did_document import DidDocument
from .did_error import DidErrorCode, DidException
from .hcs.hcs_did_message import HcsDidMessage, HcsDidMessageEnvelope
from .hcs.hcs_did_message import HcsDidMessageEnvelope
from .hedera_did import HederaDid
from .types import DIDDocument, DIDDocumentMetadata, DIDResolutionResult

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

messages = [
cast(HcsDidMessage, envelope.message) for envelope in cast(list[HcsDidMessageEnvelope], result)
]

await did_document.process_messages(messages)
await did_document.process_messages(cast(list[HcsDidMessageEnvelope], result))

self._cache.set(
topic_id,
Expand Down
2 changes: 1 addition & 1 deletion hiero_did_sdk_python/hcs/hcs_message_envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def sign(self, signing_key: PrivateKey):
raise Exception("Message is already signed")

message_bytes = self.message.to_json().encode()
signature_bytes = bytes(signing_key.sign(message_bytes))
signature_bytes = signing_key.sign(message_bytes)

self.signature = bytes_to_b64(signature_bytes)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "hiero-did-sdk-python"
version = "0.1.0"
version = "0.1.1"
description = "The repository contains the Python SDK for managing DID Documents and Anoncreds Verifiable Credentials registry using Hedera Consensus Service."
authors = ["Alexander Shenshin <alexander.shenshin@dsr-corporation.com>", "Paulo Caldas <paulo.caldas@dsr-corporation.com>"]
repository = "https://github.com/hiero-ledger/hiero-did-sdk-python"
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ async def test_demo(self, client: Client):
)

rev_reg_entries_topic_id = rev_reg_def_registration_result.revocation_registry_definition_metadata.get(
"entries_topic_id"
"entriesTopicId"
)
assert rev_reg_entries_topic_id

Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_hedera_anoncreds_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async def test_creates_anoncreds_rev_reg_def(self, client: Client, Something):
revocation_registry_definition_id=Something,
),
registration_metadata={},
revocation_registry_definition_metadata={"entries_topic_id": Something},
revocation_registry_definition_metadata={"entriesTopicId": Something},
)

rev_reg_def_id = registration_result.revocation_registry_definition_state.revocation_registry_definition_id
Expand All @@ -167,7 +167,7 @@ async def test_creates_anoncreds_rev_reg_def(self, client: Client, Something):
revocation_registry_definition=rev_reg_def,
revocation_registry_definition_id=rev_reg_def_id,
resolution_metadata={},
revocation_registry_definition_metadata={"entries_topic_id": Something},
revocation_registry_definition_metadata={"entriesTopicId": Something},
)

async def test_creates_and_updates_rev_list(self, client: Client, Something):
Expand Down Expand Up @@ -200,7 +200,7 @@ async def test_creates_and_updates_rev_list(self, client: Client, Something):
)

rev_reg_entries_topic_id = rev_reg_def_registration_result.revocation_registry_definition_metadata.get(
"entries_topic_id"
"entriesTopicId"
)
assert rev_reg_entries_topic_id

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/anoncreds/test_hedera_anoncreds_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@

MOCK_REV_REG_ENTRIES_TOPIC_ID = "0.0.5063060"
MOCK_REV_REG_DEF_WITH_METADATA = RevRegDefWithHcsMetadata(
rev_reg_def=MOCK_REV_REG_DEF, hcs_metadata={"entries_topic_id": MOCK_REV_REG_ENTRIES_TOPIC_ID}
rev_reg_def=MOCK_REV_REG_DEF, hcs_metadata={"entriesTopicId": MOCK_REV_REG_ENTRIES_TOPIC_ID}
)

MOCK_REV_ENTRY_1 = HcsRevRegEntryMessage(value=RevRegEntryValue(accum="accum-1", revoked=[5, 10]))
Expand Down Expand Up @@ -147,7 +147,7 @@ def mock_hcs_topic_service(mocker: MockerFixture):
)

mock_hsc_topic_service = MockHcsTopicService.return_value
mock_hsc_topic_service.create_topic.return_value = MOCK_REV_REG_DEF_WITH_METADATA.hcs_metadata["entries_topic_id"]
mock_hsc_topic_service.create_topic.return_value = MOCK_REV_REG_DEF_WITH_METADATA.hcs_metadata["entriesTopicId"]

return mock_hsc_topic_service

Expand Down
6 changes: 3 additions & 3 deletions tests/unit/anoncreds/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ def test_parse_anoncreds_identifier(self, publisher_did, object_type, topic_id):
[
("invalid_identifier", "Identifier has invalid structure"),
(
f"{PUBLISHER_DID_1}/non-anoncreds/v0/{AnonCredsObjectType.SCHEMA}/{TOPIC_ID_1}",
f"{PUBLISHER_DID_1}/non-anoncreds/v1/{AnonCredsObjectType.SCHEMA}/{TOPIC_ID_1}",
"Identifier contains invalid object definition",
),
(f"{PUBLISHER_DID_1}/anoncreds/v0/INVALID_TYPE/{TOPIC_ID_1}", "Invalid AnonCreds object type"),
(f"invalid_did/anoncreds/v0/{AnonCredsObjectType.SCHEMA}/{TOPIC_ID_1}", "Cannot parse issuer identifier"),
(f"{PUBLISHER_DID_1}/anoncreds/v1/INVALID_TYPE/{TOPIC_ID_1}", "Invalid AnonCreds object type"),
(f"invalid_did/anoncreds/v1/{AnonCredsObjectType.SCHEMA}/{TOPIC_ID_1}", "Cannot parse issuer identifier"),
],
)
def test_parse_throws_on_invalid_data(self, invalid_identifier, expected_error_message):
Expand Down
Loading