Skip to content
This repository was archived by the owner on Sep 22, 2023. It is now read-only.

feat: Add revocation support to credentials #88

Merged
merged 15 commits into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from 12 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
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ jobs:
- name: Run integration tests
run: |
docker-compose -f int/docker-compose.yml run tests
- name: Print logs on failure
if: failure()
run: |
docker-compose -f int/docker-compose.yml logs acapy_plugin_agent
- name: Clean up integration tests
if: always()
run: |
Expand Down
144 changes: 139 additions & 5 deletions acapy_plugin_toolbox/credential_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# pylint: disable=too-few-public-methods

import logging
from asyncio import shield
from asyncio import ensure_future, shield

from aries_cloudagent.core.profile import ProfileSession
from aries_cloudagent.core.protocol_registry import ProtocolRegistry
Expand All @@ -21,6 +21,17 @@
from aries_cloudagent.storage.error import StorageNotFoundError
from marshmallow import fields

from aries_cloudagent.revocation.error import (
RevocationError,
RevocationNotSupportedError,
)
from aries_cloudagent.revocation.indy import IndyRevocation
from aries_cloudagent.storage.base import BaseStorage
from aries_cloudagent.storage.error import StorageError
from aries_cloudagent.tails.base import BaseTailsServer

from aries_cloudagent.messaging.valid import INDY_REV_REG_SIZE

from .schemas import SchemaRecord
from .util import admin_only, generate_model_schema

Expand Down Expand Up @@ -64,6 +75,9 @@ class CredDefRecord(BaseRecord):
STATE_UNWRITTEN = "unwritten"
STATE_WRITTEN = "written"

REVOCATION_SUPPORTED = True
REVOCATION_UNSUPPORTED = False

class Meta:
"""CredDefRecord metadata."""

Expand All @@ -78,14 +92,18 @@ def __init__(
attributes: [str] = None,
author: str = None,
state: str = None,
**kwargs
support_revocation: bool = False,
revocation_registry_size: int = None,
**kwargs,
):
"""Initialize a new SchemaRecord."""
super().__init__(record_id, state or self.STATE_UNWRITTEN, **kwargs)
self.cred_def_id = cred_def_id
self.schema_id = schema_id
self.attributes = attributes
self.author = author
self.support_revocation = support_revocation
self.revocation_registry_size = revocation_registry_size

@property
def record_id(self) -> str:
Expand All @@ -95,7 +113,11 @@ def record_id(self) -> str:
@property
def record_value(self) -> dict:
"""Get record value."""
return {"attributes": self.attributes}
return {
"attributes": self.attributes,
"support_revocation": self.support_revocation,
"revocation_registry_size": self.revocation_registry_size,
}

@property
def record_tags(self) -> dict:
Expand Down Expand Up @@ -125,13 +147,21 @@ class Meta:
schema_id = fields.Str(required=False)
attributes = fields.List(fields.Str(), required=False)
author = fields.Str(required=False)
support_revocation = fields.Bool(required=False, missing=False)
revocation_registry_size = fields.Int(required=False)


SendCredDef, SendCredDefSchema = generate_model_schema(
name="SendCredDef",
handler="acapy_plugin_toolbox.credential_definitions" ".SendCredDefHandler",
msg_type=SEND_CRED_DEF,
schema={"schema_id": fields.Str(required=True)},
schema={
"schema_id": fields.Str(required=True),
"support_revocation": fields.Bool(required=False),
"revocation_registry_size": fields.Int(
required=False, strict=True, **INDY_REV_REG_SIZE
),
},
)

