diff --git a/AnonCredsWalletType.md b/AnonCredsWalletType.md new file mode 100644 index 0000000000..d8cfcd8409 --- /dev/null +++ b/AnonCredsWalletType.md @@ -0,0 +1,106 @@ +# AnonCreds-Rs Support + +A new wallet type has been added to Aca-Py to support the new anoncreds-rs library: + +``` +--wallet-type askar-anoncreds +``` + +When Aca-Py is run with this wallet type it will run with an Askar format wallet (and askar libraries) but will use `anoncreds-rs` instead of `credx`. + +There is a new package under `aries_cloudagent/anoncreds` with code that supports the new library. + +There are new endpoints (under `/anoncreds`) for creating a Schema and Credential Definition. However the new anoncreds code is integrated into the existing Credential and Presentation endpoints (V2.0 endpoints only). + +Within the protocols, there are new `handler` libraries to support the new `anoncreds` format (these are in parallel to the existing `indy` libraries). + +The existing `indy` code are in: + +``` +aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py +aries_cloudagent/protocols/indy/anoncreds/pres_exch_handler.py +aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py +``` + +The new `anoncreds` code is in: + +``` +aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py +aries_cloudagent/protocols/present_proof/anoncreds/pres_exch_handler.py +aries_cloudagent/protocols/present_proof/v2_0/formats/anoncreds/handler.py +``` + +The Indy handler checks to see if the wallet type is `askar-anoncreds` and if so delegates the calls to the anoncreds handler, for example: + +``` + # Temporary shim while the new anoncreds library integration is in progress + wallet_type = profile.settings.get_value("wallet.type") + if wallet_type == "askar-anoncreds": + self.anoncreds_handler = AnonCredsPresExchangeHandler(profile) +``` + +... and then: + +``` + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return self.anoncreds_handler.get_format_identifier(message_type) +``` + +To run the alice/faber demo using the new anoncreds library, start the demo with: + +``` +--wallet-type askar-anoncreds +``` + +There are no anoncreds-specific integration tests, for the new anoncreds functionality the agents within the integration tests are started with: + +``` +--wallet-type askar-anoncreds +``` + +Everything should just work!!! + +Theoretically ATH should work with anoncreds as well, by setting the wallet type (see https://github.com/hyperledger/aries-agent-test-harness#extra-backchannel-specific-parameters). + +## Outstanding work + +- unit tests (in the new anoncreds package) +- unit tests (review and possibly update unit tests for the credential and presentation integration) +- revocation support - migrate code from `anoncreds-rs` branch +- revocation support - complete the revocation implementation (support for unhappy path scenarios) +- endorsement (not implemented with new anoncreds code) +- testing - various scenarios like mediation, multitenancy etc. +- wallet upgrade (askar to askar-anoncreds) +- update V1.0 versions of the Credential and Presentation endpoints to use anoncreds +- any other anoncreds issues - https://github.com/hyperledger/aries-cloudagent-python/issues?q=is%3Aopen+is%3Aissue+label%3AAnonCreds + +## Retiring old Indy and Askar (credx) Code + +The main changes for the Credential and Presentation support are in the following two files: + +``` +aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py +aries_cloudagent/protocols/present_proof/v2_0/messages/pres_format.py +``` + +The `INDY` handler just need to be re-pointed to the new anoncreds handler, and then all the old Indy code can be retired. + +The new code is already in place (in comments). For example for the Credential handler: + +``` + To make the switch from indy to anoncreds replace the above with the following + INDY = FormatSpec( + "hlindy/", + DeferLoad( + "aries_cloudagent.protocols.present_proof.v2_0" + ".formats.anoncreds.handler.AnonCredsPresExchangeHandler" + ), + ) +``` + +There is a bunch of duplicated code, i.e. the new anoncreds code was added either as new classes (as above) or as new methods within an existing class. + +Some new methods were added within the Ledger class. + +New unit tests were added - in some cases as methods within existing test classes, and in some cases as new classes (whichever was easiest at the time). diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index 662cc61c74..22a3b1e62f 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -1684,6 +1684,7 @@ def get_settings(self, args: Namespace) -> dict: if args.recreate_wallet: settings["wallet.recreate"] = True # check required settings for 'indy' wallets + # TODO should this also include "askar*" wallet types? if settings["wallet.type"] == "indy": # requires name, key if not args.wallet_name or not args.wallet_key: diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/__init__.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py new file mode 100644 index 0000000000..664735e751 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py @@ -0,0 +1,441 @@ +"""V2.0 issue-credential indy credential format handler.""" + +import json +import logging +from typing import Mapping, Tuple + +from marshmallow import RAISE + +from ......anoncreds.revocation import AnonCredsRevocation + +from ......anoncreds.registry import AnonCredsRegistry +from ......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ......anoncreds.issuer import ( + AnonCredsIssuer, +) +from ......indy.models.cred import IndyCredentialSchema +from ......indy.models.cred_abstract import IndyCredAbstractSchema +from ......indy.models.cred_request import IndyCredRequestSchema +from ......cache.base import BaseCache +from ......ledger.base import BaseLedger +from ......ledger.multiple_ledger.ledger_requests_executor import ( + GET_CRED_DEF, + IndyLedgerRequestsExecutor, +) +from ......messaging.credential_definitions.util import ( + CRED_DEF_SENT_RECORD_TYPE, + CredDefQueryStringSchema, +) +from ......messaging.decorators.attach_decorator import AttachDecorator +from ......multitenant.base import BaseMultitenantManager +from ......revocation.models.issuer_cred_rev_record import IssuerCredRevRecord +from ......storage.base import BaseStorage +from ...message_types import ( + ATTACHMENT_FORMAT, + CRED_20_ISSUE, + CRED_20_OFFER, + CRED_20_PROPOSAL, + CRED_20_REQUEST, +) +from ...messages.cred_format import V20CredFormat +from ...messages.cred_issue import V20CredIssue +from ...messages.cred_offer import V20CredOffer +from ...messages.cred_proposal import V20CredProposal +from ...messages.cred_request import V20CredRequest +from ...models.cred_ex_record import V20CredExRecord +from ...models.detail.indy import V20CredExRecordIndy +from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler + +LOGGER = logging.getLogger(__name__) + + +class AnonCredsCredFormatHandler(V20CredFormatHandler): + """Indy credential format handler.""" + + format = V20CredFormat.Format.INDY + + @classmethod + def validate_fields(cls, message_type: str, attachment_data: Mapping): + """Validate attachment data for a specific message type. + + Uses marshmallow schemas to validate if format specific attachment data + is valid for the specified message type. Only does structural and type + checks, does not validate if .e.g. the issuer value is valid. + + + Args: + message_type (str): The message type to validate the attachment data for. + Should be one of the message types as defined in message_types.py + attachment_data (Mapping): [description] + The attachment data to valide + + Raises: + Exception: When the data is not valid. + + """ + mapping = { + CRED_20_PROPOSAL: CredDefQueryStringSchema, + CRED_20_OFFER: IndyCredAbstractSchema, + CRED_20_REQUEST: IndyCredRequestSchema, + CRED_20_ISSUE: IndyCredentialSchema, + } + + # Get schema class + Schema = mapping[message_type] + + # Validate, throw if not valid + Schema(unknown=RAISE).load(attachment_data) + + async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordIndy: + """Retrieve credential exchange detail record by cred_ex_id.""" + + async with self.profile.session() as session: + records = ( + await AnonCredsCredFormatHandler.format.detail.query_by_cred_ex_id( + session, cred_ex_id + ) + ) + + if len(records) > 1: + LOGGER.warning( + "Cred ex id %s has %d %s detail records: should be 1", + cred_ex_id, + len(records), + AnonCredsCredFormatHandler.format.api, + ) + return records[0] if records else None + + async def _check_uniqueness(self, cred_ex_id: str): + """Raise exception on evidence that cred ex already has cred issued to it.""" + async with self.profile.session() as session: + exist = await AnonCredsCredFormatHandler.format.detail.query_by_cred_ex_id( + session, cred_ex_id + ) + if exist: + raise V20CredFormatError( + f"{AnonCredsCredFormatHandler.format.api} detail record already " + f"exists for cred ex id {cred_ex_id}" + ) + + def get_format_identifier(self, message_type: str) -> str: + """Get attachment format identifier for format and message combination. + + Args: + message_type (str): Message type for which to return the format identifier + + Returns: + str: Issue credential attachment format identifier + + """ + return ATTACHMENT_FORMAT[message_type][AnonCredsCredFormatHandler.format.api] + + def get_format_data(self, message_type: str, data: dict) -> CredFormatAttachment: + """Get credential format and attachment objects for use in cred ex messages. + + Returns a tuple of both credential format and attachment decorator for use + in credential exchange messages. It looks up the correct format identifier and + encodes the data as a base64 attachment. + + Args: + message_type (str): The message type for which to return the cred format. + Should be one of the message types defined in the message types file + data (dict): The data to include in the attach decorator + + Returns: + CredFormatAttachment: Credential format and attachment data objects + + """ + return ( + V20CredFormat( + attach_id=AnonCredsCredFormatHandler.format.api, + format_=self.get_format_identifier(message_type), + ), + AttachDecorator.data_base64( + data, ident=AnonCredsCredFormatHandler.format.api + ), + ) + + async def _match_sent_cred_def_id(self, tag_query: Mapping[str, str]) -> str: + """Return most recent matching id of cred def that agent sent to ledger.""" + + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + found = await storage.find_all_records( + type_filter=CRED_DEF_SENT_RECORD_TYPE, tag_query=tag_query + ) + if not found: + raise V20CredFormatError( + f"Issuer has no operable cred def for proposal spec {tag_query}" + ) + return max(found, key=lambda r: int(r.tags["epoch"])).tags["cred_def_id"] + + async def create_proposal( + self, cred_ex_record: V20CredExRecord, proposal_data: Mapping[str, str] + ) -> Tuple[V20CredFormat, AttachDecorator]: + """Create indy credential proposal.""" + if proposal_data is None: + proposal_data = {} + + return self.get_format_data(CRED_20_PROPOSAL, proposal_data) + + async def receive_proposal( + self, cred_ex_record: V20CredExRecord, cred_proposal_message: V20CredProposal + ) -> None: + """Receive indy credential proposal. + + No custom handling is required for this step. + """ + + async def create_offer( + self, cred_proposal_message: V20CredProposal + ) -> CredFormatAttachment: + """Create indy credential offer.""" + + issuer = AnonCredsIssuer(self.profile) + ledger = self.profile.inject(BaseLedger) + cache = self.profile.inject_or(BaseCache) + + cred_def_id = await issuer.match_created_credential_definitions( + **cred_proposal_message.attachment(AnonCredsCredFormatHandler.format) + ) + + async def _create(): + offer_json = await issuer.create_credential_offer(cred_def_id) + return json.loads(offer_json) + + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + )[1] + async with ledger: + schema_id = await ledger.credential_definition_id2schema_id(cred_def_id) + schema = await ledger.get_schema(schema_id) + schema_attrs = set(schema["attrNames"]) + preview_attrs = set(cred_proposal_message.credential_preview.attr_dict()) + if preview_attrs != schema_attrs: + raise V20CredFormatError( + f"Preview attributes {preview_attrs} " + f"mismatch corresponding schema attributes {schema_attrs}" + ) + + cred_offer = None + cache_key = f"credential_offer::{cred_def_id}" + + if cache: + async with cache.acquire(cache_key) as entry: + if entry.result: + cred_offer = entry.result + else: + cred_offer = await _create() + await entry.set_result(cred_offer, 3600) + if not cred_offer: + cred_offer = await _create() + + return self.get_format_data(CRED_20_OFFER, cred_offer) + + async def receive_offer( + self, cred_ex_record: V20CredExRecord, cred_offer_message: V20CredOffer + ) -> None: + """Receive indy credential offer.""" + + async def create_request( + self, cred_ex_record: V20CredExRecord, request_data: Mapping = None + ) -> CredFormatAttachment: + """Create indy credential request.""" + if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: + raise V20CredFormatError( + "Indy issue credential format cannot start from credential request" + ) + + await self._check_uniqueness(cred_ex_record.cred_ex_id) + + holder_did = request_data.get("holder_did") if request_data else None + cred_offer = cred_ex_record.cred_offer.attachment( + AnonCredsCredFormatHandler.format + ) + + if "nonce" not in cred_offer: + raise V20CredFormatError("Missing nonce in credential offer") + + nonce = cred_offer["nonce"] + cred_def_id = cred_offer["cred_def_id"] + + async def _create(): + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred_def_id + ) + + holder = AnonCredsHolder(self.profile) + request_json, metadata_json = await holder.create_credential_request( + cred_offer, cred_def_result.credential_definition, holder_did + ) + + return { + "request": json.loads(request_json), + "metadata": json.loads(metadata_json), + } + + cache_key = f"credential_request::{cred_def_id}::{holder_did}::{nonce}" + cred_req_result = None + cache = self.profile.inject_or(BaseCache) + if cache: + async with cache.acquire(cache_key) as entry: + if entry.result: + cred_req_result = entry.result + else: + cred_req_result = await _create() + await entry.set_result(cred_req_result, 3600) + if not cred_req_result: + cred_req_result = await _create() + + detail_record = V20CredExRecordIndy( + cred_ex_id=cred_ex_record.cred_ex_id, + cred_request_metadata=cred_req_result["metadata"], + ) + + async with self.profile.session() as session: + await detail_record.save(session, reason="create v2.0 credential request") + + return self.get_format_data(CRED_20_REQUEST, cred_req_result["request"]) + + async def receive_request( + self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest + ) -> None: + """Receive indy credential request.""" + if not cred_ex_record.cred_offer: + raise V20CredFormatError( + "Indy issue credential format cannot start from credential request" + ) + + async def issue_credential( + self, cred_ex_record: V20CredExRecord, retries: int = 5 + ) -> CredFormatAttachment: + """Issue indy credential.""" + await self._check_uniqueness(cred_ex_record.cred_ex_id) + + cred_offer = cred_ex_record.cred_offer.attachment( + AnonCredsCredFormatHandler.format + ) + cred_request = cred_ex_record.cred_request.attachment( + AnonCredsCredFormatHandler.format + ) + cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict( + decode=False + ) + + issuer = AnonCredsIssuer(self.profile) + cred_def_id = cred_offer["cred_def_id"] + if await issuer.cred_def_supports_revocation(cred_def_id): + revocation = AnonCredsRevocation(self.profile) + cred_json, cred_rev_id, rev_reg_def_id = await revocation.create_credential( + cred_offer, cred_request, cred_values + ) + else: + cred_json = await issuer.create_credential( + cred_offer, cred_request, cred_values + ) + cred_rev_id = None + rev_reg_def_id = None + + result = self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + + async with self._profile.transaction() as txn: + detail_record = V20CredExRecordIndy( + cred_ex_id=cred_ex_record.cred_ex_id, + rev_reg_id=rev_reg_def_id, + cred_rev_id=cred_rev_id, + ) + await detail_record.save(txn, reason="v2.0 issue credential") + + if cred_rev_id: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id=cred_ex_record.cred_ex_id, + cred_ex_version=IssuerCredRevRecord.VERSION_2, + rev_reg_id=rev_reg_def_id, + cred_rev_id=cred_rev_id, + ) + await issuer_cr_rec.save( + txn, + reason=( + "Created issuer cred rev record for " + f"rev reg id {rev_reg_def_id}, index {cred_rev_id}" + ), + ) + await txn.commit() + + return result + + async def receive_credential( + self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue + ) -> None: + """Receive indy credential. + + Validation is done in the store credential step. + """ + + async def store_credential( + self, cred_ex_record: V20CredExRecord, cred_id: str = None + ) -> None: + """Store indy credential.""" + cred = cred_ex_record.cred_issue.attachment(AnonCredsCredFormatHandler.format) + + rev_reg_def = None + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred["cred_def_id"] + ) + if cred.get("rev_reg_id"): + rev_reg_def_result = ( + await anoncreds_registry.get_revocation_registry_definition( + self.profile, cred["rev_reg_id"] + ) + ) + rev_reg_def = rev_reg_def_result.revocation_registry + + holder = AnonCredsHolder(self.profile) + cred_offer_message = cred_ex_record.cred_offer + mime_types = None + if cred_offer_message and cred_offer_message.credential_preview: + mime_types = cred_offer_message.credential_preview.mime_types() or None + + if rev_reg_def: + revocation = AnonCredsRevocation(self.profile) + await revocation.get_or_fetch_local_tails_path(rev_reg_def) + try: + detail_record = await self.get_detail_record(cred_ex_record.cred_ex_id) + if detail_record is None: + raise V20CredFormatError( + f"No credential exchange {AnonCredsCredFormatHandler.format.aries} " + f"detail record found for cred ex id {cred_ex_record.cred_ex_id}" + ) + cred_id_stored = await holder.store_credential( + cred_def_result.credential_definition.serialize(), + cred, + detail_record.cred_request_metadata, + mime_types, + credential_id=cred_id, + rev_reg_def=rev_reg_def.serialize() if rev_reg_def else None, + ) + + detail_record.cred_id_stored = cred_id_stored + detail_record.rev_reg_id = cred.get("rev_reg_id", None) + detail_record.cred_rev_id = cred.get("cred_rev_id", None) + + async with self.profile.session() as session: + # Store detail record, emit event + await detail_record.save( + session, reason="store credential v2.0", event=True + ) + except AnonCredsHolderError as e: + LOGGER.error(f"Error storing credential: {e.error_code} - {e.message}") + raise e diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/tests/__init__.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py new file mode 100644 index 0000000000..91a4609674 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py @@ -0,0 +1,1282 @@ +from copy import deepcopy +import json +import pytest +from time import time + +from asynctest import mock as async_mock +from unittest import IsolatedAsyncioTestCase +from aries_cloudagent.tests import mock +from marshmallow import ValidationError + +from .. import handler as test_module +from .......anoncreds.holder import AnonCredsHolder +from .......anoncreds.issuer import AnonCredsIssuer +from .......anoncreds.revocation import AnonCredsRevocationRegistryFullError +from .......cache.base import BaseCache +from .......cache.in_memory import InMemoryCache +from .......core.in_memory import InMemoryProfile +from .......ledger.base import BaseLedger +from .......ledger.multiple_ledger.ledger_requests_executor import ( + IndyLedgerRequestsExecutor, +) +from .......messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE +from .......messaging.decorators.attach_decorator import AttachDecorator +from .......multitenant.base import BaseMultitenantManager +from .......multitenant.manager import MultitenantManager +from .......storage.record import StorageRecord + +from ....models.detail.indy import V20CredExRecordIndy +from ....messages.cred_proposal import V20CredProposal +from ....messages.cred_format import V20CredFormat +from ....messages.cred_issue import V20CredIssue +from ....messages.inner.cred_preview import V20CredPreview, V20CredAttrSpec +from ....messages.cred_offer import V20CredOffer +from ....messages.cred_request import ( + V20CredRequest, +) +from ....models.cred_ex_record import V20CredExRecord +from ....message_types import ( + ATTACHMENT_FORMAT, + CRED_20_ISSUE, + CRED_20_OFFER, + CRED_20_PROPOSAL, + CRED_20_REQUEST, +) +from ...handler import V20CredFormatError +from ..handler import AnonCredsCredFormatHandler +from ..handler import LOGGER as INDY_LOGGER + +TEST_DID = "LjgpST2rjsoxYegQDRm7EL" +SCHEMA_NAME = "bc-reg" +SCHEMA_TXN = 12 +SCHEMA_ID = f"{TEST_DID}:2:{SCHEMA_NAME}:1.0" +SCHEMA = { + "ver": "1.0", + "id": SCHEMA_ID, + "name": SCHEMA_NAME, + "version": "1.0", + "attrNames": ["legalName", "jurisdictionId", "incorporationDate"], + "seqNo": SCHEMA_TXN, +} +CRED_DEF_ID = f"{TEST_DID}:3:CL:12:tag1" +CRED_DEF = { + "ver": "1.0", + "id": CRED_DEF_ID, + "schemaId": SCHEMA_TXN, + "type": "CL", + "tag": "tag1", + "value": { + "primary": { + "n": "...", + "s": "...", + "r": { + "master_secret": "...", + "legalName": "...", + "jurisdictionId": "...", + "incorporationDate": "...", + }, + "rctxt": "...", + "z": "...", + }, + "revocation": { + "g": "1 ...", + "g_dash": "1 ...", + "h": "1 ...", + "h0": "1 ...", + "h1": "1 ...", + "h2": "1 ...", + "htilde": "1 ...", + "h_cap": "1 ...", + "u": "1 ...", + "pk": "1 ...", + "y": "1 ...", + }, + }, +} +REV_REG_DEF_TYPE = "CL_ACCUM" +REV_REG_ID = f"{TEST_DID}:4:{CRED_DEF_ID}:{REV_REG_DEF_TYPE}:tag1" +TAILS_DIR = "/tmp/indy/revocation/tails_files" +TAILS_HASH = "8UW1Sz5cqoUnK9hqQk7nvtKK65t7Chu3ui866J23sFyJ" +TAILS_LOCAL = f"{TAILS_DIR}/{TAILS_HASH}" +REV_REG_DEF = { + "ver": "1.0", + "id": REV_REG_ID, + "revocDefType": "CL_ACCUM", + "tag": "tag1", + "credDefId": CRED_DEF_ID, + "value": { + "issuanceType": "ISSUANCE_ON_DEMAND", + "maxCredNum": 5, + "publicKeys": {"accumKey": {"z": "1 ..."}}, + "tailsHash": TAILS_HASH, + "tailsLocation": TAILS_LOCAL, + }, +} +INDY_OFFER = { + "schema_id": SCHEMA_ID, + "cred_def_id": CRED_DEF_ID, + "key_correctness_proof": { + "c": "123467890", + "xz_cap": "12345678901234567890", + "xr_cap": [ + [ + "remainder", + "1234567890", + ], + [ + "number", + "12345678901234", + ], + [ + "master_secret", + "12345678901234", + ], + ], + }, + "nonce": "1234567890", +} +INDY_CRED_REQ = { + "prover_did": TEST_DID, + "cred_def_id": CRED_DEF_ID, + "blinded_ms": { + "u": "12345", + "ur": "1 123467890ABCDEF", + "hidden_attributes": ["master_secret"], + "committed_attributes": {}, + }, + "blinded_ms_correctness_proof": { + "c": "77777", + "v_dash_cap": "12345678901234567890", + "m_caps": {"master_secret": "271283714"}, + "r_caps": {}, + }, + "nonce": "9876543210", +} +INDY_CRED = { + "schema_id": SCHEMA_ID, + "cred_def_id": CRED_DEF_ID, + "rev_reg_id": REV_REG_ID, + "values": { + "legalName": { + "raw": "The Original House of Pies", + "encoded": "108156129846915621348916581250742315326283968964", + }, + "busId": {"raw": "11155555", "encoded": "11155555"}, + "jurisdictionId": {"raw": "1", "encoded": "1"}, + "incorporationDate": { + "raw": "2021-01-01", + "encoded": "121381685682968329568231", + }, + "pic": {"raw": "cG90YXRv", "encoded": "125362825623562385689562"}, + }, + "signature": { + "p_credential": { + "m_2": "13683295623862356", + "a": "1925723185621385238953", + "e": "253516862326", + "v": "26890295622385628356813632", + }, + "r_credential": { + "sigma": "1 00F81D", + "c": "158698926BD09866E", + "vr_prime_prime": "105682396DDF1A", + "witness_signature": {"sigma_i": "1 ...", "u_i": "1 ...", "g_i": "1 ..."}, + "g_i": "1 ...", + "i": 1, + "m2": "862186285926592362384FA97FF3A4AB", + }, + }, + "signature_correctness_proof": { + "se": "10582965928638296868123", + "c": "2816389562839651", + }, + "rev_reg": {"accum": "21 ..."}, + "witness": {"omega": "21 ..."}, +} + + +class TestV20AnonCredsCredFormatHandler(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.session = InMemoryProfile.test_session() + self.profile = self.session.profile + self.context = self.profile.context + setattr(self.profile, "session", mock.MagicMock(return_value=self.session)) + + # Ledger + Ledger = mock.MagicMock() + self.ledger = Ledger() + self.ledger.get_schema = mock.CoroutineMock(return_value=SCHEMA) + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value=CRED_DEF + ) + self.ledger.get_revoc_reg_def = mock.CoroutineMock(return_value=REV_REG_DEF) + self.ledger.__aenter__ = mock.CoroutineMock(return_value=self.ledger) + self.ledger.credential_definition_id2schema_id = mock.CoroutineMock( + return_value=SCHEMA_ID + ) + self.context.injector.bind_instance(BaseLedger, self.ledger) + self.context.injector.bind_instance( + IndyLedgerRequestsExecutor, + mock.MagicMock( + get_ledger_for_identifier=mock.CoroutineMock( + return_value=(None, self.ledger) + ) + ), + ) + # Context + self.cache = InMemoryCache() + self.context.injector.bind_instance(BaseCache, self.cache) + + # Issuer + self.issuer = async_mock.MagicMock(AnonCredsIssuer, autospec=True) + self.context.injector.bind_instance(AnonCredsIssuer, self.issuer) + + # Holder + self.holder = async_mock.MagicMock(AnonCredsHolder, autospec=True) + self.context.injector.bind_instance(AnonCredsHolder, self.holder) + + self.handler = AnonCredsCredFormatHandler(self.profile) + assert self.handler.profile + + async def test_validate_fields(self): + # Test correct data + self.handler.validate_fields(CRED_20_PROPOSAL, {"cred_def_id": CRED_DEF_ID}) + self.handler.validate_fields(CRED_20_OFFER, INDY_OFFER) + self.handler.validate_fields(CRED_20_REQUEST, INDY_CRED_REQ) + self.handler.validate_fields(CRED_20_ISSUE, INDY_CRED) + + # test incorrect proposal + with self.assertRaises(ValidationError): + self.handler.validate_fields( + CRED_20_PROPOSAL, {"some_random_key": "some_random_value"} + ) + + # test incorrect offer + with self.assertRaises(ValidationError): + offer = INDY_OFFER.copy() + offer.pop("nonce") + self.handler.validate_fields(CRED_20_OFFER, offer) + + # test incorrect request + with self.assertRaises(ValidationError): + req = INDY_CRED_REQ.copy() + req.pop("nonce") + self.handler.validate_fields(CRED_20_REQUEST, req) + + # test incorrect cred + with self.assertRaises(ValidationError): + cred = INDY_CRED.copy() + cred.pop("schema_id") + self.handler.validate_fields(CRED_20_ISSUE, cred) + + async def test_get_indy_detail_record(self): + cred_ex_id = "dummy" + details_indy = [ + V20CredExRecordIndy( + cred_ex_id=cred_ex_id, + rev_reg_id="rr-id", + cred_rev_id="0", + ), + V20CredExRecordIndy( + cred_ex_id=cred_ex_id, + rev_reg_id="rr-id", + cred_rev_id="1", + ), + ] + await details_indy[0].save(self.session) + await details_indy[1].save(self.session) # exercise logger warning on get() + + with mock.patch.object( + INDY_LOGGER, "warning", mock.MagicMock() + ) as mock_warning: + assert await self.handler.get_detail_record(cred_ex_id) in details_indy + mock_warning.assert_called_once() + + async def test_check_uniqueness(self): + with mock.patch.object( + self.handler.format.detail, + "query_by_cred_ex_id", + mock.CoroutineMock(), + ) as mock_indy_query: + mock_indy_query.return_value = [] + await self.handler._check_uniqueness("dummy-cx-id") + + with mock.patch.object( + self.handler.format.detail, + "query_by_cred_ex_id", + mock.CoroutineMock(), + ) as mock_indy_query: + mock_indy_query.return_value = [mock.MagicMock()] + with self.assertRaises(V20CredFormatError) as context: + await self.handler._check_uniqueness("dummy-cx-id") + assert "detail record already exists" in str(context.exception) + + async def test_create_proposal(self): + cred_ex_record = mock.MagicMock() + proposal_data = {"schema_id": SCHEMA_ID} + + (cred_format, attachment) = await self.handler.create_proposal( + cred_ex_record, proposal_data + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == proposal_data + + # assert data is encoded as base64 + assert attachment.data.base64 + + async def test_create_proposal_none(self): + cred_ex_record = mock.MagicMock() + proposal_data = None + + (cred_format, attachment) = await self.handler.create_proposal( + cred_ex_record, proposal_data + ) + + # assert content of attachment is proposal data + assert attachment.content == {} + + async def test_receive_proposal(self): + cred_ex_record = mock.MagicMock() + cred_proposal_message = mock.MagicMock() + + # Not much to assert. Receive proposal doesn't do anything + await self.handler.receive_proposal(cred_ex_record, cred_proposal_message) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_offer(self): + schema_id_parts = SCHEMA_ID.split(":") + + cred_preview = V20CredPreview( + attributes=( + V20CredAttrSpec(name="legalName", value="value"), + V20CredAttrSpec(name="jurisdictionId", value="value"), + V20CredAttrSpec(name="incorporationDate", value="value"), + ) + ) + + cred_proposal = V20CredProposal( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ + V20CredFormat.Format.INDY.api + ], + ) + ], + filters_attach=[ + AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0") + ], + ) + + cred_def_record = StorageRecord( + CRED_DEF_SENT_RECORD_TYPE, + CRED_DEF_ID, + { + "schema_id": SCHEMA_ID, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "issuer_did": TEST_DID, + "cred_def_id": CRED_DEF_ID, + "epoch": str(int(time())), + }, + ) + await self.session.storage.add_record(cred_def_record) + + self.issuer.create_credential_offer = mock.CoroutineMock( + return_value=json.dumps(INDY_OFFER) + ) + + (cred_format, attachment) = await self.handler.create_offer(cred_proposal) + + self.issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == INDY_OFFER + + # assert data is encoded as base64 + assert attachment.data.base64 + + self.issuer.create_credential_offer.reset_mock() + (cred_format, attachment) = await self.handler.create_offer(cred_proposal) + self.issuer.create_credential_offer.assert_not_called() + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_offer_no_cache(self): + schema_id_parts = SCHEMA_ID.split(":") + + cred_preview = V20CredPreview( + attributes=( + V20CredAttrSpec(name="legalName", value="value"), + V20CredAttrSpec(name="jurisdictionId", value="value"), + V20CredAttrSpec(name="incorporationDate", value="value"), + ) + ) + + cred_proposal = V20CredProposal( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ + V20CredFormat.Format.INDY.api + ], + ) + ], + filters_attach=[ + AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0") + ], + ) + + cred_def_record = StorageRecord( + CRED_DEF_SENT_RECORD_TYPE, + CRED_DEF_ID, + { + "schema_id": SCHEMA_ID, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "issuer_did": TEST_DID, + "cred_def_id": CRED_DEF_ID, + "epoch": str(int(time())), + }, + ) + + # Remove cache from injection context + self.context.injector.clear_binding(BaseCache) + + await self.session.storage.add_record(cred_def_record) + + self.issuer.create_credential_offer = mock.CoroutineMock( + return_value=json.dumps(INDY_OFFER) + ) + + (cred_format, attachment) = await self.handler.create_offer(cred_proposal) + + self.issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == INDY_OFFER + + # assert data is encoded as base64 + assert attachment.data.base64 + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_offer_attr_mismatch(self): + schema_id_parts = SCHEMA_ID.split(":") + + cred_preview = V20CredPreview( + attributes=( # names have spaces instead of camel case + V20CredAttrSpec(name="legal name", value="value"), + V20CredAttrSpec(name="jurisdiction id", value="value"), + V20CredAttrSpec(name="incorporation date", value="value"), + ) + ) + + cred_proposal = V20CredProposal( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ + V20CredFormat.Format.INDY.api + ], + ) + ], + filters_attach=[ + AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0") + ], + ) + self.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + + cred_def_record = StorageRecord( + CRED_DEF_SENT_RECORD_TYPE, + CRED_DEF_ID, + { + "schema_id": SCHEMA_ID, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "issuer_did": TEST_DID, + "cred_def_id": CRED_DEF_ID, + "epoch": str(int(time())), + }, + ) + await self.session.storage.add_record(cred_def_record) + + self.issuer.create_credential_offer = mock.CoroutineMock( + return_value=json.dumps(INDY_OFFER) + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=(None, self.ledger)), + ): + with self.assertRaises(V20CredFormatError): + await self.handler.create_offer(cred_proposal) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_offer_no_matching_sent_cred_def(self): + cred_proposal = V20CredProposal( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ + V20CredFormat.Format.INDY.api + ], + ) + ], + filters_attach=[AttachDecorator.data_base64({}, ident="0")], + ) + + self.issuer.create_credential_offer = mock.CoroutineMock( + return_value=json.dumps(INDY_OFFER) + ) + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.create_offer(cred_proposal) + assert "Issuer has no operable cred def" in str(context.exception) + + async def test_receive_offer(self): + cred_ex_record = mock.MagicMock() + cred_offer_message = mock.MagicMock() + + # Not much to assert. Receive offer doesn't do anything + await self.handler.receive_offer(cred_ex_record, cred_offer_message) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_request(self): + holder_did = "did" + + cred_offer = V20CredOffer( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-id", + state=V20CredExRecord.STATE_OFFER_RECEIVED, + cred_offer=cred_offer.serialize(), + ) + + cred_def = {"cred": "def"} + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value=cred_def + ) + + cred_req_meta = {} + self.holder.create_credential_request = mock.CoroutineMock( + return_value=(json.dumps(INDY_CRED_REQ), json.dumps(cred_req_meta)) + ) + + (cred_format, attachment) = await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) + + self.holder.create_credential_request.assert_called_once_with( + INDY_OFFER, cred_def, holder_did + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == INDY_CRED_REQ + + # assert data is encoded as base64 + assert attachment.data.base64 + + # cover case with cache (change ID to prevent already exists error) + cred_ex_record._id = "dummy-id2" + await self.handler.create_request(cred_ex_record, {"holder_did": holder_did}) + + # cover case with no cache in injection context + self.context.injector.clear_binding(BaseCache) + cred_ex_record._id = "dummy-id3" + self.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=(None, self.ledger)), + ): + await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) + + async def test_create_request_bad_state(self): + cred_ex_record = V20CredExRecord(state=V20CredExRecord.STATE_OFFER_SENT) + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.create_request(cred_ex_record) + assert ( + "Indy issue credential format cannot start from credential request" + in str(context.exception) + ) + + cred_ex_record.state = None + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.create_request(cred_ex_record) + assert ( + "Indy issue credential format cannot start from credential request" + in str(context.exception) + ) + + async def test_create_request_not_unique_x(self): + cred_ex_record = V20CredExRecord(state=V20CredExRecord.STATE_OFFER_RECEIVED) + + with mock.patch.object( + self.handler, "_check_uniqueness", mock.CoroutineMock() + ) as mock_unique: + mock_unique.side_effect = ( + V20CredFormatError("indy detail record already exists"), + ) + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.create_request(cred_ex_record) + + assert "indy detail record already exists" in str(context.exception) + + async def test_receive_request(self): + cred_ex_record = mock.MagicMock() + cred_request_message = mock.MagicMock() + + # Not much to assert. Receive request doesn't do anything + await self.handler.receive_request(cred_ex_record, cred_request_message) + + async def test_receive_request_no_offer(self): + cred_ex_record = mock.MagicMock(cred_offer=None) + cred_request_message = mock.MagicMock() + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.receive_request(cred_ex_record, cred_request_message) + + assert ( + "Indy issue credential format cannot start from credential request" + in str(context.exception) + ) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_issue_credential_revocable(self): + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + ) + + cred_rev_id = "1000" + self.issuer.create_credential = mock.CoroutineMock( + return_value=(json.dumps(INDY_CRED), cred_rev_id) + ) + + with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: + revoc.return_value.get_or_create_active_registry = mock.CoroutineMock( + return_value=( + mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + ), + mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(mock.CoroutineMock()), + max_creds=10, + ), + ) + ) + + (cred_format, attachment) = await self.handler.issue_credential( + cred_ex_record, retries=1 + ) + + self.issuer.create_credential.assert_called_once_with( + SCHEMA, + INDY_OFFER, + INDY_CRED_REQ, + attr_values, + REV_REG_ID, + "dummy-path", + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == INDY_CRED + + # assert data is encoded as base64 + assert attachment.data.base64 + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_issue_credential_non_revocable(self): + CRED_DEF_NR = deepcopy(CRED_DEF) + CRED_DEF_NR["value"]["revocation"] = None + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + ) + + self.issuer.create_credential = mock.CoroutineMock( + return_value=(json.dumps(INDY_CRED), None) + ) + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value=CRED_DEF_NR + ) + self.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ): + (cred_format, attachment) = await self.handler.issue_credential( + cred_ex_record, retries=0 + ) + + self.issuer.create_credential.assert_called_once_with( + SCHEMA, + INDY_OFFER, + INDY_CRED_REQ, + attr_values, + None, + None, + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == INDY_CRED + + # assert data is encoded as base64 + assert attachment.data.base64 + + async def test_issue_credential_not_unique_x(self): + cred_ex_record = V20CredExRecord(state=V20CredExRecord.STATE_REQUEST_RECEIVED) + + with mock.patch.object( + self.handler, "_check_uniqueness", mock.CoroutineMock() + ) as mock_unique: + mock_unique.side_effect = ( + V20CredFormatError("indy detail record already exists"), + ) + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.issue_credential(cred_ex_record) + + assert "indy detail record already exists" in str(context.exception) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_issue_credential_no_active_rr_no_retries(self): + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_rev_id = "1" + + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + ) + + self.issuer.create_credential = mock.CoroutineMock( + return_value=(json.dumps(INDY_CRED), cred_rev_id) + ) + + with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: + revoc.return_value.get_or_create_active_registry = mock.CoroutineMock( + return_value=() + ) + with self.assertRaises(V20CredFormatError) as context: + await self.handler.issue_credential(cred_ex_record, retries=0) + assert "has no active revocation registry" in str(context.exception) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_issue_credential_no_active_rr_retry(self): + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_rev_id = "1" + + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + ) + + self.issuer.create_credential = mock.CoroutineMock( + return_value=(json.dumps(INDY_CRED), cred_rev_id) + ) + + with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: + revoc.return_value.get_or_create_active_registry = mock.CoroutineMock( + side_effect=[ + None, + ( + mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=mock.CoroutineMock(), + ), + mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(mock.CoroutineMock()), + ), + ), + ] + ) + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.issue_credential(cred_ex_record, retries=1) + assert "has no active revocation registry" in str(context.exception) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_issue_credential_rr_full(self): + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_rev_id = "1" + + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + ) + + self.issuer.create_credential = async_mock.CoroutineMock( + side_effect=AnonCredsRevocationRegistryFullError("Nope") + ) + with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: + revoc.return_value.get_or_create_active_registry = mock.CoroutineMock( + return_value=( + mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=mock.CoroutineMock(), + ), + mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(mock.CoroutineMock()), + ), + ) + ) + + with self.assertRaises(V20CredFormatError) as context: + await self.handler.issue_credential(cred_ex_record, retries=1) + assert "has no active revocation registry" in str(context.exception) + + async def test_receive_credential(self): + cred_ex_record = mock.MagicMock() + cred_issue_message = mock.MagicMock() + + # Not much to assert. Receive credential doesn't do anything + await self.handler.receive_credential(cred_ex_record, cred_issue_message) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_store_credential(self): + connection_id = "test_conn_id" + attr_values = { + "legalName": ["value", None], + "jurisdictionId": ["value", None], + "incorporationDate": ["value", None], + "pic": ["cG90YXRv", "image/jpeg"], + } + cred_req_meta = {"req": "meta"} + thread_id = "thread-id" + + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v[0], mime_type=v[1]) + for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_offer.assign_thread_id(thread_id) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + cred_issue = V20CredIssue( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_ISSUE][ + V20CredFormat.Format.INDY.api + ], + ) + ], + credentials_attach=[AttachDecorator.data_base64(INDY_CRED, ident="0")], + ) + + stored_cx_rec = V20CredExRecord( + cred_ex_id="dummy-cxid", + connection_id=connection_id, + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + cred_issue=cred_issue.serialize(), + initiator=V20CredExRecord.INITIATOR_EXTERNAL, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_CREDENTIAL_RECEIVED, + thread_id=thread_id, + auto_remove=True, + ) + + cred_id = "cred-id" + + self.holder.store_credential = mock.CoroutineMock(return_value=cred_id) + stored_cred = {"stored": "cred"} + self.holder.get_credential = mock.CoroutineMock( + return_value=json.dumps(stored_cred) + ) + + with mock.patch.object( + test_module, "RevocationRegistry", autospec=True + ) as mock_rev_reg: + mock_rev_reg.from_definition = mock.MagicMock( + return_value=mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock() + ) + ) + with self.assertRaises(V20CredFormatError) as context: + await self.handler.store_credential(stored_cx_rec, cred_id=cred_id) + assert "No credential exchange " in str(context.exception) + self.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), mock.patch.object( + test_module, "RevocationRegistry", autospec=True + ) as mock_rev_reg, mock.patch.object( + test_module.AnonCredsCredFormatHandler, "get_detail_record", autospec=True + ) as mock_get_detail_record: + mock_rev_reg.from_definition = mock.MagicMock( + return_value=mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock() + ) + ) + mock_get_detail_record.return_value = mock.MagicMock( + cred_request_metadata=cred_req_meta, + save=mock.CoroutineMock(), + ) + + self.ledger.get_credential_definition.reset_mock() + await self.handler.store_credential(stored_cx_rec, cred_id=cred_id) + + self.ledger.get_credential_definition.assert_called_once_with(CRED_DEF_ID) + + self.holder.store_credential.assert_called_once_with( + CRED_DEF, + INDY_CRED, + cred_req_meta, + {"pic": "image/jpeg"}, + credential_id=cred_id, + rev_reg_def=REV_REG_DEF, + ) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_store_credential_holder_store_indy_error(self): + connection_id = "test_conn_id" + attr_values = { + "legalName": ["value", None], + "jurisdictionId": ["value", None], + "incorporationDate": ["value", None], + "pic": ["cG90YXRv", "image/jpeg"], + } + cred_req_meta = {"req": "meta"} + thread_id = "thread-id" + + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v[0], mime_type=v[1]) + for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_offer.assign_thread_id(thread_id) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + cred_issue = V20CredIssue( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_ISSUE][ + V20CredFormat.Format.INDY.api + ], + ) + ], + credentials_attach=[AttachDecorator.data_base64(INDY_CRED, ident="0")], + ) + + stored_cx_rec = V20CredExRecord( + cred_ex_id="dummy-cxid", + connection_id=connection_id, + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + cred_issue=cred_issue.serialize(), + initiator=V20CredExRecord.INITIATOR_EXTERNAL, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_CREDENTIAL_RECEIVED, + thread_id=thread_id, + auto_remove=True, + ) + + cred_id = "cred-id" + self.holder.store_credential = async_mock.CoroutineMock( + side_effect=test_module.AnonCredsHolderError("Problem", {"message": "Nope"}) + ) + + with mock.patch.object( + test_module.AnonCredsCredFormatHandler, "get_detail_record", autospec=True + ) as mock_get_detail_record, mock.patch.object( + test_module.RevocationRegistry, "from_definition", mock.MagicMock() + ) as mock_rev_reg: + mock_get_detail_record.return_value = mock.MagicMock( + cred_request_metadata=cred_req_meta, + save=mock.CoroutineMock(), + ) + mock_rev_reg.return_value = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock() + ) + with self.assertRaises(test_module.AnonCredsHolderError) as context: + await self.handler.store_credential(stored_cx_rec, cred_id) + assert "Nope" in str(context.exception) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py index d6329cfcb6..f18cab567f 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py @@ -8,6 +8,7 @@ import asyncio from ......cache.base import BaseCache +from ......core.profile import Profile from ......indy.issuer import IndyIssuer, IndyIssuerRevocationRegistryFullError from ......indy.holder import IndyHolder, IndyHolderError from ......indy.models.cred import IndyCredentialSchema @@ -46,6 +47,8 @@ from ...models.detail.indy import V20CredExRecordIndy from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler +from ..anoncreds.handler import AnonCredsCredFormatHandler + LOGGER = logging.getLogger(__name__) @@ -54,6 +57,16 @@ class IndyCredFormatHandler(V20CredFormatHandler): """Indy credential format handler.""" format = V20CredFormat.Format.INDY + anoncreds_handler = None + + def __init__(self, profile: Profile): + """Shim initialization to check for new AnonCreds library.""" + super().__init__(profile) + + # Temporary shim while the new anoncreds library integration is in progress + wallet_type = profile.settings.get_value("wallet.type") + if wallet_type == "askar-anoncreds": + self.anoncreds_handler = AnonCredsCredFormatHandler(profile) @classmethod def validate_fields(cls, message_type: str, attachment_data: Mapping): @@ -95,6 +108,10 @@ async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordIndy: session, cred_ex_id ) + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.get_detail_record(cred_ex_id) + if len(records) > 1: LOGGER.warning( "Cred ex id %s has %d %s detail records: should be 1", @@ -126,6 +143,10 @@ def get_format_identifier(self, message_type: str) -> str: str: Issue credential attachment format identifier """ + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return self.anoncreds_handler.get_format_identifier(message_type) + return ATTACHMENT_FORMAT[message_type][IndyCredFormatHandler.format.api] def get_format_data(self, message_type: str, data: dict) -> CredFormatAttachment: @@ -144,6 +165,10 @@ def get_format_data(self, message_type: str, data: dict) -> CredFormatAttachment CredFormatAttachment: Credential format and attachment data objects """ + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return self.anoncreds_handler.get_format_data(message_type, data) + return ( V20CredFormat( attach_id=IndyCredFormatHandler.format.api, @@ -170,6 +195,13 @@ async def create_proposal( self, cred_ex_record: V20CredExRecord, proposal_data: Mapping[str, str] ) -> Tuple[V20CredFormat, AttachDecorator]: """Create indy credential proposal.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.create_proposal( + cred_ex_record, + proposal_data, + ) + if proposal_data is None: proposal_data = {} @@ -188,6 +220,10 @@ async def create_offer( ) -> CredFormatAttachment: """Create indy credential offer.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.create_offer(cred_proposal_message) + issuer = self.profile.inject(IndyIssuer) ledger = self.profile.inject(BaseLedger) cache = self.profile.inject_or(BaseCache) @@ -246,6 +282,13 @@ async def create_request( self, cred_ex_record: V20CredExRecord, request_data: Mapping = None ) -> CredFormatAttachment: """Create indy credential request.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.create_request( + cred_ex_record, + request_data, + ) + if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: raise V20CredFormatError( "Indy issue credential format cannot start from credential request" @@ -314,6 +357,13 @@ async def receive_request( self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest ) -> None: """Receive indy credential request.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.receive_request( + cred_ex_record, + cred_request_message, + ) + if not cred_ex_record.cred_offer: raise V20CredFormatError( "Indy issue credential format cannot start from credential request" @@ -323,6 +373,12 @@ async def issue_credential( self, cred_ex_record: V20CredExRecord, retries: int = 5 ) -> CredFormatAttachment: """Issue indy credential.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.issue_credential( + cred_ex_record, retries + ) + await self._check_uniqueness(cred_ex_record.cred_ex_id) cred_offer = cred_ex_record.cred_offer.attachment(IndyCredFormatHandler.format) @@ -439,6 +495,12 @@ async def store_credential( self, cred_ex_record: V20CredExRecord, cred_id: str = None ) -> None: """Store indy credential.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.store_credential( + cred_ex_record, cred_id + ) + cred = cred_ex_record.cred_issue.attachment(IndyCredFormatHandler.format) rev_reg_def = None diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py index f3c5704252..0b91be6151 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_issue_handler.py @@ -1,6 +1,7 @@ """Credential issue message handler.""" from .....core.oob_processor import OobMessageProcessor +from .....anoncreds.holder import AnonCredsHolderError from .....indy.holder import IndyHolderError from .....messaging.base_handler import BaseHandler, HandlerException from .....messaging.models.base import BaseModelError @@ -70,6 +71,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): cred_ex_record = await cred_manager.store_credential(cred_ex_record) except ( BaseModelError, + AnonCredsHolderError, IndyHolderError, StorageError, V20CredManagerError, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py index 3184b79b55..3d1432e5bc 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_offer_handler.py @@ -2,6 +2,7 @@ from .....wallet.util import default_did_from_verkey from .....core.oob_processor import OobMessageProcessor +from .....anoncreds.holder import AnonCredsHolderError from .....indy.holder import IndyHolderError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -88,6 +89,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send_reply(cred_request_message) except ( BaseModelError, + AnonCredsHolderError, IndyHolderError, LedgerError, StorageError, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py index 4ff09f70aa..d6762223e6 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_proposal_handler.py @@ -1,5 +1,6 @@ """Credential proposal message handler.""" +from .....anoncreds.issuer import AnonCredsIssuerError from .....indy.issuer import IndyIssuerError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -68,6 +69,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send_reply(cred_offer_message) except ( BaseModelError, + AnonCredsIssuerError, IndyIssuerError, LedgerError, StorageError, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py index b48e4ba0b9..06a45fb8c6 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/cred_request_handler.py @@ -1,6 +1,7 @@ """Credential request message handler.""" from .....core.oob_processor import OobMessageProcessor +from .....anoncreds.issuer import AnonCredsIssuerError from .....indy.issuer import IndyIssuerError from .....ledger.error import LedgerError from .....messaging.base_handler import BaseHandler, HandlerException @@ -91,6 +92,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send_reply(cred_issue_message) except ( BaseModelError, + AnonCredsIssuerError, IndyIssuerError, LedgerError, StorageError, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py index 6bf6d4db8b..b82bc846f7 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_issue_handler.py @@ -80,7 +80,7 @@ async def test_called_auto_store(self): ) assert mock_cred_mgr.return_value.send_cred_ack.call_count == 1 - async def test_called_auto_store_x(self): + async def test_called_auto_store_x_indy(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_store_credential"] = True @@ -118,6 +118,44 @@ async def test_called_auto_store_x(self): await handler_inst.handle(request_context, responder) # storage error assert mock_cred_mgr.return_value.send_cred_ack.call_count == 2 + async def test_called_auto_store_x_anoncreds(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.settings["debug.auto_store_credential"] = True + request_context.connection_record = mock.MagicMock() + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr: + mock_cred_mgr.return_value = mock.MagicMock( + receive_credential=mock.CoroutineMock( + return_value=mock.MagicMock(save_error_state=mock.CoroutineMock()) + ), + store_credential=mock.CoroutineMock( + side_effect=[ + test_module.AnonCredsHolderError, + test_module.StorageError(), + ] + ), + send_cred_ack=mock.CoroutineMock(), + ) + + request_context.message = V20CredIssue() + request_context.connection_ready = True + handler_inst = test_module.V20CredIssueHandler() + responder = MockResponder() + + await handler_inst.handle(request_context, responder) # holder error + await handler_inst.handle(request_context, responder) # storage error + assert mock_cred_mgr.return_value.send_cred_ack.call_count == 2 + async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py index 5b9368285f..6fa2d03b3e 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_offer_handler.py @@ -1,5 +1,6 @@ from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase +from asynctest import mock as async_mock from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext @@ -84,7 +85,7 @@ async def test_called_auto_request(self): assert result == "cred_request_message" assert target == {} - async def test_called_auto_request_x(self): + async def test_called_auto_request_x_indy(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() request_context.settings["debug.auto_respond_credential_offer"] = True @@ -121,6 +122,43 @@ async def test_called_auto_request_x(self): await handler.handle(request_context, responder) mock_log_exc.assert_called_once() + async def test_called_auto_request_x_anoncreds(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.settings["debug.auto_respond_credential_offer"] = True + request_context.connection_record = mock.MagicMock() + request_context.connection_record.my_did = "dummy" + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr: + mock_cred_mgr.return_value.receive_offer = mock.CoroutineMock( + return_value=mock.MagicMock(save_error_state=mock.CoroutineMock()) + ) + mock_cred_mgr.return_value.create_request = async_mock.CoroutineMock( + side_effect=test_module.AnonCredsHolderError() + ) + + request_context.message = V20CredOffer() + request_context.connection_ready = True + handler = test_module.V20CredOfferHandler() + responder = MockResponder() + + with mock.patch.object( + responder, "send_reply", mock.CoroutineMock() + ) as mock_send_reply, mock.patch.object( + handler._logger, "exception", mock.MagicMock() + ) as mock_log_exc: + await handler.handle(request_context, responder) + mock_log_exc.assert_called_once() + async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py index e49d5e8a4c..28c8c5a4e9 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_proposal_handler.py @@ -1,5 +1,6 @@ from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase +from asynctest import mock as async_mock from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder @@ -66,7 +67,7 @@ async def test_called_auto_offer(self): assert result == "cred_offer_message" assert target == {} - async def test_called_auto_offer_x(self): + async def test_called_auto_offer_x_indy(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() request_context.connection_record = mock.MagicMock() @@ -95,6 +96,35 @@ async def test_called_auto_offer_x(self): await handler.handle(request_context, responder) mock_log_exc.assert_called_once() + async def test_called_auto_offer_x_anoncreds(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.connection_record = mock.MagicMock() + + with mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr: + mock_cred_mgr.return_value.receive_proposal = mock.CoroutineMock( + return_value=mock.MagicMock(save_error_state=mock.CoroutineMock()) + ) + mock_cred_mgr.return_value.receive_proposal.return_value.auto_offer = True + mock_cred_mgr.return_value.create_offer = async_mock.CoroutineMock( + side_effect=test_module.AnonCredsIssuerError() + ) + + request_context.message = V20CredProposal() + request_context.connection_ready = True + handler = test_module.V20CredProposalHandler() + responder = MockResponder() + + with mock.patch.object( + responder, "send_reply", mock.CoroutineMock() + ) as mock_send_reply, mock.patch.object( + handler._logger, "exception", mock.MagicMock() + ) as mock_log_exc: + await handler.handle(request_context, responder) + mock_log_exc.assert_called_once() + async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py index 3c1d654244..0f4ead20b7 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/handlers/tests/test_cred_request_handler.py @@ -1,5 +1,6 @@ from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase +from asynctest import mock as async_mock from ......core.oob_processor import OobMessageProcessor from ......messaging.request_context import RequestContext @@ -91,7 +92,7 @@ async def test_called_auto_issue(self): assert result == "cred_issue_message" assert target == {} - async def test_called_auto_issue_x(self): + async def test_called_auto_issue_x_indy(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() request_context.connection_record = mock.MagicMock() @@ -132,6 +133,47 @@ async def test_called_auto_issue_x(self): await handler.handle(request_context, responder) mock_log_exc.assert_called_once() + async def test_called_auto_issue_x_anoncreds(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.connection_record = mock.MagicMock() + + cred_ex_rec = V20CredExRecord() + + oob_record = mock.MagicMock() + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=oob_record + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr, mock.patch.object( + cred_ex_rec, "save_error_state", mock.CoroutineMock() + ): + mock_cred_mgr.return_value.receive_request = mock.CoroutineMock( + return_value=cred_ex_rec + ) + mock_cred_mgr.return_value.receive_request.return_value.auto_issue = True + mock_cred_mgr.return_value.issue_credential = async_mock.CoroutineMock( + side_effect=test_module.AnonCredsIssuerError() + ) + + request_context.message = V20CredRequest() + request_context.connection_ready = True + handler = test_module.V20CredRequestHandler() + responder = MockResponder() + + with mock.patch.object( + responder, "send_reply", mock.CoroutineMock() + ) as mock_send_reply, mock.patch.object( + handler._logger, "exception", mock.MagicMock() + ) as mock_log_exc: + await handler.handle(request_context, responder) + mock_log_exc.assert_called_once() + async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py index 3e5771c3fd..374a188a3a 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py @@ -39,6 +39,19 @@ class Format(Enum): ".formats.indy.handler.IndyCredFormatHandler" ), ) + """ + Once we switch to anoncreds this will replace the above INDY definition. + In the meantime there are some hardcoded references in the + "...formats.indy.handler.IndyCredFormatHandler" class. + INDY = FormatSpec( + "hlindy/", + V20CredExRecordIndy, + DeferLoad( + "aries_cloudagent.protocols.issue_credential.v2_0" + ".formats.anoncreds.handler.AnonCredsCredFormatHandler" + ), + ) + """ LD_PROOF = FormatSpec( "aries/", V20CredExRecordLDProof, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py index d1f0995624..74877fc18b 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py @@ -18,6 +18,8 @@ from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....core.profile import Profile +from ....anoncreds.holder import AnonCredsHolderError +from ....anoncreds.issuer import AnonCredsIssuerError from ....indy.holder import IndyHolderError from ....indy.issuer import IndyIssuerError from ....ledger.error import LedgerError @@ -1027,6 +1029,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): except ( BaseModelError, + AnonCredsIssuerError, IndyIssuerError, LedgerError, StorageNotFoundError, @@ -1128,6 +1131,7 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): except ( BaseModelError, + AnonCredsIssuerError, IndyIssuerError, LedgerError, StorageError, @@ -1233,6 +1237,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest): except ( BaseModelError, + AnonCredsHolderError, IndyHolderError, LedgerError, StorageError, @@ -1343,6 +1348,7 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): except ( BaseModelError, + AnonCredsHolderError, IndyHolderError, LedgerError, StorageError, @@ -1436,6 +1442,7 @@ async def credential_exchange_issue(request: web.BaseRequest): except ( BaseModelError, + AnonCredsIssuerError, IndyIssuerError, LedgerError, StorageError, @@ -1524,6 +1531,7 @@ async def credential_exchange_store(request: web.BaseRequest): cred_ex_record = await cred_manager.store_credential(cred_ex_record, cred_id) except ( + AnonCredsHolderError, IndyHolderError, StorageError, V20CredManagerError, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py index ba3c0c38fb..f8cde14b03 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_manager.py @@ -7,6 +7,7 @@ from .....cache.base import BaseCache from .....cache.in_memory import InMemoryCache from .....core.in_memory import InMemoryProfile +from .....anoncreds.issuer import AnonCredsIssuer from .....indy.issuer import IndyIssuer from .....messaging.decorators.thread_decorator import ThreadDecorator from .....messaging.decorators.attach_decorator import AttachDecorator @@ -897,7 +898,7 @@ async def test_receive_request_no_cred_ex_with_offer_found(self): cx_rec, cred_request ) - async def test_issue_credential(self): + async def test_issue_credential_indy(self): connection_id = "test_conn_id" thread_id = "thread-id" comment = "comment" @@ -1006,6 +1007,115 @@ async def test_issue_credential(self): assert ret_cx_rec.state == V20CredExRecord.STATE_ISSUED assert ret_cred_issue._thread_id == thread_id + async def test_issue_credential_anoncreds(self): + connection_id = "test_conn_id" + thread_id = "thread-id" + comment = "comment" + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_proposal = V20CredProposal( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ + V20CredFormat.Format.INDY.api + ], + ) + ], + filters_attach=[ + AttachDecorator.data_base64( + { + "schema_id": SCHEMA_ID, + "cred_def_id": CRED_DEF_ID, + }, + ident="0", + ) + ], + ) + cred_offer = V20CredOffer( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.INDY.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], + ) + cred_offer.assign_thread_id(thread_id) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.INDY.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], + ) + + stored_cx_rec = V20CredExRecord( + cred_ex_id="dummy-cxid", + connection_id=connection_id, + cred_proposal=cred_proposal, + cred_offer=cred_offer, + cred_request=cred_request, + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + thread_id=thread_id, + ) + + issuer = mock.MagicMock() + cred_rev_id = "1000" + issuer.create_credential = mock.CoroutineMock( + return_value=(json.dumps(INDY_CRED), cred_rev_id) + ) + self.context.injector.bind_instance(AnonCredsIssuer, issuer) + + with mock.patch.object( + V20CredExRecord, "save", autospec=True + ) as mock_save, mock.patch.object( + V20CredFormat.Format, "handler" + ) as mock_handler: + mock_handler.return_value.issue_credential = mock.CoroutineMock( + return_value=( + V20CredFormat( + attach_id=V20CredFormat.Format.INDY.api, + format_=ATTACHMENT_FORMAT[CRED_20_ISSUE][ + V20CredFormat.Format.INDY.api + ], + ), + AttachDecorator.data_base64( + INDY_CRED, ident=V20CredFormat.Format.INDY.api + ), + ) + ) + (ret_cx_rec, ret_cred_issue) = await self.manager.issue_credential( + stored_cx_rec, comment=comment + ) + + mock_save.assert_called_once() + mock_handler.return_value.issue_credential.assert_called_once_with( + ret_cx_rec + ) + + assert ret_cx_rec.cred_issue.attachment() == INDY_CRED + assert ret_cred_issue.attachment() == INDY_CRED + assert ret_cx_rec.state == V20CredExRecord.STATE_ISSUED + assert ret_cred_issue._thread_id == thread_id + async def test_issue_credential_x_no_formats(self): comment = "comment" diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py index 9d7d8dab9c..6e9adbf17f 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py @@ -1,6 +1,7 @@ from .....vc.ld_proofs.error import LinkedDataProofException from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase +from asynctest import mock as async_mock from .....admin.request_context import AdminRequestContext @@ -1296,7 +1297,7 @@ async def test_credential_exchange_issue_not_ready(self): with self.assertRaises(test_module.web.HTTPForbidden): await test_module.credential_exchange_issue(self.request) - async def test_credential_exchange_issue_rev_reg_full(self): + async def test_credential_exchange_issue_rev_reg_full_indy(self): self.request.json = mock.CoroutineMock() self.request.match_info = {"cred_ex_id": "dummy"} @@ -1328,6 +1329,38 @@ async def test_credential_exchange_issue_rev_reg_full(self): with self.assertRaises(test_module.web.HTTPBadRequest) as context: await test_module.credential_exchange_issue(self.request) + async def test_credential_exchange_issue_rev_reg_full_anoncreds(self): + self.request.json = mock.CoroutineMock() + self.request.match_info = {"cred_ex_id": "dummy"} + + mock_cx_rec = mock.MagicMock( + conn_id="dummy", + serialize=mock.MagicMock(), + save_error_state=mock.CoroutineMock(), + ) + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec, mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr, mock.patch.object( + test_module, "V20CredExRecord", autospec=True + ) as mock_cx_rec_cls: + mock_cx_rec.state = mock_cx_rec_cls.STATE_REQUEST_RECEIVED + mock_cx_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_cx_rec + ) + + mock_conn_rec.retrieve_by_id = mock.CoroutineMock() + mock_conn_rec.retrieve_by_id.return_value.is_ready = True + + mock_issue_cred = async_mock.CoroutineMock( + side_effect=test_module.AnonCredsIssuerError() + ) + mock_cred_mgr.return_value.issue_credential = mock_issue_cred + + with self.assertRaises(test_module.web.HTTPBadRequest) as context: + await test_module.credential_exchange_issue(self.request) + async def test_credential_exchange_issue_deser_x(self): self.request.json = mock.CoroutineMock() self.request.match_info = {"cred_ex_id": "dummy"} diff --git a/aries_cloudagent/protocols/present_proof/anoncreds/__init__.py b/aries_cloudagent/protocols/present_proof/anoncreds/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/present_proof/anoncreds/pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/anoncreds/pres_exch_handler.py new file mode 100644 index 0000000000..416c13a2a9 --- /dev/null +++ b/aries_cloudagent/protocols/present_proof/anoncreds/pres_exch_handler.py @@ -0,0 +1,264 @@ +"""Utilities for dif presentation exchange attachment.""" +import json +import logging +import time +from typing import Dict, Tuple, Union + +from ....anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ....anoncreds.models.anoncreds_cred_def import CredDef +from ....anoncreds.models.anoncreds_revocation import RevRegDef +from ....anoncreds.models.anoncreds_schema import AnonCredsSchema +from ....indy.models.xform import indy_proof_req2non_revoc_intervals +from ....anoncreds.registry import AnonCredsRegistry +from ....anoncreds.revocation import AnonCredsRevocation +from ....core.error import BaseError +from ....core.profile import Profile +from ..v1_0.models.presentation_exchange import V10PresentationExchange +from ..v2_0.messages.pres_format import V20PresFormat +from ..v2_0.models.pres_exchange import V20PresExRecord + +LOGGER = logging.getLogger(__name__) + + +class AnonCredsPresExchHandlerError(BaseError): + """Base class for Indy Presentation Exchange related errors.""" + + +class AnonCredsPresExchHandler: + """Base Presentation Exchange Handler.""" + + def __init__( + self, + profile: Profile, + ): + """Initialize PresExchange Handler.""" + super().__init__() + self._profile = profile + self.holder = AnonCredsHolder(profile) + + def _extract_proof_request(self, pres_ex_record): + if isinstance(pres_ex_record, V20PresExRecord): + return pres_ex_record.pres_request.attachment(V20PresFormat.Format.INDY) + elif isinstance(pres_ex_record, V10PresentationExchange): + return pres_ex_record._presentation_request.ser + + raise TypeError( + "pres_ex_record must be V10PresentationExchange or V20PresExRecord" + ) + + def _get_requested_referents( + self, + proof_request: dict, + requested_credentials: dict, + non_revoc_intervals: dict, + ) -> dict: + """Get requested referents for a proof request and requested credentials. + + Returns a dictionary that looks like: + { + "referent-0": {"cred_id": "0", "non_revoked": {"from": ..., "to": ...}}, + "referent-1": {"cred_id": "1", "non_revoked": {"from": ..., "to": ...}} + } + """ + + requested_referents = {} + attr_creds = requested_credentials.get("requested_attributes", {}) + req_attrs = proof_request.get("requested_attributes", {}) + for reft in attr_creds: + requested_referents[reft] = {"cred_id": attr_creds[reft]["cred_id"]} + if reft in req_attrs and reft in non_revoc_intervals: + requested_referents[reft]["non_revoked"] = non_revoc_intervals[reft] + + pred_creds = requested_credentials.get("requested_predicates", {}) + req_preds = proof_request.get("requested_predicates", {}) + for reft in pred_creds: + requested_referents[reft] = {"cred_id": pred_creds[reft]["cred_id"]} + if reft in req_preds and reft in non_revoc_intervals: + requested_referents[reft]["non_revoked"] = non_revoc_intervals[reft] + return requested_referents + + async def _get_credentials(self, requested_referents: dict): + """Extract mapping of presentation referents to credential ids.""" + credentials = {} + for reft in requested_referents: + credential_id = requested_referents[reft]["cred_id"] + if credential_id not in credentials: + credentials[credential_id] = json.loads( + await self.holder.get_credential(credential_id) + ) + return credentials + + def _remove_superfluous_timestamps(self, requested_credentials, credentials): + """Remove any timestamps that cannot correspond to non-revoc intervals.""" + for r in ("requested_attributes", "requested_predicates"): + for reft, req_item in requested_credentials.get(r, {}).items(): + if not credentials[req_item["cred_id"]].get( + "rev_reg_id" + ) and req_item.pop("timestamp", None): + LOGGER.info( + f"Removed superfluous timestamp from requested_credentials {r} " + f"{reft} for non-revocable credential {req_item['cred_id']}" + ) + + async def _get_ledger_objects( + self, credentials: dict + ) -> Tuple[Dict[str, AnonCredsSchema], Dict[str, CredDef], Dict[str, RevRegDef]]: + """Get all schemas, credential definitions, and revocation registries in use.""" + schemas = {} + cred_defs = {} + revocation_registries = {} + + for credential in credentials.values(): + schema_id = credential["schema_id"] + anoncreds_registry = self._profile.inject(AnonCredsRegistry) + if schema_id not in schemas: + schemas[schema_id] = ( + await anoncreds_registry.get_schema(self._profile, schema_id) + ).schema + cred_def_id = credential["cred_def_id"] + if cred_def_id not in cred_defs: + cred_defs[cred_def_id] = ( + await anoncreds_registry.get_credential_definition( + self._profile, cred_def_id + ) + ).credential_definition + if credential.get("rev_reg_id"): + revocation_registry_id = credential["rev_reg_id"] + if revocation_registry_id not in revocation_registries: + rev_reg = ( + await anoncreds_registry.get_revocation_registry_definition( + self._profile, revocation_registry_id + ) + ).revocation_registry + revocation_registries[revocation_registry_id] = rev_reg + + return schemas, cred_defs, revocation_registries + + async def _get_revocation_lists(self, requested_referents: dict, credentials: dict): + """Get revocation lists. + + Get revocation lists with non-revocation interval defined in + "non_revoked" of the presentation request or attributes + """ + epoch_now = int(time.time()) + rev_lists = {} + for precis in requested_referents.values(): # cred_id, non-revoc interval + credential_id = precis["cred_id"] + if not credentials[credential_id].get("rev_reg_id"): + continue + if "timestamp" in precis: + continue + rev_reg_id = credentials[credential_id]["rev_reg_id"] + + anoncreds_registry = self._profile.inject(AnonCredsRegistry) + reft_non_revoc_interval = precis.get("non_revoked") + if reft_non_revoc_interval: + key = ( + f"{rev_reg_id}_" + f"{reft_non_revoc_interval.get('from', 0)}_" + f"{reft_non_revoc_interval.get('to', epoch_now)}" + ) + if key not in rev_lists: + result = await anoncreds_registry.get_revocation_list( + self._profile, + rev_reg_id, + reft_non_revoc_interval.get("to", epoch_now), + ) + + rev_lists[key] = ( + rev_reg_id, + credential_id, + result.revocation_list.serialize(), + result.revocation_list.timestamp, + ) + for stamp_me in requested_referents.values(): + # often one cred satisfies many requested attrs/preds + if stamp_me["cred_id"] == credential_id: + stamp_me["timestamp"] = rev_lists[key][3] + + return rev_lists + + async def _get_revocation_states( + self, revocation_registries: dict, credentials: dict, rev_lists: dict + ): + """Get revocation states to prove non-revoked.""" + revocation_states = {} + for ( + rev_reg_id, + credential_id, + rev_list, + timestamp, + ) in rev_lists.values(): + if rev_reg_id not in revocation_states: + revocation_states[rev_reg_id] = {} + rev_reg_def = revocation_registries[rev_reg_id] + revocation = AnonCredsRevocation(self._profile) + tails_local_path = await revocation.get_or_fetch_local_tails_path( + rev_reg_def + ) + try: + revocation_states[rev_reg_id][timestamp] = json.loads( + await self.holder.create_revocation_state( + credentials[credential_id]["cred_rev_id"], + rev_reg_def.serialize(), + rev_list, + tails_local_path, + ) + ) + except AnonCredsHolderError as e: + LOGGER.error( + f"Failed to create revocation state: {e.error_code}, {e.message}" + ) + raise e + return revocation_states + + def _set_timestamps(self, requested_credentials: dict, requested_referents: dict): + for referent, precis in requested_referents.items(): + if "timestamp" not in precis: + continue + if referent in requested_credentials["requested_attributes"]: + requested_credentials["requested_attributes"][referent][ + "timestamp" + ] = precis["timestamp"] + if referent in requested_credentials["requested_predicates"]: + requested_credentials["requested_predicates"][referent][ + "timestamp" + ] = precis["timestamp"] + + async def return_presentation( + self, + pres_ex_record: Union[V10PresentationExchange, V20PresExRecord], + requested_credentials: dict = {}, + ) -> dict: + """Return Indy proof request as dict.""" + proof_request = self._extract_proof_request(pres_ex_record) + non_revoc_intervals = indy_proof_req2non_revoc_intervals(proof_request) + + requested_referents = self._get_requested_referents( + proof_request, requested_credentials, non_revoc_intervals + ) + + credentials = await self._get_credentials(requested_referents) + self._remove_superfluous_timestamps(requested_credentials, credentials) + + schemas, cred_defs, revocation_registries = await self._get_ledger_objects( + credentials + ) + + rev_lists = await self._get_revocation_lists(requested_referents, credentials) + + revocation_states = await self._get_revocation_states( + revocation_registries, credentials, rev_lists + ) + + self._set_timestamps(requested_credentials, requested_referents) + + indy_proof_json = await self.holder.create_presentation( + proof_request, + requested_credentials, + schemas, + cred_defs, + revocation_states, + ) + indy_proof = json.loads(indy_proof_json) + return indy_proof diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/anoncreds/__init__.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/anoncreds/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/anoncreds/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/anoncreds/handler.py new file mode 100644 index 0000000000..43ee2cdc6f --- /dev/null +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/anoncreds/handler.py @@ -0,0 +1,346 @@ +"""V2.0 present-proof indy presentation-exchange format handler.""" + +import json +import logging + +from marshmallow import RAISE +from typing import Mapping, Tuple + +from ......anoncreds.holder import AnonCredsHolder +from ......indy.models.predicate import Predicate +from ......indy.models.proof import IndyProofSchema +from ......indy.models.proof_request import IndyProofRequestSchema +from ......indy.models.xform import indy_proof_req_preview2indy_requested_creds +from ......anoncreds.util import generate_pr_nonce +from ......anoncreds.verifier import AnonCredsVerifier +from ......messaging.decorators.attach_decorator import AttachDecorator +from ......messaging.util import canon + +from ....anoncreds.pres_exch_handler import AnonCredsPresExchHandler + +from ...message_types import ( + ATTACHMENT_FORMAT, + PRES_20_REQUEST, + PRES_20, + PRES_20_PROPOSAL, +) +from ...messages.pres import V20Pres +from ...messages.pres_format import V20PresFormat +from ...models.pres_exchange import V20PresExRecord + +from ..handler import V20PresFormatHandler, V20PresFormatHandlerError + +LOGGER = logging.getLogger(__name__) + + +class AnonCredsPresExchangeHandler(V20PresFormatHandler): + """Anoncreds presentation format handler.""" + + format = V20PresFormat.Format.INDY + + @classmethod + def validate_fields(cls, message_type: str, attachment_data: Mapping): + """Validate attachment data for a specific message type. + + Uses marshmallow schemas to validate if format specific attachment data + is valid for the specified message type. Only does structural and type + checks, does not validate if .e.g. the issuer value is valid. + + + Args: + message_type (str): The message type to validate the attachment data for. + Should be one of the message types as defined in message_types.py + attachment_data (Mapping): [description] + The attachment data to valide + + Raises: + Exception: When the data is not valid. + + """ + mapping = { + PRES_20_REQUEST: IndyProofRequestSchema, + PRES_20_PROPOSAL: IndyProofRequestSchema, + PRES_20: IndyProofSchema, + } + + # Get schema class + Schema = mapping[message_type] + + # Validate, throw if not valid + Schema(unknown=RAISE).load(attachment_data) + + def get_format_identifier(self, message_type: str) -> str: + """Get attachment format identifier for format and message combination. + + Args: + message_type (str): Message type for which to return the format identifier + + Returns: + str: Issue credential attachment format identifier + + """ + return ATTACHMENT_FORMAT[message_type][AnonCredsPresExchangeHandler.format.api] + + def get_format_data( + self, message_type: str, data: dict + ) -> Tuple[V20PresFormat, AttachDecorator]: + """Get presentation format and attach objects for use in pres_ex messages.""" + return ( + V20PresFormat( + attach_id=AnonCredsPresExchangeHandler.format.api, + format_=self.get_format_identifier(message_type), + ), + AttachDecorator.data_base64( + data, + ident=AnonCredsPresExchangeHandler.format.api, + ), + ) + + async def create_bound_request( + self, + pres_ex_record: V20PresExRecord, + request_data: dict = None, + ) -> Tuple[V20PresFormat, AttachDecorator]: + """Create a presentation request bound to a proposal. + + Args: + pres_ex_record: Presentation exchange record for which + to create presentation request + request_data: Dict + + Returns: + A tuple (updated presentation exchange record, presentation request message) + + """ + indy_proof_request = pres_ex_record.pres_proposal.attachment( + AnonCredsPresExchangeHandler.format + ) + if request_data: + indy_proof_request["name"] = request_data.get("name", "proof-request") + indy_proof_request["version"] = request_data.get("version", "1.0") + indy_proof_request["nonce"] = ( + request_data.get("nonce") or await generate_pr_nonce() + ) + else: + indy_proof_request["name"] = "proof-request" + indy_proof_request["version"] = "1.0" + indy_proof_request["nonce"] = await generate_pr_nonce() + return self.get_format_data(PRES_20_REQUEST, indy_proof_request) + + async def create_pres( + self, + pres_ex_record: V20PresExRecord, + request_data: dict = None, + ) -> Tuple[V20PresFormat, AttachDecorator]: + """Create a presentation.""" + requested_credentials = {} + if not request_data: + try: + proof_request = pres_ex_record.pres_request + indy_proof_request = proof_request.attachment( + AnonCredsPresExchangeHandler.format + ) + requested_credentials = ( + await indy_proof_req_preview2indy_requested_creds( + indy_proof_request, + preview=None, + holder=AnonCredsHolder(self._profile), + ) + ) + except ValueError as err: + LOGGER.warning(f"{err}") + raise V20PresFormatHandlerError( + f"No matching Indy credentials found: {err}" + ) + else: + if AnonCredsPresExchangeHandler.format.api in request_data: + indy_spec = request_data.get(AnonCredsPresExchangeHandler.format.api) + requested_credentials = { + "self_attested_attributes": indy_spec["self_attested_attributes"], + "requested_attributes": indy_spec["requested_attributes"], + "requested_predicates": indy_spec["requested_predicates"], + } + indy_handler = AnonCredsPresExchHandler(self._profile) + indy_proof = await indy_handler.return_presentation( + pres_ex_record=pres_ex_record, + requested_credentials=requested_credentials, + ) + return self.get_format_data(PRES_20, indy_proof) + + async def receive_pres(self, message: V20Pres, pres_ex_record: V20PresExRecord): + """Receive a presentation and check for presented values vs. proposal request.""" + + def _check_proof_vs_proposal(): + """Check for bait and switch in presented values vs. proposal request.""" + proof_req = pres_ex_record.pres_request.attachment( + AnonCredsPresExchangeHandler.format + ) + + # revealed attrs + for reft, attr_spec in proof["requested_proof"]["revealed_attrs"].items(): + proof_req_attr_spec = proof_req["requested_attributes"].get(reft) + if not proof_req_attr_spec: + raise V20PresFormatHandlerError( + f"Presentation referent {reft} not in proposal request" + ) + req_restrictions = proof_req_attr_spec.get("restrictions", {}) + + name = proof_req_attr_spec["name"] + proof_value = attr_spec["raw"] + sub_proof_index = attr_spec["sub_proof_index"] + schema_id = proof["identifiers"][sub_proof_index]["schema_id"] + cred_def_id = proof["identifiers"][sub_proof_index]["cred_def_id"] + criteria = { + "schema_id": schema_id, + "schema_issuer_did": schema_id.split(":")[-4], + "schema_name": schema_id.split(":")[-2], + "schema_version": schema_id.split(":")[-1], + "cred_def_id": cred_def_id, + "issuer_did": cred_def_id.split(":")[-5], + f"attr::{name}::value": proof_value, + } + + if ( + not any(r.items() <= criteria.items() for r in req_restrictions) + and len(req_restrictions) != 0 + ): + raise V20PresFormatHandlerError( + f"Presented attribute {reft} does not satisfy proof request " + f"restrictions {req_restrictions}" + ) + + # revealed attr groups + for reft, attr_spec in ( + proof["requested_proof"].get("revealed_attr_groups", {}).items() + ): + proof_req_attr_spec = proof_req["requested_attributes"].get(reft) + if not proof_req_attr_spec: + raise V20PresFormatHandlerError( + f"Presentation referent {reft} not in proposal request" + ) + req_restrictions = proof_req_attr_spec.get("restrictions", {}) + proof_values = { + name: values["raw"] for name, values in attr_spec["values"].items() + } + sub_proof_index = attr_spec["sub_proof_index"] + schema_id = proof["identifiers"][sub_proof_index]["schema_id"] + cred_def_id = proof["identifiers"][sub_proof_index]["cred_def_id"] + criteria = { + "schema_id": schema_id, + "schema_issuer_did": schema_id.split(":")[-4], + "schema_name": schema_id.split(":")[-2], + "schema_version": schema_id.split(":")[-1], + "cred_def_id": cred_def_id, + "issuer_did": cred_def_id.split(":")[-5], + **{ + f"attr::{name}::value": value + for name, value in proof_values.items() + }, + } + + if ( + not any(r.items() <= criteria.items() for r in req_restrictions) + and len(req_restrictions) != 0 + ): + raise V20PresFormatHandlerError( + f"Presented attr group {reft} does not satisfy proof request " + f"restrictions {req_restrictions}" + ) + + # predicate bounds + for reft, pred_spec in proof["requested_proof"]["predicates"].items(): + proof_req_pred_spec = proof_req["requested_predicates"].get(reft) + if not proof_req_pred_spec: + raise V20PresFormatHandlerError( + f"Presentation referent {reft} not in proposal request" + ) + req_name = proof_req_pred_spec["name"] + req_pred = Predicate.get(proof_req_pred_spec["p_type"]) + req_value = proof_req_pred_spec["p_value"] + req_restrictions = proof_req_pred_spec.get("restrictions", {}) + for req_restriction in req_restrictions: + for k in list(req_restriction): # cannot modify en passant + if k.startswith("attr::"): + req_restriction.pop(k) # let indy-sdk reject mismatch here + sub_proof_index = pred_spec["sub_proof_index"] + for ge_proof in proof["proof"]["proofs"][sub_proof_index][ + "primary_proof" + ]["ge_proofs"]: + proof_pred_spec = ge_proof["predicate"] + if proof_pred_spec["attr_name"] != canon(req_name): + continue + if not ( + Predicate.get(proof_pred_spec["p_type"]) is req_pred + and proof_pred_spec["value"] == req_value + ): + raise V20PresFormatHandlerError( + f"Presentation predicate on {req_name} " + "mismatches proposal request" + ) + break + else: + raise V20PresFormatHandlerError( + f"Proposed request predicate on {req_name} not in presentation" + ) + + schema_id = proof["identifiers"][sub_proof_index]["schema_id"] + cred_def_id = proof["identifiers"][sub_proof_index]["cred_def_id"] + criteria = { + "schema_id": schema_id, + "schema_issuer_did": schema_id.split(":")[-4], + "schema_name": schema_id.split(":")[-2], + "schema_version": schema_id.split(":")[-1], + "cred_def_id": cred_def_id, + "issuer_did": cred_def_id.split(":")[-5], + } + + if ( + not any(r.items() <= criteria.items() for r in req_restrictions) + and len(req_restrictions) != 0 + ): + raise V20PresFormatHandlerError( + f"Presented predicate {reft} does not satisfy proof request " + f"restrictions {req_restrictions}" + ) + + proof = message.attachment(AnonCredsPresExchangeHandler.format) + _check_proof_vs_proposal() + + async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: + """Verify a presentation. + + Args: + pres_ex_record: presentation exchange record + with presentation request and presentation to verify + + Returns: + presentation exchange record, updated + + """ + pres_request_msg = pres_ex_record.pres_request + indy_proof_request = pres_request_msg.attachment( + AnonCredsPresExchangeHandler.format + ) + indy_proof = pres_ex_record.pres.attachment(AnonCredsPresExchangeHandler.format) + verifier = AnonCredsVerifier(self._profile) + + ( + schemas, + cred_defs, + rev_reg_defs, + rev_lists, + ) = await verifier.process_pres_identifiers(indy_proof["identifiers"]) + + verifier = AnonCredsVerifier(self._profile) + + (verified, verified_msgs) = await verifier.verify_presentation( + indy_proof_request, + indy_proof, + schemas, + cred_defs, + rev_reg_defs, + rev_lists, + ) + pres_ex_record.verified = json.dumps(verified) + pres_ex_record.verified_msgs = list(set(verified_msgs)) + return pres_ex_record diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py index 16ebb7fec6..cfe0e84c61 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py @@ -6,6 +6,7 @@ from marshmallow import RAISE from typing import Mapping, Tuple +from ......core.profile import Profile from ......indy.holder import IndyHolder from ......indy.models.predicate import Predicate from ......indy.models.proof import IndyProofSchema @@ -28,6 +29,7 @@ from ...messages.pres_format import V20PresFormat from ...models.pres_exchange import V20PresExRecord +from ..anoncreds.handler import AnonCredsPresExchangeHandler from ..handler import V20PresFormatHandler, V20PresFormatHandlerError LOGGER = logging.getLogger(__name__) @@ -37,6 +39,16 @@ class IndyPresExchangeHandler(V20PresFormatHandler): """Indy presentation format handler.""" format = V20PresFormat.Format.INDY + anoncreds_handler = None + + def __init__(self, profile: Profile): + """Shim initialization to check for new AnonCreds library.""" + super().__init__(profile) + + # Temporary shim while the new anoncreds library integration is in progress + wallet_type = profile.settings.get_value("wallet.type") + if wallet_type == "askar-anoncreds": + self.anoncreds_handler = AnonCredsPresExchangeHandler(profile) @classmethod def validate_fields(cls, message_type: str, attachment_data: Mapping): @@ -79,12 +91,19 @@ def get_format_identifier(self, message_type: str) -> str: str: Issue credential attachment format identifier """ + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return self.anoncreds_handler.get_format_identifier(message_type) + return ATTACHMENT_FORMAT[message_type][IndyPresExchangeHandler.format.api] def get_format_data( self, message_type: str, data: dict ) -> Tuple[V20PresFormat, AttachDecorator]: """Get presentation format and attach objects for use in pres_ex messages.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return self.anoncreds_handler.get_format_data(message_type, data) return ( V20PresFormat( @@ -110,6 +129,13 @@ async def create_bound_request( A tuple (updated presentation exchange record, presentation request message) """ + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.create_bound_request( + pres_ex_record, + request_data, + ) + indy_proof_request = pres_ex_record.pres_proposal.attachment( IndyPresExchangeHandler.format ) @@ -131,6 +157,12 @@ async def create_pres( request_data: dict = None, ) -> Tuple[V20PresFormat, AttachDecorator]: """Create a presentation.""" + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.create_pres( + pres_ex_record, request_data + ) + requested_credentials = {} if not request_data: try: @@ -301,6 +333,10 @@ def _check_proof_vs_proposal(): f"restrictions {req_restrictions}" ) + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.receive_pres(message, pres_ex_record) + proof = message.attachment(IndyPresExchangeHandler.format) _check_proof_vs_proposal() @@ -315,6 +351,10 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: presentation exchange record, updated """ + # Temporary shim while the new anoncreds library integration is in progress + if self.anoncreds_handler: + return await self.anoncreds_handler.verify_pres(pres_ex_record) + pres_request_msg = pres_ex_record.pres_request indy_proof_request = pres_request_msg.attachment(IndyPresExchangeHandler.format) indy_proof = pres_ex_record.pres.attachment(IndyPresExchangeHandler.format) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py index abac0f7398..60b1fcff9d 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_request_handler.py @@ -1,5 +1,6 @@ """Presentation request message handler.""" +from .....anoncreds.holder import AnonCredsHolderError from .....core.oob_processor import OobMessageProcessor from .....indy.holder import IndyHolderError from .....ledger.error import LedgerError @@ -116,6 +117,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send_reply(pres_message) except ( BaseModelError, + AnonCredsHolderError, IndyHolderError, LedgerError, StorageError, diff --git a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py index 9a493027a7..31a600b7ae 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/handlers/tests/test_pres_request_handler.py @@ -1,6 +1,8 @@ from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase +from asynctest import mock as async_mock +from ......anoncreds.holder import AnonCredsHolder from ......core.oob_processor import OobMessageProcessor from ......indy.holder import IndyHolder from ......messaging.decorators.attach_decorator import AttachDecorator @@ -300,7 +302,7 @@ async def test_called_not_found(self): ) assert not responder.messages - async def test_called_auto_present_x(self): + async def test_called_auto_present_x_indy(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() request_context.connection_record.connection_id = "dummy" @@ -365,6 +367,71 @@ async def test_called_auto_present_x(self): await handler.handle(request_context, responder) mock_log_exc.assert_called_once() + async def test_called_auto_present_x_anoncreds(self): + request_context = RequestContext.test_context() + request_context.connection_record = mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = V20PresRequest() + request_context.message.attachment = mock.MagicMock(return_value=INDY_PROOF_REQ) + request_context.message_receipt = MessageReceipt() + + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ, ident="indy") + ], + ) + mock_px_rec = mock.MagicMock( + pres_proposal=pres_proposal.serialize(), + auto_present=True, + save_error_state=mock.CoroutineMock(), + ) + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + mock_holder = mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock(return_value=[{"cred_info": {"referent": "dummy"}}]) + ) + ) + request_context.injector.bind_instance(AnonCredsHolder, mock_holder) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.return_value = mock_px_rec + mock_pres_ex_rec_cls.retrieve_by_tag_filter = mock.CoroutineMock( + return_value=mock_px_rec + ) + mock_pres_mgr.return_value.receive_pres_request = mock.CoroutineMock( + return_value=mock_px_rec + ) + + mock_pres_mgr.return_value.create_pres = async_mock.CoroutineMock( + side_effect=test_module.AnonCredsHolderError() + ) + + request_context.connection_ready = True + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + + with mock.patch.object( + handler._logger, "exception", mock.MagicMock() + ) as mock_log_exc: + await handler.handle(request_context, responder) + mock_log_exc.assert_called_once() + async def test_called_auto_present_indy(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() @@ -439,6 +506,80 @@ async def test_called_auto_present_indy(self): assert result == "pres message" assert target == {} + async def test_called_auto_present_anoncreds(self): + request_context = RequestContext.test_context() + request_context.connection_record = mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = V20PresRequest() + request_context.message.attachment = mock.MagicMock(return_value=INDY_PROOF_REQ) + request_context.message_receipt = MessageReceipt() + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ, ident="indy") + ], + ) + mock_px_rec = test_module.V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + auto_present=True, + ) + + mock_holder = mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock(return_value=[{"cred_info": {"referent": "dummy"}}]) + ) + ) + request_context.injector.bind_instance(AnonCredsHolder, mock_holder) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.return_value = mock_px_rec + mock_pres_ex_rec_cls.retrieve_by_tag_filter = mock.CoroutineMock( + return_value=mock_px_rec + ) + mock_pres_mgr.return_value.receive_pres_request = mock.CoroutineMock( + return_value=mock_px_rec + ) + + mock_pres_mgr.return_value.create_pres = mock.CoroutineMock( + return_value=(mock_px_rec, "pres message") + ) + + request_context.connection_ready = True + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + + await handler.handle(request_context, responder) + mock_pres_mgr.return_value.create_pres.assert_called_once() + + mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( + mock_px_rec + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "pres message" + assert target == {} + async def test_called_auto_present_dif(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() @@ -509,7 +650,7 @@ async def test_called_auto_present_dif(self): assert result == "pres message" assert target == {} - async def test_called_auto_present_no_preview(self): + async def test_called_auto_present_no_preview_indy(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() request_context.connection_record.connection_id = "dummy" @@ -577,7 +718,75 @@ async def test_called_auto_present_no_preview(self): assert result == "pres message" assert target == {} - async def test_called_auto_present_pred_no_match(self): + async def test_called_auto_present_no_preview_anoncreds(self): + request_context = RequestContext.test_context() + request_context.connection_record = mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ] + ) + request_context.message.attachment = mock.MagicMock(return_value=INDY_PROOF_REQ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = test_module.V20PresExRecord(auto_present=True) + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + mock_holder = mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock( + return_value=[ + {"cred_info": {"referent": "dummy-0"}}, + {"cred_info": {"referent": "dummy-1"}}, + ] + ) + ) + ) + request_context.injector.bind_instance(AnonCredsHolder, mock_holder) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.return_value = px_rec_instance + mock_pres_ex_rec_cls.retrieve_by_tag_filter = mock.CoroutineMock( + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_pres_request = mock.CoroutineMock( + return_value=px_rec_instance + ) + + mock_pres_mgr.return_value.create_pres = mock.CoroutineMock( + return_value=(px_rec_instance, "pres message") + ) + request_context.connection_ready = True + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + await handler.handle(request_context, responder) + mock_pres_mgr.return_value.create_pres.assert_called_once() + + mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( + px_rec_instance + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "pres message" + assert target == {} + + async def test_called_auto_present_pred_no_match_indy(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() request_context.connection_record.connection_id = "dummy" @@ -645,7 +854,75 @@ async def test_called_auto_present_pred_no_match(self): request_context ) - async def test_called_auto_present_pred_single_match(self): + async def test_called_auto_present_pred_no_match_anoncreds(self): + request_context = RequestContext.test_context() + request_context.connection_record = mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = V20PresRequest() + request_context.message.attachment = mock.MagicMock(return_value=INDY_PROOF_REQ) + request_context.message_receipt = MessageReceipt() + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ, ident="indy") + ], + ) + mock_px_rec = mock.MagicMock( + pres_proposal=pres_proposal.serialize(), + auto_present=True, + save_error_state=mock.CoroutineMock(), + ) + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + mock_holder = mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock(return_value=[]) + ) + ) + request_context.injector.bind_instance(AnonCredsHolder, mock_holder) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.return_value = mock_px_rec + mock_pres_ex_rec_cls.retrieve_by_tag_filter = mock.CoroutineMock( + return_value=mock_px_rec + ) + mock_pres_mgr.return_value.receive_pres_request = mock.CoroutineMock( + return_value=mock_px_rec + ) + + mock_pres_mgr.return_value.create_pres = mock.CoroutineMock( + side_effect=test_indy_handler.V20PresFormatHandlerError + ) + request_context.connection_ready = True + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + + await handler.handle(request_context, responder) + mock_px_rec.save_error_state.assert_called_once() + + mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( + mock_px_rec + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) + + async def test_called_auto_present_pred_single_match_indy(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() request_context.connection_record.connection_id = "dummy" @@ -713,7 +990,75 @@ async def test_called_auto_present_pred_single_match(self): assert result == "pres message" assert target == {} - async def test_called_auto_present_pred_multi_match(self): + async def test_called_auto_present_pred_single_match_anoncreds(self): + request_context = RequestContext.test_context() + request_context.connection_record = mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ] + ) + request_context.message.attachment = mock.MagicMock( + return_value=INDY_PROOF_REQ_PRED + ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = test_module.V20PresExRecord(auto_present=True) + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + mock_holder = mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock( + return_value=[{"cred_info": {"referent": "dummy-0"}}] + ) + ) + ) + request_context.injector.bind_instance(AnonCredsHolder, mock_holder) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.return_value = px_rec_instance + mock_pres_ex_rec_cls.retrieve_by_tag_filter = mock.CoroutineMock( + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_pres_request = mock.CoroutineMock( + return_value=px_rec_instance + ) + + mock_pres_mgr.return_value.create_pres = mock.CoroutineMock( + return_value=(px_rec_instance, "pres message") + ) + request_context.connection_ready = True + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + await handler.handle(request_context, responder) + mock_pres_mgr.return_value.create_pres.assert_called_once() + + mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( + px_rec_instance + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "pres message" + assert target == {} + + async def test_called_auto_present_pred_multi_match_indy(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() request_context.connection_record.connection_id = "dummy" @@ -784,7 +1129,78 @@ async def test_called_auto_present_pred_multi_match(self): assert result == "pres message" assert target == {} - async def test_called_auto_present_multi_cred_match_reft(self): + async def test_called_auto_present_pred_multi_match_anoncreds(self): + request_context = RequestContext.test_context() + request_context.connection_record = mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ] + ) + request_context.message.attachment = mock.MagicMock( + return_value=INDY_PROOF_REQ_PRED + ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = test_module.V20PresExRecord(auto_present=True) + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + mock_holder = mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock( + return_value=[ + {"cred_info": {"referent": "dummy-0"}}, + {"cred_info": {"referent": "dummy-1"}}, + ] + ) + ) + ) + request_context.injector.bind_instance(AnonCredsHolder, mock_holder) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.return_value = px_rec_instance + mock_pres_ex_rec_cls.retrieve_by_tag_filter = mock.CoroutineMock( + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_pres_request = mock.CoroutineMock( + return_value=px_rec_instance + ) + + mock_pres_mgr.return_value.create_pres = mock.CoroutineMock( + return_value=(px_rec_instance, "pres message") + ) + request_context.connection_ready = True + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + await handler.handle(request_context, responder) + mock_pres_mgr.return_value.create_pres.assert_called_once() + + mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( + px_rec_instance + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "pres message" + assert target == {} + + async def test_called_auto_present_multi_cred_match_reft_indy(self): request_context = RequestContext.test_context() request_context.connection_record = mock.MagicMock() request_context.connection_record.connection_id = "dummy" @@ -898,6 +1314,120 @@ async def test_called_auto_present_multi_cred_match_reft(self): assert result == "pres message" assert target == {} + async def test_called_auto_present_multi_cred_match_reft_anoncreds(self): + request_context = RequestContext.test_context() + request_context.connection_record = mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ] + ) + request_context.message.attachment = mock.MagicMock(return_value=INDY_PROOF_REQ) + request_context.message_receipt = MessageReceipt() + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=V20PresFormat.Format.INDY.aries, + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ, ident="indy") + ], + ) + + mock_oob_processor = mock.MagicMock( + find_oob_record_for_inbound_message=mock.CoroutineMock( + return_value=mock.MagicMock() + ) + ) + request_context.injector.bind_instance(OobMessageProcessor, mock_oob_processor) + + mock_holder = mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock( + return_value=[ + { + "cred_info": { + "referent": "dummy-0", + "cred_def_id": CD_ID, + "attrs": { + "ident": "zero", + "favourite": "potato", + "icon": "cG90YXRv", + }, + } + }, + { + "cred_info": { + "referent": "dummy-1", + "cred_def_id": CD_ID, + "attrs": { + "ident": "one", + "favourite": "spud", + "icon": "c3B1ZA==", + }, + } + }, + { + "cred_info": { + "referent": "dummy-2", + "cred_def_id": CD_ID, + "attrs": { + "ident": "two", + "favourite": "patate", + "icon": "cGF0YXRl", + }, + } + }, + ] + ) + ) + ) + request_context.injector.bind_instance(AnonCredsHolder, mock_holder) + + px_rec_instance = test_module.V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + auto_present=True, + ) + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.return_value = px_rec_instance + mock_pres_ex_rec_cls.retrieve_by_tag_filter = mock.CoroutineMock( + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_pres_request = mock.CoroutineMock( + return_value=px_rec_instance + ) + + mock_pres_mgr.return_value.create_pres = mock.CoroutineMock( + return_value=(px_rec_instance, "pres message") + ) + request_context.connection_ready = True + handler = test_module.V20PresRequestHandler() + responder = MockResponder() + await handler.handle(request_context, responder) + mock_pres_mgr.return_value.create_pres.assert_called_once() + + mock_pres_mgr.return_value.receive_pres_request.assert_called_once_with( + px_rec_instance + ) + mock_oob_processor.find_oob_record_for_inbound_message.assert_called_once_with( + request_context + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "pres message" + assert target == {} + async def test_called_not_ready(self): request_context = RequestContext.test_context() request_context.message_receipt = MessageReceipt() diff --git a/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_format.py b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_format.py index 9b98076e95..7de8d98794 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_format.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/messages/pres_format.py @@ -37,6 +37,16 @@ class Format(Enum): ".formats.indy.handler.IndyPresExchangeHandler" ), ) + """ + To make the switch from indy to anoncreds replace the above with the following + INDY = FormatSpec( + "hlindy/", + DeferLoad( + "aries_cloudagent.protocols.present_proof.v2_0" + ".formats.anoncreds.handler.AnonCredsPresExchangeHandler" + ), + ) + """ DIF = FormatSpec( "dif/", DeferLoad( diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index eba9138663..78253eadf0 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -16,6 +16,7 @@ from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord +from ....anoncreds.holder import AnonCredsHolder, AnonCredsHolderError from ....indy.holder import IndyHolder, IndyHolderError from ....indy.models.cred_precis import IndyCredPrecisSchema from ....indy.models.proof import IndyPresSpecSchema @@ -547,7 +548,11 @@ async def present_proof_credentials_list(request: web.BaseRequest): start = int(start) if isinstance(start, str) else 0 count = int(count) if isinstance(count, str) else 10 - indy_holder = profile.inject(IndyHolder) + wallet_type = profile.settings.get_value("wallet.type") + if wallet_type == "askar-anoncreds": + indy_holder = AnonCredsHolder(profile) + else: + indy_holder = profile.inject(IndyHolder) indy_credentials = [] # INDY try: @@ -564,7 +569,7 @@ async def present_proof_credentials_list(request: web.BaseRequest): extra_query, ) ) - except IndyHolderError as err: + except (IndyHolderError, AnonCredsHolderError) as err: if pres_ex_record: async with profile.session() as session: await pres_ex_record.save_error_state(session, reason=err.roll_up) @@ -1205,6 +1210,7 @@ async def present_proof_send_presentation(request: web.BaseRequest): except ( BaseModelError, IndyHolderError, + AnonCredsHolderError, LedgerError, V20PresFormatHandlerError, StorageError, diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py new file mode 100644 index 0000000000..91bd9fdb48 --- /dev/null +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager_anoncreds.py @@ -0,0 +1,2369 @@ +import json +import pytest + +from copy import deepcopy +from time import time + +from aries_cloudagent.tests import mock +from unittest import IsolatedAsyncioTestCase +from asynctest import mock as async_mock + +from .....core.in_memory import InMemoryProfile +from .....anoncreds.holder import AnonCredsHolder +from .....indy.models.xform import indy_proof_req_preview2indy_requested_creds +from .....indy.models.pres_preview import ( + IndyPresAttrSpec, + IndyPresPreview, + IndyPresPredSpec, +) +from .....anoncreds.verifier import AnonCredsVerifier +from .....ledger.base import BaseLedger +from .....ledger.multiple_ledger.ledger_requests_executor import ( + IndyLedgerRequestsExecutor, +) +from .....messaging.decorators.attach_decorator import AttachDecorator +from .....messaging.responder import BaseResponder, MockResponder +from .....multitenant.base import BaseMultitenantManager +from .....multitenant.manager import MultitenantManager +from .....storage.error import StorageNotFoundError + +from ...indy import pres_exch_handler as test_indy_util_module + +from .. import manager as test_module +from ..formats.handler import V20PresFormatHandlerError +from ..formats.dif.handler import DIFPresFormatHandler +from ..formats.dif.tests.test_handler import ( + DIF_PRES_REQUEST_B as DIF_PRES_REQ, + DIF_PRES, +) +from ..formats.indy import handler as test_indy_handler +from ..manager import V20PresManager, V20PresManagerError +from ..message_types import ( + ATTACHMENT_FORMAT, + PRES_20_PROPOSAL, + PRES_20_REQUEST, + PRES_20, +) +from ..messages.pres import V20Pres +from ..messages.pres_format import V20PresFormat +from ..messages.pres_problem_report import V20PresProblemReport +from ..messages.pres_proposal import V20PresProposal +from ..messages.pres_request import V20PresRequest +from ..models.pres_exchange import V20PresExRecord + +from .....vc.vc_ld.validation_result import PresentationVerificationResult +from .....vc.tests.document_loader import custom_document_loader +from .....vc.ld_proofs import DocumentLoader + +CONN_ID = "connection_id" +ISSUER_DID = "NcYxiDXkpYi6ov5FcYDi1e" +S_ID = f"{ISSUER_DID}:2:vidya:1.0" +CD_ID = f"{ISSUER_DID}:3:CL:{S_ID}:tag1" +RR_ID = f"{ISSUER_DID}:4:{CD_ID}:CL_ACCUM:0" +PROOF_REQ_NAME = "name" +PROOF_REQ_VERSION = "1.0" +PROOF_REQ_NONCE = "12345" + +NOW = int(time()) +PRES_PREVIEW = IndyPresPreview( + attributes=[ + IndyPresAttrSpec(name="player", cred_def_id=CD_ID, value="Richie Knucklez"), + IndyPresAttrSpec( + name="screenCapture", + cred_def_id=CD_ID, + mime_type="image/png", + value="aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + ), + ], + predicates=[ + IndyPresPredSpec( + name="highScore", cred_def_id=CD_ID, predicate=">=", threshold=1000000 + ) + ], +) +INDY_PROOF_REQ_NAME = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "0_player_uuid": { + "name": "player", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + "0_screencapture_uuid": { + "name": "screenCapture", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + }, + "requested_predicates": { + "0_highscore_GE_uuid": { + "name": "highScore", + "p_type": ">=", + "p_value": 1000000, + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, +} +INDY_PROOF_REQ_NAMES = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "0_player_uuid": { + "names": ["player", "screenCapture"], + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, + "requested_predicates": { + "0_highscore_GE_uuid": { + "name": "highScore", + "p_type": ">=", + "p_value": 1000000, + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, +} +INDY_PROOF_REQ_SELFIE = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "self_player_uuid": {"name": "player"}, + "self_screencapture_uuid": {"name": "screenCapture"}, + }, + "requested_predicates": { + "0_highscore_GE_uuid": {"name": "highScore", "p_type": ">=", "p_value": 1000000} + }, +} +INDY_PROOF = { + "proof": { + "proofs": [ + { + "primary_proof": { + "eq_proof": { + "revealed_attrs": { + "player": "51643998292319337989", + "screencapture": "124831723185628395682368329568235681", + }, + "a_prime": "98381845469564775640588", + "e": "2889201651469315129053056279820725958192110265136", + "v": "337782521199137176224", + "m": { + "master_secret": "88675074759262558623", + "date": "3707627155679953691027082306", + "highscore": "251972383037120760793174059437326", + }, + "m2": "2892781443118611948331343540849982215419978654911341", + }, + "ge_proofs": [ + { + "u": { + "0": "99189584890680947709857922351898933228959", + "3": "974568160016086782335901983921278203", + "2": "127290395299", + "1": "7521808223922", + }, + "r": { + "3": "247458", + "2": "263046", + "1": "285214", + "DELTA": "4007402", + "0": "12066738", + }, + "mj": "1507606", + "alpha": "20251550018805200", + "t": { + "1": "1262519732727", + "3": "82102416", + "0": "100578099981822", + "2": "47291", + "DELTA": "556736142765", + }, + "predicate": { + "attr_name": "highscore", + "p_type": "GE", + "value": 1000000, + }, + } + ], + }, + "non_revoc_proof": { + "x_list": { + "rho": "128121489ACD4D778ECE", + "r": "1890DEFBB8A254", + "r_prime": "0A0861FFE96C", + "r_prime_prime": "058376CE", + "r_prime_prime_prime": "188DF30745A595", + "o": "0D0F7FA1", + "o_prime": "28165", + "m": "0187A9817897FC", + "m_prime": "91261D96B", + "t": "10FE96", + "t_prime": "10856A", + "m2": "B136089AAF", + "s": "018969A6D", + "c": "09186B6A", + }, + "c_list": { + "e": "6 1B161", + "d": "6 19E861869", + "a": "6 541441EE2", + "g": "6 7601B068C", + "w": "21 10DE6 4 AAAA 5 2458 6 16161", + "s": "21 09616 4 1986 5 9797 6 BBBBB", + "u": "21 3213123 4 0616FFE 5 323 6 110861861", + }, + }, + } + ], + "aggregated_proof": { + "c_hash": "81147637626525127013830996", + "c_list": [ + [3, 18, 46, 12], + [3, 136, 2, 39], + [100, 111, 148, 193], + [1, 123, 11, 152], + [2, 138, 162, 227], + [1, 239, 33, 47], + ], + }, + }, + "requested_proof": { + "revealed_attrs": { + "0_player_uuid": { + "sub_proof_index": 0, + "raw": "Richie Knucklez", + "encoded": "516439982", + }, + "0_screencapture_uuid": { + "sub_proof_index": 0, + "raw": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + "encoded": "4434954949", + }, + }, + "self_attested_attrs": {}, + "unrevealed_attrs": {}, + "predicates": {"0_highscore_GE_uuid": {"sub_proof_index": 0}}, + }, + "identifiers": [ + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": RR_ID, + "timestamp": NOW, + } + ], +} +INDY_PROOF_NAMES = { + "proof": { + "proofs": [ + { + "primary_proof": { + "eq_proof": { + "revealed_attrs": { + "player": "51643998292319337989", + "screencapture": "124831723185628395682368329568235681", + }, + "a_prime": "98381845469564775640588", + "e": "2889201651469315129053056279820725958192110265136", + "v": "337782521199137176224", + "m": { + "master_secret": "88675074759262558623", + "date": "3707627155679953691027082306", + "highscore": "251972383037120760793174059437326", + }, + "m2": "2892781443118611948331343540849982215419978654911341", + }, + "ge_proofs": [ + { + "u": { + "0": "99189584890680947709857922351898933228959", + "3": "974568160016086782335901983921278203", + "2": "127290395299", + "1": "7521808223922", + }, + "r": { + "3": "247458", + "2": "263046", + "1": "285214", + "DELTA": "4007402", + "0": "12066738", + }, + "mj": "1507606", + "alpha": "20251550018805200", + "t": { + "1": "1262519732727", + "3": "82102416", + "0": "100578099981822", + "2": "47291", + "DELTA": "556736142765", + }, + "predicate": { + "attr_name": "highscore", + "p_type": "GE", + "value": 1000000, + }, + } + ], + }, + "non_revoc_proof": { + "x_list": { + "rho": "128121489ACD4D778ECE", + "r": "1890DEFBB8A254", + "r_prime": "0A0861FFE96C", + "r_prime_prime": "058376CE", + "r_prime_prime_prime": "188DF30745A595", + "o": "0D0F7FA1", + "o_prime": "28165", + "m": "0187A9817897FC", + "m_prime": "91261D96B", + "t": "10FE96", + "t_prime": "10856A", + "m2": "B136089AAF", + "s": "018969A6D", + "c": "09186B6A", + }, + "c_list": { + "e": "6 1B161", + "d": "6 19E861869", + "a": "6 541441EE2", + "g": "6 7601B068C", + "w": "21 10DE6 4 AAAA 5 2458 6 16161", + "s": "21 09616 4 1986 5 9797 6 BBBBB", + "u": "21 3213123 4 0616FFE 5 323 6 110861861", + }, + }, + } + ], + "aggregated_proof": { + "c_hash": "81147637626525127013830996", + "c_list": [ + [3, 18, 46, 12], + [3, 136, 2, 39], + [100, 111, 148, 193], + [1, 123, 11, 152], + [2, 138, 162, 227], + [1, 239, 33, 47], + ], + }, + }, + "requested_proof": { + "revealed_attrs": {}, + "revealed_attr_groups": { + "0_player_uuid": { + "sub_proof_index": 0, + "values": { + "player": { + "raw": "Richie Knucklez", + "encoded": "516439982", + }, + "screenCapture": { + "raw": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + "encoded": "4434954949", + }, + }, + }, + }, + "self_attested_attrs": {}, + "unrevealed_attrs": {}, + "predicates": {"0_highscore_GE_uuid": {"sub_proof_index": 0}}, + }, + "identifiers": [ + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": RR_ID, + "timestamp": NOW, + } + ], +} + + +class TestV20PresManagerAnonCreds(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.profile = InMemoryProfile.test_profile() + self.profile.settings.set_value("wallet.type", "askar-anoncreds") + injector = self.profile.context.injector + + Ledger = mock.MagicMock(BaseLedger, autospec=True) + self.ledger = Ledger() + self.ledger.get_schema = mock.CoroutineMock(return_value=mock.MagicMock()) + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value={"value": {"revocation": {"...": "..."}}} + ) + self.ledger.get_revoc_reg_def = mock.CoroutineMock( + return_value={ + "ver": "1.0", + "id": RR_ID, + "revocDefType": "CL_ACCUM", + "tag": RR_ID.split(":")[-1], + "credDefId": CD_ID, + "value": { + "IssuanceType": "ISSUANCE_BY_DEFAULT", + "maxCredNum": 1000, + "publicKeys": {"accumKey": {"z": "1 ..."}}, + "tailsHash": "3MLjUFQz9x9n5u9rFu8Ba9C5bo4HNFjkPNc54jZPSNaZ", + "tailsLocation": "http://sample.ca/path", + }, + } + ) + self.ledger.get_revoc_reg_delta = mock.CoroutineMock( + return_value=( + { + "ver": "1.0", + "value": {"prevAccum": "1 ...", "accum": "21 ...", "issued": [1]}, + }, + NOW, + ) + ) + self.ledger.get_revoc_reg_entry = mock.CoroutineMock( + return_value=( + { + "ver": "1.0", + "value": {"prevAccum": "1 ...", "accum": "21 ...", "issued": [1]}, + }, + NOW, + ) + ) + injector.bind_instance(BaseLedger, self.ledger) + injector.bind_instance( + IndyLedgerRequestsExecutor, + mock.MagicMock( + get_ledger_for_identifier=mock.CoroutineMock( + return_value=(None, self.ledger) + ) + ), + ) + + Holder = async_mock.MagicMock(AnonCredsHolder, autospec=True) + self.holder = Holder() + get_creds = mock.CoroutineMock( + return_value=( + { + "cred_info": { + "referent": "dummy_reft", + "attrs": { + "player": "Richie Knucklez", + "screenCapture": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + "highScore": "1234560", + }, + } + }, # leave this comma: return a tuple + ) + ) + self.holder.get_credentials_for_presentation_request_by_referent = get_creds + self.holder.get_credential = mock.CoroutineMock( + return_value=json.dumps( + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": RR_ID, + "cred_rev_id": 1, + } + ) + ) + self.holder.create_presentation = mock.CoroutineMock(return_value="{}") + self.holder.create_revocation_state = mock.CoroutineMock( + return_value=json.dumps( + { + "witness": {"omega": "1 ..."}, + "rev_reg": {"accum": "21 ..."}, + "timestamp": NOW, + } + ) + ) + injector.bind_instance(AnonCredsHolder, self.holder) + + Verifier = async_mock.MagicMock(AnonCredsVerifier, autospec=True) + self.verifier = Verifier() + self.verifier.verify_presentation = mock.CoroutineMock( + return_value=("true", []) + ) + injector.bind_instance(AnonCredsVerifier, self.verifier) + + self.manager = V20PresManager(self.profile) + + async def test_record_eq(self): + same = [ + V20PresExRecord( + pres_ex_id="dummy-0", + thread_id="thread-0", + role=V20PresExRecord.ROLE_PROVER, + ) + ] * 2 + diff = [ + V20PresExRecord( + pres_ex_id="dummy-1", + role=V20PresExRecord.ROLE_PROVER, + ), + V20PresExRecord( + pres_ex_id="dummy-0", + thread_id="thread-1", + role=V20PresExRecord.ROLE_PROVER, + ), + V20PresExRecord( + pres_ex_id="dummy-1", + thread_id="thread-0", + role=V20PresExRecord.ROLE_VERIFIER, + ), + ] + + for i in range(len(same) - 1): + for j in range(i, len(same)): + assert same[i] == same[j] + + for i in range(len(diff) - 1): + for j in range(i, len(diff)): + assert diff[i] == diff[j] if i == j else diff[i] != diff[j] + + async def test_create_exchange_for_proposal(self): + proposal = V20PresProposal( + formats=[ + V20PresFormat(attach_id="indy", format_=V20PresFormat.Format.INDY.aries) + ] + ) + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object(V20PresProposal, "serialize", autospec=True): + px_rec = await self.manager.create_exchange_for_proposal( + CONN_ID, + proposal, + auto_present=None, + auto_remove=True, + ) + save_ex.assert_called_once() + + assert px_rec.thread_id == proposal._thread_id + assert px_rec.initiator == V20PresExRecord.INITIATOR_SELF + assert px_rec.role == V20PresExRecord.ROLE_PROVER + assert px_rec.state == V20PresExRecord.STATE_PROPOSAL_SENT + assert px_rec.auto_remove is True + + async def test_receive_proposal(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + proposal = V20PresProposal( + formats=[ + V20PresFormat(attach_id="indy", format_=V20PresFormat.Format.INDY.aries) + ] + ) + with mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: + px_rec = await self.manager.receive_pres_proposal( + proposal, + connection_record, + ) + save_ex.assert_called_once() + + assert px_rec.state == V20PresExRecord.STATE_PROPOSAL_RECEIVED + + async def test_create_bound_request_a(self): + comment = "comment" + + proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + px_rec = V20PresExRecord( + pres_proposal=proposal.serialize(), + role=V20PresExRecord.ROLE_VERIFIER, + ) + px_rec.save = mock.CoroutineMock() + request_data = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + } + (ret_px_rec, pres_req_msg) = await self.manager.create_bound_request( + pres_ex_record=px_rec, + request_data=request_data, + comment=comment, + ) + assert ret_px_rec is px_rec + px_rec.save.assert_called_once() + + async def test_create_bound_request_b(self): + comment = "comment" + + proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + px_rec = V20PresExRecord( + pres_proposal=proposal.serialize(), + role=V20PresExRecord.ROLE_VERIFIER, + ) + px_rec.save = mock.CoroutineMock() + (ret_px_rec, pres_req_msg) = await self.manager.create_bound_request( + pres_ex_record=px_rec, + comment=comment, + ) + assert ret_px_rec is px_rec + px_rec.save.assert_called_once() + + async def test_create_bound_request_no_format(self): + px_rec = V20PresExRecord( + pres_proposal=V20PresProposal( + formats=[], + proposals_attach=[], + ).serialize(), + role=V20PresExRecord.ROLE_VERIFIER, + ) + with self.assertRaises(V20PresManagerError) as context: + await self.manager.create_bound_request( + pres_ex_record=px_rec, + request_data={}, + comment="test", + ) + assert "No supported formats" in str(context.exception) + + async def test_create_pres_no_format(self): + px_rec = V20PresExRecord( + pres_proposal=V20PresProposal( + formats=[], + proposals_attach=[], + ).serialize(), + pres_request=V20PresRequest( + formats=[], request_presentations_attach=[] + ).serialize(), + ) + with self.assertRaises(V20PresManagerError) as context: + await self.manager.create_pres( + pres_ex_record=px_rec, + request_data={}, + comment="test", + ) + assert "No supported formats" in str(context.exception) + + async def test_create_pres_catch_diferror(self): + px_rec = V20PresExRecord( + pres_request=V20PresRequest( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_json(DIF_PRES_REQ, ident="dif") + ], + ).serialize(), + ) + with mock.patch.object( + DIFPresFormatHandler, "create_pres", autospec=True + ) as mock_create_pres: + mock_create_pres.return_value = None + with self.assertRaises(V20PresManagerError) as context: + await self.manager.create_pres( + pres_ex_record=px_rec, + request_data={}, + comment="test", + ) + assert "Unable to create presentation. ProblemReport message sent" in str( + context.exception + ) + + async def test_receive_pres_catch_diferror(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_json( + mapping=DIF_PRES, + ident="dif", + ) + ], + ) + pres_req = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_json(DIF_PRES_REQ, ident="dif") + ], + ) + px_rec = V20PresExRecord( + pres_request=pres_req.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + DIFPresFormatHandler, "receive_pres", autospec=True + ) as mock_receive_pres, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + mock_receive_pres.return_value = False + retrieve_ex.side_effect = [px_rec] + with self.assertRaises(V20PresManagerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "Unable to verify received presentation." in str(context.exception) + + async def test_create_exchange_for_request(self): + pres_req = V20PresRequest( + comment="Test", + will_confirm=True, + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(mapping=INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + pres_req.assign_thread_id("dummy") + + with mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: + px_rec = await self.manager.create_exchange_for_request( + CONN_ID, + pres_req, + auto_remove=True, + ) + save_ex.assert_called_once() + + assert px_rec.thread_id == pres_req._thread_id + assert px_rec.initiator == V20PresExRecord.INITIATOR_SELF + assert px_rec.role == V20PresExRecord.ROLE_VERIFIER + assert px_rec.state == V20PresExRecord.STATE_REQUEST_SENT + assert px_rec.auto_remove is True + + async def test_receive_pres_request(self): + px_rec_in = V20PresExRecord() + + with mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: + px_rec_out = await self.manager.receive_pres_request(px_rec_in) + save_ex.assert_called_once() + + assert px_rec_out.state == V20PresExRecord.STATE_REQUEST_RECEIVED + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_pres_indy(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + more_magic_rr = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock( + return_value="/tmp/sample/tails/path" + ) + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator, mock.patch.object( + test_indy_util_module, "RevocationRegistry", autospec=True + ) as mock_rr: + mock_rr.from_definition = mock.MagicMock(return_value=more_magic_rr) + + mock_attach_decorator.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator + ) + + req_creds = await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_NAME, preview=None, holder=self.holder + ) + request_data = {"indy": req_creds} + assert not req_creds["self_attested_attributes"] + assert len(req_creds["requested_attributes"]) == 2 + assert len(req_creds["requested_predicates"]) == 1 + + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + save_ex.assert_called_once() + assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_pres_indy_and_dif(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ), + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ), + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy"), + AttachDecorator.data_json(DIF_PRES_REQ, ident="dif"), + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + more_magic_rr = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock( + return_value="/tmp/sample/tails/path" + ) + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator_indy, mock.patch.object( + test_indy_util_module, "RevocationRegistry", autospec=True + ) as mock_rr, mock.patch.object( + DIFPresFormatHandler, "create_pres", autospec=True + ) as mock_create_pres: + mock_rr.from_definition = mock.MagicMock(return_value=more_magic_rr) + + mock_attach_decorator_indy.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator_indy + ) + + mock_create_pres.return_value = ( + PRES_20, + AttachDecorator.data_json(DIF_PRES, ident="dif"), + ) + + req_creds = await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_NAME, preview=None, holder=self.holder + ) + request_data = {"indy": req_creds, "dif": DIF_PRES_REQ} + assert not req_creds["self_attested_attributes"] + assert len(req_creds["requested_attributes"]) == 2 + assert len(req_creds["requested_predicates"]) == 1 + + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + save_ex.assert_called_once() + assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_pres_proof_req_non_revoc_interval_none(self): + indy_proof_req_vcx = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req_vcx["non_revoked"] = None # simulate interop with indy-vcx + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req_vcx, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + + more_magic_rr = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock( + return_value="/tmp/sample/tails/path" + ) + ) + self.profile.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator, mock.patch.object( + test_indy_util_module, "RevocationRegistry", autospec=True + ) as mock_rr: + mock_rr.from_definition = mock.MagicMock(return_value=more_magic_rr) + + mock_attach_decorator.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator + ) + + req_creds = await indy_proof_req_preview2indy_requested_creds( + indy_proof_req_vcx, preview=None, holder=self.holder + ) + request_data = {"indy": req_creds} + assert not req_creds["self_attested_attributes"] + assert len(req_creds["requested_attributes"]) == 2 + assert len(req_creds["requested_predicates"]) == 1 + + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + save_ex.assert_called_once() + assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_pres_self_asserted(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_SELFIE, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + + more_magic_rr = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock( + return_value="/tmp/sample/tails/path" + ) + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator, mock.patch.object( + test_indy_util_module, "RevocationRegistry", autospec=True + ) as mock_rr: + mock_rr.from_definition = mock.MagicMock(return_value=more_magic_rr) + + mock_attach_decorator.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator + ) + + req_creds = await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_SELFIE, preview=None, holder=self.holder + ) + request_data = {"indy": req_creds} + + assert len(req_creds["self_attested_attributes"]) == 3 + assert not req_creds["requested_attributes"] + assert not req_creds["requested_predicates"] + + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + save_ex.assert_called_once() + assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_pres_no_revocation(self): + Ledger = mock.MagicMock(BaseLedger, autospec=True) + self.ledger = Ledger() + self.ledger.get_schema = mock.CoroutineMock(return_value=mock.MagicMock()) + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value={"value": {"revocation": None}} + ) + self.profile.context.injector.bind_instance(BaseLedger, self.ledger) + + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + + Holder = async_mock.MagicMock(AnonCredsHolder, autospec=True) + self.holder = Holder() + get_creds = mock.CoroutineMock( + return_value=( + { + "cred_info": {"referent": "dummy_reft"}, + "attrs": { + "player": "Richie Knucklez", + "screenCapture": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + "highScore": "1234560", + }, + }, # leave this comma: return a tuple + ) + ) + self.holder.get_credentials_for_presentation_request_by_referent = get_creds + self.holder.get_credential = mock.CoroutineMock( + return_value=json.dumps( + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": None, + "cred_rev_id": None, + } + ) + ) + self.holder.create_presentation = async_mock.CoroutineMock(return_value="{}") + self.profile.context.injector.bind_instance(AnonCredsHolder, self.holder) + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator, mock.patch.object( + test_indy_util_module.LOGGER, "info", mock.MagicMock() + ) as mock_log_info: + mock_attach_decorator.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator + ) + + req_creds = await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_NAME, preview=None, holder=self.holder + ) + request_data = { + "indy": { + "self_attested_attributes": req_creds["self_attested_attributes"], + "requested_attributes": req_creds["requested_attributes"], + "requested_predicates": req_creds["requested_predicates"], + } + } + + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + save_ex.assert_called_once() + assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + + # exercise superfluous timestamp removal + for pred_reft_spec in req_creds["requested_predicates"].values(): + pred_reft_spec["timestamp"] = 1234567890 + request_data = { + "indy": { + "self_attested_attributes": req_creds["self_attested_attributes"], + "requested_attributes": req_creds["requested_attributes"], + "requested_predicates": req_creds["requested_predicates"], + } + } + await self.manager.create_pres(px_rec_in, request_data) + mock_log_info.assert_called_once() + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_pres_bad_revoc_state(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + + Holder = async_mock.MagicMock(AnonCredsHolder, autospec=True) + self.holder = Holder() + get_creds = mock.CoroutineMock( + return_value=( + { + "cred_info": {"referent": "dummy_reft"}, + "attrs": { + "player": "Richie Knucklez", + "screenCapture": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + "highScore": "1234560", + }, + }, # leave this comma: return a tuple + ) + ) + self.holder.get_credentials_for_presentation_request_by_referent = get_creds + + self.holder.get_credential = mock.CoroutineMock( + return_value=json.dumps( + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": RR_ID, + "cred_rev_id": 1, + } + ) + ) + self.holder.create_presentation = async_mock.CoroutineMock(return_value="{}") + self.holder.create_revocation_state = async_mock.CoroutineMock( + side_effect=test_indy_util_module.AnonCredsHolderError( + "Problem", {"message": "Nope"} + ) + ) + self.profile.context.injector.bind_instance(AnonCredsHolder, self.holder) + + more_magic_rr = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock( + return_value="/tmp/sample/tails/path" + ) + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator, mock.patch.object( + test_indy_util_module, "RevocationRegistry", autospec=True + ) as mock_rr, mock.patch.object( + test_indy_util_module.LOGGER, "error", mock.MagicMock() + ) as mock_log_error: + mock_rr.from_definition = mock.MagicMock(return_value=more_magic_rr) + + mock_attach_decorator.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator + ) + request_data = {} + with self.assertRaises(test_indy_util_module.AnonCredsHolderError): + await self.manager.create_pres(px_rec_in, request_data) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_pres_multi_matching_proposal_creds_names(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAMES, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + + Holder = async_mock.MagicMock(AnonCredsHolder, autospec=True) + self.holder = Holder() + get_creds = mock.CoroutineMock( + return_value=( + { + "cred_info": { + "referent": "dummy_reft_0", + "cred_def_id": CD_ID, + "attrs": { + "player": "Richie Knucklez", + "screenCapture": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + "highScore": "1234560", + }, + } + }, + { + "cred_info": { + "referent": "dummy_reft_1", + "cred_def_id": CD_ID, + "attrs": { + "player": "Richie Knucklez", + "screenCapture": "aW1hZ2luZSBhbm90aGVyIHNjcmVlbiBjYXB0dXJl", + "highScore": "1515880", + }, + } + }, + ) + ) + self.holder.get_credentials_for_presentation_request_by_referent = get_creds + self.holder.get_credential = mock.CoroutineMock( + return_value=json.dumps( + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": RR_ID, + "cred_rev_id": 1, + } + ) + ) + self.holder.create_presentation = mock.CoroutineMock(return_value="{}") + self.holder.create_revocation_state = mock.CoroutineMock( + return_value=json.dumps( + { + "witness": {"omega": "1 ..."}, + "rev_reg": {"accum": "21 ..."}, + "timestamp": NOW, + } + ) + ) + self.profile.context.injector.bind_instance(AnonCredsHolder, self.holder) + + more_magic_rr = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock( + return_value="/tmp/sample/tails/path" + ) + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator, mock.patch.object( + test_indy_util_module, "RevocationRegistry", autospec=True + ) as mock_rr: + mock_rr.from_definition = mock.MagicMock(return_value=more_magic_rr) + + mock_attach_decorator.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator + ) + + req_creds = await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_NAMES, preview=None, holder=self.holder + ) + assert not req_creds["self_attested_attributes"] + assert len(req_creds["requested_attributes"]) == 1 + assert len(req_creds["requested_predicates"]) == 1 + request_data = {"indy": req_creds} + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + save_ex.assert_called_once() + assert px_rec_out.state == V20PresExRecord.STATE_PRESENTATION_SENT + + async def test_no_matching_creds_for_proof_req(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAMES, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + get_creds = mock.CoroutineMock(return_value=()) + self.holder.get_credentials_for_presentation_request_by_referent = get_creds + + with self.assertRaises(ValueError): + await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_NAMES, preview=None, holder=self.holder + ) + + get_creds = mock.CoroutineMock( + return_value=( + { + "cred_info": {"referent": "dummy_reft"}, + "attrs": { + "player": "Richie Knucklez", + "screenCapture": "aW1hZ2luZSBhIHNjcmVlbiBjYXB0dXJl", + "highScore": "1234560", + }, + }, # leave this comma: return a tuple + ) + ) + self.holder.get_credentials_for_presentation_request_by_referent = get_creds + await indy_proof_req_preview2indy_requested_creds( + INDY_PROOF_REQ_NAMES, preview=None, holder=self.holder + ) + + async def test_no_matching_creds_indy_handler(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAMES, ident="indy") + ], + ) + px_rec_in = V20PresExRecord(pres_request=pres_request.serialize()) + get_creds = mock.CoroutineMock(return_value=()) + self.holder.get_credentials_for_presentation_request_by_referent = get_creds + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + test_indy_handler, "AttachDecorator", autospec=True + ) as mock_attach_decorator: + mock_attach_decorator.data_base64 = mock.MagicMock( + return_value=mock_attach_decorator + ) + request_data = {} + with self.assertRaises( + test_indy_handler.V20PresFormatHandlerError + ) as context: + (px_rec_out, pres_msg) = await self.manager.create_pres( + px_rec_in, request_data + ) + assert "No matching Indy" in str(context.exception) + + async def test_receive_pres(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + pres.assign_thread_id("thread-id") + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + ) + + # cover by_format property + by_format = px_rec_dummy.by_format + + assert by_format.get("pres_proposal").get("indy") == INDY_PROOF_REQ_NAME + assert by_format.get("pres_request").get("indy") == INDY_PROOF_REQ_NAME + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex, mock.patch.object( + self.profile, + "session", + mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) + + async def test_receive_pres_receive_pred_value_mismatch_punt_to_indy(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + indy_proof_req = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"]["restrictions"][ + 0 + ]["attr::player::value"] = "impostor" + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + pres.assign_thread_id("thread-id") + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + ) + + # cover by_format property + by_format = px_rec_dummy.by_format + + assert by_format.get("pres_proposal").get("indy") == INDY_PROOF_REQ_NAME + assert by_format.get("pres_request").get("indy") == indy_proof_req + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex, mock.patch.object( + self.profile, + "session", + mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) + + async def test_receive_pres_indy_no_predicate_restrictions(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + indy_proof_req = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "0_player_uuid": { + "name": "player", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + "0_screencapture_uuid": { + "name": "screenCapture", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + }, + "requested_predicates": { + "0_highscore_GE_uuid": { + "name": "highScore", + "p_type": ">=", + "p_value": 1000000, + "restrictions": [], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, + } + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + pres.assign_thread_id("thread-id") + + px_rec_dummy = V20PresExRecord( + pres_request=pres_request.serialize(), + ) + + # cover by_format property + by_format = px_rec_dummy.by_format + + assert by_format.get("pres_request").get("indy") == indy_proof_req + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex, mock.patch.object( + self.profile, + "session", + mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) + + async def test_receive_pres_indy_no_attr_restrictions(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + indy_proof_req = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "0_player_uuid": { + "name": "player", + "restrictions": [], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, + "requested_predicates": {}, + } + proof = deepcopy(INDY_PROOF) + proof["requested_proof"]["revealed_attrs"] = { + "0_player_uuid": { + "sub_proof_index": 0, + "raw": "Richie Knucklez", + "encoded": "516439982", + } + } + proof["requested_proof"]["predicates"] = {} + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[AttachDecorator.data_base64(proof, ident="indy")], + ) + pres.assign_thread_id("thread-id") + + px_rec_dummy = V20PresExRecord( + pres_request=pres_request.serialize(), + ) + + # cover by_format property + by_format = px_rec_dummy.by_format + + assert by_format.get("pres_request").get("indy") == indy_proof_req + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex, mock.patch.object( + self.profile, + "session", + mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.side_effect = [px_rec_dummy] + px_rec_out = await self.manager.receive_pres(pres, connection_record, None) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": "thread-id"}, + {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID}, + ) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED) + + async def test_receive_pres_bait_and_switch_attr_name(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + indy_proof_req = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req["requested_attributes"]["0_screencapture_uuid"]["restrictions"][ + 0 + ][ + "attr::screenCapture::value" + ] = "c2NyZWVuIGNhcHR1cmUgc2hvd2luZyBzY29yZSBpbiB0aGUgbWlsbGlvbnM=" + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "does not satisfy proof request restrictions" in str( + context.exception + ) + + indy_proof_req["requested_attributes"]["shenanigans"] = indy_proof_req[ + "requested_attributes" + ].pop("0_screencapture_uuid") + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "Presentation referent" in str(context.exception) + + async def test_receive_pres_bait_and_switch_attr_names(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + indy_proof_req = deepcopy(INDY_PROOF_REQ_NAMES) + indy_proof_req["requested_attributes"]["0_player_uuid"]["restrictions"][0][ + "attr::screenCapture::value" + ] = "c2NyZWVuIGNhcHR1cmUgc2hvd2luZyBzY29yZSBpbiB0aGUgbWlsbGlvbnM=" + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_NAMES, ident="indy") + ], + ) + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "does not satisfy proof request restrictions " in str( + context.exception + ) + + indy_proof_req["requested_attributes"]["shenanigans"] = indy_proof_req[ + "requested_attributes" + ].pop("0_player_uuid") + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_NAMES, ident="indy") + ], + ) + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "Presentation referent" in str(context.exception) + + async def test_receive_pres_bait_and_switch_pred(self): + connection_record = mock.MagicMock(connection_id=CONN_ID) + indy_proof_req = deepcopy(INDY_PROOF_REQ_NAME) + indy_proof_req["requested_predicates"] = {} + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "not in proposal request" in str(context.exception) + + indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"] = { + "name": "shenanigans", + "p_type": ">=", + "p_value": 1000000, + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + } + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "shenanigans not in presentation" in str(context.exception) + + indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"] = { + "name": "highScore", + "p_type": ">=", + "p_value": 8000000, # propose >= 8 million, prove >= 1 million + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + } + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "highScore mismatches proposal request" in str(context.exception) + + indy_proof_req["requested_predicates"]["0_highscore_GE_uuid"] = { + "name": "highScore", + "p_type": ">=", + "p_value": 1000000, + "restrictions": [{"issuer_did": "FFFFFFFFFFFFFFFFFFFFFF"}], # fake issuer + "non_revoked": {"from": NOW, "to": NOW}, + } + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(indy_proof_req, ident="indy") + ], + ) + pres_x = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + + px_rec_dummy = V20PresExRecord( + pres_proposal=pres_proposal.serialize(), + pres_request=pres_request.serialize(), + pres=pres_x.serialize(), + ) + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + with self.assertRaises(V20PresFormatHandlerError) as context: + await self.manager.receive_pres(pres_x, connection_record, None) + assert "does not satisfy proof request restrictions " in str( + context.exception + ) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_verify_pres(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + will_confirm=True, + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy") + ], + ) + px_rec_in = V20PresExRecord( + pres_request=pres_request, + pres=pres, + ) + self.profile.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: + px_rec_out = await self.manager.verify_pres(px_rec_in) + save_ex.assert_called_once() + + assert px_rec_out.state == (V20PresExRecord.STATE_DONE) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_verify_pres_indy_and_dif(self): + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ), + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.DIF.api + ], + ), + ], + will_confirm=True, + request_presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF_REQ_NAME, ident="indy"), + AttachDecorator.data_json(DIF_PRES_REQ, ident="dif"), + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], + ), + V20PresFormat( + attach_id="dif", + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.DIF.api], + ), + ], + presentations_attach=[ + AttachDecorator.data_base64(INDY_PROOF, ident="indy"), + AttachDecorator.data_json(DIF_PRES, ident="dif"), + ], + ) + px_rec_in = V20PresExRecord( + pres_request=pres_request, + pres=pres, + ) + + self.profile.context.injector.bind_instance( + DocumentLoader, custom_document_loader + ) + self.profile.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), mock.patch.object(V20PresExRecord, "save", autospec=True) as save_ex: + px_rec_out = await self.manager.verify_pres(px_rec_in) + save_ex.assert_called_once() + + assert px_rec_out.state == (V20PresExRecord.STATE_DONE) + + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ), mock.patch( + "aries_cloudagent.vc.vc_ld.verify.verify_presentation", + mock.CoroutineMock( + return_value=PresentationVerificationResult(verified=False) + ), + ), async_mock.patch.object( + AnonCredsVerifier, + "verify_presentation", + mock.CoroutineMock( + return_value=PresentationVerificationResult(verified=True) + ), + ), mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex: + px_rec_out = await self.manager.verify_pres(px_rec_in) + save_ex.assert_called_once() + assert px_rec_out.state == (V20PresExRecord.STATE_DONE) + assert px_rec_out.verified == "false" + + async def test_send_pres_ack(self): + px_rec = V20PresExRecord() + + responder = MockResponder() + self.profile.context.injector.bind_instance(BaseResponder, responder) + + await self.manager.send_pres_ack(px_rec) + messages = responder.messages + assert len(messages) == 1 + + px_rec = V20PresExRecord(verified="true") + + responder = MockResponder() + self.profile.context.injector.bind_instance(BaseResponder, responder) + + await self.manager.send_pres_ack(px_rec) + messages = responder.messages + assert len(messages) == 1 + + px_rec = V20PresExRecord(verified="false") + + responder = MockResponder() + self.profile.context.injector.bind_instance(BaseResponder, responder) + + await self.manager.send_pres_ack(px_rec) + messages = responder.messages + assert len(messages) == 1 + + async def test_send_pres_ack_no_responder(self): + px_rec = V20PresExRecord() + + self.profile.context.injector.clear_binding(BaseResponder) + await self.manager.send_pres_ack(px_rec) + + async def test_receive_pres_ack_a(self): + conn_record = mock.MagicMock(connection_id=CONN_ID) + + px_rec_dummy = V20PresExRecord() + message = mock.MagicMock() + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + px_rec_out = await self.manager.receive_pres_ack(message, conn_record) + save_ex.assert_called_once() + + assert px_rec_out.state == V20PresExRecord.STATE_DONE + + async def test_receive_pres_ack_b(self): + conn_record = mock.MagicMock(connection_id=CONN_ID) + + px_rec_dummy = V20PresExRecord() + message = mock.MagicMock(_verification_result="true") + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = px_rec_dummy + px_rec_out = await self.manager.receive_pres_ack(message, conn_record) + save_ex.assert_called_once() + + assert px_rec_out.state == V20PresExRecord.STATE_DONE + assert px_rec_out.verified == "true" + + async def test_receive_problem_report(self): + connection_id = "connection-id" + stored_exchange = V20PresExRecord( + pres_ex_id="dummy-cxid", + connection_id=connection_id, + initiator=V20PresExRecord.INITIATOR_SELF, + role=V20PresExRecord.ROLE_VERIFIER, + state=V20PresExRecord.STATE_PROPOSAL_RECEIVED, + thread_id="dummy-thid", + ) + problem = V20PresProblemReport( + description={ + "en": "Change of plans", + "code": test_module.ProblemReportReason.ABANDONED.value, + } + ) + + with mock.patch.object( + V20PresExRecord, "save", autospec=True + ) as save_ex, mock.patch.object( + V20PresExRecord, + "retrieve_by_tag_filter", + mock.CoroutineMock(), + ) as retrieve_ex, mock.patch.object( + self.profile, + "session", + mock.MagicMock(return_value=self.profile.session()), + ) as session: + retrieve_ex.return_value = stored_exchange + + ret_exchange = await self.manager.receive_problem_report( + problem, connection_id + ) + retrieve_ex.assert_called_once_with( + session.return_value, + {"thread_id": problem._thread_id}, + {"connection_id": connection_id}, + ) + save_ex.assert_called_once() + + assert stored_exchange.state == V20PresExRecord.STATE_ABANDONED + + async def test_receive_problem_report_x(self): + connection_id = "connection-id" + stored_exchange = V20PresExRecord( + pres_ex_id="dummy-cxid", + connection_id=connection_id, + initiator=V20PresExRecord.INITIATOR_SELF, + role=V20PresExRecord.ROLE_VERIFIER, + state=V20PresExRecord.STATE_PROPOSAL_RECEIVED, + thread_id="dummy-thid", + ) + problem = V20PresProblemReport( + description={ + "en": "Change of plans", + "code": test_module.ProblemReportReason.ABANDONED.value, + } + ) + + with mock.patch.object( + V20PresExRecord, + "retrieve_by_tag_filter", + mock.CoroutineMock(), + ) as retrieve_ex: + retrieve_ex.side_effect = StorageNotFoundError("No such record") + + with self.assertRaises(StorageNotFoundError): + await self.manager.receive_problem_report(problem, connection_id) diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py new file mode 100644 index 0000000000..cb9a16f933 --- /dev/null +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py @@ -0,0 +1,2474 @@ +import pytest +from copy import deepcopy +from unittest import IsolatedAsyncioTestCase +from aries_cloudagent.tests import mock +from asynctest import mock as async_mock +from marshmallow import ValidationError +from time import time +from unittest.mock import ANY + +from .....admin.request_context import AdminRequestContext +from .....anoncreds.holder import AnonCredsHolder +from .....indy.models.proof_request import IndyProofReqAttrSpecSchema +from .....anoncreds.verifier import AnonCredsVerifier +from .....ledger.base import BaseLedger +from .....storage.error import StorageNotFoundError +from .....storage.vc_holder.base import VCHolder +from .....storage.vc_holder.vc_record import VCRecord + +from ...dif.pres_exch import SchemaInputDescriptor + +from .. import routes as test_module +from ..messages.pres_format import V20PresFormat +from ..models.pres_exchange import V20PresExRecord + +ISSUER_DID = "NcYxiDXkpYi6ov5FcYDi1e" +S_ID = f"{ISSUER_DID}:2:vidya:1.0" +CD_ID = f"{ISSUER_DID}:3:CL:{S_ID}:tag1" +RR_ID = f"{ISSUER_DID}:4:{CD_ID}:CL_ACCUM:0" +PROOF_REQ_NAME = "name" +PROOF_REQ_VERSION = "1.0" +PROOF_REQ_NONCE = "12345" + +NOW = int(time()) +INDY_PROOF_REQ = { + "name": PROOF_REQ_NAME, + "version": PROOF_REQ_VERSION, + "nonce": PROOF_REQ_NONCE, + "requested_attributes": { + "0_player_uuid": { + "name": "player", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + "1_screencapture_uuid": { + "name": "screenCapture", + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + }, + }, + "requested_predicates": { + "0_highscore_GE_uuid": { + "name": "highScore", + "p_type": ">=", + "p_value": 1000000, + "restrictions": [{"cred_def_id": CD_ID}], + "non_revoked": {"from": NOW, "to": NOW}, + } + }, +} + +DIF_PROOF_REQ = { + "options": { + "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "domain": "4jt78h47fh47", + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "submission_requirements": [ + { + "name": "Citizenship Information", + "rule": "pick", + "min": 1, + "from": "A", + } + ], + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "group": ["A"], + "schema": [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.givenName"], + "purpose": "The claim must be from one of the specified issuers", + "filter": { + "type": "string", + "enum": ["JOHN", "CAI"], + }, + } + ], + }, + } + ], + }, +} + + +DIF_PRES_PROPOSAL = { + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "group": ["A"], + "schema": [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.givenName"], + "purpose": "The claim must be from one of the specified issuers", + "filter": {"type": "string", "enum": ["JOHN", "CAI"]}, + } + ], + }, + } + ] +} + + +class TestPresentProofRoutesAnonCreds(IsolatedAsyncioTestCase): + def setUp(self): + self.context = AdminRequestContext.test_context() + self.context.profile.settings.set_value("wallet.type", "askar-anoncreds") + self.profile = self.context.profile + injector = self.profile.context.injector + + Ledger = mock.MagicMock(BaseLedger, autospec=True) + self.ledger = Ledger() + self.ledger.get_schema = mock.CoroutineMock(return_value=mock.MagicMock()) + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value={"value": {"revocation": {"...": "..."}}} + ) + self.ledger.get_revoc_reg_def = mock.CoroutineMock( + return_value={ + "ver": "1.0", + "id": RR_ID, + "revocDefType": "CL_ACCUM", + "tag": RR_ID.split(":")[-1], + "credDefId": CD_ID, + "value": { + "IssuanceType": "ISSUANCE_BY_DEFAULT", + "maxCredNum": 1000, + "publicKeys": {"accumKey": {"z": "1 ..."}}, + "tailsHash": "3MLjUFQz9x9n5u9rFu8Ba9C5bo4HNFjkPNc54jZPSNaZ", + "tailsLocation": "http://sample.ca/path", + }, + } + ) + self.ledger.get_revoc_reg_delta = mock.CoroutineMock( + return_value=( + { + "ver": "1.0", + "value": {"prevAccum": "1 ...", "accum": "21 ...", "issued": [1]}, + }, + NOW, + ) + ) + self.ledger.get_revoc_reg_entry = mock.CoroutineMock( + return_value=( + { + "ver": "1.0", + "value": {"prevAccum": "1 ...", "accum": "21 ...", "issued": [1]}, + }, + NOW, + ) + ) + injector.bind_instance(BaseLedger, self.ledger) + + self.request_dict = { + "context": self.context, + "outbound_message_router": mock.CoroutineMock(), + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + ) + + async def test_validate(self): + schema = test_module.V20PresProposalByFormatSchema() + schema.validate_fields({"indy": {"attributes": [], "predicates": []}}) + schema.validate_fields({"dif": {"some_dif_criterion": "..."}}) + schema.validate_fields( + { + "indy": {"attributes": [], "predicates": []}, + "dif": {"some_dif_criterion": "..."}, + } + ) + with self.assertRaises(test_module.ValidationError): + schema.validate_fields({}) + with self.assertRaises(test_module.ValidationError): + schema.validate_fields({"veres-one": {"no": "support"}}) + + schema = test_module.V20PresRequestByFormatSchema() + schema.validate_fields({"indy": {"...": "..."}}) + schema.validate_fields({"dif": {"...": "..."}}) + schema.validate_fields({"indy": {"...": "..."}, "dif": {"...": "..."}}) + with self.assertRaises(test_module.ValidationError): + schema.validate_fields({}) + with self.assertRaises(test_module.ValidationError): + schema.validate_fields({"veres-one": {"no": "support"}}) + + schema = test_module.V20PresSpecByFormatRequestSchema() + schema.validate_fields({"indy": {"...": "..."}}) + schema.validate_fields({"dif": {"...": "..."}}) + schema.validate_fields({"indy": {"...": "..."}, "dif": {"...": "..."}}) + with self.assertRaises(test_module.ValidationError): + schema.validate_fields({}) + with self.assertRaises(test_module.ValidationError): + schema.validate_fields({"veres-one": {"no": "support"}}) + + async def test_validate_proof_req_attr_spec(self): + aspec = IndyProofReqAttrSpecSchema() + aspec.validate_fields({"name": "attr0"}) + aspec.validate_fields( + { + "names": ["attr0", "attr1"], + "restrictions": [{"attr::attr1::value": "my-value"}], + } + ) + aspec.validate_fields( + {"name": "attr0", "restrictions": [{"schema_name": "preferences"}]} + ) + with self.assertRaises(ValidationError): + aspec.validate_fields({}) + with self.assertRaises(ValidationError): + aspec.validate_fields({"name": "attr0", "names": ["attr1", "attr2"]}) + with self.assertRaises(ValidationError): + aspec.validate_fields({"names": ["attr1", "attr2"]}) + with self.assertRaises(ValidationError): + aspec.validate_fields({"names": ["attr0", "attr1"], "restrictions": []}) + with self.assertRaises(ValidationError): + aspec.validate_fields({"names": ["attr0", "attr1"], "restrictions": [{}]}) + + async def test_present_proof_list(self): + self.request.query = { + "thread_id": "thread_id_0", + "connection_id": "conn_id_0", + "role": "dummy", + "state": "dummy", + } + + mock_pres_ex_rec_inst = mock.MagicMock( + serialize=mock.MagicMock(return_value={"thread_id": "sample-thread-id"}) + ) + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.query = mock.CoroutineMock( + return_value=[mock_pres_ex_rec_inst] + ) + + await test_module.present_proof_list(self.request) + mock_response.assert_called_once_with( + {"results": [mock_pres_ex_rec_inst.serialize.return_value]} + ) + + async def test_present_proof_list_x(self): + self.request.query = { + "thread_id": "thread_id_0", + "connection_id": "conn_id_0", + "role": "dummy", + "state": "dummy", + } + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.query = mock.CoroutineMock( + side_effect=test_module.StorageError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_list(self.request) + + async def test_present_proof_credentials_list_not_found(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.retrieve_by_id = mock.CoroutineMock() + + # Emulate storage not found (bad presentation exchange id) + mock_pres_ex_rec_cls.retrieve_by_id.side_effect = StorageNotFoundError() + + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.present_proof_credentials_list(self.request) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_present_proof_credentials_x(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + "referent": "myReferent1", + } + self.request.query = {"extra_query": {}} + returned_credentials = [{"name": "Credential1"}, {"name": "Credential2"}] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + async_mock.CoroutineMock( + side_effect=test_module.AnonCredsHolderError() + ) + ) + ), + ) + mock_px_rec = mock.MagicMock(save_error_state=mock.CoroutineMock()) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_credentials_list(self.request) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_present_proof_credentials_list_single_referent(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + "referent": "myReferent1", + } + self.request.query = {"extra_query": {}} + + returned_credentials = [{"name": "Credential1"}, {"name": "Credential2"}] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock(return_value=returned_credentials) + ) + ), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.return_value = mock.MagicMock( + retrieve_by_id=mock.CoroutineMock() + ) + + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with(returned_credentials) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_present_proof_credentials_list_multiple_referents(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + "referent": "myReferent1,myReferent2", + } + self.request.query = {"extra_query": {}} + + returned_credentials = [{"name": "Credential1"}, {"name": "Credential2"}] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock(return_value=returned_credentials) + ) + ), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.return_value = mock.MagicMock( + retrieve_by_id=mock.CoroutineMock() + ) + + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with(returned_credentials) + + async def test_present_proof_credentials_list_dif(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + + returned_credentials = [ + mock.MagicMock(cred_value={"name": "Credential1"}), + mock.MagicMock(cred_value={"name": "Credential2"}), + ] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock( + search_credentials=mock.MagicMock( + return_value=mock.MagicMock( + fetch=mock.CoroutineMock(return_value=returned_credentials) + ) + ) + ), + ) + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": DIF_PROOF_REQ}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with( + [ + {"name": "Credential1", "record_id": ANY}, + {"name": "Credential2", "record_id": ANY}, + ] + ) + + async def test_present_proof_credentials_list_dif_one_of_filter(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + + returned_credentials = [ + mock.MagicMock(cred_value={"name": "Credential1"}, record_id="test_1"), + mock.MagicMock(cred_value={"name": "Credential2"}, record_id="test_2"), + ] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock( + search_credentials=mock.MagicMock( + return_value=mock.MagicMock( + fetch=mock.CoroutineMock(return_value=returned_credentials) + ) + ) + ), + ) + pres_request = deepcopy(DIF_PROOF_REQ) + pres_request["presentation_definition"]["input_descriptors"][0]["schema"] = { + "oneof_filter": [ + [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + [{"uri": "https://www.w3.org/Test#Test"}], + ] + } + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": pres_request}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with( + [ + {"name": "Credential1", "record_id": "test_1"}, + {"name": "Credential2", "record_id": "test_2"}, + ] + ) + + async def test_present_proof_credentials_dif_no_tag_query(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["input_descriptors"][0]["schema"][0][ + "required" + ] = False + test_pd["presentation_definition"]["input_descriptors"][0]["schema"][1][ + "required" + ] = False + returned_credentials = [ + mock.MagicMock(cred_value={"name": "Credential1"}), + mock.MagicMock(cred_value={"name": "Credential2"}), + ] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock( + search_credentials=mock.MagicMock( + return_value=mock.MagicMock( + fetch=mock.CoroutineMock(return_value=returned_credentials) + ) + ) + ), + ) + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with( + [ + {"name": "Credential1", "record_id": ANY}, + {"name": "Credential2", "record_id": ANY}, + ] + ) + + async def test_present_proof_credentials_single_ldp_vp_claim_format(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["format"] = { + "ldp_vp": {"proof_type": ["Ed25519Signature2018"]} + } + del test_pd["presentation_definition"]["input_descriptors"][0]["constraints"][ + "limit_disclosure" + ] + returned_credentials = [ + mock.MagicMock(cred_value={"name": "Credential1"}), + mock.MagicMock(cred_value={"name": "Credential2"}), + ] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock( + search_credentials=mock.MagicMock( + return_value=mock.MagicMock( + fetch=mock.CoroutineMock(return_value=returned_credentials) + ) + ) + ), + ) + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with( + [ + {"name": "Credential1", "record_id": ANY}, + {"name": "Credential2", "record_id": ANY}, + ] + ) + + async def test_present_proof_credentials_double_ldp_vp_claim_format(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["format"] = { + "ldp_vp": {"proof_type": ["BbsBlsSignature2020", "Ed25519Signature2018"]} + } + del test_pd["presentation_definition"]["input_descriptors"][0]["constraints"][ + "limit_disclosure" + ] + returned_credentials = [ + mock.MagicMock(cred_value={"name": "Credential1"}), + mock.MagicMock(cred_value={"name": "Credential2"}), + ] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock( + search_credentials=mock.MagicMock( + return_value=mock.MagicMock( + fetch=mock.CoroutineMock(return_value=returned_credentials) + ) + ) + ), + ) + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with( + [ + {"name": "Credential1", "record_id": ANY}, + {"name": "Credential2", "record_id": ANY}, + ] + ) + + async def test_present_proof_credentials_single_ldp_vp_error(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["format"] = { + "ldp_vp": {"proof_type": ["test"]} + } + del test_pd["presentation_definition"]["input_descriptors"][0]["constraints"][ + "limit_disclosure" + ] + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock(search_credentials=mock.CoroutineMock()), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_credentials_list(self.request) + + async def test_present_proof_credentials_double_ldp_vp_error(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["format"] = { + "ldp_vp": {"proof_type": ["test1", "test2"]} + } + del test_pd["presentation_definition"]["input_descriptors"][0]["constraints"][ + "limit_disclosure" + ] + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock(search_credentials=mock.CoroutineMock()), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_credentials_list(self.request) + + async def test_present_proof_credentials_list_limit_disclosure_no_bbs(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["format"] = { + "ldp_vp": {"proof_type": ["Ed25519Signature2018"]} + } + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock(search_credentials=mock.CoroutineMock()), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_credentials_list(self.request) + + async def test_present_proof_credentials_no_ldp_vp(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["format"] = { + "ldp_vc": {"proof_type": ["test"]} + } + del test_pd["presentation_definition"]["input_descriptors"][0]["constraints"][ + "limit_disclosure" + ] + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock(search_credentials=mock.CoroutineMock()), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_credentials_list(self.request) + + async def test_present_proof_credentials_list_schema_uri(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + test_pd = deepcopy(DIF_PROOF_REQ) + test_pd["presentation_definition"]["input_descriptors"][0]["schema"][0][ + "uri" + ] = "https://example.org/test.json" + test_pd["presentation_definition"]["input_descriptors"][0]["schema"].pop(1) + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": test_pd}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + returned_credentials = [ + mock.MagicMock(cred_value={"name": "Credential1"}), + mock.MagicMock(cred_value={"name": "Credential2"}), + ] + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock( + search_credentials=mock.MagicMock( + return_value=mock.MagicMock( + fetch=mock.CoroutineMock(return_value=returned_credentials) + ) + ) + ), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + await test_module.present_proof_credentials_list(self.request) + mock_response.assert_called_once_with( + [ + {"name": "Credential1", "record_id": ANY}, + {"name": "Credential2", "record_id": ANY}, + ] + ) + + async def test_present_proof_credentials_list_dif_error(self): + self.request.match_info = { + "pres_ex_id": "123-456-789", + } + self.request.query = {"extra_query": {}} + + self.profile.context.injector.bind_instance( + AnonCredsHolder, + async_mock.MagicMock( + get_credentials_for_presentation_request_by_referent=( + mock.CoroutineMock() + ) + ), + ) + self.profile.context.injector.bind_instance( + VCHolder, + mock.MagicMock( + search_credentials=mock.MagicMock( + return_value=mock.MagicMock( + fetch=mock.CoroutineMock( + side_effect=test_module.StorageNotFoundError() + ) + ) + ) + ), + ) + record = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": DIF_PROOF_REQ}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + with self.assertRaises(test_module.web.HTTPBadRequest): + mock_pres_ex_rec_cls.retrieve_by_id.return_value = record + await test_module.present_proof_credentials_list(self.request) + + async def test_present_proof_retrieve(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_pres_ex_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock( + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ) + ) + ) + + await test_module.present_proof_retrieve(self.request) + mock_response.assert_called_once_with({"thread_id": "sample-thread-id"}) + + async def test_present_proof_retrieve_not_found(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError() + ) + + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.present_proof_retrieve(self.request) + + async def test_present_proof_retrieve_x(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + mock_pres_ex_rec_inst = mock.MagicMock( + connection_id="abc123", + thread_id="thid123", + serialize=mock.MagicMock(side_effect=test_module.BaseModelError()), + save_error_state=mock.CoroutineMock(), + ) + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls: + mock_pres_ex_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_pres_ex_rec_inst + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_retrieve(self.request) + + async def test_present_proof_send_proposal(self): + self.request.json = mock.CoroutineMock( + return_value={ + "connection_id": "dummy-conn-id", + "presentation_proposal": { + V20PresFormat.Format.INDY.api: INDY_PROOF_REQ + }, + } + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_conn_rec.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock(is_ready=True) + ) + mock_px_rec_inst = mock.MagicMock() + mock_pres_mgr.return_value.create_exchange_for_proposal = ( + mock.CoroutineMock(return_value=mock_px_rec_inst) + ) + + await test_module.present_proof_send_proposal(self.request) + mock_response.assert_called_once_with( + mock_px_rec_inst.serialize.return_value + ) + + async def test_present_proof_send_proposal_no_conn_record(self): + self.request.json = mock.CoroutineMock() + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec: + mock_conn_rec.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_proposal(self.request) + + async def test_present_proof_send_proposal_not_ready(self): + self.request.json = mock.CoroutineMock() + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresProposal", autospec=True + ) as mock_proposal: + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock(is_ready=False) + ) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.present_proof_send_proposal(self.request) + + async def test_present_proof_send_proposal_x(self): + self.request.json = mock.CoroutineMock() + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr: + mock_pres_mgr.return_value.create_exchange_for_proposal = ( + mock.CoroutineMock( + return_value=mock.MagicMock( + serialize=mock.MagicMock( + side_effect=test_module.StorageError() + ), + save_error_state=mock.CoroutineMock(), + ) + ) + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_proposal(self.request) + + async def test_present_proof_create_request(self): + indy_proof_req = deepcopy(INDY_PROOF_REQ) + indy_proof_req.pop("nonce") # exercise _add_nonce() + + self.request.json = mock.CoroutineMock( + return_value={ + "comment": "dummy", + "presentation_request": {V20PresFormat.Format.INDY.api: indy_proof_req}, + } + ) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresRequest", autospec=True + ) as mock_pres_request, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_px_rec_inst = mock.MagicMock( + serialize=mock.MagicMock(return_value={"thread_id": "sample-thread-id"}) + ) + mock_pres_mgr_inst = mock.MagicMock( + create_exchange_for_request=mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + await test_module.present_proof_create_request(self.request) + mock_response.assert_called_once_with( + mock_px_rec_inst.serialize.return_value + ) + + async def test_present_proof_create_request_x(self): + self.request.json = mock.CoroutineMock( + return_value={ + "comment": "dummy", + "presentation_request": {V20PresFormat.Format.INDY.api: INDY_PROOF_REQ}, + } + ) + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresRequest", autospec=True + ) as mock_pres_request, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_px_rec_inst = mock.MagicMock() + mock_pres_mgr_inst = mock.MagicMock( + create_exchange_for_request=mock.CoroutineMock( + return_value=mock.MagicMock( + serialize=mock.MagicMock( + side_effect=test_module.StorageError() + ), + save_error_state=mock.CoroutineMock(), + ) + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_create_request(self.request) + + async def test_present_proof_send_free_request(self): + self.request.json = mock.CoroutineMock( + return_value={ + "connection_id": "dummy", + "comment": "dummy", + "presentation_request": {V20PresFormat.Format.INDY.api: INDY_PROOF_REQ}, + } + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresRequest", autospec=True + ) as mock_pres_request, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_pres_ex_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock() + mock_px_rec_inst = mock.MagicMock( + serialize=mock.MagicMock({"thread_id": "sample-thread-id"}) + ) + + mock_pres_mgr_inst = mock.MagicMock( + create_exchange_for_request=mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + await test_module.present_proof_send_free_request(self.request) + mock_response.assert_called_once_with( + mock_px_rec_inst.serialize.return_value + ) + + async def test_present_proof_send_free_request_not_found(self): + self.request.json = mock.CoroutineMock(return_value={"connection_id": "dummy"}) + + with mock.patch.object( + test_module, "ConnRecord", mock.MagicMock() + ) as mock_conn_rec_cls: + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_free_request(self.request) + + async def test_present_proof_send_free_request_not_ready(self): + self.request.json = mock.CoroutineMock( + return_value={"connection_id": "dummy", "proof_request": {}} + ) + + with mock.patch.object( + test_module, "ConnRecord", mock.MagicMock() + ) as mock_conn_rec_cls: + mock_conn_rec_inst = mock.MagicMock(is_ready=False) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.present_proof_send_free_request(self.request) + + async def test_present_proof_send_free_request_x(self): + self.request.json = mock.CoroutineMock( + return_value={ + "connection_id": "dummy", + "comment": "dummy", + "presentation_request": {V20PresFormat.Format.INDY.api: INDY_PROOF_REQ}, + } + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresRequest", autospec=True + ) as mock_pres_request, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_conn_rec_inst = mock.MagicMock() + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + mock_px_rec_inst = mock.MagicMock( + serialize=mock.MagicMock(return_value={"thread_id": "sample-thread-id"}) + ) + mock_pres_mgr_inst = mock.MagicMock( + create_exchange_for_request=mock.CoroutineMock( + return_value=mock.MagicMock( + serialize=mock.MagicMock( + side_effect=test_module.StorageError() + ), + save_error_state=mock.CoroutineMock(), + ) + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_free_request(self.request) + + async def test_present_proof_send_bound_request(self): + self.request.json = mock.CoroutineMock(return_value={"trace": False}) + self.request.match_info = {"pres_ex_id": "dummy"} + + self.profile.context.injector.bind_instance( + BaseLedger, + mock.MagicMock( + __aenter__=mock.CoroutineMock(), + __aexit__=mock.CoroutineMock(), + ), + ) + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_PROPOSAL_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + mock_conn_rec_inst = mock.MagicMock( + is_ready=True, + ) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + mock_pres_request = mock.MagicMock() + + mock_pres_mgr_inst = mock.MagicMock( + create_bound_request=mock.CoroutineMock( + return_value=(mock_px_rec_inst, mock_pres_request) + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + await test_module.present_proof_send_bound_request(self.request) + mock_response.assert_called_once_with( + mock_px_rec_inst.serialize.return_value + ) + + async def test_present_proof_send_bound_request_not_found(self): + self.request.json = mock.CoroutineMock(return_value={"trace": False}) + self.request.match_info = {"pres_ex_id": "dummy"} + + self.profile.context.injector.bind_instance( + BaseLedger, + mock.MagicMock( + __aenter__=mock.CoroutineMock(), + __aexit__=mock.CoroutineMock(), + ), + ) + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_PROPOSAL_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_bound_request(self.request) + + async def test_present_proof_send_bound_request_not_ready(self): + self.request.json = mock.CoroutineMock(return_value={"trace": False}) + self.request.match_info = {"pres_ex_id": "dummy"} + + self.profile.context.injector.bind_instance( + BaseLedger, + mock.MagicMock( + __aenter__=mock.CoroutineMock(), + __aexit__=mock.CoroutineMock(), + ), + ) + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_PROPOSAL_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + mock_conn_rec_inst = mock.MagicMock( + is_ready=False, + ) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.present_proof_send_bound_request(self.request) + + async def test_present_proof_send_bound_request_px_rec_not_found(self): + self.request.json = mock.CoroutineMock(return_value={"trace": False}) + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError("no such record") + ) + with self.assertRaises(test_module.web.HTTPNotFound) as context: + await test_module.present_proof_send_bound_request(self.request) + assert "no such record" in str(context.exception) + + async def test_present_proof_send_bound_request_bad_state(self): + self.request.json = mock.CoroutineMock(return_value={"trace": False}) + self.request.match_info = {"pres_ex_id": "dummy"} + + self.profile.context.injector.bind_instance( + BaseLedger, + mock.MagicMock( + __aenter__=mock.CoroutineMock(), + __aexit__=mock.CoroutineMock(), + ), + ) + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_DONE, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_bound_request(self.request) + + async def test_present_proof_send_bound_request_x(self): + self.request.json = mock.CoroutineMock(return_value={"trace": False}) + self.request.match_info = {"pres_ex_id": "dummy"} + + self.profile.context.injector.bind_instance( + BaseLedger, + mock.MagicMock( + __aenter__=mock.CoroutineMock(), + __aexit__=mock.CoroutineMock(), + ), + ) + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_PROPOSAL_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + save_error_state=mock.CoroutineMock(), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + mock_conn_rec_inst = mock.MagicMock( + is_ready=True, + ) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + mock_pres_mgr_inst = mock.MagicMock( + create_bound_request=mock.CoroutineMock( + side_effect=test_module.StorageError() + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_bound_request(self.request) + + async def test_present_proof_send_presentation(self): + self.request.json = mock.CoroutineMock( + return_value={ + "indy": { + "comment": "dummy", + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + } + } + ) + self.request.match_info = { + "pres_ex_id": "dummy", + } + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls, mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_REQUEST_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + mock_pres_mgr_inst = mock.MagicMock( + create_pres=mock.CoroutineMock( + return_value=(mock_px_rec_inst, mock.MagicMock()) + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + await test_module.present_proof_send_presentation(self.request) + mock_response.assert_called_once_with( + mock_px_rec_inst.serialize.return_value + ) + + async def test_present_proof_send_presentation_dif(self): + proof_req = deepcopy(DIF_PROOF_REQ) + proof_req["issuer_id"] = "test123" + self.request.json = mock.CoroutineMock( + return_value={ + "dif": proof_req, + } + ) + self.request.match_info = { + "pres_ex_id": "dummy", + } + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls, mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_REQUEST_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + mock_pres_mgr_inst = mock.MagicMock( + create_pres=mock.CoroutineMock( + return_value=(mock_px_rec_inst, mock.MagicMock()) + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + await test_module.present_proof_send_presentation(self.request) + mock_response.assert_called_once_with( + mock_px_rec_inst.serialize.return_value + ) + + async def test_present_proof_send_presentation_dif_error(self): + self.request.json = mock.CoroutineMock(return_value={"dif": DIF_PROOF_REQ}) + self.request.match_info = { + "pres_ex_id": "dummy", + } + px_rec_instance = V20PresExRecord( + state="request-received", + role="prover", + pres_proposal=None, + pres_request={ + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/2.0/request-presentation", + "@id": "6ae00c6c-87fa-495a-b546-5f5953817c92", + "comment": "string", + "formats": [ + { + "attach_id": "dif", + "format": "dif/presentation-exchange/definitions@v1.0", + } + ], + "request_presentations~attach": [ + { + "@id": "dif", + "mime-type": "application/json", + "data": {"json": DIF_PROOF_REQ}, + } + ], + "will_confirm": True, + }, + pres=None, + verified=None, + auto_present=False, + error_msg=None, + ) + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls, mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=px_rec_instance + ) + + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + mock_pres_mgr_inst = mock.MagicMock( + create_pres=mock.CoroutineMock(side_effect=test_module.LedgerError()) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_presentation(self.request) + mock_response.assert_called_once_with(px_rec_instance.serialize()) + + async def test_present_proof_send_presentation_px_rec_not_found(self): + self.request.json = mock.CoroutineMock( + return_value={ + "indy": { + "comment": "dummy", + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + } + } + ) + self.request.match_info = { + "pres_ex_id": "dummy", + } + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError("no such record") + ) + + with self.assertRaises(test_module.web.HTTPNotFound) as context: + await test_module.present_proof_send_presentation(self.request) + assert "no such record" in str(context.exception) + + async def test_present_proof_send_presentation_not_found(self): + self.request.json = mock.CoroutineMock( + return_value={ + "indy": { + "comment": "dummy", + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + } + } + ) + self.request.match_info = { + "pres_ex_id": "dummy", + } + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_REQUEST_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_presentation(self.request) + + async def test_present_proof_send_presentation_not_ready(self): + self.request.json = mock.CoroutineMock( + return_value={ + "indy": { + "comment": "dummy", + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + } + } + ) + self.request.match_info = { + "pres_ex_id": "dummy", + } + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_REQUEST_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock(is_ready=False) + ) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.present_proof_send_presentation(self.request) + + async def test_present_proof_send_presentation_bad_state(self): + self.request.json = mock.CoroutineMock( + return_value={ + "indy": { + "comment": "dummy", + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + } + } + ) + self.request.match_info = { + "pres_ex_id": "dummy", + } + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id=None, + state=test_module.V20PresExRecord.STATE_DONE, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_presentation(self.request) + + async def test_present_proof_send_presentation_x(self): + self.request.json = mock.CoroutineMock( + return_value={ + "indy": { + "comment": "dummy", + "self_attested_attributes": {}, + "requested_attributes": {}, + "requested_predicates": {}, + } + } + ) + self.request.match_info = { + "pres_ex_id": "dummy", + } + self.profile.context.injector.bind_instance( + AnonCredsVerifier, + async_mock.MagicMock( + verify_presentation=async_mock.CoroutineMock(), + ), + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls, mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_REQUEST_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + save_error_state=mock.CoroutineMock(), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + mock_pres_mgr_inst = mock.MagicMock( + create_pres=mock.CoroutineMock( + side_effect=[ + test_module.LedgerError(), + test_module.StorageError(), + ] + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + with self.assertRaises(test_module.web.HTTPBadRequest): # ledger error + await test_module.present_proof_send_presentation(self.request) + with self.assertRaises(test_module.web.HTTPBadRequest): # storage error + await test_module.present_proof_send_presentation(self.request) + + async def test_present_proof_verify_presentation(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_PRESENTATION_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + mock_pres_mgr_inst = mock.MagicMock( + verify_pres=mock.CoroutineMock(return_value=mock_px_rec_inst) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + await test_module.present_proof_verify_presentation(self.request) + mock_response.assert_called_once_with({"thread_id": "sample-thread-id"}) + + async def test_present_proof_verify_presentation_px_rec_not_found(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError("no such record") + ) + + with self.assertRaises(test_module.web.HTTPNotFound) as context: + await test_module.present_proof_verify_presentation(self.request) + assert "no such record" in str(context.exception) + + async def test_present_proof_verify_presentation_bad_state(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_DONE, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_verify_presentation(self.request) + + async def test_present_proof_verify_presentation_x(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec_cls, mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec_cls, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_px_rec_inst = mock.MagicMock( + connection_id="dummy", + state=test_module.V20PresExRecord.STATE_PRESENTATION_RECEIVED, + serialize=mock.MagicMock( + return_value={"thread_id": "sample-thread-id"} + ), + save_error_state=mock.CoroutineMock(), + ) + mock_px_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_px_rec_inst + ) + mock_conn_rec_inst = mock.MagicMock(is_ready=True) + mock_conn_rec_cls.retrieve_by_id = mock.CoroutineMock( + return_value=mock_conn_rec_inst + ) + + mock_pres_mgr_inst = mock.MagicMock( + verify_pres=mock.CoroutineMock( + side_effect=[ + test_module.LedgerError(), + test_module.StorageError(), + ] + ) + ) + mock_pres_mgr_cls.return_value = mock_pres_mgr_inst + + with self.assertRaises(test_module.web.HTTPBadRequest): # ledger error + await test_module.present_proof_verify_presentation(self.request) + with self.assertRaises(test_module.web.HTTPBadRequest): # storage error + await test_module.present_proof_verify_presentation(self.request) + + async def test_present_proof_problem_report(self): + self.request.json = mock.CoroutineMock( + return_value={"description": "Did I say no problem? I meant 'No! Problem.'"} + ) + self.request.match_info = {"pres_ex_id": "dummy"} + magic_report = mock.MagicMock() + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "problem_report_for_record", mock.MagicMock() + ) as mock_problem_report, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec, mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_px_rec.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock(save_error_state=mock.CoroutineMock()) + ) + mock_problem_report.return_value = magic_report + + await test_module.present_proof_problem_report(self.request) + + self.request["outbound_message_router"].assert_awaited_once() + mock_response.assert_called_once_with({}) + + async def test_present_proof_problem_report_bad_pres_ex_id(self): + self.request.json = mock.CoroutineMock( + return_value={"description": "Did I say no problem? I meant 'No! Problem.'"} + ) + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec: + mock_px_rec.retrieve_by_id = mock.CoroutineMock( + side_effect=test_module.StorageNotFoundError() + ) + + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.present_proof_problem_report(self.request) + + async def test_present_proof_problem_report_x(self): + self.request.json = mock.CoroutineMock( + return_value={"description": "Did I say no problem? I meant 'No! Problem.'"} + ) + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresManager", autospec=True + ) as mock_pres_mgr_cls, mock.patch.object( + test_module, "problem_report_for_record", mock.MagicMock() + ) as mock_problem_report, mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec: + mock_px_rec.retrieve_by_id = mock.CoroutineMock( + side_effect=test_module.StorageError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_problem_report(self.request) + + async def test_present_proof_remove(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec, mock.patch.object( + test_module.web, "json_response", mock.MagicMock() + ) as mock_response: + mock_px_rec.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock( + state=test_module.V20PresExRecord.STATE_DONE, + connection_id="dummy", + delete_record=mock.CoroutineMock(), + ) + ) + + await test_module.present_proof_remove(self.request) + mock_response.assert_called_once_with({}) + + async def test_present_proof_remove_px_rec_not_found(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec: + mock_px_rec.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError() + ) + + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.present_proof_remove(self.request) + + async def test_present_proof_remove_x(self): + self.request.match_info = {"pres_ex_id": "dummy"} + + with mock.patch.object( + test_module, "V20PresExRecord", autospec=True + ) as mock_px_rec: + mock_px_rec.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock( + state=test_module.V20PresExRecord.STATE_DONE, + connection_id="dummy", + delete_record=mock.CoroutineMock( + side_effect=test_module.StorageError() + ), + ) + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_remove(self.request) + + async def test_register(self): + mock_app = mock.MagicMock() + mock_app.add_routes = mock.MagicMock() + + await test_module.register(mock_app) + mock_app.add_routes.assert_called_once() + + async def test_post_process_routes(self): + mock_app = mock.MagicMock(_state={"swagger_dict": {}}) + test_module.post_process_routes(mock_app) + assert "tags" in mock_app._state["swagger_dict"] + + def test_format_attach_dif(self): + req_dict = {"dif": DIF_PROOF_REQ} + pres_req_dict = test_module._formats_attach( + by_format=req_dict, + msg_type="present-proof/2.0/request-presentation", + spec="request_presentations", + ) + assert pres_req_dict.get("formats")[0].attach_id == "dif" + assert ( + pres_req_dict.get("request_presentations_attach")[0].data.json_ + == DIF_PROOF_REQ + ) + + async def test_process_vcrecords_return_list(self): + cred_list = [ + VCRecord( + contexts=[ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + expanded_types=[ + "https://www.w3.org/2018/credentials#VerifiableCredential", + "https://example.org/examples#UniversityDegreeCredential", + ], + issuer_id="https://example.edu/issuers/565049", + subject_ids=[ + "did:sov:LjgpST2rjsoxYegQDRm7EL", + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + ], + proof_types=["BbsBlsSignature2020"], + schema_ids=["https://example.org/examples/degree.json"], + cred_value={"...": "..."}, + given_id="http://example.edu/credentials/3732", + cred_tags={"some": "tag"}, + record_id="test1", + ), + VCRecord( + contexts=[ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + expanded_types=[ + "https://www.w3.org/2018/credentials#VerifiableCredential", + "https://example.org/examples#UniversityDegreeCredential", + ], + issuer_id="https://example.edu/issuers/565049", + subject_ids=[ + "did:sov:LjgpST2rjsoxYegQDRm7EL", + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + ], + proof_types=["BbsBlsSignature2020"], + schema_ids=["https://example.org/examples/degree.json"], + cred_value={"...": "..."}, + given_id="http://example.edu/credentials/3732", + cred_tags={"some": "tag"}, + record_id="test2", + ), + ] + record_ids = {"test1"} + ( + returned_cred_list, + returned_record_ids, + ) = await test_module.process_vcrecords_return_list(cred_list, record_ids) + assert len(returned_cred_list) == 1 + assert len(returned_record_ids) == 2 + assert returned_cred_list[0].record_id == "test2" + + async def test_retrieve_uri_list_from_schema_filter(self): + test_schema_filter = [ + [ + SchemaInputDescriptor(uri="test123"), + SchemaInputDescriptor(uri="test321", required=True), + ] + ] + test_one_of_uri_groups = await test_module.retrieve_uri_list_from_schema_filter( + test_schema_filter + ) + assert test_one_of_uri_groups == [["test123", "test321"]] + + async def test_send_presentation_no_specification(self): + self.request.json = mock.CoroutineMock(return_value={"comment": "test"}) + self.request.match_info = { + "pres_ex_id": "dummy", + } + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.present_proof_send_presentation(self.request) + + async def test_v20presentationsendreqschema(self): + test_input = { + "comment": "string", + "connection_id": "631522e9-ca17-4c88-9a4c-d1cad35e463a", + "presentation_request": { + "dif": { + "_schema": [ + { + "uri": "https://www.w3.org/2018/credentials/#VerifiableCredential" + } + ], + "presentation_definition": { + "format": {"ldp_vp": {"proof_type": "BbsBlsSignature2020"}}, + "id": "fa2c4a76-c7bd-4313-a0f8-d9f5979c1fd2", + "input_descriptors": [ + { + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials/#VerifiableCredential" + } + ], + "constraints": { + "fields": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "path": ["$.credentialSubject.id"], + } + ], + "is_holder": [ + { + "directive": "required", + "field_id": [ + "3fa85f64-5717-4562-b3fc-2c963f66afa6" + ], + } + ], + "limit_disclosure": "required", + }, + "id": "XXXXXXX", + "name": "XXXXXXX", + } + ], + }, + } + }, + } + with self.assertRaises(TypeError): + test_module.V20PresSendRequestRequestSchema.load(test_input) diff --git a/demo/features/0160-connection.feature b/demo/features/0160-connection.feature index beccc4df34..40259e5114 100644 --- a/demo/features/0160-connection.feature +++ b/demo/features/0160-connection.feature @@ -18,3 +18,4 @@ Feature: RFC 0160 Aries agent connection functions | --public-did --mediation | --mediation | | --public-did --multitenant | --multitenant | | --public-did --mediation --multitenant | --mediation --multitenant | + | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | diff --git a/demo/features/0453-issue-credential.feature b/demo/features/0453-issue-credential.feature index 3228424a15..79378198a0 100644 --- a/demo/features/0453-issue-credential.feature +++ b/demo/features/0453-issue-credential.feature @@ -1,50 +1,6 @@ @RFC0453 Feature: RFC 0453 Aries agent issue credential - @T004-RFC0453 @GHA-Anoncreds-skip-revoc - Scenario Outline: Using anoncreds, Issue a credential with revocation, with the Issuer beginning with an offer, and then revoking the credential - Given we have "2" agents - | name | role | capabilities | - | Acme | issuer | | - | Bob | holder | | - And "Acme" and "Bob" have an existing connection - And Using anoncreds, "Bob" has an issued credential from "Acme" - Then Using anoncreds, "Acme" revokes the credential - And "Bob" has the credential issued - - Examples: - | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | - | --revocation --cred-type anoncreds --public-did | | anoncreds-testing | Data_AC_NormalizedValues | - | --revocation --cred-type anoncreds --public-did --did-exchange | --did-exchange| anoncreds-testing | Data_AC_NormalizedValues | - | --revocation --cred-type anoncreds --public-did --multitenant | --multitenant | anoncreds-testing | Data_AC_NormalizedValues | - - @T004-RFC0453 @GHA - Scenario Outline: Using anoncreds, create a schema/cred def in preparation for Issuing a credential - Given we have "2" agents - | name | role | capabilities | - | Acme | issuer | | - | Bob | holder | | - And "Acme" and "Bob" have an existing connection - And Using anoncreds, "Acme" is ready to issue a credential for - - Examples: - | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | - | --cred-type anoncreds --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | anoncreds-testing | Data_AC_NormalizedValues | - - @T004-RFC0453 @GHA-Anoncreds-test1 - Scenario Outline: Using anoncreds, Issue a credential, with the Issuer beginning with an offer - Given we have "2" agents - | name | role | capabilities | - | Acme | issuer | | - | Bob | holder | | - And "Acme" and "Bob" have an existing connection - And Using anoncreds, "Bob" has an issued credential from "Acme" - Then "Bob" has the credential issued - - Examples: - | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | - | --cred-type anoncreds --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | anoncreds-testing | Data_AC_NormalizedValues | - @T003-RFC0453 @GHA Scenario Outline: Issue a credential with the Issuer beginning with an offer Given we have "2" agents @@ -62,6 +18,7 @@ Feature: RFC 0453 Aries agent issue credential | --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | | --public-did --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | | --public-did --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | + | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | @T003-RFC0453 @GHA Scenario Outline: Holder accepts a deleted credential offer @@ -77,6 +34,7 @@ Feature: RFC 0453 Aries agent issue credential Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | | --public-did | | driverslicense | Data_DL_NormalizedValues | + | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | #| --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | #| --public-did --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | #| --public-did --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | @@ -94,6 +52,7 @@ Feature: RFC 0453 Aries agent issue credential Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | | --public-did | | driverslicense | Data_DL_NormalizedValues | + | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | #| --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | #| --public-did --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | #| --public-did --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | @@ -115,6 +74,7 @@ Feature: RFC 0453 Aries agent issue credential Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | | --public-did --cred-type json-ld | | driverslicense | Data_DL_NormalizedValues | + | --public-did --cred-type json-ld --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | # | --public-did --cred-type json-ld --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | # | --public-did --cred-type json-ld --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | # | --public-did --cred-type json-ld --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | @@ -137,6 +97,7 @@ Feature: RFC 0453 Aries agent issue credential | --public-did --cred-type json-ld --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | | --public-did --cred-type json-ld --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | | --public-did --cred-type json-ld --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | + | --public-did --cred-type json-ld --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | @T003.1-RFC0453 @GHA @@ -157,6 +118,7 @@ Feature: RFC 0453 Aries agent issue credential | --public-did --cred-type json-ld --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | | --public-did --cred-type json-ld --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | | --public-did --cred-type json-ld --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | + | --public-did --cred-type json-ld --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | @T004-RFC0453 @GHA diff --git a/demo/features/0454-present-proof.feature b/demo/features/0454-present-proof.feature index 6d5ec1ed84..44f36c5536 100644 --- a/demo/features/0454-present-proof.feature +++ b/demo/features/0454-present-proof.feature @@ -16,6 +16,7 @@ Feature: RFC 0454 Aries agent present proof | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | | Faber | --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Faber | --public-did --did-exchange | --did-exchange | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T001.1-RFC0454 @@ -36,6 +37,7 @@ Feature: RFC 0454 Aries agent present proof | Acme | --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Faber | --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Acme | --public-did --mediation --multitenant | --mediation --multitenant | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T001.2-RFC0454 @GHA @@ -55,6 +57,7 @@ Feature: RFC 0454 Aries agent present proof | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | | Acme | --public-did --cred-type json-ld | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Faber | --public-did --cred-type json-ld --did-exchange | --did-exchange | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --public-did --cred-type json-ld --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T002-RFC0454 @GHA diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index 83c4442fd1..fc20658664 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -54,31 +54,6 @@ def step_impl(context, issuer, schema_name): context.cred_def_id = cred_def_id -@given('Using anoncreds, "{issuer}" is ready to issue a credential for {schema_name}') -@then('Using anoncreds, "{issuer}" is ready to issue a credential for {schema_name}') -def step_impl(context, issuer, schema_name): - agent = context.active_agents[issuer] - - schema_info = read_schema_data(schema_name) - - cred_def_id = aries_container_create_schema_cred_def( - agent["agent"], - schema_info["schema"]["name"], - schema_info["schema"]["attrNames"], - version=schema_info["schema"]["version"], - ) - - # confirm the cred def was actually created - async_sleep(2.0) - cred_def_saved = agent_container_GET( - agent["agent"], f"/anoncreds/credential-definition/{cred_def_id}" - ) - assert cred_def_saved - - context.schema_name = schema_name - context.cred_def_id = cred_def_id - - @given('"{issuer}" offers a credential with data {credential_data}') @when('"{issuer}" offers a credential with data {credential_data}') def step_impl(context, issuer, credential_data): @@ -219,59 +194,6 @@ def step_impl(context, holder): print("cred_exchange:", json.dumps(cred_exchange)) -@given('Using anoncreds, "{holder}" revokes the credential') -@when('Using anoncreds, "{holder}" revokes the credential') -@then('Using anoncreds, "{holder}" revokes the credential') -def step_impl(context, holder): - agent = context.active_agents[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"] - ) - # refresh it... - cred_exchange = agent_container_GET( - agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id - ) - context.cred_exchange = cred_exchange - print("cred_exchange:", json.dumps(cred_exchange)) - # we need the connection id... - connection_id = ( - cred_exchange["connection_id"] - if "connection_id" in cred_exchange - else cred_exchange["cred_ex_record"]["connection_id"] - ) - - cred_exchange = agent_container_POST( - agent["agent"], - "/anoncreds/revoke", - data={ - "cred_ex_id": cred_ex_id, - "connection_id": connection_id, - "notify": True, - }, - ) - async_sleep(3.0) - # publish the revocations - publish_response = agent_container_POST( - agent["agent"], - "/anoncreds/publish-revocations", - data={}, - ) - print("publish_response:", json.dumps(publish_response)) - # pause for a few seconds - async_sleep(3.0) - # refresh it... - cred_exchange = agent_container_GET( - agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id - ) - context.cred_exchange = cred_exchange - # print("cred_exchange:", json.dumps(cred_exchange)) - - @given('"{holder}" successfully revoked the credential') @when('"{holder}" successfully revoked the credential') @then('"{holder}" successfully revoked the credential') @@ -762,26 +684,3 @@ def step_impl(context, holder, schema_name, credential_data, issuer): + """" has the credential issued """ ) - - -@given( - 'Using anoncreds, "{holder}" has an issued {schema_name} credential {credential_data} from "{issuer}"' -) -def step_impl(context, holder, schema_name, credential_data, issuer): - context.execute_steps( - ''' - Given Using anoncreds, "''' - + issuer - + """" is ready to issue a credential for """ - + schema_name - + ''' - When "''' - + issuer - + """" offers a credential with data """ - + credential_data - + ''' - Then "''' - + holder - + """" has the credential issued - """ - ) diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index 5f52f8f74b..2916fffb27 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -21,9 +21,9 @@ connect_wallet_to_mediator, start_endorser_agent, connect_wallet_to_endorser, + WALLET_TYPE_INDY, CRED_FORMAT_INDY, CRED_FORMAT_JSON_LD, - CRED_FORMAT_ANONCREDS, DID_METHOD_KEY, KEY_TYPE_BLS, ) @@ -643,7 +643,7 @@ async def create_schema_and_cred_def( schema_attrs, revocation, version=None, - cred_type=CRED_FORMAT_INDY, + wallet_type=WALLET_TYPE_INDY, ): with log_timer("Publish schema/cred def duration:"): log_status("#3/4 Create a new schema/cred def on the ledger") @@ -665,7 +665,7 @@ async def create_schema_and_cred_def( schema_attrs, support_revocation=revocation, revocation_registry_size=TAILS_FILE_COUNT if revocation else None, - cred_type=cred_type, + wallet_type=wallet_type, ) return cred_def_id @@ -900,14 +900,16 @@ async def create_schema_and_cred_def( ): if not self.public_did: raise Exception("Can't create a schema/cred def without a public DID :-(") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_ANONCREDS]: + if self.cred_type in [ + CRED_FORMAT_INDY, + ]: # need to redister schema and cred def on the ledger self.cred_def_id = await self.agent.create_schema_and_cred_def( schema_name, schema_attrs, self.revocation, version=version, - cred_type=self.cred_type, + wallet_type=self.agent.wallet_type, ) return self.cred_def_id elif self.cred_type == CRED_FORMAT_JSON_LD: @@ -924,7 +926,9 @@ async def issue_credential( ): log_status("#13 Issue credential offer to X") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_ANONCREDS]: + if self.cred_type in [ + CRED_FORMAT_INDY, + ]: cred_preview = { "@type": CRED_PREVIEW_TYPE, "attributes": cred_attrs, @@ -984,7 +988,9 @@ async def receive_credential( async def request_proof(self, proof_request, explicit_revoc_required: bool = False): log_status("#20 Request proof of degree from alice") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_ANONCREDS]: + if self.cred_type in [ + CRED_FORMAT_INDY, + ]: indy_proof_request = { "name": proof_request["name"] if "name" in proof_request @@ -1065,7 +1071,9 @@ async def verify_proof(self, proof_request): # log_status(f">>> last proof received: {self.agent.last_proof_received}") - if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_ANONCREDS]: + if self.cred_type in [ + CRED_FORMAT_INDY, + ]: # return verified status return self.agent.last_proof_received["verified"] @@ -1425,13 +1433,11 @@ async def create_agent_with_args(args, ident: str = None): if "cred_type" in args and args.cred_type not in [ CRED_FORMAT_INDY, - CRED_FORMAT_ANONCREDS, ]: public_did = None aip = 20 elif "cred_type" in args and args.cred_type in [ CRED_FORMAT_INDY, - CRED_FORMAT_ANONCREDS, ]: public_did = True else: diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index fea3bc06f4..0983cc711f 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -66,9 +66,12 @@ DEFAULT_EXTERNAL_HOST = os.getenv("DOCKERHOST") or "host.docker.internal" DEFAULT_PYTHON_PATH = "." +WALLET_TYPE_INDY = "indy" +WALLET_TYPE_ASKAR = "askar" +WALLET_TYPE_ANONCREDS = "askar-anoncreds" + CRED_FORMAT_INDY = "indy" CRED_FORMAT_JSON_LD = "json-ld" -CRED_FORMAT_ANONCREDS = "anoncreds" DID_METHOD_SOV = "sov" DID_METHOD_KEY = "key" KEY_TYPE_ED255 = "ed25519" @@ -265,9 +268,9 @@ async def register_schema_and_creddef( support_revocation: bool = False, revocation_registry_size: int = None, tag=None, - cred_type=CRED_FORMAT_INDY, + wallet_type=WALLET_TYPE_INDY, ): - if cred_type == CRED_FORMAT_INDY: + if wallet_type in [WALLET_TYPE_INDY, WALLET_TYPE_ASKAR]: return await self.register_schema_and_creddef_indy( schema_name, version, @@ -276,7 +279,7 @@ async def register_schema_and_creddef( revocation_registry_size=revocation_registry_size, tag=tag, ) - elif cred_type == CRED_FORMAT_ANONCREDS: + elif wallet_type == WALLET_TYPE_ANONCREDS: return await self.register_schema_and_creddef_anoncreds( schema_name, version, @@ -286,7 +289,7 @@ async def register_schema_and_creddef( tag=tag, ) else: - return None, None + raise Exception("Invalid wallet_type: " + str(wallet_type)) async def register_schema_and_creddef_indy( self, @@ -628,7 +631,9 @@ async def register_did( role: str = "TRUST_ANCHOR", cred_type: str = CRED_FORMAT_INDY, ): - if cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_ANONCREDS]: + if cred_type in [ + CRED_FORMAT_INDY, + ]: # if registering a did for issuing indy credentials, publish the did on the ledger self.log(f"Registering {self.ident} ...") if not ledger_url: