diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index f5f4e7c6f6..a00942c35b 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -384,6 +384,7 @@ class Meta: def __init__( self, *, + id: str = None, paths: Sequence[str] = None, purpose: str = None, predicate: str = None, @@ -394,6 +395,7 @@ def __init__( self.purpose = purpose self.predicate = predicate self._filter = _filter + self.id = id class DIFFieldSchema(BaseModelSchema): @@ -405,6 +407,7 @@ class Meta: model_class = DIFField unknown = EXCLUDE + id = fields.Str(description="ID", required=False) paths = fields.List( fields.Str(description="Path", required=False), required=False, diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py index 141e5077a4..be33ca5c60 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -38,6 +38,7 @@ ) from ....vc.vc_ld.prove import sign_presentation, create_presentation, derive_credential from ....wallet.base import BaseWallet, DIDInfo +from ....wallet.error import WalletError, WalletNotFoundError from ....wallet.key_type import KeyType from .pres_exch import ( @@ -99,6 +100,7 @@ def __init__( self.proof_type = Ed25519Signature2018.signature_type else: self.proof_type = proof_type + self.is_holder = False async def _get_issue_suite( self, @@ -191,7 +193,7 @@ async def get_sign_key_credential_subject_id( else: reqd_key_type = KeyType.ED25519 for cred in applicable_creds: - if len(cred.subject_ids) > 0: + if cred.subject_ids and len(cred.subject_ids) > 0: if not issuer_id: for cred_subject_id in cred.subject_ids: if not cred_subject_id.startswith("urn:"): @@ -373,13 +375,27 @@ async def filter_constraints( continue applicable = False + is_holder_field_ids = self.field_ids_for_is_holder(constraints) for field in constraints._fields: applicable = await self.filter_by_field(field, credential) - if applicable: + # all fields in the constraint should be satisfied + if not applicable: break + # is_holder with required directive requested for this field + if applicable and field.id and field.id in is_holder_field_ids: + # Missing credentialSubject.id - cannot verify that holder of claim + # is same as subject + if not credential.subject_ids or len(credential.subject_ids) == 0: + applicable = False + break + # Holder of claim is not same as the subject + if not await self.process_constraint_holders( + subject_ids=credential.subject_ids + ): + applicable = False + break if not applicable: continue - if constraints.limit_disclosure == "required": credential_dict = credential.cred_value new_credential_dict = self.reveal_doc( @@ -400,6 +416,32 @@ async def filter_constraints( result.append(credential) return result + def field_ids_for_is_holder(self, constraints: Constraints) -> Sequence[str]: + """Return list of field ids for whose subject holder verification is requested.""" + reqd_field_ids = set() + if not constraints.holders: + reqd_field_ids = [] + return reqd_field_ids + for holder in constraints.holders: + if holder.directive == "required": + reqd_field_ids = set.union(reqd_field_ids, set(holder.field_ids)) + return list(reqd_field_ids) + + async def process_constraint_holders( + self, + subject_ids: Sequence[str], + ) -> bool: + """Check if holder or subject of claim still controls the identifier.""" + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) + try: + for subject_id in subject_ids: + await wallet.get_local_did(subject_id.replace("did:sov:", "")) + self.is_holder = True + return True + except (WalletError, WalletNotFoundError): + return False + def create_vcrecord(self, cred_dict: dict) -> VCRecord: """Return VCRecord from a credential dict.""" proofs = cred_dict.get("proof") or [] @@ -1146,7 +1188,7 @@ async def create_vp( submission_property = PresentationSubmission( id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps ) - if self.check_sign_pres(applicable_creds): + if self.is_holder: ( issuer_id, filtered_creds_list, @@ -1199,15 +1241,6 @@ async def create_vp( else: return vp - def check_sign_pres(self, creds: Sequence[VCRecord]) -> bool: - """Check if applicable creds have CredentialSubject.id set.""" - for cred in creds: - if len(cred.subject_ids) > 0 and not self.check_if_cred_id_derived( - next(iter(cred.subject_ids)) - ): - return True - return False - def check_if_cred_id_derived(self, id: str) -> bool: """Check if credential or credentialSubjet id is derived.""" if id.startswith("urn:bnid:_:c14n"): diff --git a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py index f90912d314..f45a09743d 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_data.py @@ -49,6 +49,153 @@ def create_vcrecord(cred_dict: dict, expanded_types: list): ) +is_holder_pd = PresentationDefinition.deserialize( + { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "submission_requirements": [ + { + "name": "European Union Citizenship Proofs", + "rule": "all", + "from": "A", + } + ], + "input_descriptors": [ + { + "id": "citizenship_input_1", + "group": ["A"], + "schema": [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + "constraints": { + "is_holder": [ + { + "directive": "required", + "field_id": ["1f44d55f-f161-4938-a659-f8026467f126"], + } + ], + "fields": [ + { + "id": "1f44d55f-f161-4938-a659-f8026467f126", + "path": ["$.issuanceDate", "$.vc.issuanceDate"], + "filter": { + "type": "string", + "format": "date", + "maximum": "2014-5-16", + }, + } + ], + }, + } + ], + } +) + +is_holder_pd_multiple_fields_included = PresentationDefinition.deserialize( + { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "submission_requirements": [ + { + "name": "European Union Citizenship Proofs", + "rule": "all", + "from": "A", + } + ], + "input_descriptors": [ + { + "id": "citizenship_input_1", + "group": ["A"], + "schema": [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + "constraints": { + "is_holder": [ + { + "directive": "required", + "field_id": [ + "1f44d55f-f161-4938-a659-f8026467f126", + "1f44d55f-f161-4938-a659-f8026467f127", + ], + } + ], + "fields": [ + { + "id": "1f44d55f-f161-4938-a659-f8026467f126", + "path": ["$.issuanceDate", "$.vc.issuanceDate"], + "filter": { + "type": "string", + "format": "date", + "maximum": "2014-5-16", + }, + }, + { + "id": "1f44d55f-f161-4938-a659-f8026467f127", + "path": ["$.issuanceDate", "$.vc.issuanceDate"], + "filter": { + "type": "string", + "format": "date", + "minimum": "2005-5-16", + }, + }, + ], + }, + } + ], + } +) + +is_holder_pd_multiple_fields_excluded = PresentationDefinition.deserialize( + { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "submission_requirements": [ + { + "name": "European Union Citizenship Proofs", + "rule": "all", + "from": "A", + } + ], + "input_descriptors": [ + { + "id": "citizenship_input_1", + "group": ["A"], + "schema": [ + {"uri": "https://www.w3.org/2018/credentials#VerifiableCredential"}, + {"uri": "https://w3id.org/citizenship#PermanentResidentCard"}, + ], + "constraints": { + "is_holder": [ + { + "directive": "required", + "field_id": ["1f44d55f-f161-4938-a659-f8026467f126"], + } + ], + "fields": [ + { + "id": "1f44d55f-f161-4938-a659-f8026467f126", + "path": ["$.issuanceDate", "$.vc.issuanceDate"], + "filter": { + "type": "string", + "format": "date", + "maximum": "2014-5-16", + }, + }, + { + "id": "1f44d55f-f161-4938-a659-f8026467f127", + "path": ["$.issuanceDate", "$.vc.issuanceDate"], + "filter": { + "type": "string", + "format": "date", + "minimum": "2005-5-16", + }, + }, + ], + }, + } + ], + } +) + creds_with_no_id = [ create_vcrecord( { diff --git a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py index 60d9b1edc4..417b11edaa 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch.py @@ -142,6 +142,17 @@ def test_submission_requirements_from_both_present(self): with self.assertRaises(BaseModelError) as cm: (SubmissionRequirements.deserialize(test_json)).serialize() + def test_submission_requirements_from_both_missing(self): + test_json = """ + { + "name": "Citizenship Information", + "rule": "pick", + "count": 1 + } + """ + with self.assertRaises(BaseModelError) as cm: + (SubmissionRequirements.deserialize(test_json)).serialize() + def test_is_holder(self): test_json = """ { diff --git a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py index c619dc9089..7bf07c49bb 100644 --- a/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/tests/test_pres_exch_handler.py @@ -48,6 +48,9 @@ bbs_signed_cred_no_credsubjectid, bbs_signed_cred_credsubjectid, creds_with_no_id, + is_holder_pd, + is_holder_pd_multiple_fields_excluded, + is_holder_pd_multiple_fields_included, ) @@ -77,6 +80,10 @@ async def setup_tuple(profile): await wallet.create_local_did( method=DIDMethod.SOV, key_type=KeyType.ED25519, did="WgWxqztrNooG92RXvxSTWv" ) + await wallet.create_local_did( + method=DIDMethod.KEY, + key_type=KeyType.BLS12381G2, + ) creds, pds = get_test_data() return creds, pds @@ -2149,10 +2156,6 @@ async def test_create_vp_no_issuer(self, profile, setup_tuple): "merge", async_mock.CoroutineMock(), ) as mock_merge, async_mock.patch.object( - DIFPresExchHandler, - "check_sign_pres", - async_mock.CoroutineMock(), - ) as mock_check_sign_pres, async_mock.patch.object( test_module, "create_presentation", async_mock.CoroutineMock(), @@ -2160,7 +2163,7 @@ async def test_create_vp_no_issuer(self, profile, setup_tuple): mock_make_req.return_value = async_mock.MagicMock() mock_apply_req.return_value = async_mock.MagicMock() mock_merge.return_value = (VC_RECORDS, {}) - mock_check_sign_pres.return_value = True + dif_pres_exch_handler.is_holder = True mock_create_vp.return_value = {"test": "1"} did_info = DIDInfo( did="did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", @@ -2204,10 +2207,6 @@ async def test_create_vp_with_bbs_suite(self, profile, setup_tuple): "merge", async_mock.CoroutineMock(), ) as mock_merge, async_mock.patch.object( - DIFPresExchHandler, - "check_sign_pres", - async_mock.CoroutineMock(), - ) as mock_check_sign_pres, async_mock.patch.object( test_module, "create_presentation", async_mock.CoroutineMock(), @@ -2219,7 +2218,7 @@ async def test_create_vp_with_bbs_suite(self, profile, setup_tuple): mock_make_req.return_value = async_mock.MagicMock() mock_apply_req.return_value = async_mock.MagicMock() mock_merge.return_value = (cred_list, {}) - mock_check_sign_pres.return_value = True + dif_pres_exch_handler.is_holder = True mock_create_vp.return_value = {"test": "1", "@context": ["test"]} mock_sign_vp.return_value = { "test": "1", @@ -2264,10 +2263,6 @@ async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple): "merge", async_mock.CoroutineMock(), ) as mock_merge, async_mock.patch.object( - DIFPresExchHandler, - "check_sign_pres", - async_mock.CoroutineMock(), - ) as mock_check_sign_pres, async_mock.patch.object( test_module, "create_presentation", async_mock.CoroutineMock(), @@ -2279,7 +2274,7 @@ async def test_create_vp_no_issuer_with_bbs_suite(self, profile, setup_tuple): mock_make_req.return_value = async_mock.MagicMock() mock_apply_req.return_value = async_mock.MagicMock() mock_merge.return_value = (cred_list, {}) - mock_check_sign_pres.return_value = True + dif_pres_exch_handler.is_holder = True mock_create_vp.return_value = {"test": "1", "@context": ["test"]} mock_sign_key_cred_subject.return_value = (None, []) did_info = DIDInfo( @@ -3062,3 +3057,95 @@ async def test_multiple_applicable_creds_with_no_auto_and_no_record_ids( pd=tmp_pd, challenge="1f44d55f-f161-4938-a659-f8026467f126", ) + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_is_holder_valid_a(self, profile, setup_tuple): + context = profile.context + context.update_settings({"debug.auto_respond_presentation_request": True}) + dif_pres_exch_handler = DIFPresExchHandler(profile) + cred_list, pd_list = setup_tuple + tmp_vp = await dif_pres_exch_handler.create_vp( + credentials=cred_list, + pd=is_holder_pd, + challenge="1f44d55f-f161-4938-a659-f8026467f126", + ) + assert len(tmp_vp.get("verifiableCredential")) == 6 + assert tmp_vp.get("proof") + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_is_holder_valid_b(self, profile, setup_tuple): + dif_pres_exch_handler = DIFPresExchHandler(profile) + cred_list, pd_list = setup_tuple + tmp_vp = await dif_pres_exch_handler.create_vp( + credentials=cred_list, + pd=is_holder_pd_multiple_fields_included, + challenge="1f44d55f-f161-4938-a659-f8026467f126", + ) + assert len(tmp_vp.get("verifiableCredential")) == 6 + assert tmp_vp.get("proof") + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_is_holder_valid_c(self, profile, setup_tuple): + dif_pres_exch_handler = DIFPresExchHandler(profile) + cred_list, pd_list = setup_tuple + tmp_vp = await dif_pres_exch_handler.create_vp( + credentials=cred_list, + pd=is_holder_pd_multiple_fields_excluded, + challenge="1f44d55f-f161-4938-a659-f8026467f126", + ) + assert len(tmp_vp.get("verifiableCredential")) == 6 + assert tmp_vp.get("proof") + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_is_holder_signature_suite_mismatch(self, profile, setup_tuple): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_list, pd_list = setup_tuple + tmp_vp = await dif_pres_exch_handler.create_vp( + credentials=cred_list, + pd=is_holder_pd, + challenge="1f44d55f-f161-4938-a659-f8026467f126", + ) + assert len(tmp_vp.get("verifiableCredential")) == 6 + assert not tmp_vp.get("proof") + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_is_holder_subject_mismatch(self, profile, setup_tuple): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_list, pd_list = setup_tuple + updated_cred_list = [] + for tmp_cred in deepcopy(cred_list): + tmp_cred.subject_ids = ["did:sov:test"] + updated_cred_list.append(tmp_cred) + tmp_vp = await dif_pres_exch_handler.create_vp( + credentials=updated_cred_list, + pd=is_holder_pd, + challenge="1f44d55f-f161-4938-a659-f8026467f126", + ) + assert len(tmp_vp.get("verifiableCredential")) == 0 + assert not tmp_vp.get("proof") + + @pytest.mark.asyncio + @pytest.mark.ursa_bbs_signatures + async def test_is_holder_missing_subject(self, profile, setup_tuple): + dif_pres_exch_handler = DIFPresExchHandler( + profile, proof_type=BbsBlsSignature2020.signature_type + ) + cred_list, pd_list = setup_tuple + tmp_cred = deepcopy(cred_list[0]) + tmp_cred.subject_ids = None + tmp_vp = await dif_pres_exch_handler.create_vp( + credentials=[tmp_cred], + pd=is_holder_pd, + challenge="1f44d55f-f161-4938-a659-f8026467f126", + ) + assert len(tmp_vp.get("verifiableCredential")) == 0 + assert not tmp_vp.get("proof")