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

separate revocation from publication #425

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
26 changes: 24 additions & 2 deletions aries_cloudagent/issuer/indy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
import logging
from typing import Sequence, Tuple
from typing import Mapping, Sequence, Tuple

import indy.anoncreds
import indy.blob_storage
Expand Down Expand Up @@ -117,7 +117,7 @@ async def create_and_store_credential_definition(

Args:
origin_did: the DID issuing the credential definition
schema_json: the schema used as a basis
schema: the schema used as a basis
signature_type: the credential definition signature type (default 'CL')
tag: the credential definition tag
support_revocation: whether to enable revocation for this credential def
Expand Down Expand Up @@ -305,3 +305,25 @@ async def create_and_store_revocation_registry(
tails_writer,
)
return (revoc_reg_id, revoc_reg_def_json, revoc_reg_entry_json)

async def merge_revocation_registry_deltas(
self,
fro_delta: dict,
to_delta: dict
) -> Mapping:
"""
Merge revocation registry deltas.

Args:
fro_delta: original delta
to_delta: incoming delta

Returns:
Merged delta.

"""

return json.loads(await indy.anoncreds.issuer_merge_revocation_registry_deltas(
json.dumps(fro_delta),
json.dumps(to_delta)
))
31 changes: 23 additions & 8 deletions aries_cloudagent/messaging/models/base_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,21 @@
from ..valid import INDY_ISO8601_DATETIME


def match_post_filter(record: dict, post_filter: dict) -> bool:
"""Determine if a record value matches the post-filter."""
def match_post_filter(record: dict, post_filter: dict, positive: bool = True) -> bool:
"""Determine if a record value matches the post-filter.

Args:
record: record to check
post_filter: filter to apply (empty or None filter matches everything)
positive: whether matching all filter criteria positively or negatively
"""
if not post_filter:
return True

for k, v in post_filter.items():
if record.get(k) != v:
return False
return True
return not positive
return positive


class BaseRecord(BaseModel):
Expand Down Expand Up @@ -230,7 +239,7 @@ async def retrieve_by_tag_filter(
found = None
async for record in query:
vals = json.loads(record.value)
if not post_filter or match_post_filter(vals, post_filter):
if match_post_filter(vals, post_filter):
if found:
raise StorageDuplicateError("Multiple records located")
found = cls.from_storage(record.id, vals)
Expand All @@ -243,14 +252,16 @@ async def query(
cls,
context: InjectionContext,
tag_filter: dict = None,
post_filter: dict = None,
post_filter_positive: dict = None,
post_filter_negative: dict = None,
) -> Sequence["BaseRecord"]:
"""Query stored records.

Args:
context: The injection context to use
tag_filter: An optional dictionary of tag filter clauses
post_filter: Additional value filters to apply
post_filter_positive: Additional value filters to apply matching positively
post_filter_negative: Additional value filters to apply matching negatively
"""
storage: BaseStorage = await context.inject(BaseStorage)
query = storage.search_records(
Expand All @@ -262,7 +273,11 @@ async def query(
result = []
async for record in query:
vals = json.loads(record.value)
if not post_filter or match_post_filter(vals, post_filter):
if match_post_filter(
vals, post_filter_positive, True
) and match_post_filter(
vals, post_filter_negative, False
):
result.append(cls.from_storage(record.id, vals))
return result

Expand Down
90 changes: 69 additions & 21 deletions aries_cloudagent/protocols/issue_credential/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json
import logging
from typing import Mapping, Tuple
from typing import Mapping, Text, Sequence, Tuple

from indy.error import IndyError

Expand All @@ -25,6 +25,7 @@
)
from ....revocation.indy import IndyRevocation
from ....revocation.models.revocation_registry import RevocationRegistry
from ....revocation.models.issuer_rev_reg_record import IssuerRevRegRecord
from ....storage.base import BaseStorage
from ....storage.error import StorageNotFoundError

Expand Down Expand Up @@ -500,7 +501,7 @@ async def issue_credential(

if credential_exchange_record.revoc_reg_id:
revoc = IndyRevocation(self.context)
registry_record = await revoc.get_issuer_revocation_record(
registry_record = await revoc.get_issuer_rev_reg_record(
credential_exchange_record.revoc_reg_id
)
# FIXME exception on missing
Expand Down Expand Up @@ -580,6 +581,7 @@ async def store_credential(
Args:
credential_exchange_record: credential exchange record
with credential to store and ack
credential_id: optional credential identifier to override default on storage

Returns:
Tuple: (Updated credential exchange record, credential ack message)
Expand Down Expand Up @@ -682,44 +684,90 @@ async def receive_credential_ack(self) -> V10CredentialExchange:
return credential_exchange_record

async def revoke_credential(
self, credential_exchange_record: V10CredentialExchange
self, credential_exchange_record: V10CredentialExchange, publish: bool = False
):
"""
Revoke a previously-issued credential.

Optionally, publish the corresponding revocation registry delta to the ledger.

Args:
credential_exchange_record: the active credential exchange
publish: whether to publish the resulting revocation registry delta

"""

assert (
credential_exchange_record.revocation_id
and credential_exchange_record.revoc_reg_id
)
issuer: BaseIssuer = await self.context.inject(BaseIssuer)

revoc = IndyRevocation(self.context)
registry_record = await revoc.get_issuer_revocation_record(
registry_record = await revoc.get_issuer_rev_reg_record(
credential_exchange_record.revoc_reg_id
)
# FIXME exception on missing

registry = await registry_record.get_registry()

delta_json = await issuer.revoke_credential(
registry.registry_id,
registry.tails_local_path,
credential_exchange_record.revocation_id,
)
delta = json.loads(delta_json)
if not registry_record:
raise CredentialManagerError(
"No revocation registry record found for id {}".format(
credential_exchange_record.revoc_reg_id
)
)

# create entry and send to ledger
if delta:
ledger: BaseLedger = await self.context.inject(BaseLedger)
async with ledger:
await ledger.send_revoc_reg_entry(
registry.registry_id, registry.reg_def_type, delta
if publish:
# create entry and send to ledger
delta = json.loads(
await issuer.revoke_credential(
registry_record.revoc_reg_id,
registry_record.tails_local_path,
credential_exchange_record.revocation_id,
)
)

if delta:
registry_record.revoc_reg_entry = delta
await registry_record.publish_registry_entry(self.context)
else:
await registry_record.mark_pending(
self.context,
credential_exchange_record.revocation_id
)

credential_exchange_record.state = V10CredentialExchange.STATE_REVOKED
await credential_exchange_record.save(self.context, reason="Revoked credential")

async def publish_pending_revocations(self) -> Mapping[Text, Sequence[Text]]:
"""
Publish pending revocations to the ledger.

Returns: mapping from each revocation registry id to its cred rev ids published.
"""

result = {}

issuer: BaseIssuer = await self.context.inject(BaseIssuer)

registry_records = await IssuerRevRegRecord.query_by_pending(self.context)
for registry_record in registry_records:
net_delta = {}
for cr_id in registry_record.pending_pub:
delta = json.loads(
await issuer.revoke_credential(
registry_record.revoc_reg_id,
registry_record.tails_local_path,
cr_id,
)
)
if delta:
net_delta = await issuer.merge_revocation_registry_deltas(
net_delta,
delta
) if net_delta else delta

registry_record.revoc_reg_entry = net_delta
await registry_record.publish_registry_entry(self.context)
result[registry_record.revoc_reg_id] = [
cr_id for cr_id in registry_record.pending_pub
]
await registry_record.clear_pending(self.context)

return result
57 changes: 54 additions & 3 deletions aries_cloudagent/protocols/issue_credential/v1_0/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Credential exchange admin routes."""

import json

from aiohttp import web
from aiohttp_apispec import docs, request_schema, response_schema
from json.decoder import JSONDecodeError
Expand Down Expand Up @@ -149,6 +151,18 @@ class V10CredentialProblemReportRequestSchema(Schema):
explain_ltxt = fields.Str(required=True)


class V10PublishRevocationsResultSchema(Schema):
"""Result schema for revocation publication API call."""

results = fields.Dict(
keys=fields.Str(example=INDY_REV_REG_ID["example"]), # marshmallow 3.0 ignores
values=fields.List(
fields.Str(description="Credential revocation identifier", example="23")
),
description="Credential revocation ids published by revocation registry id",
)


@docs(tags=["issue-credential"], summary="Get attribute MIME types from wallet")
@response_schema(V10AttributeMimeTypesResultSchema(), 200)
async def attribute_mime_types_get(request: web.BaseRequest):
Expand Down Expand Up @@ -689,7 +703,19 @@ async def credential_exchange_problem_report(request: web.BaseRequest):
return web.json_response({})


@docs(tags=["issue-credential"], summary="Revoke an issued credential")
@docs(
tags=["issue-credential"],
parameters=[
{
"in": "path",
"name": "publish",
"description": "Whether to publish revocation to ledger immediately.",
"schema": {"type": "boolean"},
"required": False
}
],
summary="Revoke an issued credential"
)
@response_schema(V10CredentialExchangeSchema(), 200)
async def credential_exchange_revoke(request: web.BaseRequest):
"""
Expand All @@ -704,9 +730,9 @@ async def credential_exchange_revoke(request: web.BaseRequest):
"""

context = request.app["request_context"]

try:
credential_exchange_id = request.match_info["cred_ex_id"]
publish = bool(json.loads(request.query.get("publish", json.dumps(False))))
credential_exchange_record = await V10CredentialExchange.retrieve_by_id(
context, credential_exchange_id
)
Expand All @@ -723,11 +749,32 @@ async def credential_exchange_revoke(request: web.BaseRequest):

credential_manager = CredentialManager(context)

await credential_manager.revoke_credential(credential_exchange_record)
await credential_manager.revoke_credential(credential_exchange_record, publish)

return web.json_response(credential_exchange_record.serialize())


@docs(tags=["issue-credential"], summary="Publish pending revocations to ledger")
@response_schema(V10PublishRevocationsResultSchema(), 200)
async def credential_exchange_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 = request.app["request_context"]

credential_manager = CredentialManager(context)

return web.json_response(await credential_manager.publish_pending_revocations())


@docs(
tags=["issue-credential"], summary="Remove an existing credential exchange record"
)
Expand Down Expand Up @@ -790,6 +837,10 @@ async def register(app: web.Application):
"/issue-credential/records/{cred_ex_id}/revoke",
credential_exchange_revoke,
),
web.post(
"/issue-credential/publish-revocations",
credential_exchange_publish_revocations,
),
web.post(
"/issue-credential/records/{cred_ex_id}/problem-report",
credential_exchange_problem_report,
Expand Down
Loading