Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anoncreds revoke and publish-revocations endorsement #2782

Merged
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
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