diff --git a/aries_cloudagent/anoncreds/revocation.py b/aries_cloudagent/anoncreds/revocation.py index ebe2923887..84a03ef866 100644 --- a/aries_cloudagent/anoncreds/revocation.py +++ b/aries_cloudagent/anoncreds/revocation.py @@ -8,7 +8,7 @@ import os import time from pathlib import Path -from typing import List, NamedTuple, Optional, Sequence, Tuple +from typing import List, NamedTuple, Optional, Sequence, Tuple, Union from urllib.parse import urlparse import base58 @@ -19,6 +19,7 @@ RevocationRegistryDefinition, RevocationRegistryDefinitionPrivate, RevocationStatusList, + W3cCredential, ) from aries_askar.error import AskarError from requests import RequestException, Session @@ -717,6 +718,7 @@ async def upload_tails_file(self, rev_reg_def: RevRegDef): backoff=-0.5, max_attempts=5, # heuristic: respect HTTP timeout ) + if not upload_success: raise AnonCredsRevocationError( f"Tails file for rev reg for {rev_reg_def.cred_def_id} " @@ -892,6 +894,34 @@ async def get_or_create_active_registry(self, cred_def_id: str) -> RevRegDefResu # Credential Operations + async def create_credential_w3c( + self, + w3c_credential_offer: dict, + w3c_credential_request: dict, + w3c_credential_values: dict, + *, + retries: int = 5, + ) -> Tuple[str, str, str]: + """Create a w3c_credential. + + Args: + w3c_credential_offer: Credential Offer to create w3c_credential for + w3c_credential_request: Credential request to create w3c_credential for + w3c_credential_values: Values to go in w3c_credential + retries: number of times to retry w3c_credential creation + + Returns: + A tuple of created w3c_credential and revocation id + + """ + return await self._create_credential_helper( + w3c_credential_offer, + w3c_credential_request, + w3c_credential_values, + W3cCredential, + retries=retries, + ) + async def _create_credential( self, credential_definition_id: str, @@ -899,9 +929,26 @@ async def _create_credential( credential_offer: dict, credential_request: dict, credential_values: dict, + credential_type: Union[Credential, W3cCredential], rev_reg_def_id: Optional[str] = None, tails_file_path: Optional[str] = None, ) -> Tuple[str, str]: + """Create a credential. + + Args: + credential_definition_id: The credential definition ID + schema_attributes: The schema attributes + credential_offer: The credential offer + credential_request: The credential request + credential_values: The credential values + credential_type: The credential type + rev_reg_def_id: The revocation registry definition ID + tails_file_path: The tails file path + + Returns: + A tuple of created credential and revocation ID + + """ try: async with self.profile.session() as session: cred_def = await session.handle.fetch( @@ -1004,14 +1051,13 @@ async def _create_credential( try: credential = await asyncio.get_event_loop().run_in_executor( None, - lambda: Credential.create( - cred_def.raw_value, - cred_def_private.raw_value, - credential_offer, - credential_request, - raw_values, - None, - revoc, + lambda: credential_type.create( + cred_def=cred_def.raw_value, + cred_def_private=cred_def_private.raw_value, + cred_offer=credential_offer, + cred_request=credential_request, + attr_raw_values=raw_values, + revocation_config=revoc, ), ) except AnoncredsError as err: @@ -1039,6 +1085,36 @@ async def create_credential( Returns: A tuple of created credential and revocation id + """ + return await self._create_credential_helper( + credential_offer, + credential_request, + credential_values, + Credential, + retries=retries, + ) + + async def _create_credential_helper( + self, + credential_offer: dict, + credential_request: dict, + credential_values: dict, + credential_type: Union[Credential, W3cCredential], + *, + retries: int = 5, + ) -> Tuple[str, str, str]: + """Create a credential. + + Args: + credential_offer: Credential Offer to create credential for + credential_request: Credential request to create credential for + credential_values: Values to go in credential + credential_type: Credential or W3cCredential + retries: number of times to retry credential creation + + Returns: + A tuple of created credential and revocation id + """ issuer = AnonCredsIssuer(self.profile) anoncreds_registry = self.profile.inject(AnonCredsRegistry) @@ -1081,6 +1157,7 @@ async def create_credential( credential_offer, credential_request, credential_values, + credential_type, rev_reg_def_id, tails_file_path, ) diff --git a/aries_cloudagent/anoncreds/tests/test_revocation.py b/aries_cloudagent/anoncreds/tests/test_revocation.py index 26a75d57c2..f4a3a12ad6 100644 --- a/aries_cloudagent/anoncreds/tests/test_revocation.py +++ b/aries_cloudagent/anoncreds/tests/test_revocation.py @@ -11,6 +11,9 @@ RevocationRegistryDefinitionPrivate, RevocationStatusList, Schema, + # AnoncredsError, + # W3cCredential, + # CredentialRevocationConfig, ) from aries_askar import AskarError, AskarErrorCode from requests import RequestException, Session @@ -1024,6 +1027,7 @@ async def test_create_credential_private_no_rev_reg_or_tails( "attr1": "value1", "attr2": "value2", }, + credential_type=Credential, ) assert mock_create.called @@ -1038,6 +1042,7 @@ async def test_create_credential_private_no_rev_reg_or_tails( credential_offer={}, credential_request={}, credential_values={}, + credential_type=Credential, ) # missing cred def or cred def private @@ -1049,6 +1054,7 @@ async def test_create_credential_private_no_rev_reg_or_tails( credential_offer={}, credential_request={}, credential_values={}, + credential_type=Credential, ) mock_handle.fetch = mock.CoroutineMock(side_effect=[MockEntry(), None]) with self.assertRaises(test_module.AnonCredsRevocationError): @@ -1058,6 +1064,7 @@ async def test_create_credential_private_no_rev_reg_or_tails( credential_offer={}, credential_request={}, credential_values={}, + credential_type=Credential, ) @mock.patch.object(InMemoryProfileSession, "handle") @@ -1086,6 +1093,7 @@ async def call_test_func(): }, rev_reg_def_id="test-rev-reg-def-id", tails_file_path="tails-file-path", + credential_type=Credential, ) # missing rev list @@ -1380,3 +1388,83 @@ async def test_clear_pending_revocations_with_non_anoncreds_session(self): await self.revocation.clear_pending_revocations( self.profile.session(), rev_reg_def_id="test-rev-reg-id" ) + + @mock.patch.object( + AnonCredsIssuer, "cred_def_supports_revocation", return_value=True + ) + async def test_create_credential_w3c(self, mock_supports_revocation): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock( + return_value=GetSchemaResult( + schema_id="CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3", + schema=AnonCredsSchema( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + name="MYCO Biomarker:0.0.3", + version="1.0", + attr_names=["attr1", "attr2"], + ), + schema_metadata={}, + resolution_metadata={}, + ) + ) + ) + ) + self.revocation.get_or_create_active_registry = mock.CoroutineMock( + return_value=RevRegDefResult( + job_id="test-job-id", + revocation_registry_definition_state=RevRegDefState( + state=RevRegDefState.STATE_FINISHED, + revocation_registry_definition_id="active-reg-reg", + revocation_registry_definition=rev_reg_def, + ), + registration_metadata={}, + revocation_registry_definition_metadata={}, + ) + ) + + # Test private funtion seperately - very large + self.revocation._create_credential = mock.CoroutineMock( + return_value=({"cred": "cred"}, 98) + ) + + result = await self.revocation.create_credential_w3c( + w3c_credential_offer={ + "schema_id": "CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3", + "cred_def_id": "CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker", + "key_correctness_proof": {}, + "nonce": "nonce", + }, + w3c_credential_request={}, + w3c_credential_values={}, + ) + + assert isinstance(result, tuple) + assert mock_supports_revocation.call_count == 1 + + @pytest.mark.asyncio + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_create_credential_w3c_keyerror(self, mock_handle): + mock_handle.fetch = mock.CoroutineMock(side_effect=[MockEntry(), MockEntry()]) + with pytest.raises(test_module.AnonCredsRevocationError) as excinfo: + await self.revocation._create_credential( + credential_definition_id="test-cred-def-id", + schema_attributes=["attr1", "attr2"], + credential_offer={ + "schema_id": "CsQY9MGeD3CQP4EyuVFo5m:2:MYCO Biomarker:0.0.3", + "cred_def_id": "CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker", + "key_correctness_proof": {}, + "nonce": "nonce", + }, + credential_request={}, + credential_values={ + "X": "value1", + "Y": "value2", + }, + credential_type=Credential, + ) + + assert str(excinfo.value) == ( + "Provided credential values are missing a value " + "for the schema attribute 'attr1'" + ) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py index e2a688db96..100b44530e 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py @@ -7,7 +7,7 @@ import json import logging from typing import Mapping, Tuple - +from anoncreds import W3cCredential from ...models.cred_ex_record import V20CredExRecord from ...models.detail.indy import ( V20CredExRecordIndy, @@ -460,15 +460,29 @@ async def issue_credential( async with ledger: schema_id = await ledger.credential_definition_id2schema_id(cred_def_id) + cred_def = await ledger.get_credential_definition(cred_def_id) + revocable = cred_def["value"].get("revocation") legacy_offer = await self._prepare_legacy_offer(cred_offer, schema_id) legacy_request = await self._prepare_legacy_request(cred_request, cred_def_id) - issuer = AnonCredsIssuer(self.profile) - - credential = await issuer.create_credential_w3c( - legacy_offer, legacy_request, cred_values - ) + cred_rev_id = None + rev_reg_def_id = None + credential = None + if revocable: + issuer = AnonCredsRevocation(self.profile) + ( + credential, + cred_rev_id, + rev_reg_def_id, + ) = await issuer.create_credential_w3c( + legacy_offer, legacy_request, cred_values + ) + else: + issuer = AnonCredsIssuer(self.profile) + credential = await issuer.create_credential_w3c( + legacy_offer, legacy_request, cred_values + ) vcdi_credential = { "credential": json.loads(credential), @@ -476,9 +490,6 @@ async def issue_credential( result = self.get_format_data(CRED_20_ISSUE, vcdi_credential) - cred_rev_id = None - rev_reg_def_id = None - async with self._profile.transaction() as txn: detail_record = V20CredExRecordIndy( cred_ex_id=cred_ex_record.cred_ex_id, @@ -554,10 +565,18 @@ async def store_credential( cred_def_result = await anoncreds_registry.get_credential_definition( self.profile, cred["proof"][0]["verificationMethod"] ) - if cred["proof"][0].get("rev_reg_id"): + # TODO: remove loading of W3cCredential and use the credential directly + try: + cred_w3c = W3cCredential.load(cred) + rev_reg_id = cred_w3c.rev_reg_id + rev_reg_index = cred_w3c.rev_reg_index + except AnonCredsHolderError as e: + LOGGER.error(f"Error receiving credential: {e.error_code} - {e.message}") + raise e + if rev_reg_id: rev_reg_def_result = ( await anoncreds_registry.get_revocation_registry_definition( - self.profile, cred["proof"][0]["rev_reg_id"] + self.profile, rev_reg_id ) ) rev_reg_def = rev_reg_def_result.revocation_registry @@ -588,8 +607,8 @@ async def store_credential( ) detail_record.cred_id_stored = cred_id_stored - detail_record.rev_reg_id = cred["proof"][0].get("rev_reg_id", None) - detail_record.cred_rev_id = cred["proof"][0].get("cred_rev_id", None) + detail_record.rev_reg_id = rev_reg_id + detail_record.cred_rev_id = rev_reg_index async with self.profile.session() as session: # Store detail record, emit event diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py index 04d3c552ef..02a99804b2 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py @@ -2,8 +2,25 @@ from time import time import json +from aries_cloudagent.anoncreds.models.anoncreds_cred_def import ( + CredDef, + GetCredDefResult, +) +from aries_cloudagent.anoncreds.models.anoncreds_revocation import ( + GetRevRegDefResult, + RevRegDef, +) +from aries_cloudagent.anoncreds.registry import AnonCredsRegistry +from aries_cloudagent.askar.profile_anon import AskarAnoncredsProfile +from aries_cloudagent.protocols.issue_credential.v2_0.messages.cred_issue import ( + V20CredIssue, +) +from aries_cloudagent.protocols.issue_credential.v2_0.models.cred_ex_record import ( + V20CredExRecord, +) +from aries_cloudagent.wallet.did_info import DIDInfo import pytest -from .......anoncreds.holder import AnonCredsHolder +from .......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError from .......messaging.credential_definitions.util import ( CRED_DEF_SENT_RECORD_TYPE, ) @@ -176,7 +193,8 @@ "data_model_version": "2.0", "binding_proof": { "anoncreds_link_secret": { - "entropy": "M7PyEDW7WfLDA8UH4BPhVN", + "prover_did": f"did:sov:{TEST_DID}", + "entropy": f"did:sov:{TEST_DID}", "cred_def_id": CRED_DEF_ID, "blinded_ms": { "u": "10047077609650450290609991930929594521921208780899757965398360086992099381832995073955506958821655372681970112562804577530208651675996528617262693958751195285371230790988741041496869140904046414320278189103736789305088489636024127715978439300785989247215275867951013255925809735479471883338351299180591011255281885961242995409072561940244771612447316409017677474822482928698183528232263803799926211692640155689629903898365777273000566450465466723659861801656618726777274689021162957914736922404694190070274236964163273886807208820068271673047750886130307545831668836096290655823576388755329367886670574352063509727295", @@ -234,7 +252,9 @@ async def asyncSetUp(self): self.profile = self.session.profile self.context = self.profile.context setattr(self.profile, "session", mock.MagicMock(return_value=self.session)) - + self.session.wallet.get_public_did = mock.CoroutineMock( + return_value=DIDInfo(TEST_DID, None, None, None, True) + ) # Ledger Ledger = mock.MagicMock() self.ledger = Ledger() @@ -381,7 +401,6 @@ async def test_receive_proposal(self): # 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(":") @@ -423,26 +442,36 @@ async def test_create_offer(self): ) await self.session.storage.add_record(cred_def_record) - self.issuer.create_credential = mock.CoroutineMock( - return_value=json.dumps(VCDI_OFFER) - ) + with mock.patch.object( + test_module, "AnonCredsIssuer", return_value=self.issuer + ) as mock_issuer: + self.issuer.create_credential_offer = mock.CoroutineMock( + return_value=json.dumps( + VCDI_OFFER["binding_method"]["anoncreds_link_secret"] + ) + ) - (cred_format, attachment) = await self.handler.create_offer(cred_proposal) + self.issuer.match_created_credential_definitions = mock.CoroutineMock( + return_value=CRED_DEF_ID + ) - self.issuer.create_credential.assert_called_once_with(CRED_DEF_ID) + (cred_format, attachment) = await self.handler.create_offer(cred_proposal) - # assert identifier match - assert cred_format.attach_id == self.handler.format.api == attachment.ident + self.issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID) - # assert content of attachment is proposal data - assert attachment.content == VCDI_OFFER + assert cred_format.attach_id == self.handler.format.api == attachment.ident - # assert data is encoded as base64 - assert attachment.data.base64 + assert ( + attachment.content["binding_method"]["anoncreds_link_secret"] + == VCDI_OFFER["binding_method"]["anoncreds_link_secret"] + ) + + # Assert data is encoded as base64 + assert attachment.data.base64 + # TODO: fix this get_public_did return None in the sc + # (cred_format, attachment) = await self.handler.create_offer(cred_proposal) - 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() + # self.issuer.create_credential_offer.assert_not_called() @pytest.mark.skip(reason="Anoncreds-break") async def test_receive_offer(self): @@ -452,9 +481,25 @@ async def test_receive_offer(self): # 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): + # Define your mock credential definition + mock_credential_definition_result = GetCredDefResult( + credential_definition=CredDef( + issuer_id=TEST_DID, schema_id=SCHEMA_ID, type="CL", tag="tag1", value={} + ), + credential_definition_id=CRED_DEF_ID, + resolution_metadata={}, + credential_definition_metadata={}, + ) + mock_creds_registry = mock.MagicMock() + mock_creds_registry.get_credential_definition = mock.AsyncMock( + return_value=mock_credential_definition_result + ) + + # Inject the MagicMock into the context + self.context.injector.bind_instance(AnonCredsRegistry, mock_creds_registry) + holder_did = "did" cred_offer = V20CredOffer( @@ -466,62 +511,67 @@ async def test_create_request(self): ], ) ], - # TODO here offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], ) - cred_ex_record = V20CredExRecordIndy( + cred_ex_record = V20CredExRecord( cred_ex_id="dummy-id", - state=V20CredExRecordIndy.STATE_OFFER_RECEIVED, + 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(VCDI_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( - VCDI_OFFER, cred_def, holder_did - ) - - # assert identifier match - assert cred_format.attach_id == self.handler.format.api == attachment.ident + self.profile = mock.MagicMock(AskarAnoncredsProfile) + self.context.injector.bind_instance(AskarAnoncredsProfile, self.profile) + with mock.patch.object( + AnonCredsHolder, "create_credential_request", mock.CoroutineMock() + ) as mock_create: - # assert content of attachment is proposal data - assert attachment.content == VCDI_CRED_REQ + mock_create.return_value = ( + json.dumps(VCDI_CRED_REQ["binding_proof"]["anoncreds_link_secret"]), + json.dumps(cred_req_meta), + ) + (cred_format, attachment) = await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) - # assert data is encoded as base64 - assert attachment.data.base64 + legacy_offer = await self.handler._prepare_legacy_offer( + VCDI_OFFER, SCHEMA_ID + ) + mock_create.assert_called_once_with( + legacy_offer, + mock_credential_definition_result.credential_definition, + holder_did, + ) + assert cred_format.attach_id == self.handler.format.api == attachment.ident - # 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}) + del VCDI_CRED_REQ["binding_proof"]["anoncreds_link_secret"]["prover_did"] + assert attachment.content == VCDI_CRED_REQ + assert attachment.data.base64 - # 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)), - ): + cred_ex_record._id = "dummy-id2" await self.handler.create_request( cred_ex_record, {"holder_did": holder_did} ) + 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} + ) + @pytest.mark.skip(reason="Anoncreds-break") async def test_receive_request(self): cred_ex_record = mock.MagicMock() @@ -530,7 +580,6 @@ async def test_receive_request(self): # Not much to assert. Receive request doesn't do anything await self.handler.receive_request(cred_ex_record, cred_request_message) - @pytest.mark.skip(reason="Anoncreds-break") async def test_issue_credential_revocable(self): attr_values = { "legalName": "value", @@ -552,7 +601,6 @@ async def test_issue_credential_revocable(self): ], ) ], - # TODO here offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], ) cred_request = V20CredRequest( @@ -564,59 +612,52 @@ async def test_issue_credential_revocable(self): ], ) ], - # TODO here requests_attach=[AttachDecorator.data_base64(VCDI_CRED_REQ, ident="0")], ) - cred_ex_record = V20CredExRecordIndy( + cred_ex_record = V20CredExRecord( cred_ex_id="dummy-cxid", cred_offer=cred_offer.serialize(), cred_request=cred_request.serialize(), - initiator=V20CredExRecordIndy.INITIATOR_SELF, - role=V20CredExRecordIndy.ROLE_ISSUER, - state=V20CredExRecordIndy.STATE_REQUEST_RECEIVED, + 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(VCDI_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, - ), - ) + dummy_registry = "dummy-registry" + expected_credential = json.dumps(VCDI_CRED) + + # Mock AnonCredsRevocation and its method create_credential_w3c + with mock.patch.object( + test_module, "AnonCredsRevocation", autospec=True + ) as MockAnonCredsRevocation: + mock_issuer = MockAnonCredsRevocation.return_value + mock_issuer.create_credential_w3c = mock.CoroutineMock( + return_value=(expected_credential, cred_rev_id, dummy_registry) ) + # Call the method under test (cred_format, attachment) = await self.handler.issue_credential( cred_ex_record, retries=1 ) - - self.issuer.create_credential.assert_called_once_with( - SCHEMA, - VCDI_OFFER, - VCDI_CRED_REQ, + legacy_offer = await self.handler._prepare_legacy_offer( + VCDI_OFFER, SCHEMA_ID + ) + legacy_request = await self.handler._prepare_legacy_request( + VCDI_CRED_REQ, CRED_DEF_ID + ) + # Verify the mocked method was called with the expected parameters + mock_issuer.create_credential_w3c.assert_called_once_with( + legacy_offer, + legacy_request, attr_values, - REV_REG_ID, - "dummy-path", ) - # assert identifier match + # Assert the results are as expected assert cred_format.attach_id == self.handler.format.api == attachment.ident - - # assert content of attachment is proposal data - assert attachment.content == VCDI_CRED - - # assert data is encoded as base64 assert attachment.data.base64 + assert attachment.content == {"credential": VCDI_CRED} @pytest.mark.skip(reason="Anoncreds-break") async def test_issue_credential_non_revocable(self): @@ -702,3 +743,153 @@ async def test_issue_credential_non_revocable(self): # assert data is encoded as base64 assert attachment.data.base64 + + @pytest.mark.asyncio + async def test_match_sent_cred_def_id_error(self): + tag_query = {"tag": "test_tag"} + + with self.assertRaises(V20CredFormatError) as context: + await self.handler._match_sent_cred_def_id(tag_query) + assert "Issuer has no operable cred def for proposal spec " in str( + context.exception + ) + + @pytest.mark.asyncio + async def test_store_credential(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.VC_DI.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(VCDI_CRED_REQ, ident="0")], + ) + cred_issue = V20CredIssue( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_ISSUE][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + credentials_attach=[AttachDecorator.data_base64(VCDI_CRED, ident="0")], + ) + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + cred_issue=cred_issue.serialize(), + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + ) + cred_id = "dummy-cred-id" + + # Define your mock credential definition + mock_credential_definition_result = GetCredDefResult( + credential_definition=CredDef( + issuer_id=TEST_DID, schema_id=SCHEMA_ID, type="CL", tag="tag1", value={} + ), + credential_definition_id=CRED_DEF_ID, + resolution_metadata={}, + credential_definition_metadata={}, + ) + mock_creds_registry = mock.AsyncMock() + mock_creds_registry.get_credential_definition = mock.AsyncMock( + return_value=mock_credential_definition_result + ) + + revocation_registry = RevRegDef( + cred_def_id=CRED_DEF_ID, + issuer_id=TEST_DID, + tag="tag1", + type="CL_ACCUM", + value={}, + ) + + mock_creds_registry.get_revocation_registry_definition = mock.AsyncMock( + return_value=GetRevRegDefResult( + revocation_registry=revocation_registry, + revocation_registry_id="rr-id", + resolution_metadata={}, + revocation_registry_metadata={}, + ) + ) + # Inject the MagicMock into the context + self.context.injector.bind_instance(AnonCredsRegistry, mock_creds_registry) + self.profile = mock.AsyncMock(AskarAnoncredsProfile) + self.context.injector.bind_instance(AskarAnoncredsProfile, self.profile) + with mock.patch.object( + test_module.AnonCredsRevocation, + "get_or_fetch_local_tails_path", + mock.CoroutineMock(), + ) as mock_get_or_fetch_local_tails_path: + with self.assertRaises(V20CredFormatError) as context: + await self.handler.store_credential(cred_ex_record, cred_id) + assert ( + "No credential exchange didcomm/ detail record found for cred ex id dummy-cxid" + in str(context.exception) + ) + + record = V20CredExRecordIndy( + cred_ex_indy_id="dummy-cxid", + rev_reg_id="rr-id", + cred_ex_id="dummy-cxid", + cred_id_stored=cred_id, + cred_request_metadata="dummy-metadata", + cred_rev_id="0", + ) + + record.save = mock.CoroutineMock() + self.handler.get_detail_record = mock.AsyncMock(return_value=record) + with mock.patch.object( + AnonCredsHolder, + "store_credential_w3c", + mock.AsyncMock(), + ) as mock_store_credential: + # Error case: no cred ex record found + + await self.handler.store_credential(cred_ex_record, cred_id) + + mock_store_credential.assert_called_once_with( + mock_credential_definition_result.credential_definition.serialize(), + VCDI_CRED["credential"], + record.cred_request_metadata, + None, + credential_id=cred_id, + rev_reg_def=revocation_registry.serialize(), + ) + + with mock.patch.object( + AnonCredsHolder, + "store_credential_w3c", + mock.AsyncMock(side_effect=AnonCredsHolderError), + ) as mock_store_credential: + with self.assertRaises(AnonCredsHolderError) as context: + await self.handler.store_credential(cred_ex_record, cred_id) diff --git a/demo/features/0453-issue-credential.feature b/demo/features/0453-issue-credential.feature index fbe1c49c07..a6736295b7 100644 --- a/demo/features/0453-issue-credential.feature +++ b/demo/features/0453-issue-credential.feature @@ -64,9 +64,10 @@ Feature: RFC 0453 Aries agent issue credential @WalletType_Askar_AnonCreds @SwitchCredTypeTest Examples: - | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Acme_extra | Bob_extra | New_Cred_Type | - | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | vc_di | - | --public-did --wallet-type askar-anoncreds --cred-type vc_di | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | indy | + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Acme_extra | Bob_extra | New_Cred_Type | + | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | vc_di | + | --public-did --wallet-type askar-anoncreds --cred-type vc_di | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | indy | + | --public-did --wallet-type askar-anoncreds --cred-type vc_di --revocation | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | indy | @T003-RFC0453 Scenario Outline: Holder accepts a deleted credential offer diff --git a/demo/features/0454-present-proof.feature b/demo/features/0454-present-proof.feature index f40552e003..bdbc2155c4 100644 --- a/demo/features/0454-present-proof.feature +++ b/demo/features/0454-present-proof.feature @@ -303,3 +303,42 @@ Feature: RFC 0454 Aries agent present proof Examples: | issuer1 | Acme1_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Proof_request | | Acme1 | --revocation --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T003-RFC0454.5 + Scenario Outline: Present Proof for a vc_di-issued credential using "legacy" indy proof and the proof validates + Given we have "2" agents + | name | role | capabilities | extra | + | Acme | issuer | | | + | Bob | holder | | | + And "Acme" and "Bob" have an existing connection + And "Acme" is ready to issue a credential for + When "Acme" offers a credential with data + When "Bob" has the credential issued + When "Acme" sets the credential type to + When "Acme" sends a request with explicit revocation status for proof presentation to "Bob" + Then "Acme" has the proof verified + + @WalletType_Askar_AnonCreds @SwitchCredTypeTest + Examples: + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Acme_extra | Bob_extra | New_Cred_Type | Proof_request | + | --public-did --wallet-type askar-anoncreds --cred-type vc_di --revocation | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | | | indy | DL_age_over_19_v2 | + + @T003-RFC0454.6 + Scenario Outline: Present Proof for a vc_di-issued credential using "legacy" indy proof and credential is revoked and the proof fails + Given we have "2" agents + | name | role | capabilities | extra | + | Acme | issuer | | | + | Bob | holder | | | + And "Acme" and "Bob" have an existing connection + And "Acme" is ready to issue a credential for + When "Acme" offers a credential with data + When "Bob" has the credential issued + When "Acme" sets the credential type to + And "Acme" revokes the credential + When "Acme" sends a request for proof presentation to "Bob" + Then "Acme" has the proof verification fail + + @WalletType_Askar_AnonCreds @SwitchCredTypeTest + Examples: + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Acme_extra | Bob_extra | New_Cred_Type | Proof_request | + | --public-did --wallet-type askar-anoncreds --cred-type vc_di --revocation | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | | | indy | DL_age_over_19_v2 |