CredDefID, CredDefIDSchema = generate_model_schema(
Expand All @@ -151,6 +181,23 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
session = await context.session()
ledger: BaseLedger = session.inject(BaseLedger)
issuer: IndyIssuer = session.inject(IndyIssuer)
support_revocation: bool = context.message.support_revocation
revocation_registry_size: int = None

if support_revocation:
revocation_registry_size = context.message.revocation_registry_size
if revocation_registry_size is None:
report = ProblemReport(
description={
"en": "Failed to create revokable credential definition; Error: revocation_registry_size not specified"
},
who_retries="none",
)
LOGGER.warning(
"revocation_registry_size not specified while creating revokable credential definition"
)
await responder.send_reply(report)
return
# If no schema record, make one
try:
schema_record = await SchemaRecord.retrieve_by_schema_id(
Expand All @@ -175,13 +222,14 @@ async def handle(self, context: RequestContext, responder: BaseResponder):

try:
async with ledger:
credential_definition_id, *_ = await shield(
credential_definition_id, _, novel = await shield(
ledger.create_and_send_credential_definition(
issuer,
context.message.schema_id,
tag="{}_{}".format(
schema_record.schema_name, schema_record.schema_version
),
support_revocation=support_revocation,
)
)
except Exception as err:
Expand All @@ -193,13 +241,99 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
await responder.send_reply(report)
return

# If revocation is requested and cred def is novel, create revocation registry
if support_revocation and novel:
profile = context.profile
tails_base_url = profile.settings.get("tails_server_base_url")
if not tails_base_url:
report = ProblemReport(
description={
"en": "Failed to contact Revocation Registry (Not Configured)"
},
who_retries="none",
)
LOGGER.exception("tails_server_base_url not configured")
await responder.send_reply(report)
return
try:
# Create registry
revoc = IndyRevocation(profile)
registry_record = await revoc.init_issuer_registry(
credential_definition_id,
max_cred_num=revocation_registry_size,
)

except RevocationNotSupportedError as e:
report = ProblemReport(
description={"en": "Failed to initialize Revocation Registry"},
who_retries="none",
)
LOGGER.exception("init_issuer_registry failed: %s", e)
await responder.send_reply(report)
return
await shield(registry_record.generate_registry(profile))
try:
await registry_record.set_tails_file_public_uri(
profile, f"{tails_base_url}/{registry_record.revoc_reg_id}"
)
await registry_record.send_def(profile)
await registry_record.send_entry(profile)

# stage pending registry independent of whether tails server is OK
pending_registry_record = await revoc.init_issuer_registry(
registry_record.cred_def_id,
max_cred_num=registry_record.max_cred_num,
)
ensure_future(
pending_registry_record.stage_pending_registry(
profile, max_attempts=16
)
)

tails_server = profile.inject(BaseTailsServer)
(upload_success, reason) = await tails_server.upload_tails_file(
profile,
registry_record.revoc_reg_id,
registry_record.tails_local_path,
interval=0.8,
backoff=-0.5,
max_attempts=5, # heuristic: respect HTTP timeout
)
if not upload_success:
report = ProblemReport(
description={
"en": f"Tails file for rev reg {registry_record.revoc_reg_id} failed to upload: {reason}"
},
who_retries="none",
)
LOGGER.exception(
f"Tails file for rev reg {registry_record.revoc_reg_id} failed to upload: {reason}"
)
await responder.send_reply(report)
return

except RevocationError as e:
report = ProblemReport(
description={
"en": "Error occurred while setting up revocation registry"
},
who_retries="none",
)
LOGGER.exception(
"Error occurred while setting up revocation registry: %s", e
)
await responder.send_reply(report)
return

# we may not need to save the record as below
cred_def_record = CredDefRecord(
cred_def_id=credential_definition_id,
schema_id=context.message.schema_id,
attributes=list(map(canon, schema_record.attributes)),
state=CredDefRecord.STATE_WRITTEN,
author=CredDefRecord.AUTHOR_SELF,
support_revocation=support_revocation,
revocation_registry_size=revocation_registry_size,
)
await cred_def_record.save(
session, reason="Committed credential definition to ledger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,14 @@ def __init__(self, record: PresExRecord, **kwargs):

async def retrieve_matching_credentials(self, profile: Profile):
holder = profile.inject(IndyHolder)
request = self.record.presentation_request

if not (type(request) is dict):
request = request.serialize()

self.matching_credentials = (
await holder.get_credentials_for_presentation_request_by_referent(
self.record.presentation_request.serialize(),
request,
(),
0,
self.DEFAULT_COUNT,
Expand Down
66 changes: 65 additions & 1 deletion acapy_plugin_toolbox/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,18 @@
V10PresentationSendRequestRequestSchema,
)
from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport
from aries_cloudagent.storage.error import StorageNotFoundError
from aries_cloudagent.storage.error import StorageError, StorageNotFoundError
from aries_cloudagent.revocation.error import (
RevocationError,
RevocationNotSupportedError,
)
from aries_cloudagent.revocation.indy import IndyRevocation
from aries_cloudagent.revocation.manager import (
RevocationManager,
RevocationManagerError,
)
from aries_cloudagent.indy.issuer import IndyIssuerError
from aries_cloudagent.ledger.error import LedgerError
from marshmallow import fields
from uuid import UUID

Expand All @@ -56,9 +67,14 @@
with_generic_init,
)

import logging

LOGGER = logging.getLogger(__name__)

PROTOCOL = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-issuer/0.1"

SEND_CREDENTIAL = "{}/send-credential".format(PROTOCOL)
REVOKE_CREDENTIAL = "{}/revoke-credential".format(PROTOCOL)
REQUEST_PRESENTATION = "{}/request-presentation".format(PROTOCOL)
ISSUER_CRED_EXCHANGE = "{}/credential-exchange".format(PROTOCOL)
ISSUER_PRES_EXCHANGE = "{}/presentation-exchange".format(PROTOCOL)
Expand All @@ -69,6 +85,7 @@

MESSAGE_TYPES = {
SEND_CREDENTIAL: "acapy_plugin_toolbox.issuer.SendCred",
REVOKE_CREDENTIAL: "acapy_plugin_toolbox.issuer.RevokeCred",
REQUEST_PRESENTATION: "acapy_plugin_toolbox.issuer.RequestPres",
CREDENTIALS_GET_LIST: "acapy_plugin_toolbox.issuer.CredGetList",
CREDENTIALS_LIST: "acapy_plugin_toolbox.issuer.CredList",
Expand Down Expand Up @@ -173,6 +190,53 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
await responder.send_reply(cred_exchange)


RevokeCred, RevokeCredSchema = generate_model_schema(
name="RevokeCred",
handler="acapy_plugin_toolbox.issuer.RevokeCredHandler",
msg_type=REVOKE_CREDENTIAL,
schema={
"credential_exchange_id": fields.Str(required=False),
},
)


class RevokeCredHandler(BaseHandler):
"""Handler for received send request."""

@admin_only
async def handle(self, context: RequestContext, responder: BaseResponder):
"""Handle received send request."""
rev_reg_id = None # body.get("rev_reg_id")
cred_rev_id = None # body.get("cred_rev_id") # numeric str, which indy wants
cred_ex_id = context.message.credential_exchange_id
publish = True # body.get("publish")

rev_manager = RevocationManager(context.profile)
try:
if cred_ex_id:
await rev_manager.revoke_credential_by_cred_ex_id(cred_ex_id, publish)
else:
await rev_manager.revoke_credential(rev_reg_id, cred_rev_id, publish)
except (
RevocationManagerError,
RevocationError,
StorageError,
IndyIssuerError,
LedgerError,
) as err:
# raise web.HTTPBadRequest(reason=err.roll_up) from err
report = ProblemReport(
description={
"en": "Failed to revoke credential; Error: {}".format(err)
},
who_retries="none",
)
LOGGER.exception("Failed to revoke credential: %s", err)
await responder.send_reply(report)
return
await responder.send_reply({})


@expand_message_class
class RequestPres(AdminIssuerMessage):
"""Request presentation message."""
Expand Down
2 changes: 2 additions & 0 deletions demo/configs/alice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ auto-respond-presentation-proposal: true
auto-respond-presentation-request: true
auto-verify-presentation: true

tails-server-base-url: http://tails-server:6543

# Wallet
wallet-type: indy
wallet-key: "insecure, for use in demo only"
Expand Down
2 changes: 2 additions & 0 deletions demo/configs/bob.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ auto-respond-presentation-proposal: true
auto-respond-presentation-request: true
auto-verify-presentation: true

tails-server-base-url: http://tails-server:6543

# Wallet
wallet-type: indy
wallet-key: "insecure, for use in demo only"
Expand Down
21 changes: 20 additions & 1 deletion demo/docker-compose.alice-bob.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,23 @@ services:
ACAPY_TOOLBOX_LOG_LEVEL: WARNING
ports:
- "3003:3003"
command: poetry run aca-py start --arg-file ./configs/bob.yml
command: poetry run aca-py start --arg-file ./configs/bob.yml

tunnel-tails-server:
image: efrecon/localtunnel
restart: on-failure
links:
- tails-server
depends_on:
- tails-server
command: --local-host tails-server --port 6543 --host ${TAILS_TUNNEL_HOST:-https://localtunnel.me}
tails-server:
image: dbluhm/indy-tails-server:latest
ports:
- 6543:6543
command: >
tails-server
--host 0.0.0.0
--port 6543
--storage-path ${STORAGE_PATH:-/tmp/tails-files}
--log-level ${LOG_LEVEL:-INFO}