diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 10db676f70..592a879c7a 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -157,6 +157,29 @@ async def revoke_credential( await issuer_rr_rec.mark_pending(txn, cred_rev_id) await txn.commit() + async def update_rev_reg_revoked_state( + self, + apply_ledger_update: bool, + rev_reg_record: IssuerRevRegRecord, + genesis_transactions: dict, + ) -> (dict, dict, dict): + """ + Request handler to fix ledger entry of credentials revoked against registry. + + Args: + rev_reg_id: revocation registry id + apply_ledger_update: whether to apply an update to the ledger + + Returns: + Number of credentials posted to ledger + + """ + return await rev_reg_record.fix_ledger_entry( + self._profile, + apply_ledger_update, + genesis_transactions, + ) + async def publish_pending_revocations( self, rrid2crid: Mapping[Text, Sequence[Text]] = None, diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index 98451dcb3d..2e9d7b7e42 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -7,7 +7,7 @@ from os.path import join from pathlib import Path from shutil import move -from typing import Any, Mapping, Sequence, Union +from typing import Any, Mapping, Sequence, Union, Tuple from urllib.parse import urlparse from marshmallow import fields, validate @@ -22,6 +22,7 @@ ) from ...indy.util import indy_client_dir from ...ledger.base import BaseLedger +from ...ledger.error import LedgerError, LedgerTransactionError from ...messaging.models.base_record import BaseRecord, BaseRecordSchema from ...messaging.valid import ( BASE58_SHA256_HASH, @@ -33,7 +34,9 @@ from ...tails.base import BaseTailsServer from ..error import RevocationError +from ..recover import generate_ledger_rrrecovery_txn +from .issuer_cred_rev_record import IssuerCredRevRecord from .revocation_registry import RevocationRegistry DEFAULT_REGISTRY_SIZE = 1000 @@ -290,14 +293,44 @@ async def send_entry( ledger = profile.inject(BaseLedger) async with ledger: - rev_entry_res = await ledger.send_revoc_reg_entry( - self.revoc_reg_id, - self.revoc_def_type, - self._revoc_reg_entry.ser, - self.issuer_did, - write_ledger=write_ledger, - endorser_did=endorser_did, - ) + try: + rev_entry_res = await ledger.send_revoc_reg_entry( + self.revoc_reg_id, + self.revoc_def_type, + self._revoc_reg_entry.ser, + self.issuer_did, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) + except LedgerTransactionError as err: + if "InvalidClientRequest" in err.roll_up: + # ... if the ledger write fails (with "InvalidClientRequest") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientRequest(...) + # In this scenario we try to post a correction + LOGGER.warn("Retry ledger update/fix due to error") + LOGGER.warn(err) + (_, _, res) = await self.fix_ledger_entry( + profile, + True, + ledger.pool.genesis_txns, + ) + rev_entry_res = {"result": res} + LOGGER.warn("Ledger update/fix applied") + elif "InvalidClientTaaAcceptanceError" in err.roll_up: + # if no write access (with "InvalidClientTaaAcceptanceError") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientTaaAcceptanceError(...) + LOGGER.error("Ledger update failed due to TAA issue") + LOGGER.error(err) + raise err + else: + # not sure what happened, raise an error + LOGGER.error("Ledger update failed due to unknown issue") + LOGGER.error(err) + raise err if self.state == IssuerRevRegRecord.STATE_POSTED: self.state = IssuerRevRegRecord.STATE_ACTIVE # initial entry activates async with profile.session() as session: @@ -307,6 +340,80 @@ async def send_entry( return rev_entry_res + async def fix_ledger_entry( + self, + profile: Profile, + apply_ledger_update: bool, + genesis_transactions: str, + ) -> Tuple[dict, dict, dict]: + """Fix the ledger entry to match wallet-recorded credentials.""" + # get rev reg delta (revocations published to ledger) + ledger = profile.inject(BaseLedger) + async with ledger: + (rev_reg_delta, _) = await ledger.get_revoc_reg_delta(self.revoc_reg_id) + + # get rev reg records from wallet (revocations and status) + recs = [] + rec_count = 0 + accum_count = 0 + recovery_txn = {} + applied_txn = {} + async with profile.session() as session: + recs = await IssuerCredRevRecord.query_by_ids( + session, rev_reg_id=self.revoc_reg_id + ) + + revoked_ids = [] + for rec in recs: + if rec.state == IssuerCredRevRecord.STATE_REVOKED: + revoked_ids.append(int(rec.cred_rev_id)) + if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]: + # await rec.set_state(session, IssuerCredRevRecord.STATE_ISSUED) + rec_count += 1 + + LOGGER.debug(">>> fixed entry recs count = %s", rec_count) + LOGGER.debug( + ">>> rev_reg_record.revoc_reg_entry.value: %s", + self.revoc_reg_entry.value, + ) + LOGGER.debug( + '>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value") + ) + + # if we had any revocation discrepencies, check the accumulator value + if rec_count > 0: + if (self.revoc_reg_entry.value and rev_reg_delta.get("value")) and not ( + self.revoc_reg_entry.value.accum == rev_reg_delta["value"]["accum"] + ): + # self.revoc_reg_entry = rev_reg_delta["value"] + # await self.save(session) + accum_count += 1 + + calculated_txn = await generate_ledger_rrrecovery_txn( + genesis_transactions, + self.revoc_reg_id, + revoked_ids, + ) + recovery_txn = json.loads(calculated_txn.to_json()) + + LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update) + if apply_ledger_update: + ledger = session.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not session.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise LedgerError(reason=reason) + + async with ledger: + ledger_response = await ledger.send_revoc_reg_entry( + self.revoc_reg_id, "CL_ACCUM", recovery_txn + ) + + applied_txn = ledger_response["result"] + + return (rev_reg_delta, recovery_txn, applied_txn) + @property def has_local_tails_file(self) -> bool: """Check if a local copy of the tails file is available.""" diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 964afe20b3..0e897beb02 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -59,7 +59,6 @@ IssuerCredRevRecordSchema, ) from .models.issuer_rev_reg_record import IssuerRevRegRecord, IssuerRevRegRecordSchema -from .recover import generate_ledger_rrrecovery_txn from .util import ( REVOCATION_EVENT_PREFIX, REVOCATION_REG_INIT_EVENT, @@ -729,13 +728,13 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): @response_schema(RevRegWalletUpdatedResultSchema(), 200, description="") async def update_rev_reg_revoked_state(request: web.BaseRequest): """ - Request handler to get number of credentials issued against revocation registry. + Request handler to fix ledger entry of credentials revoked against registry. Args: request: aiohttp request object Returns: - Number of credentials updated in wallet + Number of credentials posted to ledger """ context: AdminRequestContext = request["context"] @@ -746,16 +745,8 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): LOGGER.debug(">>> apply_ledger_update_json = %s", apply_ledger_update_json) apply_ledger_update = json.loads(request.query.get("apply_ledger_update", "false")) - # get rev reg delta (revocations published to ledger) - revoc = IndyRevocation(context.profile) - rev_reg_delta = await revoc.get_issuer_rev_reg_delta(rev_reg_id) - - # get rev reg records from wallet (revocations and status) - recs = [] - rec_count = 0 - accum_count = 0 - recovery_txn = {} - applied_txn = {} + rev_reg_record = None + genesis_transactions = None async with context.profile.session() as session: try: rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( @@ -763,72 +754,49 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): ) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - recs = await IssuerCredRevRecord.query_by_ids(session, rev_reg_id=rev_reg_id) - revoked_ids = [] - for rec in recs: - if rec.state == IssuerCredRevRecord.STATE_REVOKED: - revoked_ids.append(int(rec.cred_rev_id)) - if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]: - # await rec.set_state(session, IssuerCredRevRecord.STATE_ISSUED) - rec_count += 1 - - LOGGER.debug(">>> fixed entry recs count = %s", rec_count) - LOGGER.debug( - ">>> rev_reg_record.revoc_reg_entry.value: %s", - rev_reg_record.revoc_reg_entry.value, - ) - LOGGER.debug('>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value")) - - # if we had any revocation discrepencies, check the accumulator value - if rec_count > 0: - if ( - rev_reg_record.revoc_reg_entry.value and rev_reg_delta.get("value") - ) and not ( - rev_reg_record.revoc_reg_entry.value.accum - == rev_reg_delta["value"]["accum"] - ): - # rev_reg_record.revoc_reg_entry = rev_reg_delta["value"] - # await rev_reg_record.save(session) - accum_count += 1 - - genesis_transactions = context.settings.get("ledger.genesis_transactions") - if not genesis_transactions: - ledger_manager = context.injector.inject(BaseMultipleLedgerManager) - write_ledgers = await ledger_manager.get_write_ledger() - LOGGER.debug(f"write_ledgers = {write_ledgers}") - pool = write_ledgers[1].pool - LOGGER.debug(f"write_ledger pool = {pool}") - - genesis_transactions = pool.genesis_txns - - if not genesis_transactions: - raise web.HTTPInternalServerError( - reason="no genesis_transactions for writable ledger" - ) + genesis_transactions = context.settings.get("ledger.genesis_transactions") + if not genesis_transactions: + ledger_manager = context.injector.inject(BaseMultipleLedgerManager) + write_ledgers = await ledger_manager.get_write_ledger() + LOGGER.debug(f"write_ledgers = {write_ledgers}") + pool = write_ledgers[1].pool + LOGGER.debug(f"write_ledger pool = {pool}") + + genesis_transactions = pool.genesis_txns - calculated_txn = await generate_ledger_rrrecovery_txn( - genesis_transactions, - rev_reg_id, - revoked_ids, + if not genesis_transactions: + raise web.HTTPInternalServerError( + reason="no genesis_transactions for writable ledger" ) - recovery_txn = json.loads(calculated_txn.to_json()) - - LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update) - if apply_ledger_update: - ledger = session.inject_or(BaseLedger) - if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise web.HTTPInternalServerError(reason=reason) - - async with ledger: - ledger_response = await ledger.send_revoc_reg_entry( - rev_reg_id, "CL_ACCUM", recovery_txn - ) - applied_txn = ledger_response["result"] + if apply_ledger_update: + ledger = session.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not session.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise web.HTTPInternalServerError(reason=reason) + + rev_manager = RevocationManager(context.profile) + try: + ( + rev_reg_delta, + recovery_txn, + applied_txn, + ) = await rev_manager.update_rev_reg_revoked_state( + apply_ledger_update, rev_reg_record, genesis_transactions + ) + except ( + RevocationManagerError, + RevocationError, + StorageError, + IndyIssuerError, + LedgerError, + ) as err: + raise web.HTTPBadRequest(reason=err.roll_up) + except Exception as err: + raise web.HTTPBadRequest(reason=str(err)) return web.json_response( { diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index e59f668b15..b5fac909fc 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -83,8 +83,14 @@ def step_impl(context, holder): # get the required revocation info from the last credential exchange cred_exchange = context.cred_exchange + cred_ex_id = ( + cred_exchange["cred_ex_id"] + if "cred_ex_id" in cred_exchange + else cred_exchange["cred_ex_record"]["cred_ex_id"] + ) + cred_exchange = agent_container_GET( - agent["agent"], "/issue-credential-2.0/records/" + cred_exchange["cred_ex_id"] + agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id ) context.cred_exchange = cred_exchange print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) @@ -141,6 +147,13 @@ def step_impl(context, holder): + cred_exchange["indy"]["rev_reg_id"] + "/issued/indy_recs", ) + print("ledger_revoked_creds:", ledger_revoked_creds) + print( + "assert", + cred_exchange["indy"]["cred_rev_id"], + "in", + ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"], + ) assert ( int(cred_exchange["indy"]["cred_rev_id"]) in ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"] @@ -155,9 +168,16 @@ def step_impl(context, holder): # get the required revocation info from the last credential exchange cred_exchange = context.cred_exchange + print("cred_exchange:", json.dumps(cred_exchange)) + + cred_ex_id = ( + cred_exchange["cred_ex_id"] + if "cred_ex_id" in cred_exchange + else cred_exchange["cred_ex_record"]["cred_ex_id"] + ) cred_exchange = agent_container_GET( - agent["agent"], "/issue-credential-2.0/records/" + cred_exchange["cred_ex_id"] + agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id ) context.cred_exchange = cred_exchange print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) @@ -217,6 +237,7 @@ def step_impl(context, holder): + cred_exchange["indy"]["rev_reg_id"] + "/issued/indy_recs", ) + print("ledger_revoked_creds:", ledger_revoked_creds) assert ( int(cred_exchange["indy"]["cred_rev_id"]) not in ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"] diff --git a/demo/features/taa-txn-author-acceptance.feature b/demo/features/taa-txn-author-acceptance.feature index bcaaa3dc32..55d5790227 100644 --- a/demo/features/taa-txn-author-acceptance.feature +++ b/demo/features/taa-txn-author-acceptance.feature @@ -90,3 +90,159 @@ Feature: TAA Transaction Author Agreement related tests Examples: | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger authomatically with the next revoked credential + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.0-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger manually before revoking more credentials + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + Then "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.1-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger by manually applying a correction + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + Then "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.2-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger automatically with the next revocation + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.5-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger authomatically by revoking the last credential + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 |