Skip to content

Commit

Permalink
Anoncreds revoke and publish-revocations endorsement
Browse files Browse the repository at this point in the history
Signed-off-by: jamshale <[email protected]>
  • Loading branch information
jamshale committed Feb 14, 2024
1 parent 41e3bdc commit 6f6ad0d
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 341 deletions.
30 changes: 17 additions & 13 deletions aries_cloudagent/anoncreds/revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ async def store_revocation_registry_list(self, result: RevListResult):
value_json={
"rev_list": rev_list.serialize(),
"pending": None,
# TODO THIS IS A HACK; this fixes ACA-Py expecting 1-based indexes
# TODO THIS IS A HACK; this fixes ACA-Py expecting 1-based indexes # noqa: E501
"next_index": 1,
},
tags={
Expand All @@ -505,16 +505,21 @@ async def store_revocation_registry_list(self, result: RevListResult):
async def finish_revocation_list(self, job_id: str, rev_reg_def_id: str):
"""Mark a revocation list as finished."""
async with self.profile.transaction() as txn:
await self._finish_registration(
txn,
# Finish the registration if the list is new, otherwise already updated
existing_list = await txn.handle.fetch(
CATEGORY_REV_LIST,
job_id,
rev_reg_def_id,
state=STATE_FINISHED,
)
await txn.commit()

await self.notify(RevListFinishedEvent.with_payload(rev_reg_def_id))
if not existing_list:
await self._finish_registration(
txn,
CATEGORY_REV_LIST,
job_id,
rev_reg_def_id,
state=STATE_FINISHED,
)
await txn.commit()
await self.notify(RevListFinishedEvent.with_payload(rev_reg_def_id))

async def update_revocation_list(
self,
Expand Down Expand Up @@ -566,22 +571,21 @@ async def update_revocation_list(
self.profile, rev_reg_def, prev, curr, revoked, options
)

# TODO Handle `failed` state

# # TODO Handle `failed` state
try:
async with self.profile.session() as session:
rev_list_entry_upd = await session.handle.fetch(
CATEGORY_REV_LIST, rev_reg_def_id, for_update=True
CATEGORY_REV_LIST, result.rev_reg_def_id, for_update=True
)
if not rev_list_entry_upd:
raise AnonCredsRevocationError(
"Revocation list not found for id {rev_reg_def_id}"
f"Revocation list not found for id {rev_reg_def_id}"
)
tags = rev_list_entry_upd.tags
tags["state"] = result.revocation_list_state.state
await session.handle.replace(
CATEGORY_REV_LIST,
rev_reg_def_id,
result.rev_reg_def_id,
value=rev_list_entry_upd.value,
tags=tags,
)
Expand Down
4 changes: 4 additions & 0 deletions aries_cloudagent/anoncreds/revocation_setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Automated setup process for AnonCreds credential definitions with revocation."""

import logging
from abc import ABC, abstractmethod

from aries_cloudagent.protocols.endorse_transaction.v1_0.util import is_author_role
Expand All @@ -16,6 +17,8 @@
RevRegDefFinishedEvent,
)

LOGGER = logging.getLogger(__name__)


class AnonCredsRevocationSetupManager(ABC):
"""Base class for automated setup of revocation."""
Expand Down Expand Up @@ -102,3 +105,4 @@ async def on_rev_reg_def(self, profile: Profile, event: RevRegDefFinishedEvent):

async def on_rev_list(self, profile: Profile, event: RevListFinishedEvent):
"""Handle rev list finished."""
LOGGER.debug("Revocation list finished: %s", event.payload.rev_reg_def_id)
97 changes: 3 additions & 94 deletions aries_cloudagent/anoncreds/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@
INDY_SCHEMA_ID_EXAMPLE,
UUIDFour,
)
from ..revocation.error import RevocationError, RevocationNotSupportedError
from ..revocation_anoncreds.manager import RevocationManager, RevocationManagerError
from ..revocation_anoncreds.routes import (
PublishRevocationsSchema,
from ..revocation.error import RevocationNotSupportedError
from ..revocation.routes import (
RevocationModuleResponseSchema,
RevokeRequestSchema,
RevRegIdMatchInfoSchema,
TxnOrPublishRevocationsResultSchema,
)
from ..storage.error import StorageError, StorageNotFoundError
from ..storage.error import StorageNotFoundError
from .base import (
AnonCredsObjectNotFound,
AnonCredsRegistrationError,
Expand Down Expand Up @@ -685,91 +681,6 @@ async def set_active_registry(request: web.BaseRequest):
raise web.HTTPInternalServerError(reason=str(e)) from e


@docs(
tags=["anoncreds"],
summary="Revoke an issued credential",
)
@request_schema(RevokeRequestSchema())
@response_schema(RevocationModuleResponseSchema(), description="")
async def revoke(request: web.BaseRequest):
"""Request handler for storing a credential revocation.
Args:
request: aiohttp request object
Returns:
The credential revocation details.
"""
context: AdminRequestContext = request["context"]
body = await request.json()
cred_ex_id = body.get("cred_ex_id")
body["notify"] = body.get("notify", context.settings.get("revocation.notify"))
notify = body.get("notify")
connection_id = body.get("connection_id")
body["notify_version"] = body.get("notify_version", "v1_0")
notify_version = body["notify_version"]

if notify and not connection_id:
raise web.HTTPBadRequest(reason="connection_id must be set when notify is true")
if notify and not notify_version:
raise web.HTTPBadRequest(
reason="Request must specify notify_version if notify is true"
)

rev_manager = RevocationManager(context.profile)
try:
if cred_ex_id:
# rev_reg_id and cred_rev_id should not be present so we can
# safely splat the body
await rev_manager.revoke_credential_by_cred_ex_id(**body)
else:
# no cred_ex_id so we can safely splat the body
await rev_manager.revoke_credential(**body)
return web.json_response({})
except (
RevocationManagerError,
AnonCredsRevocationError,
StorageError,
AnonCredsIssuerError,
AnonCredsRegistrationError,
) as err:
raise web.HTTPBadRequest(reason=err.roll_up) from err


@docs(tags=["revocation"], summary="Publish pending revocations to ledger")
@request_schema(PublishRevocationsSchema())
@response_schema(TxnOrPublishRevocationsResultSchema(), 200, description="")
async def publish_revocations(request: web.BaseRequest):
"""Request handler for publishing pending revocations to the ledger.
Args:
request: aiohttp request object
Returns:
Credential revocation ids published as revoked by revocation registry id.
"""
context: AdminRequestContext = request["context"]
body = await request.json()
rrid2crid = body.get("rrid2crid")

rev_manager = RevocationManager(context.profile)

try:
rev_reg_resp = await rev_manager.publish_pending_revocations(
rrid2crid,
)
return web.json_response({"rrid2crid": rev_reg_resp})
except (
RevocationError,
StorageError,
AnonCredsIssuerError,
AnonCredsRevocationError,
) as err:
raise web.HTTPBadRequest(reason=err.roll_up) from err


def register_events(event_bus: EventBus):
"""Register events."""
# TODO Make this pluggable?
Expand Down Expand Up @@ -800,8 +711,6 @@ async def register(app: web.Application):
web.post("/anoncreds/revocation-list", rev_list_post),
web.put("/anoncreds/registry/{rev_reg_id}/tails-file", upload_tails_file),
web.put("/anoncreds/registry/{rev_reg_id}/active", set_active_registry),
web.post("/anoncreds/revoke", revoke),
web.post("/anoncreds/publish-revocations", publish_revocations),
]
)

Expand Down
11 changes: 11 additions & 0 deletions aries_cloudagent/anoncreds/tests/test_revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,12 +591,23 @@ async def test_create_and_register_revocation_list(
@mock.patch.object(test_module.AnonCredsRevocation, "_finish_registration")
async def test_finish_revocation_list(self, mock_finish, mock_handle):
self.profile.context.injector.bind_instance(EventBus, MockEventBus())

mock_handle.fetch = mock.CoroutineMock(side_effect=[None, MockEntry()])

# Fetch doesn't find list then it should be created
await self.revocation.finish_revocation_list(
job_id="test-job-id",
rev_reg_def_id="test-rev-reg-def-id",
)
assert mock_finish.called

# Fetch finds list then there's nothing to do, it's already finished and updated
await self.revocation.finish_revocation_list(
job_id="test-job-id",
rev_reg_def_id="test-rev-reg-def-id",
)
assert mock_finish.call_count == 1

@mock.patch.object(InMemoryProfileSession, "handle")
async def test_update_revocation_list_get_rev_reg_errors(self, mock_handle):
mock_handle.fetch = mock.CoroutineMock(
Expand Down
40 changes: 0 additions & 40 deletions aries_cloudagent/anoncreds/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from aries_cloudagent.core.in_memory.profile import (
InMemoryProfile,
)
from aries_cloudagent.revocation_anoncreds.manager import RevocationManager
from aries_cloudagent.tests import mock

from .. import routes as test_module
Expand Down Expand Up @@ -338,45 +337,6 @@ async def test_set_active_registry(self, mock_set):
with self.assertRaises(KeyError):
await test_module.set_active_registry(self.request)

async def test_revoke_notify_without_connection_throws_x(self):
self.request.json = mock.CoroutineMock(return_value={"notify": True})
with self.assertRaises(web.HTTPBadRequest):
await test_module.revoke(self.request)

@mock.patch.object(
RevocationManager,
"revoke_credential_by_cred_ex_id",
return_value=None,
)
@mock.patch.object(
RevocationManager,
"revoke_credential",
return_value=None,
)
async def test_revoke(self, mock_revoke, mock_revoke_by_id):
self.request.json = mock.CoroutineMock(
return_value={"cred_ex_id": "cred_ex_id"}
)
await test_module.revoke(self.request)
assert mock_revoke_by_id.call_count == 1
assert mock_revoke.call_count == 0

self.request.json = mock.CoroutineMock(return_value={})
await test_module.revoke(self.request)
assert mock_revoke.call_count == 1

@mock.patch.object(
RevocationManager,
"publish_pending_revocations",
return_value="test-rrid",
)
async def test_publish_revocations(self, mock_publish):
self.request.json = mock.CoroutineMock(return_value={"rrid2crid": "rrid2crid"})
result = await test_module.publish_revocations(self.request)

assert json.loads(result.body)["rrid2crid"] == "test-rrid"
assert mock_publish.call_count == 1

@mock.patch.object(DefaultRevocationSetup, "register_events")
async def test_register_events(self, mock_revocation_setup_listeners):
mock_event_bus = MockEventBus()
Expand Down
14 changes: 11 additions & 3 deletions aries_cloudagent/revocation_anoncreds/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async def revoke_credential_by_cred_ex_id(
thread_id: str = None,
connection_id: str = None,
comment: str = None,
options: Optional[dict] = None,
):
"""Revoke a credential by its credential exchange identifier at issue.
Expand Down Expand Up @@ -79,6 +80,7 @@ async def revoke_credential_by_cred_ex_id(
thread_id=thread_id,
connection_id=connection_id,
comment=comment,
options=options,
)

async def revoke_credential(
Expand All @@ -91,6 +93,7 @@ async def revoke_credential(
thread_id: str = None,
connection_id: str = None,
comment: str = None,
options: Optional[dict] = None,
):
"""Revoke a credential.
Expand Down Expand Up @@ -120,15 +123,18 @@ async def revoke_credential(
if result.curr and result.revoked:
await self.set_cred_revoked_state(rev_reg_id, result.revoked)
await revoc.update_revocation_list(
rev_reg_id, result.prev, result.curr, result.revoked
rev_reg_id,
result.prev,
result.curr,
result.revoked,
options=options,
)
await notify_revocation_published_event(
self._profile, rev_reg_id, [cred_rev_id]
)

else:
await revoc.mark_pending_revocations(rev_reg_id, int(cred_rev_id))

if notify:
thread_id = thread_id or f"indy::{rev_reg_id}::{cred_rev_id}"
rev_notify_rec = RevNotificationRecord(
Expand Down Expand Up @@ -185,6 +191,7 @@ async def update_rev_reg_revoked_state(
async def publish_pending_revocations(
self,
rrid2crid: Optional[Mapping[Text, Sequence[Text]]] = None,
options: Optional[dict] = None,
) -> Mapping[Text, Sequence[Text]]:
"""Publish pending revocations to the ledger.
Expand All @@ -208,6 +215,7 @@ async def publish_pending_revocations(
Returns: mapping from each revocation registry id to its cred rev ids published.
"""
options = options or {}
published_crids = {}
revoc = AnonCredsRevocation(self._profile)

Expand All @@ -226,7 +234,7 @@ async def publish_pending_revocations(
if result.curr and result.revoked:
await self.set_cred_revoked_state(rrid, result.revoked)
await revoc.update_revocation_list(
rrid, result.prev, result.curr, result.revoked
rrid, result.prev, result.curr, result.revoked, options
)
published_crids[rrid] = sorted(result.revoked)
await notify_revocation_published_event(
Expand Down
Loading

0 comments on commit 6f6ad0d

Please sign in to comment.