diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py index 98f600192a..23b861743a 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/presentation_request_handler.py @@ -10,9 +10,10 @@ from .....storage.error import StorageNotFoundError from ..manager import PresentationManager +from ..messages.presentation_proposal import PresentationProposal from ..messages.presentation_request import PresentationRequest from ..models.presentation_exchange import V10PresentationExchange -from ..util.indy import indy_proof_request2indy_requested_creds +from ..util.indy import indy_proof_req_preview2indy_requested_creds class PresentationRequestHandler(BaseHandler): @@ -41,7 +42,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): indy_proof_request = context.message.indy_proof_request(0) - # Get credential exchange record (holder initiated via proposal) + # Get presentation exchange record (holder initiated via proposal) # or create it (verifier sent request first) try: ( @@ -70,9 +71,18 @@ async def handle(self, context: RequestContext, responder: BaseResponder): # If auto_present is enabled, respond immediately with presentation if presentation_exchange_record.auto_present: + presentation_preview = None + if presentation_exchange_record.presentation_proposal_dict: + exchange_pres_proposal = PresentationProposal.deserialize( + presentation_exchange_record.presentation_proposal_dict + ) + presentation_preview = exchange_pres_proposal.presentation_proposal + try: - req_creds = await indy_proof_request2indy_requested_creds( - indy_proof_request, await context.inject(BaseHolder) + req_creds = await indy_proof_req_preview2indy_requested_creds( + indy_proof_request, + presentation_preview, + holder=await context.inject(BaseHolder) ) except ValueError as err: self._logger.warning(f"{err}") diff --git a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py index 07e266851b..9f4afe3d6f 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/handlers/tests/test_presentation_request_handler.py @@ -13,6 +13,10 @@ from .. import presentation_request_handler as handler +S_ID = "NcYxiDXkpYi6ov5FcYDi1e:2:vidya:1.0" +CD_ID = f"NcYxiDXkpYi6ov5FcYDi1e:3:CL:{S_ID}:tag1" + + class TestPresentationRequestHandler(AsyncTestCase): async def test_called(self): request_context = RequestContext() @@ -93,9 +97,57 @@ async def test_called_auto_present(self): request_context.connection_record.connection_id = "dummy" request_context.message = PresentationRequest() request_context.message.indy_proof_request = async_mock.MagicMock( - return_value=async_mock.MagicMock() + return_value={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + "0_favourite_uuid": { + "name": "favourite", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + }, + "1_icon_uuid": { + "name": "icon", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + }, + "requested_predicates": { + } + } ) request_context.message_receipt = MessageReceipt() + px_rec_instance = handler.V10PresentationExchange( + presentation_proposal_dict={ + "presentation_proposal": { + "@type": ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;" + "spec/present-proof/1.0/presentation-preview" + ), + "attributes": [ + { + "name": "favourite", + "cred_def_id": CD_ID, + "value": "potato" + }, + { + "name": "icon", + "cred_def_id": CD_ID, + "value": "cG90YXRv" + } + ], + "predicates": [] + } + }, + auto_present=True + ) with async_mock.patch.object( handler, "PresentationManager", autospec=True @@ -105,23 +157,261 @@ async def test_called_auto_present(self): handler, "BaseHolder", autospec=True ) as mock_holder: + mock_holder.get_credentials_for_presentation_request_by_referent = ( + async_mock.CoroutineMock( + return_value=[ + { + "cred_info": { + "referent": "dummy" + } + } + ] + ) + ) request_context.inject = async_mock.CoroutineMock(return_value=mock_holder) + mock_pres_ex_rec.return_value = px_rec_instance mock_pres_ex_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( - return_value=mock_pres_ex_rec + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_request = async_mock.CoroutineMock( + return_value=px_rec_instance ) + mock_pres_mgr.return_value.create_presentation = async_mock.CoroutineMock( + return_value=(px_rec_instance, "presentation_message") + ) + request_context.connection_ready = True + handler_inst = handler.PresentationRequestHandler() + responder = MockResponder() + await handler_inst.handle(request_context, responder) + mock_pres_mgr.return_value.create_presentation.assert_called_once() + + mock_pres_mgr.assert_called_once_with(request_context) + mock_pres_mgr.return_value.receive_request.assert_called_once_with( + px_rec_instance + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "presentation_message" + assert target == {} + + async def test_called_auto_present_no_preview(self): + request_context = RequestContext() + request_context.connection_record = async_mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = PresentationRequest() + request_context.message.indy_proof_request = async_mock.MagicMock( + return_value={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + "0_favourite_uuid": { + "name": "favourite", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + }, + "1_icon_uuid": { + "name": "icon", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + }, + "requested_predicates": { + } + } + ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = handler.V10PresentationExchange(auto_present=True) + + with async_mock.patch.object( + handler, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + handler, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_rec, async_mock.patch.object( + handler, "BaseHolder", autospec=True + ) as mock_holder: + + mock_holder.get_credentials_for_presentation_request_by_referent = ( + async_mock.CoroutineMock( + return_value=[ + { + "cred_info": { + "referent": "dummy-0" + } + }, + { + "cred_info": { + "referent": "dummy-1" + } + } + ] + ) + ) + request_context.inject = async_mock.CoroutineMock(return_value=mock_holder) + + mock_pres_ex_rec.return_value = px_rec_instance + mock_pres_ex_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( + return_value=px_rec_instance + ) mock_pres_mgr.return_value.receive_request = async_mock.CoroutineMock( - return_value=mock_pres_ex_rec + return_value=px_rec_instance ) - mock_pres_mgr.return_value.receive_request.return_value.auto_present = True - handler.indy_proof_request2indy_requested_creds = async_mock.CoroutineMock( - return_value=async_mock.MagicMock() + mock_pres_mgr.return_value.create_presentation = async_mock.CoroutineMock( + return_value=(px_rec_instance, "presentation_message") + ) + request_context.connection_ready = True + handler_inst = handler.PresentationRequestHandler() + responder = MockResponder() + await handler_inst.handle(request_context, responder) + mock_pres_mgr.return_value.create_presentation.assert_called_once() + + mock_pres_mgr.assert_called_once_with(request_context) + mock_pres_mgr.return_value.receive_request.assert_called_once_with( + px_rec_instance + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "presentation_message" + assert target == {} + + async def test_called_auto_present_pred_no_match(self): + request_context = RequestContext() + request_context.connection_record = async_mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = PresentationRequest() + request_context.message.indy_proof_request = async_mock.MagicMock( + return_value={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + }, + "requested_predicates": { + "0_score_GE_uuid": { + "name": "score", + "p_type": ">=", + "p_value": "1000000", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + } + } + ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = handler.V10PresentationExchange(auto_present=True) + + with async_mock.patch.object( + handler, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + handler, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_rec, async_mock.patch.object( + handler, "BaseHolder", autospec=True + ) as mock_holder: + + mock_holder.get_credentials_for_presentation_request_by_referent = ( + async_mock.CoroutineMock( + return_value=[] + ) + ) + request_context.inject = async_mock.CoroutineMock(return_value=mock_holder) + + mock_pres_ex_rec.return_value = px_rec_instance + mock_pres_ex_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_request = async_mock.CoroutineMock( + return_value=px_rec_instance + ) + + mock_pres_mgr.return_value.create_presentation = async_mock.CoroutineMock( + return_value=(px_rec_instance, "presentation_message") + ) + request_context.connection_ready = True + handler_inst = handler.PresentationRequestHandler() + responder = MockResponder() + await handler_inst.handle(request_context, responder) + mock_pres_mgr.return_value.create_presentation.assert_not_called() + + mock_pres_mgr.assert_called_once_with(request_context) + mock_pres_mgr.return_value.receive_request.assert_called_once_with( + px_rec_instance + ) + assert not responder.messages + + async def test_called_auto_present_pred_single_match(self): + request_context = RequestContext() + request_context.connection_record = async_mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = PresentationRequest() + request_context.message.indy_proof_request = async_mock.MagicMock( + return_value={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + }, + "requested_predicates": { + "0_score_GE_uuid": { + "name": "score", + "p_type": ">=", + "p_value": "1000000", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + } + } + ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = handler.V10PresentationExchange(auto_present=True) + + with async_mock.patch.object( + handler, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + handler, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_rec, async_mock.patch.object( + handler, "BaseHolder", autospec=True + ) as mock_holder: + + mock_holder.get_credentials_for_presentation_request_by_referent = ( + async_mock.CoroutineMock( + return_value=[ + { + "cred_info": { + "referent": "dummy-0" + } + } + ] + ) + ) + request_context.inject = async_mock.CoroutineMock(return_value=mock_holder) + + mock_pres_ex_rec.return_value = px_rec_instance + mock_pres_ex_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_request = async_mock.CoroutineMock( + return_value=px_rec_instance ) mock_pres_mgr.return_value.create_presentation = async_mock.CoroutineMock( - return_value=(mock_pres_ex_rec, "presentation_message") + return_value=(px_rec_instance, "presentation_message") ) request_context.connection_ready = True handler_inst = handler.PresentationRequestHandler() @@ -131,7 +421,7 @@ async def test_called_auto_present(self): mock_pres_mgr.assert_called_once_with(request_context) mock_pres_mgr.return_value.receive_request.assert_called_once_with( - mock_pres_ex_rec + px_rec_instance ) messages = responder.messages assert len(messages) == 1 @@ -139,15 +429,34 @@ async def test_called_auto_present(self): assert result == "presentation_message" assert target == {} - async def test_called_auto_present_value_error(self): + async def test_called_auto_present_pred_multi_match(self): request_context = RequestContext() request_context.connection_record = async_mock.MagicMock() request_context.connection_record.connection_id = "dummy" request_context.message = PresentationRequest() request_context.message.indy_proof_request = async_mock.MagicMock( - return_value=async_mock.MagicMock() + return_value={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + }, + "requested_predicates": { + "0_score_GE_uuid": { + "name": "score", + "p_type": ">=", + "p_value": "1000000", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + } + } ) request_context.message_receipt = MessageReceipt() + px_rec_instance = handler.V10PresentationExchange(auto_present=True) with async_mock.patch.object( handler, "PresentationManager", autospec=True @@ -157,33 +466,297 @@ async def test_called_auto_present_value_error(self): handler, "BaseHolder", autospec=True ) as mock_holder: + mock_holder.get_credentials_for_presentation_request_by_referent = ( + async_mock.CoroutineMock( + return_value=[ + { + "cred_info": { + "referent": "dummy-0" + } + }, + { + "cred_info": { + "referent": "dummy-1" + } + } + ] + ) + ) request_context.inject = async_mock.CoroutineMock(return_value=mock_holder) + mock_pres_ex_rec.return_value = px_rec_instance mock_pres_ex_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( - return_value=mock_pres_ex_rec + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_request = async_mock.CoroutineMock( + return_value=px_rec_instance + ) + + mock_pres_mgr.return_value.create_presentation = async_mock.CoroutineMock( + return_value=(px_rec_instance, "presentation_message") + ) + request_context.connection_ready = True + handler_inst = handler.PresentationRequestHandler() + responder = MockResponder() + await handler_inst.handle(request_context, responder) + mock_pres_mgr.return_value.create_presentation.assert_called_once() + + mock_pres_mgr.assert_called_once_with(request_context) + mock_pres_mgr.return_value.receive_request.assert_called_once_with( + px_rec_instance + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "presentation_message" + assert target == {} + + async def test_called_auto_present_multi_cred_match_reft(self): + request_context = RequestContext() + request_context.connection_record = async_mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = PresentationRequest() + request_context.message.indy_proof_request = async_mock.MagicMock( + return_value={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + "0_favourite_uuid": { + "name": "favourite", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + }, + "1_icon_uuid": { + "name": "icon", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + }, + "requested_predicates": { + } + } + ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = handler.V10PresentationExchange( + presentation_proposal_dict={ + "presentation_proposal": { + "@type": ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;" + "spec/present-proof/1.0/presentation-preview" + ), + "attributes": [ + { + "name": "favourite", + "cred_def_id": CD_ID, + "value": "potato" + }, + { + "name": "icon", + "cred_def_id": CD_ID, + "value": "cG90YXRv" + } + ], + "predicates": [] + } + }, + auto_present=True + ) + + with async_mock.patch.object( + handler, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + handler, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_rec, async_mock.patch.object( + handler, "BaseHolder", autospec=True + ) as mock_holder: + + mock_holder.get_credentials_for_presentation_request_by_referent = ( + async_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.inject = async_mock.CoroutineMock(return_value=mock_holder) + mock_pres_ex_rec.return_value = px_rec_instance + mock_pres_ex_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( + return_value=px_rec_instance + ) mock_pres_mgr.return_value.receive_request = async_mock.CoroutineMock( - return_value=mock_pres_ex_rec + return_value=px_rec_instance ) - mock_pres_mgr.return_value.receive_request.return_value.auto_present = True - handler.indy_proof_request2indy_requested_creds = async_mock.CoroutineMock( - side_effect=ValueError + mock_pres_mgr.return_value.create_presentation = async_mock.CoroutineMock( + return_value=(px_rec_instance, "presentation_message") + ) + request_context.connection_ready = True + handler_inst = handler.PresentationRequestHandler() + responder = MockResponder() + await handler_inst.handle(request_context, responder) + mock_pres_mgr.return_value.create_presentation.assert_called_once() + + mock_pres_mgr.assert_called_once_with(request_context) + mock_pres_mgr.return_value.receive_request.assert_called_once_with( + px_rec_instance + ) + messages = responder.messages + assert len(messages) == 1 + (result, target) = messages[0] + assert result == "presentation_message" + assert target == {} + + async def test_called_auto_present_bait_and_switch(self): + request_context = RequestContext() + request_context.connection_record = async_mock.MagicMock() + request_context.connection_record.connection_id = "dummy" + request_context.message = PresentationRequest() + request_context.message.indy_proof_request = async_mock.MagicMock( + return_value={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + "0_favourite_uuid": { + "name": "favourite", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + }, + "requested_predicates": { + } + } + ) + request_context.message_receipt = MessageReceipt() + px_rec_instance = handler.V10PresentationExchange( + presentation_proposal_dict={ + "presentation_proposal": { + "@type": ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;" + "spec/present-proof/1.0/presentation-preview" + ), + "attributes": [ + { + "name": "favourite", + "cred_def_id": CD_ID, + "value": "potato" + } + ], + "predicates": [] + } + }, + auto_present=True + ) + + with async_mock.patch.object( + handler, "PresentationManager", autospec=True + ) as mock_pres_mgr, async_mock.patch.object( + handler, "V10PresentationExchange", autospec=True + ) as mock_pres_ex_rec, async_mock.patch.object( + handler, "BaseHolder", autospec=True + ) as mock_holder: + + mock_holder.get_credentials_for_presentation_request_by_referent = ( + async_mock.CoroutineMock( + return_value=[ + { + "cred_info": { + "referent": "dummy-0", + "cred_def_id": CD_ID, + "attrs": { + "ident": "zero", + "favourite": "yam" + } + } + }, + { + "cred_info": { + "referent": "dummy-1", + "cred_def_id": CD_ID, + "attrs": { + "ident": "one", + "favourite": "turnip" + } + } + }, + { + "cred_info": { + "referent": "dummy-2", + "cred_def_id": CD_ID, + "attrs": { + "ident": "two", + "favourite": "the idea of a potato but not a potato" + } + } + } + ] + ) + ) + request_context.inject = async_mock.CoroutineMock(return_value=mock_holder) + + mock_pres_ex_rec.return_value = px_rec_instance + mock_pres_ex_rec.retrieve_by_tag_filter = async_mock.CoroutineMock( + return_value=px_rec_instance + ) + mock_pres_mgr.return_value.receive_request = async_mock.CoroutineMock( + return_value=px_rec_instance ) mock_pres_mgr.return_value.create_presentation = async_mock.CoroutineMock( - return_value=(mock_pres_ex_rec, "presentation_message") + return_value=(px_rec_instance, "presentation_message") ) request_context.connection_ready = True handler_inst = handler.PresentationRequestHandler() responder = MockResponder() + await handler_inst.handle(request_context, responder) mock_pres_mgr.return_value.create_presentation.assert_not_called() mock_pres_mgr.assert_called_once_with(request_context) mock_pres_mgr.return_value.receive_request.assert_called_once_with( - mock_pres_ex_rec + px_rec_instance ) assert not responder.messages diff --git a/aries_cloudagent/protocols/present_proof/v1_0/manager.py b/aries_cloudagent/protocols/present_proof/v1_0/manager.py index ec731ad552..71f680a98c 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/manager.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/manager.py @@ -332,6 +332,7 @@ async def receive_presentation(self): """ presentation = self.context.message.indy_proof() + thread_id = self.context.message._thread_id connection_id_filter = ( {"connection_id": self.context.connection_record.connection_id} @@ -344,6 +345,31 @@ async def receive_presentation(self): self.context, {"thread_id": thread_id}, connection_id_filter ) + # Check for bait-and-switch in presented attribute values vs. proposal + if presentation_exchange_record.presentation_proposal_dict: + exchange_pres_proposal = PresentationProposal.deserialize( + presentation_exchange_record.presentation_proposal_dict + ) + presentation_preview = exchange_pres_proposal.presentation_proposal + + proof_req = presentation_exchange_record.presentation_request + for ( + reft, + attr_spec + ) in presentation["requested_proof"]["revealed_attrs"].items(): + name = proof_req["requested_attributes"][reft]["name"] + value = attr_spec["raw"] + if not presentation_preview.has_attr_spec( + cred_def_id=presentation["identifiers"][ + attr_spec["sub_proof_index"] + ]["cred_def_id"], + name=name, + value=value + ): + raise PresentationManagerError( + f"Presentation {name}={value} mismatches proposal value" + ) + presentation_exchange_record.presentation = presentation presentation_exchange_record.state = ( V10PresentationExchange.STATE_PRESENTATION_RECEIVED diff --git a/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/presentation_preview.py b/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/presentation_preview.py index 091ff8ceae..e27d060ab2 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/presentation_preview.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/presentation_preview.py @@ -15,7 +15,7 @@ from ......wallet.util import b64_to_str from ...message_types import PRESENTATION_PREVIEW -from ...util.indy import Predicate +from ...util.predicate import Predicate class PresPredSpec(BaseModel): @@ -238,6 +238,28 @@ def _type(self): return PresentationPreview.Meta.message_type + def has_attr_spec(self, cred_def_id: str, name: str, value: str) -> bool: + """ + Return whether preview contains given attribute specification. + + Args: + cred_def_id: credential definition identifier + name: attribute name + value: attribute value + + Returns: + Whether preview contains matching attribute specification. + + """ + + return any( + a.name == canon(name) and a.value in ( + value, + None + ) and a.cred_def_id == cred_def_id + for a in self.attributes + ) + async def indy_proof_request( self, name: str = None, @@ -272,18 +294,7 @@ def non_revo(cred_def_id: str): return (timestamps or {}).get(cred_def_id, epoch_now) - def ord_cred_def_id(cred_def_id: str): - """Ordinal for cred def id to use in suggestive proof req referent.""" - - nonlocal cred_def_ids - - if cred_def_id in cred_def_ids: - return cred_def_ids.index(cred_def_id) - cred_def_ids.append(cred_def_id) - return len(cred_def_ids) - 1 - epoch_now = int(time()) # TODO: take cred_def_id->timestamp here, default now - cred_def_ids = [] proof_req = { "name": name or "proof-request", @@ -295,7 +306,9 @@ def ord_cred_def_id(cred_def_id: str): for attr_spec in self.attributes: if attr_spec.posture == PresAttrSpec.Posture.SELF_ATTESTED: - proof_req["requested_attributes"][f"{canon(attr_spec.name)}"] = { + proof_req["requested_attributes"][ + "self_{}_uuid".format(canon(attr_spec.name)) + ] = { "name": canon(attr_spec.name) } else: @@ -309,7 +322,9 @@ def ord_cred_def_id(cred_def_id: str): timestamp = non_revo(attr_spec.cred_def_id) proof_req["requested_attributes"][ - "{}_{}_uuid".format(ord_cred_def_id(cd_id), canon(attr_spec.name)) + "{}_{}_uuid".format( + len(proof_req["requested_attributes"]), + canon(attr_spec.name)) ] = { "name": canon(attr_spec.name), "restrictions": [{"cred_def_id": cd_id}], @@ -330,7 +345,7 @@ def ord_cred_def_id(cred_def_id: str): timestamp = non_revo(attr_spec.cred_def_id) proof_req["requested_predicates"][ "{}_{}_{}_uuid".format( - ord_cred_def_id(cd_id), + len(proof_req["requested_predicates"]), canon(pred_spec.name), Predicate.get(pred_spec.predicate).value.fortran, ) diff --git a/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/tests/test_presentation_preview.py b/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/tests/test_presentation_preview.py index b41dd49225..59d7dcbe4d 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/tests/test_presentation_preview.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/messages/inner/tests/test_presentation_preview.py @@ -13,7 +13,7 @@ from .......messaging.util import canon, str_to_datetime, str_to_epoch from ....message_types import PRESENTATION_PREVIEW -from ....util.indy import Predicate +from ....util.predicate import Predicate from ..presentation_preview import ( PresAttrSpec, @@ -62,7 +62,7 @@ }} ] }}, - "0_screencapture_uuid": {{ + "1_screencapture_uuid": {{ "name": "screenCapture", "restrictions": [ {{ diff --git a/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_proposal.py b/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_proposal.py index a7ca59387d..70da18fd9c 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_proposal.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/messages/presentation_proposal.py @@ -42,9 +42,7 @@ def __init__( """ super().__init__(_id, **kwargs) self.comment = comment - self.presentation_proposal = ( - presentation_proposal if presentation_proposal else PresentationPreview() - ) + self.presentation_proposal = presentation_proposal class PresentationProposalSchema(AgentMessageSchema): diff --git a/aries_cloudagent/protocols/present_proof/v1_0/routes.py b/aries_cloudagent/protocols/present_proof/v1_0/routes.py index 64f6f020c8..2770139127 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/routes.py @@ -523,7 +523,10 @@ async def presentation_exchange_send_free_request(request: web.BaseRequest): presentation_request_message = PresentationRequest( comment=comment, request_presentations_attach=[ - AttachDecorator.from_indy_dict(indy_proof_request) + AttachDecorator.from_indy_dict( + indy_dict=indy_proof_request, + ident=ATTACH_DECO_IDS[PRESENTATION_REQUEST] + ) ], ) @@ -737,9 +740,6 @@ async def register(app: web.Application): web.post( "/present-proof/send-proposal", presentation_exchange_send_proposal ), - web.post( - "/present-proof/create-request", presentation_exchange_create_request - ), web.post( "/present-proof/send-request", presentation_exchange_send_free_request ), diff --git a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py index 46329acb67..918903410d 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_manager.py @@ -24,7 +24,7 @@ PresPredSpec, ) from ..models.presentation_exchange import V10PresentationExchange -from ..util.indy import indy_proof_request2indy_requested_creds +from ..util.indy import indy_proof_req_preview2indy_requested_creds CONN_ID = "connection_id" @@ -209,8 +209,9 @@ async def test_create_presentation(self): return_value=mock_attach_decorator ) - req_creds = await indy_proof_request2indy_requested_creds( - indy_proof_req, self.holder + req_creds = await indy_proof_req_preview2indy_requested_creds( + indy_proof_req, + holder=self.holder ) (exchange_out, pres_msg) = await self.manager.create_presentation( @@ -227,7 +228,10 @@ async def test_no_matching_creds_for_proof_req(self): self.holder.get_credentials_for_presentation_request_by_referent.return_value = () with self.assertRaises(ValueError): - await indy_proof_request2indy_requested_creds(indy_proof_req, self.holder) + await indy_proof_req_preview2indy_requested_creds( + indy_proof_req, + holder=self.holder + ) self.holder.get_credentials_for_presentation_request_by_referent.return_value = ( { @@ -235,11 +239,93 @@ async def test_no_matching_creds_for_proof_req(self): }, # leave this comma: return a tuple ) - async def test_receive_presentation_active_connection(self): + async def test_receive_presentation(self): self.context.connection_record = async_mock.MagicMock() self.context.connection_record.connection_id = CONN_ID - exchange_dummy = V10PresentationExchange() + exchange_dummy = V10PresentationExchange( + presentation_proposal_dict={ + "presentation_proposal": { + "@type": ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;" + "spec/present-proof/1.0/presentation-preview" + ), + "attributes": [ + { + "name": "favourite", + "cred_def_id": CD_ID, + "value": "potato" + }, + { + "name": "icon", + "cred_def_id": CD_ID, + "value": "cG90YXRv" + } + ], + "predicates": [] + } + }, + presentation_request={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + "0_favourite_uuid": { + "name": "favourite", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + }, + "1_icon_uuid": { + "name": "icon", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + } + }, + presentation={ + "proof": { + "proofs": [], + "requested_proof": { + "revealed_attrs": { + "0_favourite_uuid": { + "sub_proof_index": 0, + "raw": "potato", + "encoded": "12345678901234567890" + }, + "1_icon_uuid": { + "sub_proof_index": 1, + "raw": "cG90YXRv", + "encoded": "12345678901234567890" + } + }, + "self_attested_attrs": {}, + "unrevealed_attrs": {}, + "predicates": { + } + } + }, + "identifiers": [ + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": None, + "timestamp": None + }, + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": None, + "timestamp": None + } + ] + } + ) self.context.message = async_mock.MagicMock() with async_mock.patch.object( @@ -260,6 +346,106 @@ async def test_receive_presentation_active_connection(self): V10PresentationExchange.STATE_PRESENTATION_RECEIVED ) + async def test_receive_presentation_bait_and_switch(self): + self.context.connection_record = async_mock.MagicMock() + self.context.connection_record.connection_id = CONN_ID + + exchange_dummy = V10PresentationExchange( + presentation_proposal_dict={ + "presentation_proposal": { + "@type": ( + "did:sov:BzCbsNYhMrjHiqZDTUASHg;" + "spec/present-proof/1.0/presentation-preview" + ), + "attributes": [ + { + "name": "favourite", + "cred_def_id": CD_ID, + "value": "no potato" + }, + { + "name": "icon", + "cred_def_id": CD_ID, + "value": "cG90YXRv" + } + ], + "predicates": [] + } + }, + presentation_request={ + "name": "proof-request", + "version": "1.0", + "nonce": "1234567890", + "requested_attributes": { + "0_favourite_uuid": { + "name": "favourite", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + }, + "1_icon_uuid": { + "name": "icon", + "restrictions": [ + { + "cred_def_id": CD_ID, + } + ] + } + } + } + ) + self.context.message = async_mock.MagicMock() + self.context.message.indy_proof = async_mock.MagicMock( + return_value={ + "proof": { + "proofs": [], + }, + "requested_proof": { + "revealed_attrs": { + "0_favourite_uuid": { + "sub_proof_index": 0, + "raw": "potato", + "encoded": "12345678901234567890" + }, + "1_icon_uuid": { + "sub_proof_index": 1, + "raw": "cG90YXRv", + "encoded": "23456789012345678901" + } + }, + "self_attested_attrs": {}, + "unrevealed_attrs": {}, + "predicates": { + } + }, + "identifiers": [ + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": None, + "timestamp": None + }, + { + "schema_id": S_ID, + "cred_def_id": CD_ID, + "rev_reg_id": None, + "timestamp": None + } + ] + } + ) + + with async_mock.patch.object( + V10PresentationExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10PresentationExchange, "retrieve_by_tag_filter", autospec=True + ) as retrieve_ex: + retrieve_ex.return_value = exchange_dummy + with self.assertRaises(PresentationManagerError): + await self.manager.receive_presentation() + async def test_receive_presentation_connection_less(self): exchange_dummy = V10PresentationExchange() self.context.message = async_mock.MagicMock() diff --git a/aries_cloudagent/protocols/present_proof/v1_0/util/indy.py b/aries_cloudagent/protocols/present_proof/v1_0/util/indy.py index 1890884444..d1b1d7e2da 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/util/indy.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/util/indy.py @@ -1,82 +1,27 @@ """Utilities for dealing with indy conventions.""" -from collections import namedtuple -from enum import Enum -from typing import Any from .....holder.base import BaseHolder +from ..messages.inner.presentation_preview import PresentationPreview -Relation = namedtuple("Relation", "fortran wql math yes no") - -class Predicate(Enum): - """Enum for predicate types that indy-sdk supports.""" - - LT = Relation( - 'LT', - '$lt', - '<', - lambda x, y: Predicate.to_int(x) < Predicate.to_int(y), - lambda x, y: Predicate.to_int(x) >= Predicate.to_int(y)) - LE = Relation( - 'LE', - '$lte', - '<=', - lambda x, y: Predicate.to_int(x) <= Predicate.to_int(y), - lambda x, y: Predicate.to_int(x) > Predicate.to_int(y)) - GE = Relation( - 'GE', - '$gte', - '>=', - lambda x, y: Predicate.to_int(x) >= Predicate.to_int(y), - lambda x, y: Predicate.to_int(x) < Predicate.to_int(y)) - GT = Relation( - 'GT', - '$gt', - '>', - lambda x, y: Predicate.to_int(x) > Predicate.to_int(y), - lambda x, y: Predicate.to_int(x) <= Predicate.to_int(y)) - - @staticmethod - def get(relation: str) -> 'Predicate': - """Return enum instance corresponding to input relation string.""" - - for pred in Predicate: - if relation.upper() in ( - pred.value.fortran, pred.value.wql.upper(), pred.value.math - ): - return pred - return None - - @staticmethod - def to_int(value: Any) -> int: - """ - Cast a value as its equivalent int for indy predicate argument. - - Raise ValueError for any input but int, stringified int, or boolean. - - Args: - value: value to coerce - """ - - if isinstance(value, (bool, int)): - return int(value) - return int(str(value)) # kick out floats - - -async def indy_proof_request2indy_requested_creds( +async def indy_proof_req_preview2indy_requested_creds( indy_proof_request: dict, + preview: PresentationPreview = None, + *, holder: BaseHolder ): """ Build indy requested-credentials structure. - Given input proof request, use credentials in holder's wallet to - build indy requested credentials structure for input to proof creation. + Given input proof request and presentation preview, use credentials in + holder's wallet to build indy requested credentials structure for input + to proof creation. Args: indy_proof_request: indy proof request + pres_preview: preview from presentation proposal, if applicable holder: holder injected into current context """ @@ -86,30 +31,80 @@ async def indy_proof_request2indy_requested_creds( "requested_predicates": {} } - for category in ("requested_attributes", "requested_predicates"): - for referent in indy_proof_request[category]: - credentials = ( - await holder.get_credentials_for_presentation_request_by_referent( - indy_proof_request, - (referent,), - 0, - 2, - {} - ) + for referent in indy_proof_request["requested_attributes"]: + credentials = ( + await holder.get_credentials_for_presentation_request_by_referent( + presentation_request=indy_proof_request, + referents=(referent,), + start=0, + count=100 + ) + ) + if not credentials: + raise ValueError( + f"Could not automatically construct presentation for " + + f"presentation request {indy_proof_request['name']}" + + f":{indy_proof_request['version']} because referent " + + f"{referent} did not produce any credentials." + ) + + # match returned creds against any preview values + if len(credentials) == 1: + cred_id = credentials[0]["cred_info"]["referent"] + else: + if preview: + for cred in sorted( + credentials, + key=lambda c: c["cred_info"]["referent"] + ): + name = indy_proof_request["requested_attributes"][referent]["name"] + value = cred["cred_info"]["attrs"][name] + if preview.has_attr_spec( + cred_def_id=cred["cred_info"]["cred_def_id"], + name=name, + value=value + ): + cred_id = cred["cred_info"]["referent"] + break + else: + raise ValueError( + f"Could not automatically construct presentation for " + + f"presentation request {indy_proof_request['name']}" + + f":{indy_proof_request['version']} because referent " + + f"{referent} did not produce any credentials matching " + + f"proposed preview." + ) + else: + cred_id = min(cred["cred_info"]["referent"] for cred in credentials) + req_creds["requested_attributes"][referent] = { + "cred_id": cred_id, + "revealed": True # TODO allow specification of unrevealed attrs? + } + + for referent in indy_proof_request["requested_predicates"]: + credentials = ( + await holder.get_credentials_for_presentation_request_by_referent( + presentation_request=indy_proof_request, + referents=(referent,), + start=0, + count=100 + ) + ) + if not credentials: + raise ValueError( + f"Could not automatically construct presentation for " + + f"presentation request {indy_proof_request['name']}" + + f":{indy_proof_request['version']} because predicate " + + f"referent {referent} did not produce any credentials." ) - if len(credentials) != 1: - raise ValueError( - f"Could not automatically construct presentation for " - + f"presentation request {indy_proof_request['name']}" - + f":{indy_proof_request['version']} because referent " - + f"{referent} did not produce exactly one credential " - + f"result. The wallet returned {len(credentials)} " - + f"matching credentials." - ) - req_creds[category][referent] = { - "cred_id": credentials[0]["cred_info"]["referent"], - "revealed": True # TODO allow specification of unrevealed attrs? - } + if len(credentials) == 1: + cred_id = credentials[0]["cred_info"]["referent"] + else: + cred_id = min(cred["cred_info"]["referent"] for cred in credentials) + req_creds["requested_predicates"][referent] = { + "cred_id": cred_id, + "revealed": True # TODO allow specification of unrevealed attrs? + } return req_creds diff --git a/aries_cloudagent/protocols/present_proof/v1_0/util/predicate.py b/aries_cloudagent/protocols/present_proof/v1_0/util/predicate.py new file mode 100644 index 0000000000..7af9bfd3e8 --- /dev/null +++ b/aries_cloudagent/protocols/present_proof/v1_0/util/predicate.py @@ -0,0 +1,63 @@ +"""Utilities for dealing with predicates.""" + +from collections import namedtuple +from enum import Enum +from typing import Any + + +Relation = namedtuple("Relation", "fortran wql math yes no") + + +class Predicate(Enum): + """Enum for predicate types that indy-sdk supports.""" + + LT = Relation( + 'LT', + '$lt', + '<', + lambda x, y: Predicate.to_int(x) < Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) >= Predicate.to_int(y)) + LE = Relation( + 'LE', + '$lte', + '<=', + lambda x, y: Predicate.to_int(x) <= Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) > Predicate.to_int(y)) + GE = Relation( + 'GE', + '$gte', + '>=', + lambda x, y: Predicate.to_int(x) >= Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) < Predicate.to_int(y)) + GT = Relation( + 'GT', + '$gt', + '>', + lambda x, y: Predicate.to_int(x) > Predicate.to_int(y), + lambda x, y: Predicate.to_int(x) <= Predicate.to_int(y)) + + @staticmethod + def get(relation: str) -> 'Predicate': + """Return enum instance corresponding to input relation string.""" + + for pred in Predicate: + if relation.upper() in ( + pred.value.fortran, pred.value.wql.upper(), pred.value.math + ): + return pred + return None + + @staticmethod + def to_int(value: Any) -> int: + """ + Cast a value as its equivalent int for indy predicate argument. + + Raise ValueError for any input but int, stringified int, or boolean. + + Args: + value: value to coerce + """ + + if isinstance(value, (bool, int)): + return int(value) + return int(str(value)) # kick out floats