Skip to content
Merged
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
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
74 changes: 66 additions & 8 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:
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_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 <[email protected]>", "Paulo Caldas <[email protected]>"]
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